diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ad802d791e8..5ba5885d725 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -31,6 +31,7 @@ libs/common/src/tools @bitwarden/team-tools-dev libs/importer @bitwarden/team-tools-dev libs/tools @bitwarden/team-tools-dev bitwarden_license/bit-web/src/app/tools @bitwarden/team-tools-dev +bitwarden_license/bit-common/src/tools @bitwarden/team-tools-dev ## Localization/Crowdin (Tools team) apps/browser/src/_locales @bitwarden/team-tools-dev @@ -105,7 +106,7 @@ apps/desktop/desktop_native @bitwarden/team-platform-dev ## Key management team files ## apps/desktop/src/key-management @bitwarden/team-key-management-dev -apps/web/src/key-management @bitwarden/team-key-management-dev +apps/web/src/app/key-management @bitwarden/team-key-management-dev apps/browser/src/key-management @bitwarden/team-key-management-dev apps/cli/src/key-management @bitwarden/team-key-management-dev libs/key-management @bitwarden/team-key-management-dev diff --git a/.github/renovate.json b/.github/renovate.json index a1200912dc8..562622807c2 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -81,7 +81,6 @@ "cross-env", "del", "gulp", - "gulp-filter", "gulp-if", "gulp-json-editor", "gulp-replace", diff --git a/.github/workflows/auto-branch-updater.yml b/.github/workflows/auto-branch-updater.yml index 7b69b4405ca..97f020fde7b 100644 --- a/.github/workflows/auto-branch-updater.yml +++ b/.github/workflows/auto-branch-updater.yml @@ -1,4 +1,3 @@ ---- name: Auto Update Branch on: diff --git a/.github/workflows/automatic-issue-responses.yml b/.github/workflows/automatic-issue-responses.yml index 289b8bd662b..e38f8103cb5 100644 --- a/.github/workflows/automatic-issue-responses.yml +++ b/.github/workflows/automatic-issue-responses.yml @@ -1,4 +1,3 @@ ---- name: Automatic issue responses on: issues: diff --git a/.github/workflows/automatic-pull-request-responses.yml b/.github/workflows/automatic-pull-request-responses.yml index 16e1a46c052..6bd069d21ac 100644 --- a/.github/workflows/automatic-pull-request-responses.yml +++ b/.github/workflows/automatic-pull-request-responses.yml @@ -1,4 +1,3 @@ ---- name: Automatic pull request responses on: pull_request: diff --git a/.github/workflows/brew-bump-desktop.yml b/.github/workflows/brew-bump-desktop.yml index 31239aa9b29..1b3c99128bf 100644 --- a/.github/workflows/brew-bump-desktop.yml +++ b/.github/workflows/brew-bump-desktop.yml @@ -1,4 +1,3 @@ ---- name: Bump Desktop Cask on: diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index 0e59610bb26..20a36dc5b23 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -1,4 +1,3 @@ ---- name: Build Browser on: diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 26d30fb5b90..f88c4767407 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -1,4 +1,3 @@ ---- name: Build CLI on: @@ -289,7 +288,7 @@ jobs: name: bitwarden-cli.${{ env._PACKAGE_VERSION }}.nupkg path: apps/cli/dist/chocolatey/bitwarden-cli.${{ env._PACKAGE_VERSION }}.nupkg if-no-files-found: error - + - name: Zip NPM Build Artifact run: Get-ChildItem -Path .\build | Compress-Archive -DestinationPath .\bitwarden-cli-${{ env._PACKAGE_VERSION }}-npm-build.zip diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 7f01d1fa66c..2c89e0d156f 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -1,4 +1,3 @@ ---- name: Build Desktop on: diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index 21b976f9120..ec09f25ac19 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -1,4 +1,3 @@ ---- name: Build Web on: diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index 0352c0ca2ea..d6f63d48032 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -1,4 +1,3 @@ ---- name: Chromatic on: diff --git a/.github/workflows/crowdin-pull.yml b/.github/workflows/crowdin-pull.yml index 974e30c28e6..dfcd3294b01 100644 --- a/.github/workflows/crowdin-pull.yml +++ b/.github/workflows/crowdin-pull.yml @@ -1,4 +1,3 @@ ---- name: Crowdin Sync on: diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml index d0f791aa000..5cc4eb90861 100644 --- a/.github/workflows/deploy-web.yml +++ b/.github/workflows/deploy-web.yml @@ -1,4 +1,3 @@ ---- name: Deploy Web Vault run-name: Deploy Web Vault to ${{ inputs.environment }} from ${{ inputs.branch-or-tag }} diff --git a/.github/workflows/enforce-labels.yml b/.github/workflows/enforce-labels.yml index a98c4ae1bea..40ddfe7739f 100644 --- a/.github/workflows/enforce-labels.yml +++ b/.github/workflows/enforce-labels.yml @@ -1,4 +1,3 @@ ---- name: Enforce PR labels on: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b06365fa975..db7fef83fb8 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,4 +1,3 @@ ---- name: Lint on: diff --git a/.github/workflows/locales-lint.yml b/.github/workflows/locales-lint.yml index a701d63d10f..ef944526111 100644 --- a/.github/workflows/locales-lint.yml +++ b/.github/workflows/locales-lint.yml @@ -1,4 +1,3 @@ ---- name: Locales lint for Crowdin on: diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml index a54d70b8d39..c9a4e841ea8 100644 --- a/.github/workflows/publish-cli.yml +++ b/.github/workflows/publish-cli.yml @@ -1,4 +1,3 @@ ---- name: Publish CLI run-name: Publish CLI ${{ inputs.publish_type }} diff --git a/.github/workflows/publish-desktop.yml b/.github/workflows/publish-desktop.yml index 8103dd7b7cc..c46a7a27601 100644 --- a/.github/workflows/publish-desktop.yml +++ b/.github/workflows/publish-desktop.yml @@ -1,4 +1,3 @@ ---- name: Publish Desktop run-name: Publish Desktop ${{ inputs.publish_type }} @@ -125,7 +124,7 @@ jobs: secrets: "aws-electron-access-id, aws-electron-access-key, aws-electron-bucket-name" - + - name: Create artifacts directory run: mkdir -p apps/desktop/artifacts diff --git a/.github/workflows/publish-web.yml b/.github/workflows/publish-web.yml index e26d536aada..7e0e8737344 100644 --- a/.github/workflows/publish-web.yml +++ b/.github/workflows/publish-web.yml @@ -1,4 +1,3 @@ ---- name: Publish Web run-name: Publish Web ${{ inputs.publish_type }} diff --git a/.github/workflows/release-browser.yml b/.github/workflows/release-browser.yml index 860acce2b7d..aed9ab293e8 100644 --- a/.github/workflows/release-browser.yml +++ b/.github/workflows/release-browser.yml @@ -1,4 +1,3 @@ ---- name: Release Browser run-name: Release Browser ${{ inputs.release_type }} diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 60e5bc025bb..8660744f944 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -1,4 +1,3 @@ ---- name: Release CLI run-name: Release CLI ${{ inputs.release_type }} diff --git a/.github/workflows/release-desktop-beta.yml b/.github/workflows/release-desktop-beta.yml index 19b3ac2d229..7518daf0b16 100644 --- a/.github/workflows/release-desktop-beta.yml +++ b/.github/workflows/release-desktop-beta.yml @@ -1,4 +1,3 @@ ---- name: Release Desktop Beta on: diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index 7c5228478af..b0ddc4b804d 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -1,4 +1,3 @@ ---- name: Release Desktop run-name: Release Desktop ${{ inputs.release_type }} diff --git a/.github/workflows/release-web.yml b/.github/workflows/release-web.yml index 7f18242909f..e3462a98fb6 100644 --- a/.github/workflows/release-web.yml +++ b/.github/workflows/release-web.yml @@ -1,4 +1,3 @@ ---- name: Release Web run-name: Release Web ${{ inputs.release_type }} diff --git a/.github/workflows/retrieve-current-desktop-rollout.yml b/.github/workflows/retrieve-current-desktop-rollout.yml index 45a2bf5ce42..2ab3072f566 100644 --- a/.github/workflows/retrieve-current-desktop-rollout.yml +++ b/.github/workflows/retrieve-current-desktop-rollout.yml @@ -1,4 +1,3 @@ ---- name: Retrieve Current Desktop Rollout on: diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index 5afd133afd4..143d049bd63 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -1,4 +1,3 @@ ---- name: Scan on: diff --git a/.github/workflows/staged-rollout-desktop.yml b/.github/workflows/staged-rollout-desktop.yml index 1f751507640..91250a443f2 100644 --- a/.github/workflows/staged-rollout-desktop.yml +++ b/.github/workflows/staged-rollout-desktop.yml @@ -1,4 +1,3 @@ ---- name: Staged Rollout Desktop on: diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml index 4d3085ce003..6caa7b99331 100644 --- a/.github/workflows/stale-bot.yml +++ b/.github/workflows/stale-bot.yml @@ -1,4 +1,3 @@ ---- name: 'Close stale issues and PRs' on: workflow_dispatch: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0f3cfd56c94..4ea08a24373 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,3 @@ ---- name: Testing on: diff --git a/.github/workflows/version-auto-bump.yml b/.github/workflows/version-auto-bump.yml index f10abee300d..cc6feeba026 100644 --- a/.github/workflows/version-auto-bump.yml +++ b/.github/workflows/version-auto-bump.yml @@ -1,4 +1,3 @@ ---- name: Auto Bump Desktop Version on: diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml index 457cdd08fe0..7f6dfef79cf 100644 --- a/.github/workflows/version-bump.yml +++ b/.github/workflows/version-bump.yml @@ -1,4 +1,3 @@ ---- name: Version Bump on: diff --git a/apps/browser/gulpfile.js b/apps/browser/gulpfile.js index 5212fc58659..573f86efc18 100644 --- a/apps/browser/gulpfile.js +++ b/apps/browser/gulpfile.js @@ -14,23 +14,9 @@ const betaBuild = process.env.BETA_BUILD === "1"; const paths = { build: "./build/", dist: "./dist/", - node_modules: "./node_modules/", - popupDir: "./src/popup/", - cssDir: "./src/popup/css/", safari: "./src/safari/", }; -const filters = { - fonts: [ - "!build/popup/fonts/*", - "build/popup/fonts/Open_Sans*.woff", - "build/popup/fonts/bwi-font.woff2", - "build/popup/fonts/bwi-font.woff", - "build/popup/fonts/bwi-font.ttf", - ], - safari: ["!build/safari/**/*"], -}; - /** * Converts a number to a tuple containing two Uint16's * @param num {number} This number is expected to be a integer style number with no decimals @@ -64,11 +50,9 @@ function distFileName(browserName, ext) { async function dist(browserName, manifest) { const { default: zip } = await import("gulp-zip"); - const { default: filter } = await import("gulp-filter"); return gulp .src(paths.build + "**/*") - .pipe(filter(["**"].concat(filters.fonts).concat(filters.safari))) .pipe(gulpif("popup/index.html", replace("__BROWSER__", "browser_" + browserName))) .pipe(gulpif("manifest.json", jeditor(manifest))) .pipe(zip(distFileName(browserName, "zip"))) @@ -192,8 +176,6 @@ function distSafariApp(cb, subBuildPath) { return new Promise((resolve) => proc.on("close", resolve)); }) .then(async () => { - const { default: filter } = await import("gulp-filter"); - const libs = fs .readdirSync(builtAppexFrameworkPath) .filter((p) => p.endsWith(".dylib")) @@ -237,13 +219,10 @@ function safariCopyAssets(source, dest) { } async function safariCopyBuild(source, dest) { - const { default: filter } = await import("gulp-filter"); - return new Promise((resolve, reject) => { gulp .src(source) .on("error", reject) - .pipe(filter(["**"].concat(filters.fonts))) .pipe(gulpif("popup/index.html", replace("__BROWSER__", "browser_safari"))) .pipe( gulpif( diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 592ca6238ea..415ade9820b 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "إنشاء حساب" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Set a strong password" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Join organization" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organization by setting a master password." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "نسخ كلمة المرور" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "نسخ الملاحظة" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Copy notes" }, + "fill": { + "message": "Fill", + "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": "التعبئة التلقائية" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "توليد كلمة مرور" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "إعادة توليد كلمة المرور" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Launch website" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "الموقع الإلكتروني" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Log in" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Restart registration" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "رابط الخادم" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "رابط خادم API" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "سجل كلمة المرور" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "رجوع" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Clear history" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "إزالة" @@ -2449,8 +2511,8 @@ "message": "Optionally require a password for users to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Your organization requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "التحقق مطلوب", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "إنشاء اسم المستخدم" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "نوع اسم المستخدم" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "إنشاء بريد إلكتروني مستعار مع خدمة إعادة توجيه خارجية." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ error: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index f9f2cb10b56..c041c6c1923 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -11,7 +11,7 @@ "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { - "message": "Güvənli anbarınıza müraciət etmək üçün giriş edin və ya yeni bir hesab yaradın." + "message": "Güvənli seyfinizə müraciət etmək üçün giriş edin və ya yeni bir hesab yaradın." }, "inviteAccepted": { "message": "Dəvət qəbul edildi" @@ -19,6 +19,18 @@ "createAccount": { "message": "Hesab yarat" }, + "newToBitwarden": { + "message": "Bitwarden-də yenisiniz?" + }, + "logInWithPasskey": { + "message": "Keçid açarı ilə giriş et" + }, + "useSingleSignOn": { + "message": "Tək daxil olma üsulunu istifadə et" + }, + "welcomeBack": { + "message": "Yenidən xoş gəlmisiniz" + }, "setAStrongPassword": { "message": "Güclü bir parol təyin et" }, @@ -44,7 +56,7 @@ "message": "Ana parol" }, "masterPassDesc": { - "message": "Ana parol, anbarınıza müraciət etmək üçün istifadə edəcəyiniz paroldur. Ana parolu yadda saxlamaq çox vacibdir. Unutsanız, parolu geri qaytarmağın heç bir yolu yoxdur." + "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." }, "masterPassHintDesc": { "message": "Ana parol məsləhəti, unutduğunuz parolunuzu xatırlamağınıza kömək edir." @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Təşkilata qoşul" }, + "joinOrganizationName": { + "message": "$ORGANIZATIONNAME$ - qoşul", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Bu ana parol təyin edərək bu təşkilata qoşulmağı tamamlayın." }, @@ -78,13 +99,13 @@ "message": "Vərəq" }, "vault": { - "message": "Anbar" + "message": "Seyf" }, "myVault": { - "message": "Anbarım" + "message": "Seyfim" }, "allVaults": { - "message": "Bütün anbarlar" + "message": "Bütün seyflər" }, "tools": { "message": "Alətlər" @@ -98,6 +119,9 @@ "copyPassword": { "message": "Parolu kopyala" }, + "copyPassphrase": { + "message": "Keçid ifadəsini kopyala" + }, "copyNote": { "message": "Notu kopyala" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Notları kopyala" }, + "fill": { + "message": "Doldur", + "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": "Avto-doldurma" }, @@ -180,10 +208,10 @@ "message": "Kimlik əlavə et" }, "unlockVaultMenu": { - "message": "Anbarınızın kilidini açın" + "message": "Seyfinizin kilidini açın" }, "loginToVaultMenu": { - "message": "Anbarınıza giriş edin" + "message": "Seyfinizə giriş edin" }, "autoFillInfo": { "message": "Hazırkı brauzer vərəqi üçün avto-doldurulacaq giriş məlumatları yoxdur." @@ -341,7 +369,7 @@ "message": "Heç bir qovluq əlavə edilmədi" }, "createFoldersToOrganize": { - "message": "Anbar elementlərinizi təşkil etmək üçün qovluq yaradın" + "message": "Seyf elementlərinizi təşkil etmək üçün qovluq yaradın" }, "deleteFolderPermanently": { "message": "Bu qovluğu həmişəlik silmək istədiyinizə əminsiniz?" @@ -371,7 +399,7 @@ "message": "Sinxr" }, "syncVaultNow": { - "message": "Anbarı indi sinxronlaşdır" + "message": "Seyfi indi sinxronlaşdır" }, "lastSync": { "message": "Son sinxr:" @@ -398,6 +426,9 @@ "generatePassword": { "message": "Parol yarat" }, + "generatePassphrase": { + "message": "Keçid ifadələri yarat" + }, "regeneratePassword": { "message": "Parolu yenidən yarat" }, @@ -494,7 +525,7 @@ "description": "Indicates that a policy limits the credential generator screen." }, "searchVault": { - "message": "Anbarda axtar" + "message": "Seyfdə axtar" }, "edit": { "message": "Düzəliş et" @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Veb saytı başlat" }, + "launchWebsiteName": { + "message": "$ITEMNAME$ veb saytını başlat", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Veb sayt" }, @@ -575,7 +615,7 @@ "message": "Kilid açma seçimləri" }, "unlockMethodNeededToChangeTimeoutActionDesc": { - "message": "Anbar vaxt bitməsi əməliyyatınızı dəyişdirmək üçün bir kilid açma üsulu qurun." + "message": "Seyf vaxt bitmə əməliyyatınızı dəyişdirmək üçün bir kilid açma üsulu qurun." }, "unlockMethodNeeded": { "message": "Ayarlarda bir kilid açma üsulu qurun" @@ -584,7 +624,7 @@ "message": "Seans vaxt bitməsi" }, "vaultTimeoutHeader": { - "message": "Anbar vaxtının bitməsi" + "message": "Seyf vaxtının bitməsi" }, "otherOptions": { "message": "Digər seçimlər" @@ -602,10 +642,10 @@ "message": "Kimliyi doğrula" }, "yourVaultIsLocked": { - "message": "Anbarınız kilidlənib. Davam etmək üçün kimliyinizi doğrulayın." + "message": "Seyfiniz kilidlənib. Davam etmək üçün kimliyinizi doğrulayın." }, "yourVaultIsLockedV2": { - "message": "Anbarınız kilidlənib" + "message": "Seyfiniz kilidlənib" }, "yourAccountIsLocked": { "message": "Hesabınız kilidlənib" @@ -633,7 +673,7 @@ "message": "Yararsız ana parol" }, "vaultTimeout": { - "message": "Anbara müraciət bitəcək" + "message": "Seyf vaxtının bitməsi" }, "vaultTimeout1": { "message": "Vaxt bitmə" @@ -805,6 +845,9 @@ "logIn": { "message": "Giriş et" }, + "logInToBitwarden": { + "message": "Bitwarden-ə giriş edin" + }, "restartRegistration": { "message": "Qeydiyyatı yenidən başlat" }, @@ -836,7 +879,7 @@ "message": "Qovluq əlavə edildi" }, "twoStepLoginConfirmation": { - "message": "İki addımlı giriş, güvənlik açarı, kimlik doğrulayıcı tətbiq, SMS, telefon zəngi və ya e-poçt kimi digər cihazlarla girişinizi doğrulamanızı tələb edərək hesabınızı daha da güvənli edir. İki addımlı giriş, bitwarden.com veb anbarında qurula bilər. Veb saytı indi ziyarət etmək istəyirsiniz?" + "message": "İki addımlı giriş, güvənlik açarı, kimlik doğrulayıcı tətbiq, SMS, telefon zəngi və ya e-poçt kimi digər cihazlarla girişinizi doğrulamanızı tələb edərək hesabınızı daha da güvənli edir. İki addımlı giriş, bitwarden.com veb seyfində qurula bilər. Veb saytı indi ziyarət etmək istəyirsiniz?" }, "twoStepLoginConfirmationContent": { "message": "Bitwarden veb tətbiqində iki addımlı girişi quraraq hesabınızı daha güvənli edin." @@ -929,16 +972,16 @@ "message": "Giriş əlavə etmək üçün soruş" }, "vaultSaveOptionsTitle": { - "message": "Anbar seçimlərində saxla" + "message": "Seyf seçimlərində saxla" }, "addLoginNotificationDesc": { - "message": "Anbarınızda tapılmayan bir elementin əlavə edilməsi soruşulsun." + "message": "Seyfinizdə tapılmayan elementin əlavə edilməsi soruşulsun." }, "addLoginNotificationDescAlt": { - "message": "Anbarınızda tapılmayan bir elementin əlavə edilməsi soruşulsun. Giriş etmiş bütün hesablara aiddir." + "message": "Seyfinizdə tapılmayan elementin əlavə edilməsi soruşulsun. Giriş etmiş bütün hesablara aiddir." }, "showCardsInVaultView": { - "message": "Kartları, Anbar görünüşündə Avto-doldurma təklifləri olaraq göstər" + "message": "Kartları, Seyf görünüşündə Avto-doldurma təklifləri olaraq göstər" }, "showCardsCurrentTab": { "message": "Kartları Vərəq səhifəsində göstər" @@ -947,7 +990,7 @@ "message": "Asan avto-doldurma üçün Vərəq səhifəsində kart elementlərini sadalayın." }, "showIdentitiesInVaultView": { - "message": "Kimlikləri, Anbar görünüşündə Avto-doldurma təklifləri olaraq göstər" + "message": "Kimlikləri, Seyf görünüşündə Avto-doldurma təklifləri olaraq göstər" }, "showIdentitiesCurrentTab": { "message": "Vərəq səhifəsində kimlikləri göstər" @@ -982,7 +1025,7 @@ "message": "Keçid açarlarını saxlamağı və istifadə etməyi soruş" }, "usePasskeysDesc": { - "message": "Yeni keçid açarlarını saxlamağı və ya anbarınızda saxlanılan keçid açarları ilə giriş etmək soruşulsun. Giriş etmiş bütün hesablara aiddir." + "message": "Yeni keçid açarlarını saxlamağı və ya seyfinizdə saxlanılan keçid açarları ilə giriş etmək soruşulsun. Giriş etmiş bütün hesablara aiddir." }, "notificationChangeDesc": { "message": "Bu parolu \"Bitwarden\"də güncəlləmək istəyirsiniz?" @@ -991,7 +1034,7 @@ "message": "Güncəllə" }, "notificationUnlockDesc": { - "message": "Avto-doldurma tələblərini tamamlamaq üçün Bitwarden anbarınızın kilidini açın." + "message": "Avto-doldurma tələblərini tamamlamaq üçün Bitwarden seyfinizin kilidini açın." }, "notificationUnlock": { "message": "Kilidi aç" @@ -1013,7 +1056,7 @@ "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { - "message": "Avto-doldurma kimi əməliyyatları icra edərkən giriş etmə prosesi üçün URI uyuşma aşkarlamasının idarə edliəcəyi ilkin yolu seçin." + "message": "Avto-doldurma kimi əməliyyatları icra edərkən giriş etmə prosesi üçün URI uyuşma aşkarlamasının idarə ediləcəyi ilkin yolu seçin." }, "theme": { "message": "Tema" @@ -1040,7 +1083,7 @@ "message": "Buradan xaricə köçür" }, "exportVault": { - "message": "Anbarı xaricə köçür" + "message": "Seyfi xaricə köçür" }, "fileFormat": { "message": "Fayl formatı" @@ -1074,7 +1117,7 @@ "description": "WARNING (should stay in capitalized letters if the language permits)" }, "confirmVaultExport": { - "message": "Anbarın xaricə köçürülməsini təsdiqlə" + "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." @@ -1086,13 +1129,13 @@ "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": "Anbar datanızı xaricə köçürmək üçün ana parolunuzu daxil edin." + "message": "Seyf datanızı xaricə köçürmək üçün ana parolunuzu daxil edin." }, "shared": { "message": "Paylaşılan" }, "bitwardenForBusinessPageDesc": { - "message": "Bitwarden for Business, bir təşkilat hesabı istifadə edərək anbar elementlərinizi başqaları ilə paylaşmağınıza imkan verir. Daha ətraflı bitwarden.com veb saytında öyrənə bilərsiniz." + "message": "Bitwarden for Business, bir təşkilatı istifadə edərək seyf elementlərinizi başqaları ilə paylaşmağınıza imkan verir. Daha ətraflı məlumat üçün bitwarden.com saytını ziyarət edin." }, "moveToOrganization": { "message": "Təşkilata daşı" @@ -1165,7 +1208,7 @@ "message": "Özəllik əlçatmazdır" }, "encryptionKeyMigrationRequired": { - "message": "Şifrələmə açarının daşınması tələb olunur. Şifrələmə açarınızı güncəlləmək üçün lütfən veb anbar üzərindən giriş edin." + "message": "Şifrələmə açarının daşınması tələb olunur. Şifrələmə açarınızı güncəlləmək üçün lütfən veb seyfinizə giriş edin." }, "premiumMembership": { "message": "Premium üzvlük" @@ -1174,7 +1217,7 @@ "message": "Üzvlüyü idarə edin" }, "premiumManageAlert": { - "message": "Üzvlüyünüzü bitwarden.com veb anbarında idarə edə bilərsiniz. İndi saytı ziyarət etmək istəyirsiniz?" + "message": "Üzvlüyünüzü bitwarden.com veb seyfində idarə edə bilərsiniz. İndi saytı ziyarət etmək istəyirsiniz?" }, "premiumRefresh": { "message": "Üzvlüyü təzələ" @@ -1186,7 +1229,7 @@ "message": "Premium üzvlük üçün qeydiyyatdan keçin və bunları əldə edin:" }, "ppremiumSignUpStorage": { - "message": "Fayl qoşmaları üçün 1 GB şifrələnmiş saxlama sahəsi" + "message": "Fayl qoşmaları üçün 1 GB şifrələnmiş anbar sahəsi" }, "premiumSignUpEmergency": { "message": "Fövqəladə hal müraciəti" @@ -1195,10 +1238,10 @@ "message": "YubiKey və Duo kimi mülkiyyətçi iki addımlı giriş seçimləri." }, "ppremiumSignUpReports": { - "message": "Anbarınızın güvənliyini təmin etmək üçün parol gigiyenası, hesab sağlamlığı və data pozuntusu hesabatları." + "message": "Seyfinizi güvəndə saxlamaq üçün parol gigiyenası, hesab sağlamlığı və data pozuntusu hesabatları." }, "ppremiumSignUpTotp": { - "message": "Anbarınızdakı hesablar üçün TOTP doğrulama kodu (2FA) yaradıcısı." + "message": "Seyfinizdəki girişlər üçün TOTP doğrulama kodu (2FA) yaradıcısı." }, "ppremiumSignUpSupport": { "message": "Prioritet müştəri dəstəyi." @@ -1210,7 +1253,7 @@ "message": "Premium satın al" }, "premiumPurchaseAlert": { - "message": "Premium üzvlüyü bitwarden.com veb anbarında satın ala bilərsiniz. İndi saytı ziyarət etmək istəyirsiniz?" + "message": "Premium üzvlüyü bitwarden.com veb seyfində satın ala bilərsiniz. İndi saytı ziyarət etmək istəyirsiniz?" }, "premiumPurchaseAlertV2": { "message": "Bitwarden veb tətbiqindəki hesab ayarlarınızda Premium satın ala bilərsiniz." @@ -1380,11 +1423,15 @@ "baseUrl": { "message": "Server URL-si" }, + "selfHostBaseUrl": { + "message": "Self-host server URL-si", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "API server URL-si" }, "webVaultUrl": { - "message": "Veb anbar server URL-si" + "message": "Veb seyf server URL-si" }, "identityUrl": { "message": "Kimlik server URL-si" @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Avto-doldurma təkliflərini form xanalarında göstər" }, + "showInlineMenuIdentitiesLabel": { + "message": "Kimlikləri təklif kimi göstər" + }, + "showInlineMenuCardsLabel": { + "message": "Kartları təklif kimi göstər" + }, "showInlineMenuOnIconSelectionLabel": { "message": "İkon seçildikdə təklifləri göstər" }, @@ -1482,10 +1535,10 @@ "message": "Səhifə yüklənəndə avto-doldurma" }, "commandOpenPopup": { - "message": "Anbarı açılan pəncərədə aç" + "message": "Seyfi pəncərədə aç" }, "commandOpenSidebar": { - "message": "Anbarı yan çubuqda aç" + "message": "Seyfi yan çubuqda aç" }, "commandAutofillLoginDesc": { "message": "Hazırkı veb sayt üçün son istifadə edilən girişi avto-doldur" @@ -1500,7 +1553,7 @@ "message": "Təsadüfi yeni bir parol yarat və lövhəyə kopyala" }, "commandLockVaultDesc": { - "message": "Anbarı kilidlə" + "message": "Seyfi kilidlə" }, "customFields": { "message": "Özəl xanalar" @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Parol tarixçəsi" }, + "generatorHistory": { + "message": "Yaradıcı tarixçəsi" + }, + "clearGeneratorHistoryTitle": { + "message": "Yaradıcı tarixçəsini təmizlə" + }, + "cleargGeneratorHistoryDescription": { + "message": "Davam etsəniz, yaradıcı tarixçəsindəki bütün girişlər həmişəlik silinəcək. Davam etmək istədiyinizə əminsiniz?" + }, "back": { "message": "Geri" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Tarixçəni təmizlə" }, - "noPasswordsToShow": { - "message": "Göstəriləcək parol yoxdur." + "nothingToShow": { + "message": "Göstəriləcək heç nə yoxdur" }, - "noRecentlyGeneratedPassword": { - "message": "Təzəlikcə parol yaratmamısınız." + "nothingGeneratedRecently": { + "message": "Təzəlikcə heç nə yaratmamısınız" }, "remove": { "message": "Çıxart" @@ -1882,7 +1944,7 @@ "description": "ex. Date this password was updated" }, "neverLockWarning": { - "message": "\"Heç vaxt\" seçimini istifadə etmək istədiyinizə əminsiniz? Kilid seçimini \"Heç vaxt\" olaraq ayarlasanız, anbarınızın şifrələmə açarı cihazınızda saxlanılacaq. Bu seçimi istifadə etsəniz, cihazınızı daha yaxşı mühafizə etməlisiniz." + "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." }, "noOrganizationsList": { "message": "Heç bir təşkilata aid deyilsiniz. Təşkilatlar, elementlərinizi digər istifadəçilərlə güvənli şəkildə paylaşmağınızı təmin edir." @@ -1996,7 +2058,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'" }, "vaultTimeoutAction": { - "message": "Anbar vaxtının bitmə əməliyyatı" + "message": "Seyf vaxtının bitmə əməliyyatı" }, "vaultTimeoutAction1": { "message": "Vaxt bitmə əməliyyatı" @@ -2031,7 +2093,7 @@ "message": "Artıq hesabınız var?" }, "vaultTimeoutLogOutConfirmation": { - "message": "Çıxış etdikdə anbarınıza 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ə 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?" }, "vaultTimeoutLogOutConfirmationTitle": { "message": "Vaxt bitmə əməliyyat təsdiqi" @@ -2199,7 +2261,7 @@ "message": "Biometrik açarı uyuşmazlığı" }, "nativeMessagingWrongUserKeyDesc": { - "message": "Biometrik kilidini açma uğursuz oldu. Biometrik sirr açarı anbarın kilidini aça bilmədi. Lütfən biometriki yenidən qurmağa çalışın." + "message": "Biometrik ilə kilid açma uğursuz oldu. Biometrik sirr açarı seyfin kilidini aça bilmədi. Lütfən biometriki yenidən qurmağa çalışın." }, "biometricsNotEnabledTitle": { "message": "Biometriklər qurulmayıb" @@ -2244,13 +2306,13 @@ "message": "Bu əməliyyat yan çubuqda icra edilə bilməz. Lütfən açılan pəncərədə yenidən sınayın." }, "personalOwnershipSubmitError": { - "message": "Müəssisə Siyasətinə görə, elementləri şəxsi anbarınızda saxlamağınız məhdudlaşdırılıb. Sahiblik seçimini təşkilat olaraq dəyişdirin və mövcud kolleksiyalar arasından seçim edin." + "message": "Müəssisə Siyasətinə görə, elementləri şəxsi seyfinizdə saxlamağınız məhdudlaşdırılıb. Sahiblik seçimini təşkilat olaraq dəyişdirin və mövcud kolleksiyalar arasından seçim edin." }, "personalOwnershipPolicyInEffect": { "message": "Bir təşkilat siyasəti, sahiblik seçimlərinizə təsir edir." }, "personalOwnershipPolicyInEffectImports": { - "message": "Bir təşkilat siyasəti, elementlərin fərdi anbarınıza köçürülməsini əngəllədi." + "message": "Bir təşkilat siyasəti, elementlərin fərdi seyfinizə köçürülməsini əngəllədi." }, "domainsTitle": { "message": "Domenlər", @@ -2449,8 +2511,8 @@ "message": "İstəyinizə görə istifadəçilərdən bu \"Send\"ə müraciət edərkə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." }, - "sendPasswordDescV2": { - "message": "Send-ə baxmaq üçün bu parol tələb edilsin.", + "sendPasswordDescV3": { + "message": "Alıcıların bu \"Send\"ə müraciət etmə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." }, "sendNotesDesc": { @@ -2609,7 +2671,7 @@ "message": "E-poçt doğrulandı" }, "emailVerificationRequiredDesc": { - "message": "Bu özəlliyi istifadə etmək üçün e-poçtunuzu doğrulamalısınız. E-poçtunuzu veb anbarında doğrulaya bilərsiniz." + "message": "Bu özəlliyi istifadə etmək üçün e-poçtunuzu doğrulamalısınız. E-poçtunuzu veb seyfdə doğrulaya bilərsiniz." }, "updatedMasterPassword": { "message": "Güncəllənmiş ana parol" @@ -2618,13 +2680,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. Anbara müraciət üçün 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 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." }, "updateWeakMasterPasswordWarning": { - "message": "Ana parolunuz təşkilatınızdakı siyasətlərdən birinə və ya bir neçəsinə uyğun gəlmir. Anbara müraciət üçün ana parolunuzu indi güncəlləməlisiniz. Davam etsəniz, hazırkı seansdan çıxış edəcəksiniz və təkrar giriş etməli olacaqsınız. Digər cihazlardakı aktiv seanslar bir saata qədər aktiv qalmağa davam edə bilər." + "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." }, "tdeDisabledMasterPasswordRequired": { - "message": "Təşkilatınız, güvənli cihaz şifrələməsini sıradan çıxartdı. Anbarınıza müraciət etmək üçün lütfən ana parol təyin edin." + "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." }, "resetPasswordPolicyAutoEnroll": { "message": "Avtomatik yazılma" @@ -2647,6 +2709,15 @@ "message": "Təşkilatınız bir ana parol ayarlamağı tələb edir.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "/$TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Doğrulama tələb olunur", "description": "Default title for the user verification dialog." @@ -2661,7 +2732,7 @@ "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, anbarınızın vaxt bitişinə təsir edir. Anbar vaxt bitişi üçün icazə verilən maksimum vaxt $HOURS$ saat $MINUTES$ dəqiqədir", + "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", @@ -2700,7 +2771,7 @@ } }, "vaultTimeoutPolicyWithActionInEffect": { - "message": "Təşkilatınızın siyasətləri, anbarınızın vaxt bitişinə təsir edir. Anbar vaxt bitişi üçün icazə verilən maksimum vaxt $HOURS$ saat $MINUTES$ dəqiqədir. Anbar vaxt bitişi əməliyyatı $ACTION$ olaraq ayarlandı.", + "message": "Təşkilatınızın siyasətləri, seyfinizin bitmə vaxtına təsir edir. İcazə verilən maksimum seyf bitmə vaxtı $HOURS$ saat $MINUTES$ dəqiqədir. Seyf vaxt bitmə əməliyyatı $ACTION$ olaraq ayarlandı.", "placeholders": { "hours": { "content": "$1", @@ -2717,7 +2788,7 @@ } }, "vaultTimeoutActionPolicyInEffect": { - "message": "Təşkilatınızın siyasətləri, anbar vaxt bitişi əməliyyatınızı $ACTION$ olaraq ayarladı.", + "message": "Təşkilatınızın siyasətləri, seyfinizin vaxt bitmə əməliyyatını $ACTION$ olaraq ayarladı.", "placeholders": { "action": { "content": "$1", @@ -2726,13 +2797,13 @@ } }, "vaultTimeoutTooLarge": { - "message": "Anbar vaxt bitişi, təşkilatınız tərəfindən ayarlanan məhdudiyyətləri aşır." + "message": "Seyfin bitmə vaxtı, təşkilatınız tərəfindən ayarlanan məhdudiyyətləri aşır." }, "vaultExportDisabled": { - "message": "Anbarın xaricə köçürülməsi əlçatmazdır" + "message": "Seyfin xaricə köçürülməsi əlçatmazdır" }, "personalVaultExportPolicyInEffect": { - "message": "Bir və ya daha çox təşkilat siyasəti, fərdi anbarınızı xaricə köçürməyinizi əngəlləyir." + "message": "Bir və ya daha çox təşkilat siyasəti, fərdi seyfi xaricə köçürməyinizi əngəlləyir." }, "copyCustomFieldNameInvalidElement": { "message": "Yararlı bir form elementi müəyyənləşdirilə bilmir. Bunun əvəzinə HTML-i incələməyi sınayın." @@ -2771,10 +2842,10 @@ "message": "Seansınızın vaxtı bitdi. Lütfən geri qayıdıb yenidən giriş etməyə cəhd edin." }, "exportingPersonalVaultTitle": { - "message": "Fərdi anbarın xaricə köçürülməsi" + "message": "Fərdi seyfin xaricə köçürülməsi" }, "exportingIndividualVaultDescription": { - "message": "Yalnız $EMAIL$ ilə əlaqələndirilmiş fərdi anbar elementləri xaricə köçürüləcək. Təşkilat anbar elementləri daxil edilməyəcək. Yalnız anbar element məlumatları xaricə köçürüləcək və əlaqələndirilmiş qoşmalar daxil edilməyəcək.", + "message": "Yalnız $EMAIL$ ilə əlaqələndirilmiş fərdi seyf elementləri xaricə köçürüləcək. Təşkilat seyf elementləri daxil edilməyəcək. Yalnız seyf element məlumatları xaricə köçürüləcək və əlaqələndirilmiş qoşmalar daxil edilməyəcək.", "placeholders": { "email": { "content": "$1", @@ -2783,10 +2854,10 @@ } }, "exportingOrganizationVaultTitle": { - "message": "Təşkilat anbarını xaricə köçürmə" + "message": "Təşkilat seyfini xaricə köçürmə" }, "exportingOrganizationVaultDesc": { - "message": "Yalnız $ORGANIZATION$ ilə əlaqələndirilmiş təşkilat anbarı ixrac ediləcək. Fərdi anbardakı və digər təşkilat elementlər daxil edilmir.", + "message": "Yalnız $ORGANIZATION$ ilə əlaqələndirilmiş təşkilat seyfi xaricə köçürüləcək. Fərdi seyfdə və digər təşkilatlardakı elementlər daxil edilməyəcək.", "placeholders": { "organization": { "content": "$1", @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "İstifadəçi adı yarat" }, + "generateEmail": { + "message": "E-poçt yarat" + }, "usernameType": { "message": "İstifadəçi adı növü" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Xarici yönləndirmə xidməti ilə e-poçt ləqəbi yaradın." }, + "forwarderDomainName": { + "message": "E-poçt domeni", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Seçilmiş xidmət tərəfindən dəstəklənən bir domen seçin", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ xətası: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -3037,7 +3119,7 @@ "message": "Barmaq izi ifadəsi" }, "fingerprintMatchInfo": { - "message": "Lütfən anbarınızın kilidinin açıq olduğuna və Barmaq izi ifadəsinin digər cihazla uyuşduğuna əmin olun." + "message": "Lütfən seyfinizin kilidinin açıq olduğuna və Barmaq izi ifadəsinin digər cihazla uyuşduğuna əmin olun." }, "resendNotification": { "message": "Bildirişi təkrar göndər" @@ -3445,7 +3527,7 @@ "description": "Button text to display in overlay when there are no matching items" }, "addNewVaultItem": { - "message": "Yeni anbar elementi əlavə et", + "message": "Yeni seyf elementi əlavə et", "description": "Screen reader text (aria-label) for new item button in overlay" }, "newLogin": { @@ -3453,7 +3535,7 @@ "description": "Button text to display within inline menu when there are no matching items on a login field" }, "addNewLoginItemAria": { - "message": "Yeni anbar giriş elementini əlavə et, yeni bir pəncərədə açılır", + "message": "Yeni seyf giriş elementini əlavə et, yeni bir pəncərədə açılır", "description": "Screen reader text (aria-label) for new login button within inline menu" }, "newCard": { @@ -3461,7 +3543,7 @@ "description": "Button text to display within inline menu when there are no matching items on a credit card field" }, "addNewCardItemAria": { - "message": "Yeni anbar kart elementini əlavə et, yeni bir pəncərədə açılır", + "message": "Yeni seyf kart elementini əlavə et, yeni bir pəncərədə açılır", "description": "Screen reader text (aria-label) for new card button within inline menu" }, "newIdentity": { @@ -3469,7 +3551,7 @@ "description": "Button text to display within inline menu when there are no matching items on an identity field" }, "addNewIdentityItemAria": { - "message": "Yeni anbar kimlik elementini əlavə et, yeni bir pəncərədə açılır", + "message": "Yeni seyf kimlik elementini əlavə et, yeni bir pəncərədə açılır", "description": "Screen reader text (aria-label) for new identity button within inline menu" }, "bitwardenOverlayMenuAvailable": { @@ -3639,7 +3721,7 @@ } }, "confirmVaultImport": { - "message": "Anbarın daxilə köçürülməsini təsdiqləyin" + "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." @@ -3648,7 +3730,7 @@ "message": "Fayl parolunu təsdiqlə" }, "exportSuccess": { - "message": "Anbar datası xaricə köçürüldü" + "message": "Seyf datası xaricə köçürüldü" }, "typePasskey": { "message": "Keçid açarı" @@ -3925,7 +4007,7 @@ "message": "Bu saytın avto-doldurması üçün giriş elementini saxlayın" }, "yourVaultIsEmpty": { - "message": "Anbarınız boşdur" + "message": "Seyfiniz boşdur" }, "noItemsMatchSearch": { "message": "Axtarışınızla uyuşan heç bir element yoxdur" @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Element yeri" }, + "fileSend": { + "message": "Fayl \"Send\"i" + }, "fileSends": { "message": "Fayl \"Send\"ləri" }, + "textSend": { + "message": "Mətn \"Send\"i" + }, "textSends": { "message": "Mətn \"Send\"ləri" }, @@ -4480,7 +4568,7 @@ "message": "Bitwarden-in yeni bir görünüşü var!" }, "bitwardenNewLookDesc": { - "message": "Anbar vərəqindən avto-doldurma və axtarış etmə artıq daha asan və intuitivdir. Nəzər salın!" + "message": "Seyf vərəqindən avto-doldurma və axtarış etmə artıq daha asan və intuitivdir. Nəzər salın!" }, "accountActions": { "message": "Hesab fəaliyyətləri" @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Kimlik doğrulama" + }, + "fillGeneratedPassword": { + "message": "Yaradılmış parolu doldur", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Parol yenidən yaradıldı", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Giriş Bitwarden-də saxlanılsın?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Boşluq", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Tərs dırnaq", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Nida işarəsi", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At işarəsi", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Diyez işarəsi", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar işarəsi", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Faiz işarəsi", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Və işarəsi", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Ulduz", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Sol mötərizə", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Sağ mötərizə", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Altdan xətt", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Tire", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Üstəgəl", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Bərabərdir", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Sol bəzəkli mötərizə", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Sağ bəzəkli mötərizə", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Sol kvadrat mötərizə", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Sağ kvadrat mötərizə", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Şaquli xətt", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Tərs sləş", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Cüt nöqtə", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Nöqtəli vergül", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Cüt dırnaq", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Tək dırnaq", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Kiçikdir", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Böyükdür", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Vergül", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Nöqtə", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Sual işarəsi", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Düz sləş", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Kiçik hərf" + }, + "uppercaseAriaLabel": { + "message": "Böyük hərf" + }, + "generatedPassword": { + "message": "Parol yarat" } } diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 27bb1687bc7..e6488e5ff83 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -3,24 +3,36 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Менеджар пароляў Bitwarden", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "message": "Дома, на працы ці ў дарозе Bitwarden лёгка абараняе ўсе вашы паролі, ключы доступу і канфідэнцыяльную інфармацыю", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Увайдзіце або стварыце новы ўліковы запіс для доступу да бяспечнага сховішча." }, "inviteAccepted": { - "message": "Invitation accepted" + "message": "Запрашэнне прынята" }, "createAccount": { "message": "Стварыць уліковы запіс" }, + "newToBitwarden": { + "message": "Упершыню ў Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "З вяртаннем" + }, "setAStrongPassword": { - "message": "Set a strong password" + "message": "Прызначыць надзейны пароль" }, "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" @@ -69,7 +81,16 @@ "message": "Падказка да асноўнага пароля (неабавязкова)" }, "joinOrganization": { - "message": "Join organization" + "message": "Далучыцца да арганізацыі" + }, + "joinOrganizationName": { + "message": "Далучыцца да $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organization by setting a master password." @@ -98,6 +119,9 @@ "copyPassword": { "message": "Скапіяваць пароль" }, + "copyPassphrase": { + "message": "Скапіяваць парольную фразу" + }, "copyNote": { "message": "Скапіяваць нататку" }, @@ -114,22 +138,22 @@ "message": "Скапіяваць код бяспекі" }, "copyName": { - "message": "Copy name" + "message": "Скапіяваць імя" }, "copyCompany": { - "message": "Copy company" + "message": "Скапіяваць кампанію" }, "copySSN": { - "message": "Copy Social Security number" + "message": "Скапіяваць нумар сацыяльнага страхавання" }, "copyPassportNumber": { - "message": "Copy passport number" + "message": "Скапіяваць нумар пашпарта" }, "copyLicenseNumber": { - "message": "Copy license number" + "message": "Скапіяваць нумар ліцэнзіі" }, "copyCustomField": { - "message": "Copy $FIELD$", + "message": "Скапіяваць $FIELD$", "placeholders": { "field": { "content": "$1", @@ -138,10 +162,14 @@ } }, "copyWebsite": { - "message": "Copy website" + "message": "Скапіяваць сайт" }, "copyNotes": { - "message": "Copy notes" + "message": "Скапіяваць нататкі" + }, + "fill": { + "message": "Запоўніць", + "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": "Аўтазапаўненне" @@ -177,7 +205,7 @@ "message": "Дадаць картку" }, "addIdentityMenu": { - "message": "Add identity" + "message": "Дадаць пасведчанне" }, "unlockVaultMenu": { "message": "Разблакіраваць сховішча" @@ -195,13 +223,13 @@ "message": "Дадаць элемент" }, "accountEmail": { - "message": "Account email" + "message": "Email уліковага запісу" }, "requestHint": { - "message": "Request hint" + "message": "Запытаць падказкі" }, "requestPasswordHint": { - "message": "Request password hint" + "message": "Запытаць падказку да асноўнага пароля" }, "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": { "message": "Enter your account email address and your password hint will be sent to you" @@ -237,16 +265,16 @@ "message": "Змяніць асноўны пароль" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Працягнуць у вэб-праграме?" }, "continueToWebAppDesc": { "message": "Explore more features of your Bitwarden account on the web app." }, "continueToHelpCenter": { - "message": "Continue to Help Center?" + "message": "Працягнуць працу ў Даведачным цэнтры?" }, "continueToHelpCenterDesc": { - "message": "Learn more about how to use Bitwarden on the Help Center." + "message": "Даведайцеся больш аб тым, як выкарыстоўваць Bitwarden, у Даведачным цэнтры." }, "continueToBrowserExtensionStore": { "message": "Continue to browser extension store?" @@ -272,28 +300,28 @@ "message": "Выйсці" }, "aboutBitwarden": { - "message": "About Bitwarden" + "message": "Пра Bitwarden" }, "about": { "message": "Пра Bitwarden" }, "moreFromBitwarden": { - "message": "More from Bitwarden" + "message": "Больш ад Bitwarden" }, "continueToBitwardenDotCom": { - "message": "Continue to bitwarden.com?" + "message": "Працягнуць на bitwarden.com?" }, "bitwardenForBusiness": { - "message": "Bitwarden for Business" + "message": "Bitwarden для бізнесу" }, "bitwardenAuthenticator": { - "message": "Bitwarden Authenticator" + "message": "Аўтэнтыфікатар Bitwarden" }, "continueToAuthenticatorPageDesc": { "message": "Bitwarden Authenticator allows you to store authenticator keys and generate TOTP codes for 2-step verification flows. Learn more on the bitwarden.com website" }, "bitwardenSecretsManager": { - "message": "Bitwarden Secrets Manager" + "message": "Менеджар сакрэтаў Bitwarden" }, "continueToSecretsManagerPageDesc": { "message": "Securely store, manage, and share developer secrets with Bitwarden Secrets Manager. Learn more on the bitwarden.com website." @@ -305,7 +333,7 @@ "message": "Create smooth and secure login experiences free from traditional passwords with Passwordless.dev. Learn more on the bitwarden.com website." }, "freeBitwardenFamilies": { - "message": "Free Bitwarden Families" + "message": "Бясплатны тарыфны план Bitwarden Families" }, "freeBitwardenFamiliesPageDesc": { "message": "You are eligible for Free Bitwarden Families. Redeem this offer today in the web app." @@ -329,22 +357,22 @@ "message": "Рэдагаваць папку" }, "newFolder": { - "message": "New folder" + "message": "Новая папка" }, "folderName": { - "message": "Folder name" + "message": "Назва папкі" }, "folderHintText": { "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" }, "noFoldersAdded": { - "message": "No folders added" + "message": "Няма дададзеных папак" }, "createFoldersToOrganize": { "message": "Create folders to organize your vault items" }, "deleteFolderPermanently": { - "message": "Are you sure you want to permanently delete this folder?" + "message": "Вы ўпэўненыя, што жадаеце назаўсёды выдаліць гэту папку?" }, "deleteFolder": { "message": "Выдаліць папку" @@ -387,7 +415,7 @@ "message": "Аўтаматычна генерыруйце надзейныя і ўнікальныя паролі для вашых лагінаў." }, "bitWebVaultApp": { - "message": "Bitwarden web app" + "message": "Вэб-праграма Bitwarden" }, "importItems": { "message": "Імпартаванне элементаў" @@ -398,6 +426,9 @@ "generatePassword": { "message": "Генерыраваць пароль" }, + "generatePassphrase": { + "message": "Згенерыраваць парольную фразу" + }, "regeneratePassword": { "message": "Паўторна генерыраваць пароль" }, @@ -427,11 +458,11 @@ "description": "deprecated. Use specialCharactersLabel instead." }, "include": { - "message": "Include", + "message": "Уключыць", "description": "Card header for password generator include block" }, "uppercaseDescription": { - "message": "Include uppercase characters", + "message": "Уключыце вялікія літары", "description": "Tooltip for the password generator uppercase character checkbox" }, "uppercaseLabel": { @@ -439,7 +470,7 @@ "description": "Label for the password generator uppercase character checkbox" }, "lowercaseDescription": { - "message": "Include lowercase characters", + "message": "Уключайце малыя літары", "description": "Full description for the password generator lowercase character checkbox" }, "lowercaseLabel": { @@ -447,7 +478,7 @@ "description": "Label for the password generator lowercase character checkbox" }, "numbersDescription": { - "message": "Include numbers", + "message": "Уключаць лічбы", "description": "Full description for the password generator numbers checkbox" }, "numbersLabel": { @@ -455,7 +486,7 @@ "description": "Label for the password generator numbers checkbox" }, "specialCharactersDescription": { - "message": "Include special characters", + "message": "Уключыце спецыяльныя сімвалы", "description": "Full description for the password generator special characters checkbox" }, "specialCharactersLabel": { @@ -486,7 +517,7 @@ "description": "deprecated. Use avoidAmbiguous instead." }, "avoidAmbiguous": { - "message": "Avoid ambiguous characters", + "message": "Пазбягаць неадназначных сімвалаў", "description": "Label for the avoid ambiguous characters checkbox." }, "generatorPolicyInEffect": { @@ -515,7 +546,7 @@ "message": "Пароль" }, "totp": { - "message": "Authenticator secret" + "message": "Сакрэт аўтэнтыфікацыі" }, "passphrase": { "message": "Парольная фраза" @@ -524,7 +555,7 @@ "message": "Абраны" }, "unfavorite": { - "message": "Unfavorite" + "message": "Выдаліць з абранага" }, "itemAddedToFavorites": { "message": "Item added to favorites" @@ -536,7 +567,7 @@ "message": "Нататкі" }, "privateNote": { - "message": "Private note" + "message": "Прыватная нататка" }, "note": { "message": "Нататка" @@ -557,7 +588,16 @@ "message": "Запусціць" }, "launchWebsite": { - "message": "Launch website" + "message": "Адкрыць сайт" + }, + "launchWebsiteName": { + "message": "Адкрыць сайт $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } }, "website": { "message": "Вэб-сайт" @@ -572,7 +612,7 @@ "message": "Iншае" }, "unlockMethods": { - "message": "Unlock options" + "message": "Параметры блакіроўкі" }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Наладзіць метад разблакіроўкі для змянення дзеяння часу чакання вашага сховішча." @@ -581,13 +621,13 @@ "message": "Set up an unlock method in Settings" }, "sessionTimeoutHeader": { - "message": "Session timeout" + "message": "Час чакання сеанса" }, "vaultTimeoutHeader": { - "message": "Vault timeout" + "message": "Час чакання сховішча" }, "otherOptions": { - "message": "Other options" + "message": "Іншыя налады" }, "rateExtension": { "message": "Ацаніць пашырэнне" @@ -605,13 +645,13 @@ "message": "Ваша сховішча заблакіравана. Каб працягнуць, пацвердзіце сваю асобу." }, "yourVaultIsLockedV2": { - "message": "Your vault is locked" + "message": "Ваша сховішча заблакіравана" }, "yourAccountIsLocked": { - "message": "Your account is locked" + "message": "Ваш уліковы запіс заблакіраваны" }, "or": { - "message": "or" + "message": "або" }, "unlock": { "message": "Разблакіраваць" @@ -636,7 +676,7 @@ "message": "Час чакання сховішча" }, "vaultTimeout1": { - "message": "Timeout" + "message": "Час чакання" }, "lockNow": { "message": "Заблакіраваць зараз" @@ -690,16 +730,16 @@ "message": "Бяспека" }, "confirmMasterPassword": { - "message": "Confirm master password" + "message": "Пацвердзіць асноўны пароль" }, "masterPassword": { - "message": "Master password" + "message": "Асноўны пароль" }, "masterPassImportant": { - "message": "Your master password cannot be recovered if you forget it!" + "message": "Ваш асноўны пароль немагчыма будзе аднавіць, калі вы яго забудзеце!" }, "masterPassHintLabel": { - "message": "Master password hint" + "message": "Падказка да асноўнага пароля" }, "errorOccurred": { "message": "Адбылася памылка" @@ -736,7 +776,7 @@ "message": "Your new account has been created!" }, "youHaveBeenLoggedIn": { - "message": "You have been logged in!" + "message": "Вы ўвайшлі ва ўліковы запіс!" }, "youSuccessfullyLoggedIn": { "message": "Вы паспяхова аўтарызаваны" @@ -773,7 +813,7 @@ "message": "Unable to scan QR code from the current webpage" }, "totpCaptureSuccess": { - "message": "Authenticator key added" + "message": "Ключ аўтэнтыфікацыі дададзены" }, "totpCapture": { "message": "Scan authenticator QR code from current webpage" @@ -791,7 +831,7 @@ "message": "Learn more about authenticators" }, "copyTOTP": { - "message": "Copy Authenticator key (TOTP)" + "message": "Скапіяваць ключ аўтэнтыфікацыі (TOTP)" }, "loggedOut": { "message": "Вы выйшлі" @@ -803,13 +843,16 @@ "message": "Тэрмін дзеяння вашага сеансу завяршыўся." }, "logIn": { - "message": "Log in" + "message": "Увайсці" + }, + "logInToBitwarden": { + "message": "Увайсці ў Bitwarden" }, "restartRegistration": { "message": "Restart registration" }, "expiredLink": { - "message": "Expired link" + "message": "Пратэрмінаваная спасылка" }, "pleaseRestartRegistrationOrTryLoggingIn": { "message": "Please restart registration or try logging in." @@ -842,7 +885,7 @@ "message": "Make your account more secure by setting up two-step login in the Bitwarden web app." }, "twoStepLoginConfirmationTitle": { - "message": "Continue to web app?" + "message": "Працягнуць у вэб-праграме?" }, "editedFolder": { "message": "Папка адрэдагавана" @@ -885,7 +928,7 @@ "message": "Новы URI" }, "addDomain": { - "message": "Add domain", + "message": "Дадаць дамен", "description": "'Domain' here refers to an internet domain name (e.g. 'bitwarden.com') and the message in whole described the act of putting a domain value into the context." }, "addedItem": { @@ -997,7 +1040,7 @@ "message": "Разблакіраваць" }, "additionalOptions": { - "message": "Additional options" + "message": "Дадатковы параметры" }, "enableContextMenuItem": { "message": "Паказваць параметры кантэкстнага меню" @@ -1006,7 +1049,7 @@ "message": "Выкарыстоўваць падвоены націск для доступу да генератара пароляў і супастаўлення лагінаў для вэб-сайтаў. " }, "contextMenuItemDescAlt": { - "message": "Use a secondary click to access password generation and matching logins for the website. Applies to all logged in accounts." + "message": "Выкарыстоўваць падвоены націск для доступу да генератара пароляў і супастаўлення лагінаў для вэб-сайтаў. Прымяняецца да ўсіх уліковых запісаў, якія ўвайшлі ў сістэму." }, "defaultUriMatchDetection": { "message": "Прадвызначанае выяўленне супадзення URI", @@ -1037,7 +1080,7 @@ "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, "exportFrom": { - "message": "Export from" + "message": "Экспартаванне з" }, "exportVault": { "message": "Экспартаваць сховішча" @@ -1046,28 +1089,28 @@ "message": "Фармат файла" }, "fileEncryptedExportWarningDesc": { - "message": "This file export will be password protected and require the file password to decrypt." + "message": "Гэты экспартаваны файл будзе абаронены паролем, які неабходна будзе ўвесці для яго расшыфроўкі." }, "filePassword": { - "message": "File password" + "message": "Пароль файла" }, "exportPasswordDescription": { - "message": "This password will be used to export and import this file" + "message": "Гэты пароль будзе выкарыстоўвацца для экспартавання і імпартавання гэтага файла" }, "accountRestrictedOptionDescription": { - "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + "message": "Выкарыстоўвайце свой ключ шыфравання, які атрыманы з імя карыстальніка і асноўнага пароля, каб зашыфраваць экспартаванне і абмежаваць імпартаванне толькі бягучага ўліковага запісу Bitwarden." }, "passwordProtectedOptionDescription": { "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." }, "exportTypeHeading": { - "message": "Export type" + "message": "Тып экспартавання" }, "accountRestricted": { - "message": "Account restricted" + "message": "Абмежавана ўліковым запісам" }, "filePasswordAndConfirmFilePasswordDoNotMatch": { - "message": "“File password” and “Confirm file password“ do not match." + "message": "“Пароль файла” і “Пацвярджэнне пароля файла“ не супадаюць." }, "warning": { "message": "ПАПЯРЭДЖАННЕ", @@ -1189,10 +1232,10 @@ "message": "1 ГБ зашыфраванага сховішча для далучаных файлаў." }, "premiumSignUpEmergency": { - "message": "Emergency access." + "message": "Экстранны доступ." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Прапрыетарныя варыянты двухэтапнага ўваходу, такія як YubiKey і Duo." }, "ppremiumSignUpReports": { "message": "Гігіена пароляў, здароўе ўліковага запісу і справаздачы аб уцечках даных для забеспячэння бяспекі вашага сховішча." @@ -1234,7 +1277,7 @@ } }, "premiumPriceV2": { - "message": "All for just $PRICE$ per year!", + "message": "Усяго за $PRICE$ у год!", "placeholders": { "price": { "content": "$1", @@ -1331,7 +1374,7 @@ "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP Security Key" + "message": "Ключ бяспекі Yubico OTP" }, "yubiKeyDesc": { "message": "Выкарыстоўвайце YubiKey для доступу да вашага ўліковага запісу. Працуе з ключамі бяспекі YubiKey 4, 4 Nano, 4C і NEO." @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "URL-адрас сервера" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "Сервер URL-адраса API" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1421,7 +1474,7 @@ "message": "Edit browser settings." }, "autofillOverlayVisibilityOff": { - "message": "Off", + "message": "Выключана", "description": "Overlay setting select option for disabling autofill overlay" }, "autofillOverlayVisibilityOnFieldFocus": { @@ -1433,7 +1486,7 @@ "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, "enableAutoFillOnPageLoadSectionTitle": { - "message": "Autofill on page load" + "message": "Аўтазапаўненне пры загрузцы старонкі" }, "enableAutoFillOnPageLoad": { "message": "Аўтазапаўненне пры загрузцы старонкі" @@ -1458,7 +1511,7 @@ "message": "Скампраметаваныя або ненадзейныя вэб-сайты могуць задзейнічаць функцыю аўтазапаўнення падчас загрузкі старонкі." }, "learnMoreAboutAutofillOnPageLoadLinkText": { - "message": "Learn more about risks" + "message": "Даведацца больш пра рызыкі" }, "learnMoreAboutAutofill": { "message": "Даведацца больш пра аўтазапаўненне" @@ -1488,13 +1541,13 @@ "message": "Адкрыць сховішча ў бакавой панэлі" }, "commandAutofillLoginDesc": { - "message": "Autofill the last used login for the current website" + "message": "Аўтазапаўненне апошняга скарыстанага лагіна для бягучага вэб-сайта" }, "commandAutofillCardDesc": { - "message": "Autofill the last used card for the current website" + "message": "Аўтазапаўненне апошняй скарыстанай карткі для бягучага вэб-сайта" }, "commandAutofillIdentityDesc": { - "message": "Autofill the last used identity for the current website" + "message": "Аўтазапаўненне апошняга скарыстанага пасведчання для бягучага вэб-сайта" }, "commandGeneratePasswordDesc": { "message": "Генерыраваць і скапіяваць новы выпадковы пароль у буфер абмену" @@ -1527,7 +1580,7 @@ "message": "Булева" }, "cfTypeCheckbox": { - "message": "Checkbox" + "message": "Птушка" }, "cfTypeLinked": { "message": "Звязана", @@ -1712,7 +1765,7 @@ "message": "Пасведчанне" }, "newItemHeader": { - "message": "New $TYPE$", + "message": "Новы $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1721,7 +1774,7 @@ } }, "editItemHeader": { - "message": "Edit $TYPE$", + "message": "Рэдагаваць $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1730,7 +1783,7 @@ } }, "viewItemHeader": { - "message": "View $TYPE$", + "message": "Прагляд $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Гісторыя пароляў" }, + "generatorHistory": { + "message": "Гісторыя генератара" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "Назад" }, @@ -1855,13 +1917,13 @@ "message": "У спісе адсутнічаюць паролі." }, "clearHistory": { - "message": "Clear history" + "message": "Ачысціць гісторыю" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Адсутнічаюць элементы для паказу" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "Выдаліць" @@ -1922,10 +1984,10 @@ "message": "Разблакіраваць PIN-кодам" }, "setYourPinTitle": { - "message": "Set PIN" + "message": "Задаць PIN" }, "setYourPinButton": { - "message": "Set PIN" + "message": "Задаць PIN" }, "setYourPinCode": { "message": "Прызначце PIN-код для разблакіроўкі Bitwarden. Налады PIN-кода будуць скінуты, калі вы калі-небудзь цалкам выйдзеце з праграмы." @@ -1946,7 +2008,7 @@ "message": "Разблакіраваць з дапамогай біяметрыі" }, "unlockWithMasterPassword": { - "message": "Unlock with master password" + "message": "Разблакіраваць з дапамогай асноўнага пароля" }, "awaitDesktop": { "message": "Чаканне пацвярджэння з камп'ютара" @@ -1958,7 +2020,7 @@ "message": "Заблакіраваць асноўным паролем пры перазапуску браўзера" }, "lockWithMasterPassOnRestart1": { - "message": "Require master password on browser restart" + "message": "Патрабаваць асноўны пароль пры перазапуску браўзера" }, "selectOneCollection": { "message": "Вы павінны выбраць прынамсі адну калекцыю." @@ -1973,26 +2035,26 @@ "message": "Адна або больш палітык арганізацыі ўплывае на налады генератара." }, "passwordGenerator": { - "message": "Password generator" + "message": "Генератар пароляў" }, "usernameGenerator": { - "message": "Username generator" + "message": "Генератар імені карыстальніка" }, "useThisPassword": { - "message": "Use this password" + "message": "Выкарыстоўваць гэты пароль" }, "useThisUsername": { - "message": "Use this username" + "message": "Выкарыстоўваць гэта імя карыстальніка" }, "securePasswordGenerated": { "message": "Secure password generated! Don't forget to also update your password on the website." }, "useGeneratorHelpTextPartOne": { - "message": "Use the generator", + "message": "Выкарыстоўваць генератар", "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" }, "useGeneratorHelpTextPartTwo": { - "message": "to create a strong unique password", + "message": "каб стварыць надзейны ўнікальны пароль", "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" }, "vaultTimeoutAction": { @@ -2028,7 +2090,7 @@ "message": "Элемент адноўлены" }, "alreadyHaveAccount": { - "message": "Already have an account?" + "message": "Ужо маеце ўліковы запіс?" }, "vaultTimeoutLogOutConfirmation": { "message": "Выхад з сістэмы скасуе ўсе магчымасці доступу да сховішча і запатрабуе аўтэнтыфікацыю праз інтэрнэт пасля завяршэння часу чакання. Вы сапраўды хочаце выкарыстоўваць гэты параметр?" @@ -2040,7 +2102,7 @@ "message": "Аўтазапоўніць і захаваць" }, "fillAndSave": { - "message": "Fill and save" + "message": "Запоўніць і захаваць" }, "autoFillSuccessAndSavedUri": { "message": "Аўтазапоўнены элемент і захаваны URI" @@ -2124,16 +2186,16 @@ "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { - "message": "Unsubscribe" + "message": "Адпісацца" }, "atAnyTime": { - "message": "at any time." + "message": "ў любы час." }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "Працягваючы, вы згаджаецеся з" }, "and": { - "message": "and" + "message": "і" }, "acceptPolicies": { "message": "Ставячы гэты сцяжок, вы пагаджаецеся з наступным:" @@ -2253,7 +2315,7 @@ "message": "An organization policy has blocked importing items into your individual vault." }, "domainsTitle": { - "message": "Domains", + "message": "Дамены", "description": "A category title describing the concept of web domains" }, "excludedDomains": { @@ -2266,7 +2328,7 @@ "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, "websiteItemLabel": { - "message": "Website $number$ (URI)", + "message": "Вэб-сайт $number$ (URI)", "placeholders": { "number": { "content": "$1", @@ -2294,7 +2356,7 @@ "description": "Displayed under the limit views field on Send" }, "limitSendViewsCount": { - "message": "$ACCESSCOUNT$ views left", + "message": "Засталося праглядаў: $ACCESSCOUNT$", "description": "Displayed under the limit views field on Send", "placeholders": { "accessCount": { @@ -2308,7 +2370,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendDetails": { - "message": "Send details", + "message": "Падрабязнасці Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "searchSends": { @@ -2349,7 +2411,7 @@ "message": "Абаронена паролем" }, "copyLink": { - "message": "Copy link" + "message": "Скапіяваць спасылку" }, "copySendLink": { "message": "Скапіяваць спасылку на Send", @@ -2387,7 +2449,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "deleteSendPermanentConfirmation": { - "message": "Are you sure you want to permanently delete this Send?", + "message": "Вы ўпэўненыя, што хочаце назаўсёды выдаліць гэты Send?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "editSend": { @@ -2449,8 +2511,8 @@ "message": "Па магчымасці запытваць у карыстальнікаў пароль для доступу да гэтага Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2495,7 +2557,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "createdSendSuccessfully": { - "message": "Send created successfully!", + "message": "Send паспяхова створаны!", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInHoursSingle": { @@ -2606,7 +2668,7 @@ "message": "Патрабуецца праверка электроннай пошты" }, "emailVerifiedV2": { - "message": "Email verified" + "message": "Email пацверджаны" }, "emailVerificationRequiredDesc": { "message": "Вы павінны праверыць свой адрас электроннай пошты, каб выкарыстоўваць гэту функцыю. Зрабіць гэта можна ў вэб-сховішчы." @@ -2647,8 +2709,17 @@ "message": "Your organization requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "з $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { - "message": "Verification required", + "message": "Патрабуецца праверка", "description": "Default title for the user verification dialog." }, "hours": { @@ -2774,7 +2845,7 @@ "message": "Экспартаванне асабістага сховішча" }, "exportingIndividualVaultDescription": { - "message": "Only the individual vault items associated with $EMAIL$ will be exported. Organization vault items will not be included. Only vault item information will be exported and will not include associated attachments.", + "message": "Будуць экспартаваны толькі індывідуальныя элементы сховішча, якія звязаны з $EMAIL$. Элементы сховішча арганізацыі не будуць уключаны. Толькі звесткі элемента сховішча будуць экспартаваны і яны не будуць уключаць звязаныя далучэнні.", "placeholders": { "email": { "content": "$1", @@ -2783,10 +2854,10 @@ } }, "exportingOrganizationVaultTitle": { - "message": "Exporting organization vault" + "message": "Экспартаванне сховішча арганізацыі" }, "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "message": "Толькі сховішча арганізацыі, якія звязаны з $ORGANIZATION$ будуць экспартаваны. Элементы асабістага сховішча і элементы з іншых арганізацый не будуць уключаны.", "placeholders": { "organization": { "content": "$1", @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Генерыраваць імя карыстальніка" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Тып імя карыстальніка" }, @@ -2843,8 +2917,16 @@ "forwardedEmailDesc": { "message": "Генерыраваць псеўданім электроннай пошты са знешнім сэрвісам перасылкі." }, + "forwarderDomainName": { + "message": "Дамен электроннай пошты", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { - "message": "$SERVICENAME$ error: $ERRORMESSAGE$", + "message": "Памылка $SERVICENAME$: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", "placeholders": { "servicename": { @@ -2858,7 +2940,7 @@ } }, "forwarderGeneratedBy": { - "message": "Generated by Bitwarden.", + "message": "Сгенеравана Bitwarden.", "description": "Displayed with the address on the forwarding service's configuration screen." }, "forwarderGeneratedByWithWebsite": { @@ -3203,13 +3285,13 @@ "message": "Уліковы запіс паспяхова створаны!" }, "adminApprovalRequested": { - "message": "Admin approval requested" + "message": "Патрабуецца ўхваленне адміністратара" }, "adminApprovalRequestSentToAdmins": { - "message": "Your request has been sent to your admin." + "message": "Ваш запыт адпраўлены адміністратару." }, "youWillBeNotifiedOnceApproved": { - "message": "You will be notified once approved." + "message": "Вы атрымаеце апавяшчэння пасля яго ўхвалення." }, "troubleLoggingIn": { "message": "Праблемы з уваходам?" @@ -3218,30 +3300,30 @@ "message": "Уваход ухвалены" }, "userEmailMissing": { - "message": "User email missing" + "message": "Адсутнічае электронная пошта карыстальніка" }, "deviceTrusted": { - "message": "Device trusted" + "message": "Давераная прылада" }, "sendsNoItemsTitle": { - "message": "No active Sends", + "message": "Няма актыўных Send'аў", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendsNoItemsMessage": { - "message": "Use Send to securely share encrypted information with anyone.", + "message": "Выкарыстоўвайце Send'ы, каб бяспечна абагуляць зашыфраваную інфармацыю з іншымі.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "inputRequired": { - "message": "Input is required." + "message": "Неабходны ўвод даных." }, "required": { - "message": "required" + "message": "патрабуецца" }, "search": { - "message": "Search" + "message": "Пошук" }, "inputMinLength": { - "message": "Input must be at least $COUNT$ characters long.", + "message": "Даўжыня ўведзеных даных павінна складаць прынамсі $COUNT$ сімв.", "placeholders": { "count": { "content": "$1", @@ -3250,7 +3332,7 @@ } }, "inputMaxLength": { - "message": "Input must not exceed $COUNT$ characters in length.", + "message": "Даўжыня ўведзеных даных не можа перавышаць наступную колькасць сімвалаў: $COUNT$.", "placeholders": { "count": { "content": "$1", @@ -3259,7 +3341,7 @@ } }, "inputForbiddenCharacters": { - "message": "The following characters are not allowed: $CHARACTERS$", + "message": "Наступныя сімвалы забаронены: $CHARACTERS$", "placeholders": { "characters": { "content": "$1", @@ -3268,7 +3350,7 @@ } }, "inputMinValue": { - "message": "Input value must be at least $MIN$.", + "message": "Мінімальная даўжыня сімвалаў значэння, якое будзе ўводзіцца: $MIN$.", "placeholders": { "min": { "content": "$1", @@ -3277,7 +3359,7 @@ } }, "inputMaxValue": { - "message": "Input value must not exceed $MAX$.", + "message": "Максімальная даўжыня сімвалаў значэння, якое будзе ўводзіцца: $MAX$.", "placeholders": { "max": { "content": "$1", @@ -3296,7 +3378,7 @@ "message": "Уведзеныя даныя не з'яўляюцца адрасам электроннай пошты." }, "fieldsNeedAttention": { - "message": "$COUNT$ field(s) above need your attention.", + "message": "Колькасць палёў, якія патрабуюць вашай увагі: $COUNT$.", "placeholders": { "count": { "content": "$1", @@ -3305,10 +3387,10 @@ } }, "singleFieldNeedsAttention": { - "message": "1 field needs your attention." + "message": "1 поле патрабуе вашай увагі." }, "multipleFieldsNeedAttention": { - "message": "$COUNT$ fields need your attention.", + "message": "Колькасць палёў, якія патрабуюць вашай увагі: $COUNT$.", "placeholders": { "count": { "content": "$1", @@ -3332,7 +3414,7 @@ "message": "Ачысціць усё" }, "plusNMore": { - "message": "+ $QUANTITY$ more", + "message": "+ яшчэ $QUANTITY$", "placeholders": { "quantity": { "content": "$1", @@ -3344,7 +3426,7 @@ "message": "Падменю" }, "toggleCollapse": { - "message": "Toggle collapse", + "message": "Згарнуць/Разгарнуць", "description": "Toggling an expand/collapse state." }, "filelessImport": { @@ -3364,11 +3446,11 @@ "description": "Notification button text for starting a fileless import." }, "importing": { - "message": "Importing...", + "message": "Імпартаванне...", "description": "Notification message for when an import is in progress." }, "dataSuccessfullyImported": { - "message": "Data successfully imported!", + "message": "Даныя паспяхова імпартаваны!", "description": "Notification message for when an import has completed successfully." }, "dataImportFailed": { @@ -3398,7 +3480,7 @@ "message": "Toggle side navigation" }, "skipToContent": { - "message": "Skip to content" + "message": "Перайсці да змесціва" }, "bitwardenOverlayButton": { "message": "Bitwarden autofill menu button", @@ -3409,7 +3491,7 @@ "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden autofill menu", + "message": "Меню аўтазапаўнення Bitwarden", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { @@ -3437,11 +3519,11 @@ "description": "Screen reader text for when a login item is focused where a partial username is displayed. SR will announce this phrase before reading the text of the partial username" }, "noItemsToShow": { - "message": "No items to show", + "message": "Няма элементаў для паказу", "description": "Text to show in overlay if there are no matching items" }, "newItem": { - "message": "New item", + "message": "Новы элемент", "description": "Button text to display in overlay when there are no matching items" }, "addNewVaultItem": { @@ -3449,7 +3531,7 @@ "description": "Screen reader text (aria-label) for new item button in overlay" }, "newLogin": { - "message": "New login", + "message": "Новы лагін", "description": "Button text to display within inline menu when there are no matching items on a login field" }, "addNewLoginItemAria": { @@ -3477,17 +3559,17 @@ "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { - "message": "Turn on" + "message": "Уключыць" }, "ignore": { - "message": "Ignore" + "message": "Iгнараваць" }, "importData": { - "message": "Import data", + "message": "Імпартаванне даных", "description": "Used for the header of the import dialog, the import button and within the file-password-prompt" }, "importError": { - "message": "Import error" + "message": "Памылка імпартавання" }, "importErrorDesc": { "message": "There was a problem with the data you tried to import. Please resolve the errors listed below in your source file and try again." @@ -3511,13 +3593,13 @@ } }, "tryAgain": { - "message": "Try again" + "message": "Паспрабуйце зноў" }, "verificationRequiredForActionSetPinToContinue": { "message": "Verification required for this action. Set a PIN to continue." }, "setPin": { - "message": "Set PIN" + "message": "Задаць PIN" }, "verifyWithBiometrics": { "message": "Verify with biometrics" @@ -3535,22 +3617,22 @@ "message": "Use master password" }, "usePin": { - "message": "Use PIN" + "message": "Увесці PIN-код" }, "useBiometrics": { - "message": "Use biometrics" + "message": "Выкарыстоўваць біяметрыю" }, "enterVerificationCodeSentToEmail": { - "message": "Enter the verification code that was sent to your email." + "message": "Увядзіце праверачны код, які быў адпраўлены на вашу электронную пошту." }, "resendCode": { - "message": "Resend code" + "message": "Адправіць код яшчэ раз" }, "total": { "message": "Усяго" }, "importWarning": { - "message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organization. Do you want to proceed?", + "message": "Вы імпартуеце даныя ў $ORGANIZATION$. Вашы даныя могуць быць абагулены з удзельнікамі арганізацыі. Вы сапраўды хочаце працягнуць?", "placeholders": { "organization": { "content": "$1", @@ -3577,31 +3659,31 @@ "message": "Launch Duo" }, "importFormatError": { - "message": "Data is not formatted correctly. Please check your import file and try again." + "message": "Даныя няправільна адфарматаваныя. Калі ласка, праверце файл імпарту і паспрабуйце яшчэ раз." }, "importNothingError": { - "message": "Nothing was imported." + "message": "Нічога не было імпартавана." }, "importEncKeyError": { - "message": "Error decrypting the exported file. Your encryption key does not match the encryption key used export the data." + "message": "Памылка дэшыфроўкі экспартаванага файла. Ваш ключ шыфравання не супадае з ключом шыфравання, які выкарыстоўваецца для экспартавання даных." }, "invalidFilePassword": { - "message": "Invalid file password, please use the password you entered when you created the export file." + "message": "Памылковы пароль файла. Скарыстайцеся паролем, які вы ўводзілі пры стварэнні файла экспартавання." }, "destination": { - "message": "Destination" + "message": "Прызначэнне" }, "learnAboutImportOptions": { "message": "Даведацца пра параметры імпартавання" }, "selectImportFolder": { - "message": "Select a folder" + "message": "Выбраць папку" }, "selectImportCollection": { - "message": "Select a collection" + "message": "Выбраць калекцыю" }, "importTargetHint": { - "message": "Select this option if you want the imported file contents moved to a $DESTINATION$", + "message": "Выберыце гэты параметр, калі вы хочаце, каб змесціва імпартаванага файла было перамешчанага ў $DESTINATION$", "description": "Located as a hint under the import target. Will be appended by either folder or collection, depending if the user is importing into an individual or an organizational vault.", "placeholders": { "destination": { @@ -3611,13 +3693,13 @@ } }, "importUnassignedItemsError": { - "message": "File contains unassigned items." + "message": "Файл змяшчае непадпісаныя элементы." }, "selectFormat": { - "message": "Select the format of the import file" + "message": "Выберыце фармат файла імпартавання" }, "selectImportFile": { - "message": "Select the import file" + "message": "Выберыце файл імпартавання" }, "chooseFile": { "message": "Выбраць файл" @@ -3626,10 +3708,10 @@ "message": "Файл не выбраны" }, "orCopyPasteFileContents": { - "message": "or copy/paste the import file contents" + "message": "або скапіюйце/устаўце змесціва файла, які імпартуецца" }, "instructionsFor": { - "message": "$NAME$ Instructions", + "message": "Інструкцыі для $NAME$", "description": "The title for the import tool instructions.", "placeholders": { "name": { @@ -3639,10 +3721,10 @@ } }, "confirmVaultImport": { - "message": "Confirm vault import" + "message": "Пацвердзіць імпартаванне сховішча" }, "confirmVaultImportDesc": { - "message": "This file is password-protected. Please enter the file password to import data." + "message": "Гэты файл абаронены паролем. Калі ласка, увядзіце пароль для імпартавання даных." }, "confirmFilePassword": { "message": "Пацвердзіць пароль файла" @@ -3651,7 +3733,7 @@ "message": "Vault data exported" }, "typePasskey": { - "message": "Passkey" + "message": "Ключ доступу" }, "accessing": { "message": "Accessing" @@ -3687,7 +3769,7 @@ "message": "Пацвердзіць" }, "savePasskey": { - "message": "Save passkey" + "message": "Захаваць ключ доступу" }, "savePasskeyNewLogin": { "message": "Save passkey as new login" @@ -3702,7 +3784,7 @@ "message": "Passkey Item" }, "overwritePasskey": { - "message": "Overwrite passkey?" + "message": "Перазапісаць ключ доступу?" }, "overwritePasskeyAlert": { "message": "This item already contains a passkey. Are you sure you want to overwrite the current passkey?" @@ -3723,13 +3805,13 @@ "message": "Incorrect username or password" }, "incorrectPassword": { - "message": "Incorrect password" + "message": "Няправільны пароль" }, "incorrectCode": { - "message": "Incorrect code" + "message": "Няправільны код" }, "incorrectPin": { - "message": "Incorrect PIN" + "message": "Няправільны PIN-код" }, "multifactorAuthenticationFailed": { "message": "Multifactor authentication failed" @@ -3753,7 +3835,7 @@ "message": "Approve the login request in your authentication app or enter a one-time passcode." }, "passcode": { - "message": "Passcode" + "message": "Код доступу" }, "lastPassMasterPassword": { "message": "LastPass master password" @@ -3775,19 +3857,19 @@ "message": "Import directly from LastPass" }, "importFromCSV": { - "message": "Import from CSV" + "message": "Імпартаваць з CSV" }, "lastPassTryAgainCheckEmail": { "message": "Try again or look for an email from LastPass to verify it's you." }, "collection": { - "message": "Collection" + "message": "Калекцыя" }, "lastPassYubikeyDesc": { "message": "Insert the YubiKey associated with your LastPass account into your computer's USB port, then touch its button." }, "switchAccount": { - "message": "Switch account" + "message": "Змяніць уліковы запіс" }, "switchAccounts": { "message": "Switch accounts" @@ -3802,19 +3884,19 @@ "message": "Available accounts" }, "accountLimitReached": { - "message": "Account limit reached. Log out of an account to add another." + "message": "Дасягнута абмежаванне ўліковага запісу. Выйдзіце, каб дадаць іншы ўліковы запіс." }, "active": { - "message": "active" + "message": "актыўны" }, "locked": { - "message": "locked" + "message": "заблакіравана" }, "unlocked": { - "message": "unlocked" + "message": "разблакавана" }, "server": { - "message": "server" + "message": "сервер" }, "hostedAt": { "message": "hosted at" @@ -3886,7 +3968,7 @@ "description": "Description for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "makeDefault": { - "message": "Make default", + "message": "Зрабіць прадвызначаным", "description": "Button text for the setting that allows overriding the default browser autofill settings" }, "saveCipherAttemptSuccess": { @@ -3894,7 +3976,7 @@ "description": "Notification message for when saving credentials has succeeded." }, "passwordSaved": { - "message": "Password saved!", + "message": "Пароль захаваны!", "description": "Notification message for when saving credentials has succeeded." }, "updateCipherAttemptSuccess": { @@ -3902,7 +3984,7 @@ "description": "Notification message for when updating credentials has succeeded." }, "passwordUpdated": { - "message": "Password updated!", + "message": "Пароль абноўлены!", "description": "Notification message for when updating credentials has succeeded." }, "saveCipherAttemptFailed": { @@ -3910,7 +3992,7 @@ "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "Поспех" }, "removePasskey": { "message": "Remove passkey" @@ -3925,10 +4007,10 @@ "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { - "message": "Your vault is empty" + "message": "Ваша сховішча нічога не змяшчае" }, "noItemsMatchSearch": { - "message": "No items match your search" + "message": "Пошук не даў вынікаў" }, "clearFiltersOrTryAnother": { "message": "Clear filters or try another search term" @@ -4000,25 +4082,25 @@ "message": "Assign to collections" }, "copyEmail": { - "message": "Copy email" + "message": "Скапіяваць электронную пошту" }, "copyPhone": { - "message": "Copy phone" + "message": "Скапіяваць тэлефон" }, "copyAddress": { - "message": "Copy address" + "message": "Скапіяваць адрас" }, "adminConsole": { - "message": "Admin Console" + "message": "Кансоль адміністратара" }, "accountSecurity": { - "message": "Account security" + "message": "Бяспеке акаўнта" }, "notifications": { - "message": "Notifications" + "message": "Апавяшчэнні" }, "appearance": { - "message": "Appearance" + "message": "Знешні выгляд" }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." @@ -4047,10 +4129,10 @@ } }, "new": { - "message": "New" + "message": "Новы" }, "removeItem": { - "message": "Remove $NAME$", + "message": "Выдаліць $NAME$", "description": "Remove a selected option, such as a folder or collection", "placeholders": { "name": { @@ -4060,13 +4142,13 @@ } }, "itemsWithNoFolder": { - "message": "Items with no folder" + "message": "Элементы без папкі" }, "itemDetails": { - "message": "Item details" + "message": "Падрабязнасці элемента" }, "itemName": { - "message": "Item name" + "message": "Назва элемента" }, "cannotRemoveViewOnlyCollections": { "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", @@ -4081,38 +4163,38 @@ "message": "Organization is deactivated" }, "owner": { - "message": "Owner" + "message": "Уладальнік" }, "selfOwnershipLabel": { - "message": "You", + "message": "Вы", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, "additionalInformation": { - "message": "Additional information" + "message": "Дадатковая інфармацыя" }, "itemHistory": { - "message": "Item history" + "message": "Гісторыя элемента" }, "lastEdited": { "message": "Last edited" }, "ownerYou": { - "message": "Owner: You" + "message": "Уладальнік: Вы" }, "linked": { - "message": "Linked" + "message": "Звязана" }, "copySuccessful": { "message": "Copy Successful" }, "upload": { - "message": "Upload" + "message": "Запампаваць" }, "addAttachment": { - "message": "Add attachment" + "message": "Дадаць укладанне" }, "maxFileSizeSansPunctuation": { "message": "Maximum file size is 500 MB" @@ -4238,10 +4320,10 @@ "message": "If you've renewed it, update the card's information" }, "cardDetails": { - "message": "Card details" + "message": "Падрабязнасці карткі" }, "cardBrandDetails": { - "message": "$BRAND$ details", + "message": "Падрабязнасці $BRAND$", "placeholders": { "brand": { "content": "$1", @@ -4250,26 +4332,26 @@ } }, "enableAnimations": { - "message": "Enable animations" + "message": "Уключыць анімацыі" }, "showAnimations": { - "message": "Show animations" + "message": "Паказаць анімацыі" }, "addAccount": { - "message": "Add account" + "message": "Дадаць уліковы запіс" }, "loading": { - "message": "Loading" + "message": "Загрузка" }, "data": { - "message": "Data" + "message": "Даныя" }, "passkeys": { - "message": "Passkeys", + "message": "Ключы доступу", "description": "A section header for a list of passkeys." }, "passwords": { - "message": "Passwords", + "message": "Паролі", "description": "A section header for a list of passwords." }, "logInWithPasskeyAriaLabel": { @@ -4277,7 +4359,7 @@ "description": "ARIA label for the inline menu button that logs in with a passkey." }, "assign": { - "message": "Assign" + "message": "Прызначыць" }, "bulkCollectionAssignmentDialogDescriptionSingular": { "message": "Only organization members with access to these collections will be able to see the item." @@ -4298,16 +4380,16 @@ } }, "addField": { - "message": "Add field" + "message": "Дадаць поле" }, "add": { - "message": "Add" + "message": "Дадаць" }, "fieldType": { - "message": "Field type" + "message": "Тып поля" }, "fieldLabel": { - "message": "Field label" + "message": "Назва поля" }, "textHelpText": { "message": "Use text fields for data like security questions" @@ -4325,10 +4407,10 @@ "message": "Enter the the field's html id, name, aria-label, or placeholder." }, "editField": { - "message": "Edit field" + "message": "Рэдагаваць поле" }, "editFieldLabel": { - "message": "Edit $LABEL$", + "message": "Рэдагаваць $LABEL$", "placeholders": { "label": { "content": "$1", @@ -4337,7 +4419,7 @@ } }, "deleteCustomField": { - "message": "Delete $LABEL$", + "message": "Выдалісь $LABEL$", "placeholders": { "label": { "content": "$1", @@ -4421,10 +4503,10 @@ "message": "Successfully assigned collections" }, "nothingSelected": { - "message": "You have not selected anything." + "message": "Вы пакуль нічога не выбралі." }, "movedItemsToOrg": { - "message": "Selected items moved to $ORGNAME$", + "message": "Выбраныя элементы перамешчаны ў $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Прагал", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Тыльда", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Карэтка", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Зорачка", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Левая дужка", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Правая дужка", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Падкрэсленне", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Злучок", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Плюс", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Левая фігурная дужка", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Правая фігурная дужка", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Левая квадратная дужка", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Правая квадратная дужка", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Раздзяляльнік", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Двукроп'е", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Кропка з коскай", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Падвойнае двукоссе", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Адзінарнае двукоссе", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Менш", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Больш", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Коска", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Кропка", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Пытальнік", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Малыя літары" + }, + "uppercaseAriaLabel": { + "message": "Вялікія літары" + }, + "generatedPassword": { + "message": "Згенерыраваны пароль" } } diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 8400a142403..626221f06d1 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Създаване на акаунт" }, + "newToBitwarden": { + "message": "За пръв път ли ползвате Битуорден?" + }, + "logInWithPasskey": { + "message": "Вписване със секретен ключ" + }, + "useSingleSignOn": { + "message": "Използване на еднократна идентификация" + }, + "welcomeBack": { + "message": "Добре дошли отново" + }, "setAStrongPassword": { "message": "Използвайте сложна парола" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Присъединяване към организацията" }, + "joinOrganizationName": { + "message": "Присъединяване към $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Завършете присъединяването си към тази организация като зададете главна парола." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Копиране на паролата" }, + "copyPassphrase": { + "message": "Копиране на паролата-фраза" + }, "copyNote": { "message": "Копиране на бележката" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Копиране на бележките" }, + "fill": { + "message": "Попълване", + "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": "Автоматично дописване" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Нова парола" }, + "generatePassphrase": { + "message": "Генериране на парола-фраза" + }, "regeneratePassword": { "message": "Регенериране на паролата" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Посещаване на уеб сайта" }, + "launchWebsiteName": { + "message": "Отваряне на уеб сайта $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Сайт" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Вписване" }, + "logInToBitwarden": { + "message": "Впишете се в Битуорден" + }, "restartRegistration": { "message": "Рестартиране на регистрацията" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "Адрес на сървъра" }, + "selfHostBaseUrl": { + "message": "Адрес на собствения сървър", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "Адрес на ППИ-сървъра" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Показване на предложения за авт. попълване на полетата във формуляри" }, + "showInlineMenuIdentitiesLabel": { + "message": "Показване на идентичности като предложения" + }, + "showInlineMenuCardsLabel": { + "message": "Показване на карти като предложения" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Показване на предложения когато иконката е избрана" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Хронология на паролата" }, + "generatorHistory": { + "message": "История на генерирането" + }, + "clearGeneratorHistoryTitle": { + "message": "Изчистване на историята на генериране" + }, + "cleargGeneratorHistoryDescription": { + "message": "Ако продължите, всички записи в историята на генериране ще бъдат изтрити завинаги. Наистина ли искате това?" + }, "back": { "message": "Назад" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Изчистване на историята" }, - "noPasswordsToShow": { - "message": "Няма пароли за показване" + "nothingToShow": { + "message": "Няма нищо за показване" }, - "noRecentlyGeneratedPassword": { - "message": "Скоро не сте генерирали пароли" + "nothingGeneratedRecently": { + "message": "Скоро не сте генерирали нищо" }, "remove": { "message": "Премахване" @@ -2449,8 +2511,8 @@ "message": "Изискване на парола за достъп до това изпращане.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Изискване на тази парола за преглеждане на Изпращането.", + "sendPasswordDescV3": { + "message": "Добавете незадължителна парола, с която получателите да имат достъп до това Изпращане.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Организацията Ви изисква да зададете главна парола.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "от $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Изисква се потвърждение", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Генериране на потр. име" }, + "generateEmail": { + "message": "Генериране на електронна поща" + }, "usernameType": { "message": "Тип потребителско име" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Създайте псевдоним на е-поща с външна услуга за препращане." }, + "forwarderDomainName": { + "message": "Домейн на електронната поща", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Изберете домейн, който се поддържа от избраната услуга", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "Грешка от $SERVICENAME$: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Местоположение на елемента" }, + "fileSend": { + "message": "Файлово изпращане" + }, "fileSends": { "message": "Файлови изпращания" }, + "textSend": { + "message": "Текстово изпращане" + }, "textSends": { "message": "Текстови изпращания" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Удостоверяване" + }, + "fillGeneratedPassword": { + "message": "Попълване на генерираната парола", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Паролата е прегенерирана", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Запазване на данните за вписване в Битуорден?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Интервал", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Вълничка", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Удивителен знак", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "Кльомба", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Диез", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Знак за долар", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Процент", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Колибка", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Амперсанд", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Звездичка", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Лява скоба", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Дясна скоба", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Долна черта", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Тире", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Плюс", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Равно", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Лява фигурна скоба", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Дясна фигурна скоба", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Лява квадратна скоба", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Дясна квадратна скоба", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Вертикална черта", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Обратна наклонена черта", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Двоеточие", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Точка и запетая", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Двойна кавичка", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Единична кавичка", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "По-малко", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "По-голямо", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Запетая", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Точка", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Въпросителен знак", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Наклонена черта", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Малки букви" + }, + "uppercaseAriaLabel": { + "message": "Главни букви" + }, + "generatedPassword": { + "message": "Генерирана парола" } } diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index b65edc673f2..b808ab5eb62 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "অ্যাকাউন্ট তৈরি করুন" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Set a strong password" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Join organization" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organization by setting a master password." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "পাসওয়ার্ড অনুলিপিত করুন" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "নোট অনুলিপিত করুন" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Copy notes" }, + "fill": { + "message": "Fill", + "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": "স্বতঃপূরণ" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "পাসওয়ার্ড তৈরি করুন" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "পাসওয়ার্ড পুনঃতৈরি করুন" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Launch website" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "ওয়েবসাইট" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Log in" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Restart registration" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "সার্ভার URL" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "এপিআই সার্ভার URL" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "পাসওয়ার্ড ইতিহাস" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "পেছন" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Clear history" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "সরান" @@ -2449,8 +2511,8 @@ "message": "Optionally require a password for users to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Your organization requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Verification required", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Generate username" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Username type" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Generate an email alias with an external forwarding service." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ error: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index a7451706dbf..2793b4dee62 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Napravi račun" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Set a strong password" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Join organization" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organization by setting a master password." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Copy password" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "Copy note" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Copy notes" }, + "fill": { + "message": "Fill", + "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": "Autofill" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Generate password" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Launch website" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Website" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Log in" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Restart registration" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "Server URL" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "API server URL" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Password history" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "Back" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Clear history" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "Remove" @@ -2449,8 +2511,8 @@ "message": "Optionally require a password for users to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Your organization requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Verification required", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Generate username" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Username type" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Generate an email alias with an external forwarding service." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ error: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index 13aa585d70e..eee9e4956ce 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Crea un compte" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Estableix una contrasenya segura" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Uneix-te a l'organització" }, + "joinOrganizationName": { + "message": "Uniu-vos a $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Acabeu d'unir-vos a aquesta organització establint una contrasenya mestra." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Copia contrasenya" }, + "copyPassphrase": { + "message": "Copia clau de pas" + }, "copyNote": { "message": "Copia nota" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Copia notes" }, + "fill": { + "message": "Fill", + "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": "Emplenament automàtic" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Genera contrasenya" }, + "generatePassphrase": { + "message": "Genera clau de pas" + }, "regeneratePassword": { "message": "Regenera contrasenya" }, @@ -431,7 +462,7 @@ "description": "Card header for password generator include block" }, "uppercaseDescription": { - "message": "Include uppercase characters", + "message": "Inclou majúscules", "description": "Tooltip for the password generator uppercase character checkbox" }, "uppercaseLabel": { @@ -439,7 +470,7 @@ "description": "Label for the password generator uppercase character checkbox" }, "lowercaseDescription": { - "message": "Include lowercase characters", + "message": "Inclou minúscules", "description": "Full description for the password generator lowercase character checkbox" }, "lowercaseLabel": { @@ -447,7 +478,7 @@ "description": "Label for the password generator lowercase character checkbox" }, "numbersDescription": { - "message": "Include numbers", + "message": "Inclou números", "description": "Full description for the password generator numbers checkbox" }, "numbersLabel": { @@ -455,7 +486,7 @@ "description": "Label for the password generator numbers checkbox" }, "specialCharactersDescription": { - "message": "Include special characters", + "message": "Inclou caràcters especials", "description": "Full description for the password generator special characters checkbox" }, "specialCharactersLabel": { @@ -486,7 +517,7 @@ "description": "deprecated. Use avoidAmbiguous instead." }, "avoidAmbiguous": { - "message": "Avoid ambiguous characters", + "message": "Eviteu caràcters ambigus", "description": "Label for the avoid ambiguous characters checkbox." }, "generatorPolicyInEffect": { @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Obri la web" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Lloc web" }, @@ -584,7 +624,7 @@ "message": "Temps de sessió" }, "vaultTimeoutHeader": { - "message": "Vault timeout" + "message": "Temps d'espera de la caixa forta" }, "otherOptions": { "message": "Altres opcions" @@ -605,13 +645,13 @@ "message": "La caixa forta està bloquejada. Comproveu la contrasenya mestra per continuar." }, "yourVaultIsLockedV2": { - "message": "Your vault is locked" + "message": "La caixa forta està bloquejada" }, "yourAccountIsLocked": { - "message": "Your account is locked" + "message": "El compte està bloquejat" }, "or": { - "message": "or" + "message": "o" }, "unlock": { "message": "Desbloqueja" @@ -805,6 +845,9 @@ "logIn": { "message": "Inicia sessió" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Restart registration" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "URL del servidor" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "URL del servidor API" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Historial de les contrasenyes" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "Arrere" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Clear history" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "Suprimeix" @@ -2449,8 +2511,8 @@ "message": "Opcionalment, necessiteu una contrasenya perquè els usuaris accedisquen a aquest Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "La vostra organització requereix que establiu una contrasenya mestra.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Es requereix verificació", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Genera un nom d'usuari" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Tipus de nom d'usuari" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Genera un àlies de correu electrònic amb un servei de reenviament extern." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "Error de $SERVICENAME$: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index 98d7336e987..80cce02f219 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Vytvořit účet" }, + "newToBitwarden": { + "message": "Jste noví na Bitwardenu?" + }, + "logInWithPasskey": { + "message": "Přihlásit se pomocí přístupového klíče" + }, + "useSingleSignOn": { + "message": "Použít jednotné přihlášení" + }, + "welcomeBack": { + "message": "Vítejte zpět" + }, "setAStrongPassword": { "message": "Nastavit hlavní heslo" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Přidat se k organizaci" }, + "joinOrganizationName": { + "message": "Připojit se k $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Dokončete připojení k této organizaci nastavením hlavního hesla." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Kopírovat heslo" }, + "copyPassphrase": { + "message": "Kopírovat heslovou frázi" + }, "copyNote": { "message": "Kopírovat poznámku" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Kopírovat poznámky" }, + "fill": { + "message": "Vyplnit", + "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": "Automatické vyplňování" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Vygenerovat heslo" }, + "generatePassphrase": { + "message": "Vygenerovat heslovou frázi" + }, "regeneratePassword": { "message": "Vygenerovat jiné heslo" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Otevřít webovou stránku" }, + "launchWebsiteName": { + "message": "Spustit web $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Webová stránka" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Přihlásit se" }, + "logInToBitwarden": { + "message": "Přihlásit se do Bitwardenu" + }, "restartRegistration": { "message": "Restartovat registraci" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "URL serveru" }, + "selfHostBaseUrl": { + "message": "Adresa URL serveru vlastního hostování", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "URL API serveru" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Zobrazit návrhy automatického vyplňování v polích formuláře" }, + "showInlineMenuIdentitiesLabel": { + "message": "Zobrazit identity jako návrhy" + }, + "showInlineMenuCardsLabel": { + "message": "Zobrazit karty jako návrhy" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Zobrazit návrhy, když je vybrána ikona" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Historie hesel" }, + "generatorHistory": { + "message": "Historie generátoru" + }, + "clearGeneratorHistoryTitle": { + "message": "Vymazat historii generátoru" + }, + "cleargGeneratorHistoryDescription": { + "message": "Pokud budete pokračovat, všechny položky budou trvale smazány z historie generátoru. Jste si jisti, že chcete pokračovat?" + }, "back": { "message": "Zpět" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Vymazat historii" }, - "noPasswordsToShow": { - "message": "Žádná hesla k zobrazení" + "nothingToShow": { + "message": "Nic k zobrazení" }, - "noRecentlyGeneratedPassword": { - "message": "Nedávno jste nevygenerovali heslo" + "nothingGeneratedRecently": { + "message": "Nedávno jste nic nevygenerovali" }, "remove": { "message": "Odebrat" @@ -2449,8 +2511,8 @@ "message": "Volitelně vyžadovat heslo pro přístup k tomuto Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Pro zobrazení Send bude vyžadováno toto heslo.", + "sendPasswordDescV3": { + "message": "Přidá volitelné heslo pro příjemce pro přístup k tomuto Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Vaše organizace vyžaduje nastavení hlavního hesla.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "z $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Je vyžadováno ověření", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Vygenerovat uživatelské jméno" }, + "generateEmail": { + "message": "Vygenerovat e-mail" + }, "usernameType": { "message": "Typ uživatelského jména" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Vygenerovat e-mailový alias pomocí externí služby pro přesměrování." }, + "forwarderDomainName": { + "message": "E-mailová doména", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Vyberte doménu, která je podporována vybranou službou", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "Chyba $SERVICENAME$: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Umístění položky" }, + "fileSend": { + "message": "Send souboru" + }, "fileSends": { "message": "Sends se soubory" }, + "textSend": { + "message": "Send textu" + }, "textSends": { "message": "Sends s texty" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Ověřování" + }, + "fillGeneratedPassword": { + "message": "Vyplnit vygenerované heslo", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Heslo bylo znovu vygenerováno", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Uložit přihlášení do Bitwardenu?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Mezera", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilda", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Zpětný apostrof", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Vykřičník", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "Zavináč", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Mřížka", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dolar", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Procento", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Stříška", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Hvězdička", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Levá závorka", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Pravá závorka", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Podtržítko", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Spojovník", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Rovnítko", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Levá složená závorka", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Pravá složená závorka", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Levá hranatá závorka", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Pravá hranatá závorka", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Svislá čára", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Zpětné lomítko", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Dvojtečka", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Středník", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Dvojitá uvozovka", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Apostrof", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Menší než", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Větší než", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Čárka", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Tečka", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Otazník", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Lomítko", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Všechna malá písmena" + }, + "uppercaseAriaLabel": { + "message": "Všechna velká písmena" + }, + "generatedPassword": { + "message": "Vygenerované heslo" } } diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index ba8b5fec1c8..13a75b413f7 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Creu cyfrif" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Gosod cyfrinair cryf" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Join organization" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organization by setting a master password." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Copïo cyfrinair" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "Copïo'r nodyn" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Copy notes" }, + "fill": { + "message": "Fill", + "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": "Llenwi'n awtomatig" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Cynhyrchu cyfrinair" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "Ailgynhyrchu cyfrinair" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Launch website" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Gwefan" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Log in" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Restart registration" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "Server URL" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "API server URL" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Hanes cyfrineiriau" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "Yn ôl" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Clear history" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "Tynnu" @@ -2449,8 +2511,8 @@ "message": "Optionally require a password for users to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Your organization requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Verification required", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Cynhyrchu enw defnyddiwr" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Math o enw defnyddiwr" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Generate an email alias with an external forwarding service." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ error: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index 54c1bc41627..437a3fa7df4 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Opret konto" }, + "newToBitwarden": { + "message": "Ny på Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log ind med adgangsnøgle" + }, + "useSingleSignOn": { + "message": "Brug Single Sign-On" + }, + "welcomeBack": { + "message": "Velkommen tilbage" + }, "setAStrongPassword": { "message": "Indstil en stærk adgangskode" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Bliv medlem af organisation" }, + "joinOrganizationName": { + "message": "Bliv nedlem af $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Færdiggør tilmeldingen til denne organisation ved at opsætte en hovedadgangskode." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Kopiér adgangskode" }, + "copyPassphrase": { + "message": "Kopiér adgangssætning" + }, "copyNote": { "message": "Kopiér notat" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Kopiér notater" }, + "fill": { + "message": "Udfyld", + "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": "Autoudfyld" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Generér adgangskode" }, + "generatePassphrase": { + "message": "Generér adgangssætning" + }, "regeneratePassword": { "message": "Regenerér adgangskode" }, @@ -536,7 +567,7 @@ "message": "Notater" }, "privateNote": { - "message": "Private note" + "message": "Privat notat" }, "note": { "message": "Notat" @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Åbn websted" }, + "launchWebsiteName": { + "message": "Åbn webstedet $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Hjemmeside" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Log ind" }, + "logInToBitwarden": { + "message": "Log ind på Bitwarden" + }, "restartRegistration": { "message": "Genstart registrering" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "Server URL" }, + "selfHostBaseUrl": { + "message": "URL til selv-hostet server", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "API-server URL" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Vis autoudfyld-menu i formularfelter" }, + "showInlineMenuIdentitiesLabel": { + "message": "Vis identiteter som forslag" + }, + "showInlineMenuCardsLabel": { + "message": "Vis kort som forslag" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Vis forslag, når ikonet vælges" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Adgangskodehistorik" }, + "generatorHistory": { + "message": "Generatorhistorik" + }, + "clearGeneratorHistoryTitle": { + "message": "Ryd generatorhistorik" + }, + "cleargGeneratorHistoryDescription": { + "message": "Fortsættes, slettes alle poster permanent fra generatorens historik. Sikker på, at handlingen skal udføres?" + }, "back": { "message": "Tilbage" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Ryd historik" }, - "noPasswordsToShow": { - "message": "Ingen adgangskoder at vise" + "nothingToShow": { + "message": "Intet at vise" }, - "noRecentlyGeneratedPassword": { - "message": "Der er ikke genereret nogen adgangskode for nylig" + "nothingGeneratedRecently": { + "message": "Intet genereret for nylig" }, "remove": { "message": "Fjern" @@ -2287,14 +2349,14 @@ "message": "Ekskluderet domæne-ændringer gemt" }, "limitSendViews": { - "message": "Limit views" + "message": "Begræns visninger" }, "limitSendViewsHint": { - "message": "No one can view this Send after the limit is reached.", + "message": "Ingen kan se denne Send efter grænsen er nået.", "description": "Displayed under the limit views field on Send" }, "limitSendViewsCount": { - "message": "$ACCESSCOUNT$ views left", + "message": "$ACCESSCOUNT$ visninger tilbage", "description": "Displayed under the limit views field on Send", "placeholders": { "accessCount": { @@ -2449,8 +2511,8 @@ "message": "Valgfrit brugeradgangskodekrav for at tilgå denne Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Tilføj en valgfri adgangskode til modtagere for adgang til denne Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2539,7 +2601,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendFilePopoutDialogDesc": { - "message": "For at oprette en fil-Send, skal udvidelsen poppes ud til et nyt vindue.", + "message": "For at oprette en fil-Send, skal udvidelsen poppes ud i et nyt vindue.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendLinuxChromiumFileWarning": { @@ -2588,7 +2650,7 @@ "message": "Skjul min e-mailadresse for modtagere." }, "hideYourEmail": { - "message": "Hide your email address from viewers." + "message": "Skjul e-mailadressen for modtagere." }, "sendOptionsPolicyInEffect": { "message": "Én eller flere organisationspolitikker påvirker dine Send-valgmuligheder." @@ -2647,6 +2709,15 @@ "message": "Organisationen kræver, at der oprettes en hovedadgangskode.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "ud af $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Bekræftelse kræves", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Generér brugernavn" }, + "generateEmail": { + "message": "Generér e-mail" + }, "usernameType": { "message": "Brugernavnstype" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Generér et e-mail alias med en ekstern viderestillingstjeneste." }, + "forwarderDomainName": { + "message": "E-maildomæne", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Vælg et domæne understøttet af den valgte tjeneste", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$-fejl: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Emneplacering" }, + "fileSend": { + "message": "Fil Send" + }, "fileSends": { "message": "Fil-Sends" }, + "textSend": { + "message": "Tekst Send" + }, "textSends": { "message": "Tekst-Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Godkender" + }, + "fillGeneratedPassword": { + "message": "Udfyld genereret adgangskode", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Adgangskode genereret igen", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Gem login til Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Mellemrum", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Accent grave", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Udråbstegn", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "Snabel-a", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash-tegn", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar-tegn", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Procenttegn", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Cirkumfleks", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Og-tegn", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Stjerne", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Venstre parentes", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Højre parentes", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Understregning", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Bindestreg", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Lighedstegn", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Venstre tuborg", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Højre tuborg", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Venstre klamme", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Højre klamme", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Lodret streg", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Omvendt skråstreg", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Kolon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semikolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Dobbelt anførselstegn", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Enkelt anførselstegn", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Mindre end", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Større end", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Komma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Punktum", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Spørgsmålstegn", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Skråstreg", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Minuskel" + }, + "uppercaseAriaLabel": { + "message": "Majuskel" + }, + "generatedPassword": { + "message": "Genereret adgangskode" } } diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index b61645180c4..1e3a38ffc7b 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Konto erstellen" }, + "newToBitwarden": { + "message": "Neu bei Bitwarden?" + }, + "logInWithPasskey": { + "message": "Mit Passkey anmelden" + }, + "useSingleSignOn": { + "message": "Single Sign-on verwenden" + }, + "welcomeBack": { + "message": "Willkommen zurück" + }, "setAStrongPassword": { "message": "Lege ein starkes Passwort fest" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Organisation beitreten" }, + "joinOrganizationName": { + "message": "$ORGANIZATIONNAME$ beitreten", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Schließe den Beitritt zu dieser Organisation ab, indem du ein Master-Passwort festlegst." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Passwort kopieren" }, + "copyPassphrase": { + "message": "Passphrase kopieren" + }, "copyNote": { "message": "Notiz kopieren" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Notizen kopieren" }, + "fill": { + "message": "Ausfüllen", + "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": "Auto-Ausfüllen" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Passwort generieren" }, + "generatePassphrase": { + "message": "Passphrase generieren" + }, "regeneratePassword": { "message": "Passwort neu generieren" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Website öffnen" }, + "launchWebsiteName": { + "message": "Website aufrufen: $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Webseite" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Anmelden" }, + "logInToBitwarden": { + "message": "Bei Bitwarden anmelden" + }, "restartRegistration": { "message": "Registrierung neu starten" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "Server URL" }, + "selfHostBaseUrl": { + "message": "Selbst gehostete Server-URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "API Server-URL" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Vorschläge zum Auto-Ausfüllen in Formularfeldern anzeigen" }, + "showInlineMenuIdentitiesLabel": { + "message": "Identitäten als Vorschläge anzeigen" + }, + "showInlineMenuCardsLabel": { + "message": "Karten als Vorschläge anzeigen" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Vorschläge anzeigen, wenn Symbol ausgewählt ist" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Passwortverlauf" }, + "generatorHistory": { + "message": "Generator-Verlauf" + }, + "clearGeneratorHistoryTitle": { + "message": "Generator-Verlauf löschen" + }, + "cleargGeneratorHistoryDescription": { + "message": "Wenn du fortfährst, werden alle Einträge dauerhaft aus dem Generator-Verlauf gelöscht. Bist du sicher, dass du fortfahren möchtest?" + }, "back": { "message": "Zurück" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Verlauf löschen" }, - "noPasswordsToShow": { - "message": "Keine Passwörter zum Anzeigen" + "nothingToShow": { + "message": "Nichts anzuzeigen" }, - "noRecentlyGeneratedPassword": { - "message": "Du hast in letzter Zeit kein Passwort generiert" + "nothingGeneratedRecently": { + "message": "Du hast in letzter Zeit nichts generiert" }, "remove": { "message": "Entfernen" @@ -2449,8 +2511,8 @@ "message": "Optional ein Passwort verlangen, damit Benutzer auf dieses Send zugreifen können.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Dieses Passwort zum Anzeigen des Sends verlangen.", + "sendPasswordDescV3": { + "message": "Füge ein optionales Passwort hinzu, mit dem Empfänger auf dieses Send zugreifen können.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Deine Organisation verlangt, dass du ein Master-Passwort festlegen musst.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "von $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Verifizierung erforderlich", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Benutzername generieren" }, + "generateEmail": { + "message": "E-Mail generieren" + }, "usernameType": { "message": "Benutzernamenstyp" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Generiere ein E-Mail-Alias mit einem externen Weiterleitungsdienst." }, + "forwarderDomainName": { + "message": "E-Mail-Domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Wähle eine Domain aus, die vom ausgewählten Dienst unterstützt wird", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ Fehler: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Eintrags-Standort" }, + "fileSend": { + "message": "Datei-Send" + }, "fileSends": { "message": "Datei-Sends" }, + "textSend": { + "message": "Text-Send" + }, "textSends": { "message": "Text-Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authentifizierung" + }, + "fillGeneratedPassword": { + "message": "Generiertes Passwort ausfüllen", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Passwort neu generiert", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Zugangsdaten in Bitwarden speichern?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Leerzeichen", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Gravis", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Ausrufezeichen", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At-Zeichen", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Raute", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollarzeichen", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Prozentzeichen", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Zirkumflex", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Sternzeichen", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Linke Klammer", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Rechte Klammer", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Unterstrich", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Bindestrich", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Gleich", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Linke geschweifte Klammer", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Rechte geschweifte Klammer", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Linke eckige Klammer", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Rechte eckige Klammer", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Senkrechter Strich", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Rückstrich", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Doppelpunkt", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semikolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Doppelte Anführungszeichen", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Einfaches Anführungszeichen", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Kleiner als", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Größer als", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Komma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Punkt", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Fragezeichen", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Schrägstrich", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Kleinbuchstaben" + }, + "uppercaseAriaLabel": { + "message": "Großbuchstaben" + }, + "generatedPassword": { + "message": "Generiertes Passwort" } } diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 11f57667b4f..c2bc58519cc 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -3,7 +3,7 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden: Διαχείριση κωδικών πρόσβασης", + "message": "Διαχειριστής Κωδικών Πρόσβασης Bitwarden", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { @@ -19,6 +19,18 @@ "createAccount": { "message": "Δημιουργία λογαριασμού" }, + "newToBitwarden": { + "message": "Νέος/α στο Bitwarden;" + }, + "logInWithPasskey": { + "message": "Σύνδεση με κλειδί πρόσβασης" + }, + "useSingleSignOn": { + "message": "Χρήση ενιαίας σύνδεσης" + }, + "welcomeBack": { + "message": "Καλωσορίσατε και πάλι" + }, "setAStrongPassword": { "message": "Ορίστε έναν ισχυρό κωδικό πρόσβασης" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Συμμετοχή στον οργανισμό" }, + "joinOrganizationName": { + "message": "Συμμετοχή στο $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Ολοκληρώστε τη συμμετοχή σας σε αυτόν τον οργανισμό ορίζοντας έναν κύριο κωδικό πρόσβασης." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Αντιγραφή κωδικού πρόσβασης" }, + "copyPassphrase": { + "message": "Αντιγραφή φράσης πρόσβασης" + }, "copyNote": { "message": "Αντιγραφή σημείωσης" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Αντιγραφή σημειώσεων" }, + "fill": { + "message": "Συμπλήρωση", + "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": "Αυτόματη συμπλήρωση" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Δημιουργία κωδικού πρόσβασης" }, + "generatePassphrase": { + "message": "Δημιουργία φράσης πρόσβασης" + }, "regeneratePassword": { "message": "Επαναδημιουργία κωδικού πρόσβασης" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Εκκίνηση ιστοσελίδας" }, + "launchWebsiteName": { + "message": "Εκκίνηση ιστοσελίδας $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Ιστοσελίδα" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Σύνδεση" }, + "logInToBitwarden": { + "message": "Σύνδεση στο Bitwarden" + }, "restartRegistration": { "message": "Επανεκκίνηση εγγραφής" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "URL Διακομιστή" }, + "selfHostBaseUrl": { + "message": "URL διακομιστή αυτο-φιλοξενίας", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "URL διακομιστή API" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Εμφάνιση μενού αυτόματης συμπλήρωσης στα πεδία της φόρμας" }, + "showInlineMenuIdentitiesLabel": { + "message": "Εμφάνιση ταυτοτήτων ως προτάσεις" + }, + "showInlineMenuCardsLabel": { + "message": "Εμφάνιση καρτών ως προτάσεις" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Εμφάνιση προτάσεων όταν το εικονίδιο είναι επιλεγμένο" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Ιστορικό κωδικού πρόσβασης" }, + "generatorHistory": { + "message": "Ιστορικό γεννήτριας" + }, + "clearGeneratorHistoryTitle": { + "message": "Εκκαθάριση ιστορικού γεννήτριας" + }, + "cleargGeneratorHistoryDescription": { + "message": "Αν συνεχίσετε, όλες οι καταχωρήσεις θα διαγραφούν οριστικά από το ιστορικό της γεννήτριας. Είστε σίγουροι ότι θέλετε να συνεχίσετε;" + }, "back": { "message": "Πίσω" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Διαγραφή ιστορικού" }, - "noPasswordsToShow": { - "message": "Δεν υπάρχουν κωδικοί πρόσβασης για εμφάνιση" + "nothingToShow": { + "message": "Τίποτα για προβολή" }, - "noRecentlyGeneratedPassword": { - "message": "Δεν έχετε δημιουργήσει πρόσφατα έναν κωδικό πρόσβασης" + "nothingGeneratedRecently": { + "message": "Δεν έχετε δημιουργήσει τίποτα πρόσφατα" }, "remove": { "message": "Αφαίρεση" @@ -2449,8 +2511,8 @@ "message": "Προαιρετικά απαιτείται κωδικός πρόσβασης για τους χρήστες για να έχουν πρόσβαση σε αυτό το Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Να απαιτείται αυτός ο κωδικός πρόσβασης για την προβολή του Send.", + "sendPasswordDescV3": { + "message": "Προσθέστε έναν προαιρετικό κωδικό πρόσβασης για τους παραλήπτες για πρόσβαση σε αυτό το Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Ο οργανισμός σας απαιτεί να ορίσετε έναν κύριο κωδικό πρόσβασης.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "από $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Απαιτείται επαλήθευση", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Δημιουργία ονόματος χρήστη" }, + "generateEmail": { + "message": "Δημιουργία email" + }, "usernameType": { "message": "Τύπος ονόματος χρήστη" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Δημιουργήστε ένα alias email με μια εξωτερική υπηρεσία προώθησης." }, + "forwarderDomainName": { + "message": "Τομέας ηλ. ταχυδρομείου", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Επιλέξτε ένα τομέα που υποστηρίζεται από την επιλεγμένη υπηρεσία", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ σφάλμα: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Τοποθεσία Αντικειμένου" }, + "fileSend": { + "message": "Send αρχείου" + }, "fileSends": { "message": "Send αρχείων" }, + "textSend": { + "message": "Send κειμένου" + }, "textSends": { "message": "Send κειμένων" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Ταυτοποίηση" + }, + "fillGeneratedPassword": { + "message": "Συμπληρώση του δημιουργημένου κωδικού πρόσβασης", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Ο κωδικός επαναδημιουργήθηκε", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Αποθήκευση σύνδεσης στο Bitwarden;", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Κενό", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Περισπωμένη", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Θαυμαστικό", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "Παπάκι", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Δίεση", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Δολάριο", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Τοις εκατό", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Αστερίσκος", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Αριστερή παρένθεση", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Δεξιά παρένθεση", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Κάτω παύλα", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Παύλα", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Συν", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Ίσον", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Αριστερό άγκιστρο", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Δεξί άγκιστρο", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Αριστερή αγκύλη", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Δεξιά αγκύλη", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Άνω κάτω τελεία", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Λιγότερο από", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Μεγαλύτερο από", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Τελεία", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Αγγλικό ερωτηματικό", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Πεζά" + }, + "uppercaseAriaLabel": { + "message": "Κεφαλαία" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 290663f4347..92fabdae3ee 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Create account" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Set a strong password" }, @@ -107,6 +119,9 @@ "copyPassword": { "message": "Copy password" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "Copy note" }, @@ -152,6 +167,10 @@ "copyNotes": { "message": "Copy notes" }, + "fill":{ + "message": "Fill", + "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": "Autofill" }, @@ -407,6 +426,9 @@ "generatePassword": { "message": "Generate password" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -568,6 +590,15 @@ "launchWebsite": { "message": "Launch website" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Website" }, @@ -814,6 +845,9 @@ "logIn": { "message": "Log in" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Restart registration" }, @@ -1389,6 +1423,10 @@ "baseUrl": { "message": "Server URL" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "API server URL" }, @@ -1756,6 +1794,15 @@ "passwordHistory": { "message": "Password history" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "Back" }, @@ -1872,11 +1919,11 @@ "clearHistory": { "message": "Clear history" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "Remove" @@ -2464,8 +2511,8 @@ "message": "Optionally require a password for users to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2827,6 +2874,23 @@ "generateUsername": { "message": "Generate username" }, + "generateEmail": { + "message": "Generate email" + }, + "generatorBoundariesHint": { + "message": "Value must be between $MIN$ and $MAX$", + "description": "Explains spin box minimum and maximum values to the user", + "placeholders": { + "min": { + "content": "$1", + "example": "8" + }, + "max": { + "content": "$2", + "example": "128" + } + } + }, "usernameType": { "message": "Username type" }, @@ -2867,6 +2931,14 @@ "forwardedEmailDesc": { "message": "Generate an email alias with an external forwarding service." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ error: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -3162,9 +3234,6 @@ } } }, - "loggingInOn": { - "message": "Logging in on" - }, "opensInANewWindow": { "message": "Opens in a new window" }, @@ -4494,9 +4563,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4559,5 +4634,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index fe295c7c249..71161b14e5c 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Create account" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Set a strong password" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Join organisation" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organisation by setting a master password." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Copy password" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "Copy note" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Copy notes" }, + "fill": { + "message": "Fill", + "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": "Auto-fill" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Generate password" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Launch website" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Website" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Log in" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Restart registration" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "Server URL" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "API server URL" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Password history" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "Back" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Clear history" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "Remove" @@ -2449,8 +2511,8 @@ "message": "Optionally require a password for users to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Your organisation requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Verification required", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Generate username" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Username type" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Generate an email alias with an external forwarding service." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ error: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Full stop", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 0e938644fdd..4265ffa6964 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Create account" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Set a strong password" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Join organisation" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organisation by setting a master password." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Copy password" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "Copy note" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Copy notes" }, + "fill": { + "message": "Fill", + "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": "Auto-fill" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Generate password" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Launch website" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Website" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Log in" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Restart registration" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "Server URL" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "API server URL" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Password history" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "Back" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Clear history" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "Remove" @@ -2449,8 +2511,8 @@ "message": "Optionally require a password for users to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Your organisation requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Verification required", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Generate Username" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Username Type" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Generate an email alias with an external forwarding service." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ error: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index 4884a76ba58..5c7e82f89ba 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Crear cuenta" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Establece una contraseña fuerte" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Incorporarse a la organización" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Termine de unirse a esta organización estableciendo una contraseña maestra." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Copiar contraseña" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "Copiar nota" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Copy notes" }, + "fill": { + "message": "Fill", + "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" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Generar contraseña" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "Regenerar contraseña" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Iniciar página web" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Web" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Acceder" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Reiniciar registro" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "URL del servidor" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "URL del servidor de la API" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Historial de contraseñas" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "Atrás" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Clear history" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "Eliminar" @@ -2449,8 +2511,8 @@ "message": "Opcionalmente se requiere una contraseña para que los usuarios accedan a este Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Su organización requiere que establezca una contraseña maestra.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Se requiere verificación", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Generar nombre de usuario" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Tipo de nombre de usuario" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Genera un alias de correo electrónico con un servicio de reenvío externo." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ error: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Ubicación del elemento" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index b8121aa2de9..38543a88842 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Konto loomine" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Määra tugev parool" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Liitu organisatsiooniga" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Lõpeta organisatsiooniga liitumine määrates ülemparool." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Kopeeri parool" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "Kopeeri märkus" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Copy notes" }, + "fill": { + "message": "Fill", + "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": "Automaatne täitmine" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Loo parool" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "Genereeri parool uuesti" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Ava Veebileht" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Veebileht" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Logi sisse" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Alusta registreerimist uuesti" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "Serveri URL" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "API serveri URL" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Paroolide ajalugu" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "Tagasi" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Clear history" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "Eemalda" @@ -2449,8 +2511,8 @@ "message": "Soovi korral nõua parooli, millega Sendile ligi pääseb.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Your organization requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Verification required", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Genereeri kasutajanimi" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Kasutajanime tüüp" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Genereeri e-posti alias, kasutades selleks välist teenuspakkujat." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ error: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 6563a5397d2..3bdc49f61ad 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Sortu kontua" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Set a strong password" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Join organization" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organization by setting a master password." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Kopiatu pasahitza" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "Kopiatu oharra" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Copy notes" }, + "fill": { + "message": "Fill", + "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": "Auto-betetzea" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Sortu pasahitza" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "Berrezarri pasahitza" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Launch website" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Webgunea" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Log in" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Restart registration" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "Zerbitzariaren URL-a" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "API zerbitzariaren URL-a" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Pasahitz historia" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "Itzuli" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Clear history" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "Ezabatu" @@ -2449,8 +2511,8 @@ "message": "Nahi izanez gero, pasahitza eskatu erabiltzaileak Send honetara sar daitezen.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Your organization requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Verification required", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Sortu erabiltzaile izena" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Erabiltzaile izen mota" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Emaileko ezizen bat sortu kanpoko bidalketa zerbitzu batekin." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ error: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index 93dd5f98200..94c420155e7 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "ایجاد حساب کاربری" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "تنظیم رمز عبور قوی" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Join organization" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organization by setting a master password." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "کپی کلمه عبور" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "کپی یادداشت" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Copy notes" }, + "fill": { + "message": "Fill", + "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": "پر کردن خودکار" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "تولید کلمه عبور" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "تولید مجدد کلمه عبور" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Launch website" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "وب‌سایت" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Log in" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Restart registration" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "نشانی اینترنتی سرور" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "نشانی API سرور" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "تاریخچه کلمه عبور" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "بازگشت" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Clear history" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "حذف" @@ -2449,8 +2511,8 @@ "message": "به صورت اختیاری برای دسترسی کاربران به این ارسال به یک کلمه عبور نیاز دارید.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Your organization requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "تایید لازم است", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "ایجاد نام کاربری" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "نوع نام کاربری" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "یک نام مستعار ایمیل با یک سرویس ارسال خارجی ایجاد کنید." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ error: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 801926eb345..3d0fe6ea64d 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Luo tili" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Aseta vahva salasana" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Liity organisaatioon" }, + "joinOrganizationName": { + "message": "Liity organisaatioon $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Viimeistele liittyminen organisaatioon asettamalla pääsalasana." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Kopioi salasana" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "Kopioi merkinnät" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Kopioi merkinnät" }, + "fill": { + "message": "Fill", + "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": "Automaattitäyttö" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Luo salasana" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "Luo uusi salasana" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Avaa verkkosivusto" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Verkkosivusto" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Kirjaudu" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Aloita rekisteröityminen alusta" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "Palvelimen URL" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "API-palvelimen URL" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Näytä automaattitäytön ehdotukset lomakekentissä" }, + "showInlineMenuIdentitiesLabel": { + "message": "Näytä identiteetit ehdotuksina" + }, + "showInlineMenuCardsLabel": { + "message": "Näytä kortit ehdotuksina" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Näytä ehdotukset kun kuvaketta painetaan" }, @@ -1574,7 +1627,7 @@ "message": "Erääntymisvuosi" }, "expiration": { - "message": "Erääntymisaika" + "message": "Voimassaolo päättyy" }, "january": { "message": "Tammikuu" @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Salasanahistoria" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "Takaisin" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Tyhjennä historia" }, - "noPasswordsToShow": { - "message": "Näytettäviä salasanoja ei ole" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "Et ole luonut salasanoja hiljattain" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "Poista" @@ -2449,8 +2511,8 @@ "message": "Halutessasi, vaadi käyttäjiä syöttämään salasana Sendin avaamiseksi.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Vaadi tämä salasana Sendin avaukseen.", + "sendPasswordDescV3": { + "message": "Lisää valinnainen salasana vastaanottajille tähän Sendiin.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2499,11 +2561,11 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInHoursSingle": { - "message": "The Send will be available to anyone with the link for the next 1 hour.", + "message": "Tämän linkin välityksellä Send on kenen tahansa avattavissa seuraavan tunnin ajan.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInHours": { - "message": "The Send will be available to anyone with the link for the next $HOURS$ hours.", + "message": "Tämän linkin välityksellä Send on kenen tahansa avattavissa seuraavien $HOURS$ tunnin ajan.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "hours": { @@ -2513,11 +2575,11 @@ } }, "sendExpiresInDaysSingle": { - "message": "The Send will be available to anyone with the link for the next 1 day.", + "message": "Tämän linkin välityksellä Send on kenen tahansa avattavissa seuraavan päivän ajan.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInDays": { - "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "message": "Tämän linkin välityksellä Send on kenen tahansa avattavissa seuraavien $DAYS$ päivän ajan.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "days": { @@ -2647,6 +2709,15 @@ "message": "Organisaatiosi edellyttää, että asetat pääsalasanan.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Vahvistus vaaditaan", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Luo käyttäjätunnus" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Käyttäjätunnuksen tyyppi" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Luo sähköpostialias ulkoisella ohjauspalvelulla." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ -virhe: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -3681,7 +3763,7 @@ "message": "Tälle sivustolle sopivia kirjautumistietoja ei ole" }, "searchSavePasskeyNewLogin": { - "message": "Search or save passkey as new login" + "message": "Hae tai tallenna pääsyavain uutena kirjautumistietona" }, "confirm": { "message": "Vahvista" @@ -3934,7 +4016,7 @@ "message": "Tyhjennä suodattimet tai kokeile toista hakutermiä" }, "copyInfoTitle": { - "message": "Kopioi tietoja - $ITEMNAME$", + "message": "Kopioi tiedot - $ITEMNAME$", "description": "Title for a button that opens a menu with options to copy information from an item.", "placeholders": { "itemname": { @@ -3997,7 +4079,7 @@ "message": "Ei kopioitavia arvoja" }, "assignToCollections": { - "message": "Määritä kokoelmiin" + "message": "Määritä kokoelmat" }, "copyEmail": { "message": "Kopioi sähköpostiosoite" @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Kohteen sijainti" }, + "fileSend": { + "message": "Tiedosto-Send" + }, "fileSends": { "message": "Tiedosto-Sendit" }, + "textSend": { + "message": "Teksti-Send" + }, "textSends": { "message": "Teksti-Sendit" }, @@ -4480,7 +4568,7 @@ "message": "Bitwardenilla on uusi ulkoasu!" }, "bitwardenNewLookDesc": { - "message": "Automaattitäyttäminen ja hakujen tekeminen Holvi-välilehdeltä on entistä helpompaa ja luontevampaa. Kokeile nyt!" + "message": "Automaattinen täyttö ja sisällön haku Holvi-välilehdeltä on nyt entistä helpompaa ja luontevampaa. Kokeile nyt!" }, "accountActions": { "message": "Tilitoiminnot" @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Todennetaan" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 46a353fc765..5adeda013f9 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Gumawa ng Account" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Set a strong password" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Join organization" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organization by setting a master password." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Kopyahin ang Password" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "Kopyahin ang Note" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Copy notes" }, + "fill": { + "message": "Fill", + "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": "Auto-fill sa Filipino ay Awtomatikong Pagpuno" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Magtatag ng Password" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "Muling I-generate ang Password" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Launch website" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Website" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Log in" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Restart registration" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "URL ng Server" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "API Server URL" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Kasaysayan ng Password" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "Bumalik" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Clear history" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "Alisin" @@ -2449,8 +2511,8 @@ "message": "Maipapayo na mag-require ng password para sa mga user na ma-access ang Send na ito.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Your organization requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Verification required", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Lumikha ng username" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Uri ng username" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Bumuo ng isang email alias na may isang panlabas na serbisyo sa pagpapasa." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ error: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index eb09e65cc4a..ed2cfb19df7 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Créer un compte" }, + "newToBitwarden": { + "message": "Nouveau sur Bitwarden ?" + }, + "logInWithPasskey": { + "message": "Se connecter avec une clé d'accès" + }, + "useSingleSignOn": { + "message": "Utiliser l'authentification unique" + }, + "welcomeBack": { + "message": "Content de vous revoir" + }, "setAStrongPassword": { "message": "Définir un mot de passe fort" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Rejoindre l'organisation" }, + "joinOrganizationName": { + "message": "Rejoindre $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Terminer de rejoindre cette organisation en configurant un mot de passe principal." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Copier le mot de passe" }, + "copyPassphrase": { + "message": "Copier la phrase de passe" + }, "copyNote": { "message": "Copier la note" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Copier les notes" }, + "fill": { + "message": "Remplir", + "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": "Saisie automatique" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Générer un mot de passe" }, + "generatePassphrase": { + "message": "Générer une phrase de passe" + }, "regeneratePassword": { "message": "Régénérer un mot de passe" }, @@ -536,7 +567,7 @@ "message": "Notes" }, "privateNote": { - "message": "Private note" + "message": "Note privée" }, "note": { "message": "Note" @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Ouvrir le site web" }, + "launchWebsiteName": { + "message": "Lancer le site Web $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Site web" }, @@ -584,7 +624,7 @@ "message": "Expiration de la session" }, "vaultTimeoutHeader": { - "message": "Vault timeout" + "message": "Délai d'expiration du coffre" }, "otherOptions": { "message": "Autres options" @@ -636,7 +676,7 @@ "message": "Délai d'expiration du coffre" }, "vaultTimeout1": { - "message": "Timeout" + "message": "Délai d'expiration" }, "lockNow": { "message": "Verrouiller maintenant" @@ -805,6 +845,9 @@ "logIn": { "message": "Se connecter" }, + "logInToBitwarden": { + "message": "Se connecter à Bitwarden" + }, "restartRegistration": { "message": "Redémarrer l'inscription" }, @@ -839,7 +882,7 @@ "message": "L'authentification à deux facteurs rend votre compte plus sûr en vous demandant de vérifier votre connexion avec un autre dispositif tel qu'une clé de sécurité, une application d'authentification, un SMS, un appel téléphonique ou un courriel. L'authentification à deux facteurs peut être configurée dans le coffre web de bitwarden.com. Voulez-vous visiter le site web maintenant ?" }, "twoStepLoginConfirmationContent": { - "message": "Make your account more secure by setting up two-step login in the Bitwarden web app." + "message": "Rendez votre compte plus sécurisé en configurant la connexion en deux étapes dans l'application web Bitwarden." }, "twoStepLoginConfirmationTitle": { "message": "Poursuivre vers l'application web ?" @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "URL du serveur" }, + "selfHostBaseUrl": { + "message": "URL du serveur auto-hébergé", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "URL du serveur de l'API" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Afficher les suggestions de saisie automatique dans les champs d'un formulaire" }, + "showInlineMenuIdentitiesLabel": { + "message": "Afficher les identités sous forme de suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Afficher les cartes de paiement sous forme de suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Afficher les suggestions lorsque l'icône est sélectionnée" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Historique des mots de passe" }, + "generatorHistory": { + "message": "Historique du générateur" + }, + "clearGeneratorHistoryTitle": { + "message": "Effacer l'historique du générateur" + }, + "cleargGeneratorHistoryDescription": { + "message": "Si vous continuez, toutes les entrées seront définitivement supprimées de l'historique du générateur. Êtes-vous sûr de vouloir continuer ?" + }, "back": { "message": "Retour" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Effacer l'historique" }, - "noPasswordsToShow": { - "message": "Aucun mot de passe à afficher" + "nothingToShow": { + "message": "Rien à montrer" }, - "noRecentlyGeneratedPassword": { - "message": "Vous n'avez pas généré de mot de passe récemment" + "nothingGeneratedRecently": { + "message": "Vous n'avez rien généré récemment" }, "remove": { "message": "Supprimer" @@ -1931,7 +1993,7 @@ "message": "Définissez votre code PIN pour déverrouiller Bitwarden. Les paramètres relatifs à votre code PIN seront réinitialisés si vous vous déconnectez complètement de l'application." }, "setYourPinCode1": { - "message": "Your PIN will be used to unlock Bitwarden instead of your master password. Your PIN will reset if you ever fully log out of Bitwarden." + "message": "Votre code PIN sera utilisé pour déverrouiller Bitwarden au lieu de votre mot de passe principal. Votre code PIN sera réinitialisé si vous vous déconnectez complètement de Bitwarden." }, "pinRequired": { "message": "Le code PIN est requis." @@ -1958,7 +2020,7 @@ "message": "Verrouiller avec le mot de passe principal au redémarrage du navigateur" }, "lockWithMasterPassOnRestart1": { - "message": "Require master password on browser restart" + "message": "Exiger le mot de passe principal au redémarrage du navigateur" }, "selectOneCollection": { "message": "Vous devez sélectionner au moins une collection." @@ -1999,7 +2061,7 @@ "message": "Action après délai d'expiration du coffre" }, "vaultTimeoutAction1": { - "message": "Timeout action" + "message": "Expiration de l'action" }, "lock": { "message": "Verrouiller", @@ -2287,14 +2349,14 @@ "message": "Changements de domaines exclus enregistrés" }, "limitSendViews": { - "message": "Limit views" + "message": "Limiter les vues" }, "limitSendViewsHint": { - "message": "No one can view this Send after the limit is reached.", + "message": "Personne ne peut voir ce Send une fois la limite atteinte.", "description": "Displayed under the limit views field on Send" }, "limitSendViewsCount": { - "message": "$ACCESSCOUNT$ views left", + "message": "$ACCESSCOUNT$ vues restantes", "description": "Displayed under the limit views field on Send", "placeholders": { "accessCount": { @@ -2449,8 +2511,8 @@ "message": "Vous pouvez, si vous le souhaitez, exiger un mot de passe pour accéder à ce Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Ajouter un mot de passe facultatif pour que les destinataires puissent accéder à ce Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Votre organisation exige que vous définissiez un mot de passe principal.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "sur $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Vérification requise", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Générer un nom d'utilisateur" }, + "generateEmail": { + "message": "Générer un courriel" + }, "usernameType": { "message": "Type de nom d'utilisateur" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Générer un alias de courriel avec un service de transfert externe." }, + "forwarderDomainName": { + "message": "Domaine du courriel", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "Erreur $SERVICENAME$ : $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Emplacement de l'élément" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authentification" + }, + "fillGeneratedPassword": { + "message": "Remplir le mot de passe généré", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Mot de passe régénéré", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Enregistrer l'identifiant sur Bitwarden ?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Espace", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Point d'exclamation", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Signe du dollar", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Astérisque", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Tiret bas", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Trait d'union", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Egal à", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Accolade gauche", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Accolade droite", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Crochet gauche", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Crochet droit", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Barre oblique inverse", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Point-virgule", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Guillemets doubles", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Guillemets simples", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Inférieure à", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Supérieur à", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Virgule", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Point", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Point d'interrogation", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Barre oblique", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Minuscule" + }, + "uppercaseAriaLabel": { + "message": "Majuscule" + }, + "generatedPassword": { + "message": "Mot de passe généré" } } diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 539631cb9e1..b6aea83973b 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Crea unha conta" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Set a strong password" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Join organization" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organization by setting a master password." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Copiar contrasinal" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "Copiar nota" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Copy notes" }, + "fill": { + "message": "Fill", + "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": "Auto-encher" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Xerar contrasinal" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "Volver xerar contrasinal" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Launch website" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Sitio web" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Log in" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Restart registration" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "URL do servidor" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "URL do servidor da API" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Historial de contrasinais" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "Atrás" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Clear history" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "Eliminar" @@ -2449,8 +2511,8 @@ "message": "Optionally require a password for users to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Your organization requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Verification required", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Generate username" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Username type" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Generate an email alias with an external forwarding service." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ error: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index 6fa90bfd51b..ceaa7370256 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "צור חשבון" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Set a strong password" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Join organization" }, + "joinOrganizationName": { + "message": "הצטרפות אל $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organization by setting a master password." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "העתק סיסמה" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "העתק פתק" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Copy notes" }, + "fill": { + "message": "Fill", + "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": "השלמה אוטומטית" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "צור סיסמה" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "צור סיסמה חדשה" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Launch website" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "אתר" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Log in" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Restart registration" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "כתובת שרת" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "כתובת שרת הAPI" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "היסטוריית סיסמאות" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "הקודם" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Clear history" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "הסר" @@ -2449,8 +2511,8 @@ "message": "Optionally require a password for users to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "הארגון שלך דורש ממך להגדיר סיסמה ראשית.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Verification required", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Generate username" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "סוג שם משתמש" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "יצירת כינוי דוא״ל עם שירות העברה חיצוני." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ error: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index b349f2c3e99..f9b11310f6a 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Create Account" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "मजबूत पासवर्ड सेट करें" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Join organization" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organization by setting a master password." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Copy Password" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "Copy Note" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Copy notes" }, + "fill": { + "message": "Fill", + "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": "स्वत:भरण" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Generate Password" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "Regenerate Password" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Launch website" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "वेबसाइट" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Log in" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Restart registration" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "सर्वर URL" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "API Server URL" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "पासवर्ड इतिहास" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "वापस जाएं" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Clear history" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "हटाएं" @@ -2449,8 +2511,8 @@ "message": "वैकल्पिक रूप से उपयोगकर्ताओं को इस सेंड तक पहुंचने के लिए पासवर्ड की आवश्यकता होगी।", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Your organization requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "कुल $TOTAL$ में से", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Verification required", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "उपयोगकर्ता नाम बनाएँ" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Username type" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Generate an email alias with an external forwarding service." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ error: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index fbd497a4c02..aef566431bb 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Stvori račun" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Postavi jaku lozinku" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Pridruži se organizaciji" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Dovrši pridruživanje organizaciji postavljanjem glavne lozinke." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Kopiraj lozinku" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "Kopiraj bilješku" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Kopiraj bilješke" }, + "fill": { + "message": "Fill", + "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": "Auto-ispuna" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Generiraj lozinku" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "Ponovno generiraj lozinku" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Pokreni web stranicu" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Web stranica" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Prijavi se" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Ponovno pokreni registraciju" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "URL poslužitelja" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "URL API poslužitelja" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Prikaži prijedloge auto-ispune na poljima obrazaca" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Prikaži prijedloge kada je odabrana ikona" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Povijest" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "Natrag" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Očisti povijest" }, - "noPasswordsToShow": { - "message": "Nema lozinki za prikaz" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "Nema nedavno generiranih lozinki" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "Ukloni" @@ -2449,8 +2511,8 @@ "message": "Neobavezno zahtijevaj korisnika lozinku za pristup ovom Sendu.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Zahtijevaj lozinku za pregled Senda.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Tvoja organizacija zahtijeva da postaviš glavnu lozinku.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Potrebna je potvrda", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Generiraj korisničko ime" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Tip korisničkog imena" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Generiraj pseudonim e-pošte s vanjskom uslugom prosljeđivanja." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ greška: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Lokacija stavke" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "Send datoteke" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Send tekstovi" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Autentifikacija" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index 26f09ff7e0d..32b577b0e83 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Fiók létrehozása" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Erős jelszó beállítása" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Csatlakozás szervezethez" }, + "joinOrganizationName": { + "message": "Csatlakozás: $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Fejezzük be a szervezethez csatlakozást egy mesterjelszó beállításával." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Jelszó másolása" }, + "copyPassphrase": { + "message": "Jelmondat másolása" + }, "copyNote": { "message": "Jegyzet másolása" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Jegyzet másolása" }, + "fill": { + "message": "Kitöltés", + "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": "Automatikus kitöltés" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Jelszó generálása" }, + "generatePassphrase": { + "message": "Jelmondat generálás" + }, "regeneratePassword": { "message": "Jelszó újragenerálása" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Webhely indítása" }, + "launchWebsiteName": { + "message": "$ITEMNAME$ webhely elindítása", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Weboldal" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Log in" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Restart registration" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "Szerver URL" }, + "selfHostBaseUrl": { + "message": "Saját üzemeltetésű szerver webcím", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "API szerver webcím" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Az identitások megjelenítése javaslatként" + }, + "showInlineMenuCardsLabel": { + "message": "A kártyák megjelenítése javaslatként" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Jelszó előzmények" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Beenerátor előzmények kiürítése" + }, + "cleargGeneratorHistoryDescription": { + "message": "Ha folytatjuk, az összes bejegyzés véglegesen törlődik a generátor előzményeiből. Biztosan folytatjuk?" + }, "back": { "message": "Vissza" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Előzmények törlése" }, - "noPasswordsToShow": { - "message": "Nincsenek megjeleníthető jelszavak." + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "Mostanában nem lett jelszó generálva." + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "Eltávolítás" @@ -2449,8 +2511,8 @@ "message": "Opcionálisan megadhatunk egy jelszót a felhasználók számára a Küldés eléréséhez. ", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Jelszó szükséges a Send elem megtekintéséhez.", + "sendPasswordDescV3": { + "message": "Adjunk meg egy opcionális jelszót a címzetteknek a Send eléréséhez.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "A szervezet megköveteli egy mesterjelszó beállítását.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "/ $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Ellenőrzés szükséges", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Felhasználónév generálása" }, + "generateEmail": { + "message": "Email generálása" + }, "usernameType": { "message": "Felhasználónév típusa" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Email álnév generálása külső továbbító szolgáltatással." }, + "forwarderDomainName": { + "message": "Email tartomány", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Válasszunk a kiválasztott szolgáltatás által támogatott tartományt.", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ hiba: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Elem helyek" }, + "fileSend": { + "message": "Fájl típusú Send" + }, "fileSends": { "message": "Fájl küldés" }, + "textSend": { + "message": "Szöveg típusú Send" + }, "textSends": { "message": "Szöveg küldés" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index b339e210323..1b0f9d19295 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Buat Akun" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Set a strong password" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Join organization" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organization by setting a master password." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Salin Kata Sandi" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "Salin Catatan" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Copy notes" }, + "fill": { + "message": "Fill", + "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": "Isi otomatis" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Buat Kata Sandi" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "Buat Ulang Kata Sandi" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Launch website" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Situs Web" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Log in" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Restart registration" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "URL Server" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "URL Server API" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Riwayat Kata Sandi" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "Kembali" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Clear history" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "Hapus" @@ -2449,8 +2511,8 @@ "message": "Secara opsional, minta kata sandi bagi pengguna untuk mengakses Send ini.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Your organization requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Verification required", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Buat nama pengguna baru" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Jenis nama pengguna" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Generate an email alias with an external forwarding service." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ error: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index df23403c9a2..300d99cbed2 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Crea account" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Imposta una password robusta" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Unisciti all'organizzazione" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Termina l'adesione a questa organizzazione impostando una password principale." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Copia password" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "Copia nota" }, @@ -143,17 +167,21 @@ "copyNotes": { "message": "Copy notes" }, + "fill": { + "message": "Fill", + "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": "Riempimento automatico" }, "autoFillLogin": { - "message": "Autocompletamento login" + "message": "Riempi automaticamente login" }, "autoFillCard": { - "message": "Autocompletamento carta" + "message": "Riempi automaticamente carta" }, "autoFillIdentity": { - "message": "Autocompletamento identità" + "message": "Riempi automaticamente identità" }, "generatePasswordCopied": { "message": "Genera password e copiala" @@ -207,7 +235,7 @@ "message": "Enter your account email address and your password hint will be sent to you" }, "passwordHint": { - "message": "Suggerimento password" + "message": "Suggerimento per la password" }, "enterEmailToGetHint": { "message": "Inserisci l'indirizzo email del tuo account per ricevere il suggerimento per la password principale." @@ -398,6 +426,9 @@ "generatePassword": { "message": "Genera password" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "Rigenera password" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Avvia il sito web" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Sito web" }, @@ -782,10 +822,10 @@ "message": "Rendi la 2FA facile" }, "totpHelper": { - "message": "Bitwarden può memorizzare e autocompletare codici di verifica 2FA. Copia e incolla la chiave in questo campo." + "message": "Bitwarden può memorizzare e riempire automaticamente i codici di verifica 2FA. Copia e incolla la chiave in questo campo." }, "totpHelperWithCapture": { - "message": "Bitwarden può memorizzare e autocompletare codici di verifica 2FA. Selezionare l'icona della fotocamera per creare uno screenshot del codice QR dell'autenticatore di questo sito web, oppure copia e incolla la chiave in questo campo." + "message": "Bitwarden può memorizzare e riempire automaticamente i codici di verifica 2FA. Selezionare l'icona della fotocamera per creare uno screenshot del codice QR dell'autenticatore di questo sito web, oppure copia e incolla la chiave in questo campo." }, "learnMoreAboutAuthenticators": { "message": "Ulteriori informazioni sugli autenticatori" @@ -805,6 +845,9 @@ "logIn": { "message": "Accedi" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Riprova la registrazione" }, @@ -944,7 +987,7 @@ "message": "Mostra le carte nella sezione Scheda" }, "showCardsCurrentTabDesc": { - "message": "Mostra le carte nella sezione Scheda per un riempimento automatico più facile." + "message": "Mostra le carte nella sezione Scheda per riempirle automaticamente." }, "showIdentitiesInVaultView": { "message": "Mostra le identità come suggerimenti di riempimento automatico nella vista cassaforte" @@ -1285,7 +1328,7 @@ "message": "Ricordami" }, "sendVerificationCodeEmailAgain": { - "message": "Invia email con codice di verifica di nuovo" + "message": "Invia di nuovo l'email con codice di verifica" }, "useAnotherTwoStepMethod": { "message": "Usa un altro metodo di verifica in due passaggi" @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "URL del server" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "URL del server API" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Mostra suggerimenti di riempimento automatico nei campi del modulo" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Mostra suggerimenti quando l'icona è selezionata" }, @@ -1436,13 +1489,13 @@ "message": "Riempi automaticamente al caricamento della pagina" }, "enableAutoFillOnPageLoad": { - "message": "Abilita l'auto-completamento al caricamento della pagina" + "message": "Riempi automaticamente al caricamento della pagina" }, "enableAutoFillOnPageLoadDesc": { "message": "Se sono rilevati campi di login, riempili automaticamente quando la pagina si carica." }, "autofillOnPageLoadWarning": { - "message": "$OPENTAG$Attenzione:$CLOSETAG$ Siti Web compromessi o non attendibili possono sfruttare l'auto-riempimento al caricamento della pagina.", + "message": "$OPENTAG$Attenzione:$CLOSETAG$ Siti Web compromessi o non attendibili possono sfruttare il riempimento automatico al caricamento della pagina.", "placeholders": { "openTag": { "content": "$1", @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Cronologia delle password" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "Indietro" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Cancella cronologia" }, - "noPasswordsToShow": { - "message": "Nessuna password da mostrare" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "Non hai generato una password di recente" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "Rimuovi" @@ -2046,7 +2108,7 @@ "message": "Elemento riempito automaticamente e URI salvato" }, "autoFillSuccess": { - "message": "Elemento riempito automaticamente" + "message": "Elemento riempito automaticamente " }, "insecurePageWarning": { "message": "Attenzione: questa è una pagina HTTP non protetta, e tutte le informazioni che invii potrebbero essere viste e modificate da altri. Questo login è stato originariamente salvato su una pagina sicura (HTTPS)." @@ -2082,7 +2144,7 @@ "message": "Una o più politiche dell'organizzazione richiedono che la tua password principale soddisfi questi requisiti:" }, "policyInEffectMinComplexity": { - "message": "Punteggio minimo di complessità di $SCORE$", + "message": "Punteggio di complessità minimo di $SCORE$", "placeholders": { "score": { "content": "$1", @@ -2449,8 +2511,8 @@ "message": "Richiedi una password agli utenti per accedere a questo Send (facoltativo).", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "La tua organizzazione ti obbliga di impostare di una password principale.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Verifica necessaria", "description": "Default title for the user verification dialog." @@ -2771,7 +2842,7 @@ "message": "La tua sessione è scaduta. Torna indietro e prova ad accedere di nuovo." }, "exportingPersonalVaultTitle": { - "message": "Esportazione cassaforte personale" + "message": "Esportando cassaforte individuale" }, "exportingIndividualVaultDescription": { "message": "Solo gli elementi della cassaforte personale associati a $EMAIL$ saranno esportati. Gli elementi della cassaforte dell'organizzazione non saranno inclusi. Solo le informazioni sugli elementi della cassaforte saranno esportate e non includeranno gli allegati.", @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Genera nome utente" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Tipo di nome utente" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Genera un alias email con un servizio di inoltro esterno." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "Errore $SERVICENAME$: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -2986,7 +3068,7 @@ "message": "per ritornare alle impostazioni preconfigurate" }, "serverVersion": { - "message": "Versione Server" + "message": "Versione server" }, "selfHostedServer": { "message": "self-hosted" @@ -3061,7 +3143,7 @@ "message": "Password principale debole e violata" }, "weakAndBreachedMasterPasswordDesc": { - "message": "Password debole e trovata una violazione dei dati. Usa una password forte e unica per proteggere il tuo account. Sei sicuro di voler usare questa password?" + "message": "Password debole e trovata in una violazione dei dati. Usa una password forte e unica per proteggere il tuo account. Sei sicuro di voler usare questa password?" }, "checkForBreaches": { "message": "Controlla se la tua password è presente in una violazione dei dati" @@ -3082,7 +3164,7 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Le politiche della tua organizzazione hanno abilitato l'autocompletamento al caricamento della pagina." + "message": "Le politiche della tua organizzazione hanno abilitato il riempimento automatico al caricamento della pagina." }, "howToAutofill": { "message": "Come riempire automaticamente" @@ -3106,22 +3188,22 @@ "message": "Impostazioni di riempimento automatico" }, "autofillKeyboardShortcutSectionTitle": { - "message": "Scorciatoia auto-riempimento" + "message": "Scorciatoia del riempimento automatico" }, "autofillKeyboardShortcutUpdateLabel": { "message": "Cambia scorciatoia" }, "autofillKeyboardManagerShortcutsLabel": { - "message": "Gestisci scorciatoia" + "message": "Gestisci scorciatoie" }, "autofillShortcut": { "message": "Scorciatoia da tastiera per riempire automaticamente" }, "autofillLoginShortcutNotSet": { - "message": "Non è stata impostata nessuna scorciatoia per il riempimento automatico. Cambiala nelle impostazioni del browser." + "message": "Non è stata impostata nessuna scorciatoia per il riempimento automatico. Aggiungila dalle impostazioni del browser." }, "autofillLoginShortcutText": { - "message": "La scorciatoia per l'auto-riempimento è $COMMAND$.\nGestisci tutte le scorciatoie dalle impostazioni del browser.", + "message": "La scorciatoia per il riempimento automatico è $COMMAND$. Gestisci tutte le scorciatoie dalle impostazioni del browser.", "placeholders": { "command": { "content": "$1", @@ -3922,7 +4004,7 @@ "message": "Suggerimenti per il riempimento automatico" }, "autofillSuggestionsTip": { - "message": "Salva un elemento login per questo sito da riempire automaticamente" + "message": "Salva un elemento di accesso per questo sito da riempire automaticamente" }, "yourVaultIsEmpty": { "message": "La tua cassaforte è vuota" @@ -4319,7 +4401,7 @@ "message": "Usa le caselle di controllo se vuoi riempire automaticamente la casella di controllo di un modulo, come una email da ricordare" }, "linkedHelpText": { - "message": "Utilizzare un campo collegato quando si verificano problemi di riempimento automatico per un sito web specifico." + "message": "Usa un campo collegato quando si verificano problemi di riempimento automatico per un sito web specifico." }, "linkedLabelHelpText": { "message": "Inserisci l'id html del campo, il nome, l'aria-label o il segnaposto." @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Posizione elemento" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "Send File" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Send Testo" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 669ddd63df2..75378889d1a 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "アカウントの作成" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "強力なパスワードを設定する" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "組織に参加" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "マスターパスワードを設定して、この組織への参加を完了します。" }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "パスワードをコピー" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "メモをコピー" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "メモをコピー" }, + "fill": { + "message": "Fill", + "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": "自動入力" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "パスワードの自動生成" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "パスワードの再生成" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "ウェブサイトを開く" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "ウェブサイト" }, @@ -805,6 +845,9 @@ "logIn": { "message": "ログイン" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "登録を再度始める" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "サーバー URL" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "API サーバー URL" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "フォームフィールドに自動入力の候補を表示する" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "アイコンが選択されているときに候補を表示する" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "パスワードの履歴" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "戻る" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "履歴を消去" }, - "noPasswordsToShow": { - "message": "パスワードがありません" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "最近パスワードを生成していません" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "削除" @@ -2449,8 +2511,8 @@ "message": "必要に応じて、ユーザーがこの Send にアクセスするためのパスワードを要求します。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Send を表示するにはこのパスワードが必要になります。", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "あなたの組織では、マスターパスワードの設定を義務付けています。", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": " / $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "認証が必要です", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "ユーザー名を生成" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "ユーザー名の種類" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "外部転送サービスを使用してメールエイリアスを生成します。" }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ エラー: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "アイテムの場所" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "ファイル Send" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "テキスト Send" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "認証中" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index 167251e99c2..a5f82794ec8 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "ანგარიშის შექმნა" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Set a strong password" }, @@ -35,10 +47,10 @@ "message": "დახურვა" }, "submit": { - "message": "დადასტურება" + "message": "გადაცემა" }, "emailAddress": { - "message": "ელ-ფოსტა" + "message": "ელფოსტის მისამართი" }, "masterPass": { "message": "Master password" @@ -71,14 +83,23 @@ "joinOrganization": { "message": "Join organization" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organization by setting a master password." }, "tab": { - "message": "Tab" + "message": "ჩანართი" }, "vault": { - "message": "Vault" + "message": "საცავი" }, "myVault": { "message": "My vault" @@ -90,7 +111,7 @@ "message": "ხელსაწყოები" }, "settings": { - "message": "პარამეტრები" + "message": "მორგება" }, "currentTab": { "message": "Current tab" @@ -98,6 +119,9 @@ "copyPassword": { "message": "პაროლის კოპირება" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "Copy note" }, @@ -114,7 +138,7 @@ "message": "უსაფრთხოების კოდის კოპირება" }, "copyName": { - "message": "Copy name" + "message": "სახელის კოპირება" }, "copyCompany": { "message": "Copy company" @@ -143,8 +167,12 @@ "copyNotes": { "message": "Copy notes" }, + "fill": { + "message": "Fill", + "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": "თვითშევსება" + "message": "ავტომატური შევსება" }, "autoFillLogin": { "message": "Autofill login" @@ -192,7 +220,7 @@ "message": "ავტორიზაციის დამატება" }, "addItem": { - "message": "Add item" + "message": "ელემენტის დამატება" }, "accountEmail": { "message": "Account email" @@ -228,7 +256,7 @@ "message": "კოდი გაიგზავნა" }, "verificationCode": { - "message": "ერთჯერადი კოდი" + "message": "გადამოწმების კოდი" }, "confirmIdentity": { "message": "Confirm your identity to continue." @@ -269,7 +297,7 @@ "message": "ორსაფეხურიანი ავტორიზაცია" }, "logOut": { - "message": "გამოსვლა" + "message": "სისტემიდან გასვლა" }, "aboutBitwarden": { "message": "About Bitwarden" @@ -317,7 +345,7 @@ "message": "შენახვა" }, "move": { - "message": "Move" + "message": "გადატანა" }, "addFolder": { "message": "საქაღალდის დამატება" @@ -326,13 +354,13 @@ "message": "სახელი" }, "editFolder": { - "message": "საქაღალდის რედაქტირება" + "message": "საქაღალდის ჩასწორება" }, "newFolder": { - "message": "New folder" + "message": "ახალი საქაღალდე" }, "folderName": { - "message": "Folder name" + "message": "საქაღალდის სახელი" }, "folderHintText": { "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" @@ -356,7 +384,7 @@ "message": "There are no folders to list." }, "helpFeedback": { - "message": "დახმარება & გამოხმაურება" + "message": "დახმარება და უკუკავშირი" }, "helpCenter": { "message": "Bitwarden Help center" @@ -374,13 +402,13 @@ "message": "Sync vault now" }, "lastSync": { - "message": "Last sync:" + "message": "ბოლო სინქი:" }, "passGen": { "message": "Password generator" }, "generator": { - "message": "Generator", + "message": "გენერატორი", "description": "Short for 'credential generator'." }, "passGenInfo": { @@ -393,22 +421,25 @@ "message": "Import items" }, "select": { - "message": "მონიშვნა" + "message": "არჩევა" }, "generatePassword": { - "message": "Generate password" + "message": "პაროლის გენერირება" + }, + "generatePassphrase": { + "message": "Generate passphrase" }, "regeneratePassword": { "message": "Regenerate password" }, "options": { - "message": "პარამეტრები" + "message": "მორგება" }, "length": { "message": "სიგრძე" }, "passwordMinLength": { - "message": "Minimum password length" + "message": "პაროლის მინიმალური სიგრძე" }, "uppercase": { "message": "Uppercase (A-Z)", @@ -427,7 +458,7 @@ "description": "deprecated. Use specialCharactersLabel instead." }, "include": { - "message": "Include", + "message": "ჩართვა", "description": "Card header for password generator include block" }, "uppercaseDescription": { @@ -435,7 +466,7 @@ "description": "Tooltip for the password generator uppercase character checkbox" }, "uppercaseLabel": { - "message": "A-Z", + "message": "ა-ჰ", "description": "Label for the password generator uppercase character checkbox" }, "lowercaseDescription": { @@ -469,7 +500,7 @@ "message": "Word separator" }, "capitalize": { - "message": "დიდი ასოთი აღნიშვნა", + "message": "მაღალ რეგისტრში გადაყვანა", "description": "Make the first letter of a work uppercase." }, "includeNumber": { @@ -497,10 +528,10 @@ "message": "Search vault" }, "edit": { - "message": "შეცვლა" + "message": "ჩასწორება" }, "view": { - "message": "ნახვა" + "message": "ხედი" }, "noItemsInList": { "message": "There are no items to list." @@ -518,13 +549,13 @@ "message": "Authenticator secret" }, "passphrase": { - "message": "Passphrase" + "message": "საკვანძო სიტყვა" }, "favorite": { "message": "რჩეული" }, "unfavorite": { - "message": "Unfavorite" + "message": "რჩეულებიდან წაშლა" }, "itemAddedToFavorites": { "message": "Item added to favorites" @@ -533,37 +564,46 @@ "message": "Item removed from favorites" }, "notes": { - "message": "Notes" + "message": "შენიშვნები" }, "privateNote": { - "message": "Private note" + "message": "პირადი მინაწერი" }, "note": { - "message": "Note" + "message": "შენიშვნა" }, "editItem": { - "message": "Edit item" + "message": "ჩანაწერის ჩასწორება" }, "folder": { "message": "საქაღალდე" }, "deleteItem": { - "message": "Delete item" + "message": "ჩანაწერის წაშლა" }, "viewItem": { "message": "View item" }, "launch": { - "message": "Launch" + "message": "გაშვება" }, "launchWebsite": { "message": "Launch website" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "ვებგვერდი" }, "toggleVisibility": { - "message": "Toggle visibility" + "message": "ხილულობის გადართვა" }, "manage": { "message": "მართვა" @@ -587,7 +627,7 @@ "message": "Vault timeout" }, "otherOptions": { - "message": "Other options" + "message": "სხვა პარამეტრები" }, "rateExtension": { "message": "Rate the extension" @@ -614,7 +654,7 @@ "message": "or" }, "unlock": { - "message": "გახსნა" + "message": "განბლოკვა" }, "loggedInAsOn": { "message": "Logged in as $EMAIL$ on $HOSTNAME$.", @@ -636,7 +676,7 @@ "message": "Vault timeout" }, "vaultTimeout1": { - "message": "Timeout" + "message": "მოლოდინის ვადა" }, "lockNow": { "message": "Lock now" @@ -645,7 +685,7 @@ "message": "Lock all" }, "immediately": { - "message": "დაუყონებლივ" + "message": "დაუყოვნებლივ" }, "tenSeconds": { "message": "10 წამი" @@ -705,10 +745,10 @@ "message": "დაფიქსირდა შეცდომა" }, "emailRequired": { - "message": "ელ-ფოსტის მისამართი აუცილებელია." + "message": "ელფოსტის მისამართი აუცილებელია." }, "invalidEmail": { - "message": "არასწორი ელ-ფოსტის მისამართი." + "message": "არასწორი ელფოსტის მისამართი." }, "masterPasswordRequired": { "message": "Master password is required." @@ -748,13 +788,13 @@ "message": "We've sent you an email with your master password hint." }, "verificationCodeRequired": { - "message": "ერთჯერადი კოდი აუცილებელია." + "message": "გადამოწმების კოდი აუცილებელია." }, "webauthnCancelOrTimeout": { "message": "The authentication was cancelled or took too long. Please try again." }, "invalidVerificationCode": { - "message": "Invalid verification code" + "message": "არასწორი გადამოწმების კოდი" }, "valueCopied": { "message": "$VALUE$ copied", @@ -794,7 +834,7 @@ "message": "Copy Authenticator key (TOTP)" }, "loggedOut": { - "message": "Logged out" + "message": "გამოხვედით" }, "loggedOutDesc": { "message": "You have been logged out of your account." @@ -803,7 +843,10 @@ "message": "Your login session has expired." }, "logIn": { - "message": "Log in" + "message": "შესვლა" + }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" }, "restartRegistration": { "message": "Restart registration" @@ -818,10 +861,10 @@ "message": "You may already have an account" }, "logOutConfirmation": { - "message": "Are you sure you want to log out?" + "message": "დარწმუნებული ბრძანდებით, რომ გნებავთ, გახვიდეთ?" }, "yes": { - "message": "Yes" + "message": "კი" }, "no": { "message": "არა" @@ -866,7 +909,7 @@ "message": "Syncing failed" }, "passwordCopied": { - "message": "Password copied" + "message": "პაროლი დაკოპირდა" }, "uri": { "message": "URI" @@ -882,10 +925,10 @@ } }, "newUri": { - "message": "New URI" + "message": "ახალი URI" }, "addDomain": { - "message": "Add domain", + "message": "დომენის დამატება", "description": "'Domain' here refers to an internet domain name (e.g. 'bitwarden.com') and the message in whole described the act of putting a domain value into the context." }, "addedItem": { @@ -916,13 +959,13 @@ "message": "Search folder" }, "searchCollection": { - "message": "Search collection" + "message": "კოლექციაში ძებნა" }, "searchType": { "message": "Search type" }, "noneFolder": { - "message": "No folder", + "message": "საქაღალდის გარეშე", "description": "This is the folder for uncategorized items" }, "enableAddLoginNotification": { @@ -956,7 +999,7 @@ "message": "List identity items on the Tab page for easy autofill." }, "clearClipboard": { - "message": "Clear clipboard", + "message": "ბუფერის გასუფთავება", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." }, "clearClipboardDesc": { @@ -967,7 +1010,7 @@ "message": "Should Bitwarden remember this password for you?" }, "notificationAddSave": { - "message": "Save" + "message": "შენახვა" }, "enableChangedPasswordNotification": { "message": "Ask to update existing login" @@ -988,16 +1031,16 @@ "message": "Do you want to update this password in Bitwarden?" }, "notificationChangeSave": { - "message": "Update" + "message": "განახლება" }, "notificationUnlockDesc": { "message": "Unlock your Bitwarden vault to complete the autofill request." }, "notificationUnlock": { - "message": "Unlock" + "message": "განბლოკვა" }, "additionalOptions": { - "message": "Additional options" + "message": "დამატებითი პარამეტრები" }, "enableContextMenuItem": { "message": "Show context menu options" @@ -1016,7 +1059,7 @@ "message": "Choose the default way that URI match detection is handled for logins when performing actions such as autofill." }, "theme": { - "message": "Theme" + "message": "თემა" }, "themeDesc": { "message": "Change the application's color theme." @@ -1025,15 +1068,15 @@ "message": "Change the application's color theme. Applies to all logged in accounts." }, "dark": { - "message": "Dark", + "message": "ბნელი", "description": "Dark color" }, "light": { - "message": "Light", + "message": "ღია", "description": "Light color" }, "solarizedDark": { - "message": "Solarized dark", + "message": "სოლარიზებული მუქი", "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, "exportFrom": { @@ -1061,7 +1104,7 @@ "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." }, "exportTypeHeading": { - "message": "Export type" + "message": "გატანის ტიპი" }, "accountRestricted": { "message": "Account restricted" @@ -1070,7 +1113,7 @@ "message": "“File password” and “Confirm file password“ do not match." }, "warning": { - "message": "WARNING", + "message": "გაფრთხილება", "description": "WARNING (should stay in capitalized letters if the language permits)" }, "confirmVaultExport": { @@ -1089,7 +1132,7 @@ "message": "Enter your master password to export your vault data." }, "shared": { - "message": "Shared" + "message": "გაზიარებული" }, "bitwardenForBusinessPageDesc": { "message": "Bitwarden for Business allows you to share your vault items with others by using an organization. Learn more on the bitwarden.com website." @@ -1098,7 +1141,7 @@ "message": "Move to organization" }, "share": { - "message": "Share" + "message": "გაზიარება" }, "movedItemToOrg": { "message": "$ITEMNAME$ moved to $ORGNAME$", @@ -1117,7 +1160,7 @@ "message": "Choose an organization that you wish to move this item to. Moving to an organization transfers ownership of the item to that organization. You will no longer be the direct owner of this item once it has been moved." }, "learnMore": { - "message": "Learn more" + "message": "გაიგეთ მეტი" }, "authenticatorKeyTotp": { "message": "Authenticator key (TOTP)" @@ -1129,7 +1172,7 @@ "message": "Copy verification code" }, "attachments": { - "message": "Attachments" + "message": "დანართი" }, "deleteAttachment": { "message": "Delete attachment" @@ -1150,13 +1193,13 @@ "message": "Attachment saved" }, "file": { - "message": "File" + "message": "ფაილი" }, "fileToShare": { "message": "File to share" }, "selectFile": { - "message": "Select a file" + "message": "აირჩიეთ ფაილი" }, "maxFileSize": { "message": "Maximum file size is 500 MB." @@ -1282,7 +1325,7 @@ } }, "rememberMe": { - "message": "Remember me" + "message": "დამიმახსოვრე" }, "sendVerificationCodeEmailAgain": { "message": "Send verification code email again" @@ -1300,7 +1343,7 @@ "message": "To start the WebAuthn 2FA verification. Click the button below to open a new tab and follow the instructions provided in the new tab." }, "webAuthnNewTabOpen": { - "message": "Open new tab" + "message": "ახალი ჩანართის გახსნა" }, "webAuthnAuthenticate": { "message": "Authenticate WebAuthn" @@ -1324,7 +1367,7 @@ "message": "Recovery code" }, "authenticatorAppTitle": { - "message": "Authenticator app" + "message": "ავთენტიკატორი აპი" }, "authenticatorAppDescV2": { "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", @@ -1351,7 +1394,7 @@ "message": "Use any WebAuthn compatible security key to access your account." }, "emailTitle": { - "message": "Email" + "message": "ელ-ფოსტა" }, "emailDescV2": { "message": "Enter a code sent to your email." @@ -1378,7 +1421,11 @@ "message": "For advanced users. You can specify the base URL of each service independently." }, "baseUrl": { - "message": "Server URL" + "message": "სერვერის URL" + }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" }, "apiUrl": { "message": "API server URL" @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1421,7 +1474,7 @@ "message": "Edit browser settings." }, "autofillOverlayVisibilityOff": { - "message": "Off", + "message": "გამორთული", "description": "Overlay setting select option for disabling autofill overlay" }, "autofillOverlayVisibilityOnFieldFocus": { @@ -1506,10 +1559,10 @@ "message": "Custom fields" }, "copyValue": { - "message": "Copy value" + "message": "დააკოპირეთ მნიშვნელობა" }, "value": { - "message": "Value" + "message": "მნიშვნელობა" }, "newCustomField": { "message": "New custom field" @@ -1518,19 +1571,19 @@ "message": "Drag to sort" }, "cfTypeText": { - "message": "Text" + "message": "ტექსტი" }, "cfTypeHidden": { - "message": "Hidden" + "message": "დამალული" }, "cfTypeBoolean": { - "message": "Boolean" + "message": "ბულევი" }, "cfTypeCheckbox": { - "message": "Checkbox" + "message": "ჩასართავი" }, "cfTypeLinked": { - "message": "Linked", + "message": "მიბმული", "description": "This describes a field that is 'linked' (tied) to another field." }, "linkedValue": { @@ -1562,10 +1615,10 @@ "message": "Cardholder name" }, "number": { - "message": "Number" + "message": "რიცხვი" }, "brand": { - "message": "Brand" + "message": "სავაჭრო მარკა" }, "expirationMonth": { "message": "Expiration month" @@ -1574,43 +1627,43 @@ "message": "Expiration year" }, "expiration": { - "message": "Expiration" + "message": "ვადა" }, "january": { - "message": "January" + "message": "იანვარი" }, "february": { - "message": "February" + "message": "თებერვალი" }, "march": { - "message": "March" + "message": "მარტი" }, "april": { - "message": "April" + "message": "აპრილი" }, "may": { - "message": "May" + "message": "მაისი" }, "june": { - "message": "June" + "message": "ივნისი" }, "july": { - "message": "July" + "message": "ივლისი" }, "august": { - "message": "August" + "message": "აგვისტო" }, "september": { - "message": "September" + "message": "სექტემბერი" }, "october": { - "message": "October" + "message": "ოქტომბერი" }, "november": { - "message": "November" + "message": "ნოემბერი" }, "december": { - "message": "December" + "message": "დეკემბერი" }, "securityCode": { "message": "Security code" @@ -1619,40 +1672,40 @@ "message": "ex." }, "title": { - "message": "Title" + "message": "სათაური" }, "mr": { - "message": "Mr" + "message": "ბატონი" }, "mrs": { - "message": "Mrs" + "message": "ქალბატონი" }, "ms": { - "message": "Ms" + "message": "ქალბატონი" }, "dr": { - "message": "Dr" + "message": "დოქტორი" }, "mx": { "message": "Mx" }, "firstName": { - "message": "First name" + "message": "სახელი" }, "middleName": { - "message": "Middle name" + "message": "შუა სახელი" }, "lastName": { - "message": "Last name" + "message": "გვარი" }, "fullName": { - "message": "Full name" + "message": "სრული სახელი" }, "identityName": { "message": "Identity name" }, "company": { - "message": "Company" + "message": "კომპანია" }, "ssn": { "message": "Social Security number" @@ -1664,13 +1717,13 @@ "message": "License number" }, "email": { - "message": "Email" + "message": "ელ-ფოსტა" }, "phone": { - "message": "Phone" + "message": "ტელეფონი" }, "address": { - "message": "Address" + "message": "მისამართი" }, "address1": { "message": "Address 1" @@ -1685,19 +1738,19 @@ "message": "City / Town" }, "stateProvince": { - "message": "State / Province" + "message": "რეგიონი/მხარე" }, "zipPostalCode": { "message": "Zip / Postal code" }, "country": { - "message": "Country" + "message": "ქვეყანა" }, "type": { - "message": "Type" + "message": "ტიპი" }, "typeLogin": { - "message": "Login" + "message": "შესვლა" }, "typeLogins": { "message": "Logins" @@ -1706,10 +1759,10 @@ "message": "Secure note" }, "typeCard": { - "message": "Card" + "message": "ბარათი" }, "typeIdentity": { - "message": "Identity" + "message": "იდენტიფიკაცია" }, "newItemHeader": { "message": "New $TYPE$", @@ -1741,11 +1794,20 @@ "passwordHistory": { "message": "Password history" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { - "message": "Back" + "message": "უკან" }, "collections": { - "message": "Collections" + "message": "კოლექციები" }, "nCollections": { "message": "$COUNT$ collections", @@ -1757,19 +1819,19 @@ } }, "favorites": { - "message": "Favorites" + "message": "სანიშნეები" }, "popOutNewWindow": { "message": "Pop out to a new window" }, "refresh": { - "message": "Refresh" + "message": "განახლება" }, "cards": { - "message": "Cards" + "message": "კარტი" }, "identities": { - "message": "Identities" + "message": "იდენტიფიკატორები" }, "logins": { "message": "Logins" @@ -1778,7 +1840,7 @@ "message": "Secure notes" }, "clear": { - "message": "Clear", + "message": "გაწმენდა", "description": "To clear something out. example: To clear browser history." }, "checkPassword": { @@ -1805,21 +1867,21 @@ "description": "Domain name. Ex. website.com" }, "domainName": { - "message": "Domain name", + "message": "დომენის სახელი", "description": "Domain name. Ex. website.com" }, "host": { - "message": "Host", + "message": "ჰოსტი", "description": "A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'." }, "exact": { - "message": "Exact" + "message": "ზუსტი" }, "startsWith": { - "message": "Starts with" + "message": "იწყება" }, "regEx": { - "message": "Regular expression", + "message": "რეგულარულ გამოსახულება", "description": "A programming term, also known as 'RegEx'." }, "matchDetection": { @@ -1842,39 +1904,39 @@ "description": "The URI of one of the current open tabs in the browser." }, "organization": { - "message": "Organization", + "message": "ორგანიზაცია", "description": "An entity of multiple related people (ex. a team or business organization)." }, "types": { - "message": "Types" + "message": "ტიპები" }, "allItems": { - "message": "All items" + "message": "ყველა ჩანაწერი" }, "noPasswordsInList": { "message": "There are no passwords to list." }, "clearHistory": { - "message": "Clear history" + "message": "პაროლების ისტორია" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { - "message": "Remove" + "message": "წაშლა" }, "default": { - "message": "Default" + "message": "ნაგულისხმევი" }, "dateUpdated": { - "message": "Updated", + "message": "განახლებულია", "description": "ex. Date this item was updated" }, "dateCreated": { - "message": "Created", + "message": "შექმნის თარიღი", "description": "ex. Date this item was created" }, "datePasswordUpdated": { @@ -1891,21 +1953,21 @@ "message": "There are no collections to list." }, "ownership": { - "message": "Ownership" + "message": "მფლობელობა" }, "whoOwnsThisItem": { "message": "Who owns this item?" }, "strong": { - "message": "Strong", + "message": "ძლიერი", "description": "ex. A strong password. Scale: Weak -> Good -> Strong" }, "good": { - "message": "Good", + "message": "კარგი", "description": "ex. A good password. Scale: Weak -> Good -> Strong" }, "weak": { - "message": "Weak", + "message": "სუსტი", "description": "ex. A weak password. Scale: Weak -> Good -> Strong" }, "weakMasterPassword": { @@ -1915,7 +1977,7 @@ "message": "The master password you have chosen is weak. You should use a strong master password (or a passphrase) to properly protect your Bitwarden account. Are you sure you want to use this master password?" }, "pin": { - "message": "PIN", + "message": "PIN კოდი", "description": "PIN code. Ex. The short code (often numeric) that you use to unlock a device." }, "unlockWithPin": { @@ -1967,7 +2029,7 @@ "message": "Clone item" }, "clone": { - "message": "Clone" + "message": "კლონი" }, "passwordGeneratorPolicyInEffect": { "message": "One or more organization policies are affecting your generator settings." @@ -2002,11 +2064,11 @@ "message": "Timeout action" }, "lock": { - "message": "Lock", + "message": "ჩაკეტვა", "description": "Verb form: to make secure or inaccessible by" }, "trash": { - "message": "Trash", + "message": "ნაგავი", "description": "Noun: a special folder to hold deleted items" }, "searchTrash": { @@ -2028,7 +2090,7 @@ "message": "Item restored" }, "alreadyHaveAccount": { - "message": "Already have an account?" + "message": "უკვე გაქვთ ანგარიში?" }, "vaultTimeoutLogOutConfirmation": { "message": "Logging out will remove all access to your vault and requires online authentication after the timeout period. Are you sure you want to use this setting?" @@ -2124,7 +2186,7 @@ "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { - "message": "Unsubscribe" + "message": "გამოწერის გაუქმება" }, "atAnyTime": { "message": "at any time." @@ -2142,16 +2204,16 @@ "message": "Terms of Service and Privacy Policy have not been acknowledged." }, "termsOfService": { - "message": "Terms of Service" + "message": "მომსახურების პირობები" }, "privacyPolicy": { - "message": "Privacy Policy" + "message": "კონფიდენციალობის პოლიტიკა" }, "hintEqualsPassword": { "message": "Your password hint cannot be the same as your password." }, "ok": { - "message": "Ok" + "message": "დიახ" }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" @@ -2253,7 +2315,7 @@ "message": "An organization policy has blocked importing items into your individual vault." }, "domainsTitle": { - "message": "Domains", + "message": "დომენები", "description": "A category title describing the concept of web domains" }, "excludedDomains": { @@ -2304,7 +2366,7 @@ } }, "send": { - "message": "Send", + "message": "გაგზავნა", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendDetails": { @@ -2320,13 +2382,13 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendTypeText": { - "message": "Text" + "message": "ტექსტი" }, "sendTypeTextToShare": { "message": "Text to share" }, "sendTypeFile": { - "message": "File" + "message": "ფაილი" }, "allSends": { "message": "All Sends", @@ -2340,26 +2402,26 @@ "description": "This text will be displayed after a Send has been accessed the maximum amount of times." }, "expired": { - "message": "Expired" + "message": "ვადაგასულია" }, "pendingDeletion": { - "message": "Pending deletion" + "message": "ელოდება წაშლას" }, "passwordProtected": { "message": "Password protected" }, "copyLink": { - "message": "Copy link" + "message": "Ბმულის კოპირება" }, "copySendLink": { "message": "Copy Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "removePassword": { - "message": "Remove Password" + "message": "პაროლის წაშლა" }, "delete": { - "message": "Delete" + "message": "წაშლა" }, "removedPassword": { "message": "Password removed" @@ -2373,7 +2435,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "disabled": { - "message": "Disabled" + "message": "გამორთული" }, "removePasswordConfirmation": { "message": "Are you sure you want to remove the password?" @@ -2417,14 +2479,14 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "expirationDate": { - "message": "Expiration date" + "message": "ვადა" }, "expirationDateDesc": { "message": "If set, access to this Send will expire on the specified date and time.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "oneDay": { - "message": "1 day" + "message": "1 დღე" }, "days": { "message": "$DAYS$ days", @@ -2436,7 +2498,7 @@ } }, "custom": { - "message": "Custom" + "message": "განსხვავებული" }, "maximumAccessCount": { "message": "Maximum Access Count" @@ -2449,8 +2511,8 @@ "message": "Optionally require a password for users to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2480,7 +2542,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "newPassword": { - "message": "New password" + "message": "ახალი პაროლი" }, "sendDisabled": { "message": "Send removed", @@ -2552,7 +2614,7 @@ "message": "In order to choose a file using Safari, pop out to a new window by clicking this banner." }, "popOut": { - "message": "Pop out" + "message": "გატანა" }, "sendFileCalloutHeader": { "message": "Before you start" @@ -2562,7 +2624,7 @@ "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" }, "sendFirefoxCustomDatePopoutMessage2": { - "message": "click here", + "message": "აქ დააწკაპუნეთ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" }, "sendFirefoxCustomDatePopoutMessage3": { @@ -2606,7 +2668,7 @@ "message": "Email verification required" }, "emailVerifiedV2": { - "message": "Email verified" + "message": "ელფოსტა გადამოწმებულია" }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." @@ -2633,7 +2695,7 @@ "message": "This organization has an enterprise policy that will automatically enroll you in password reset. Enrollment will allow organization administrators to change your master password." }, "selectFolder": { - "message": "Select folder..." + "message": "აირჩიეთ საქაღალდე..." }, "noFoldersFound": { "message": "No folders found", @@ -2647,15 +2709,24 @@ "message": "Your organization requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Verification required", "description": "Default title for the user verification dialog." }, "hours": { - "message": "Hours" + "message": "საათი" }, "minutes": { - "message": "Minutes" + "message": "წუთი" }, "vaultTimeoutPolicyAffectingOptions": { "message": "Enterprise policy requirements have been applied to your timeout options" @@ -2795,7 +2866,7 @@ } }, "error": { - "message": "Error" + "message": "შეცდომა" }, "regenerateUsername": { "message": "Regenerate username" @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Generate username" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Username type" }, @@ -2820,7 +2894,7 @@ "message": "Use your domain's configured catch-all inbox." }, "random": { - "message": "Random" + "message": "შემთხვევითი" }, "randomWord": { "message": "Random word" @@ -2835,7 +2909,7 @@ "message": "Password type" }, "service": { - "message": "Service" + "message": "სერვისი" }, "forwardedEmail": { "message": "Forwarded email alias" @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Generate an email alias with an external forwarding service." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ error: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -2946,14 +3028,14 @@ } }, "hostname": { - "message": "Hostname", + "message": "ჰოსტის სახელი", "description": "Part of a URL." }, "apiAccessToken": { "message": "API Access Token" }, "apiKey": { - "message": "API Key" + "message": "API-ის გასაღები" }, "ssoKeyConnectorError": { "message": "Key connector error: make sure key connector is available and working correctly." @@ -2980,19 +3062,19 @@ "message": "Settings have been edited" }, "environmentEditedClick": { - "message": "Click here" + "message": "დააწკაპუნეთ აქ" }, "environmentEditedReset": { "message": "to reset to pre-configured settings" }, "serverVersion": { - "message": "Server version" + "message": "სერვერის ვერსია" }, "selfHostedServer": { "message": "self-hosted" }, "thirdParty": { - "message": "Third-party" + "message": "მესამე-პირი" }, "thirdPartyServerMessage": { "message": "Connected to third-party server implementation, $SERVERNAME$. Please verify bugs using the official server, or report them to the third-party server.", @@ -3100,7 +3182,7 @@ "message": "Select an item from this screen, or explore other options in settings." }, "gotIt": { - "message": "Got it" + "message": "გავიგე" }, "autofillSettings": { "message": "Autofill settings" @@ -3112,7 +3194,7 @@ "message": "Change shortcut" }, "autofillKeyboardManagerShortcutsLabel": { - "message": "Manage shortcuts" + "message": "მალსახმობების მართვა" }, "autofillShortcut": { "message": "Autofill keyboard shortcut" @@ -3169,7 +3251,7 @@ "message": "Creating account on" }, "checkYourEmail": { - "message": "Check your email" + "message": "შეამოწმეთ თქვენი ელფოსტა" }, "followTheLinkInTheEmailSentTo": { "message": "Follow the link in the email sent to" @@ -3181,7 +3263,7 @@ "message": "No email?" }, "goBack": { - "message": "Go back" + "message": "უკან დაბრუნება" }, "toEditYourEmailAddress": { "message": "to edit your email address." @@ -3194,10 +3276,10 @@ "message": "Access denied. You do not have permission to view this page." }, "general": { - "message": "General" + "message": "ზოგადი" }, "display": { - "message": "Display" + "message": "ჩვენება" }, "accountSuccessfullyCreated": { "message": "Account successfully created!" @@ -3235,10 +3317,10 @@ "message": "Input is required." }, "required": { - "message": "required" + "message": "აუცილებელია" }, "search": { - "message": "Search" + "message": "ძებნა" }, "inputMinLength": { "message": "Input must be at least $COUNT$ characters long.", @@ -3317,7 +3399,7 @@ } }, "selectPlaceholder": { - "message": "-- Select --" + "message": "-- არჩევა --" }, "multiSelectPlaceholder": { "message": "-- Type to filter --" @@ -3326,10 +3408,10 @@ "message": "Retrieving options..." }, "multiSelectNotFound": { - "message": "No items found" + "message": "ჩანაწერები ნაპოვნი არაა" }, "multiSelectClearAll": { - "message": "Clear all" + "message": "ყველას გასუფთავება" }, "plusNMore": { "message": "+ $QUANTITY$ more", @@ -3341,7 +3423,7 @@ } }, "submenu": { - "message": "Submenu" + "message": "ქვემენიუ" }, "toggleCollapse": { "message": "Toggle collapse", @@ -3364,7 +3446,7 @@ "description": "Notification button text for starting a fileless import." }, "importing": { - "message": "Importing...", + "message": "შემოტანა...", "description": "Notification message for when an import is in progress." }, "dataSuccessfullyImported": { @@ -3398,7 +3480,7 @@ "message": "Toggle side navigation" }, "skipToContent": { - "message": "Skip to content" + "message": "შემცველობაზე გადახტომა" }, "bitwardenOverlayButton": { "message": "Bitwarden autofill menu button", @@ -3421,7 +3503,7 @@ "description": "Text to display in overlay when the account is locked." }, "unlockAccount": { - "message": "Unlock account", + "message": "ანგარიშის განბლოკვა", "description": "Button text to display in overlay when the account is locked." }, "unlockAccountAria": { @@ -3441,7 +3523,7 @@ "description": "Text to show in overlay if there are no matching items" }, "newItem": { - "message": "New item", + "message": "ახალი ჩანაწერი", "description": "Button text to display in overlay when there are no matching items" }, "addNewVaultItem": { @@ -3477,17 +3559,17 @@ "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { - "message": "Turn on" + "message": "ჩართვა" }, "ignore": { - "message": "Ignore" + "message": "იგნორი" }, "importData": { - "message": "Import data", + "message": "მონაცემების შემოტანა", "description": "Used for the header of the import dialog, the import button and within the file-password-prompt" }, "importError": { - "message": "Import error" + "message": "შემოტანის შეცდომა" }, "importErrorDesc": { "message": "There was a problem with the data you tried to import. Please resolve the errors listed below in your source file and try again." @@ -3496,7 +3578,7 @@ "message": "Resolve the errors below and try again." }, "description": { - "message": "Description" + "message": "აღწერა" }, "importSuccess": { "message": "Data successfully imported" @@ -3511,7 +3593,7 @@ } }, "tryAgain": { - "message": "Try again" + "message": "კიდევ სცადეთ" }, "verificationRequiredForActionSetPinToContinue": { "message": "Verification required for this action. Set a PIN to continue." @@ -3547,7 +3629,7 @@ "message": "Resend code" }, "total": { - "message": "Total" + "message": "სულ" }, "importWarning": { "message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organization. Do you want to proceed?", @@ -3589,16 +3671,16 @@ "message": "Invalid file password, please use the password you entered when you created the export file." }, "destination": { - "message": "Destination" + "message": "სამიზნე" }, "learnAboutImportOptions": { "message": "Learn about your import options" }, "selectImportFolder": { - "message": "Select a folder" + "message": "აირჩიეთ საქაღალდე" }, "selectImportCollection": { - "message": "Select a collection" + "message": "აირჩიეთ კოლექცია" }, "importTargetHint": { "message": "Select this option if you want the imported file contents moved to a $DESTINATION$", @@ -3620,10 +3702,10 @@ "message": "Select the import file" }, "chooseFile": { - "message": "Choose File" + "message": "ფაილის არჩევა" }, "noFileChosen": { - "message": "No file chosen" + "message": "ფაილი არჩეული არაა" }, "orCopyPasteFileContents": { "message": "or copy/paste the import file contents" @@ -3651,10 +3733,10 @@ "message": "Vault data exported" }, "typePasskey": { - "message": "Passkey" + "message": "საკვანძო გასაღები" }, "accessing": { - "message": "Accessing" + "message": "წვდომა" }, "passkeyNotCopied": { "message": "Passkey will not be copied" @@ -3684,7 +3766,7 @@ "message": "Search or save passkey as new login" }, "confirm": { - "message": "Confirm" + "message": "დადასტურება" }, "savePasskey": { "message": "Save passkey" @@ -3720,16 +3802,16 @@ "message": "No LastPass data found" }, "incorrectUsernameOrPassword": { - "message": "Incorrect username or password" + "message": "არასწორი მომხმარებელი ან პაროლი" }, "incorrectPassword": { - "message": "Incorrect password" + "message": "არასწორი პაროლი" }, "incorrectCode": { "message": "Incorrect code" }, "incorrectPin": { - "message": "Incorrect PIN" + "message": "PIN კოდი არასწორია" }, "multifactorAuthenticationFailed": { "message": "Multifactor authentication failed" @@ -3753,7 +3835,7 @@ "message": "Approve the login request in your authentication app or enter a one-time passcode." }, "passcode": { - "message": "Passcode" + "message": "საკვანძო კოდი" }, "lastPassMasterPassword": { "message": "LastPass master password" @@ -3775,19 +3857,19 @@ "message": "Import directly from LastPass" }, "importFromCSV": { - "message": "Import from CSV" + "message": "CVS-დან შემოტანა" }, "lastPassTryAgainCheckEmail": { "message": "Try again or look for an email from LastPass to verify it's you." }, "collection": { - "message": "Collection" + "message": "კოლექცია" }, "lastPassYubikeyDesc": { "message": "Insert the YubiKey associated with your LastPass account into your computer's USB port, then touch its button." }, "switchAccount": { - "message": "Switch account" + "message": "ანგარიშის გადართვა" }, "switchAccounts": { "message": "Switch accounts" @@ -3805,16 +3887,16 @@ "message": "Account limit reached. Log out of an account to add another." }, "active": { - "message": "active" + "message": "აქტიური" }, "locked": { - "message": "locked" + "message": "დაბლოკილია" }, "unlocked": { "message": "unlocked" }, "server": { - "message": "server" + "message": "სერვერი" }, "hostedAt": { "message": "hosted at" @@ -3823,7 +3905,7 @@ "message": "Use your device or hardware key" }, "justOnce": { - "message": "Just once" + "message": "მხოლოდ ერთხელ" }, "alwaysForThisSite": { "message": "Always for this site" @@ -3886,7 +3968,7 @@ "description": "Description for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "makeDefault": { - "message": "Make default", + "message": "ნაგულისხმებად გამოყენება", "description": "Button text for the setting that allows overriding the default browser autofill settings" }, "saveCipherAttemptSuccess": { @@ -3910,7 +3992,7 @@ "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "წარმატება" }, "removePasskey": { "message": "Remove passkey" @@ -4006,19 +4088,19 @@ "message": "Copy phone" }, "copyAddress": { - "message": "Copy address" + "message": "მისამართის დაკოპირება" }, "adminConsole": { "message": "Admin Console" }, "accountSecurity": { - "message": "Account security" + "message": "ანგარიშის უსაფრთხოება" }, "notifications": { - "message": "Notifications" + "message": "გაფრთხილებები" }, "appearance": { - "message": "Appearance" + "message": "გარეგნობა" }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." @@ -4047,7 +4129,7 @@ } }, "new": { - "message": "New" + "message": "ახალი" }, "removeItem": { "message": "Remove $NAME$", @@ -4066,7 +4148,7 @@ "message": "Item details" }, "itemName": { - "message": "Item name" + "message": "ჩანაწერის სახელი" }, "cannotRemoveViewOnlyCollections": { "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", @@ -4081,10 +4163,10 @@ "message": "Organization is deactivated" }, "owner": { - "message": "Owner" + "message": "მფლობელი" }, "selfOwnershipLabel": { - "message": "You", + "message": "თქვენ", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { @@ -4097,22 +4179,22 @@ "message": "Item history" }, "lastEdited": { - "message": "Last edited" + "message": "ბოლოს ჩასწორებული" }, "ownerYou": { "message": "Owner: You" }, "linked": { - "message": "Linked" + "message": "მიბმული" }, "copySuccessful": { "message": "Copy Successful" }, "upload": { - "message": "Upload" + "message": "ატვირთვა" }, "addAttachment": { - "message": "Add attachment" + "message": "მიმაგრებული ფაილის დამატება" }, "maxFileSizeSansPunctuation": { "message": "Maximum file size is 500 MB" @@ -4139,22 +4221,22 @@ "message": "Are you sure you want to permanently delete this attachment?" }, "premium": { - "message": "Premium" + "message": "პრემიუმი" }, "freeOrgsCannotUseAttachments": { "message": "Free organizations cannot use attachments" }, "filters": { - "message": "Filters" + "message": "ფილტრები" }, "personalDetails": { "message": "Personal details" }, "identification": { - "message": "Identification" + "message": "იდენტიფიკაცია" }, "contactInfo": { - "message": "Contact info" + "message": "საკონტაქტო ინფორმაცია" }, "downloadAttachment": { "message": "Download - $ITEMNAME$", @@ -4238,7 +4320,7 @@ "message": "If you've renewed it, update the card's information" }, "cardDetails": { - "message": "Card details" + "message": "ბარათის დეტალები" }, "cardBrandDetails": { "message": "$BRAND$ details", @@ -4250,26 +4332,26 @@ } }, "enableAnimations": { - "message": "Enable animations" + "message": "ანიმაციების ჩართვა" }, "showAnimations": { "message": "Show animations" }, "addAccount": { - "message": "Add account" + "message": "ანგარიშის დამატება" }, "loading": { - "message": "Loading" + "message": "იტვირთება" }, "data": { - "message": "Data" + "message": "მონაცემები" }, "passkeys": { "message": "Passkeys", "description": "A section header for a list of passkeys." }, "passwords": { - "message": "Passwords", + "message": "პაროლები", "description": "A section header for a list of passwords." }, "logInWithPasskeyAriaLabel": { @@ -4277,7 +4359,7 @@ "description": "ARIA label for the inline menu button that logs in with a passkey." }, "assign": { - "message": "Assign" + "message": "მინიჭება" }, "bulkCollectionAssignmentDialogDescriptionSingular": { "message": "Only organization members with access to these collections will be able to see the item." @@ -4298,13 +4380,13 @@ } }, "addField": { - "message": "Add field" + "message": "ველის დამატება" }, "add": { - "message": "Add" + "message": "დამატება" }, "fieldType": { - "message": "Field type" + "message": "ველის ტიპი" }, "fieldLabel": { "message": "Field label" @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4489,13 +4577,13 @@ "message": "Show number of login autofill suggestions on extension icon" }, "systemDefault": { - "message": "System default" + "message": "სისტემურად ნაგულისხმევი" }, "enterprisePolicyRequirementsApplied": { "message": "Enterprise policy requirements have been applied to this setting" }, "retry": { - "message": "Retry" + "message": "თავიდან ცდა" }, "vaultCustomTimeoutMinimum": { "message": "Minimum custom timeout is 1 minute." @@ -4525,7 +4613,7 @@ "message": "Items that have been in trash more than 30 days will automatically be deleted" }, "restore": { - "message": "Restore" + "message": "აღდგენა" }, "deleteForever": { "message": "Delete forever" @@ -4534,6 +4622,159 @@ "message": "You don't have permission to edit this item" }, "authenticating": { - "message": "Authenticating" + "message": "ავთენტიკაცია" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index e52f78583d4..3414760d2a3 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Create account" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Set a strong password" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Join organization" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organization by setting a master password." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Copy password" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "Copy note" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Copy notes" }, + "fill": { + "message": "Fill", + "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": "Autofill" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Generate password" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Launch website" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Website" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Log in" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Restart registration" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "Server URL" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "API server URL" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Password history" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "Back" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Clear history" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "Remove" @@ -2449,8 +2511,8 @@ "message": "Optionally require a password for users to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Your organization requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Verification required", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Generate username" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Username type" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Generate an email alias with an external forwarding service." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ error: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 4751a2e3a5b..96078aa7edb 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "ಖಾತೆ ತೆರೆ" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Set a strong password" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Join organization" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organization by setting a master password." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "ಪಾಸ್ವರ್ಡ್ ನಕಲಿಸಿ" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "ಟಿಪ್ಪಣಿ ನಕಲಿಸಿ" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Copy notes" }, + "fill": { + "message": "Fill", + "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": "ಸ್ವಯಂ ಭರ್ತಿ" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "ಪಾಸ್ವರ್ಡ್ ರಚಿಸಿ" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "ಪಾಸ್ವರ್ಡ್ ಅನ್ನು ಪುನರುತ್ಪಾದಿಸಿ" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Launch website" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "ಜಾಲತಾಣ" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Log in" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Restart registration" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "ಸರ್ವರ್ URL" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "API ಸರ್ವರ್ URL" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "ಪಾಸ್ವರ್ಡ್ ಇತಿಹಾಸ" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "ಹಿಂದಕ್ಕೆ" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Clear history" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "ತೆಗೆ" @@ -2449,8 +2511,8 @@ "message": "ಈ ಕಳುಹಿಸುವಿಕೆಯನ್ನು ಪ್ರವೇಶಿಸಲು ಬಳಕೆದಾರರಿಗೆ ಪಾಸ್‌ವರ್ಡ್ ಐಚ್ ಗತ್ಯವಿದೆ.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Your organization requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Verification required", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Generate username" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Username type" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Generate an email alias with an external forwarding service." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ error: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index 7d4e8a7bc68..de015d285a4 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "계정 만들기" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "비밀번호 설정" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Join organization" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organization by setting a master password." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "비밀번호 복사" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "메모 복사" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Copy notes" }, + "fill": { + "message": "Fill", + "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": "자동 완성" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "비밀번호 생성" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "비밀번호 재생성" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "웹사이트 열기" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "웹 사이트" }, @@ -805,6 +845,9 @@ "logIn": { "message": "로그인" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Restart registration" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "서버 URL" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "API 서버 URL" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "비밀번호 변경 기록" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "뒤로" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Clear history" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "제거" @@ -2449,8 +2511,8 @@ "message": "이 Send에 접근하기 위해 암호를 입력하도록 선택적으로 요구합니다.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Your organization requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "인증 필요", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "아이디 생성" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "아이디 유형" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Generate an email alias with an external forwarding service." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ error: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 3249db20116..716c474b53e 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Sukurti paskyrą" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Nustatyti stiprų slaptažodį" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Join organization" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organization by setting a master password." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Kopijuoti slaptažodį" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "Kopijuoti pastabą" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Copy notes" }, + "fill": { + "message": "Fill", + "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": "Automatinis užpildymas" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Sugeneruoti slaptažodį" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "Generuoti slaptažodį iš naujo" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Atidaryti svetainę" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Tinklapis" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Log in" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Restart registration" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "Serverio URL" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "API serverio nuoroda" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Slaptažodžio istorija" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "Atgal" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Clear history" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "Pašalinti" @@ -2449,8 +2511,8 @@ "message": "Pasirinktinai reikalauti slaptažodžio, kad vartotojai galėtų pasiekti šį „Send“.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Jūsų organizacija reikalauja Jus nustatyti pagrindinį slaptažodį.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Reikalingas patikrinimas", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Generuoti vartotojo vardą" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Vartotojo prisijungimo vardo tipas" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Sugeneruoti el. pašto slapyvardį su išorine persiuntimo paslauga." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "„$SERVICENAME$“ klaida: $ERRORMESSAGE$.", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index dd60641046d..8e277cf7f8a 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Izveidot kontu" }, + "newToBitwarden": { + "message": "Bitwarden iepriekš nav izmantots?" + }, + "logInWithPasskey": { + "message": "Pieteikties ar piekļuves atslēgu" + }, + "useSingleSignOn": { + "message": "Izmantot vienoto pieteikšanos" + }, + "welcomeBack": { + "message": "Laipni lūdzam atpakaļ" + }, "setAStrongPassword": { "message": "Jāiestata droša parole" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Pievienoties apvienībai" }, + "joinOrganizationName": { + "message": "Pievienoties $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Pabeigt pievienošanos šai apvienībai ar galvenās paroles iestatīšanu." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Ievietot paroli starpliktuvē" }, + "copyPassphrase": { + "message": "Ievietot paroles vārdkopu starpliktuvē" + }, "copyNote": { "message": "Ievietot piezīmi starpliktuvē" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Ievietot piezīmes starpliktuvē" }, + "fill": { + "message": "Aizpildīt", + "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": "Automātiskā aizpilde" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Veidot paroli" }, + "generatePassphrase": { + "message": "Izveidot paroles vārdkopu" + }, "regeneratePassword": { "message": "Pārizveidot paroli" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Atvērt tīmekļvietni" }, + "launchWebsiteName": { + "message": "Palaist tīmekļvietni $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Tīmekļa vietne" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Pieteikties" }, + "logInToBitwarden": { + "message": "Pieteikties Bitwarden" + }, "restartRegistration": { "message": "Sākt reģistrēšanos no jauna" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "Servera URL" }, + "selfHostBaseUrl": { + "message": "Pašmitināta servera URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "API servera URL" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Rādīt automātiskās aizpildes ieteikumuis veidlapu laukos" }, + "showInlineMenuIdentitiesLabel": { + "message": "Attēlot identitātes kā ieteikumus" + }, + "showInlineMenuCardsLabel": { + "message": "Attēlot kartes kā ieteikumus" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Attēlot ieteikumus, kad tiek atlasīta ikona" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Paroļu vēsture" }, + "generatorHistory": { + "message": "Veidotāja vēsture" + }, + "clearGeneratorHistoryTitle": { + "message": "Iztīrīt veidotāja vēsturi" + }, + "cleargGeneratorHistoryDescription": { + "message": "Turpinot visi veidotāja vēstures ieraksti tiks neatgrieziniski izdzēsti. Vai tiešām turpināt?" + }, "back": { "message": "Atpakaļ" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Notīrīt vēsturi" }, - "noPasswordsToShow": { - "message": "Nav paroļu, ko parādīt" + "nothingToShow": { + "message": "Nav nekā, ko parādīt" }, - "noRecentlyGeneratedPassword": { - "message": "Pēdējā laikā nav izveidota neviena parole" + "nothingGeneratedRecently": { + "message": "Pēdējā laikā nav nekas izveidots" }, "remove": { "message": "Noņemt" @@ -2449,8 +2511,8 @@ "message": "Pēc izvēles pieprasīt paroli, lai lietotāji varētu piekļūt šim Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Pieprasīt šo paroli, lai apskatītu Send.", + "sendPasswordDescV3": { + "message": "Pēc izvēles pievieno paroli, lai saņēmēji varētu piekļūt šim Send!", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Apvienība pieprasa iestatīt galveno paroli.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "no $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Nepieciešams apliecinājums", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Izveidot lietotājvārdu" }, + "generateEmail": { + "message": "Izveidot e-pastu" + }, "usernameType": { "message": "Lietotājvārda veids" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Izveidot e-pastu aizstājvārdu ar ārēju pārvirzīšanas pakalpojumu." }, + "forwarderDomainName": { + "message": "E-pasta domēns", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Jāizvēlas domēns, kuru atbalsta atlasītais pakalpojums", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ kļūda: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Vienuma atrašanās vieta" }, + "fileSend": { + "message": "Datnes Send" + }, "fileSends": { "message": "Datņu Send" }, + "textSend": { + "message": "Teksta Send" + }, "textSends": { "message": "Teksta Send" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Autentificē" + }, + "fillGeneratedPassword": { + "message": "Aizpildīt izveidoto paroli", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Parole pārizveidota", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Saglabāt pieteikšanās vienumu Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Atstarpe", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Atpakaļpēdiņa", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Izsaukuma zīme", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At zīme", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Restes zīme", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dolāra zīme", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Procentu zīme", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Jumtiņš", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Un zīme", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisks", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Kreisās iekavas", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Labās iekavas", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Apakšsvītra", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Defise", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Pluss", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Vienādības zīme", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Kreisā figūriekava", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Labā figūriekava", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Kreisā kvadrātiekava", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Labā kvadrātiekava", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Stateniska svītra", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Atpakaļslīpsvītra", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Kols", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semikols", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Divkāršās pēdiņas", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Vienpēdiņas", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Mazāks par", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Lielāks par", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Komats", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Punkts", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Jautājuma zīme", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Slīpsvītra", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Mazie burti" + }, + "uppercaseAriaLabel": { + "message": "Lielie burti" + }, + "generatedPassword": { + "message": "Izveidotā parole" } } diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index 9e285ceeb43..2cfabae5b96 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "അക്കൗണ്ട് സൃഷ്ടിക്കുക" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Set a strong password" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Join organization" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organization by setting a master password." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "പാസ്‌വേഡ് പകർത്തുക" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "കുറിപ്പ് പകർത്തുക" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Copy notes" }, + "fill": { + "message": "Fill", + "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": "ഓട്ടോഫിൽ" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "പാസ്‌വേഡ് സൃഷ്ടിക്കുക" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "പാസ്സ്‌വേഡ് വീണ്ടും സൃഷ്ടിക്കുക" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Launch website" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "വെബ്സൈറ്റ്" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Log in" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Restart registration" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "സെർവർ URL" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "API സെർവർ URL" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "പാസ്സ്‌വേഡ് നാൾവഴി" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "പുറകോട്ട്" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Clear history" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "നീക്കുക" @@ -2449,8 +2511,8 @@ "message": "Optionally require a password for users to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Your organization requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Verification required", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Generate username" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Username type" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Generate an email alias with an external forwarding service." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ error: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index 009a1ffc2fa..d8e86c4c9e8 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "खाते तयार करा" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Set a strong password" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Join organization" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organization by setting a master password." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "पासवर्ड कॉपी करा" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "टीप कॉपी करा" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Copy notes" }, + "fill": { + "message": "Fill", + "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": "स्वयंभरण" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Generate password" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "पासवर्ड पुनर्जनित करा" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Launch website" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "संकेतस्थळ" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Log in" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Restart registration" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "Server URL" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "API server URL" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Password history" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "Back" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Clear history" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "Remove" @@ -2449,8 +2511,8 @@ "message": "Optionally require a password for users to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Your organization requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Verification required", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Generate username" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Username type" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Generate an email alias with an external forwarding service." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ error: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index e52f78583d4..3414760d2a3 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Create account" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Set a strong password" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Join organization" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organization by setting a master password." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Copy password" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "Copy note" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Copy notes" }, + "fill": { + "message": "Fill", + "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": "Autofill" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Generate password" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Launch website" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Website" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Log in" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Restart registration" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "Server URL" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "API server URL" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Password history" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "Back" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Clear history" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "Remove" @@ -2449,8 +2511,8 @@ "message": "Optionally require a password for users to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Your organization requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Verification required", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Generate username" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Username type" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Generate an email alias with an external forwarding service." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ error: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 096b764f1fc..ecd0b1e5c10 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Opprett en konto" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Set a strong password" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Join organization" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organization by setting a master password." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Kopier passordet" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "Kopier notatet" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Copy notes" }, + "fill": { + "message": "Fill", + "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": "Auto-utfylling" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Generer et passord" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "Omgenerer et passord" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Launch website" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Nettsted" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Log in" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Restart registration" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "Tjener-nettadresse" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "API-tjenernettadresse" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Passordhistorikk" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "Tilbake" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Clear history" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "Fjern" @@ -2449,8 +2511,8 @@ "message": "Eventuelt krever et passord for brukere å få tilgang til denne Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Your organization requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Verifisering kreves", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Generer brukernavn" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Brukernavntype" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Generer et e-postalias med en ekstern videresendingstjeneste." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ error: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index e52f78583d4..3414760d2a3 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Create account" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Set a strong password" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Join organization" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organization by setting a master password." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Copy password" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "Copy note" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Copy notes" }, + "fill": { + "message": "Fill", + "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": "Autofill" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Generate password" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Launch website" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Website" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Log in" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Restart registration" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "Server URL" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "API server URL" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Password history" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "Back" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Clear history" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "Remove" @@ -2449,8 +2511,8 @@ "message": "Optionally require a password for users to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Your organization requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Verification required", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Generate username" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Username type" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Generate an email alias with an external forwarding service." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ error: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index dcc1ac56e9b..fe415cfb1dc 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Account aanmaken" }, + "newToBitwarden": { + "message": "Nieuw bij Bitwarden?" + }, + "logInWithPasskey": { + "message": "Inloggen met passkey" + }, + "useSingleSignOn": { + "message": "Single sign-on gebruiken" + }, + "welcomeBack": { + "message": "Welkom terug" + }, "setAStrongPassword": { "message": "Sterk wachtwoord instellen" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Lid van organisatie worden" }, + "joinOrganizationName": { + "message": "Aansluiten bij $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Voltooi je lidmaatschap aan deze organisatie door een hoofdwachtwoord in te stellen." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Wachtwoord kopiëren" }, + "copyPassphrase": { + "message": "Wachtwoordzin kopiëren" + }, "copyNote": { "message": "Notitie kopiëren" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Notities kopiëren" }, + "fill": { + "message": "Invullen", + "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": "Auto-invullen" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Wachtwoord genereren" }, + "generatePassphrase": { + "message": "Wachtwoordzin genereren" + }, "regeneratePassword": { "message": "Wachtwoord opnieuw genereren" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Website openen" }, + "launchWebsiteName": { + "message": "Start website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Website" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Inloggen" }, + "logInToBitwarden": { + "message": "Inloggen op Bitwarden" + }, "restartRegistration": { "message": "Registratie herstarten" }, @@ -1046,7 +1089,7 @@ "message": "Bestandsindeling" }, "fileEncryptedExportWarningDesc": { - "message": "We beveiligen deze bestandsexport met een wachtwoord beveiligd, je hebt het bestandswachtwoord nodig om het te decoderen." + "message": "We beveiligen deze bestandsexport met een wachtwoord, je hebt het bestandswachtwoord nodig om het bestand te decoderen." }, "filePassword": { "message": "Bestandswachtwoord" @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "Server-URL" }, + "selfHostBaseUrl": { + "message": "URL zelfgehoste server", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "API server-URL" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Suggesties voor automatisch invullen op formuliervelden weergeven" }, + "showInlineMenuIdentitiesLabel": { + "message": "Identiteiten als suggesties weergeven" + }, + "showInlineMenuCardsLabel": { + "message": "Kaarten als suggesties weergeven" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Suggesties weergeven wanneer pictogram is geselecteerd" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Geschiedenis" }, + "generatorHistory": { + "message": "Generatorgeschiedenis" + }, + "clearGeneratorHistoryTitle": { + "message": "Generatorgeschiedenis wissen" + }, + "cleargGeneratorHistoryDescription": { + "message": "Als je doorgaat, wis je definitief de geschiedenis van de generator. Weet je zeker dat je wilt doorgaan?" + }, "back": { "message": "Terug" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Geschiedenis wissen" }, - "noPasswordsToShow": { - "message": "Geen wachtwoorden weer te geven" + "nothingToShow": { + "message": "Niets om te laten zien" }, - "noRecentlyGeneratedPassword": { - "message": "Je hebt onlangs geen wachtwoord gegenereerd" + "nothingGeneratedRecently": { + "message": "Je hebt de laatste tijd niets gegenereerd" }, "remove": { "message": "Verwijderen" @@ -2449,8 +2511,8 @@ "message": "Vereis optioneel een wachtwoord voor gebruikers om toegang te krijgen tot deze Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Dit wachtwoord vereisen voor het weergeven van de Send.", + "sendPasswordDescV3": { + "message": "Voeg een optioneel wachtwoord toe voor ontvangers om toegang te krijgen tot deze Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Your organization requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "van $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Verificatie vereist", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Gebruikersnamen genereren" }, + "generateEmail": { + "message": "E-mailadres genereren" + }, "usernameType": { "message": "Type gebruikersnaam" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Genereer een e-mailalias met een externe doorschakelservice." }, + "forwarderDomainName": { + "message": "E-maildomein", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Kies een domein dat wordt ondersteund door de geselecteerde dienst", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ error: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Itemlocatie" }, + "fileSend": { + "message": "Bestand-Sends" + }, "fileSends": { "message": "Bestand-Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Tekst-Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Aan het inloggen" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index e52f78583d4..3414760d2a3 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Create account" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Set a strong password" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Join organization" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organization by setting a master password." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Copy password" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "Copy note" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Copy notes" }, + "fill": { + "message": "Fill", + "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": "Autofill" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Generate password" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Launch website" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Website" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Log in" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Restart registration" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "Server URL" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "API server URL" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Password history" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "Back" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Clear history" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "Remove" @@ -2449,8 +2511,8 @@ "message": "Optionally require a password for users to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Your organization requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Verification required", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Generate username" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Username type" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Generate an email alias with an external forwarding service." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ error: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index e52f78583d4..3414760d2a3 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Create account" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Set a strong password" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Join organization" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organization by setting a master password." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Copy password" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "Copy note" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Copy notes" }, + "fill": { + "message": "Fill", + "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": "Autofill" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Generate password" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Launch website" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Website" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Log in" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Restart registration" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "Server URL" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "API server URL" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Password history" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "Back" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Clear history" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "Remove" @@ -2449,8 +2511,8 @@ "message": "Optionally require a password for users to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Your organization requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Verification required", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Generate username" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Username type" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Generate an email alias with an external forwarding service." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ error: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index d05ccc7ae0b..39f5541e596 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -19,11 +19,23 @@ "createAccount": { "message": "Utwórz konto" }, + "newToBitwarden": { + "message": "Nowy użytkownik Bitwarden?" + }, + "logInWithPasskey": { + "message": "Zaloguj się używając passkey" + }, + "useSingleSignOn": { + "message": "Użyj jednokrotnego logowania" + }, + "welcomeBack": { + "message": "Witaj ponownie" + }, "setAStrongPassword": { "message": "Ustaw silne hasło" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "Ukończ tworzenie konta poprzez ustawienie hasła" + "message": "Ukończ tworzenie konta poprzez utworzenie hasła" }, "enterpriseSingleSignOn": { "message": "Logowanie jednokrotne" @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Dołącz do organizacji" }, + "joinOrganizationName": { + "message": "Dołącz do $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Zakończ dołączanie do tej organizacji przez ustawienie hasła głównego." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Kopiuj hasło" }, + "copyPassphrase": { + "message": "Kopiuj frazę bezpieczeństwa" + }, "copyNote": { "message": "Kopiuj notatkę" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Kopiuj notatki" }, + "fill": { + "message": "Uzupełnij", + "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": "Autouzupełnianie" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Wygeneruj hasło" }, + "generatePassphrase": { + "message": "Wygenruj frazę zabezpieczającą" + }, "regeneratePassword": { "message": "Wygeneruj ponownie hasło" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Otwórz stronę" }, + "launchWebsiteName": { + "message": "Otwórz stronę internetową $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Strona" }, @@ -605,13 +645,13 @@ "message": "Sejf jest zablokowany. Zweryfikuj swoją tożsamość, aby kontynuować." }, "yourVaultIsLockedV2": { - "message": "Your vault is locked" + "message": "Twój sejf jest zablokowany" }, "yourAccountIsLocked": { - "message": "Your account is locked" + "message": "Twoje konto jest zablokowane" }, "or": { - "message": "or" + "message": "lub" }, "unlock": { "message": "Odblokuj" @@ -805,6 +845,9 @@ "logIn": { "message": "Zaloguj się" }, + "logInToBitwarden": { + "message": "Zaloguj się do Bitwarden" + }, "restartRegistration": { "message": "Zrestartuj rejestrację" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "Adres URL serwera" }, + "selfHostBaseUrl": { + "message": "URL samodzielnie hostowanego serwera", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "Adres URL serwera API" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Pokaż sugestie autouzupełniania na polach formularza" }, + "showInlineMenuIdentitiesLabel": { + "message": "Pokazuj tożsamości jako sugestie" + }, + "showInlineMenuCardsLabel": { + "message": "Pokazuj karty jako sugestie" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Wyświetlaj sugestie kiedy ikona jest zaznaczona" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Historia hasła" }, + "generatorHistory": { + "message": "Historia generatora" + }, + "clearGeneratorHistoryTitle": { + "message": "Wyczyść historię generatora" + }, + "cleargGeneratorHistoryDescription": { + "message": "Jeśli kontynuujesz, wszystkie elementy zostaną usunięte z historii generatora. Czy chcesz kontynuować mimo to?" + }, "back": { "message": "Powrót" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Wyczyść historię" }, - "noPasswordsToShow": { - "message": "Brak haseł do wyświetlenia" + "nothingToShow": { + "message": "Nic do pokazania" }, - "noRecentlyGeneratedPassword": { - "message": "W ostatnim czasie nie wygenerowano hasła" + "nothingGeneratedRecently": { + "message": "Nic nie zostało wygenerowane przez ciebie w ostatnim czasie" }, "remove": { "message": "Usuń" @@ -1946,7 +2008,7 @@ "message": "Odblokuj danymi biometrycznymi" }, "unlockWithMasterPassword": { - "message": "Unlock with master password" + "message": "Odblokuj za pomocą głównego hasła" }, "awaitDesktop": { "message": "Oczekiwanie na potwierdzenie z aplikacji desktopowej" @@ -2449,8 +2511,8 @@ "message": "Opcjonalne hasło dla użytkownika, aby uzyskać dostęp do wysyłki.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Wymagaj tego hasła aby wyświetlić Send.", + "sendPasswordDescV3": { + "message": "Zabezpiecz tę wiadomość hasłem, które będzie wymagane, aby uzyskać do niej dostęp.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2499,11 +2561,11 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInHoursSingle": { - "message": "The Send will be available to anyone with the link for the next 1 hour.", + "message": "Wiadomość będzie dostępna dla każdego z dostępem do tego linku przez następną godzinę.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInHours": { - "message": "The Send will be available to anyone with the link for the next $HOURS$ hours.", + "message": "Wiadomość będzie dostępna dla każdego z dostępem do tego linku przez następne $HOURS$ godzin.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "hours": { @@ -2513,11 +2575,11 @@ } }, "sendExpiresInDaysSingle": { - "message": "The Send will be available to anyone with the link for the next 1 day.", + "message": "Wiadomość będzie dostępna dla każdego z dostępem do tego linku przez 1 dzień.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInDays": { - "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "message": "Wiadomość będzie dostępna dla każdego z dostępem do tego linku przez następne $DAYS$ dni.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "days": { @@ -2647,6 +2709,15 @@ "message": "Twoja organizacja wymaga ustawienia hasła głównego.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "z $TOTAL$ elementów", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Wymagana weryfikacja", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Wygeneruj nazwę użytkownika" }, + "generateEmail": { + "message": "Wygenruj adres e-mail" + }, "usernameType": { "message": "Rodzaj nazwy użytkownika" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Wygeneruj alias adresu e-mail z zewnętrznej usługi przekierowania." }, + "forwarderDomainName": { + "message": "Domena adresu e-mail", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "Błąd $SERVICENAME$: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -3654,7 +3736,7 @@ "message": "Passkey" }, "accessing": { - "message": "Accessing" + "message": "Uzyskiwanie dostępu" }, "passkeyNotCopied": { "message": "Passkey nie zostanie skopiowany" @@ -3681,7 +3763,7 @@ "message": "Brak pasujących loginów dla tej witryny" }, "searchSavePasskeyNewLogin": { - "message": "Search or save passkey as new login" + "message": "Wyszukaj alb zapisz passkey jako nowy login" }, "confirm": { "message": "Potwierdź" @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Lokalizacja elementu" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4534,6 +4622,159 @@ "message": "Nie masz uprawnień do edycji tego elementu" }, "authenticating": { - "message": "Authenticating" + "message": "Uwierzytelnianie" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Hasło zostało ponownie wygenerowane", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Zapisać dane logowania w Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Spacja", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Wykrzyknik", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "Małpa", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hashtag", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Znak dolara", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Znak procenta", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Gwiazdka", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Podkreślenie", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Znak równości", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Lewy nawias klamrowy", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Prawy nawias klamrowy", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Lewy nawias kwadratowy", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Prawy nawias kwadratowy", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 55654fd4355..63ab1eb3d8f 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Criar Conta" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Defina uma senha forte" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Juntar-se à organização" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Termine de juntar-se nessa organização definindo uma senha mestra." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Copiar Senha" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "Copiar Nota" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Copiar Notas" }, + "fill": { + "message": "Fill", + "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": "Autopreencher" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Gerar Senha" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "Gerar Nova Senha" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Abrir site" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Site" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Fazer login" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Reiniciar registro" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "URL do Servidor" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "URL do Servidor da API" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Mostrar sugestões de preenchimento automático nos campos de formulários" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Exibir sugestões quando o ícone for selecionado" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Histórico de Senha" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "Voltar" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Limpar histórico" }, - "noPasswordsToShow": { - "message": "Nenhuma senha para mostrar" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "Você não gerou uma senha recentemente" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "Remover" @@ -2449,8 +2511,8 @@ "message": "Exigir opcionalmente uma senha para os usuários acessarem este Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Exigir essa senha para visualizar o Envio.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Sua organização requer que você defina uma senha mestra.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Verificação necessária", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Gerar Usuário" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Tipo de usuário" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Gere um apelido de e-mail com um serviço de encaminhamento externo." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "Erro $SERVICENAME$: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Localização do Item" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "Arquivos enviados" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Texto enviado" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Autenticando" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 738363679ed..a40a5ec1113 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Criar conta" }, + "newToBitwarden": { + "message": "Novo no Bitwarden?" + }, + "logInWithPasskey": { + "message": "Iniciar sessão com a chave de acesso" + }, + "useSingleSignOn": { + "message": "Utilizar início de sessão único" + }, + "welcomeBack": { + "message": "Bem-vindo de volta" + }, "setAStrongPassword": { "message": "Defina uma palavra-passe forte" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Aderir à organização" }, + "joinOrganizationName": { + "message": "Aderir a $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Conclua a adesão a esta organização ao definir uma palavra-passe mestra." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Copiar palavra-passe" }, + "copyPassphrase": { + "message": "Copiar frase de acesso" + }, "copyNote": { "message": "Copiar nota" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Copiar notas" }, + "fill": { + "message": "Preencher", + "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": "Preencher automaticamente" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Gerar palavra-passe" }, + "generatePassphrase": { + "message": "Gerar frase de acesso" + }, "regeneratePassword": { "message": "Regenerar palavra-passe" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Iniciar site" }, + "launchWebsiteName": { + "message": "Iniciar site $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Site" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Iniciar sessão" }, + "logInToBitwarden": { + "message": "Iniciar sessão no Bitwarden" + }, "restartRegistration": { "message": "Reiniciar registo" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "URL do servidor" }, + "selfHostBaseUrl": { + "message": "URL do servidor auto-hospedado", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "URL do servidor da API" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Mostrar sugestões de preenchimento automático nos campos do formulário" }, + "showInlineMenuIdentitiesLabel": { + "message": "Apresentar as identidades como sugestões" + }, + "showInlineMenuCardsLabel": { + "message": "Apresentar os cartões como sugestões" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Apresentar sugestões quando o ícone é selecionado" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Histórico de palavras-passe" }, + "generatorHistory": { + "message": "Histórico do gerador" + }, + "clearGeneratorHistoryTitle": { + "message": "Limpar o histórico do gerador" + }, + "cleargGeneratorHistoryDescription": { + "message": "Se continuar, todas as entradas serão permanentemente eliminadas do histórico do gerador. Tem a certeza de que pretende continuar?" + }, "back": { "message": "Voltar" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Limpar histórico" }, - "noPasswordsToShow": { - "message": "Não há palavras-passe para mostrar" + "nothingToShow": { + "message": "Nada a mostrar" }, - "noRecentlyGeneratedPassword": { - "message": "Não gerou nenhuma palavra-passe recentemente" + "nothingGeneratedRecently": { + "message": "Não gerou nada recentemente" }, "remove": { "message": "Remover" @@ -2449,8 +2511,8 @@ "message": "Opcionalmente, exigir uma palavra-passe para os utilizadores acederem a este Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Esta palavra-passe é necessária para visualizar o Send.", + "sendPasswordDescV3": { + "message": "Adicione uma palavra-passe opcional para os destinatários acederem a este Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "A sua organização exige a definição de uma palavra-passe mestra.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "de $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Verificação necessária", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Gerar nome de utilizador" }, + "generateEmail": { + "message": "Gerar e-mail" + }, "usernameType": { "message": "Tipo de nome de utilizador" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Gerar um alias de e-mail com um serviço de reencaminhamento externo." }, + "forwarderDomainName": { + "message": "Domínio de e-mail", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Escolha um domínio que seja suportado pelo serviço selecionado", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "Erro no $SERVICENAME$: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Localização do item" }, + "fileSend": { + "message": "Send de ficheiro" + }, "fileSends": { "message": "Sends de ficheiros" }, + "textSend": { + "message": "Send de texto" + }, "textSends": { "message": "Sends de texto" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "A autenticar" + }, + "fillGeneratedPassword": { + "message": "Preencher a palavra-passe gerada", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Palavra-passe gerada novamente", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Guardar credencial no Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Espaço", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Til", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Plicas", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Ponto de exclamação", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "Arroba", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Cardinal", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dólar", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percentagem", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Acento circunflexo", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "E comercial", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisco", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Parêntesis esquerdo", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Parêntesis direito", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hífen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Mais", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Igual", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Chaveta esquerda", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Chaveta direita", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Parêntesis reto esquerdo", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Parêntesis reto direito", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Barra vertical", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Barra invertida", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Dois pontos", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Ponto e vírgula", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Aspas", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Aspas simples", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Menor", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Maior", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Vírgula", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Ponto final", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Ponto de interrogação", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Barra oblíqua", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Minúsculas" + }, + "uppercaseAriaLabel": { + "message": "Maiúsculas" + }, + "generatedPassword": { + "message": "Palavra-passe gerada" } } diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index cbee5127a53..99bf86c9876 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Creare cont" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Setați o parolă puternică" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Alăturați-vă organizației" }, + "joinOrganizationName": { + "message": "Alătură-te $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finalizați aderarea la această organizație prin setarea unei parole principale." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Copiere parolă" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "Copiere notă" }, @@ -129,7 +153,7 @@ "message": "Copiați numărul de licență" }, "copyCustomField": { - "message": "Copy $FIELD$", + "message": "Copiază $FIELD$", "placeholders": { "field": { "content": "$1", @@ -138,10 +162,14 @@ } }, "copyWebsite": { - "message": "Copy website" + "message": "Copiază site-ul" }, "copyNotes": { - "message": "Copy notes" + "message": "Copiază notițele" + }, + "fill": { + "message": "Fill", + "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": "Auto-completare" @@ -195,16 +223,16 @@ "message": "Adăugare articol" }, "accountEmail": { - "message": "Account email" + "message": "Adresa de email a contului" }, "requestHint": { - "message": "Request hint" + "message": "Solicită indiciu" }, "requestPasswordHint": { - "message": "Request password hint" + "message": "Solicită indiciu parolă" }, "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": { - "message": "Enter your account email address and your password hint will be sent to you" + "message": "Introduceți adresa de e-mail a contului și indiciul pentru parolă va fi trimis pe email" }, "passwordHint": { "message": "Indiciu parolă" @@ -329,10 +357,10 @@ "message": "Editare dosar" }, "newFolder": { - "message": "New folder" + "message": "Folder nou" }, "folderName": { - "message": "Folder name" + "message": "Numele folderului" }, "folderHintText": { "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" @@ -398,6 +426,9 @@ "generatePassword": { "message": "Generare parolă" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "Regenerare parolă" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Lansați siteul web" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Sait web" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Autentificare" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Reporniți înregistrarea" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "URL server" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "URL server API" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Istoric parole" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "Înapoi" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Clear history" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "Ștergere" @@ -2449,8 +2511,8 @@ "message": "Opțional, este necesară o parolă pentru ca utilizatorii să acceseze acest Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Your organization requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Verification required", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Generare nume de utilizator" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Tip de nume de utilizator" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Generați un alias de e-mail cu un serviciu de redirecționare extern." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ error: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index ffdb59465dd..e898d68ae86 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Создать аккаунт" }, + "newToBitwarden": { + "message": "Впервые на Bitwarden?" + }, + "logInWithPasskey": { + "message": "Войти с passkey" + }, + "useSingleSignOn": { + "message": "Использовать единый вход" + }, + "welcomeBack": { + "message": "С возвращением" + }, "setAStrongPassword": { "message": "Задайте надежный пароль" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Присоединиться к организации" }, + "joinOrganizationName": { + "message": "Присоединиться к $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Завершите присоединение к этой организации, установив мастер-пароль." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Скопировать пароль" }, + "copyPassphrase": { + "message": "Скопировать парольную фразу" + }, "copyNote": { "message": "Скопировать заметку" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Скопировать заметки" }, + "fill": { + "message": "Заполнить", + "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": "Автозаполнение" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Сгенерировать пароль" }, + "generatePassphrase": { + "message": "Создать парольную фразу" + }, "regeneratePassword": { "message": "Создать новый пароль" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Открыть сайт" }, + "launchWebsiteName": { + "message": "Открыть сайт $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Сайт" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Войти" }, + "logInToBitwarden": { + "message": "Войти в Bitwarden" + }, "restartRegistration": { "message": "Перезапустить регистрацию" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "URL сервера" }, + "selfHostBaseUrl": { + "message": "URL собственного сервера", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "URL API сервера" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Показывать предположения автозаполнения в полях формы" }, + "showInlineMenuIdentitiesLabel": { + "message": "Показывать Личности как предложения" + }, + "showInlineMenuCardsLabel": { + "message": "Показывать Карты как предложения" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Показывать подсказки при выборе значка" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "История паролей" }, + "generatorHistory": { + "message": "История генератора" + }, + "clearGeneratorHistoryTitle": { + "message": "Очистить историю генератора" + }, + "cleargGeneratorHistoryDescription": { + "message": "Если вы продолжите, все записи будут навсегда удалены из истории генератора. Вы уверены, что хотите продолжить?" + }, "back": { "message": "Назад" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Очистить историю" }, - "noPasswordsToShow": { - "message": "Нет паролей для отображения" + "nothingToShow": { + "message": "Нечего показать" }, - "noRecentlyGeneratedPassword": { - "message": "Нет недавно сгенерированных паролей" + "nothingGeneratedRecently": { + "message": "Вы ничего не создавали в последнее время" }, "remove": { "message": "Удалить" @@ -2449,8 +2511,8 @@ "message": "По возможности запрашивать у пользователей пароль для доступа к этой Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Требовать этот пароль для просмотра Send.", + "sendPasswordDescV3": { + "message": "Добавьте опциональный пароль для доступа получателей к этой Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Необходимо установить мастер-пароль для организации.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "из $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Требуется верификация", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Создать имя пользователя" }, + "generateEmail": { + "message": "Сгенерировать email" + }, "usernameType": { "message": "Тип имени пользователя" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Создать псевдоним электронной почты для внешней службы пересылки." }, + "forwarderDomainName": { + "message": "Домен электронной почты", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Выберите домен, который поддерживается выбранным сервисом", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "Ошибка $SERVICENAME$: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Расположение элемента" }, + "fileSend": { + "message": "Файловая Send" + }, "fileSends": { "message": "Файловая Send" }, + "textSend": { + "message": "Текстовая Send" + }, "textSends": { "message": "Текстовая Send" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Аутентификация" + }, + "fillGeneratedPassword": { + "message": "Заполнить сгенерированный пароль", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Пароль сгенерирован", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Сохранить логин в Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Пробел", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Тильда", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Кавычка", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Восклицательный знак", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "Символ At", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Символ хэша", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Символ доллара", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Символ процента", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Каре", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Амперсанд", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Звездочка", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Левая круглая скобка", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Правая круглая скобка", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Подчеркивание", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Дефис", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Плюс", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Равно", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Левая фигурная скобка", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Правая фигурная скобка", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Левая квадратная скобка", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Правая квадратная скобка", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Вертикальная черта", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Обратный слэш", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Двоеточие", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Точка с запятой", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Двойные кавычки", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Одинарная кавычка", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Меньше", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Больше", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Запятая", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Точка", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Вопросительный знак", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Слэш", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Строчные буквы" + }, + "uppercaseAriaLabel": { + "message": "Заглавные буквы" + }, + "generatedPassword": { + "message": "Сгенерированный пароль" } } diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index 030deefac2f..3ff7dde6897 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "ගිණුමක් සාදන්න" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Set a strong password" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Join organization" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organization by setting a master password." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "මුරපදය පිටපත් කරන්න" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "සටහන පිටපත් කරන්න" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Copy notes" }, + "fill": { + "message": "Fill", + "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": "ස්වයං-පිරවීම" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "මුරපදය ජනනය කරන්න" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "මුරපදය ප්රතිජනනය" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Launch website" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "වියමන අඩවිය" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Log in" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Restart registration" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "සේවාදායකය URL" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "API සේවාදායකය URL" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "මුරපද ඉතිහාසය" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "ආපසු" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Clear history" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "ඉවත් කරන්න" @@ -2449,8 +2511,8 @@ "message": "විකල්පයක් ලෙස පරිශීලකයින්ට මෙම යවන්න වෙත ප්රවේශ වීමට මුරපදයක් අවශ්ය වේ.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Your organization requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Verification required", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Generate username" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Username type" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Generate an email alias with an external forwarding service." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ error: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index 87ca21a89e1..e47d80bca26 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Vytvoriť účet" }, + "newToBitwarden": { + "message": "Ste noví na Bitwardene?" + }, + "logInWithPasskey": { + "message": "Prihlásiť sa s prístupovým kľúčom" + }, + "useSingleSignOn": { + "message": "Použiť jednotné prihlásenie" + }, + "welcomeBack": { + "message": "Vitajte späť" + }, "setAStrongPassword": { "message": "Nastavte silné heslo" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Pripojte sa k organizácii" }, + "joinOrganizationName": { + "message": "Pripojiť sa k $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Dokončite pripojenie k tejto organizácii nastavením hlavného hesla." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Kopírovať heslo" }, + "copyPassphrase": { + "message": "Kopírovať prístupovú frázu" + }, "copyNote": { "message": "Kopírovať poznámku" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Kopírovať poznámky" }, + "fill": { + "message": "Vyplniť", + "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": "Automatické vypĺňanie" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Generovať heslo" }, + "generatePassphrase": { + "message": "Generovať prístupovú frázu" + }, "regeneratePassword": { "message": "Vygenerovať nové heslo" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Otvoriť stránku" }, + "launchWebsiteName": { + "message": "Otvoriť stránku $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Webstránka" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Prihlásiť sa" }, + "logInToBitwarden": { + "message": "Prihlásiť sa do Bitwardenu" + }, "restartRegistration": { "message": "Zopakovať registráciu" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "URL servera" }, + "selfHostBaseUrl": { + "message": "Adresa URL vlastného hostingu", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "URL API servera" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Zobraziť návrhy automatického vypĺňania v poliach formulára" }, + "showInlineMenuIdentitiesLabel": { + "message": "Zobrazovať identity ako návrhy" + }, + "showInlineMenuCardsLabel": { + "message": "Zobrazovať karty ako návrhy" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Zobraziť návrhy, keď je vybratá ikona" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "História hesla" }, + "generatorHistory": { + "message": "História generátora" + }, + "clearGeneratorHistoryTitle": { + "message": "Vymazať históriu generátora" + }, + "cleargGeneratorHistoryDescription": { + "message": "Ak budete pokračovať, všetky položky z histórie generátora budu natrvalo vymazané. Naozaj chcete pokračovať?" + }, "back": { "message": "Späť" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Vymazať históriu" }, - "noPasswordsToShow": { - "message": "Žiadne heslá na zobrazenie" + "nothingToShow": { + "message": "Nič na zobrazenie" }, - "noRecentlyGeneratedPassword": { - "message": "V poslednej dobe ste negenerovali žiadne heslá" + "nothingGeneratedRecently": { + "message": "V poslednej dobe ste nič negenerovali" }, "remove": { "message": "Odstrániť" @@ -2449,8 +2511,8 @@ "message": "Voliteľne môžete vyžadovať heslo pre používateľov na prístup k tomuto Sendu.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Na zobrazenie tohto Sendu vyžadovať toto heslo.", + "sendPasswordDescV3": { + "message": "Pridajte voliteľné heslo pre príjemcov na prístup k tomuto Sendu.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Vaša organizácia vyžaduje, aby ste nastavili hlavné heslo.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "z $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Vyžaduje sa overenie", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Vygenerovať používateľské meno" }, + "generateEmail": { + "message": "Generovať e-mail" + }, "usernameType": { "message": "Typ používateľského mena" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Vytvoriť e-mailový alias pomocou externej služby preposielania." }, + "forwarderDomainName": { + "message": "E-mailová doména", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Vyberte doménu, ktorá je podporovaná vybranou službou", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ chyba: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Umiestnenie položky" }, + "fileSend": { + "message": "Send so súborom" + }, "fileSends": { "message": "Sendy so súborom" }, + "textSend": { + "message": "Textový Send" + }, "textSends": { "message": "Textové Sendy" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Overuje sa" + }, + "fillGeneratedPassword": { + "message": "Vložiť vygenerované heslo", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Vygenerované nové heslo", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Uložiť prihlasovacie údaje do Bitwardenu?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Medzera", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilda", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Opačný dĺžeň", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Výkričník", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "Zavináč", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Mriežka", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dolár", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percento", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Striežka", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Hviezdička", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Ľavá zátvorka", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Pravá zátvorka", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Podčiarkovník", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Spojovník", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Rovná sa", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Ľavá zložená zátvorka", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Pravá zložená zátvorka", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Ľavá hranatá zátvorka", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Pravá hranatá zátvorka", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Rúra", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Spätná lomka", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Dvojbodka", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Bodkočiarka", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Dvojité úvodzovky", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Jednoduché úvodzovky", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Ľavá lomená zátvorka", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Pravá lomená zátvorka", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Čiarka", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Bodka", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Otáznik", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Lomka", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Malé písmená" + }, + "uppercaseAriaLabel": { + "message": "Veľké písmená" + }, + "generatedPassword": { + "message": "Vygenerované heslo" } } diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 50896bcd21f..7ee7d1c5c77 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Ustvari račun" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Set a strong password" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Join organization" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organization by setting a master password." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Kopiraj geslo" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "Kopiraj opombo" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Copy notes" }, + "fill": { + "message": "Fill", + "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": "Samodejno izpolnjevanje" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Generiraj geslo" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "Ponovno ustvari geslo" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Launch website" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Spletna stran" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Log in" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Restart registration" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "URL naslov strežnika" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "URL naslov API strežnika" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Zgodovina gesel" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "Nazaj" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Clear history" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "Odstrani" @@ -2449,8 +2511,8 @@ "message": "Za dostop do te pošiljke lahko nastavite geslo.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Your organization requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Verification required", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Ustvari uporabniško ime" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Vrsta uporabniškega imena" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Generate an email alias with an external forwarding service." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ error: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index f9c366ce5fc..ad9ee20f295 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Креирај налог" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Поставите јаку лозинку" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Придружи Организацију" }, + "joinOrganizationName": { + "message": "Придружити се $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Завршите придруживање овој организацији постављањем главне лозинке." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Копирај лозинку" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "Копирај белешку" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Копирати белешке" }, + "fill": { + "message": "Fill", + "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": "Аутоматско допуњавање" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Генерисање лозинке" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "Поново генериши лозинку" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Покрените веб локацију" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Веб сајт" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Пријави се" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Поново покрените регистрацију" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "УРЛ Сервера" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "УРЛ АПИ Сервера" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Прикажи предлоге за ауто-попуњавање у пољима обрасца" }, + "showInlineMenuIdentitiesLabel": { + "message": "Приказати идентитете као предлоге" + }, + "showInlineMenuCardsLabel": { + "message": "Приказати картице као предлоге" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Приказати предлоге када је изабрана икона" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Историја Лозинке" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "Назад" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Обриши историју" }, - "noPasswordsToShow": { - "message": "Нема лозинке за приказ" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "Нисте недавно генерисали лозинку" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "Уклони" @@ -2449,8 +2511,8 @@ "message": "Опционално захтевајте лозинку за приступ корисницима „Send“-у.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Захтева ову лозинку за преглед Send-а.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Ваша организација захтева да поставите главну лозинку.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "од $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Потребдна верификација", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Генериши име" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Тип имена" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Генеришите псеудоним е-поште помоћу екстерне услуге прослеђивања." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ грешка: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Смештај ставке" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "Датотека „Send“" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Текст „Send“" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Аутентификација" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 2852e3ad1f3..93a0b899a3c 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Skapa konto" }, + "newToBitwarden": { + "message": "Ny på Bitwarden?" + }, + "logInWithPasskey": { + "message": "Logga in med nyckel" + }, + "useSingleSignOn": { + "message": "Använd Single Sign-On" + }, + "welcomeBack": { + "message": "Välkommen tillbaka" + }, "setAStrongPassword": { "message": "Ställ in ett starkt lösenord" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Join organization" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organization by setting a master password." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Kopiera lösenord" }, + "copyPassphrase": { + "message": "Kopiera lösenfras" + }, "copyNote": { "message": "Kopiera anteckning" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Kopiera anteckningar" }, + "fill": { + "message": "Fill", + "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": "Fyll i automatiskt" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Generera lösenord" }, + "generatePassphrase": { + "message": "Generera lösenfras" + }, "regeneratePassword": { "message": "Återskapa lösenord" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Launch website" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Webbplats" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Logga in" }, + "logInToBitwarden": { + "message": "Logga in på Bitwarden" + }, "restartRegistration": { "message": "Restart registration" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "Server-URL" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "API-server-URL" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Visa identiteter som förslag" + }, + "showInlineMenuCardsLabel": { + "message": "Visa kort som förslag" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Lösenordshistorik" }, + "generatorHistory": { + "message": "Generatorhistorik" + }, + "clearGeneratorHistoryTitle": { + "message": "Rensa generatorhistorik" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "Tillbaka" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Rensa historik" }, - "noPasswordsToShow": { - "message": "Inga lösenord att visa" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "Ta bort" @@ -2040,7 +2102,7 @@ "message": "Fyll i automatiskt och spara" }, "fillAndSave": { - "message": "Fill and save" + "message": "Fyll och spara" }, "autoFillSuccessAndSavedUri": { "message": "Fyllde i objektet automatiskt och sparade URI:n" @@ -2449,8 +2511,8 @@ "message": "Kräv valfritt ett lösenord för att användare ska komma åt denna Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Din organisation kräver att du anger ett huvudlösenord.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "av $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Verifiering krävs", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Generera användarnamn" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Typ av användarnamn" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Skapa ett e-postalias med en extern vidarebefordranstjänst." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$-fel: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -3681,7 +3763,7 @@ "message": "No matching logins for this site" }, "searchSavePasskeyNewLogin": { - "message": "Search or save passkey as new login" + "message": "Sök eller spara nyckel som ny inloggning" }, "confirm": { "message": "Bekräfta" @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Spara inloggning på Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index e52f78583d4..3414760d2a3 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Create account" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Set a strong password" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Join organization" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organization by setting a master password." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Copy password" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "Copy note" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Copy notes" }, + "fill": { + "message": "Fill", + "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": "Autofill" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Generate password" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "Regenerate password" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Launch website" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Website" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Log in" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Restart registration" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "Server URL" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "API server URL" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Password history" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "Back" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Clear history" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "Remove" @@ -2449,8 +2511,8 @@ "message": "Optionally require a password for users to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Your organization requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Verification required", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Generate username" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Username type" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Generate an email alias with an external forwarding service." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ error: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index f4cba694d49..3b6ac3ebd15 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "สร้างบัญชี" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Set a strong password" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Join organization" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Finish joining this organization by setting a master password." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "คัดลอกรหัสผ่าน" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "Copy Note" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Copy notes" }, + "fill": { + "message": "Fill", + "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": "กรอกข้อมูลอัตโนมัติ" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Generate Password" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "Regenerate Password" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Launch website" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "เว็บไซต์" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Log in" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Restart registration" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "URL ของเซิร์ฟเวอร์" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "API Server URL" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "ประวัติของรหัสผ่าน" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "ย้อนกลับ" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Clear history" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "ลบ" @@ -2449,8 +2511,8 @@ "message": "Optionally require a password for users to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Your organization requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Verification required", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Generate username" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Username type" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Generate an email alias with an external forwarding service." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ error: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index b26f3fe64aa..063dd63ede9 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Hesap oluştur" }, + "newToBitwarden": { + "message": "Bitwarden'da yeni misiniz?" + }, + "logInWithPasskey": { + "message": "Geçiş anahtarıyla giriş yap" + }, + "useSingleSignOn": { + "message": "Çoklu oturum açma kullan" + }, + "welcomeBack": { + "message": "Tekrar hoş geldiniz" + }, "setAStrongPassword": { "message": "Güçlü bir parola belirleyin" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Kuruluşa katıl" }, + "joinOrganizationName": { + "message": "$ORGANIZATIONNAME$ kuruluşuna katıl", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Kuruluşa katılmayı tamamlamak için ana parolanızı belirleyin." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Parolayı kopyala" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "Notu kopyala" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Notları kopyala" }, + "fill": { + "message": "Doldur", + "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": "Otomatik doldur" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Parola oluştur" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "Yeni parola oluştur" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Web sitesini aç" }, + "launchWebsiteName": { + "message": "$ITEMNAME$ web sitesini aç", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Web sitesi" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Giriş yap" }, + "logInToBitwarden": { + "message": "Bitwarden'a giriş yapın" + }, "restartRegistration": { "message": "Kaydı yeniden başlat" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "Sunucu URL'si" }, + "selfHostBaseUrl": { + "message": "Kendi kendine barındırılan sunucu URL'si", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "API sunucu URL'si" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Form alanlarında otomatik doldurma önerilerini göster" }, + "showInlineMenuIdentitiesLabel": { + "message": "Kimlikleri öneri olarak göster" + }, + "showInlineMenuCardsLabel": { + "message": "Kartları öneri olarak göster" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Simge seçildiğinde önerileri göster" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Parola geçmişi" }, + "generatorHistory": { + "message": "Üreteç geçmişi" + }, + "clearGeneratorHistoryTitle": { + "message": "Üreteç geçmişini temizle" + }, + "cleargGeneratorHistoryDescription": { + "message": "Devam ederseniz üreteç geçmişindeki tüm kayıtlar kalıcı olarak silinecektir. Devam etmek istediğinizden emin misiniz?" + }, "back": { "message": "Geri" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Geçmişi temizle" }, - "noPasswordsToShow": { - "message": "Gösterilecek parola yok" + "nothingToShow": { + "message": "Gösterilecek bir şey yok" }, - "noRecentlyGeneratedPassword": { - "message": "Yakın zamanda parola üretmediniz" + "nothingGeneratedRecently": { + "message": "Yakın zamanda herhangi bir şey üretmediniz" }, "remove": { "message": "Kaldır" @@ -2449,8 +2511,8 @@ "message": "Kullanıcıların bu Send'e erişmek için parola girmelerini isteyebilirsiniz.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Send'i görüntülemek için bu parolayı iste.", + "sendPasswordDescV3": { + "message": "Alıcıların bu Send'e erişmesi için isterseniz parola ekleyebilirsiniz.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Kuruluşunuz bir ana parola belirlemenizi gerektiriyor.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "/ $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Doğrulama gerekli", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Kullanıcı adı oluştur" }, + "generateEmail": { + "message": "E-posta oluştur" + }, "usernameType": { "message": "Kullanıcı adı türü" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Harici bir yönlendirme servisiyle e-posta maskesi oluştur." }, + "forwarderDomainName": { + "message": "E-posta alan adı", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Seçtiğiniz servisin desteklediği bir alan adı seçin", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ hatası: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Kayıt konumu" }, + "fileSend": { + "message": "Dosya Send'i" + }, "fileSends": { "message": "Dosya Send'leri" }, + "textSend": { + "message": "Metin Send'i" + }, "textSends": { "message": "Metin Send'leri" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Kimlik doğrulanıyor" + }, + "fillGeneratedPassword": { + "message": "Üretilen parolayı doldur", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Parola yeniden üretildi", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Hesap Bitwarden'a kaydedilsin mi?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Boşluk", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Ters tırnak", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Ünlem işareti", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At işareti", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Kare işareti", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dolar işareti", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Yüzde işareti", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Düzeltme işareti", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ve işareti", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Yıldız işareti", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Sol parantez", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Sağ parantez", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Alt çizgi", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Tire", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Artı", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Eşittir", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Sol küme parantezi", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Sağ küme parantezi", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Sol köşeli parantez", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Sağ köşeli parantez", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Çubuk", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Ters eğik çizgi", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "İki nokta", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Noktalı virgül", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Çift tırnak", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Tek tırnak", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Küçüktür", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Büyüktür", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Virgül", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Nokta", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Soru işareti", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Bölü işareti", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Küçük harf" + }, + "uppercaseAriaLabel": { + "message": "Büyük harf" + }, + "generatedPassword": { + "message": "Üretilen parola" } } diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index 702b58a62c9..25d12ca2600 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Створити обліковий запис" }, + "newToBitwarden": { + "message": "Вперше у Bitwarden?" + }, + "logInWithPasskey": { + "message": "Увійти з ключем доступу" + }, + "useSingleSignOn": { + "message": "Використовувати єдиний вхід" + }, + "welcomeBack": { + "message": "З поверненням" + }, "setAStrongPassword": { "message": "Встановіть надійний пароль" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Приєднатися до організації" }, + "joinOrganizationName": { + "message": "Приєднатися до $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Завершіть приєднання до цієї організації, встановивши головний пароль." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Копіювати пароль" }, + "copyPassphrase": { + "message": "Копіювати парольну фразу" + }, "copyNote": { "message": "Копіювати нотатку" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Копіювати нотатки" }, + "fill": { + "message": "Заповнити", + "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": "Автозаповнення" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Генерувати пароль" }, + "generatePassphrase": { + "message": "Генерувати парольну фразу" + }, "regeneratePassword": { "message": "Генерувати новий" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Відкрити вебсайт" }, + "launchWebsiteName": { + "message": "Відкрити вебсайт $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Вебсайт" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Увійти" }, + "logInToBitwarden": { + "message": "Увійти в Bitwarden" + }, "restartRegistration": { "message": "Перезапустити реєстрацію" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "URL-адреса сервера" }, + "selfHostBaseUrl": { + "message": "URL-адреса власного сервера", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "URL-адреса сервера API" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Пропозиції автозаповнення на полях форм" }, + "showInlineMenuIdentitiesLabel": { + "message": "Показувати посвідчення як пропозиції" + }, + "showInlineMenuCardsLabel": { + "message": "Показувати картки як пропозиції" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Показувати пропозиції, якщо вибрано піктограму" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Історія паролів" }, + "generatorHistory": { + "message": "Історія генератора" + }, + "clearGeneratorHistoryTitle": { + "message": "Очистити історію генератора" + }, + "cleargGeneratorHistoryDescription": { + "message": "Якщо ви продовжите, усі записи будуть остаточно видалені з історії генератора. Справді продовжити?" + }, "back": { "message": "Назад" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Очистити історію" }, - "noPasswordsToShow": { - "message": "Немає паролів" + "nothingToShow": { + "message": "Немає даних для показу" }, - "noRecentlyGeneratedPassword": { - "message": "Ви не генерували паролі останнім часом" + "nothingGeneratedRecently": { + "message": "Ви нічого не генерували останнім часом" }, "remove": { "message": "Вилучити" @@ -2449,8 +2511,8 @@ "message": "Ви можете встановити пароль для доступу до цього відправлення.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Вимагати цей пароль для перегляду відправлення.", + "sendPasswordDescV3": { + "message": "За бажання додайте пароль для отримувачів цього відправлення.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2499,11 +2561,11 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInHoursSingle": { - "message": "The Send will be available to anyone with the link for the next 1 hour.", + "message": "Це відправлення буде доступним будь-кому за посиланням протягом 1 години.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInHours": { - "message": "The Send will be available to anyone with the link for the next $HOURS$ hours.", + "message": "Це відправлення буде доступним будь-кому за посиланням протягом $HOURS$ годин.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "hours": { @@ -2513,11 +2575,11 @@ } }, "sendExpiresInDaysSingle": { - "message": "The Send will be available to anyone with the link for the next 1 day.", + "message": "Це відправлення буде доступним будь-кому за посиланням протягом 1 дня.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInDays": { - "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "message": "Це відправлення буде доступним будь-кому за посиланням протягом $DAYS$ днів.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "days": { @@ -2647,6 +2709,15 @@ "message": "Ваша організація вимагає, щоб ви встановили головний пароль.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "з $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Потрібне підтвердження", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Генерувати ім'я користувача" }, + "generateEmail": { + "message": "Генерувати е-пошту" + }, "usernameType": { "message": "Тип імені користувача" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Згенеруйте псевдонім е-пошти зі стороннім сервісом пересилання." }, + "forwarderDomainName": { + "message": "Домен електронної пошти", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Виберіть домен, який підтримується вибраною службою", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "Помилка $SERVICENAME$: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -3681,7 +3763,7 @@ "message": "Немає відповідних записів для цього сайту" }, "searchSavePasskeyNewLogin": { - "message": "Search or save passkey as new login" + "message": "Шукати або зберегти ключ доступу як новий запис" }, "confirm": { "message": "Підтвердити" @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Розташування запису" }, + "fileSend": { + "message": "Відправлення файлу" + }, "fileSends": { "message": "Відправлення файлів" }, + "textSend": { + "message": "Відправлення тексту" + }, "textSends": { "message": "Відправлення тексту" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Аутентифікація" + }, + "fillGeneratedPassword": { + "message": "Заповнити згенерований пароль", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Пароль згенеровано повторно", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Зберегти запис у Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Пробіл", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Тильда", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Зворотна лапка", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Знак оклику", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "Знак @ (At)", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Знак решітки", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Знак долара", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Знак відсотка", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Карет", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Амперсанд", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Зірочка", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Ліва дужка", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Права дужка", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Знак підкреслення", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Дефіс", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Плюс", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Дорівнює", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Ліва фігурна дужка", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Права фігурна дужка", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Ліва квадратна дужка", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Права квадратна дужка", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Вертикальна риска", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Обернена скісна риска", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Двокрапка", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Крапка з комою", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Подвійні лапки", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Одинарні лапки", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Менше", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Більше", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Кома", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Крапка", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Знак питання", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Скісна риска", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Малі літери" + }, + "uppercaseAriaLabel": { + "message": "Великі літери" + }, + "generatedPassword": { + "message": "Згенерований пароль" } } diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 42fc36a8f2b..cd390c6496e 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "Tạo tài khoản" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "Đặt mật khẩu mạnh" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "Tham gia tổ chức" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "Hoàn tất gia nhập tổ chức này bằng cách đặt một mật khẩu chính." }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "Sao chép mật khẩu" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "Sao chép ghi chú" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "Copy notes" }, + "fill": { + "message": "Fill", + "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": "Tự động điền" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "Tạo mật khẩu" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "Tạo lại mật khẩu" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "Mở trang web" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "Trang web" }, @@ -805,6 +845,9 @@ "logIn": { "message": "Đăng nhập" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Tiến hành đăng ký lại" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "URL máy chủ" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "Địa chỉ API máy chủ" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Hiển thị các gợi ý tự động điền trên các trường biểu mẫu" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Hiện gợi ý khi nhấp vào biểu tượng" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "Lịch sử mật khẩu" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "Quay lại" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "Xóa lịch sử" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "Xoá" @@ -2449,8 +2511,8 @@ "message": "Yêu cầu nhập mật khẩu khi người dùng truy cập vào phần Gửi này.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "Tổ chức của bạn yêu cầu bạn đặt mật khẩu chính.", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "Yêu cầu xác minh", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "Tạo tên người dùng" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "Loại tên người dùng" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "Tạo bí danh email với dịch vụ chuyển tiếp bên ngoài." }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "Lỗi $SERVICENAME$: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Vị trí mục" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index 7cbc946cd0e..25baa71d7a1 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "创建账户" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "设置强密码" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "加入组织" }, + "joinOrganizationName": { + "message": "加入 $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "通过设置主密码完成加入此组织。" }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "复制密码" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "复制备注" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "复制备注" }, + "fill": { + "message": "Fill", + "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": "自动填充" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "生成密码" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "重新生成密码" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "启动网站" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "网站" }, @@ -805,6 +845,9 @@ "logIn": { "message": "登录" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "重新开始注册" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "服务器 URL" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "API 服务器 URL" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "在表单字段中显示自动填充建议" }, + "showInlineMenuIdentitiesLabel": { + "message": "将身份显示为建议" + }, + "showInlineMenuCardsLabel": { + "message": "将支付卡显示为建议" + }, "showInlineMenuOnIconSelectionLabel": { "message": "选择图标时显示建议" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "密码历史记录" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "后退" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "清除历史记录" }, - "noPasswordsToShow": { - "message": "没有可显示的密码" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "您最近还没有生成过密码" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "移除" @@ -2449,8 +2511,8 @@ "message": "可选,用户需要提供密码才能访问此 Send。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "需要此密码才能查看此 Send。", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2499,11 +2561,11 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInHoursSingle": { - "message": "The Send will be available to anyone with the link for the next 1 hour.", + "message": "在接下来的 1 小时内,任何人都可以通过链接访问此 Send。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInHours": { - "message": "The Send will be available to anyone with the link for the next $HOURS$ hours.", + "message": "在接下来的 $HOURS$ 小时内,任何人都可以通过链接访问此 Send。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "hours": { @@ -2513,11 +2575,11 @@ } }, "sendExpiresInDaysSingle": { - "message": "The Send will be available to anyone with the link for the next 1 day.", + "message": "在接下来的 1 天内,任何人都可以通过链接访问此 Send。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInDays": { - "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "message": "在接下来的 $DAYS$ 天内,任何人都可以通过链接访问此 Send。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "days": { @@ -2647,6 +2709,15 @@ "message": "您的组织要求您设置主密码。", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "$TOTAL$ 不足", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "需要验证", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "生成用户名" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "用户名类型" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "使用外部转发服务生成一个电子邮件别名。" }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ 错误:$ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "项目位置" }, + "fileSend": { + "message": "文件 Send" + }, "fileSends": { "message": "文件 Send" }, + "textSend": { + "message": "文本 Send" + }, "textSends": { "message": "文本 Send" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "正在验证" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index 5d961e1fe10..f022d75a718 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -19,6 +19,18 @@ "createAccount": { "message": "建立帳戶" }, + "newToBitwarden": { + "message": "New to Bitwarden?" + }, + "logInWithPasskey": { + "message": "Log in with passkey" + }, + "useSingleSignOn": { + "message": "Use single sign-on" + }, + "welcomeBack": { + "message": "Welcome back" + }, "setAStrongPassword": { "message": "設定一個強密碼" }, @@ -71,6 +83,15 @@ "joinOrganization": { "message": "加入組織" }, + "joinOrganizationName": { + "message": "Join $ORGANIZATIONNAME$", + "placeholders": { + "organizationName": { + "content": "$1", + "example": "My Org Name" + } + } + }, "finishJoiningThisOrganizationBySettingAMasterPassword": { "message": "設定主密碼以完成加入這個組織" }, @@ -98,6 +119,9 @@ "copyPassword": { "message": "複製密碼" }, + "copyPassphrase": { + "message": "Copy passphrase" + }, "copyNote": { "message": "複製備註" }, @@ -143,6 +167,10 @@ "copyNotes": { "message": "複製備註" }, + "fill": { + "message": "Fill", + "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": "自動填入" }, @@ -398,6 +426,9 @@ "generatePassword": { "message": "產生密碼" }, + "generatePassphrase": { + "message": "Generate passphrase" + }, "regeneratePassword": { "message": "重新產生密碼" }, @@ -559,6 +590,15 @@ "launchWebsite": { "message": "開啟網站" }, + "launchWebsiteName": { + "message": "Launch website $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Secret item" + } + } + }, "website": { "message": "網站" }, @@ -805,6 +845,9 @@ "logIn": { "message": "登入" }, + "logInToBitwarden": { + "message": "Log in to Bitwarden" + }, "restartRegistration": { "message": "Restart registration" }, @@ -1380,6 +1423,10 @@ "baseUrl": { "message": "伺服器 URL" }, + "selfHostBaseUrl": { + "message": "Self-host server URL", + "description": "Label for field requesting a self-hosted integration service URL" + }, "apiUrl": { "message": "API 伺服器網址" }, @@ -1408,6 +1455,12 @@ "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" }, + "showInlineMenuIdentitiesLabel": { + "message": "Display identities as suggestions" + }, + "showInlineMenuCardsLabel": { + "message": "Display cards as suggestions" + }, "showInlineMenuOnIconSelectionLabel": { "message": "Display suggestions when icon is selected" }, @@ -1741,6 +1794,15 @@ "passwordHistory": { "message": "密碼歷史記錄" }, + "generatorHistory": { + "message": "Generator history" + }, + "clearGeneratorHistoryTitle": { + "message": "Clear generator history" + }, + "cleargGeneratorHistoryDescription": { + "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + }, "back": { "message": "返回" }, @@ -1857,11 +1919,11 @@ "clearHistory": { "message": "清除歷史紀錄" }, - "noPasswordsToShow": { - "message": "No passwords to show" + "nothingToShow": { + "message": "Nothing to show" }, - "noRecentlyGeneratedPassword": { - "message": "You haven't generated a password recently" + "nothingGeneratedRecently": { + "message": "You haven't generated anything recently" }, "remove": { "message": "移除" @@ -2449,8 +2511,8 @@ "message": "選用功能。使用者需提供密碼才能存取此 Send。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "sendPasswordDescV2": { - "message": "Require this password to view the Send.", + "sendPasswordDescV3": { + "message": "Add an optional password for recipients to access this Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2647,6 +2709,15 @@ "message": "您的組織要求您設定主密碼。", "description": "Used as a card title description on the set password page to explain why the user is there" }, + "cardMetrics": { + "message": "out of $TOTAL$", + "placeholders": { + "total": { + "content": "$1", + "example": "5" + } + } + }, "verificationRequired": { "message": "需要驗證", "description": "Default title for the user verification dialog." @@ -2803,6 +2874,9 @@ "generateUsername": { "message": "產生使用者名稱" }, + "generateEmail": { + "message": "Generate email" + }, "usernameType": { "message": "使用者名稱類型" }, @@ -2843,6 +2917,14 @@ "forwardedEmailDesc": { "message": "使用外部轉寄服務產生一個電子郵件別名。" }, + "forwarderDomainName": { + "message": "Email domain", + "description": "Labels the domain name email forwarder service option" + }, + "forwarderDomainNameHint": { + "message": "Choose a domain that is supported by the selected service", + "description": "Guidance provided for email forwarding services that support multiple email domains." + }, "forwarderError": { "message": "$SERVICENAME$ error: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", @@ -4470,9 +4552,15 @@ "itemLocation": { "message": "Item Location" }, + "fileSend": { + "message": "File Send" + }, "fileSends": { "message": "File Sends" }, + "textSend": { + "message": "Text Send" + }, "textSends": { "message": "Text Sends" }, @@ -4535,5 +4623,158 @@ }, "authenticating": { "message": "Authenticating" + }, + "fillGeneratedPassword": { + "message": "Fill generated password", + "description": "Heading for the password generator within the inline menu" + }, + "passwordRegenerated": { + "message": "Password regenerated", + "description": "Notification message for when a password has been regenerated" + }, + "saveLoginToBitwarden": { + "message": "Save login to Bitwarden?", + "description": "Confirmation message for saving a login to Bitwarden" + }, + "spaceCharacterDescriptor": { + "message": "Space", + "description": "Represents the space key in screen reader content as a readable word" + }, + "tildeCharacterDescriptor": { + "message": "Tilde", + "description": "Represents the ~ key in screen reader content as a readable word" + }, + "backtickCharacterDescriptor": { + "message": "Backtick", + "description": "Represents the ` key in screen reader content as a readable word" + }, + "exclamationCharacterDescriptor": { + "message": "Exclamation mark", + "description": "Represents the ! key in screen reader content as a readable word" + }, + "atSignCharacterDescriptor": { + "message": "At sign", + "description": "Represents the @ key in screen reader content as a readable word" + }, + "hashSignCharacterDescriptor": { + "message": "Hash sign", + "description": "Represents the # key in screen reader content as a readable word" + }, + "dollarSignCharacterDescriptor": { + "message": "Dollar sign", + "description": "Represents the $ key in screen reader content as a readable word" + }, + "percentSignCharacterDescriptor": { + "message": "Percent sign", + "description": "Represents the % key in screen reader content as a readable word" + }, + "caretCharacterDescriptor": { + "message": "Caret", + "description": "Represents the ^ key in screen reader content as a readable word" + }, + "ampersandCharacterDescriptor": { + "message": "Ampersand", + "description": "Represents the & key in screen reader content as a readable word" + }, + "asteriskCharacterDescriptor": { + "message": "Asterisk", + "description": "Represents the * key in screen reader content as a readable word" + }, + "parenLeftCharacterDescriptor": { + "message": "Left parenthesis", + "description": "Represents the ( key in screen reader content as a readable word" + }, + "parenRightCharacterDescriptor": { + "message": "Right parenthesis", + "description": "Represents the ) key in screen reader content as a readable word" + }, + "hyphenCharacterDescriptor": { + "message": "Underscore", + "description": "Represents the _ key in screen reader content as a readable word" + }, + "underscoreCharacterDescriptor": { + "message": "Hyphen", + "description": "Represents the - key in screen reader content as a readable word" + }, + "plusCharacterDescriptor": { + "message": "Plus", + "description": "Represents the + key in screen reader content as a readable word" + }, + "equalsCharacterDescriptor": { + "message": "Equals", + "description": "Represents the = key in screen reader content as a readable word" + }, + "braceLeftCharacterDescriptor": { + "message": "Left brace", + "description": "Represents the { key in screen reader content as a readable word" + }, + "braceRightCharacterDescriptor": { + "message": "Right brace", + "description": "Represents the } key in screen reader content as a readable word" + }, + "bracketLeftCharacterDescriptor": { + "message": "Left bracket", + "description": "Represents the [ key in screen reader content as a readable word" + }, + "bracketRightCharacterDescriptor": { + "message": "Right bracket", + "description": "Represents the ] key in screen reader content as a readable word" + }, + "pipeCharacterDescriptor": { + "message": "Pipe", + "description": "Represents the | key in screen reader content as a readable word" + }, + "backSlashCharacterDescriptor": { + "message": "Back slash", + "description": "Represents the back slash key in screen reader content as a readable word" + }, + "colonCharacterDescriptor": { + "message": "Colon", + "description": "Represents the : key in screen reader content as a readable word" + }, + "semicolonCharacterDescriptor": { + "message": "Semicolon", + "description": "Represents the ; key in screen reader content as a readable word" + }, + "doubleQuoteCharacterDescriptor": { + "message": "Double quote", + "description": "Represents the double quote key in screen reader content as a readable word" + }, + "singleQuoteCharacterDescriptor": { + "message": "Single quote", + "description": "Represents the ' key in screen reader content as a readable word" + }, + "lessThanCharacterDescriptor": { + "message": "Less than", + "description": "Represents the < key in screen reader content as a readable word" + }, + "greaterThanCharacterDescriptor": { + "message": "Greater than", + "description": "Represents the > key in screen reader content as a readable word" + }, + "commaCharacterDescriptor": { + "message": "Comma", + "description": "Represents the , key in screen reader content as a readable word" + }, + "periodCharacterDescriptor": { + "message": "Period", + "description": "Represents the . key in screen reader content as a readable word" + }, + "questionCharacterDescriptor": { + "message": "Question mark", + "description": "Represents the ? key in screen reader content as a readable word" + }, + "forwardSlashCharacterDescriptor": { + "message": "Forward slash", + "description": "Represents the / key in screen reader content as a readable word" + }, + "lowercaseAriaLabel": { + "message": "Lowercase" + }, + "uppercaseAriaLabel": { + "message": "Uppercase" + }, + "generatedPassword": { + "message": "Generated password" } } diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts index 9d7644878d0..db85b28fa64 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts @@ -131,31 +131,35 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy { return; } - if (data.pageTitle) { - this.pageTitle = this.handleStringOrTranslation(data.pageTitle); + // Null emissions are used to reset the page data as all fields are optional. + + if (data.pageTitle !== undefined) { + this.pageTitle = + data.pageTitle !== null ? this.handleStringOrTranslation(data.pageTitle) : null; } - if (data.pageSubtitle) { - this.pageSubtitle = this.handleStringOrTranslation(data.pageSubtitle); + if (data.pageSubtitle !== undefined) { + this.pageSubtitle = + data.pageSubtitle !== null ? this.handleStringOrTranslation(data.pageSubtitle) : null; } - if (data.pageIcon) { - this.pageIcon = data.pageIcon; + if (data.pageIcon !== undefined) { + this.pageIcon = data.pageIcon !== null ? data.pageIcon : null; } - if (data.showReadonlyHostname != null) { + if (data.showReadonlyHostname !== undefined) { this.showReadonlyHostname = data.showReadonlyHostname; } - if (data.showAcctSwitcher != null) { + if (data.showAcctSwitcher !== undefined) { this.showAcctSwitcher = data.showAcctSwitcher; } - if (data.showBackButton != null) { + if (data.showBackButton !== undefined) { this.showBackButton = data.showBackButton; } - if (data.showLogo != null) { + if (data.showLogo !== undefined) { this.showLogo = data.showLogo; } } diff --git a/apps/browser/src/auth/popup/lock.component.ts b/apps/browser/src/auth/popup/lock.component.ts index 75fcfc58f6a..c7fb108de80 100644 --- a/apps/browser/src/auth/popup/lock.component.ts +++ b/apps/browser/src/auth/popup/lock.component.ts @@ -16,7 +16,6 @@ import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -26,7 +25,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { DialogService, ToastService } from "@bitwarden/components"; -import { BiometricsService, BiometricStateService } from "@bitwarden/key-management"; +import { KeyService, BiometricsService, BiometricStateService } from "@bitwarden/key-management"; import { BiometricErrors, BiometricErrorTypes } from "../../models/biometricErrors"; import { BrowserRouterService } from "../../platform/popup/services/browser-router.service"; @@ -49,7 +48,7 @@ export class LockComponent extends BaseLockComponent implements OnInit { i18nService: I18nService, platformUtilsService: PlatformUtilsService, messagingService: MessagingService, - cryptoService: CryptoService, + keyService: KeyService, vaultTimeoutService: VaultTimeoutService, vaultTimeoutSettingsService: VaultTimeoutSettingsService, environmentService: EnvironmentService, @@ -79,7 +78,7 @@ export class LockComponent extends BaseLockComponent implements OnInit { i18nService, platformUtilsService, messagingService, - cryptoService, + keyService, vaultTimeoutService, vaultTimeoutSettingsService, environmentService, diff --git a/apps/browser/src/auth/popup/login.component.html b/apps/browser/src/auth/popup/login-v1.component.html similarity index 100% rename from apps/browser/src/auth/popup/login.component.html rename to apps/browser/src/auth/popup/login-v1.component.html diff --git a/apps/browser/src/auth/popup/login.component.ts b/apps/browser/src/auth/popup/login-v1.component.ts similarity index 95% rename from apps/browser/src/auth/popup/login.component.ts rename to apps/browser/src/auth/popup/login-v1.component.ts index fd4d9bc547a..eee1bcc4d3f 100644 --- a/apps/browser/src/auth/popup/login.component.ts +++ b/apps/browser/src/auth/popup/login-v1.component.ts @@ -3,7 +3,7 @@ import { FormBuilder } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { firstValueFrom } from "rxjs"; -import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/auth/components/login.component"; +import { LoginComponentV1 as BaseLoginComponent } from "@bitwarden/angular/auth/components/login-v1.component"; import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service"; import { LoginStrategyServiceAbstraction, @@ -29,9 +29,9 @@ import { flagEnabled } from "../../platform/flags"; @Component({ selector: "app-login", - templateUrl: "login.component.html", + templateUrl: "login-v1.component.html", }) -export class LoginComponent extends BaseLoginComponent implements OnInit { +export class LoginComponentV1 extends BaseLoginComponent implements OnInit { showPasswordless = false; constructor( devicesApiService: DevicesApiServiceAbstraction, diff --git a/apps/browser/src/auth/popup/login-via-auth-request.component.ts b/apps/browser/src/auth/popup/login-via-auth-request.component.ts index 33ec2acf387..9dc0d7d5454 100644 --- a/apps/browser/src/auth/popup/login-via-auth-request.component.ts +++ b/apps/browser/src/auth/popup/login-via-auth-request.component.ts @@ -15,7 +15,6 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -24,6 +23,7 @@ import { ValidationService } from "@bitwarden/common/platform/abstractions/valid import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; +import { KeyService } from "@bitwarden/key-management"; @Component({ selector: "app-login-via-auth-request", @@ -32,7 +32,7 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legac export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { constructor( router: Router, - cryptoService: CryptoService, + keyService: KeyService, cryptoFunctionService: CryptoFunctionService, appIdService: AppIdService, passwordGenerationService: PasswordGenerationServiceAbstraction, @@ -55,7 +55,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { ) { super( router, - cryptoService, + keyService, cryptoFunctionService, appIdService, passwordGenerationService, diff --git a/apps/browser/src/auth/popup/login/extension-login-component.service.spec.ts b/apps/browser/src/auth/popup/login/extension-login-component.service.spec.ts new file mode 100644 index 00000000000..a7e29171015 --- /dev/null +++ b/apps/browser/src/auth/popup/login/extension-login-component.service.spec.ts @@ -0,0 +1,85 @@ +import { TestBed } from "@angular/core/testing"; +import { MockProxy, mock } from "jest-mock-extended"; + +import { DefaultLoginComponentService } from "@bitwarden/auth/angular"; +import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; +import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; + +import { flagEnabled } from "../../../platform/flags"; +import { BrowserPlatformUtilsService } from "../../../platform/services/platform-utils/browser-platform-utils.service"; +import { ExtensionAnonLayoutWrapperDataService } from "../extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service"; + +import { ExtensionLoginComponentService } from "./extension-login-component.service"; + +jest.mock("../../../platform/flags", () => ({ + flagEnabled: jest.fn(), +})); + +describe("ExtensionLoginComponentService", () => { + let service: ExtensionLoginComponentService; + let cryptoFunctionService: MockProxy; + let environmentService: MockProxy; + let passwordGenerationService: MockProxy; + let platformUtilsService: MockProxy; + let ssoLoginService: MockProxy; + let extensionAnonLayoutWrapperDataService: MockProxy; + beforeEach(() => { + cryptoFunctionService = mock(); + environmentService = mock(); + passwordGenerationService = mock(); + platformUtilsService = mock(); + ssoLoginService = mock(); + extensionAnonLayoutWrapperDataService = mock(); + TestBed.configureTestingModule({ + providers: [ + { + provide: ExtensionLoginComponentService, + useFactory: () => + new ExtensionLoginComponentService( + cryptoFunctionService, + environmentService, + passwordGenerationService, + platformUtilsService, + ssoLoginService, + extensionAnonLayoutWrapperDataService, + ), + }, + { provide: DefaultLoginComponentService, useExisting: ExtensionLoginComponentService }, + { provide: CryptoFunctionService, useValue: cryptoFunctionService }, + { provide: EnvironmentService, useValue: environmentService }, + { provide: PasswordGenerationServiceAbstraction, useValue: passwordGenerationService }, + { provide: PlatformUtilsService, useValue: platformUtilsService }, + { provide: SsoLoginServiceAbstraction, useValue: ssoLoginService }, + ], + }); + service = TestBed.inject(ExtensionLoginComponentService); + }); + + it("creates the service", () => { + expect(service).toBeTruthy(); + }); + + describe("isLoginViaAuthRequestSupported", () => { + it("returns true if showPasswordless flag is enabled", () => { + (flagEnabled as jest.Mock).mockReturnValue(true); + expect(service.isLoginViaAuthRequestSupported()).toBe(true); + }); + + it("returns false if showPasswordless flag is disabled", () => { + (flagEnabled as jest.Mock).mockReturnValue(false); + expect(service.isLoginViaAuthRequestSupported()).toBeFalsy(); + }); + }); + + describe("showBackButton", () => { + it("sets showBackButton in extensionAnonLayoutWrapperDataService", () => { + service.showBackButton(true); + expect(extensionAnonLayoutWrapperDataService.setAnonLayoutWrapperData).toHaveBeenCalledWith({ + showBackButton: true, + }); + }); + }); +}); diff --git a/apps/browser/src/auth/popup/login/extension-login-component.service.ts b/apps/browser/src/auth/popup/login/extension-login-component.service.ts new file mode 100644 index 00000000000..8630030e8e2 --- /dev/null +++ b/apps/browser/src/auth/popup/login/extension-login-component.service.ts @@ -0,0 +1,43 @@ +import { Injectable } from "@angular/core"; + +import { DefaultLoginComponentService, LoginComponentService } from "@bitwarden/auth/angular"; +import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; +import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; + +import { flagEnabled } from "../../../platform/flags"; +import { ExtensionAnonLayoutWrapperDataService } from "../extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service"; + +@Injectable() +export class ExtensionLoginComponentService + extends DefaultLoginComponentService + implements LoginComponentService +{ + constructor( + cryptoFunctionService: CryptoFunctionService, + environmentService: EnvironmentService, + passwordGenerationService: PasswordGenerationServiceAbstraction, + platformUtilsService: PlatformUtilsService, + ssoLoginService: SsoLoginServiceAbstraction, + private extensionAnonLayoutWrapperDataService: ExtensionAnonLayoutWrapperDataService, + ) { + super( + cryptoFunctionService, + environmentService, + passwordGenerationService, + platformUtilsService, + ssoLoginService, + ); + this.clientType = this.platformUtilsService.getClientType(); + } + + isLoginViaAuthRequestSupported(): boolean { + return flagEnabled("showPasswordless"); + } + + showBackButton(showBackButton: boolean): void { + this.extensionAnonLayoutWrapperDataService.setAnonLayoutWrapperData({ showBackButton }); + } +} diff --git a/apps/browser/src/auth/popup/register.component.ts b/apps/browser/src/auth/popup/register.component.ts index dab1e62f850..7c785d1912a 100644 --- a/apps/browser/src/auth/popup/register.component.ts +++ b/apps/browser/src/auth/popup/register.component.ts @@ -7,7 +7,6 @@ import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstrac import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -15,6 +14,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; +import { KeyService } from "@bitwarden/key-management"; @Component({ selector: "app-register", @@ -30,7 +30,7 @@ export class RegisterComponent extends BaseRegisterComponent { loginStrategyService: LoginStrategyServiceAbstraction, router: Router, i18nService: I18nService, - cryptoService: CryptoService, + keyService: KeyService, apiService: ApiService, stateService: StateService, platformUtilsService: PlatformUtilsService, @@ -47,7 +47,7 @@ export class RegisterComponent extends BaseRegisterComponent { loginStrategyService, router, i18nService, - cryptoService, + keyService, apiService, stateService, platformUtilsService, diff --git a/apps/browser/src/auth/popup/settings/account-security-v1.component.ts b/apps/browser/src/auth/popup/settings/account-security-v1.component.ts index d2a515b2599..9d8a2ac4c88 100644 --- a/apps/browser/src/auth/popup/settings/account-security-v1.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security-v1.component.ts @@ -25,7 +25,6 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -37,7 +36,7 @@ import { VaultTimeoutStringType, } from "@bitwarden/common/types/vault-timeout.type"; import { DialogService } from "@bitwarden/components"; -import { BiometricStateService, BiometricsService } from "@bitwarden/key-management"; +import { KeyService, BiometricStateService, BiometricsService } from "@bitwarden/key-management"; import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors"; import { BrowserApi } from "../../../platform/browser/browser-api"; @@ -58,7 +57,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { availableVaultTimeoutActions: VaultTimeoutAction[] = []; vaultTimeoutOptions: VaultTimeoutOption[]; vaultTimeoutPolicyCallout: Observable<{ - timeout: { hours: number; minutes: number }; + timeout: { hours: string; minutes: string }; action: VaultTimeoutAction; }>; supportsBiometric: boolean; @@ -87,7 +86,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { private vaultTimeoutSettingsService: VaultTimeoutSettingsService, public messagingService: MessagingService, private environmentService: EnvironmentService, - private cryptoService: CryptoService, + private keyService: KeyService, private stateService: StateService, private userVerificationService: UserVerificationService, private dialogService: DialogService, @@ -106,8 +105,8 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { let timeout; if (policy.data?.minutes) { timeout = { - hours: Math.floor(policy.data?.minutes / 60), - minutes: policy.data?.minutes % 60, + hours: Math.floor(policy.data?.minutes / 60).toString(), + minutes: (policy.data?.minutes % 60).toString(), }; } return { timeout: timeout, action: policy.data?.action }; @@ -386,7 +385,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { const awaitDesktopDialogRef = AwaitDesktopDialogComponent.open(this.dialogService); const awaitDesktopDialogClosed = firstValueFrom(awaitDesktopDialogRef.closed); - await this.cryptoService.refreshAdditionalKeys(); + await this.keyService.refreshAdditionalKeys(); await Promise.race([ awaitDesktopDialogClosed.then(async (result) => { @@ -465,9 +464,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { } async fingerprint() { - const fingerprint = await this.cryptoService.getFingerprint( - await this.stateService.getUserId(), - ); + const fingerprint = await this.keyService.getFingerprint(await this.stateService.getUserId()); const dialogRef = FingerprintDialogComponent.open(this.dialogService, { fingerprint, diff --git a/apps/browser/src/auth/popup/settings/account-security.component.html b/apps/browser/src/auth/popup/settings/account-security.component.html index af6525daa8a..00e1fd17150 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.html +++ b/apps/browser/src/auth/popup/settings/account-security.component.html @@ -94,7 +94,7 @@

{{ "otherOptions" | i18n }}

@@ -115,11 +115,11 @@

{{ "otherOptions" | i18n }}

" > -
{{ "lockNow" | i18n }}
+ {{ "lockNow" | i18n }}
-
{{ "logOut" | i18n }}
+ {{ "logOut" | i18n }}
diff --git a/apps/browser/src/auth/popup/settings/account-security.component.ts b/apps/browser/src/auth/popup/settings/account-security.component.ts index 20286435edb..1617ed84767 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.ts @@ -27,7 +27,6 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -52,7 +51,7 @@ import { TypographyModule, ToastService, } from "@bitwarden/components"; -import { BiometricsService, BiometricStateService } from "@bitwarden/key-management"; +import { KeyService, BiometricsService, BiometricStateService } from "@bitwarden/key-management"; import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors"; import { BrowserApi } from "../../../platform/browser/browser-api"; @@ -127,7 +126,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { private vaultTimeoutSettingsService: VaultTimeoutSettingsService, public messagingService: MessagingService, private environmentService: EnvironmentService, - private cryptoService: CryptoService, + private keyService: KeyService, private stateService: StateService, private userVerificationService: UserVerificationService, private dialogService: DialogService, @@ -436,7 +435,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { return; } - await this.cryptoService.refreshAdditionalKeys(); + await this.keyService.refreshAdditionalKeys(); const successful = await this.trySetupBiometrics(); this.form.controls.biometric.setValue(successful); @@ -562,8 +561,8 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { const activeUserId = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); - const publicKey = await firstValueFrom(this.cryptoService.userPublicKey$(activeUserId)); - const fingerprint = await this.cryptoService.getFingerprint(activeUserId, publicKey); + const publicKey = await firstValueFrom(this.keyService.userPublicKey$(activeUserId)); + const fingerprint = await this.keyService.getFingerprint(activeUserId, publicKey); const dialogRef = FingerprintDialogComponent.open(this.dialogService, { fingerprint, diff --git a/apps/browser/src/autofill/background/abstractions/overlay.background.ts b/apps/browser/src/autofill/background/abstractions/overlay.background.ts index abe7d097016..db50b784453 100644 --- a/apps/browser/src/autofill/background/abstractions/overlay.background.ts +++ b/apps/browser/src/autofill/background/abstractions/overlay.background.ts @@ -2,6 +2,7 @@ import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { InlineMenuFillTypes } from "../../enums/autofill-overlay.enum"; import AutofillPageDetails from "../../models/autofill-page-details"; import { PageDetail } from "../../services/abstractions/autofill.service"; @@ -32,14 +33,18 @@ export type WebsiteIconData = { icon: string; }; +export type UpdateOverlayCiphersParams = { + updateAllCipherTypes: boolean; + refocusField: boolean; +}; + export type FocusedFieldData = { focusedFieldStyles: Partial; focusedFieldRects: Partial; - filledByCipherType?: CipherType; + inlineMenuFillType?: InlineMenuFillTypes; tabId?: number; frameId?: number; accountCreationFieldType?: string; - showInlineMenuAccountCreation?: boolean; showPasskeys?: boolean; }; @@ -111,6 +116,12 @@ export type ToggleInlineMenuHiddenMessage = { setTransparentInlineMenu?: boolean; }; +export type UpdateInlineMenuVisibilityMessage = { + overlayElement?: string; + isVisible?: boolean; + forceUpdate?: boolean; +}; + export type OverlayBackgroundExtensionMessage = { command: string; portKey?: string; @@ -119,14 +130,15 @@ export type OverlayBackgroundExtensionMessage = { details?: AutofillPageDetails; isFieldCurrentlyFocused?: boolean; isFieldCurrentlyFilling?: boolean; - isVisible?: boolean; subFrameData?: SubFrameOffsetData; focusedFieldData?: FocusedFieldData; + isOpeningFullInlineMenu?: boolean; styles?: Partial; data?: LockedVaultPendingNotificationsData; } & OverlayAddNewItemMessage & CloseInlineMenuMessage & - ToggleInlineMenuHiddenMessage; + ToggleInlineMenuHiddenMessage & + UpdateInlineMenuVisibilityMessage; export type OverlayPortMessage = { [key: string]: any; @@ -188,16 +200,12 @@ export type OverlayBackgroundExtensionMessageHandlers = { updateIsFieldCurrentlyFilling: ({ message }: BackgroundMessageParam) => void; checkIsFieldCurrentlyFilling: () => boolean; getAutofillInlineMenuVisibility: () => void; + openAutofillInlineMenu: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; getInlineMenuCardsVisibility: () => void; getInlineMenuIdentitiesVisibility: () => void; - openAutofillInlineMenu: () => void; closeAutofillInlineMenu: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; checkAutofillInlineMenuFocused: ({ sender }: BackgroundSenderParam) => void; focusAutofillInlineMenuList: () => void; - updateAutofillInlineMenuPosition: ({ - message, - sender, - }: BackgroundOnMessageHandlerParams) => Promise; getAutofillInlineMenuPosition: () => InlineMenuPosition; updateAutofillInlineMenuElementIsVisibleStatus: ({ message, @@ -208,6 +216,7 @@ export type OverlayBackgroundExtensionMessageHandlers = { getCurrentTabFrameId: ({ sender }: BackgroundSenderParam) => number; updateSubFrameData: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; triggerSubFrameFocusInRebuild: ({ sender }: BackgroundSenderParam) => void; + shouldRepositionSubFrameInlineMenuOnScroll: ({ sender }: BackgroundSenderParam) => void; destroyAutofillInlineMenuListeners: ({ message, sender, @@ -219,6 +228,7 @@ export type OverlayBackgroundExtensionMessageHandlers = { addEditCipherSubmitted: () => void; editedCipher: () => void; deletedCipher: () => void; + bgSaveCipher: () => void; fido2AbortRequest: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; }; @@ -241,14 +251,16 @@ export type InlineMenuButtonPortMessageHandlers = { export type InlineMenuListPortMessageHandlers = { [key: string]: CallableFunction; - checkAutofillInlineMenuButtonFocused: () => void; - autofillInlineMenuBlurred: () => void; + checkAutofillInlineMenuButtonFocused: ({ port }: PortConnectionParam) => void; + autofillInlineMenuBlurred: ({ port }: PortConnectionParam) => void; unlockVault: ({ port }: PortConnectionParam) => void; fillAutofillInlineMenuCipher: ({ message, port }: PortOnMessageHandlerParams) => void; addNewVaultItem: ({ message, port }: PortOnMessageHandlerParams) => void; viewSelectedCipher: ({ message, port }: PortOnMessageHandlerParams) => void; redirectAutofillInlineMenuFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void; updateAutofillInlineMenuListHeight: ({ message, port }: PortOnMessageHandlerParams) => void; + refreshGeneratedPassword: () => Promise; + fillGeneratedPassword: ({ port }: PortConnectionParam) => Promise; }; export interface OverlayBackground { diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index 0ede9b96091..e043dbfdd2e 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -1114,8 +1114,9 @@ describe("NotificationBackground", () => { it("skips saving the domain as a never value if the tab url does not match the queue message domain", async () => { const tab = createChromeTabMock({ id: 2, url: "https://example.com" }); - const sender = mock({ tab }); const message: NotificationBackgroundExtensionMessage = { command: "bgNeverSave" }; + const secondaryTab = createChromeTabMock({ id: 3, url: "https://another.com" }); + const sender = mock({ tab: secondaryTab }); notificationBackground["notificationQueue"] = [ mock({ type: NotificationQueueMessageType.AddLogin, diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 683e3d8f581..e77996fe903 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -173,13 +173,8 @@ export default class NotificationBackground { } private async doNotificationQueueCheck(tab: chrome.tabs.Tab): Promise { - const tabDomain = Utils.getDomain(tab?.url); - if (!tabDomain) { - return; - } - const queueMessage = this.notificationQueue.find( - (message) => message.tab.id === tab.id && message.domain === tabDomain, + (message) => message.tab.id === tab.id && this.queueMessageIsFromTabOrigin(message, tab), ); if (queueMessage) { await this.sendNotificationQueueMessage(tab, queueMessage); @@ -537,8 +532,7 @@ export default class NotificationBackground { continue; } - const tabDomain = Utils.getDomain(tab.url); - if (tabDomain != null && tabDomain !== queueMessage.domain) { + if (!this.queueMessageIsFromTabOrigin(queueMessage, tab)) { continue; } @@ -685,8 +679,7 @@ export default class NotificationBackground { continue; } - const tabDomain = Utils.getDomain(tab.url); - if (tabDomain != null && tabDomain !== queueMessage.domain) { + if (!this.queueMessageIsFromTabOrigin(queueMessage, tab)) { continue; } @@ -829,4 +822,18 @@ export default class NotificationBackground { .catch((error) => this.logService.error(error)); return true; }; + + /** + * Validates whether the queue message is associated with the passed tab. + * + * @param queueMessage - The queue message to check + * @param tab - The tab to check the queue message against + */ + private queueMessageIsFromTabOrigin( + queueMessage: NotificationQueueMessageItem, + tab: chrome.tabs.Tab, + ) { + const tabDomain = Utils.getDomain(tab.url); + return tabDomain === queueMessage.domain || tabDomain === Utils.getDomain(queueMessage.tab.url); + } } diff --git a/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts b/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts index 8bac8ea6913..57930496978 100644 --- a/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts @@ -60,6 +60,27 @@ describe("OverlayNotificationsBackground", () => { jest.clearAllTimers(); }); + describe("feature flag behavior", () => { + let runtimeRemoveListenerSpy: jest.SpyInstance; + + beforeEach(() => { + runtimeRemoveListenerSpy = jest.spyOn(chrome.runtime.onMessage, "removeListener"); + }); + + it("removes the extension listeners if the current flag value is set to `false`", () => { + getFeatureFlagMock$.next(false); + + expect(runtimeRemoveListenerSpy).toHaveBeenCalled(); + }); + + it("ignores the feature flag change if the previous flag value is equal to the current flag value", () => { + getFeatureFlagMock$.next(false); + getFeatureFlagMock$.next(false); + + expect(runtimeRemoveListenerSpy).toHaveBeenCalledTimes(1); + }); + }); + describe("setting up the form submission listeners", () => { let fields: MockProxy[]; let details: MockProxy; @@ -180,6 +201,40 @@ describe("OverlayNotificationsBackground", () => { await flushPromises(); }); + it("ignores the store request if the sender is not within the website origins set", () => { + sendMockExtensionMessage( + { + command: "formFieldSubmitted", + uri: "example.com", + username: "username", + password: "password", + newPassword: "newPassword", + }, + mock({ tab: { id: 2 } }), + ); + + expect( + overlayNotificationsBackground["modifyLoginCipherFormData"].get(sender.tab.id), + ).toBeUndefined(); + }); + + it("ignores the store request if the form submission does not include a username, password, or newPassword", () => { + sendMockExtensionMessage( + { + command: "formFieldSubmitted", + uri: "example.com", + username: "", + password: "", + newPassword: "", + }, + sender, + ); + + expect( + overlayNotificationsBackground["modifyLoginCipherFormData"].get(sender.tab.id), + ).toBeUndefined(); + }); + it("stores the modified login cipher form data", async () => { sendMockExtensionMessage( { @@ -203,6 +258,41 @@ describe("OverlayNotificationsBackground", () => { }); }); + it("overrides previously stored modified login cipher form data with a subsequent store request", async () => { + sendMockExtensionMessage( + { + command: "formFieldSubmitted", + uri: "example.com", + username: "oldUsername", + password: "oldPassword", + newPassword: "oldNewPassword", + }, + sender, + ); + await flushPromises(); + + sendMockExtensionMessage( + { + command: "formFieldSubmitted", + uri: "example.com", + username: "username", + password: "", + newPassword: "", + }, + sender, + ); + await flushPromises(); + + expect( + overlayNotificationsBackground["modifyLoginCipherFormData"].get(sender.tab.id), + ).toEqual({ + uri: "example.com", + username: "username", + password: "oldPassword", + newPassword: "oldNewPassword", + }); + }); + it("clears the modified login cipher form data after 5 seconds", () => { sendMockExtensionMessage( { @@ -323,10 +413,9 @@ describe("OverlayNotificationsBackground", () => { it("ignores requests that are not part of an active form submission", async () => { triggerWebRequestOnCompletedEvent( - mock({ + mock({ url: sender.url, tabId: sender.tab.id, - method: "POST", requestId: "123345", }), ); @@ -348,10 +437,9 @@ describe("OverlayNotificationsBackground", () => { await flushPromises(); triggerWebRequestOnCompletedEvent( - mock({ + mock({ url: sender.url, tabId: sender.tab.id, - method: "POST", requestId, }), ); @@ -359,6 +447,36 @@ describe("OverlayNotificationsBackground", () => { expect(notificationChangedPasswordSpy).not.toHaveBeenCalled(); expect(notificationAddLoginSpy).not.toHaveBeenCalled(); }); + + it("clears the notification fallback timeout if the request is completed with an invalid status code", async () => { + const clearFallbackSpy = jest.spyOn( + overlayNotificationsBackground as any, + "clearNotificationFallbackTimeout", + ); + + const requestId = "123345"; + triggerWebRequestOnBeforeRequestEvent( + mock({ + url: sender.url, + tabId: sender.tab.id, + method: "POST", + requestId, + }), + ); + await flushPromises(); + + triggerWebRequestOnCompletedEvent( + mock({ + url: sender.url, + tabId: sender.tab.id, + statusCode: 404, + requestId, + }), + ); + await flushPromises(); + + expect(clearFallbackSpy).toHaveBeenCalled(); + }); }); describe("web requests that trigger notifications", () => { @@ -402,10 +520,9 @@ describe("OverlayNotificationsBackground", () => { ); }); triggerWebRequestOnCompletedEvent( - mock({ + mock({ url: sender.url, tabId: sender.tab.id, - method: "POST", requestId, }), ); @@ -452,10 +569,9 @@ describe("OverlayNotificationsBackground", () => { }); triggerWebRequestOnCompletedEvent( - mock({ + mock({ url: sender.url, tabId: sender.tab.id, - method: "POST", requestId, }), ); @@ -560,14 +676,59 @@ describe("OverlayNotificationsBackground", () => { expect(overlayNotificationsBackground["websiteOriginsWithFields"].size).toBe(0); }); - it("clears all associated data with a tab that is entering a `loading` state", () => { - triggerTabOnUpdatedEvent( - sender.tab.id, - mock({ status: "loading" }), - mock({ status: "loading" }), - ); + describe("tab onUpdated", () => { + it("skips clearing the website origins if the changeInfo does not contain a `loading` status", () => { + triggerTabOnUpdatedEvent( + sender.tab.id, + mock({ status: "complete" }), + mock({ status: "complete" }), + ); - expect(overlayNotificationsBackground["websiteOriginsWithFields"].size).toBe(0); + expect(overlayNotificationsBackground["websiteOriginsWithFields"].size).toBe(1); + }); + + it("skips clearing the website origins if the changeInfo does not contain a url", () => { + triggerTabOnUpdatedEvent( + sender.tab.id, + mock({ status: "loading", url: "" }), + mock({ status: "loading" }), + ); + + expect(overlayNotificationsBackground["websiteOriginsWithFields"].size).toBe(1); + }); + + it("skips clearing the website origins if the tab does not contain known website origins", () => { + triggerTabOnUpdatedEvent( + 199, + mock({ status: "loading", url: "https://example.com" }), + mock({ status: "loading", id: 199 }), + ); + + expect(overlayNotificationsBackground["websiteOriginsWithFields"].size).toBe(1); + }); + + it("skips clearing the website origins if the changeInfo's url is present as part of the know website origin match patterns", () => { + triggerTabOnUpdatedEvent( + sender.tab.id, + mock({ + status: "loading", + url: "https://subdomain.example.com", + }), + mock({ status: "loading" }), + ); + + expect(overlayNotificationsBackground["websiteOriginsWithFields"].size).toBe(1); + }); + + it("clears all associated data with a tab that is entering a `loading` state", () => { + triggerTabOnUpdatedEvent( + sender.tab.id, + mock({ status: "loading" }), + mock({ status: "loading" }), + ); + + expect(overlayNotificationsBackground["websiteOriginsWithFields"].size).toBe(0); + }); }); }); }); diff --git a/apps/browser/src/autofill/background/overlay-notifications.background.ts b/apps/browser/src/autofill/background/overlay-notifications.background.ts index 5ea3e8b8d6b..3472bfcc72f 100644 --- a/apps/browser/src/autofill/background/overlay-notifications.background.ts +++ b/apps/browser/src/autofill/background/overlay-notifications.background.ts @@ -333,7 +333,7 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg const response = (await BrowserApi.tabSendMessage( tab, - { command: "getFormFieldDataForNotification" }, + { command: "getInlineMenuFormFieldData" }, { frameId }, )) as OverlayNotificationsExtensionMessage; if (response) { @@ -471,7 +471,7 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg private shouldTriggerChangePasswordNotification = ( modifyLoginData: ModifyLoginCipherFormData, ) => { - return modifyLoginData.newPassword && !modifyLoginData.username; + return modifyLoginData?.newPassword && !modifyLoginData.username; }; /** @@ -480,7 +480,7 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg * @param modifyLoginData - The modified login form data */ private shouldTriggerAddLoginNotification = (modifyLoginData: ModifyLoginCipherFormData) => { - return modifyLoginData.username && (modifyLoginData.password || modifyLoginData.newPassword); + return modifyLoginData?.username && (modifyLoginData.password || modifyLoginData.newPassword); }; /** @@ -576,8 +576,20 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg * @param changeInfo - The change info of the tab */ private handleTabUpdated = (tabId: number, changeInfo: chrome.tabs.TabChangeInfo) => { - if (changeInfo.status === "loading" && this.websiteOriginsWithFields.has(tabId)) { - this.websiteOriginsWithFields.delete(tabId); + if (changeInfo.status !== "loading" || !changeInfo.url) { + return; + } + + const originPatterns = this.websiteOriginsWithFields.get(tabId); + if (!originPatterns) { + return; } + + const matchPatters = generateDomainMatchPatterns(changeInfo.url); + if (matchPatters.some((pattern) => originPatterns.has(pattern))) { + return; + } + + this.websiteOriginsWithFields.delete(tabId); }; } diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts index b6a04f63d54..d59ed447dde 100644 --- a/apps/browser/src/autofill/background/overlay.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay.background.spec.ts @@ -42,11 +42,16 @@ import { BrowserPlatformUtilsService } from "../../platform/services/platform-ut import { AutofillOverlayElement, AutofillOverlayPort, + InlineMenuAccountCreationFieldType, + InlineMenuFillType, MAX_SUB_FRAME_DEPTH, RedirectFocusDirection, } from "../enums/autofill-overlay.enum"; +import { InlineMenuFormFieldData } from "../services/abstractions/autofill-overlay-content.service"; import { AutofillService } from "../services/abstractions/autofill.service"; +import { InlineMenuFieldQualificationService } from "../services/inline-menu-field-qualification.service"; import { + createAutofillFieldMock, createAutofillPageDetailsMock, createChromeTabMock, createFocusedFieldDataMock, @@ -66,6 +71,7 @@ import { import { FocusedFieldData, + InlineMenuPosition, PageDetailsForTab, SubFrameOffsetData, SubFrameOffsetsForTab, @@ -73,6 +79,9 @@ import { import { OverlayBackground } from "./overlay.background"; describe("OverlayBackground", () => { + const generatedPassword = "generated-password"; + const generatedPasswordCallbackMock = jest.fn().mockResolvedValue(generatedPassword); + const addPasswordCallbackMock = jest.fn(); const mockUserId = Utils.newGuid() as UserId; const sendResponse = jest.fn(); let accountService: FakeAccountService; @@ -95,6 +104,7 @@ describe("OverlayBackground", () => { let vaultSettingsServiceMock: MockProxy; let fido2ActiveRequestManager: Fido2ActiveRequestManager; let selectedThemeMock$: BehaviorSubject; + let inlineMenuFieldQualificationService: InlineMenuFieldQualificationService; let themeStateService: MockProxy; let overlayBackground: OverlayBackground; let portKeyForTabSpy: Record; @@ -117,6 +127,7 @@ describe("OverlayBackground", () => { const { initList, initButton } = options; if (initButton) { triggerPortOnConnectEvent(createPortSpyMock(AutofillOverlayPort.Button)); + await flushPromises(); buttonPortSpy = overlayBackground["inlineMenuButtonPort"]; buttonMessageConnectorSpy = createPortSpyMock(AutofillOverlayPort.ButtonMessageConnector); @@ -125,6 +136,7 @@ describe("OverlayBackground", () => { if (initList) { triggerPortOnConnectEvent(createPortSpyMock(AutofillOverlayPort.List)); + await flushPromises(); listPortSpy = overlayBackground["inlineMenuListPort"]; listMessageConnectorSpy = createPortSpyMock(AutofillOverlayPort.ListMessageConnector); @@ -143,7 +155,9 @@ describe("OverlayBackground", () => { domainSettingsService.showFavicons$ = showFaviconsMock$; domainSettingsService.neverDomains$ = neverDomainsMock$; logService = mock(); - cipherService = mock(); + cipherService = mock({ + getAllDecryptedForUrl: jest.fn().mockResolvedValue([]), + }); autofillService = mock(); activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Unlocked); authService = mock(); @@ -167,6 +181,7 @@ describe("OverlayBackground", () => { vaultSettingsServiceMock.enablePasskeys$ = enablePasskeysMock$; fido2ActiveRequestManager = new Fido2ActiveRequestManager(); selectedThemeMock$ = new BehaviorSubject(ThemeType.Light); + inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); themeStateService = mock(); themeStateService.selectedTheme$ = selectedThemeMock$; overlayBackground = new OverlayBackground( @@ -181,7 +196,10 @@ describe("OverlayBackground", () => { platformUtilsService, vaultSettingsServiceMock, fido2ActiveRequestManager, + inlineMenuFieldQualificationService, themeStateService, + generatedPasswordCallbackMock, + addPasswordCallbackMock, ); portKeyForTabSpy = overlayBackground["portKeyForTab"]; pageDetailsForTabSpy = overlayBackground["pageDetailsForTab"]; @@ -552,7 +570,10 @@ describe("OverlayBackground", () => { command: "updateIsFieldCurrentlyFocused", isFieldCurrentlyFocused: false, }, - mock({ frameId: 20 }), + mock({ + tab: createChromeTabMock({ id: 1 }), + frameId: 20, + }), ); sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); @@ -608,8 +629,11 @@ describe("OverlayBackground", () => { it("skips updating the inline menu list if the user has the inline menu set to open on button click", async () => { inlineMenuVisibilityMock$.next(AutofillOverlayVisibility.OnButtonClick); + jest + .spyOn(overlayBackground as any, "checkIsInlineMenuListVisible") + .mockReturnValue(false); tabsSendMessageSpy.mockImplementation((_tab, message, _options) => { - if (message.command === "checkMostRecentlyFocusedFieldHasValue") { + if (message.command === "checkFocusedFieldHasValue") { return Promise.resolve(true); } @@ -640,7 +664,7 @@ describe("OverlayBackground", () => { it("skips updating the inline menu list if the focused field has a value and the user status is not unlocked", async () => { activeAccountStatusMock$.next(AuthenticationStatus.Locked); tabsSendMessageSpy.mockImplementation((_tab, message, _options) => { - if (message.command === "checkMostRecentlyFocusedFieldHasValue") { + if (message.command === "checkFocusedFieldHasValue") { return Promise.resolve(true); } @@ -792,21 +816,6 @@ describe("OverlayBackground", () => { expect(cipherService.getAllDecryptedForUrl).not.toHaveBeenCalled(); }); - it("closes the inline menu on the focused field's tab if the user's auth status is not unlocked", async () => { - activeAccountStatusMock$.next(AuthenticationStatus.Locked); - const previousTab = mock({ id: 1 }); - overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ tabId: 1 }); - getTabSpy.mockResolvedValueOnce(previousTab); - - await overlayBackground.updateOverlayCiphers(); - - expect(tabsSendMessageSpy).toHaveBeenCalledWith( - previousTab, - { command: "closeAutofillInlineMenu", overlayElement: undefined }, - { frameId: 0 }, - ); - }); - it("closes the inline menu on the focused field's tab if current tab is different", async () => { getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); cipherService.getAllDecryptedForUrl.mockResolvedValue([loginCipher1, cardCipher]); @@ -816,6 +825,7 @@ describe("OverlayBackground", () => { getTabSpy.mockResolvedValueOnce(previousTab); await overlayBackground.updateOverlayCiphers(); + await flushPromises(); expect(tabsSendMessageSpy).toHaveBeenCalledWith( previousTab, @@ -830,6 +840,7 @@ describe("OverlayBackground", () => { cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); await overlayBackground.updateOverlayCiphers(); + await flushPromises(); expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled(); expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url, [ @@ -852,6 +863,7 @@ describe("OverlayBackground", () => { cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); await overlayBackground.updateOverlayCiphers(false); + await flushPromises(); expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled(); expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url); @@ -870,6 +882,7 @@ describe("OverlayBackground", () => { cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); await overlayBackground.updateOverlayCiphers(false); + await flushPromises(); expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled(); expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url, [ @@ -885,18 +898,20 @@ describe("OverlayBackground", () => { ); }); - it("posts an `updateOverlayListCiphers` message to the overlay list port, and send a `updateAutofillInlineMenuListCiphers` message to the tab indicating that the list of ciphers is populated", async () => { + it("posts an `updateAutofillInlineMenuListCiphers` message to the overlay list port, and send a `updateAutofillInlineMenuListCiphers` message to the tab indicating that the list of ciphers is populated", async () => { overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ tabId: tab.id }); cipherService.getAllDecryptedForUrl.mockResolvedValue([loginCipher1]); cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); await overlayBackground.updateOverlayCiphers(); + await flushPromises(); expect(listPortSpy.postMessage).toHaveBeenCalledWith({ command: "updateAutofillInlineMenuListCiphers", showInlineMenuAccountCreation: false, showPasskeysLabels: false, + focusedFieldHasValue: false, ciphers: [ { accountCreationFieldType: undefined, @@ -923,18 +938,20 @@ describe("OverlayBackground", () => { it("updates the inline menu list with card ciphers", async () => { overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ tabId: tab.id, - filledByCipherType: CipherType.Card, + inlineMenuFillType: CipherType.Card, }); cipherService.getAllDecryptedForUrl.mockResolvedValue([loginCipher1, cardCipher]); cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); await overlayBackground.updateOverlayCiphers(); + await flushPromises(); expect(listPortSpy.postMessage).toHaveBeenCalledWith({ command: "updateAutofillInlineMenuListCiphers", showInlineMenuAccountCreation: false, showPasskeysLabels: false, + focusedFieldHasValue: false, ciphers: [ { accountCreationFieldType: undefined, @@ -960,18 +977,19 @@ describe("OverlayBackground", () => { overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ tabId: tab.id, accountCreationFieldType: "text", - showInlineMenuAccountCreation: true, }); cipherService.getAllDecryptedForUrl.mockResolvedValue([identityCipher, cardCipher]); cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); await overlayBackground.updateOverlayCiphers(); + await flushPromises(); expect(listPortSpy.postMessage).toHaveBeenCalledWith({ command: "updateAutofillInlineMenuListCiphers", showInlineMenuAccountCreation: true, showPasskeysLabels: false, + focusedFieldHasValue: false, ciphers: [ { accountCreationFieldType: "text", @@ -999,18 +1017,20 @@ describe("OverlayBackground", () => { overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ tabId: tab.id, accountCreationFieldType: "text", - showInlineMenuAccountCreation: true, + inlineMenuFillType: InlineMenuFillType.AccountCreationUsername, }); cipherService.getAllDecryptedForUrl.mockResolvedValue([loginCipher1, identityCipher]); cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); await overlayBackground.updateOverlayCiphers(); + await flushPromises(); expect(listPortSpy.postMessage).toHaveBeenCalledWith({ command: "updateAutofillInlineMenuListCiphers", showInlineMenuAccountCreation: true, showPasskeysLabels: false, + focusedFieldHasValue: false, ciphers: [ { accountCreationFieldType: "text", @@ -1056,7 +1076,6 @@ describe("OverlayBackground", () => { overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ tabId: tab.id, accountCreationFieldType: "email", - showInlineMenuAccountCreation: true, }); const identityCipherWithoutUsername = mock({ id: "id-5", @@ -1076,11 +1095,13 @@ describe("OverlayBackground", () => { getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); await overlayBackground.updateOverlayCiphers(); + await flushPromises(); expect(listPortSpy.postMessage).toHaveBeenCalledWith({ command: "updateAutofillInlineMenuListCiphers", showInlineMenuAccountCreation: true, showPasskeysLabels: false, + focusedFieldHasValue: false, ciphers: [ { accountCreationFieldType: "email", @@ -1108,18 +1129,19 @@ describe("OverlayBackground", () => { overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ tabId: tab.id, accountCreationFieldType: "password", - showInlineMenuAccountCreation: true, }); cipherService.getAllDecryptedForUrl.mockResolvedValue([identityCipher]); cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); await overlayBackground.updateOverlayCiphers(); + await flushPromises(); expect(listPortSpy.postMessage).toHaveBeenCalledWith({ command: "updateAutofillInlineMenuListCiphers", showInlineMenuAccountCreation: true, showPasskeysLabels: false, + focusedFieldHasValue: false, ciphers: [], }); }); @@ -1133,7 +1155,7 @@ describe("OverlayBackground", () => { ); overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ tabId: tab.id, - filledByCipherType: CipherType.Login, + inlineMenuFillType: CipherType.Login, showPasskeys: true, }); cipherService.getAllDecryptedForUrl.mockResolvedValue([loginCipher1, passkeyCipher]); @@ -1141,6 +1163,7 @@ describe("OverlayBackground", () => { getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); await overlayBackground.updateOverlayCiphers(); + await flushPromises(); expect(listPortSpy.postMessage).toHaveBeenCalledWith({ command: "updateAutofillInlineMenuListCiphers", @@ -1205,6 +1228,7 @@ describe("OverlayBackground", () => { ], showInlineMenuAccountCreation: false, showPasskeysLabels: true, + focusedFieldHasValue: false, }); }); @@ -1216,7 +1240,7 @@ describe("OverlayBackground", () => { ); overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ tabId: tab.id, - filledByCipherType: CipherType.Login, + inlineMenuFillType: CipherType.Login, showPasskeys: true, }); cipherService.getAllDecryptedForUrl.mockResolvedValue([loginCipher1, passkeyCipher]); @@ -1225,6 +1249,7 @@ describe("OverlayBackground", () => { neverDomainsMock$.next({ "jest-testing-website.com": null }); await overlayBackground.updateOverlayCiphers(); + await flushPromises(); expect(listPortSpy.postMessage).toHaveBeenCalledWith({ command: "updateAutofillInlineMenuListCiphers", @@ -1268,6 +1293,7 @@ describe("OverlayBackground", () => { ], showInlineMenuAccountCreation: false, showPasskeysLabels: false, + focusedFieldHasValue: false, }); }); @@ -1280,7 +1306,7 @@ describe("OverlayBackground", () => { ); overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ tabId: tab.id, - filledByCipherType: CipherType.Login, + inlineMenuFillType: CipherType.Login, showPasskeys: true, }); cipherService.getAllDecryptedForUrl.mockResolvedValue([loginCipher1, passkeyCipher]); @@ -1288,6 +1314,7 @@ describe("OverlayBackground", () => { getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); await overlayBackground.updateOverlayCiphers(); + await flushPromises(); expect(listPortSpy.postMessage).toHaveBeenCalledWith({ command: "updateAutofillInlineMenuListCiphers", @@ -1331,8 +1358,70 @@ describe("OverlayBackground", () => { ], showInlineMenuAccountCreation: false, showPasskeysLabels: false, + focusedFieldHasValue: false, }); }); + + it("updates the inline menu list with login ciphers when the field fill type is for updating the current password", async () => { + sendMockExtensionMessage( + { + command: "updateFocusedFieldData", + focusedFieldData: createFocusedFieldDataMock({ + inlineMenuFillType: InlineMenuFillType.CurrentPasswordUpdate, + }), + }, + mock({ tab }), + ); + getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); + cipherService.getAllDecryptedForUrl.mockResolvedValue([loginCipher2, loginCipher1]); + cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); + + await overlayBackground.updateOverlayCiphers(false); + await flushPromises(); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith( + expect.objectContaining({ + ciphers: [ + { + id: "inline-menu-cipher-0", + name: loginCipher1.name, + type: CipherType.Login, + reprompt: loginCipher1.reprompt, + favorite: loginCipher1.favorite, + icon: { + fallbackImage: "images/bwi-globe.png", + icon: "bwi-globe", + image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", + imageEnabled: true, + }, + accountCreationFieldType: undefined, + login: { + username: loginCipher1.login.username, + passkey: null, + }, + }, + { + id: "inline-menu-cipher-1", + name: loginCipher2.name, + type: CipherType.Login, + reprompt: loginCipher2.reprompt, + favorite: loginCipher2.favorite, + icon: { + fallbackImage: "images/bwi-globe.png", + icon: "bwi-globe", + image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", + imageEnabled: true, + }, + accountCreationFieldType: undefined, + login: { + username: loginCipher2.login.username, + passkey: null, + }, + }, + ], + }), + ); + }); }); describe("extension message handlers", () => { @@ -1874,7 +1963,6 @@ describe("OverlayBackground", () => { const focusedFieldData = createFocusedFieldDataMock({ tabId: tab.id, frameId: sender.frameId, - showInlineMenuAccountCreation: true, }); sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender); @@ -1885,6 +1973,7 @@ describe("OverlayBackground", () => { ciphers: [], showInlineMenuAccountCreation: true, showPasskeysLabels: false, + focusedFieldHasValue: false, }); }); @@ -1895,7 +1984,7 @@ describe("OverlayBackground", () => { const focusedFieldData = createFocusedFieldDataMock({ tabId: tab.id, frameId: sender.frameId, - filledByCipherType: CipherType.Login, + inlineMenuFillType: CipherType.Login, }); sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender); await flushPromises(); @@ -1903,7 +1992,7 @@ describe("OverlayBackground", () => { const newFocusedFieldData = createFocusedFieldDataMock({ tabId: tab.id, frameId: sender.frameId, - filledByCipherType: CipherType.Card, + inlineMenuFillType: CipherType.Card, }); sendMockExtensionMessage( { command: "updateFocusedFieldData", focusedFieldData: newFocusedFieldData }, @@ -1913,6 +2002,109 @@ describe("OverlayBackground", () => { expect(updateOverlayCiphersSpy).toHaveBeenCalled(); }); + + describe("displaying the password generation menu", () => { + const tab = createChromeTabMock({ id: 2 }); + const sender = mock({ tab, frameId: 100 }); + let focusedFieldData: FocusedFieldData; + + beforeEach(async () => { + await initOverlayElementPorts(); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); + overlayBackground["focusedFieldData"] = createFocusedFieldDataMock(); + overlayBackground["isInlineMenuButtonVisible"] = true; + focusedFieldData = createFocusedFieldDataMock({ + tabId: tab.id, + frameId: sender.frameId, + }); + }); + + it("displays the password generator when the focused field is for password generation", async () => { + focusedFieldData.inlineMenuFillType = InlineMenuFillType.PasswordGeneration; + + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender); + await flushPromises(); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith( + expect.objectContaining({ + command: "updateAutofillInlineMenuGeneratedPassword", + }), + ); + }); + + it("displays the password generator when the focused field is for login and the field has an account creation type of password", async () => { + focusedFieldData.inlineMenuFillType = CipherType.Login; + focusedFieldData.accountCreationFieldType = InlineMenuAccountCreationFieldType.Password; + + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender); + await flushPromises(); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith( + expect.objectContaining({ + command: "updateAutofillInlineMenuGeneratedPassword", + }), + ); + }); + }); + + describe("displaying the save login menu", () => { + const tab = createChromeTabMock({ id: 2 }); + const sender = mock({ tab, frameId: 100 }); + let focusedFieldData: FocusedFieldData; + let formData: InlineMenuFormFieldData; + + beforeEach(async () => { + await initOverlayElementPorts(); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); + overlayBackground["focusedFieldData"] = createFocusedFieldDataMock(); + overlayBackground["isInlineMenuButtonVisible"] = true; + focusedFieldData = createFocusedFieldDataMock({ + tabId: tab.id, + frameId: sender.frameId, + }); + formData = { + uri: "https://example.com", + username: "username", + password: "password", + newPassword: "newPassword", + }; + tabsSendMessageSpy.mockImplementation((_tab, message) => { + if (message.command === "getInlineMenuFormFieldData") { + return Promise.resolve(formData); + } + + return Promise.resolve(); + }); + }); + + it("shows the save login menu when the focused field type is for password generation and the field is filled", async () => { + focusedFieldData.inlineMenuFillType = InlineMenuFillType.PasswordGeneration; + + sendMockExtensionMessage( + { command: "updateFocusedFieldData", focusedFieldData, focusedFieldHasValue: true }, + sender, + ); + await flushPromises(); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "showSaveLoginInlineMenuList", + }); + }); + + it("shows the save login menu when the focused field type is for a login cipher and the field is filled", async () => { + focusedFieldData.inlineMenuFillType = CipherType.Login; + + sendMockExtensionMessage( + { command: "updateFocusedFieldData", focusedFieldData, focusedFieldHasValue: true }, + sender, + ); + await flushPromises(); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "showSaveLoginInlineMenuList", + }); + }); + }); }); describe("updateIsFieldCurrentlyFocused message handler", () => { @@ -2004,48 +2196,195 @@ describe("OverlayBackground", () => { describe("openAutofillInlineMenu message handler", () => { let sender: chrome.runtime.MessageSender; + const topFrameSendOptions = { frameId: 0 }; beforeEach(() => { - sender = mock({ tab: { id: 1 } }); + sender = mock({ + tab: createChromeTabMock({ id: 1, url: "https://jest-testing-website.com" }), + }); getTabFromCurrentWindowIdSpy.mockResolvedValue(sender.tab); tabsSendMessageSpy.mockImplementation(); + sendMockExtensionMessage( + { command: "updateFocusedFieldData", focusedFieldData: createFocusedFieldDataMock() }, + sender, + ); }); - it("opens the autofill inline menu by sending a message to the current tab", async () => { - sendMockExtensionMessage({ command: "openAutofillInlineMenu" }, sender); + it("updates the inline menu position of both button and list elements if the inline menu is being forced open", async () => { + sendMockExtensionMessage( + { command: "openAutofillInlineMenu", isOpeningFullInlineMenu: true }, + sender, + ); await flushPromises(); expect(tabsSendMessageSpy).toHaveBeenCalledWith( sender.tab, { - command: "openAutofillInlineMenu", - isFocusingFieldElement: false, - isOpeningFullInlineMenu: false, - authStatus: AuthenticationStatus.Unlocked, + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.Button, }, - { frameId: 0 }, + topFrameSendOptions, ); - }); - - it("sends the open menu message to the focused field's frameId", async () => { - sender.frameId = 10; - sendMockExtensionMessage({ command: "updateFocusedFieldData" }, sender); - await flushPromises(); - - sendMockExtensionMessage({ command: "openAutofillInlineMenu" }, sender); - await flushPromises(); - expect(tabsSendMessageSpy).toHaveBeenCalledWith( sender.tab, { - command: "openAutofillInlineMenu", - isFocusingFieldElement: false, - isOpeningFullInlineMenu: false, - authStatus: AuthenticationStatus.Unlocked, + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.List, }, - { frameId: 10 }, + topFrameSendOptions, ); }); + + describe("when the focused field does not have a value", () => { + beforeEach(() => { + jest + .spyOn(overlayBackground as any, "checkFocusedFieldHasValue") + .mockResolvedValue(false); + }); + + it("updates the position of the both button and list elements if the user has the inline menu set to show on field focus", async () => { + inlineMenuVisibilityMock$.next(AutofillOverlayVisibility.OnFieldFocus); + + sendMockExtensionMessage({ command: "openAutofillInlineMenu" }, sender); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.Button, + }, + topFrameSendOptions, + ); + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.List, + }, + topFrameSendOptions, + ); + }); + + it("closes the list if the user has the inline menu set to show on button click and the list is open", async () => { + overlayBackground["isInlineMenuListVisible"] = true; + inlineMenuVisibilityMock$.next(AutofillOverlayVisibility.OnButtonClick); + + sendMockExtensionMessage({ command: "openAutofillInlineMenu" }, sender); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { command: "closeAutofillInlineMenu", overlayElement: AutofillOverlayElement.List }, + topFrameSendOptions, + ); + }); + + it("updates the position of the button if the user has the inline menu set to show on button click", async () => { + inlineMenuVisibilityMock$.next(AutofillOverlayVisibility.OnButtonClick); + + sendMockExtensionMessage({ command: "openAutofillInlineMenu" }, sender); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.Button, + }, + topFrameSendOptions, + ); + expect(tabsSendMessageSpy).not.toHaveBeenCalledWith( + sender.tab, + { + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.List, + }, + topFrameSendOptions, + ); + }); + }); + + describe("when the focused field has a value", () => { + beforeEach(() => { + overlayBackground["inlineMenuCiphers"] = new Map([ + ["inline-menu-cipher-1", mock({ id: "inline-menu-cipher-1" })], + ]); + jest.spyOn(overlayBackground as any, "checkFocusedFieldHasValue").mockResolvedValue(true); + }); + + it("updates the position of both button and list elements if the inline menu is showing the save login view", async () => { + overlayBackground["inlineMenuCiphers"] = new Map([]); + const formData = { + uri: "https://example.com", + username: "username", + password: "password", + newPassword: "newPassword", + }; + tabsSendMessageSpy.mockImplementation((_tab, message) => { + if (message.command === "getInlineMenuFormFieldData") { + return Promise.resolve(formData); + } + + return Promise.resolve(); + }); + + sendMockExtensionMessage({ command: "openAutofillInlineMenu" }, sender); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.Button, + }, + topFrameSendOptions, + ); + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.List, + }, + topFrameSendOptions, + ); + }); + + it("closes the inline menu list if it is visible", async () => { + overlayBackground["isInlineMenuListVisible"] = true; + + sendMockExtensionMessage({ command: "openAutofillInlineMenu" }, sender); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { command: "closeAutofillInlineMenu", overlayElement: AutofillOverlayElement.List }, + topFrameSendOptions, + ); + }); + + it("updates the position of the inline menu button", async () => { + sendMockExtensionMessage({ command: "openAutofillInlineMenu" }, sender); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.Button, + }, + topFrameSendOptions, + ); + expect(tabsSendMessageSpy).not.toHaveBeenCalledWith( + sender.tab, + { + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.List, + }, + topFrameSendOptions, + ); + }); + }); }); describe("closeAutofillInlineMenu", () => { @@ -2239,192 +2578,19 @@ describe("OverlayBackground", () => { }); }); - describe("updateAutofillInlineMenuPosition message handler", () => { - beforeEach(async () => { - await initOverlayElementPorts(); - }); - - it("ignores updating the position if the overlay element type is not provided", () => { - sendMockExtensionMessage({ command: "updateAutofillInlineMenuPosition" }); - - expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: expect.anything(), - }); - expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: expect.anything(), - }); - }); - - it("skips updating the position if the most recently focused field is different than the message sender", () => { - const sender = mock({ tab: { id: 1 } }); - const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ command: "updateAutofillInlineMenuPosition" }, sender); - - expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: expect.anything(), - }); - expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: expect.anything(), - }); - }); - - it("updates the inline menu button's position", async () => { - const focusedFieldData = createFocusedFieldDataMock(); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ - command: "updateAutofillInlineMenuPosition", - overlayElement: AutofillOverlayElement.Button, - }); - await flushPromises(); - - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "updateAutofillInlineMenuPosition", - styles: { height: "2px", left: "4px", top: "2px", width: "2px" }, - }); - }); - - it("modifies the inline menu button's height for medium sized input elements", async () => { - const focusedFieldData = createFocusedFieldDataMock({ - focusedFieldRects: { top: 1, left: 2, height: 35, width: 4 }, - }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ - command: "updateAutofillInlineMenuPosition", - overlayElement: AutofillOverlayElement.Button, - }); - await flushPromises(); - - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "updateAutofillInlineMenuPosition", - styles: { height: "20px", left: "-22px", top: "8px", width: "20px" }, - }); - }); - - it("modifies the inline menu button's height for large sized input elements", async () => { - const focusedFieldData = createFocusedFieldDataMock({ - focusedFieldRects: { top: 1, left: 2, height: 50, width: 4 }, - }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ - command: "updateAutofillInlineMenuPosition", - overlayElement: AutofillOverlayElement.Button, - }); - await flushPromises(); - - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "updateAutofillInlineMenuPosition", - styles: { height: "27px", left: "-32px", top: "13px", width: "27px" }, - }); - }); - - it("takes into account the right padding of the focused field in positioning the button if the right padding of the field is larger than the left padding", async () => { - const focusedFieldData = createFocusedFieldDataMock({ - focusedFieldStyles: { paddingRight: "20px", paddingLeft: "6px" }, - }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ - command: "updateAutofillInlineMenuPosition", - overlayElement: AutofillOverlayElement.Button, - }); - await flushPromises(); - - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "updateAutofillInlineMenuPosition", - styles: { height: "2px", left: "-18px", top: "2px", width: "2px" }, - }); - }); - - it("updates the inline menu list's position", async () => { - const focusedFieldData = createFocusedFieldDataMock(); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ - command: "updateAutofillInlineMenuPosition", - overlayElement: AutofillOverlayElement.List, - }); - await flushPromises(); - - expect(listPortSpy.postMessage).toHaveBeenCalledWith({ - command: "updateAutofillInlineMenuPosition", - styles: { left: "2px", top: "4px", width: "4px" }, - }); - }); - - it("sends a message that triggers a simultaneous fade in for both inline menu elements", async () => { - jest.useFakeTimers(); - const focusedFieldData = createFocusedFieldDataMock(); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ - command: "updateAutofillInlineMenuPosition", - overlayElement: AutofillOverlayElement.List, - }); - await flushPromises(); - jest.advanceTimersByTime(150); - - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "fadeInAutofillInlineMenuIframe", - }); - expect(listPortSpy.postMessage).toHaveBeenCalledWith({ - command: "fadeInAutofillInlineMenuIframe", - }); - }); - - describe("getAutofillInlineMenuPosition", () => { - it("returns the current inline menu position", async () => { - overlayBackground["inlineMenuPosition"] = { - button: { left: 1, top: 2, width: 3, height: 4 }, - }; - - sendMockExtensionMessage( - { command: "getAutofillInlineMenuPosition" }, - mock(), - sendResponse, - ); - await flushPromises(); - - expect(sendResponse).toHaveBeenCalledWith({ - button: { left: 1, top: 2, width: 3, height: 4 }, - }); - }); - }); - - it("triggers a debounced reposition of the inline menu if the sender frame has a `null` sub frame offsets value", async () => { - jest.useFakeTimers(); - const focusedFieldData = createFocusedFieldDataMock(); - const sender = mock({ - tab: { id: focusedFieldData.tabId }, - frameId: focusedFieldData.frameId, - }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender); - overlayBackground["subFrameOffsetsForTab"][focusedFieldData.tabId] = new Map([ - [focusedFieldData.frameId, null], - ]); - jest.spyOn(overlayBackground as any, "updateInlineMenuPositionAfterRepositionEvent"); + describe("getAutofillInlineMenuPosition", () => { + it("returns the current inline menu positio", async () => { + const inlineMenuPosition: InlineMenuPosition = mock(); + overlayBackground["inlineMenuPosition"] = inlineMenuPosition; sendMockExtensionMessage( - { - command: "updateAutofillInlineMenuPosition", - overlayElement: AutofillOverlayElement.List, - }, - sender, + { command: "getAutofillInlineMenuPosition" }, + mock(), + sendResponse, ); await flushPromises(); - jest.advanceTimersByTime(150); - expect( - overlayBackground["updateInlineMenuPositionAfterRepositionEvent"], - ).toHaveBeenCalled(); + expect(sendResponse).toHaveBeenCalledWith(inlineMenuPosition); }); }); @@ -2553,10 +2719,8 @@ describe("OverlayBackground", () => { }); describe("unlockCompleted", () => { - let updateInlineMenuCiphersSpy: jest.SpyInstance; - beforeEach(async () => { - updateInlineMenuCiphersSpy = jest.spyOn(overlayBackground, "updateOverlayCiphers"); + jest.spyOn(overlayBackground, "updateOverlayCiphers"); await initOverlayElementPorts(); }); @@ -2578,8 +2742,8 @@ describe("OverlayBackground", () => { expect(updateInlineMenuCiphersSpy).toHaveBeenCalled(); }); - it("opens the inline menu if a retry command is present in the message", async () => { - updateInlineMenuCiphersSpy.mockImplementation(); + it("focuses the most recently focused field if a retry command is present in the message", async () => { + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(createChromeTabMock({ id: 1 })); sendMockExtensionMessage({ command: "unlockCompleted", @@ -2589,16 +2753,9 @@ describe("OverlayBackground", () => { }); await flushPromises(); - expect(tabsSendMessageSpy).toHaveBeenCalledWith( - expect.any(Object), - { - command: "openAutofillInlineMenu", - isFocusingFieldElement: true, - isOpeningFullInlineMenu: false, - authStatus: AuthenticationStatus.Unlocked, - }, - { frameId: 0 }, - ); + expect(tabsSendMessageSpy).toHaveBeenCalledWith(expect.any(Object), { + command: "focusMostRecentlyFocusedField", + }); }); }); @@ -2667,7 +2824,7 @@ describe("OverlayBackground", () => { const portKey = "inlineMenuButtonPort"; beforeEach(async () => { - sender = mock({ tab: { id: 1 } }); + sender = mock({ tab: createChromeTabMock({ id: 1 }) }); portKeyForTabSpy[sender.tab.id] = portKey; activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); await initOverlayElementPorts(); @@ -2703,6 +2860,16 @@ describe("OverlayBackground", () => { it("opens the inline menu if the user auth status is unlocked", async () => { getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(sender.tab); + sendMockExtensionMessage( + { + command: "updateFocusedFieldData", + focusedFieldData: mock(), + }, + mock({ tab: buttonMessageConnectorSpy.sender.tab }), + ); + jest + .spyOn(overlayBackground as any, "getInlineMenuButtonPosition") + .mockReturnValueOnce({ x: 0, y: 0 }); sendPortMessage(buttonMessageConnectorSpy, { command: "autofillInlineMenuButtonClicked", portKey, @@ -2712,10 +2879,8 @@ describe("OverlayBackground", () => { expect(tabsSendMessageSpy).toHaveBeenCalledWith( sender.tab, { - command: "openAutofillInlineMenu", - isFocusingFieldElement: false, - isOpeningFullInlineMenu: true, - authStatus: AuthenticationStatus.Unlocked, + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.List, }, { frameId: 0 }, ); @@ -2936,7 +3101,7 @@ describe("OverlayBackground", () => { expect(autofillService.doAutoFill).not.toHaveBeenCalled(); }); - it("autofills the selected cipher and move it to the top of the front of the ciphers map", async () => { + it("autofills the selected cipher and moves it to the top of the front of the ciphers map", async () => { const cipher1 = mock({ id: "inline-menu-cipher-1" }); const cipher2 = mock({ id: "inline-menu-cipher-2" }); const cipher3 = mock({ id: "inline-menu-cipher-3" }); @@ -3094,6 +3259,54 @@ describe("OverlayBackground", () => { }); }); }); + + it("fills the current password fields exclusively when filling for a current password update", async () => { + globalThis.structuredClone = jest.fn((value) => value); + sendMockExtensionMessage( + { + command: "updateFocusedFieldData", + focusedFieldData: createFocusedFieldDataMock({ + inlineMenuFillType: InlineMenuFillType.CurrentPasswordUpdate, + }), + }, + sender, + ); + const currentPasswordField = createAutofillFieldMock({ + autoCompleteType: "current-password", + title: "Current Password", + type: "password", + }); + const newPasswordField = createAutofillFieldMock({ + autoCompleteType: "new-password", + title: "New Password", + type: "password", + }); + const confirmNewPasswordField = createAutofillFieldMock({ + autoCompleteType: "new-password", + title: "Confirm New Password", + type: "password", + }); + const pageDetails = createAutofillPageDetailsMock({ + fields: [currentPasswordField, newPasswordField, confirmNewPasswordField], + }); + overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ + [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails }], + ]); + + sendPortMessage(listMessageConnectorSpy, { + command: "fillAutofillInlineMenuCipher", + inlineMenuCipherId: "inline-menu-cipher-2", + portKey, + }); + await flushPromises(); + + pageDetails.fields = [currentPasswordField]; + expect(autofillService.doAutoFill).toHaveBeenCalledWith( + expect.objectContaining({ + pageDetails: [expect.objectContaining({ details: pageDetails })], + }), + ); + }); }); describe("addNewVaultItem message handler", () => { @@ -3102,10 +3315,17 @@ describe("OverlayBackground", () => { sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); await flushPromises(); - sendPortMessage(listMessageConnectorSpy, { command: "addNewVaultItem", portKey }); + sendPortMessage(listMessageConnectorSpy, { + command: "addNewVaultItem", + portKey, + addNewCipherType: CipherType.Login, + }); await flushPromises(); - expect(tabsSendMessageSpy).not.toHaveBeenCalled(); + expect(tabsSendMessageSpy).not.toHaveBeenCalledWith(sender.tab, { + command: "addNewVaultItemFromOverlay", + addNewCipherType: CipherType.Login, + }); }); it("sends a message to the tab to add a new vault item", async () => { @@ -3219,6 +3439,113 @@ describe("OverlayBackground", () => { }); }); }); + + describe("refreshGeneratedPassword", () => { + it("refreshes the generated password", async () => { + overlayBackground["generatedPassword"] = "populated"; + + sendPortMessage(listMessageConnectorSpy, { command: "refreshGeneratedPassword", portKey }); + await flushPromises(); + + expect(generatedPasswordCallbackMock).toHaveBeenCalled(); + }); + + it("sends a message to the list port indicating that the generated password should be updated", async () => { + sendPortMessage(listMessageConnectorSpy, { command: "refreshGeneratedPassword", portKey }); + await flushPromises(); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateAutofillInlineMenuGeneratedPassword", + generatedPassword, + refreshPassword: true, + }); + }); + }); + + describe("fillGeneratedPassword", () => { + const focusedFieldData = createFocusedFieldDataMock({ + inlineMenuFillType: InlineMenuFillType.PasswordGeneration, + }); + + beforeEach(() => { + globalThis.structuredClone = jest.fn((value) => value); + sendMockExtensionMessage( + { + command: "updateFocusedFieldData", + focusedFieldData, + }, + sender, + ); + overlayBackground["generatedPassword"] = generatedPassword; + overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ + [sender.frameId, createPageDetailMock()], + ]); + }); + + describe("skipping filling the generated password", () => { + it("skips filling when the password has not been created", () => { + overlayBackground["generatedPassword"] = ""; + + sendPortMessage(listMessageConnectorSpy, { command: "fillGeneratedPassword", portKey }); + + expect(autofillService.doAutoFill).not.toHaveBeenCalled(); + }); + + it("skips filling when the page details for the tab are not set", () => { + overlayBackground["pageDetailsForTab"][sender.tab.id] = undefined; + + sendPortMessage(listMessageConnectorSpy, { command: "fillGeneratedPassword", portKey }); + + expect(autofillService.doAutoFill).not.toHaveBeenCalled(); + }); + + it("skips filling when the page details for the tab does not contain a value", () => { + overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([]); + + sendPortMessage(listMessageConnectorSpy, { command: "fillGeneratedPassword", portKey }); + + expect(autofillService.doAutoFill).not.toHaveBeenCalled(); + }); + }); + + it("filters the page details to only include the new password fields before filling", async () => { + sendPortMessage(listMessageConnectorSpy, { command: "fillGeneratedPassword", portKey }); + await flushPromises(); + + expect(autofillService.doAutoFill).toHaveBeenCalledWith({ + tab: sender.tab, + cipher: expect.any(Object), + pageDetails: [overlayBackground["pageDetailsForTab"][sender.tab.id].get(sender.frameId)], + fillNewPassword: true, + allowTotpAutofill: false, + }); + }); + + it("opens the inline menu for fields that fill a generated password", async () => { + jest.useFakeTimers(); + const formData = { + uri: "https://example.com", + username: "username", + password: "password", + newPassword: "newPassword", + }; + tabsSendMessageSpy.mockImplementation((_tab, message) => { + if (message.command === "getInlineMenuFormFieldData") { + return Promise.resolve(formData); + } + + return Promise.resolve(); + }); + const openInlineMenuSpy = jest.spyOn(overlayBackground as any, "openInlineMenu"); + + sendPortMessage(listMessageConnectorSpy, { command: "fillGeneratedPassword", portKey }); + await flushPromises(); + jest.advanceTimersByTime(400); + await flushPromises(); + + expect(openInlineMenuSpy).toHaveBeenCalled(); + }); + }); }); describe("handle web navigation on committed events", () => { @@ -3302,6 +3629,20 @@ describe("OverlayBackground", () => { expect(overlayBackground["expiredPorts"].length).toBe(1); }); + + it("generates a password for the password generator view", async () => { + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); + const focusedFieldData = createFocusedFieldDataMock({ + inlineMenuFillType: CipherType.Login, + accountCreationFieldType: InlineMenuAccountCreationFieldType.Password, + }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + await initOverlayElementPorts(); + await flushPromises(); + + expect(generatedPasswordCallbackMock).toHaveBeenCalled(); + }); }); describe("handle overlay element port onMessage", () => { diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index 49788d67404..41791b3b75f 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -1,13 +1,13 @@ import { + debounceTime, firstValueFrom, + map, merge, + Observable, ReplaySubject, Subject, - throttleTime, switchMap, - debounceTime, - Observable, - map, + throttleTime, } from "rxjs"; import { parse } from "tldts"; @@ -52,13 +52,21 @@ import { import { AutofillOverlayElement, AutofillOverlayPort, + InlineMenuAccountCreationFieldType, + InlineMenuAccountCreationFieldTypes, + InlineMenuFillType, + InlineMenuFillTypes, MAX_SUB_FRAME_DEPTH, } from "../enums/autofill-overlay.enum"; -import { AutofillService } from "../services/abstractions/autofill.service"; +import AutofillField from "../models/autofill-field"; +import { InlineMenuFormFieldData } from "../services/abstractions/autofill-overlay-content.service"; +import { AutofillService, PageDetail } from "../services/abstractions/autofill.service"; +import { InlineMenuFieldQualificationService } from "../services/abstractions/inline-menu-field-qualifications.service"; import { generateDomainMatchPatterns, generateRandomChars, isInvalidResponseStatusCode, + specialCharacterToKeyMap, } from "../utils"; import { LockedVaultPendingNotificationsData } from "./abstractions/notification.background"; @@ -83,33 +91,39 @@ import { SubFrameOffsetData, SubFrameOffsetsForTab, ToggleInlineMenuHiddenMessage, + UpdateInlineMenuVisibilityMessage, + UpdateOverlayCiphersParams, } from "./abstractions/overlay.background"; export class OverlayBackground implements OverlayBackgroundInterface { private readonly openUnlockPopout = openUnlockPopout; private readonly openViewVaultItemPopout = openViewVaultItemPopout; private readonly openAddEditVaultItemPopout = openAddEditVaultItemPopout; - private readonly storeInlineMenuFido2CredentialsSubject = new ReplaySubject(1); + private readonly updateOverlayCiphers$ = new Subject(); + private readonly storeInlineMenuFido2Credentials$ = new ReplaySubject(1); + private readonly startInlineMenuDelayedClose$ = new Subject(); + private readonly cancelInlineMenuDelayedClose$ = new Subject(); + private readonly startInlineMenuFadeIn$ = new Subject(); + private readonly cancelInlineMenuFadeIn$ = new Subject(); + private readonly startUpdateInlineMenuPosition$ = new Subject(); + private readonly cancelUpdateInlineMenuPosition$ = new Subject(); + private readonly repositionInlineMenu$ = new Subject(); + private readonly rebuildSubFrameOffsets$ = new Subject(); + private readonly addNewVaultItem$ = new Subject(); private pageDetailsForTab: PageDetailsForTab = {}; private subFrameOffsetsForTab: SubFrameOffsetsForTab = {}; private portKeyForTab: Record = {}; private expiredPorts: chrome.runtime.Port[] = []; private inlineMenuButtonPort: chrome.runtime.Port; + private inlineMenuButtonMessageConnectorPort: chrome.runtime.Port; private inlineMenuListPort: chrome.runtime.Port; + private inlineMenuListMessageConnectorPort: chrome.runtime.Port; private inlineMenuCiphers: Map = new Map(); private inlineMenuFido2Credentials: Set = new Set(); private inlineMenuPageTranslations: Record; private inlineMenuPosition: InlineMenuPosition = {}; private cardAndIdentityCiphers: Set | null = null; private currentInlineMenuCiphersCount: number = 0; - private delayedCloseTimeout: number | NodeJS.Timeout; - private startInlineMenuFadeInSubject = new Subject(); - private cancelInlineMenuFadeInSubject = new Subject(); - private startUpdateInlineMenuPositionSubject = new Subject(); - private cancelUpdateInlineMenuPositionSubject = new Subject(); - private repositionInlineMenuSubject = new Subject(); - private rebuildSubFrameOffsetsSubject = new Subject(); - private addNewVaultItemSubject = new Subject(); private currentAddNewItemData: CurrentAddNewItemData; private focusedFieldData: FocusedFieldData; private isFieldCurrentlyFocused: boolean = false; @@ -118,6 +132,13 @@ export class OverlayBackground implements OverlayBackgroundInterface { private isInlineMenuListVisible: boolean = false; private showPasskeysLabelsWithinInlineMenu: boolean = false; private iconsServerUrl: string; + private generatedPassword: string; + private readonly validPortConnections: Set = new Set([ + AutofillOverlayPort.Button, + AutofillOverlayPort.ButtonMessageConnector, + AutofillOverlayPort.List, + AutofillOverlayPort.ListMessageConnector, + ]); private readonly extensionMessageHandlers: OverlayBackgroundExtensionMessageHandlers = { autofillOverlayElementClosed: ({ message, sender }) => this.overlayElementClosed(message, sender), @@ -132,14 +153,13 @@ export class OverlayBackground implements OverlayBackgroundInterface { updateIsFieldCurrentlyFilling: ({ message }) => this.updateIsFieldCurrentlyFilling(message), checkIsFieldCurrentlyFilling: () => this.checkIsFieldCurrentlyFilling(), getAutofillInlineMenuVisibility: () => this.getInlineMenuVisibility(), + openAutofillInlineMenu: ({ message, sender }) => + this.openInlineMenu(sender, message.isOpeningFullInlineMenu), getInlineMenuCardsVisibility: () => this.getInlineMenuCardsVisibility(), getInlineMenuIdentitiesVisibility: () => this.getInlineMenuIdentitiesVisibility(), - openAutofillInlineMenu: () => this.openInlineMenu(false), closeAutofillInlineMenu: ({ message, sender }) => this.closeInlineMenu(sender, message), checkAutofillInlineMenuFocused: ({ sender }) => this.checkInlineMenuFocused(sender), focusAutofillInlineMenuList: () => this.focusInlineMenuList(), - updateAutofillInlineMenuPosition: ({ message, sender }) => - this.updateInlineMenuPosition(message, sender), getAutofillInlineMenuPosition: () => this.getInlineMenuPosition(), updateAutofillInlineMenuElementIsVisibleStatus: ({ message, sender }) => this.updateInlineMenuElementIsVisibleStatus(message, sender), @@ -148,6 +168,8 @@ export class OverlayBackground implements OverlayBackgroundInterface { getCurrentTabFrameId: ({ sender }) => this.getSenderFrameId(sender), updateSubFrameData: ({ message, sender }) => this.updateSubFrameData(message, sender), triggerSubFrameFocusInRebuild: ({ sender }) => this.triggerSubFrameFocusInRebuild(sender), + shouldRepositionSubFrameInlineMenuOnScroll: ({ sender }) => + this.shouldRepositionSubFrameInlineMenuOnScroll(sender), destroyAutofillInlineMenuListeners: ({ message, sender }) => this.triggerDestroyInlineMenuListeners(sender.tab, message.subFrameData.frameId), collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender), @@ -157,10 +179,11 @@ export class OverlayBackground implements OverlayBackgroundInterface { addEditCipherSubmitted: () => this.updateOverlayCiphers(), editedCipher: () => this.updateOverlayCiphers(), deletedCipher: () => this.updateOverlayCiphers(), + bgSaveCipher: () => this.updateOverlayCiphers(), fido2AbortRequest: ({ sender }) => this.abortFido2ActiveRequest(sender.tab.id), }; private readonly inlineMenuButtonPortMessageHandlers: InlineMenuButtonPortMessageHandlers = { - triggerDelayedAutofillInlineMenuClosure: () => this.triggerDelayedInlineMenuClosure(), + triggerDelayedAutofillInlineMenuClosure: () => this.startInlineMenuDelayedClose$.next(), autofillInlineMenuButtonClicked: ({ port }) => this.handleInlineMenuButtonClicked(port), autofillInlineMenuBlurred: () => this.checkInlineMenuListFocused(), redirectAutofillInlineMenuFocusOut: ({ message, port }) => @@ -168,8 +191,9 @@ export class OverlayBackground implements OverlayBackgroundInterface { updateAutofillInlineMenuColorScheme: () => this.updateInlineMenuButtonColorScheme(), }; private readonly inlineMenuListPortMessageHandlers: InlineMenuListPortMessageHandlers = { - checkAutofillInlineMenuButtonFocused: () => this.checkInlineMenuButtonFocused(), - autofillInlineMenuBlurred: () => this.checkInlineMenuButtonFocused(), + checkAutofillInlineMenuButtonFocused: ({ port }) => + this.checkInlineMenuButtonFocused(port.sender), + autofillInlineMenuBlurred: ({ port }) => this.checkInlineMenuButtonFocused(port.sender), unlockVault: ({ port }) => this.unlockVault(port), fillAutofillInlineMenuCipher: ({ message, port }) => this.fillInlineMenuCipher(message, port), addNewVaultItem: ({ message, port }) => this.getNewVaultItemDetails(message, port), @@ -177,6 +201,8 @@ export class OverlayBackground implements OverlayBackgroundInterface { redirectAutofillInlineMenuFocusOut: ({ message, port }) => this.redirectInlineMenuFocusOut(message, port), updateAutofillInlineMenuListHeight: ({ message }) => this.updateInlineMenuListHeight(message), + refreshGeneratedPassword: () => this.updateGeneratedPassword(true), + fillGeneratedPassword: ({ port }) => this.fillGeneratedPassword(port), }; constructor( @@ -191,7 +217,10 @@ export class OverlayBackground implements OverlayBackgroundInterface { private platformUtilsService: PlatformUtilsService, private vaultSettingsService: VaultSettingsService, private fido2ActiveRequestManager: Fido2ActiveRequestManager, + private inlineMenuFieldQualificationService: InlineMenuFieldQualificationService, private themeStateService: ThemeStateService, + private generatePasswordCallback: () => Promise, + private addPasswordCallback: (password: string) => Promise, ) { this.initOverlayEventObservables(); } @@ -210,22 +239,30 @@ export class OverlayBackground implements OverlayBackgroundInterface { * Initializes event observables that handle events which affect the overlay's behavior. */ private initOverlayEventObservables() { - this.storeInlineMenuFido2CredentialsSubject + this.updateOverlayCiphers$ + .pipe( + throttleTime(100, null, { leading: true, trailing: true }), + switchMap((updateOverlayCiphersParams) => + this.handleOverlayCiphersUpdate(updateOverlayCiphersParams), + ), + ) + .subscribe(); + this.storeInlineMenuFido2Credentials$ .pipe(switchMap((tabId) => this.availablePasskeyAuthCredentials$(tabId))) .subscribe((credentials) => this.storeInlineMenuFido2Credentials(credentials)); - this.repositionInlineMenuSubject + this.repositionInlineMenu$ .pipe( debounceTime(1000), switchMap((sender) => this.repositionInlineMenu(sender)), ) .subscribe(); - this.rebuildSubFrameOffsetsSubject + this.rebuildSubFrameOffsets$ .pipe( - throttleTime(100), + throttleTime(100, null, { leading: true, trailing: true }), switchMap((sender) => this.rebuildSubFrameOffsets(sender)), ) .subscribe(); - this.addNewVaultItemSubject + this.addNewVaultItem$ .pipe( debounceTime(100), switchMap((addNewItemData) => @@ -234,19 +271,24 @@ export class OverlayBackground implements OverlayBackgroundInterface { ) .subscribe(); + // Delayed close of the inline menu + merge( + this.startInlineMenuDelayedClose$.pipe(debounceTime(100)), + this.cancelInlineMenuDelayedClose$, + ) + .pipe(switchMap((cancelSignal) => this.triggerDelayedInlineMenuClosure(!!cancelSignal))) + .subscribe(); + // Debounce used to update inline menu position merge( - this.startUpdateInlineMenuPositionSubject.pipe(debounceTime(150)), - this.cancelUpdateInlineMenuPositionSubject, + this.startUpdateInlineMenuPosition$.pipe(debounceTime(150)), + this.cancelUpdateInlineMenuPosition$, ) .pipe(switchMap((sender) => this.updateInlineMenuPositionAfterRepositionEvent(sender))) .subscribe(); // FadeIn Observable behavior - merge( - this.startInlineMenuFadeInSubject.pipe(debounceTime(150)), - this.cancelInlineMenuFadeInSubject, - ) + merge(this.startInlineMenuFadeIn$.pipe(debounceTime(150)), this.cancelInlineMenuFadeIn$) .pipe(switchMap((cancelSignal) => this.triggerInlineMenuFadeIn(!!cancelSignal))) .subscribe(); } @@ -266,25 +308,43 @@ export class OverlayBackground implements OverlayBackgroundInterface { if (this.portKeyForTab[tabId]) { delete this.portKeyForTab[tabId]; } + + this.generatedPassword = null; + this.focusedFieldData = null; } /** * Updates the inline menu list's ciphers and sends the updated list to the inline menu list iframe. * Queries all ciphers for the given url, and sorts them by last used. Will not update the * list of ciphers if the extension is not unlocked. + * + * @param updateAllCipherTypes - Identifies credit card and identity cipher types should also be updated + * @param refocusField - Identifies whether the most recently focused field should be refocused */ - async updateOverlayCiphers(updateAllCipherTypes = true) { + async updateOverlayCiphers(updateAllCipherTypes = true, refocusField = false) { const authStatus = await firstValueFrom(this.authService.activeAccountStatus$); - if (authStatus !== AuthenticationStatus.Unlocked) { - if (this.focusedFieldData) { - this.closeInlineMenuAfterCiphersUpdate().catch((error) => this.logService.error(error)); - } - return; + if (authStatus === AuthenticationStatus.Unlocked) { + this.inlineMenuCiphers = new Map(); + this.updateOverlayCiphers$.next({ updateAllCipherTypes, refocusField }); } + } + /** + * Handles a throttled update of the inline menu ciphers, acting on the emission of a value from + * an observable. Will update on the first and last emissions within a 100ms time frame. + * + * @param updateAllCipherTypes - Identifies credit card and identity cipher types should also be updated + * @param refocusField - Identifies whether the most recently focused field should be refocused + */ + async handleOverlayCiphersUpdate({ + updateAllCipherTypes, + refocusField, + }: UpdateOverlayCiphersParams) { const currentTab = await BrowserApi.getTabFromCurrentWindowId(); + if (this.focusedFieldData && currentTab?.id !== this.focusedFieldData.tabId) { - this.closeInlineMenuAfterCiphersUpdate().catch((error) => this.logService.error(error)); + const focusedFieldTab = await BrowserApi.getTab(this.focusedFieldData.tabId); + this.closeInlineMenu({ tab: focusedFieldTab }, { forceCloseInlineMenu: true }); } if (!currentTab || !currentTab.url?.startsWith("http")) { @@ -300,20 +360,34 @@ export class OverlayBackground implements OverlayBackgroundInterface { } this.inlineMenuFido2Credentials.clear(); - this.storeInlineMenuFido2CredentialsSubject.next(currentTab.id); + this.storeInlineMenuFido2Credentials$.next(currentTab.id); + + await this.generatePassword(); - this.inlineMenuCiphers = new Map(); const ciphersViews = await this.getCipherViews(currentTab, updateAllCipherTypes); for (let cipherIndex = 0; cipherIndex < ciphersViews.length; cipherIndex++) { this.inlineMenuCiphers.set(`inline-menu-cipher-${cipherIndex}`, ciphersViews[cipherIndex]); } - const ciphers = await this.getInlineMenuCipherData(); - this.inlineMenuListPort?.postMessage({ + await this.updateInlineMenuListCiphers(currentTab); + + if (refocusField) { + await BrowserApi.tabSendMessage(currentTab, { command: "focusMostRecentlyFocusedField" }); + } + } + + /** + * Updates the inline menu list's ciphers and sends the updated list to the inline menu list iframe. + * + * @param tab - The current tab + */ + private async updateInlineMenuListCiphers(tab: chrome.tabs.Tab) { + this.postMessageToPort(this.inlineMenuListPort, { command: "updateAutofillInlineMenuListCiphers", - ciphers, - showInlineMenuAccountCreation: this.showInlineMenuAccountCreation(), + ciphers: await this.getInlineMenuCipherData(), + showInlineMenuAccountCreation: this.shouldShowInlineMenuAccountCreation(), showPasskeysLabels: this.showPasskeysLabelsWithinInlineMenu, + focusedFieldHasValue: await this.checkFocusedFieldHasValue(tab), }); } @@ -357,6 +431,11 @@ export class OverlayBackground implements OverlayBackgroundInterface { CipherType.Identity, ]) ).sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b)); + + if (!this.cardAndIdentityCiphers) { + return cipherViews; + } + for (let cipherIndex = 0; cipherIndex < cipherViews.length; cipherIndex++) { const cipherView = cipherViews[cipherIndex]; if ( @@ -384,7 +463,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { let inlineMenuCipherData: InlineMenuCipherData[]; this.showPasskeysLabelsWithinInlineMenu = false; - if (this.showInlineMenuAccountCreation()) { + if (this.shouldShowInlineMenuAccountCreation()) { inlineMenuCipherData = this.buildInlineMenuAccountCreationCiphers( inlineMenuCiphersArray, true, @@ -476,7 +555,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { for (let cipherIndex = 0; cipherIndex < inlineMenuCiphersArray.length; cipherIndex++) { const [inlineMenuCipherId, cipher] = inlineMenuCiphersArray[cipherIndex]; - if (this.focusedFieldData?.filledByCipherType !== cipher.type) { + if (!this.focusedFieldMatchesFillType(cipher.type)) { continue; } @@ -617,29 +696,66 @@ export class OverlayBackground implements OverlayBackgroundInterface { if ( !showInlineMenuAccountCreation || !this.focusedFieldData?.accountCreationFieldType || - this.focusedFieldData.accountCreationFieldType === "password" + this.focusedFieldMatchesAccountCreationType(InlineMenuAccountCreationFieldType.Password) ) { return { fullName }; } return { fullName, - username: - this.focusedFieldData.accountCreationFieldType === "email" - ? cipher.identity.email - : cipher.identity.username, + username: this.focusedFieldMatchesAccountCreationType( + InlineMenuAccountCreationFieldType.Email, + ) + ? cipher.identity.email + : cipher.identity.username, }; } + /** + * Validates whether the currently focused field has an account + * creation field type that matches the provided field type. + * + * @param fieldType - The field type to validate against + */ + private focusedFieldMatchesAccountCreationType(fieldType: InlineMenuAccountCreationFieldTypes) { + return this.focusedFieldData?.accountCreationFieldType === fieldType; + } + + /** + * Validates whether the most recently focused field has a fill + * type value that matches the provided fill type. + * + * @param fillType - The fill type to validate against + * @param focusedFieldData - Optional focused field data to validate against + */ + private focusedFieldMatchesFillType( + fillType: InlineMenuFillTypes, + focusedFieldData?: FocusedFieldData, + ) { + const focusedFieldFillType = focusedFieldData + ? focusedFieldData.inlineMenuFillType + : this.focusedFieldData?.inlineMenuFillType; + + // When updating the current password for a field, it should fill with a login cipher + if ( + focusedFieldFillType === InlineMenuFillType.CurrentPasswordUpdate && + fillType === CipherType.Login + ) { + return true; + } + + return focusedFieldFillType === fillType; + } + /** * Identifies whether the inline menu is being shown on an account creation field. */ - private showInlineMenuAccountCreation(): boolean { - if (typeof this.focusedFieldData?.showInlineMenuAccountCreation !== "undefined") { - return this.focusedFieldData?.showInlineMenuAccountCreation; + private shouldShowInlineMenuAccountCreation(): boolean { + if (this.focusedFieldMatchesFillType(InlineMenuFillType.AccountCreationUsername)) { + return true; } - if (this.focusedFieldData?.filledByCipherType !== CipherType.Login) { + if (!this.focusedFieldMatchesFillType(CipherType.Login)) { return false; } @@ -692,14 +808,6 @@ export class OverlayBackground implements OverlayBackgroundInterface { return await firstValueFrom(this.domainSettingsService.neverDomains$); } - /** - * Gets the currently focused field and closes the inline menu on that tab. - */ - private async closeInlineMenuAfterCiphersUpdate() { - const focusedFieldTab = await BrowserApi.getTab(this.focusedFieldData.tabId); - this.closeInlineMenu({ tab: focusedFieldTab }, { forceCloseInlineMenu: true }); - } - /** * Handles aggregation of page details for a tab. Stores the page details * in association with the tabId of the tab that sent the message. @@ -864,8 +972,8 @@ export class OverlayBackground implements OverlayBackgroundInterface { * @param sender - The sender of the message */ private async rebuildSubFrameOffsets(sender: chrome.runtime.MessageSender) { - this.cancelUpdateInlineMenuPositionSubject.next(); - this.clearDelayedInlineMenuClosure(); + this.cancelUpdateInlineMenuPosition$.next(); + this.cancelInlineMenuDelayedClose$.next(true); const subFrameOffsetsForTab = this.subFrameOffsetsForTab[sender.tab.id]; if (subFrameOffsetsForTab) { @@ -897,33 +1005,43 @@ export class OverlayBackground implements OverlayBackgroundInterface { ).catch((error) => this.logService.error(error)); } - this.updateInlineMenuPosition({ overlayElement: AutofillOverlayElement.Button }, sender).catch( - (error) => this.logService.error(error), - ); - - const mostRecentlyFocusedFieldHasValue = await BrowserApi.tabSendMessage( - sender.tab, - { command: "checkMostRecentlyFocusedFieldHasValue" }, - { frameId: this.focusedFieldData?.frameId }, + this.updateInlineMenuPosition(sender, AutofillOverlayElement.Button).catch((error) => + this.logService.error(error), ); - if ((await this.getInlineMenuVisibility()) === AutofillOverlayVisibility.OnButtonClick) { + if ( + !this.checkIsInlineMenuListVisible() && + (await this.getInlineMenuVisibility()) === AutofillOverlayVisibility.OnButtonClick + ) { return; } if ( - mostRecentlyFocusedFieldHasValue && + (await this.checkFocusedFieldHasValue(sender.tab)) && (this.checkIsInlineMenuCiphersPopulated(sender) || (await this.getAuthStatus()) !== AuthenticationStatus.Unlocked) ) { return; } - this.updateInlineMenuPosition({ overlayElement: AutofillOverlayElement.List }, sender).catch( - (error) => this.logService.error(error), + this.updateInlineMenuPosition(sender, AutofillOverlayElement.List).catch((error) => + this.logService.error(error), ); } + /** + * Indicates whether the most recently focused field contains a value. + * + * @param tab - The tab to check the focused field for + */ + private async checkFocusedFieldHasValue(tab: chrome.tabs.Tab) { + return !!(await BrowserApi.tabSendMessage( + tab, + { command: "checkMostRecentlyFocusedFieldHasValue" }, + { frameId: this.focusedFieldData?.frameId || 0 }, + )); + } + /** * Triggers autofill for the selected cipher in the inline menu list. Also places * the selected cipher at the top of the list of ciphers. @@ -936,8 +1054,8 @@ export class OverlayBackground implements OverlayBackgroundInterface { { inlineMenuCipherId, usePasskey }: OverlayPortMessage, { sender }: chrome.runtime.Port, ) { - const pageDetails = this.pageDetailsForTab[sender.tab.id]; - if (!inlineMenuCipherId || !pageDetails?.size) { + const pageDetailsForTab = this.pageDetailsForTab[sender.tab.id]; + if (!inlineMenuCipherId || !pageDetailsForTab?.size) { return; } @@ -956,10 +1074,19 @@ export class OverlayBackground implements OverlayBackgroundInterface { if (await this.autofillService.isPasswordRepromptRequired(cipher, sender.tab)) { return; } + + let pageDetails = Array.from(pageDetailsForTab.values()); + if (this.focusedFieldMatchesFillType(InlineMenuFillType.CurrentPasswordUpdate)) { + pageDetails = this.getFilteredPageDetails( + pageDetails, + this.inlineMenuFieldQualificationService.isUpdateCurrentPasswordField, + ); + } + const totpCode = await this.autofillService.doAutoFill({ tab: sender.tab, - cipher: cipher, - pageDetails: Array.from(pageDetails.values()), + cipher, + pageDetails, fillNewPassword: true, allowTotpAutofill: true, }); @@ -971,6 +1098,30 @@ export class OverlayBackground implements OverlayBackgroundInterface { this.updateLastUsedInlineMenuCipher(inlineMenuCipherId, cipher); } + /** + * Filters the passed page details in order to selectively fill elements based + * on the provided callback. + * + * @param pageDetails - The page details to filter + * @param fieldsFilter - The callback to filter the fields + */ + private getFilteredPageDetails( + pageDetails: PageDetail[], + fieldsFilter: (field: AutofillField) => boolean, + ): PageDetail[] { + let filteredPageDetails: PageDetail[] = structuredClone(pageDetails); + if (!filteredPageDetails?.length) { + return []; + } + + filteredPageDetails = filteredPageDetails.map((pageDetail) => { + pageDetail.details.fields = pageDetail.details.fields.filter(fieldsFilter); + return pageDetail; + }); + + return filteredPageDetails; + } + /** * Triggers a FIDO2 authentication from the inline menu using the passed credential ID. * @@ -1040,21 +1191,32 @@ export class OverlayBackground implements OverlayBackgroundInterface { return; } - this.checkInlineMenuButtonFocused(); + this.checkInlineMenuButtonFocused(sender); } /** * Posts a message to the inline menu button iframe to check if it is focused. + * + * @param sender - The sender of the port message */ - private checkInlineMenuButtonFocused() { - this.inlineMenuButtonPort?.postMessage({ command: "checkAutofillInlineMenuButtonFocused" }); + private checkInlineMenuButtonFocused(sender: chrome.runtime.MessageSender) { + if (!this.inlineMenuButtonPort) { + this.closeInlineMenu(sender, { forceCloseInlineMenu: true }); + return; + } + + this.postMessageToPort(this.inlineMenuButtonPort, { + command: "checkAutofillInlineMenuButtonFocused", + }); } /** * Posts a message to the inline menu list iframe to check if it is focused. */ private checkInlineMenuListFocused() { - this.inlineMenuListPort?.postMessage({ command: "checkAutofillInlineMenuListFocused" }); + this.postMessageToPort(this.inlineMenuListPort, { + command: "checkAutofillInlineMenuListFocused", + }); } /** @@ -1070,12 +1232,15 @@ export class OverlayBackground implements OverlayBackgroundInterface { ) { const command = "closeAutofillInlineMenu"; const sendOptions = { frameId: 0 }; + const updateVisibilityDefaults = { overlayElement, isVisible: false, forceUpdate: true }; + this.generatedPassword = null; + if (forceCloseInlineMenu) { BrowserApi.tabSendMessage(sender.tab, { command, overlayElement }, sendOptions).catch( (error) => this.logService.error(error), ); - this.isInlineMenuButtonVisible = false; - this.isInlineMenuListVisible = false; + this.updateInlineMenuElementIsVisibleStatus(updateVisibilityDefaults, sender); + return; } @@ -1089,26 +1254,17 @@ export class OverlayBackground implements OverlayBackgroundInterface { { command, overlayElement: AutofillOverlayElement.List }, sendOptions, ).catch((error) => this.logService.error(error)); - this.isInlineMenuListVisible = false; + this.updateInlineMenuElementIsVisibleStatus( + Object.assign(updateVisibilityDefaults, { overlayElement: AutofillOverlayElement.List }), + sender, + ); return; } - if (overlayElement === AutofillOverlayElement.Button) { - this.isInlineMenuButtonVisible = false; - } - - if (overlayElement === AutofillOverlayElement.List) { - this.isInlineMenuListVisible = false; - } - - if (!overlayElement) { - this.isInlineMenuButtonVisible = false; - this.isInlineMenuListVisible = false; - } - BrowserApi.tabSendMessage(sender.tab, { command, overlayElement }, sendOptions).catch((error) => this.logService.error(error), ); + this.updateInlineMenuElementIsVisibleStatus(updateVisibilityDefaults, sender); } /** @@ -1116,27 +1272,14 @@ export class OverlayBackground implements OverlayBackgroundInterface { * This is used to ensure that we capture click events on the inline menu in the case * that some on page programmatic method attempts to force focus redirection. */ - private triggerDelayedInlineMenuClosure() { - if (this.isFieldCurrentlyFocused) { + private async triggerDelayedInlineMenuClosure(cancelDelayedClose: boolean = false) { + if (cancelDelayedClose || this.isFieldCurrentlyFocused) { return; } - this.clearDelayedInlineMenuClosure(); - this.delayedCloseTimeout = globalThis.setTimeout(() => { - const message = { command: "triggerDelayedAutofillInlineMenuClosure" }; - this.inlineMenuButtonPort?.postMessage(message); - this.inlineMenuListPort?.postMessage(message); - }, 100); - } - - /** - * Clears the delayed closure timeout for the inline menu, effectively - * cancelling the event from occurring. - */ - private clearDelayedInlineMenuClosure() { - if (this.delayedCloseTimeout) { - clearTimeout(this.delayedCloseTimeout); - } + const message = { command: "triggerDelayedAutofillInlineMenuClosure" }; + this.postMessageToPort(this.inlineMenuButtonPort, message); + this.postMessageToPort(this.inlineMenuListPort, message); } /** @@ -1160,6 +1303,8 @@ export class OverlayBackground implements OverlayBackgroundInterface { if (overlayElement === AutofillOverlayElement.Button) { this.inlineMenuButtonPort?.disconnect(); this.inlineMenuButtonPort = null; + this.inlineMenuButtonMessageConnectorPort?.disconnect(); + this.inlineMenuButtonMessageConnectorPort = null; this.isInlineMenuButtonVisible = false; return; @@ -1167,6 +1312,8 @@ export class OverlayBackground implements OverlayBackgroundInterface { this.inlineMenuListPort?.disconnect(); this.inlineMenuListPort = null; + this.inlineMenuListMessageConnectorPort?.disconnect(); + this.inlineMenuListMessageConnectorPort = null; this.isInlineMenuListVisible = false; } @@ -1174,12 +1321,12 @@ export class OverlayBackground implements OverlayBackgroundInterface { * Updates the position of either the inline menu list or button. The position * is based on the focused field's position and dimensions. * - * @param overlayElement - The overlay element to update, either the list or button * @param sender - The sender of the port message + * @param overlayElement - The overlay element to update, either the list or button */ private async updateInlineMenuPosition( - { overlayElement }: { overlayElement?: string }, sender: chrome.runtime.MessageSender, + overlayElement?: string, ) { if (!overlayElement || !this.senderTabHasFocusedField(sender)) { return; @@ -1193,32 +1340,32 @@ export class OverlayBackground implements OverlayBackgroundInterface { { frameId: 0 }, ); - const subFrameOffsetsForTab = this.subFrameOffsetsForTab[this.focusedFieldData.tabId]; + const subFrameOffsetsForTab = this.subFrameOffsetsForTab[this.focusedFieldData?.tabId]; let subFrameOffsets: SubFrameOffsetData; if (subFrameOffsetsForTab) { subFrameOffsets = subFrameOffsetsForTab.get(this.focusedFieldData.frameId); if (subFrameOffsets === null) { - this.rebuildSubFrameOffsetsSubject.next(sender); - this.startUpdateInlineMenuPositionSubject.next(sender); + this.rebuildSubFrameOffsets$.next(sender); + this.startUpdateInlineMenuPosition$.next(sender); return; } } if (overlayElement === AutofillOverlayElement.Button) { - this.inlineMenuButtonPort?.postMessage({ + this.postMessageToPort(this.inlineMenuButtonPort, { command: "updateAutofillInlineMenuPosition", styles: this.getInlineMenuButtonPosition(subFrameOffsets), }); - this.startInlineMenuFadeIn(); + this.startInlineMenuFadeIn$.next(); return; } - this.inlineMenuListPort?.postMessage({ + this.postMessageToPort(this.inlineMenuListPort, { command: "updateAutofillInlineMenuPosition", styles: this.getInlineMenuListPosition(subFrameOffsets), }); - this.startInlineMenuFadeIn(); + this.startInlineMenuFadeIn$.next(); } /** @@ -1229,20 +1376,18 @@ export class OverlayBackground implements OverlayBackgroundInterface { * @param sender - The sender of the port message */ private updateInlineMenuElementIsVisibleStatus( - message: OverlayBackgroundExtensionMessage, + { overlayElement, isVisible, forceUpdate }: UpdateInlineMenuVisibilityMessage, sender: chrome.runtime.MessageSender, ) { - if (!this.senderTabHasFocusedField(sender)) { + if (!forceUpdate && !this.senderTabHasFocusedField(sender)) { return; } - const { overlayElement, isVisible } = message; - if (overlayElement === AutofillOverlayElement.Button) { + if (!overlayElement || overlayElement === AutofillOverlayElement.Button) { this.isInlineMenuButtonVisible = isVisible; - return; } - if (overlayElement === AutofillOverlayElement.List) { + if (!overlayElement || overlayElement === AutofillOverlayElement.List) { this.isInlineMenuListVisible = isVisible; } } @@ -1254,22 +1399,6 @@ export class OverlayBackground implements OverlayBackgroundInterface { return this.inlineMenuPosition; } - /** - * Handles updating the opacity of both the inline menu button and list. - * This is used to simultaneously fade in the inline menu elements. - */ - private startInlineMenuFadeIn() { - this.cancelInlineMenuFadeIn(); - this.startInlineMenuFadeInSubject.next(); - } - - /** - * Clears the timeout used to fade in the inline menu elements. - */ - private cancelInlineMenuFadeIn() { - this.cancelInlineMenuFadeInSubject.next(true); - } - /** * Posts a message to the inline menu elements to trigger a fade in of the inline menu. * @@ -1281,8 +1410,8 @@ export class OverlayBackground implements OverlayBackgroundInterface { } const message = { command: "fadeInAutofillInlineMenuIframe" }; - this.inlineMenuButtonPort?.postMessage(message); - this.inlineMenuListPort?.postMessage(message); + this.postMessageToPort(this.inlineMenuButtonPort, message); + this.postMessageToPort(this.inlineMenuListPort, message); } /** @@ -1359,7 +1488,11 @@ export class OverlayBackground implements OverlayBackgroundInterface { { focusedFieldData }: OverlayBackgroundExtensionMessage, sender: chrome.runtime.MessageSender, ) { - if (this.focusedFieldData && !this.senderFrameHasFocusedField(sender)) { + if ( + this.focusedFieldData && + this.senderTabHasFocusedField(sender) && + !this.senderFrameHasFocusedField(sender) + ) { BrowserApi.tabSendMessage( sender.tab, { command: "unsetMostRecentlyFocusedField" }, @@ -1371,31 +1504,76 @@ export class OverlayBackground implements OverlayBackgroundInterface { this.focusedFieldData = { ...focusedFieldData, tabId: sender.tab.id, frameId: sender.frameId }; this.isFieldCurrentlyFocused = true; - const accountCreationFieldBlurred = - previousFocusedFieldData?.showInlineMenuAccountCreation && - !this.focusedFieldData.showInlineMenuAccountCreation; - - if (accountCreationFieldBlurred || this.showInlineMenuAccountCreation()) { - this.updateIdentityCiphersOnLoginField(previousFocusedFieldData).catch((error) => + if (this.shouldUpdatePasswordGeneratorMenuOnFieldFocus()) { + this.updateInlineMenuGeneratedPasswordOnFocus(sender.tab).catch((error) => this.logService.error(error), ); return; } - if (previousFocusedFieldData?.filledByCipherType !== focusedFieldData?.filledByCipherType) { - const updateAllCipherTypes = focusedFieldData.filledByCipherType !== CipherType.Login; + if (this.shouldUpdateAccountCreationMenuOnFieldFocus(previousFocusedFieldData)) { + this.updateInlineMenuAccountCreationDataOnFocus(previousFocusedFieldData, sender).catch( + (error) => this.logService.error(error), + ); + return; + } + + if ( + !this.focusedFieldMatchesFillType( + focusedFieldData?.inlineMenuFillType, + previousFocusedFieldData, + ) + ) { + const updateAllCipherTypes = !this.focusedFieldMatchesFillType( + CipherType.Login, + focusedFieldData, + ); this.updateOverlayCiphers(updateAllCipherTypes).catch((error) => this.logService.error(error), ); } } + /** + * Identifies if a recently focused field should update as a password generation field. + */ + private shouldUpdatePasswordGeneratorMenuOnFieldFocus() { + return ( + this.isInlineMenuButtonVisible && + this.focusedFieldMatchesFillType(InlineMenuFillType.PasswordGeneration) + ); + } + + /** + * Handles updating the inline menu password generator on focus of a field. + * In the case that the field has a value, will show the save login view. + * + * @param tab - The tab that the field is focused within + */ + private async updateInlineMenuGeneratedPasswordOnFocus(tab: chrome.tabs.Tab) { + if (await this.shouldShowSaveLoginInlineMenuList(tab)) { + this.showSaveLoginInlineMenuList(); + return; + } + + await this.updateGeneratedPassword(); + } + /** * Triggers an update of populated identity ciphers when a login field is focused. * * @param previousFocusedFieldData - The data set of the previously focused field + * @param sender - The sender of the extension message */ - private async updateIdentityCiphersOnLoginField(previousFocusedFieldData: FocusedFieldData) { + private async updateInlineMenuAccountCreationDataOnFocus( + previousFocusedFieldData: FocusedFieldData, + sender: chrome.runtime.MessageSender, + ) { + if (await this.shouldShowSaveLoginInlineMenuList(sender.tab)) { + this.showSaveLoginInlineMenuList(); + return; + } + if ( !previousFocusedFieldData || !this.isInlineMenuButtonVisible || @@ -1404,14 +1582,158 @@ export class OverlayBackground implements OverlayBackgroundInterface { return; } - this.inlineMenuListPort?.postMessage({ - command: "updateAutofillInlineMenuListCiphers", - ciphers: await this.getInlineMenuCipherData(), - showInlineMenuAccountCreation: this.showInlineMenuAccountCreation(), - showPasskeysLabels: this.showPasskeysLabelsWithinInlineMenu, + if ( + this.focusedFieldMatchesFillType(CipherType.Login) && + this.focusedFieldMatchesAccountCreationType(InlineMenuAccountCreationFieldType.Password) + ) { + await this.updateGeneratedPassword(); + return; + } + + await this.updateInlineMenuListCiphers(sender.tab); + } + + /** + * Identifies whether a newly focused field should trigger an update that + * displays the account creation view within the inline menu. + * + * @param previousFocusedFieldData - The data set of the previously focused field + */ + private shouldUpdateAccountCreationMenuOnFieldFocus(previousFocusedFieldData: FocusedFieldData) { + const accountCreationFieldBlurred = + this.focusedFieldMatchesFillType( + InlineMenuFillType.AccountCreationUsername, + previousFocusedFieldData, + ) && !this.focusedFieldMatchesFillType(InlineMenuFillType.AccountCreationUsername); + return accountCreationFieldBlurred || this.shouldShowInlineMenuAccountCreation(); + } + + /** + * Sends a message to the list to show the save login inline menu list view. This view + * is shown after a field is filled with a generated password. + */ + private showSaveLoginInlineMenuList() { + this.postMessageToPort(this.inlineMenuListPort, { command: "showSaveLoginInlineMenuList" }); + } + + /** + * Generates a password based on the user defined password generation options. + */ + private async generatePassword(): Promise { + this.generatedPassword = await this.generatePasswordCallback(); + await this.addPasswordCallback(this.generatedPassword); + } + + /** + * Updates the generated password in the inline menu list. + * + * @param refreshPassword - Identifies whether the generated password should be refreshed + */ + private async updateGeneratedPassword(refreshPassword: boolean = false) { + if (!this.generatedPassword || refreshPassword) { + await this.generatePassword(); + } + + this.postMessageToPort(this.inlineMenuListPort, { + command: "updateAutofillInlineMenuGeneratedPassword", + generatedPassword: this.generatedPassword, + refreshPassword, }); } + /** + * Triggers a fill of the generated password into the current tab. Will trigger + * a focus of the last focused field after filling the password. + * + * @param port - The port of the sender + */ + private async fillGeneratedPassword(port: chrome.runtime.Port) { + if (!this.generatedPassword) { + return; + } + + const pageDetailsForTab = this.pageDetailsForTab[port.sender.tab.id]; + if (!pageDetailsForTab) { + return; + } + + let pageDetails: PageDetail[] = Array.from(pageDetailsForTab.values()); + if (!pageDetails.length) { + return; + } + + // If our currently focused field is for a login form, we want to fill the current password field. + // Otherwise, map over all page details and filter out fields that are not new password fields. + if (!this.focusedFieldMatchesFillType(CipherType.Login)) { + pageDetails = this.getFilteredPageDetails( + pageDetails, + this.inlineMenuFieldQualificationService.isNewPasswordField, + ); + } + + const cipher = this.buildLoginCipherView({ + username: "", + password: this.generatedPassword, + hostname: "", + uri: "", + }); + + await this.autofillService.doAutoFill({ + tab: port.sender.tab, + cipher, + pageDetails, + fillNewPassword: true, + allowTotpAutofill: false, + }); + + globalThis.setTimeout(async () => { + if (await this.shouldShowSaveLoginInlineMenuList(port.sender.tab)) { + await this.openInlineMenu(port.sender, true); + } + }, 300); + } + + /** + * Verifies whether the save login inline menu view should be shown. This requires that + * the login data on the page contains a username and either a current or new password. + * + * @param tab - The tab to check for login data + */ + private async shouldShowSaveLoginInlineMenuList(tab: chrome.tabs.Tab) { + if (this.focusedFieldData?.tabId !== tab.id) { + return false; + } + + const loginData = await this.getInlineMenuFormFieldData(tab); + if (!loginData) { + return false; + } + + return ( + (this.shouldShowInlineMenuAccountCreation() || + this.focusedFieldMatchesFillType(InlineMenuFillType.PasswordGeneration)) && + !!(loginData.username && (loginData.password || loginData.newPassword)) + ); + } + + /** + * Gets the inline menu form field data from the provided tab. + * + * @param tab - The tab to get the form field data from + */ + private async getInlineMenuFormFieldData(tab: chrome.tabs.Tab): Promise { + return await BrowserApi.tabSendMessage( + tab, + { + command: "getInlineMenuFormFieldData", + ignoreFieldFocus: true, + }, + { + frameId: this.focusedFieldData.frameId || 0, + }, + ); + } + /** * Updates the inline menu's visibility based on the display property passed in the extension message. * @@ -1426,7 +1748,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { return; } - this.cancelInlineMenuFadeIn(); + this.cancelInlineMenuFadeIn$.next(true); const display = isInlineMenuHidden ? "none" : "block"; let styles: { display: string; opacity?: string } = { display }; @@ -1437,45 +1759,94 @@ export class OverlayBackground implements OverlayBackgroundInterface { const portMessage = { command: "toggleAutofillInlineMenuHidden", styles }; if (this.inlineMenuButtonPort) { - this.isInlineMenuButtonVisible = !isInlineMenuHidden; - this.inlineMenuButtonPort.postMessage(portMessage); + this.updateInlineMenuElementIsVisibleStatus( + { overlayElement: AutofillOverlayElement.Button, isVisible: !isInlineMenuHidden }, + sender, + ); + this.postMessageToPort(this.inlineMenuButtonPort, portMessage); } if (this.inlineMenuListPort) { this.isInlineMenuListVisible = !isInlineMenuHidden; - this.inlineMenuListPort.postMessage(portMessage); + this.updateInlineMenuElementIsVisibleStatus( + { overlayElement: AutofillOverlayElement.List, isVisible: !isInlineMenuHidden }, + sender, + ); + this.postMessageToPort(this.inlineMenuListPort, portMessage); } if (setTransparentInlineMenu) { - this.startInlineMenuFadeIn(); + this.startInlineMenuFadeIn$.next(); } } /** * Sends a message to the currently active tab to open the autofill inline menu. * - * @param isFocusingFieldElement - Identifies whether the field element should be focused when the inline menu is opened + * @param sender - The sender of the port message * @param isOpeningFullInlineMenu - Identifies whether the full inline menu should be forced open regardless of other states */ - private async openInlineMenu(isFocusingFieldElement = false, isOpeningFullInlineMenu = false) { - this.clearDelayedInlineMenuClosure(); - const currentTab = await BrowserApi.getTabFromCurrentWindowId(); - if (!currentTab) { + private async openInlineMenu( + sender: chrome.runtime.MessageSender, + isOpeningFullInlineMenu = false, + ) { + this.cancelInlineMenuDelayedClose$.next(true); + + if (isOpeningFullInlineMenu) { + await this.updateInlineMenuPosition(sender, AutofillOverlayElement.Button); + await this.updateInlineMenuPosition(sender, AutofillOverlayElement.List); return; } - await BrowserApi.tabSendMessage( - currentTab, - { - command: "openAutofillInlineMenu", - isFocusingFieldElement, - isOpeningFullInlineMenu, - authStatus: await this.getAuthStatus(), - }, - { - frameId: this.focusedFieldData?.tabId === currentTab.id ? this.focusedFieldData.frameId : 0, - }, - ); + if (!(await this.checkFocusedFieldHasValue(sender.tab))) { + await this.openInlineMenuOnEmptyField(sender); + return; + } + + await this.openInlineMenuOnFilledField(sender); + } + + /** + * Triggers logic that handles opening the inline menu on an empty form field. + * + * @param sender - The sender of the port message + */ + private async openInlineMenuOnEmptyField(sender: chrome.runtime.MessageSender) { + if ((await this.getInlineMenuVisibility()) === AutofillOverlayVisibility.OnFieldFocus) { + await this.updateInlineMenuPosition(sender, AutofillOverlayElement.Button); + await this.updateInlineMenuPosition(sender, AutofillOverlayElement.List); + + return; + } + + if (this.isInlineMenuListVisible) { + this.closeInlineMenu(sender, { + forceCloseInlineMenu: true, + overlayElement: AutofillOverlayElement.List, + }); + } + await this.updateInlineMenuPosition(sender, AutofillOverlayElement.Button); + } + + /** + * Triggers logic that handles opening the inline menu on a form field that has a value. + * + * @param sender - The sender of the port message + */ + private async openInlineMenuOnFilledField(sender: chrome.runtime.MessageSender) { + if (await this.shouldShowSaveLoginInlineMenuList(sender.tab)) { + await this.updateInlineMenuPosition(sender, AutofillOverlayElement.Button); + await this.updateInlineMenuPosition(sender, AutofillOverlayElement.List); + return; + } + + if (this.isInlineMenuListVisible) { + this.closeInlineMenu(sender, { + forceCloseInlineMenu: true, + overlayElement: AutofillOverlayElement.List, + }); + } + await this.updateInlineMenuPosition(sender, AutofillOverlayElement.Button); } /** @@ -1510,7 +1881,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { * Sends a message to the inline menu button to update its authentication status. */ private async updateInlineMenuButtonAuthStatus() { - this.inlineMenuButtonPort?.postMessage({ + this.postMessageToPort(this.inlineMenuButtonPort, { command: "updateInlineMenuButtonAuthStatus", authStatus: await this.getAuthStatus(), }); @@ -1524,7 +1895,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { * @param port - The port of the inline menu button */ private async handleInlineMenuButtonClicked(port: chrome.runtime.Port) { - this.clearDelayedInlineMenuClosure(); + this.cancelInlineMenuDelayedClose$.next(true); this.cancelInlineMenuFadeInAndPositionUpdate(); if ((await this.getAuthStatus()) !== AuthenticationStatus.Unlocked) { @@ -1532,7 +1903,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { return; } - await this.openInlineMenu(false, true); + await this.openInlineMenu(port.sender, true); } /** @@ -1543,7 +1914,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { private async unlockVault(port: chrome.runtime.Port) { const { sender } = port; - this.closeInlineMenu(port.sender); + this.closeInlineMenu(port.sender, { forceCloseInlineMenu: true }); const retryMessage: LockedVaultPendingNotificationsData = { commandToRetry: { message: { command: "openAutofillInlineMenu" }, sender }, target: "overlay.background", @@ -1582,7 +1953,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { * Facilitates redirecting focus to the inline menu list. */ private focusInlineMenuList() { - this.inlineMenuListPort?.postMessage({ command: "focusAutofillInlineMenuList" }); + this.postMessageToPort(this.inlineMenuListPort, { command: "focusAutofillInlineMenuList" }); } /** @@ -1593,11 +1964,10 @@ export class OverlayBackground implements OverlayBackgroundInterface { */ private async unlockCompleted(message: OverlayBackgroundExtensionMessage) { await this.updateInlineMenuButtonAuthStatus(); - await this.updateOverlayCiphers(); - if (message.data?.commandToRetry?.message?.command === "openAutofillInlineMenu") { - await this.openInlineMenu(true); - } + const openInlineMenu = + message.data?.commandToRetry?.message?.command === "openAutofillInlineMenu"; + await this.updateOverlayCiphers(true, openInlineMenu); } /** @@ -1605,33 +1975,45 @@ export class OverlayBackground implements OverlayBackgroundInterface { */ private getInlineMenuTranslations() { if (!this.inlineMenuPageTranslations) { - this.inlineMenuPageTranslations = { - locale: BrowserApi.getUILanguage(), - opensInANewWindow: this.i18nService.translate("opensInANewWindow"), - buttonPageTitle: this.i18nService.translate("bitwardenOverlayButton"), - toggleBitwardenVaultOverlay: this.i18nService.translate("toggleBitwardenVaultOverlay"), - listPageTitle: this.i18nService.translate("bitwardenVault"), - unlockYourAccount: this.i18nService.translate("unlockYourAccountToViewAutofillSuggestions"), - unlockAccount: this.i18nService.translate("unlockAccount"), - unlockAccountAria: this.i18nService.translate("unlockAccountAria"), - fillCredentialsFor: this.i18nService.translate("fillCredentialsFor"), - username: this.i18nService.translate("username")?.toLowerCase(), - view: this.i18nService.translate("view"), - noItemsToShow: this.i18nService.translate("noItemsToShow"), - newItem: this.i18nService.translate("newItem"), - addNewVaultItem: this.i18nService.translate("addNewVaultItem"), - newLogin: this.i18nService.translate("newLogin"), - addNewLoginItem: this.i18nService.translate("addNewLoginItemAria"), - newCard: this.i18nService.translate("newCard"), - addNewCardItem: this.i18nService.translate("addNewCardItemAria"), - newIdentity: this.i18nService.translate("newIdentity"), - addNewIdentityItem: this.i18nService.translate("addNewIdentityItemAria"), - cardNumberEndsWith: this.i18nService.translate("cardNumberEndsWith"), - passkeys: this.i18nService.translate("passkeys"), - passwords: this.i18nService.translate("passwords"), - logInWithPasskey: this.i18nService.translate("logInWithPasskeyAriaLabel"), - authenticating: this.i18nService.translate("authenticating"), - }; + const translationKeys = [ + "opensInANewWindow", + "toggleBitwardenVaultOverlay", + "unlockYourAccountToViewAutofillSuggestions", + "unlockAccount", + "unlockAccountAria", + "fillCredentialsFor", + "username", + "view", + "noItemsToShow", + "newItem", + "addNewVaultItem", + "newLogin", + "addNewLoginItemAria", + "newCard", + "addNewCardItemAria", + "newIdentity", + "addNewIdentityItemAria", + "cardNumberEndsWith", + "passkeys", + "passwords", + "logInWithPasskeyAriaLabel", + "authenticating", + "fillGeneratedPassword", + "regeneratePassword", + "passwordRegenerated", + "saveLoginToBitwarden", + "lowercaseAriaLabel", + "uppercaseAriaLabel", + "generatedPassword", + ...Object.values(specialCharacterToKeyMap), + ]; + this.inlineMenuPageTranslations = translationKeys.reduce( + (acc: Record, key) => { + acc[key] = this.i18nService.translate(key); + return acc; + }, + {}, + ); } return this.inlineMenuPageTranslations; @@ -1714,7 +2096,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { this.updateCurrentAddNewItemIdentity(identity); } - this.addNewVaultItemSubject.next(this.currentAddNewItemData); + this.addNewVaultItem$.next(this.currentAddNewItemData); } /** @@ -2101,7 +2483,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { * the same value as the page's meta "color-scheme" value. */ private updateInlineMenuButtonColorScheme() { - this.inlineMenuButtonPort?.postMessage({ + this.postMessageToPort(this.inlineMenuButtonPort, { command: "updateAutofillInlineMenuColorScheme", }); } @@ -2117,7 +2499,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { this.inlineMenuPosition.list.height = parsedHeight; } - this.inlineMenuListPort?.postMessage({ + this.postMessageToPort(this.inlineMenuListPort, { command: "updateAutofillInlineMenuPosition", styles: message.styles, }); @@ -2165,7 +2547,12 @@ export class OverlayBackground implements OverlayBackgroundInterface { * @param sender - The sender of the message */ private senderFrameHasFocusedField(sender: chrome.runtime.MessageSender) { - return sender.frameId === this.focusedFieldData?.frameId; + if (!this.focusedFieldData) { + return false; + } + + const { tabId, frameId } = this.focusedFieldData; + return sender.tab.id === tabId && sender.frameId === frameId; } /** @@ -2184,7 +2571,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { this.toggleInlineMenuHidden({ isInlineMenuHidden: true }, sender).catch((error) => this.logService.error(error), ); - this.repositionInlineMenuSubject.next(sender); + this.repositionInlineMenu$.next(sender); } /** @@ -2195,7 +2582,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { * @param sender */ private resetFocusedFieldSubFrameOffsets(sender: chrome.runtime.MessageSender) { - if (this.focusedFieldData.frameId > 0 && this.subFrameOffsetsForTab[sender.tab.id]) { + if (this.focusedFieldData?.frameId > 0 && this.subFrameOffsetsForTab[sender.tab.id]) { this.subFrameOffsetsForTab[sender.tab.id].set(this.focusedFieldData.frameId, null); } } @@ -2208,8 +2595,23 @@ export class OverlayBackground implements OverlayBackgroundInterface { */ private async triggerSubFrameFocusInRebuild(sender: chrome.runtime.MessageSender) { this.cancelInlineMenuFadeInAndPositionUpdate(); - this.rebuildSubFrameOffsetsSubject.next(sender); - this.repositionInlineMenuSubject.next(sender); + this.resetFocusedFieldSubFrameOffsets(sender); + this.rebuildSubFrameOffsets$.next(sender); + this.repositionInlineMenu$.next(sender); + } + + /** + * Triggers on scroll of a frame within the tab. Will reposition the inline menu + * if the focused field is within a sub-frame and the inline menu is visible. + * + * @param sender - The sender of the message + */ + private shouldRepositionSubFrameInlineMenuOnScroll(sender: chrome.runtime.MessageSender) { + if (!this.isInlineMenuButtonVisible || sender.tab.id !== this.focusedFieldData?.tabId) { + return false; + } + + return this.focusedFieldData.frameId > 0; } /** @@ -2228,25 +2630,25 @@ export class OverlayBackground implements OverlayBackgroundInterface { const isFieldWithinViewport = await BrowserApi.tabSendMessage( sender.tab, { command: "checkIsMostRecentlyFocusedFieldWithinViewport" }, - { frameId: this.focusedFieldData.frameId }, + { frameId: this.focusedFieldData?.frameId }, ); if (!isFieldWithinViewport) { await this.closeInlineMenuAfterReposition(sender); return; } - if (this.focusedFieldData.frameId > 0) { - this.rebuildSubFrameOffsetsSubject.next(sender); + if (this.focusedFieldData?.frameId > 0) { + this.rebuildSubFrameOffsets$.next(sender); } - this.startUpdateInlineMenuPositionSubject.next(sender); + this.startUpdateInlineMenuPosition$.next(sender); }; /** * Triggers a closure of the inline menu during a reposition event. * * @param sender - The sender of the message -| */ + */ private async closeInlineMenuAfterReposition(sender: chrome.runtime.MessageSender) { await this.toggleInlineMenuHidden( { isInlineMenuHidden: false, setTransparentInlineMenu: true }, @@ -2259,8 +2661,8 @@ export class OverlayBackground implements OverlayBackgroundInterface { * Cancels the observables that update the position and fade in of the inline menu. */ private cancelInlineMenuFadeInAndPositionUpdate() { - this.cancelInlineMenuFadeIn(); - this.cancelUpdateInlineMenuPositionSubject.next(); + this.cancelInlineMenuFadeIn$.next(true); + this.cancelUpdateInlineMenuPosition$.next(); } /** @@ -2330,14 +2732,13 @@ export class OverlayBackground implements OverlayBackgroundInterface { * @param port - The port that connected to the extension background */ private handlePortOnConnect = async (port: chrome.runtime.Port) => { - const isInlineMenuListMessageConnector = port.name === AutofillOverlayPort.ListMessageConnector; - const isInlineMenuButtonMessageConnector = - port.name === AutofillOverlayPort.ButtonMessageConnector; - if (isInlineMenuListMessageConnector || isInlineMenuButtonMessageConnector) { - port.onMessage.addListener(this.handleOverlayElementPortMessage); + if (!this.validPortConnections.has(port.name)) { return; } + this.storeOverlayPort(port); + port.onMessage.addListener(this.handleOverlayElementPortMessage); + const isInlineMenuListPort = port.name === AutofillOverlayPort.List; const isInlineMenuButtonPort = port.name === AutofillOverlayPort.Button; if (!isInlineMenuListPort && !isInlineMenuButtonPort) { @@ -2348,10 +2749,20 @@ export class OverlayBackground implements OverlayBackgroundInterface { this.portKeyForTab[port.sender.tab.id] = generateRandomChars(12); } - this.storeOverlayPort(port); port.onDisconnect.addListener(this.handlePortOnDisconnect); - port.onMessage.addListener(this.handleOverlayElementPortMessage); - port.postMessage({ + + const authStatus = await this.getAuthStatus(); + const showInlineMenuAccountCreation = this.shouldShowInlineMenuAccountCreation(); + const showInlineMenuPasswordGenerator = await this.shouldInitInlineMenuPasswordGenerator( + authStatus, + isInlineMenuListPort, + showInlineMenuAccountCreation, + ); + const showSaveLoginMenu = + (await this.checkFocusedFieldHasValue(port.sender.tab)) && + (await this.shouldShowSaveLoginInlineMenuList(port.sender.tab)); + + this.postMessageToPort(port, { command: `initAutofillInlineMenu${isInlineMenuListPort ? "List" : "Button"}`, iframeUrl: chrome.runtime.getURL( `overlay/menu-${isInlineMenuListPort ? "list" : "button"}.html`, @@ -2359,7 +2770,6 @@ export class OverlayBackground implements OverlayBackgroundInterface { pageTitle: chrome.i18n.getMessage( isInlineMenuListPort ? "bitwardenVault" : "bitwardenOverlayButton", ), - authStatus: await this.getAuthStatus(), styleSheetUrl: chrome.runtime.getURL( `overlay/menu-${isInlineMenuListPort ? "list" : "button"}.css`, ), @@ -2370,25 +2780,42 @@ export class OverlayBackground implements OverlayBackgroundInterface { portName: isInlineMenuListPort ? AutofillOverlayPort.ListMessageConnector : AutofillOverlayPort.ButtonMessageConnector, - filledByCipherType: this.focusedFieldData?.filledByCipherType, - showInlineMenuAccountCreation: this.showInlineMenuAccountCreation(), + inlineMenuFillType: this.focusedFieldData?.inlineMenuFillType, showPasskeysLabels: this.showPasskeysLabelsWithinInlineMenu, + generatedPassword: showInlineMenuPasswordGenerator ? this.generatedPassword : null, + showSaveLoginMenu, + showInlineMenuAccountCreation, + authStatus, }); this.updateInlineMenuPosition( - { - overlayElement: isInlineMenuListPort - ? AutofillOverlayElement.List - : AutofillOverlayElement.Button, - }, port.sender, + isInlineMenuListPort ? AutofillOverlayElement.List : AutofillOverlayElement.Button, ).catch((error) => this.logService.error(error)); }; + /** + * Wraps the port.postMessage method to handle any errors that may occur. + * + * @param port - The port to send the message to + * @param message - The message to send to the port + */ + private postMessageToPort = (port: chrome.runtime.Port, message: Record) => { + if (!port) { + return; + } + + try { + port.postMessage(message); + } catch { + // Catch when the port.postMessage call triggers an error to ensure login execution continues. + } + }; + /** * Stores the connected overlay port and sets up any existing ports to be disconnected. * * @param port - The port to store -| */ + */ private storeOverlayPort(port: chrome.runtime.Port) { if (port.name === AutofillOverlayPort.List) { this.storeExpiredOverlayPort(this.inlineMenuListPort); @@ -2399,6 +2826,19 @@ export class OverlayBackground implements OverlayBackgroundInterface { if (port.name === AutofillOverlayPort.Button) { this.storeExpiredOverlayPort(this.inlineMenuButtonPort); this.inlineMenuButtonPort = port; + return; + } + + if (port.name === AutofillOverlayPort.ButtonMessageConnector) { + this.storeExpiredOverlayPort(this.inlineMenuButtonMessageConnectorPort); + this.inlineMenuButtonMessageConnectorPort = port; + return; + } + + if (port.name === AutofillOverlayPort.ListMessageConnector) { + this.storeExpiredOverlayPort(this.inlineMenuListMessageConnectorPort); + this.inlineMenuListMessageConnectorPort = port; + return; } } @@ -2415,6 +2855,38 @@ export class OverlayBackground implements OverlayBackgroundInterface { } } + /** + * Identifies if the focused field should show the inline menu + * password generator when the inline menu is opened. + * + * @param authStatus - The current authentication status + * @param isInlineMenuListPort - Identifies if the port is for the inline menu list + * @param showInlineMenuAccountCreation - Identifies if the inline menu account creation should be shown + */ + private async shouldInitInlineMenuPasswordGenerator( + authStatus: AuthenticationStatus, + isInlineMenuListPort: boolean, + showInlineMenuAccountCreation: boolean, + ) { + if (!isInlineMenuListPort || authStatus !== AuthenticationStatus.Unlocked) { + return false; + } + + const focusFieldShouldShowPasswordGenerator = + this.focusedFieldMatchesFillType(InlineMenuFillType.PasswordGeneration) || + (showInlineMenuAccountCreation && + this.focusedFieldMatchesAccountCreationType(InlineMenuAccountCreationFieldType.Password)); + if (!focusFieldShouldShowPasswordGenerator) { + return false; + } + + if (!this.generatedPassword) { + await this.generatePassword(); + } + + return true; + } + /** * Handles messages sent to the overlay list or button ports. * @@ -2455,15 +2927,27 @@ export class OverlayBackground implements OverlayBackgroundInterface { * @param port - The port that was disconnected */ private handlePortOnDisconnect = (port: chrome.runtime.Port) => { + const updateVisibilityDefaults = { isVisible: false, forceUpdate: true }; + if (port.name === AutofillOverlayPort.List) { this.inlineMenuListPort = null; - this.isInlineMenuListVisible = false; + this.inlineMenuListMessageConnectorPort?.disconnect(); + this.inlineMenuListMessageConnectorPort = null; + this.updateInlineMenuElementIsVisibleStatus( + Object.assign(updateVisibilityDefaults, { overlayElement: AutofillOverlayElement.List }), + port.sender, + ); this.inlineMenuPosition.list = null; } if (port.name === AutofillOverlayPort.Button) { this.inlineMenuButtonPort = null; - this.isInlineMenuButtonVisible = false; + this.inlineMenuButtonMessageConnectorPort?.disconnect(); + this.inlineMenuButtonMessageConnectorPort = null; + this.updateInlineMenuElementIsVisibleStatus( + Object.assign(updateVisibilityDefaults, { overlayElement: AutofillOverlayElement.List }), + port.sender, + ); this.inlineMenuPosition.button = null; } }; diff --git a/apps/browser/src/autofill/background/tabs.background.ts b/apps/browser/src/autofill/background/tabs.background.ts index 0513220c277..ae57bd51cea 100644 --- a/apps/browser/src/autofill/background/tabs.background.ts +++ b/apps/browser/src/autofill/background/tabs.background.ts @@ -92,7 +92,7 @@ export default class TabsBackground { FeatureFlag.InlineMenuPositioningImprovements, ); const removePageDetailsStatus = new Set(["loading", "unloaded"]); - if (!!overlayImprovementsFlag && removePageDetailsStatus.has(changeInfo.status)) { + if (!overlayImprovementsFlag && removePageDetailsStatus.has(changeInfo.status)) { this.overlayBackground.removePageDetails(tabId); } diff --git a/apps/browser/src/autofill/content/abstractions/autofill-init.ts b/apps/browser/src/autofill/content/abstractions/autofill-init.ts index 529607949db..8a9c97e67dd 100644 --- a/apps/browser/src/autofill/content/abstractions/autofill-init.ts +++ b/apps/browser/src/autofill/content/abstractions/autofill-init.ts @@ -1,5 +1,4 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; import { CipherType } from "@bitwarden/common/vault/enums"; import { AutofillOverlayElementType } from "../../enums/autofill-overlay.enum"; @@ -21,10 +20,10 @@ export type AutofillExtensionMessage = { authStatus?: AuthenticationStatus; isOpeningFullInlineMenu?: boolean; addNewCipherType?: CipherType; + ignoreFieldFocus?: boolean; data?: { direction?: "previous" | "next" | "current"; forceCloseInlineMenu?: boolean; - newSettingValue?: InlineMenuVisibilitySetting; }; }; diff --git a/apps/browser/src/autofill/content/autofill-init.spec.ts b/apps/browser/src/autofill/content/autofill-init.spec.ts index b98d297d136..d612e63f82c 100644 --- a/apps/browser/src/autofill/content/autofill-init.spec.ts +++ b/apps/browser/src/autofill/content/autofill-init.spec.ts @@ -4,6 +4,7 @@ import AutofillPageDetails from "../models/autofill-page-details"; import AutofillScript from "../models/autofill-script"; import { AutofillInlineMenuContentService } from "../overlay/inline-menu/content/autofill-inline-menu-content.service"; import { OverlayNotificationsContentService } from "../overlay/notifications/abstractions/overlay-notifications-content.service"; +import { DomElementVisibilityService } from "../services/abstractions/dom-element-visibility.service"; import { DomQueryService } from "../services/abstractions/dom-query.service"; import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service"; import { @@ -17,6 +18,7 @@ import AutofillInit from "./autofill-init"; describe("AutofillInit", () => { let domQueryService: MockProxy; + let domElementVisibilityService: MockProxy; let overlayNotificationsContentService: MockProxy; let inlineMenuElements: MockProxy; let autofillOverlayContentService: MockProxy; @@ -32,11 +34,13 @@ describe("AutofillInit", () => { }, }); domQueryService = mock(); + domElementVisibilityService = mock(); overlayNotificationsContentService = mock(); inlineMenuElements = mock(); autofillOverlayContentService = mock(); autofillInit = new AutofillInit( domQueryService, + domElementVisibilityService, autofillOverlayContentService, inlineMenuElements, overlayNotificationsContentService, diff --git a/apps/browser/src/autofill/content/autofill-init.ts b/apps/browser/src/autofill/content/autofill-init.ts index 6c34508cb00..42933c57b1e 100644 --- a/apps/browser/src/autofill/content/autofill-init.ts +++ b/apps/browser/src/autofill/content/autofill-init.ts @@ -4,9 +4,9 @@ import AutofillPageDetails from "../models/autofill-page-details"; import { AutofillInlineMenuContentService } from "../overlay/inline-menu/abstractions/autofill-inline-menu-content.service"; import { OverlayNotificationsContentService } from "../overlay/notifications/abstractions/overlay-notifications-content.service"; import { AutofillOverlayContentService } from "../services/abstractions/autofill-overlay-content.service"; +import { DomElementVisibilityService } from "../services/abstractions/dom-element-visibility.service"; import { DomQueryService } from "../services/abstractions/dom-query.service"; import { CollectAutofillContentService } from "../services/collect-autofill-content.service"; -import DomElementVisibilityService from "../services/dom-element-visibility.service"; import InsertAutofillContentService from "../services/insert-autofill-content.service"; import { sendExtensionMessage } from "../utils"; @@ -18,7 +18,6 @@ import { class AutofillInit implements AutofillInitInterface { private readonly sendExtensionMessage = sendExtensionMessage; - private readonly domElementVisibilityService: DomElementVisibilityService; private readonly collectAutofillContentService: CollectAutofillContentService; private readonly insertAutofillContentService: InsertAutofillContentService; private collectPageDetailsOnLoadTimeout: number | NodeJS.Timeout | undefined; @@ -33,26 +32,25 @@ class AutofillInit implements AutofillInitInterface { * CollectAutofillContentService and InsertAutofillContentService classes. * * @param domQueryService - Service used to handle DOM queries. + * @param domElementVisibilityService - Used to check if an element is viewable. * @param autofillOverlayContentService - The autofill overlay content service, potentially undefined. * @param autofillInlineMenuContentService - The inline menu content service, potentially undefined. * @param overlayNotificationsContentService - The overlay notifications content service, potentially undefined. */ constructor( domQueryService: DomQueryService, + domElementVisibilityService: DomElementVisibilityService, private autofillOverlayContentService?: AutofillOverlayContentService, private autofillInlineMenuContentService?: AutofillInlineMenuContentService, private overlayNotificationsContentService?: OverlayNotificationsContentService, ) { - this.domElementVisibilityService = new DomElementVisibilityService( - this.autofillInlineMenuContentService, - ); this.collectAutofillContentService = new CollectAutofillContentService( - this.domElementVisibilityService, + domElementVisibilityService, domQueryService, this.autofillOverlayContentService, ); this.insertAutofillContentService = new InsertAutofillContentService( - this.domElementVisibilityService, + domElementVisibilityService, this.collectAutofillContentService, ); } diff --git a/apps/browser/src/autofill/content/bootstrap-autofill-overlay-menu.ts b/apps/browser/src/autofill/content/bootstrap-autofill-overlay-menu.ts index aed0f6cb940..35930647921 100644 --- a/apps/browser/src/autofill/content/bootstrap-autofill-overlay-menu.ts +++ b/apps/browser/src/autofill/content/bootstrap-autofill-overlay-menu.ts @@ -1,5 +1,6 @@ import { AutofillInlineMenuContentService } from "../overlay/inline-menu/content/autofill-inline-menu-content.service"; import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service"; +import DomElementVisibilityService from "../services/dom-element-visibility.service"; import { DomQueryService } from "../services/dom-query.service"; import { InlineMenuFieldQualificationService } from "../services/inline-menu-field-qualification.service"; import { setupAutofillInitDisconnectAction } from "../utils"; @@ -8,20 +9,25 @@ import AutofillInit from "./autofill-init"; (function (windowContext) { if (!windowContext.bitwardenAutofillInit) { + let inlineMenuContentService: AutofillInlineMenuContentService; + if (globalThis.self === globalThis.top) { + inlineMenuContentService = new AutofillInlineMenuContentService(); + } + const domQueryService = new DomQueryService(); + const domElementVisibilityService = new DomElementVisibilityService(inlineMenuContentService); const inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); const autofillOverlayContentService = new AutofillOverlayContentService( domQueryService, + domElementVisibilityService, inlineMenuFieldQualificationService, ); - let inlineMenuElements: AutofillInlineMenuContentService; - if (globalThis.self === globalThis.top) { - inlineMenuElements = new AutofillInlineMenuContentService(); - } + windowContext.bitwardenAutofillInit = new AutofillInit( domQueryService, + domElementVisibilityService, autofillOverlayContentService, - inlineMenuElements, + inlineMenuContentService, ); setupAutofillInitDisconnectAction(windowContext); diff --git a/apps/browser/src/autofill/content/bootstrap-autofill-overlay-notifications.ts b/apps/browser/src/autofill/content/bootstrap-autofill-overlay-notifications.ts index 0a810c68f56..6fbb076389e 100644 --- a/apps/browser/src/autofill/content/bootstrap-autofill-overlay-notifications.ts +++ b/apps/browser/src/autofill/content/bootstrap-autofill-overlay-notifications.ts @@ -1,5 +1,6 @@ import { OverlayNotificationsContentService } from "../overlay/notifications/content/overlay-notifications-content.service"; import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service"; +import DomElementVisibilityService from "../services/dom-element-visibility.service"; import { DomQueryService } from "../services/dom-query.service"; import { InlineMenuFieldQualificationService } from "../services/inline-menu-field-qualification.service"; import { setupAutofillInitDisconnectAction } from "../utils"; @@ -9,9 +10,11 @@ import AutofillInit from "./autofill-init"; (function (windowContext) { if (!windowContext.bitwardenAutofillInit) { const domQueryService = new DomQueryService(); + const domElementVisibilityService = new DomElementVisibilityService(); const inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); const autofillOverlayContentService = new AutofillOverlayContentService( domQueryService, + domElementVisibilityService, inlineMenuFieldQualificationService, ); @@ -22,6 +25,7 @@ import AutofillInit from "./autofill-init"; windowContext.bitwardenAutofillInit = new AutofillInit( domQueryService, + domElementVisibilityService, autofillOverlayContentService, null, overlayNotificationsContentService, diff --git a/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts b/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts index 6df9397f6d8..174a695b769 100644 --- a/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts +++ b/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts @@ -1,6 +1,7 @@ import { AutofillInlineMenuContentService } from "../overlay/inline-menu/content/autofill-inline-menu-content.service"; import { OverlayNotificationsContentService } from "../overlay/notifications/content/overlay-notifications-content.service"; import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service"; +import DomElementVisibilityService from "../services/dom-element-visibility.service"; import { DomQueryService } from "../services/dom-query.service"; import { InlineMenuFieldQualificationService } from "../services/inline-menu-field-qualification.service"; import { setupAutofillInitDisconnectAction } from "../utils"; @@ -9,24 +10,27 @@ import AutofillInit from "./autofill-init"; (function (windowContext) { if (!windowContext.bitwardenAutofillInit) { + let inlineMenuContentService: AutofillInlineMenuContentService; + let overlayNotificationsContentService: OverlayNotificationsContentService; + if (globalThis.self === globalThis.top) { + inlineMenuContentService = new AutofillInlineMenuContentService(); + overlayNotificationsContentService = new OverlayNotificationsContentService(); + } + const domQueryService = new DomQueryService(); + const domElementVisibilityService = new DomElementVisibilityService(inlineMenuContentService); const inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); const autofillOverlayContentService = new AutofillOverlayContentService( domQueryService, + domElementVisibilityService, inlineMenuFieldQualificationService, ); - let inlineMenuElements: AutofillInlineMenuContentService; - let overlayNotificationsContentService: OverlayNotificationsContentService; - if (globalThis.self === globalThis.top) { - inlineMenuElements = new AutofillInlineMenuContentService(); - overlayNotificationsContentService = new OverlayNotificationsContentService(); - } - windowContext.bitwardenAutofillInit = new AutofillInit( domQueryService, + domElementVisibilityService, autofillOverlayContentService, - inlineMenuElements, + inlineMenuContentService, overlayNotificationsContentService, ); setupAutofillInitDisconnectAction(windowContext); diff --git a/apps/browser/src/autofill/content/bootstrap-autofill.ts b/apps/browser/src/autofill/content/bootstrap-autofill.ts index 3de750cd671..ada66f233cb 100644 --- a/apps/browser/src/autofill/content/bootstrap-autofill.ts +++ b/apps/browser/src/autofill/content/bootstrap-autofill.ts @@ -1,3 +1,4 @@ +import DomElementVisibilityService from "../services/dom-element-visibility.service"; import { DomQueryService } from "../services/dom-query.service"; import { setupAutofillInitDisconnectAction } from "../utils"; @@ -6,7 +7,11 @@ import AutofillInit from "./autofill-init"; (function (windowContext) { if (!windowContext.bitwardenAutofillInit) { const domQueryService = new DomQueryService(); - windowContext.bitwardenAutofillInit = new AutofillInit(domQueryService); + const domElementVisibilityService = new DomElementVisibilityService(); + windowContext.bitwardenAutofillInit = new AutofillInit( + domQueryService, + domElementVisibilityService, + ); setupAutofillInitDisconnectAction(windowContext); windowContext.bitwardenAutofillInit.init(); diff --git a/apps/browser/src/autofill/deprecated/services/autofill-overlay-content.service.deprecated.ts b/apps/browser/src/autofill/deprecated/services/autofill-overlay-content.service.deprecated.ts index 87af2518ddc..27ec68bc678 100644 --- a/apps/browser/src/autofill/deprecated/services/autofill-overlay-content.service.deprecated.ts +++ b/apps/browser/src/autofill/deprecated/services/autofill-overlay-content.service.deprecated.ts @@ -73,6 +73,10 @@ class LegacyAutofillOverlayContentService implements LegacyAutofillOverlayConten * Satisfy the AutofillOverlayContentService interface. */ messageHandlers = {} as AutofillOverlayContentExtensionMessageHandlers; + clearUserFilledFields() { + // do nothing + } + async setupOverlayListeners( autofillFieldElement: ElementWithOpId, autofillFieldData: AutofillField, diff --git a/apps/browser/src/autofill/enums/autofill-overlay.enum.ts b/apps/browser/src/autofill/enums/autofill-overlay.enum.ts index 53f325d520f..66ad0da546d 100644 --- a/apps/browser/src/autofill/enums/autofill-overlay.enum.ts +++ b/apps/browser/src/autofill/enums/autofill-overlay.enum.ts @@ -1,3 +1,5 @@ +import { CipherType } from "@bitwarden/common/vault/enums"; + export const AutofillOverlayElement = { Button: "autofill-inline-menu-button", List: "autofill-inline-menu-list", @@ -19,4 +21,20 @@ export const RedirectFocusDirection = { Next: "next", } as const; +export enum InlineMenuFillType { + AccountCreationUsername = 5, + PasswordGeneration = 6, + CurrentPasswordUpdate = 7, +} +export type InlineMenuFillTypes = InlineMenuFillType | CipherType; + +export const InlineMenuAccountCreationFieldType = { + Text: "text", + Email: "email", + Password: "password", +} as const; + +export type InlineMenuAccountCreationFieldTypes = + (typeof InlineMenuAccountCreationFieldType)[keyof typeof InlineMenuAccountCreationFieldType]; + export const MAX_SUB_FRAME_DEPTH = 8; diff --git a/apps/browser/src/autofill/models/autofill-field.ts b/apps/browser/src/autofill/models/autofill-field.ts index 0701ef5f65a..cc9ba61f4ee 100644 --- a/apps/browser/src/autofill/models/autofill-field.ts +++ b/apps/browser/src/autofill/models/autofill-field.ts @@ -1,6 +1,8 @@ -import { CipherType } from "@bitwarden/common/vault/enums"; - import { AutofillFieldQualifierType } from "../enums/autofill-field.enums"; +import { + InlineMenuAccountCreationFieldTypes, + InlineMenuFillTypes, +} from "../enums/autofill-overlay.enum"; /** * Represents a single field that is collected from the page source and is potentially autofilled. @@ -107,15 +109,17 @@ export default class AutofillField { */ maxLength?: number | null; + dataSetValues?: string; + rel?: string | null; checked?: boolean; - filledByCipherType?: CipherType; - - showInlineMenuAccountCreation?: boolean; + inlineMenuFillType?: InlineMenuFillTypes; showPasskeys?: boolean; fieldQualifier?: AutofillFieldQualifierType; + + accountCreationFieldType?: InlineMenuAccountCreationFieldTypes; } diff --git a/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-iframe.service.ts b/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-iframe.service.ts index f5aff5d65f6..f55faec887a 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-iframe.service.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-iframe.service.ts @@ -3,6 +3,8 @@ export type AutofillInlineMenuIframeExtensionMessage = { styles?: Partial; theme?: string; portKey?: string; + generatedPassword?: string; + refreshPassword?: boolean; }; export type AutofillInlineMenuIframeExtensionMessageParam = { @@ -23,6 +25,9 @@ export type BackgroundPortMessageHandlers = { }: AutofillInlineMenuIframeExtensionMessageParam) => void; updateAutofillInlineMenuColorScheme: () => void; fadeInAutofillInlineMenuIframe: () => void; + updateAutofillInlineMenuGeneratedPassword: ({ + message, + }: AutofillInlineMenuIframeExtensionMessageParam) => void; }; export interface AutofillInlineMenuIframeService { diff --git a/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-list.ts b/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-list.ts index ea584165b4d..a20bd3c5312 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-list.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/abstractions/autofill-inline-menu-list.ts @@ -1,25 +1,34 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { CipherType } from "@bitwarden/common/vault/enums"; import { InlineMenuCipherData } from "../../../background/abstractions/overlay.background"; +import { InlineMenuFillTypes } from "../../../enums/autofill-overlay.enum"; type AutofillInlineMenuListMessage = { command: string }; -export type UpdateAutofillInlineMenuListCiphersMessage = AutofillInlineMenuListMessage & { +export type UpdateAutofillInlineMenuListCiphersParams = { ciphers: InlineMenuCipherData[]; showInlineMenuAccountCreation?: boolean; }; +export type UpdateAutofillInlineMenuListCiphersMessage = AutofillInlineMenuListMessage & + UpdateAutofillInlineMenuListCiphersParams; + +export type UpdateAutofillInlineMenuGeneratedPasswordMessage = AutofillInlineMenuListMessage & { + generatedPassword: string; +}; + export type InitAutofillInlineMenuListMessage = AutofillInlineMenuListMessage & { authStatus: AuthenticationStatus; styleSheetUrl: string; theme: string; translations: Record; ciphers?: InlineMenuCipherData[]; - filledByCipherType?: CipherType; + inlineMenuFillType?: InlineMenuFillTypes; showInlineMenuAccountCreation?: boolean; showPasskeysLabels?: boolean; portKey: string; + generatedPassword?: string; + showSaveLoginMenu?: boolean; }; export type AutofillInlineMenuListWindowMessageHandlers = { @@ -31,5 +40,10 @@ export type AutofillInlineMenuListWindowMessageHandlers = { }: { message: UpdateAutofillInlineMenuListCiphersMessage; }) => void; + updateAutofillInlineMenuGeneratedPassword: ({ + message, + }: { + message: UpdateAutofillInlineMenuGeneratedPasswordMessage; + }) => void; focusAutofillInlineMenuList: () => void; }; diff --git a/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.spec.ts b/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.spec.ts index c9d86cffc5c..8a3a7e6fa8d 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.spec.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.spec.ts @@ -3,6 +3,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import AutofillInit from "../../../content/autofill-init"; import { AutofillOverlayElement } from "../../../enums/autofill-overlay.enum"; import { DomQueryService } from "../../../services/abstractions/dom-query.service"; +import DomElementVisibilityService from "../../../services/dom-element-visibility.service"; import { createMutationRecordMock } from "../../../spec/autofill-mocks"; import { flushPromises, sendMockExtensionMessage } from "../../../spec/testing-utils"; import { ElementWithOpId } from "../../../types"; @@ -11,6 +12,7 @@ import { AutofillInlineMenuContentService } from "./autofill-inline-menu-content describe("AutofillInlineMenuContentService", () => { let domQueryService: MockProxy; + let domElementVisibilityService: DomElementVisibilityService; let autofillInlineMenuContentService: AutofillInlineMenuContentService; let autofillInit: AutofillInit; let sendExtensionMessageSpy: jest.SpyInstance; @@ -22,8 +24,14 @@ describe("AutofillInlineMenuContentService", () => { globalThis.document.body.innerHTML = ""; globalThis.requestIdleCallback = jest.fn((cb, options) => setTimeout(cb, 100)); domQueryService = mock(); + domElementVisibilityService = new DomElementVisibilityService(); autofillInlineMenuContentService = new AutofillInlineMenuContentService(); - autofillInit = new AutofillInit(domQueryService, null, autofillInlineMenuContentService); + autofillInit = new AutofillInit( + domQueryService, + domElementVisibilityService, + null, + autofillInlineMenuContentService, + ); autofillInit.init(); observeContainerMutationsSpy = jest.spyOn( autofillInlineMenuContentService["containerElementMutationObserver"] as any, @@ -37,6 +45,11 @@ describe("AutofillInlineMenuContentService", () => { afterEach(() => { jest.clearAllMocks(); + + Object.defineProperty(document, "activeElement", { + value: null, + writable: true, + }); }); describe("isElementInlineMenu", () => { @@ -197,6 +210,31 @@ describe("AutofillInlineMenuContentService", () => { ); }); }); + + it("appends the inline menu element to a containing `dialog` element if the element is a modal", async () => { + isInlineMenuButtonVisibleSpy.mockResolvedValue(false); + const dialogElement = document.createElement("dialog"); + dialogElement.setAttribute("open", "true"); + jest.spyOn(dialogElement, "matches").mockReturnValue(true); + const dialogAppendSpy = jest.spyOn(dialogElement, "appendChild"); + const inputElement = document.createElement("input"); + dialogElement.appendChild(inputElement); + document.body.appendChild(dialogElement); + Object.defineProperty(document, "activeElement", { + value: inputElement, + writable: true, + }); + + sendMockExtensionMessage({ + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.Button, + }); + await flushPromises(); + + expect(dialogAppendSpy).toHaveBeenCalledWith( + autofillInlineMenuContentService["buttonElement"], + ); + }); }); }); diff --git a/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.ts b/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.ts index 110c1be7db8..da274291731 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/content/autofill-inline-menu-content.service.ts @@ -88,7 +88,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte /** * Removes the autofill inline menu from the page. This will initially - * unobserve the body element to ensure the mutation observer no + * unobserve the menu container to ensure the mutation observer no * longer triggers. */ private closeInlineMenu = (message?: AutofillExtensionMessage) => { @@ -190,15 +190,15 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte } /** - * Appends the inline menu element to the body element. This method will also - * observe the body element to ensure that the inline menu element is not + * Appends the inline menu element to the menu container. This method will also + * observe the menu container to ensure that the inline menu element is not * interfered with by any DOM changes. * - * @param element - The inline menu element to append to the body element. + * @param element - The inline menu element to append to the menu container. */ private appendInlineMenuElementToDom(element: HTMLElement) { const parentDialogElement = globalThis.document.activeElement?.closest("dialog"); - if (parentDialogElement && parentDialogElement.open && parentDialogElement.matches(":modal")) { + if (parentDialogElement?.open && parentDialogElement.matches(":modal")) { this.observeContainerElement(parentDialogElement); parentDialogElement.appendChild(element); return; @@ -273,10 +273,10 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte } /** - * Sets up mutation observers for the inline menu elements, the body element, and + * Sets up mutation observers for the inline menu elements, the menu container, and * the document element. The mutation observers are used to remove any styles that * are added to the inline menu elements by the website. They are also used to ensure - * that the inline menu elements are always present at the bottom of the body element. + * that the inline menu elements are always present at the bottom of the menu container. */ private setupMutationObserver = () => { this.inlineMenuElementsMutationObserver = new MutationObserver( @@ -441,10 +441,10 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte /** * Handles the behavior of a persistent child element that is forcing itself to - * the bottom of the body element. This method will ensure that the inline menu + * the bottom of the menu container. This method will ensure that the inline menu * elements are not obscured by the persistent child element. * - * @param lastChild - The last child of the body element. + * @param lastChild - The last child of the menu container. */ private handlePersistentLastChildOverride(lastChild: Element) { const lastChildZIndex = parseInt((lastChild as HTMLElement).style.zIndex); @@ -460,11 +460,11 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte } /** - * Verifies if the last child of the body element is overlaying the inline menu elements. - * This is triggered when the last child of the body is being forced by some script to - * be an element other than the inline menu elements. + * Verifies if the last child of the menu container is overlaying the inline menu elements. + * This is triggered when the last child of the menu container is being forced by some + * script to be an element other than the inline menu elements. * - * @param lastChild - The last child of the body element. + * @param lastChild - The last child of the menu container. */ private verifyInlineMenuIsNotObscured = async (lastChild: Element) => { const inlineMenuPosition: InlineMenuPosition = await this.sendExtensionMessage( @@ -495,7 +495,7 @@ export class AutofillInlineMenuContentService implements AutofillInlineMenuConte } /** - * Clears the timeout that is used to verify that the last child of the body element + * Clears the timeout that is used to verify that the last child of the menu container * is not overlaying the inline menu elements. */ private clearPersistentLastChildOverrideTimeout() { diff --git a/apps/browser/src/autofill/overlay/inline-menu/iframe-content/__snapshots__/autofill-inline-menu-iframe.service.spec.ts.snap b/apps/browser/src/autofill/overlay/inline-menu/iframe-content/__snapshots__/autofill-inline-menu-iframe.service.spec.ts.snap index 4400b528d0f..8aac4f3c431 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/iframe-content/__snapshots__/autofill-inline-menu-iframe.service.spec.ts.snap +++ b/apps/browser/src/autofill/overlay/inline-menu/iframe-content/__snapshots__/autofill-inline-menu-iframe.service.spec.ts.snap @@ -3,6 +3,7 @@ exports[`AutofillInlineMenuIframeService initMenuIframe sets up the iframe's attributes 1`] = ` + +
+ + + + + +
{{ "or" | i18n }}
+ + +
+ + + + + +
+ + diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts new file mode 100644 index 00000000000..239383ddd00 --- /dev/null +++ b/libs/auth/src/angular/login/login.component.ts @@ -0,0 +1,599 @@ +import { CommonModule } from "@angular/common"; +import { Component, ElementRef, Input, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core"; +import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from "@angular/forms"; +import { ActivatedRoute, Router, RouterModule } from "@angular/router"; +import { firstValueFrom, Subject, take, takeUntil } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { + LoginEmailServiceAbstraction, + LoginStrategyServiceAbstraction, + PasswordLoginCredentials, + RegisterRouteService, +} from "@bitwarden/auth/common"; +import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; +import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; +import { CaptchaIFrame } from "@bitwarden/common/auth/captcha-iframe"; +import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; +import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; +import { ClientType, HttpStatusCode } from "@bitwarden/common/enums"; +import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; +import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { SyncService } from "@bitwarden/common/platform/sync"; +import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +import { + AsyncActionsModule, + ButtonModule, + CheckboxModule, + FormFieldModule, + IconButtonModule, + LinkModule, + ToastService, +} from "@bitwarden/components"; + +import { AnonLayoutWrapperDataService } from "../anon-layout/anon-layout-wrapper-data.service"; +import { VaultIcon, WaveIcon } from "../icons"; + +import { LoginComponentService } from "./login-component.service"; + +const BroadcasterSubscriptionId = "LoginComponent"; + +export enum LoginUiState { + EMAIL_ENTRY = "EmailEntry", + MASTER_PASSWORD_ENTRY = "MasterPasswordEntry", +} + +@Component({ + standalone: true, + templateUrl: "./login.component.html", + imports: [ + AsyncActionsModule, + ButtonModule, + CheckboxModule, + CommonModule, + FormFieldModule, + IconButtonModule, + LinkModule, + JslibModule, + ReactiveFormsModule, + RouterModule, + ], +}) +export class LoginComponent implements OnInit, OnDestroy { + @ViewChild("masterPasswordInputRef") masterPasswordInputRef: ElementRef; + @Input() captchaSiteKey: string = null; + + private destroy$ = new Subject(); + private enforcedMasterPasswordOptions: MasterPasswordPolicyOptions = undefined; + readonly Icons = { WaveIcon, VaultIcon }; + + captcha: CaptchaIFrame; + captchaToken: string = null; + clientType: ClientType; + ClientType = ClientType; + LoginUiState = LoginUiState; + registerRoute$ = this.registerRouteService.registerRoute$(); // TODO: remove when email verification flag is removed + isKnownDevice = false; + loginUiState: LoginUiState = LoginUiState.EMAIL_ENTRY; + + formGroup = this.formBuilder.group( + { + email: ["", [Validators.required, Validators.email]], + masterPassword: [ + "", + [Validators.required, Validators.minLength(Utils.originalMinimumPasswordLength)], + ], + rememberEmail: [false], + }, + { updateOn: "submit" }, + ); + + get emailFormControl(): FormControl { + return this.formGroup.controls.email; + } + + /** + * LoginViaAuthRequestSupported is a boolean that determines if we show the Login with device button. + * An AuthRequest is the mechanism that allows users to login to the client via a device that is already logged in. + */ + loginViaAuthRequestSupported = false; + + // Web properties + enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions; + policies: Policy[]; + showResetPasswordAutoEnrollWarning = false; + + // Desktop properties + deferFocus: boolean | null = null; + + constructor( + private activatedRoute: ActivatedRoute, + private anonLayoutWrapperDataService: AnonLayoutWrapperDataService, + private appIdService: AppIdService, + private broadcasterService: BroadcasterService, + private devicesApiService: DevicesApiServiceAbstraction, + private environmentService: EnvironmentService, + private formBuilder: FormBuilder, + private i18nService: I18nService, + private loginEmailService: LoginEmailServiceAbstraction, + private loginComponentService: LoginComponentService, + private loginStrategyService: LoginStrategyServiceAbstraction, + private messagingService: MessagingService, + private ngZone: NgZone, + private passwordStrengthService: PasswordStrengthServiceAbstraction, + private platformUtilsService: PlatformUtilsService, + private policyService: InternalPolicyService, + private registerRouteService: RegisterRouteService, + private router: Router, + private syncService: SyncService, + private toastService: ToastService, + private logService: LogService, + private validationService: ValidationService, + ) { + this.clientType = this.platformUtilsService.getClientType(); + this.loginViaAuthRequestSupported = this.loginComponentService.isLoginViaAuthRequestSupported(); + } + + async ngOnInit(): Promise { + await this.defaultOnInit(); + + if (this.clientType === ClientType.Desktop) { + await this.desktopOnInit(); + } + } + + ngOnDestroy(): void { + if (this.clientType === ClientType.Desktop) { + // TODO: refactor to not use deprecated broadcaster service. + this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); + } + + this.destroy$.next(); + this.destroy$.complete(); + } + + submit = async (): Promise => { + if (this.clientType === ClientType.Desktop) { + if (this.loginUiState !== LoginUiState.MASTER_PASSWORD_ENTRY) { + return; + } + } + + const { email, masterPassword } = this.formGroup.value; + + await this.setupCaptcha(); + + this.formGroup.markAllAsTouched(); + if (this.formGroup.invalid) { + return; + } + + const credentials = new PasswordLoginCredentials( + email, + masterPassword, + this.captchaToken, + null, + ); + + try { + const authResult = await this.loginStrategyService.logIn(credentials); + + await this.saveEmailSettings(); + await this.handleAuthResult(authResult); + + if (this.clientType === ClientType.Desktop) { + if (this.captchaSiteKey) { + const content = document.getElementById("content") as HTMLDivElement; + content.setAttribute("style", "width:335px"); + } + } + } catch (error) { + this.logService.error(error); + this.handleSubmitError(error); + } + }; + + /** + * Handles the error from the submit function. + * + * @param error The error object. + */ + private handleSubmitError(error: unknown) { + // Handle error responses + if (error instanceof ErrorResponse) { + switch (error.statusCode) { + case HttpStatusCode.BadRequest: { + if (error.message.toLowerCase().includes("username or password is incorrect")) { + this.formGroup.controls.masterPassword.setErrors({ + error: { + message: this.i18nService.t("invalidMasterPassword"), + }, + }); + } + break; + } + default: { + // Allow all other errors to be handled by toast + this.validationService.showError(error); + } + } + } else { + // Allow all other errors to be handled by toast + this.validationService.showError(error); + } + } + + /** + * Handles the result of the authentication process. + * + * @param authResult + * @returns A simple `return` statement for each conditional check. + * If you update this method, do not forget to add a `return` + * to each if-condition block where necessary to stop code execution. + */ + private async handleAuthResult(authResult: AuthResult): Promise { + if (this.handleCaptchaRequired(authResult)) { + this.captchaSiteKey = authResult.captchaSiteKey; + this.captcha.init(authResult.captchaSiteKey); + return; + } + + if (authResult.requiresEncryptionKeyMigration) { + /* Legacy accounts used the master key to encrypt data. + Migration is required but only performed on Web. */ + if (this.clientType === ClientType.Web) { + await this.router.navigate(["migrate-legacy-encryption"]); + } else { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccured"), + message: this.i18nService.t("encryptionKeyMigrationRequired"), + }); + } + return; + } + + if (authResult.requiresTwoFactor) { + await this.router.navigate(["2fa"]); + return; + } + + await this.syncService.fullSync(true); + + if (authResult.forcePasswordReset != ForceSetPasswordReason.None) { + this.loginEmailService.clearValues(); + await this.router.navigate(["update-temp-password"]); + return; + } + + // If none of the above cases are true, proceed with login... + await this.evaluatePassword(); + + this.loginEmailService.clearValues(); + + if (this.clientType === ClientType.Browser) { + await this.router.navigate(["/tabs/vault"]); + } else { + await this.router.navigate(["vault"]); + } + } + + protected async launchSsoBrowserWindow(clientId: "browser" | "desktop"): Promise { + await this.loginComponentService.launchSsoBrowserWindow(this.emailFormControl.value, clientId); + } + + protected async evaluatePassword(): Promise { + try { + // If we do not have any saved policies, attempt to load them from the service + if (this.enforcedMasterPasswordOptions == undefined) { + this.enforcedMasterPasswordOptions = await firstValueFrom( + this.policyService.masterPasswordPolicyOptions$(), + ); + } + + if (this.requirePasswordChange()) { + await this.router.navigate(["update-password"]); + return; + } + } catch (e) { + // Do not prevent unlock if there is an error evaluating policies + this.logService.error(e); + } + } + + /** + * Checks if the master password meets the enforced policy requirements + * If not, returns false + */ + private requirePasswordChange(): boolean { + if ( + this.enforcedMasterPasswordOptions == undefined || + !this.enforcedMasterPasswordOptions.enforceOnLogin + ) { + return false; + } + + const masterPassword = this.formGroup.controls.masterPassword.value; + + const passwordStrength = this.passwordStrengthService.getPasswordStrength( + masterPassword, + this.formGroup.value.email, + )?.score; + + return !this.policyService.evaluateMasterPassword( + passwordStrength, + masterPassword, + this.enforcedMasterPasswordOptions, + ); + } + + protected showCaptcha(): boolean { + return !Utils.isNullOrWhitespace(this.captchaSiteKey); + } + + protected async startAuthRequestLogin(): Promise { + this.formGroup.get("masterPassword")?.clearValidators(); + this.formGroup.get("masterPassword")?.updateValueAndValidity(); + + if (!this.formGroup.valid) { + return; + } + + await this.saveEmailSettings(); + await this.router.navigate(["/login-with-device"]); + } + + protected async validateEmail(): Promise { + this.formGroup.controls.email.markAsTouched(); + return this.formGroup.controls.email.valid; + } + + protected async toggleLoginUiState(value: LoginUiState): Promise { + this.loginUiState = value; + + if (this.loginUiState === LoginUiState.EMAIL_ENTRY) { + this.loginComponentService.showBackButton(false); + + this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ + pageTitle: { key: "logInToBitwarden" }, + pageIcon: this.Icons.VaultIcon, + pageSubtitle: null, // remove subtitle when going back to email entry + }); + + // Reset master password only when going from validated to not validated so that autofill can work properly + this.formGroup.controls.masterPassword.reset(); + + if (this.loginViaAuthRequestSupported) { + // Reset known device state when going back to email entry if it is supported + this.isKnownDevice = false; + } + } else if (this.loginUiState === LoginUiState.MASTER_PASSWORD_ENTRY) { + this.loginComponentService.showBackButton(true); + this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ + pageTitle: { key: "welcomeBack" }, + pageSubtitle: this.emailFormControl.value, + pageIcon: this.Icons.WaveIcon, + }); + + // Mark MP as untouched so that, when users enter email and hit enter, the MP field doesn't load with validation errors + this.formGroup.controls.masterPassword.markAsUntouched(); + + // When email is validated, focus on master password after waiting for input to be rendered + if (this.ngZone.isStable) { + this.masterPasswordInputRef?.nativeElement?.focus(); + } else { + this.ngZone.onStable.pipe(take(1), takeUntil(this.destroy$)).subscribe(() => { + this.masterPasswordInputRef?.nativeElement?.focus(); + }); + } + + if (this.loginViaAuthRequestSupported) { + await this.getKnownDevice(this.emailFormControl.value); + } + } + } + + /** + * Set the email value from the input field. + * @param event The event object from the input field. + */ + onEmailBlur(event: Event) { + const emailInput = event.target as HTMLInputElement; + this.formGroup.controls.email.setValue(emailInput.value); + // Call setLoginEmail so that the email is pre-populated when navigating to the "enter password" screen. + this.loginEmailService.setLoginEmail(this.formGroup.value.email); + } + + isLoginWithPasskeySupported() { + return this.loginComponentService.isLoginWithPasskeySupported(); + } + + protected async goToHint(): Promise { + await this.saveEmailSettings(); + await this.router.navigateByUrl("/hint"); + } + + protected async goToRegister(): Promise { + // TODO: remove when email verification flag is removed + const registerRoute = await firstValueFrom(this.registerRoute$); + + if (this.emailFormControl.valid) { + await this.router.navigate([registerRoute], { + queryParams: { email: this.emailFormControl.value }, + }); + return; + } + + await this.router.navigate([registerRoute]); + } + + protected async saveEmailSettings(): Promise { + await this.loginEmailService.setLoginEmail(this.formGroup.value.email); + this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail); + await this.loginEmailService.saveEmailSettings(); + } + + protected async continue(): Promise { + if (await this.validateEmail()) { + await this.toggleLoginUiState(LoginUiState.MASTER_PASSWORD_ENTRY); + } + } + + /** + * Call to check if the device is known. + * Known means that the user has logged in with this device before. + * @param email - The user's email + */ + private async getKnownDevice(email: string): Promise { + try { + const deviceIdentifier = await this.appIdService.getAppId(); + this.isKnownDevice = await this.devicesApiService.getKnownDevice(email, deviceIdentifier); + } catch (e) { + this.isKnownDevice = false; + } + } + + private async setupCaptcha(): Promise { + const env = await firstValueFrom(this.environmentService.environment$); + const webVaultUrl = env.getWebVaultUrl(); + + this.captcha = new CaptchaIFrame( + window, + webVaultUrl, + this.i18nService, + (token: string) => { + this.captchaToken = token; + }, + (error: string) => { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: error, + }); + }, + (info: string) => { + this.toastService.showToast({ + variant: "info", + title: this.i18nService.t("info"), + message: info, + }); + }, + ); + } + + private handleCaptchaRequired(authResult: AuthResult): boolean { + return !Utils.isNullOrWhitespace(authResult.captchaSiteKey); + } + + private async loadEmailSettings(): Promise { + // Try to load the email from memory first + const email = await firstValueFrom(this.loginEmailService.loginEmail$); + const rememberEmail = this.loginEmailService.getRememberEmail(); + + if (email) { + this.formGroup.controls.email.setValue(email); + this.formGroup.controls.rememberEmail.setValue(rememberEmail); + } else { + // If there is no email in memory, check for a storedEmail on disk + const storedEmail = await firstValueFrom(this.loginEmailService.storedEmail$); + + if (storedEmail) { + this.formGroup.controls.email.setValue(storedEmail); + // If there is a storedEmail, rememberEmail defaults to true + this.formGroup.controls.rememberEmail.setValue(true); + } + } + } + + private focusInput() { + document + .getElementById( + this.emailFormControl.value == null || this.emailFormControl.value === "" + ? "email" + : "masterPassword", + ) + ?.focus(); + } + + private async defaultOnInit(): Promise { + // If there's an existing org invite, use it to get the password policies + const orgPolicies = await this.loginComponentService.getOrgPolicies(); + + this.policies = orgPolicies?.policies; + this.showResetPasswordAutoEnrollWarning = orgPolicies?.isPolicyAndAutoEnrollEnabled; + + let paramEmailIsSet = false; + + const params = await firstValueFrom(this.activatedRoute.queryParams); + + if (params) { + const qParamsEmail = params.email; + + // If there is an email in the query params, set that email as the form field value + if (qParamsEmail != null && qParamsEmail.indexOf("@") > -1) { + this.formGroup.controls.email.setValue(qParamsEmail); + paramEmailIsSet = true; + } + } + + // If there are no params or no email in the query params, loadEmailSettings from state + if (!paramEmailIsSet) { + await this.loadEmailSettings(); + } + + if (this.loginViaAuthRequestSupported) { + await this.getKnownDevice(this.emailFormControl.value); + } + + // Backup check to handle unknown case where activatedRoute is not available + // This shouldn't happen under normal circumstances + if (!this.activatedRoute) { + await this.loadEmailSettings(); + } + } + + private async desktopOnInit(): Promise { + // TODO: refactor to not use deprecated broadcaster service. + this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => { + this.ngZone.run(() => { + switch (message.command) { + case "windowIsFocused": + if (this.deferFocus === null) { + this.deferFocus = !message.windowIsFocused; + if (!this.deferFocus) { + this.focusInput(); + } + } else if (this.deferFocus && message.windowIsFocused) { + this.focusInput(); + this.deferFocus = false; + } + break; + default: + } + }); + }); + + this.messagingService.send("getWindowIsFocused"); + } + + /** + * Helper function to determine if the back button should be shown. + * @returns true if the back button should be shown. + */ + protected shouldShowBackButton(): boolean { + return ( + this.loginUiState === LoginUiState.MASTER_PASSWORD_ENTRY && + this.clientType !== ClientType.Browser + ); + } +} diff --git a/libs/auth/src/angular/registration/registration-env-selector/registration-env-selector.component.ts b/libs/auth/src/angular/registration/registration-env-selector/registration-env-selector.component.ts index d5e588cdd96..0fe88552653 100644 --- a/libs/auth/src/angular/registration/registration-env-selector/registration-env-selector.component.ts +++ b/libs/auth/src/angular/registration/registration-env-selector/registration-env-selector.component.ts @@ -15,7 +15,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService, FormFieldModule, SelectModule, ToastService } from "@bitwarden/components"; -import { RegistrationSelfHostedEnvConfigDialogComponent } from "./registration-self-hosted-env-config-dialog.component"; +import { SelfHostedEnvConfigDialogComponent } from "../../self-hosted-env-config-dialog/self-hosted-env-config-dialog.component"; /** * Component for selecting the environment to register with in the email verification registration flow. @@ -125,9 +125,7 @@ export class RegistrationEnvSelectorComponent implements OnInit, OnDestroy { } if (selectedRegion === Region.SelfHosted) { - return from( - RegistrationSelfHostedEnvConfigDialogComponent.open(this.dialogService), - ).pipe( + return from(SelfHostedEnvConfigDialogComponent.open(this.dialogService)).pipe( tap((result: boolean | undefined) => this.handleSelfHostedEnvConfigDialogResult(result, prevSelectedRegion), ), diff --git a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts index fe6b9b2c7dc..e034e23de43 100644 --- a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts +++ b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.spec.ts @@ -2,11 +2,11 @@ import { MockProxy, mock } from "jest-mock-extended"; import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service"; import { DEFAULT_KDF_CONFIG } from "@bitwarden/common/auth/models/domain/kdf-config"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { CsprngArray } from "@bitwarden/common/types/csprng"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; +import { KeyService } from "@bitwarden/key-management"; import { PasswordInputResult } from "../../input-password/password-input-result"; @@ -15,14 +15,14 @@ import { DefaultRegistrationFinishService } from "./default-registration-finish. describe("DefaultRegistrationFinishService", () => { let service: DefaultRegistrationFinishService; - let cryptoService: MockProxy; + let keyService: MockProxy; let accountApiService: MockProxy; beforeEach(() => { - cryptoService = mock(); + keyService = mock(); accountApiService = mock(); - service = new DefaultRegistrationFinishService(cryptoService, accountApiService); + service = new DefaultRegistrationFinishService(keyService, accountApiService); }); it("instantiates", () => { @@ -76,7 +76,7 @@ describe("DefaultRegistrationFinishService", () => { }); it("throws an error if the user key cannot be created", async () => { - cryptoService.makeUserKey.mockResolvedValue([null, null]); + keyService.makeUserKey.mockResolvedValue([null, null]); await expect(service.finishRegistration(email, passwordInputResult)).rejects.toThrow( "User key could not be created", @@ -84,8 +84,8 @@ describe("DefaultRegistrationFinishService", () => { }); it("registers the user and returns a captcha bypass token when given valid email verification input", async () => { - cryptoService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); - cryptoService.makeKeyPair.mockResolvedValue(userKeyPair); + keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); + keyService.makeKeyPair.mockResolvedValue(userKeyPair); accountApiService.registerFinish.mockResolvedValue(capchaBypassToken); const result = await service.finishRegistration( @@ -96,8 +96,8 @@ describe("DefaultRegistrationFinishService", () => { expect(result).toEqual(capchaBypassToken); - expect(cryptoService.makeUserKey).toHaveBeenCalledWith(masterKey); - expect(cryptoService.makeKeyPair).toHaveBeenCalledWith(userKey); + expect(keyService.makeUserKey).toHaveBeenCalledWith(masterKey); + expect(keyService.makeKeyPair).toHaveBeenCalledWith(userKey); expect(accountApiService.registerFinish).toHaveBeenCalledWith( expect.objectContaining({ email, @@ -113,8 +113,14 @@ describe("DefaultRegistrationFinishService", () => { kdfIterations: passwordInputResult.kdfConfig.iterations, kdfMemory: undefined, kdfParallelism: undefined, - orgInviteToken: undefined, // OrgInvite only handled in web - organizationUserId: undefined, // OrgInvite only handled in web + // Web only fields should be undefined + orgInviteToken: undefined, + organizationUserId: undefined, + orgSponsoredFreeFamilyPlanToken: undefined, + acceptEmergencyAccessInviteToken: undefined, + acceptEmergencyAccessId: undefined, + providerInviteToken: undefined, + providerUserId: undefined, }), ); }); diff --git a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts index 6d77c777491..f2c4d4c98b6 100644 --- a/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts +++ b/libs/auth/src/angular/registration/registration-finish/default-registration-finish.service.ts @@ -2,8 +2,8 @@ import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/mod import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service"; import { RegisterFinishRequest } from "@bitwarden/common/auth/models/request/registration/register-finish.request"; import { KeysRequest } from "@bitwarden/common/models/request/keys.request"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { KeyService } from "@bitwarden/key-management"; import { PasswordInputResult } from "../../input-password/password-input-result"; @@ -11,7 +11,7 @@ import { RegistrationFinishService } from "./registration-finish.service"; export class DefaultRegistrationFinishService implements RegistrationFinishService { constructor( - protected cryptoService: CryptoService, + protected keyService: KeyService, protected accountApiService: AccountApiService, ) {} @@ -30,15 +30,17 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi orgSponsoredFreeFamilyPlanToken?: string, acceptEmergencyAccessInviteToken?: string, emergencyAccessId?: string, + providerInviteToken?: string, + providerUserId?: string, ): Promise { - const [newUserKey, newEncUserKey] = await this.cryptoService.makeUserKey( + const [newUserKey, newEncUserKey] = await this.keyService.makeUserKey( passwordInputResult.masterKey, ); if (!newUserKey || !newEncUserKey) { throw new Error("User key could not be created"); } - const userAsymmetricKeys = await this.cryptoService.makeKeyPair(newUserKey); + const userAsymmetricKeys = await this.keyService.makeKeyPair(newUserKey); const registerRequest = await this.buildRegisterRequest( email, @@ -49,6 +51,8 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi orgSponsoredFreeFamilyPlanToken, acceptEmergencyAccessInviteToken, emergencyAccessId, + providerInviteToken, + providerUserId, ); const capchaBypassToken = await this.accountApiService.registerFinish(registerRequest); @@ -65,6 +69,8 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi orgSponsoredFreeFamilyPlanToken?: string, // web only acceptEmergencyAccessInviteToken?: string, // web only emergencyAccessId?: string, // web only + providerInviteToken?: string, // web only + providerUserId?: string, // web only ): Promise { const userAsymmetricKeysRequest = new KeysRequest( userAsymmetricKeys[0], diff --git a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts index be9d8abe5b0..7cfa85ec3d4 100644 --- a/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts +++ b/libs/auth/src/angular/registration/registration-finish/registration-finish.component.ts @@ -49,6 +49,10 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { acceptEmergencyAccessInviteToken: string; emergencyAccessId: string; + // This token is provided when the user is coming from an emailed invite to accept a provider invite + providerInviteToken: string; + providerUserId: string; + masterPasswordPolicyOptions: MasterPasswordPolicyOptions | null = null; constructor( @@ -104,6 +108,11 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { this.acceptEmergencyAccessInviteToken = qParams.acceptEmergencyAccessInviteToken; this.emergencyAccessId = qParams.emergencyAccessId; } + + if (qParams.providerInviteToken != null && qParams.providerUserId != null) { + this.providerInviteToken = qParams.providerInviteToken; + this.providerUserId = qParams.providerUserId; + } } private async initOrgInviteFlowIfPresent(): Promise { @@ -140,6 +149,8 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { this.orgSponsoredFreeFamilyPlanToken, this.acceptEmergencyAccessInviteToken, this.emergencyAccessId, + this.providerInviteToken, + this.providerUserId, ); } catch (e) { this.validationService.showError(e); @@ -163,7 +174,12 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy { null, ); - await this.loginStrategyService.logIn(credentials); + const authenticationResult = await this.loginStrategyService.logIn(credentials); + + if (authenticationResult?.requiresTwoFactor) { + await this.router.navigate(["/2fa"]); + return; + } this.toastService.showToast({ variant: "success", diff --git a/libs/auth/src/angular/registration/registration-finish/registration-finish.service.ts b/libs/auth/src/angular/registration/registration-finish/registration-finish.service.ts index b7abd381084..3746e37b84a 100644 --- a/libs/auth/src/angular/registration/registration-finish/registration-finish.service.ts +++ b/libs/auth/src/angular/registration/registration-finish/registration-finish.service.ts @@ -25,6 +25,8 @@ export abstract class RegistrationFinishService { * @param orgSponsoredFreeFamilyPlanToken The optional org sponsored free family plan token. * @param acceptEmergencyAccessInviteToken The optional accept emergency access invite token. * @param emergencyAccessId The optional emergency access id which is required to validate the emergency access invite token. + * @param providerInviteToken The optional provider invite token. + * @param providerUserId The optional provider user id which is required to validate the provider invite token. * @returns a promise which resolves to the captcha bypass token string upon a successful account creation. */ abstract finishRegistration( @@ -34,5 +36,7 @@ export abstract class RegistrationFinishService { orgSponsoredFreeFamilyPlanToken?: string, acceptEmergencyAccessInviteToken?: string, emergencyAccessId?: string, + providerInviteToken?: string, + providerUserId?: string, ): Promise; } diff --git a/libs/auth/src/angular/registration/registration-start/registration-start-secondary.component.html b/libs/auth/src/angular/registration/registration-start/registration-start-secondary.component.html index c878724106b..f4d0767f7be 100644 --- a/libs/auth/src/angular/registration/registration-start/registration-start-secondary.component.html +++ b/libs/auth/src/angular/registration/registration-start/registration-start-secondary.component.html @@ -1,3 +1,4 @@ {{ "alreadyHaveAccount" | i18n }} {{ "logIn" | i18n }}{{ "alreadyHaveAccount" | i18n }} + {{ "logIn" | i18n }} diff --git a/libs/auth/src/angular/registration/registration-start/registration-start-secondary.component.ts b/libs/auth/src/angular/registration/registration-start/registration-start-secondary.component.ts index 1c2883beb08..f01a8c71bba 100644 --- a/libs/auth/src/angular/registration/registration-start/registration-start-secondary.component.ts +++ b/libs/auth/src/angular/registration/registration-start/registration-start-secondary.component.ts @@ -4,6 +4,7 @@ import { ActivatedRoute, RouterModule } from "@angular/router"; import { firstValueFrom } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { LinkModule } from "@bitwarden/components"; /** * RegistrationStartSecondaryComponentData @@ -17,7 +18,7 @@ export interface RegistrationStartSecondaryComponentData { standalone: true, selector: "auth-registration-start-secondary", templateUrl: "./registration-start-secondary.component.html", - imports: [CommonModule, JslibModule, RouterModule], + imports: [CommonModule, JslibModule, RouterModule, LinkModule], }) export class RegistrationStartSecondaryComponent implements OnInit { loginRoute: string; diff --git a/libs/auth/src/angular/registration/registration-start/registration-start.component.ts b/libs/auth/src/angular/registration/registration-start/registration-start.component.ts index 258f811ec8b..5ce606b5ec6 100644 --- a/libs/auth/src/angular/registration/registration-start/registration-start.component.ts +++ b/libs/auth/src/angular/registration/registration-start/registration-start.component.ts @@ -18,6 +18,7 @@ import { LinkModule, } from "@bitwarden/components"; +import { LoginEmailService } from "../../../common"; import { AnonLayoutWrapperDataService } from "../../anon-layout/anon-layout-wrapper-data.service"; import { RegistrationUserAddIcon } from "../../icons"; import { RegistrationCheckEmailIcon } from "../../icons/registration-check-email.icon"; @@ -89,6 +90,7 @@ export class RegistrationStartComponent implements OnInit, OnDestroy { private platformUtilsService: PlatformUtilsService, private accountApiService: AccountApiService, private router: Router, + private loginEmailService: LoginEmailService, private anonLayoutWrapperDataService: AnonLayoutWrapperDataService, ) { this.isSelfHost = platformUtilsService.isSelfHost(); @@ -99,6 +101,15 @@ export class RegistrationStartComponent implements OnInit, OnDestroy { this.registrationStartStateChange.emit(this.state); this.listenForQueryParamChanges(); + + /** + * If the user has a login email, set the email field to the login email. + */ + this.loginEmailService.loginEmail$.pipe(takeUntil(this.destroy$)).subscribe((email) => { + if (email) { + this.formGroup.patchValue({ email }); + } + }); } private listenForQueryParamChanges() { diff --git a/libs/auth/src/angular/registration/registration-start/registration-start.stories.ts b/libs/auth/src/angular/registration/registration-start/registration-start.stories.ts index f7f6185280a..fa3ad2ae2b9 100644 --- a/libs/auth/src/angular/registration/registration-start/registration-start.stories.ts +++ b/libs/auth/src/angular/registration/registration-start/registration-start.stories.ts @@ -4,7 +4,7 @@ import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { ActivatedRoute, Params } from "@angular/router"; import { RouterTestingModule } from "@angular/router/testing"; import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular"; -import { of } from "rxjs"; +import { of, BehaviorSubject } from "rxjs"; import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service"; import { ClientType } from "@bitwarden/common/enums"; @@ -30,6 +30,7 @@ import { // FIXME: remove `/apps` import from `/libs` // eslint-disable-next-line import/no-restricted-paths import { PreloadedEnglishI18nModule } from "../../../../../../apps/web/src/app/core/tests"; +import { LoginEmailService } from "../../../common"; import { AnonLayoutWrapperDataService } from "../../anon-layout/anon-layout-wrapper-data.service"; import { AnonLayoutWrapperData } from "../../anon-layout/anon-layout-wrapper.component"; @@ -45,6 +46,7 @@ const decorators = (options: { queryParams?: Params; clientType?: ClientType; defaultRegion?: Region; + initialLoginEmail?: string; }) => { return [ moduleMetadata({ @@ -90,6 +92,12 @@ const decorators = (options: { getClientType: () => options.clientType || ClientType.Web, } as Partial, }, + { + provide: LoginEmailService, + useValue: { + loginEmail$: new BehaviorSubject(options.initialLoginEmail || null), + } as Partial, + }, { provide: AnonLayoutWrapperDataService, useValue: { @@ -159,6 +167,21 @@ export const WebUSRegionQueryParamsExample: Story = { }), }; +export const WebUSRegionWithInitialLoginEmailExample: Story = { + render: (args) => ({ + props: args, + template: ` + + `, + }), + decorators: decorators({ + clientType: ClientType.Web, + queryParams: {}, + defaultRegion: Region.US, + initialLoginEmail: "example@bitwarden.com", + }), +}; + export const DesktopUSRegionExample: Story = { render: (args) => ({ props: args, diff --git a/libs/auth/src/angular/registration/registration-env-selector/registration-self-hosted-env-config-dialog.component.html b/libs/auth/src/angular/self-hosted-env-config-dialog/self-hosted-env-config-dialog.component.html similarity index 100% rename from libs/auth/src/angular/registration/registration-env-selector/registration-self-hosted-env-config-dialog.component.html rename to libs/auth/src/angular/self-hosted-env-config-dialog/self-hosted-env-config-dialog.component.html diff --git a/libs/auth/src/angular/registration/registration-env-selector/registration-self-hosted-env-config-dialog.component.ts b/libs/auth/src/angular/self-hosted-env-config-dialog/self-hosted-env-config-dialog.component.ts similarity index 77% rename from libs/auth/src/angular/registration/registration-env-selector/registration-self-hosted-env-config-dialog.component.ts rename to libs/auth/src/angular/self-hosted-env-config-dialog/self-hosted-env-config-dialog.component.ts index 2bedb4b3583..781dd0f154c 100644 --- a/libs/auth/src/angular/registration/registration-env-selector/registration-self-hosted-env-config-dialog.component.ts +++ b/libs/auth/src/angular/self-hosted-env-config-dialog/self-hosted-env-config-dialog.component.ts @@ -10,7 +10,7 @@ import { ValidationErrors, ValidatorFn, } from "@angular/forms"; -import { Subject, firstValueFrom } from "rxjs"; +import { Subject, firstValueFrom, take, filter, takeUntil } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { @@ -54,8 +54,8 @@ function selfHostedEnvSettingsFormValidator(): ValidatorFn { */ @Component({ standalone: true, - selector: "auth-registration-self-hosted-env-config-dialog", - templateUrl: "registration-self-hosted-env-config-dialog.component.html", + selector: "self-hosted-env-config-dialog", + templateUrl: "self-hosted-env-config-dialog.component.html", imports: [ CommonModule, JslibModule, @@ -68,14 +68,14 @@ function selfHostedEnvSettingsFormValidator(): ValidatorFn { AsyncActionsModule, ], }) -export class RegistrationSelfHostedEnvConfigDialogComponent implements OnInit, OnDestroy { +export class SelfHostedEnvConfigDialogComponent implements OnInit, OnDestroy { /** * Opens the dialog. * @param dialogService - Dialog service. * @returns Promise that resolves to true if the dialog was closed with a successful result, false otherwise. */ static async open(dialogService: DialogService): Promise { - const dialogRef = dialogService.open(RegistrationSelfHostedEnvConfigDialogComponent, { + const dialogRef = dialogService.open(SelfHostedEnvConfigDialogComponent, { disableClose: false, }); @@ -131,7 +131,33 @@ export class RegistrationSelfHostedEnvConfigDialogComponent implements OnInit, O private environmentService: EnvironmentService, ) {} - ngOnInit() {} + ngOnInit() { + /** + * Populate the form with the current self-hosted environment settings. + */ + this.environmentService.environment$ + .pipe( + take(1), + filter((env) => { + const region = env.getRegion(); + return region === Region.SelfHosted; + }), + takeUntil(this.destroy$), + ) + .subscribe({ + next: (env) => { + const urls = env.getUrls(); + this.formGroup.patchValue({ + baseUrl: urls.base || "", + webVaultUrl: urls.webVault || "", + apiUrl: urls.api || "", + identityUrl: urls.identity || "", + iconsUrl: urls.icons || "", + notificationsUrl: urls.notifications || "", + }); + }, + }); + } submit = async () => { this.showErrorSummary = false; diff --git a/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.spec.ts b/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.spec.ts index f36283e0c06..da49067d7b6 100644 --- a/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.spec.ts +++ b/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.spec.ts @@ -14,7 +14,6 @@ import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth import { DEFAULT_KDF_CONFIG } from "@bitwarden/common/auth/models/domain/kdf-config"; import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request"; import { KeysRequest } from "@bitwarden/common/models/request/keys.request"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -23,6 +22,7 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym import { CsprngArray } from "@bitwarden/common/types/csprng"; import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; +import { KeyService } from "@bitwarden/key-management"; import { PasswordInputResult } from "../input-password/password-input-result"; @@ -33,7 +33,7 @@ describe("DefaultSetPasswordJitService", () => { let sut: DefaultSetPasswordJitService; let apiService: MockProxy; - let cryptoService: MockProxy; + let keyService: MockProxy; let encryptService: MockProxy; let i18nService: MockProxy; let kdfConfigService: MockProxy; @@ -44,7 +44,7 @@ describe("DefaultSetPasswordJitService", () => { beforeEach(() => { apiService = mock(); - cryptoService = mock(); + keyService = mock(); encryptService = mock(); i18nService = mock(); kdfConfigService = mock(); @@ -55,7 +55,7 @@ describe("DefaultSetPasswordJitService", () => { sut = new DefaultSetPasswordJitService( apiService, - cryptoService, + keyService, encryptService, i18nService, kdfConfigService, @@ -141,14 +141,14 @@ describe("DefaultSetPasswordJitService", () => { function setupSetPasswordMocks(hasUserKey = true) { if (!hasUserKey) { - cryptoService.userKey$.mockReturnValue(of(null)); - cryptoService.makeUserKey.mockResolvedValue(protectedUserKey); + keyService.userKey$.mockReturnValue(of(null)); + keyService.makeUserKey.mockResolvedValue(protectedUserKey); } else { - cryptoService.userKey$.mockReturnValue(of(userKey)); - cryptoService.encryptUserKeyWithMasterKey.mockResolvedValue(protectedUserKey); + keyService.userKey$.mockReturnValue(of(userKey)); + keyService.encryptUserKeyWithMasterKey.mockResolvedValue(protectedUserKey); } - cryptoService.makeKeyPair.mockResolvedValue(keyPair); + keyService.makeKeyPair.mockResolvedValue(keyPair); apiService.setPassword.mockResolvedValue(undefined); masterPasswordService.setForceSetPasswordReason.mockResolvedValue(undefined); @@ -156,9 +156,9 @@ describe("DefaultSetPasswordJitService", () => { userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true })); userDecryptionOptionsService.setUserDecryptionOptions.mockResolvedValue(undefined); kdfConfigService.setKdfConfig.mockResolvedValue(undefined); - cryptoService.setUserKey.mockResolvedValue(undefined); + keyService.setUserKey.mockResolvedValue(undefined); - cryptoService.setPrivateKey.mockResolvedValue(undefined); + keyService.setPrivateKey.mockResolvedValue(undefined); masterPasswordService.setMasterKeyHash.mockResolvedValue(undefined); } @@ -171,7 +171,7 @@ describe("DefaultSetPasswordJitService", () => { return; } - cryptoService.userKey$.mockReturnValue(of(userKey)); + keyService.userKey$.mockReturnValue(of(userKey)); encryptService.rsaEncrypt.mockResolvedValue(userKeyEncString); organizationUserApiService.putOrganizationUserResetPasswordEnrollment.mockResolvedValue( diff --git a/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.ts b/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.ts index 1993877966f..76477a0e5df 100644 --- a/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.ts +++ b/libs/auth/src/angular/set-password-jit/default-set-password-jit.service.ts @@ -13,13 +13,13 @@ import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/for import { PBKDF2KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request"; import { KeysRequest } from "@bitwarden/common/models/request/keys.request"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; +import { KeyService } from "@bitwarden/key-management"; import { SetPasswordCredentials, @@ -29,7 +29,7 @@ import { export class DefaultSetPasswordJitService implements SetPasswordJitService { constructor( protected apiService: ApiService, - protected cryptoService: CryptoService, + protected keyService: KeyService, protected encryptService: EncryptService, protected i18nService: I18nService, protected kdfConfigService: KdfConfigService, @@ -85,7 +85,7 @@ export class DefaultSetPasswordJitService implements SetPasswordJitService { // User now has a password so update account decryption options in state await this.updateAccountDecryptionProperties(masterKey, kdfConfig, protectedUserKey, userId); - await this.cryptoService.setPrivateKey(keyPair[1].encryptedString, userId); + await this.keyService.setPrivateKey(keyPair[1].encryptedString, userId); await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, userId); @@ -100,12 +100,12 @@ export class DefaultSetPasswordJitService implements SetPasswordJitService { ): Promise<[UserKey, EncString]> { let protectedUserKey: [UserKey, EncString] = null; - const userKey = await firstValueFrom(this.cryptoService.userKey$(userId)); + const userKey = await firstValueFrom(this.keyService.userKey$(userId)); if (userKey == null) { - protectedUserKey = await this.cryptoService.makeUserKey(masterKey); + protectedUserKey = await this.keyService.makeUserKey(masterKey); } else { - protectedUserKey = await this.cryptoService.encryptUserKeyWithMasterKey(masterKey); + protectedUserKey = await this.keyService.encryptUserKeyWithMasterKey(masterKey); } return protectedUserKey; @@ -114,7 +114,7 @@ export class DefaultSetPasswordJitService implements SetPasswordJitService { private async makeKeyPairAndRequest( protectedUserKey: [UserKey, EncString], ): Promise<[[string, EncString], KeysRequest]> { - const keyPair = await this.cryptoService.makeKeyPair(protectedUserKey[0]); + const keyPair = await this.keyService.makeKeyPair(protectedUserKey[0]); if (keyPair == null) { throw new Error("keyPair not found. Could not set password."); } @@ -136,7 +136,7 @@ export class DefaultSetPasswordJitService implements SetPasswordJitService { await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts); await this.kdfConfigService.setKdfConfig(userId, kdfConfig); await this.masterPasswordService.setMasterKey(masterKey, userId); - await this.cryptoService.setUserKey(protectedUserKey[0], userId); + await this.keyService.setUserKey(protectedUserKey[0], userId); } private async handleResetPasswordAutoEnroll( @@ -153,7 +153,7 @@ export class DefaultSetPasswordJitService implements SetPasswordJitService { const publicKey = Utils.fromB64ToArray(organizationKeys.publicKey); // RSA Encrypt user key with organization public key - const userKey = await firstValueFrom(this.cryptoService.userKey$(userId)); + const userKey = await firstValueFrom(this.keyService.userKey$(userId)); if (userKey == null) { throw new Error("userKey not found. Could not handle reset password auto enroll."); diff --git a/libs/auth/src/common/abstractions/index.ts b/libs/auth/src/common/abstractions/index.ts index 6b618992e9d..e686de52013 100644 --- a/libs/auth/src/common/abstractions/index.ts +++ b/libs/auth/src/common/abstractions/index.ts @@ -3,4 +3,3 @@ export * from "./login-email.service"; export * from "./login-strategy.service"; export * from "./user-decryption-options.service.abstraction"; export * from "./auth-request.service.abstraction"; -export * from "./user-key-rotation-data-provider.abstraction"; diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts index efc6da51d9f..c0e7d2c00ae 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts @@ -11,7 +11,6 @@ import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/maste import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -24,6 +23,7 @@ import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/sp import { CsprngArray } from "@bitwarden/common/types/csprng"; import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; +import { KeyService } from "@bitwarden/key-management"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; import { AuthRequestLoginCredentials } from "../models/domain/login-credentials"; @@ -37,7 +37,7 @@ import { identityTokenResponseFactory } from "./login.strategy.spec"; describe("AuthRequestLoginStrategy", () => { let cache: AuthRequestLoginStrategyData; - let cryptoService: MockProxy; + let keyService: MockProxy; let encryptService: MockProxy; let apiService: MockProxy; let tokenService: MockProxy; @@ -73,7 +73,7 @@ describe("AuthRequestLoginStrategy", () => { const decMasterKeyHash = "LOCAL_PASSWORD_HASH"; beforeEach(async () => { - cryptoService = mock(); + keyService = mock(); apiService = mock(); tokenService = mock(); appIdService = mock(); @@ -102,7 +102,7 @@ describe("AuthRequestLoginStrategy", () => { deviceTrustService, accountService, masterPasswordService, - cryptoService, + keyService, encryptService, apiService, tokenService, @@ -161,13 +161,13 @@ describe("AuthRequestLoginStrategy", () => { decMasterKeyHash, mockUserId, ); - expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith( + expect(keyService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith( tokenResponse.key, mockUserId, ); - expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey, mockUserId); + expect(keyService.setUserKey).toHaveBeenCalledWith(userKey, mockUserId); expect(deviceTrustService.trustDeviceIfRequired).toHaveBeenCalled(); - expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, mockUserId); + expect(keyService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, mockUserId); }); it("sets keys after a successful authentication when only userKey provided in login credentials", async () => { @@ -189,12 +189,12 @@ describe("AuthRequestLoginStrategy", () => { expect(masterPasswordService.mock.setMasterKeyHash).not.toHaveBeenCalled(); // setMasterKeyEncryptedUserKey, setUserKey, and setPrivateKey should still be called - expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith( + expect(keyService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith( tokenResponse.key, mockUserId, ); - expect(cryptoService.setUserKey).toHaveBeenCalledWith(decUserKey, mockUserId); - expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, mockUserId); + expect(keyService.setUserKey).toHaveBeenCalledWith(decUserKey, mockUserId); + expect(keyService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, mockUserId); // trustDeviceIfRequired should be called expect(deviceTrustService.trustDeviceIfRequired).not.toHaveBeenCalled(); diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts index ae0024d2181..3f7e107fa98 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts @@ -99,10 +99,10 @@ export class AuthRequestLoginStrategy extends LoginStrategy { const authRequestCredentials = this.cache.value.authRequestCredentials; // User now may or may not have a master password // but set the master key encrypted user key if it exists regardless - await this.cryptoService.setMasterKeyEncryptedUserKey(response.key, userId); + await this.keyService.setMasterKeyEncryptedUserKey(response.key, userId); if (authRequestCredentials.decryptedUserKey) { - await this.cryptoService.setUserKey(authRequestCredentials.decryptedUserKey, userId); + await this.keyService.setUserKey(authRequestCredentials.decryptedUserKey, userId); } else { await this.trySetUserKeyWithMasterKey(userId); @@ -115,7 +115,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy { const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); if (masterKey) { const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey); - await this.cryptoService.setUserKey(userKey, userId); + await this.keyService.setUserKey(userKey, userId); } } @@ -123,7 +123,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy { response: IdentityTokenResponse, userId: UserId, ): Promise { - await this.cryptoService.setPrivateKey( + await this.keyService.setPrivateKey( response.privateKey ?? (await this.createKeyPairForOldAccount(userId)), userId, ); diff --git a/libs/auth/src/common/login-strategies/login.strategy.spec.ts b/libs/auth/src/common/login-strategies/login.strategy.spec.ts index 35d62ca76b3..49140cc2cc0 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -21,7 +21,6 @@ import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/maste import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -39,6 +38,7 @@ import { import { CsprngArray } from "@bitwarden/common/types/csprng"; import { UserId } from "@bitwarden/common/types/guid"; import { UserKey, MasterKey } from "@bitwarden/common/types/key"; +import { KeyService } from "@bitwarden/key-management"; import { LoginStrategyServiceAbstraction } from "../abstractions"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; @@ -104,7 +104,7 @@ describe("LoginStrategy", () => { let masterPasswordService: FakeMasterPasswordService; let loginStrategyService: MockProxy; - let cryptoService: MockProxy; + let keyService: MockProxy; let encryptService: MockProxy; let apiService: MockProxy; let tokenService: MockProxy; @@ -129,7 +129,7 @@ describe("LoginStrategy", () => { masterPasswordService = new FakeMasterPasswordService(); loginStrategyService = mock(); - cryptoService = mock(); + keyService = mock(); encryptService = mock(); apiService = mock(); tokenService = mock(); @@ -158,7 +158,7 @@ describe("LoginStrategy", () => { loginStrategyService, accountService, masterPasswordService, - cryptoService, + keyService, encryptService, apiService, tokenService, @@ -321,7 +321,7 @@ describe("LoginStrategy", () => { it("makes a new public and private key for an old account", async () => { const tokenResponse = identityTokenResponseFactory(); tokenResponse.privateKey = null; - cryptoService.makeKeyPair.mockResolvedValue(["PUBLIC_KEY", new EncString("PRIVATE_KEY")]); + keyService.makeKeyPair.mockResolvedValue(["PUBLIC_KEY", new EncString("PRIVATE_KEY")]); apiService.postIdentityToken.mockResolvedValue(tokenResponse); masterPasswordService.masterKeySubject.next(masterKey); @@ -330,10 +330,10 @@ describe("LoginStrategy", () => { await passwordLoginStrategy.logIn(credentials); // User symmetric key must be set before the new RSA keypair is generated - expect(cryptoService.setUserKey).toHaveBeenCalled(); - expect(cryptoService.makeKeyPair).toHaveBeenCalled(); - expect(cryptoService.setUserKey.mock.invocationCallOrder[0]).toBeLessThan( - cryptoService.makeKeyPair.mock.invocationCallOrder[0], + expect(keyService.setUserKey).toHaveBeenCalled(); + expect(keyService.makeKeyPair).toHaveBeenCalled(); + expect(keyService.setUserKey.mock.invocationCallOrder[0]).toBeLessThan( + keyService.makeKeyPair.mock.invocationCallOrder[0], ); expect(apiService.postAccountKeys).toHaveBeenCalled(); @@ -470,7 +470,7 @@ describe("LoginStrategy", () => { loginStrategyService, accountService, masterPasswordService, - cryptoService, + keyService, encryptService, apiService, tokenService, diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index 2e881f978dc..67a286d8195 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -25,7 +25,6 @@ import { ClientType } from "@bitwarden/common/enums"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { KeysRequest } from "@bitwarden/common/models/request/keys.request"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -34,6 +33,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv import { KdfType } from "@bitwarden/common/platform/enums"; import { Account, AccountProfile } from "@bitwarden/common/platform/models/domain/account"; import { UserId } from "@bitwarden/common/types/guid"; +import { KeyService } from "@bitwarden/key-management"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; import { @@ -66,7 +66,7 @@ export abstract class LoginStrategy { constructor( protected accountService: AccountService, protected masterPasswordService: InternalMasterPasswordServiceAbstraction, - protected cryptoService: CryptoService, + protected keyService: KeyService, protected encryptService: EncryptService, protected apiService: ApiService, protected tokenService: TokenService, @@ -284,8 +284,8 @@ export abstract class LoginStrategy { protected async createKeyPairForOldAccount(userId: UserId) { try { - const userKey = await this.cryptoService.getUserKeyWithLegacySupport(userId); - const [publicKey, privateKey] = await this.cryptoService.makeKeyPair(userKey); + const userKey = await this.keyService.getUserKeyWithLegacySupport(userId); + const [publicKey, privateKey] = await this.keyService.makeKeyPair(userKey); await this.apiService.postAccountKeys(new KeysRequest(publicKey, privateKey.encryptedString)); return privateKey.encryptedString; } catch (e) { diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts index 07cbf2424ab..4da6272ccab 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts @@ -15,7 +15,6 @@ import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/maste import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -33,6 +32,7 @@ import { import { CsprngArray } from "@bitwarden/common/types/csprng"; import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; +import { KeyService } from "@bitwarden/key-management"; import { LoginStrategyServiceAbstraction } from "../abstractions"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; @@ -63,7 +63,7 @@ describe("PasswordLoginStrategy", () => { let masterPasswordService: FakeMasterPasswordService; let loginStrategyService: MockProxy; - let cryptoService: MockProxy; + let keyService: MockProxy; let encryptService: MockProxy; let apiService: MockProxy; let tokenService: MockProxy; @@ -89,7 +89,7 @@ describe("PasswordLoginStrategy", () => { masterPasswordService = new FakeMasterPasswordService(); loginStrategyService = mock(); - cryptoService = mock(); + keyService = mock(); encryptService = mock(); apiService = mock(); tokenService = mock(); @@ -113,10 +113,10 @@ describe("PasswordLoginStrategy", () => { loginStrategyService.makePreloginKey.mockResolvedValue(masterKey); - cryptoService.hashMasterKey + keyService.hashMasterKey .calledWith(masterPassword, expect.anything(), undefined) .mockResolvedValue(hashedPassword); - cryptoService.hashMasterKey + keyService.hashMasterKey .calledWith(masterPassword, expect.anything(), HashPurpose.LocalAuthorization) .mockResolvedValue(localHashedPassword); @@ -129,7 +129,7 @@ describe("PasswordLoginStrategy", () => { loginStrategyService, accountService, masterPasswordService, - cryptoService, + keyService, encryptService, apiService, tokenService, @@ -198,12 +198,9 @@ describe("PasswordLoginStrategy", () => { localHashedPassword, userId, ); - expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith( - tokenResponse.key, - userId, - ); - expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey, userId); - expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, userId); + expect(keyService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key, userId); + expect(keyService.setUserKey).toHaveBeenCalledWith(userKey, userId); + expect(keyService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, userId); }); it("does not force the user to update their master password when there are no requirements", async () => { diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.ts b/libs/auth/src/common/login-strategies/password-login.strategy.ts index 7f73898ff6e..55e869e8229 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.ts @@ -83,15 +83,12 @@ export class PasswordLoginStrategy extends LoginStrategy { data.userEnteredEmail = email; // Hash the password early (before authentication) so we don't persist it in memory in plaintext - data.localMasterKeyHash = await this.cryptoService.hashMasterKey( + data.localMasterKeyHash = await this.keyService.hashMasterKey( masterPassword, data.masterKey, HashPurpose.LocalAuthorization, ); - const serverMasterKeyHash = await this.cryptoService.hashMasterKey( - masterPassword, - data.masterKey, - ); + const serverMasterKeyHash = await this.keyService.hashMasterKey(masterPassword, data.masterKey); data.tokenRequest = new PasswordTokenRequest( email, @@ -182,12 +179,12 @@ export class PasswordLoginStrategy extends LoginStrategy { if (this.encryptionKeyMigrationRequired(response)) { return; } - await this.cryptoService.setMasterKeyEncryptedUserKey(response.key, userId); + await this.keyService.setMasterKeyEncryptedUserKey(response.key, userId); const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); if (masterKey) { const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey); - await this.cryptoService.setUserKey(userKey, userId); + await this.keyService.setUserKey(userKey, userId); } } @@ -195,7 +192,7 @@ export class PasswordLoginStrategy extends LoginStrategy { response: IdentityTokenResponse, userId: UserId, ): Promise { - await this.cryptoService.setPrivateKey( + await this.keyService.setPrivateKey( response.privateKey ?? (await this.createKeyPairForOldAccount(userId)), userId, ); diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts index f5de10766c0..d9827c2e287 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts @@ -16,7 +16,6 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -30,6 +29,7 @@ import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/sp import { CsprngArray } from "@bitwarden/common/types/csprng"; import { UserId } from "@bitwarden/common/types/guid"; import { DeviceKey, UserKey, MasterKey } from "@bitwarden/common/types/key"; +import { KeyService } from "@bitwarden/key-management"; import { AuthRequestServiceAbstraction, @@ -44,7 +44,7 @@ describe("SsoLoginStrategy", () => { let accountService: FakeAccountService; let masterPasswordService: FakeMasterPasswordService; - let cryptoService: MockProxy; + let keyService: MockProxy; let encryptService: MockProxy; let apiService: MockProxy; let tokenService: MockProxy; @@ -79,7 +79,7 @@ describe("SsoLoginStrategy", () => { accountService = mockAccountServiceWith(userId); masterPasswordService = new FakeMasterPasswordService(); - cryptoService = mock(); + keyService = mock(); encryptService = mock(); apiService = mock(); tokenService = mock(); @@ -127,7 +127,7 @@ describe("SsoLoginStrategy", () => { i18nService, accountService, masterPasswordService, - cryptoService, + keyService, encryptService, apiService, tokenService, @@ -174,8 +174,8 @@ describe("SsoLoginStrategy", () => { await ssoLoginStrategy.logIn(credentials); expect(masterPasswordService.mock.setMasterKey).not.toHaveBeenCalled(); - expect(cryptoService.setUserKey).not.toHaveBeenCalled(); - expect(cryptoService.setPrivateKey).not.toHaveBeenCalled(); + expect(keyService.setUserKey).not.toHaveBeenCalled(); + expect(keyService.setPrivateKey).not.toHaveBeenCalled(); }); it("sets master key encrypted user key for existing SSO users", async () => { @@ -187,11 +187,8 @@ describe("SsoLoginStrategy", () => { await ssoLoginStrategy.logIn(credentials); // Assert - expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledTimes(1); - expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith( - tokenResponse.key, - userId, - ); + expect(keyService.setMasterKeyEncryptedUserKey).toHaveBeenCalledTimes(1); + expect(keyService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key, userId); }); describe("Trusted Device Decryption", () => { @@ -247,7 +244,7 @@ describe("SsoLoginStrategy", () => { deviceTrustService.getDeviceKey.mockResolvedValue(mockDeviceKey); deviceTrustService.decryptUserKeyWithDeviceKey.mockResolvedValue(mockUserKey); - const cryptoSvcSetUserKeySpy = jest.spyOn(cryptoService, "setUserKey"); + const cryptoSvcSetUserKeySpy = jest.spyOn(keyService, "setUserKey"); // Act await ssoLoginStrategy.logIn(credentials); @@ -274,7 +271,7 @@ describe("SsoLoginStrategy", () => { await ssoLoginStrategy.logIn(credentials); // Assert - expect(cryptoService.setUserKey).not.toHaveBeenCalled(); + expect(keyService.setUserKey).not.toHaveBeenCalled(); }); describe.each([ @@ -295,7 +292,7 @@ describe("SsoLoginStrategy", () => { await ssoLoginStrategy.logIn(credentials); // Assert - expect(cryptoService.setUserKey).not.toHaveBeenCalled(); + expect(keyService.setUserKey).not.toHaveBeenCalled(); }); }); @@ -314,7 +311,7 @@ describe("SsoLoginStrategy", () => { await ssoLoginStrategy.logIn(credentials); // Assert - expect(cryptoService.setUserKey).not.toHaveBeenCalled(); + expect(keyService.setUserKey).not.toHaveBeenCalled(); }); it("logs when a device key is found but no decryption keys were recieved in token response", async () => { @@ -365,7 +362,7 @@ describe("SsoLoginStrategy", () => { it("sets the user key using master key and hash from approved admin request if exists", async () => { apiService.postIdentityToken.mockResolvedValue(tokenResponse); - cryptoService.hasUserKey.mockResolvedValue(true); + keyService.hasUserKey.mockResolvedValue(true); const adminAuthResponse = { id: "1", publicKey: "PRIVATE" as any, @@ -383,7 +380,7 @@ describe("SsoLoginStrategy", () => { it("sets the user key from approved admin request if exists", async () => { apiService.postIdentityToken.mockResolvedValue(tokenResponse); - cryptoService.hasUserKey.mockResolvedValue(true); + keyService.hasUserKey.mockResolvedValue(true); const adminAuthResponse = { id: "1", publicKey: "PRIVATE" as any, @@ -400,7 +397,7 @@ describe("SsoLoginStrategy", () => { it("attempts to establish a trusted device if successful", async () => { apiService.postIdentityToken.mockResolvedValue(tokenResponse); - cryptoService.hasUserKey.mockResolvedValue(true); + keyService.hasUserKey.mockResolvedValue(true); const adminAuthResponse = { id: "1", publicKey: "PRIVATE" as any, @@ -438,7 +435,7 @@ describe("SsoLoginStrategy", () => { requestApproved: true, }; apiService.getAuthRequest.mockResolvedValue(adminAuthResponse as AuthRequestResponse); - cryptoService.hasUserKey.mockResolvedValue(false); + keyService.hasUserKey.mockResolvedValue(false); deviceTrustService.getDeviceKey.mockResolvedValue("DEVICE_KEY" as any); await ssoLoginStrategy.logIn(credentials); @@ -502,7 +499,7 @@ describe("SsoLoginStrategy", () => { undefined, undefined, ); - expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey, userId); + expect(keyService.setUserKey).toHaveBeenCalledWith(userKey, userId); }); }); @@ -558,7 +555,7 @@ describe("SsoLoginStrategy", () => { undefined, undefined, ); - expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey, userId); + expect(keyService.setUserKey).toHaveBeenCalledWith(userKey, userId); }); }); }); diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.ts index 5ddf7428d24..d2660eef8a2 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.ts @@ -192,7 +192,7 @@ export class SsoLoginStrategy extends LoginStrategy { if (masterKeyEncryptedUserKey) { // set the master key encrypted user key if it exists - await this.cryptoService.setMasterKeyEncryptedUserKey(masterKeyEncryptedUserKey, userId); + await this.keyService.setMasterKeyEncryptedUserKey(masterKeyEncryptedUserKey, userId); } const userDecryptionOptions = tokenResponse?.userDecryptionOptions; @@ -205,7 +205,7 @@ export class SsoLoginStrategy extends LoginStrategy { // Using it will clear it from state and future requests will use the device key. await this.trySetUserKeyWithApprovedAdminRequestIfExists(userId); - const hasUserKey = await this.cryptoService.hasUserKey(userId); + const hasUserKey = await this.keyService.hasUserKey(userId); // Only try to set user key with device key if admin approval request was not successful. if (!hasUserKey) { @@ -267,7 +267,7 @@ export class SsoLoginStrategy extends LoginStrategy { ); } - if (await this.cryptoService.hasUserKey()) { + if (await this.keyService.hasUserKey()) { // Now that we have a decrypted user key in memory, we can check if we // need to establish trust on the current device await this.deviceTrustService.trustDeviceIfRequired(userId); @@ -323,7 +323,7 @@ export class SsoLoginStrategy extends LoginStrategy { ); if (userKey) { - await this.cryptoService.setUserKey(userKey, userId); + await this.keyService.setUserKey(userKey, userId); } } @@ -339,7 +339,7 @@ export class SsoLoginStrategy extends LoginStrategy { } const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey); - await this.cryptoService.setUserKey(userKey, userId); + await this.keyService.setUserKey(userKey, userId); } protected override async setPrivateKey( @@ -349,7 +349,7 @@ export class SsoLoginStrategy extends LoginStrategy { const newSsoUser = tokenResponse.key == null; if (!newSsoUser) { - await this.cryptoService.setPrivateKey( + await this.keyService.setPrivateKey( tokenResponse.privateKey ?? (await this.createKeyPairForOldAccount(userId)), userId, ); diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts index d299a8e0ced..14fafcb58c3 100644 --- a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts @@ -10,7 +10,6 @@ import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/maste import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { Environment, @@ -27,6 +26,7 @@ import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/sp import { CsprngArray } from "@bitwarden/common/types/csprng"; import { UserId } from "@bitwarden/common/types/guid"; import { UserKey, MasterKey } from "@bitwarden/common/types/key"; +import { KeyService } from "@bitwarden/key-management"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; import { UserApiLoginCredentials } from "../models/domain/login-credentials"; @@ -39,7 +39,7 @@ describe("UserApiLoginStrategy", () => { let accountService: FakeAccountService; let masterPasswordService: FakeMasterPasswordService; - let cryptoService: MockProxy; + let keyService: MockProxy; let encryptService: MockProxy; let apiService: MockProxy; let tokenService: MockProxy; @@ -72,7 +72,7 @@ describe("UserApiLoginStrategy", () => { accountService = mockAccountServiceWith(userId); masterPasswordService = new FakeMasterPasswordService(); - cryptoService = mock(); + keyService = mock(); apiService = mock(); tokenService = mock(); appIdService = mock(); @@ -100,7 +100,7 @@ describe("UserApiLoginStrategy", () => { keyConnectorService, accountService, masterPasswordService, - cryptoService, + keyService, encryptService, apiService, tokenService, @@ -175,11 +175,8 @@ describe("UserApiLoginStrategy", () => { await apiLogInStrategy.logIn(credentials); - expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith( - tokenResponse.key, - userId, - ); - expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, userId); + expect(keyService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key, userId); + expect(keyService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, userId); }); it("gets and sets the master key if Key Connector is enabled", async () => { @@ -219,6 +216,6 @@ describe("UserApiLoginStrategy", () => { undefined, undefined, ); - expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey, userId); + expect(keyService.setUserKey).toHaveBeenCalledWith(userKey, userId); }); }); diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.ts index 3b112c79a0f..4ae95fdbc70 100644 --- a/libs/auth/src/common/login-strategies/user-api-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.ts @@ -64,13 +64,13 @@ export class UserApiLoginStrategy extends LoginStrategy { response: IdentityTokenResponse, userId: UserId, ): Promise { - await this.cryptoService.setMasterKeyEncryptedUserKey(response.key, userId); + await this.keyService.setMasterKeyEncryptedUserKey(response.key, userId); if (response.apiUseKeyConnector) { const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); if (masterKey) { const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey); - await this.cryptoService.setUserKey(userKey, userId); + await this.keyService.setUserKey(userKey, userId); } } } @@ -79,7 +79,7 @@ export class UserApiLoginStrategy extends LoginStrategy { response: IdentityTokenResponse, userId: UserId, ): Promise { - await this.cryptoService.setPrivateKey( + await this.keyService.setPrivateKey( response.privateKey ?? (await this.createKeyPairForOldAccount(userId)), userId, ); diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts index e4b1f740310..88392b57c53 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts @@ -13,7 +13,6 @@ import { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/se import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -25,6 +24,7 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-ti import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; import { PrfKey, UserKey } from "@bitwarden/common/types/key"; +import { KeyService } from "@bitwarden/key-management"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; import { WebAuthnLoginCredentials } from "../models/domain/login-credentials"; @@ -37,7 +37,7 @@ describe("WebAuthnLoginStrategy", () => { let accountService: FakeAccountService; let masterPasswordService: FakeMasterPasswordService; - let cryptoService!: MockProxy; + let keyService!: MockProxy; let encryptService!: MockProxy; let apiService!: MockProxy; let tokenService!: MockProxy; @@ -80,7 +80,7 @@ describe("WebAuthnLoginStrategy", () => { accountService = mockAccountServiceWith(userId); masterPasswordService = new FakeMasterPasswordService(); - cryptoService = mock(); + keyService = mock(); encryptService = mock(); apiService = mock(); tokenService = mock(); @@ -105,7 +105,7 @@ describe("WebAuthnLoginStrategy", () => { cache, accountService, masterPasswordService, - cryptoService, + keyService, encryptService, apiService, tokenService, @@ -233,8 +233,8 @@ describe("WebAuthnLoginStrategy", () => { // Assert // Master key encrypted user key should be set - expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledTimes(1); - expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith( + expect(keyService.setMasterKeyEncryptedUserKey).toHaveBeenCalledTimes(1); + expect(keyService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith( idTokenResponse.key, userId, ); @@ -249,8 +249,8 @@ describe("WebAuthnLoginStrategy", () => { idTokenResponse.userDecryptionOptions.webAuthnPrfOption.encryptedUserKey, mockPrfPrivateKey, ); - expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockUserKey, userId); - expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(idTokenResponse.privateKey, userId); + expect(keyService.setUserKey).toHaveBeenCalledWith(mockUserKey, userId); + expect(keyService.setPrivateKey).toHaveBeenCalledWith(idTokenResponse.privateKey, userId); // Master key and private key should not be set expect(masterPasswordService.mock.setMasterKey).not.toHaveBeenCalled(); @@ -274,7 +274,7 @@ describe("WebAuthnLoginStrategy", () => { // Assert expect(encryptService.decryptToBytes).not.toHaveBeenCalled(); expect(encryptService.rsaDecrypt).not.toHaveBeenCalled(); - expect(cryptoService.setUserKey).not.toHaveBeenCalled(); + expect(keyService.setUserKey).not.toHaveBeenCalled(); }); describe.each([ @@ -294,7 +294,7 @@ describe("WebAuthnLoginStrategy", () => { await webAuthnLoginStrategy.logIn(webAuthnCredentials); // Assert - expect(cryptoService.setUserKey).not.toHaveBeenCalled(); + expect(keyService.setUserKey).not.toHaveBeenCalled(); }); }); @@ -313,7 +313,7 @@ describe("WebAuthnLoginStrategy", () => { await webAuthnLoginStrategy.logIn(webAuthnCredentials); // Assert - expect(cryptoService.setUserKey).not.toHaveBeenCalled(); + expect(keyService.setUserKey).not.toHaveBeenCalled(); }); it("does not set the user key when the encrypted user key decryption fails", async () => { @@ -331,7 +331,7 @@ describe("WebAuthnLoginStrategy", () => { await webAuthnLoginStrategy.logIn(webAuthnCredentials); // Assert - expect(cryptoService.setUserKey).not.toHaveBeenCalled(); + expect(keyService.setUserKey).not.toHaveBeenCalled(); }); }); diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts index c5451d13df5..df671080986 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts @@ -66,7 +66,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy { if (masterKeyEncryptedUserKey) { // set the master key encrypted user key if it exists - await this.cryptoService.setMasterKeyEncryptedUserKey(masterKeyEncryptedUserKey, userId); + await this.keyService.setMasterKeyEncryptedUserKey(masterKeyEncryptedUserKey, userId); } const userDecryptionOptions = idTokenResponse?.userDecryptionOptions; @@ -93,7 +93,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy { ); if (userKey) { - await this.cryptoService.setUserKey(new SymmetricCryptoKey(userKey) as UserKey, userId); + await this.keyService.setUserKey(new SymmetricCryptoKey(userKey) as UserKey, userId); } } } @@ -102,7 +102,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy { response: IdentityTokenResponse, userId: UserId, ): Promise { - await this.cryptoService.setPrivateKey( + await this.keyService.setPrivateKey( response.privateKey ?? (await this.createKeyPairForOldAccount(userId)), userId, ); diff --git a/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts b/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts index 58dbae6d789..a4f1d5d9724 100644 --- a/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts +++ b/libs/auth/src/common/services/auth-request/auth-request.service.spec.ts @@ -5,7 +5,6 @@ import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; @@ -14,6 +13,7 @@ import { StateProvider } from "@bitwarden/common/platform/state"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; +import { KeyService } from "@bitwarden/key-management"; import { AuthRequestService } from "./auth-request.service"; @@ -24,7 +24,7 @@ describe("AuthRequestService", () => { let accountService: FakeAccountService; let masterPasswordService: FakeMasterPasswordService; const appIdService = mock(); - const cryptoService = mock(); + const keyService = mock(); const encryptService = mock(); const apiService = mock(); @@ -41,7 +41,7 @@ describe("AuthRequestService", () => { appIdService, accountService, masterPasswordService, - cryptoService, + keyService, encryptService, apiService, stateProvider, @@ -115,7 +115,7 @@ describe("AuthRequestService", () => { }); it("should use the user key if the master key and hash do not exist", async () => { - cryptoService.getUserKey.mockResolvedValueOnce({ key: new Uint8Array(64) } as UserKey); + keyService.getUserKey.mockResolvedValueOnce({ key: new Uint8Array(64) } as UserKey); await sut.approveOrDenyAuthRequest( true, @@ -135,7 +135,7 @@ describe("AuthRequestService", () => { const mockDecryptedUserKey = {} as UserKey; jest.spyOn(sut, "decryptPubKeyEncryptedUserKey").mockResolvedValueOnce(mockDecryptedUserKey); - cryptoService.setUserKey.mockResolvedValueOnce(undefined); + keyService.setUserKey.mockResolvedValueOnce(undefined); // Act await sut.setUserKeyAfterDecryptingSharedUserKey( @@ -149,7 +149,7 @@ describe("AuthRequestService", () => { mockAuthReqResponse.key, mockPrivateKey, ); - expect(cryptoService.setUserKey).toBeCalledWith(mockDecryptedUserKey, mockUserId); + expect(keyService.setUserKey).toBeCalledWith(mockDecryptedUserKey, mockUserId); }); }); @@ -175,7 +175,7 @@ describe("AuthRequestService", () => { masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue( mockDecryptedUserKey, ); - cryptoService.setUserKey.mockResolvedValueOnce(undefined); + keyService.setUserKey.mockResolvedValueOnce(undefined); // Act await sut.setKeysAfterDecryptingSharedMasterKeyAndHash( @@ -203,7 +203,7 @@ describe("AuthRequestService", () => { undefined, undefined, ); - expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockDecryptedUserKey, mockUserId); + expect(keyService.setUserKey).toHaveBeenCalledWith(mockDecryptedUserKey, mockUserId); }); }); diff --git a/libs/auth/src/common/services/auth-request/auth-request.service.ts b/libs/auth/src/common/services/auth-request/auth-request.service.ts index 51926d65983..0e416a4a255 100644 --- a/libs/auth/src/common/services/auth-request/auth-request.service.ts +++ b/libs/auth/src/common/services/auth-request/auth-request.service.ts @@ -9,7 +9,6 @@ import { PasswordlessAuthRequest } from "@bitwarden/common/auth/models/request/p import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; @@ -21,6 +20,7 @@ import { } from "@bitwarden/common/platform/state"; import { UserId } from "@bitwarden/common/types/guid"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; +import { KeyService } from "@bitwarden/key-management"; import { AuthRequestServiceAbstraction } from "../../abstractions/auth-request.service.abstraction"; @@ -45,7 +45,7 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { private appIdService: AppIdService, private accountService: AccountService, private masterPasswordService: InternalMasterPasswordServiceAbstraction, - private cryptoService: CryptoService, + private keyService: KeyService, private encryptService: EncryptService, private apiService: ApiService, private stateProvider: StateProvider, @@ -111,7 +111,7 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { ); keyToEncrypt = masterKey.encKey; } else { - const userKey = await this.cryptoService.getUserKey(); + const userKey = await this.keyService.getUserKey(); keyToEncrypt = userKey.key; } @@ -135,7 +135,7 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { authReqResponse.key, authReqPrivateKey, ); - await this.cryptoService.setUserKey(userKey, userId); + await this.keyService.setUserKey(userKey, userId); } async setKeysAfterDecryptingSharedMasterKeyAndHash( @@ -156,7 +156,7 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { await this.masterPasswordService.setMasterKey(masterKey, userId); await this.masterPasswordService.setMasterKeyHash(masterKeyHash, userId); - await this.cryptoService.setUserKey(userKey, userId); + await this.keyService.setUserKey(userKey, userId); } // Decryption helpers @@ -203,6 +203,6 @@ export class AuthRequestService implements AuthRequestServiceAbstraction { } async getFingerprintPhrase(email: string, publicKey: Uint8Array): Promise { - return (await this.cryptoService.getFingerprint(email.toLowerCase(), publicKey)).join("-"); + return (await this.keyService.getFingerprint(email.toLowerCase(), publicKey)).join("-"); } } diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts index 14662bb4b89..8647260ce5a 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts @@ -20,7 +20,6 @@ import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/maste import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -38,6 +37,7 @@ import { } from "@bitwarden/common/spec"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { UserId } from "@bitwarden/common/types/guid"; +import { KeyService } from "@bitwarden/key-management"; import { AuthRequestServiceAbstraction, @@ -54,7 +54,7 @@ describe("LoginStrategyService", () => { let accountService: FakeAccountService; let masterPasswordService: FakeMasterPasswordService; - let cryptoService: MockProxy; + let keyService: MockProxy; let apiService: MockProxy; let tokenService: MockProxy; let appIdService: MockProxy; @@ -85,7 +85,7 @@ describe("LoginStrategyService", () => { beforeEach(() => { accountService = mockAccountServiceWith(userId); masterPasswordService = new FakeMasterPasswordService(); - cryptoService = mock(); + keyService = mock(); apiService = mock(); tokenService = mock(); appIdService = mock(); @@ -112,7 +112,7 @@ describe("LoginStrategyService", () => { sut = new LoginStrategyService( accountService, masterPasswordService, - cryptoService, + keyService, apiService, tokenService, appIdService, diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts index 35f2b90bbda..721ee984974 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts @@ -29,7 +29,6 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs import { PreloginRequest } from "@bitwarden/common/models/request/prelogin.request"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -43,6 +42,7 @@ import { GlobalState, GlobalStateProvider } from "@bitwarden/common/platform/sta import { DeviceTrustServiceAbstraction } from "@bitwarden/common/src/auth/abstractions/device-trust.service.abstraction"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { MasterKey } from "@bitwarden/common/types/key"; +import { KeyService } from "@bitwarden/key-management"; import { AuthRequestServiceAbstraction, LoginStrategyServiceAbstraction } from "../../abstractions"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../../abstractions/user-decryption-options.service.abstraction"; @@ -91,7 +91,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { constructor( protected accountService: AccountService, protected masterPasswordService: InternalMasterPasswordServiceAbstraction, - protected cryptoService: CryptoService, + protected keyService: KeyService, protected apiService: ApiService, protected tokenService: TokenService, protected appIdService: AppIdService, @@ -267,7 +267,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { kdfConfig.validateKdfConfigForPrelogin(); - return await this.cryptoService.makeMasterKey(masterPassword, email, kdfConfig); + return await this.keyService.makeMasterKey(masterPassword, email, kdfConfig); } private async clearCache(): Promise { @@ -319,7 +319,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { const sharedDeps: ConstructorParameters = [ this.accountService, this.masterPasswordService, - this.cryptoService, + this.keyService, this.encryptService, this.apiService, this.tokenService, diff --git a/libs/common/spec/matchers/index.ts b/libs/common/spec/matchers/index.ts index 235f54d7754..44440be5b54 100644 --- a/libs/common/spec/matchers/index.ts +++ b/libs/common/spec/matchers/index.ts @@ -1,3 +1,5 @@ +import * as matchers from "jest-extended"; + import { toBeFulfilled, toBeResolved, toBeRejected } from "./promise-fulfilled"; import { toAlmostEqual } from "./to-almost-equal"; import { toEqualBuffer } from "./to-equal-buffer"; @@ -6,6 +8,9 @@ export * from "./to-equal-buffer"; export * from "./to-almost-equal"; export * from "./promise-fulfilled"; +// add all jest-extended matchers +expect.extend(matchers); + export function addCustomMatchers() { expect.extend({ toEqualBuffer: toEqualBuffer, diff --git a/libs/common/spec/utils.ts b/libs/common/spec/utils.ts index d3722329370..1cead2aa624 100644 --- a/libs/common/spec/utils.ts +++ b/libs/common/spec/utils.ts @@ -46,8 +46,15 @@ export function makeStaticByteArray(length: number, start = 0) { return arr; } -export function makeSymmetricCryptoKey(length: 32 | 64 = 64) { - return new SymmetricCryptoKey(makeStaticByteArray(length)) as T; +/** + * Creates a symmetric crypto key for use in tests. This is deterministic, i.e. it will produce identical keys + * for identical argument values. Provide a unique value to the `seed` parameter to create different keys. + */ +export function makeSymmetricCryptoKey( + length: 32 | 64 = 64, + seed = 0, +) { + return new SymmetricCryptoKey(makeStaticByteArray(length, seed)) as T; } /** diff --git a/libs/common/src/admin-console/models/response/policy.response.ts b/libs/common/src/admin-console/models/response/policy.response.ts index 25a1f208a0b..0544cd996f4 100644 --- a/libs/common/src/admin-console/models/response/policy.response.ts +++ b/libs/common/src/admin-console/models/response/policy.response.ts @@ -8,6 +8,7 @@ export class PolicyResponse extends BaseResponse { type: PolicyType; data: any; enabled: boolean; + canToggleState: boolean; constructor(response: any) { super(response); @@ -16,5 +17,6 @@ export class PolicyResponse extends BaseResponse { this.type = this.getResponseProperty("Type"); this.data = this.getResponseProperty("Data"); this.enabled = this.getResponseProperty("Enabled"); + this.canToggleState = this.getResponseProperty("CanToggleState") ?? true; } } diff --git a/libs/common/src/auth/abstractions/kdf-config.service.ts b/libs/common/src/auth/abstractions/kdf-config.service.ts index 6b41979e1b9..f4ffe31baa4 100644 --- a/libs/common/src/auth/abstractions/kdf-config.service.ts +++ b/libs/common/src/auth/abstractions/kdf-config.service.ts @@ -1,7 +1,10 @@ +import { Observable } from "rxjs"; + import { UserId } from "../../types/guid"; import { KdfConfig } from "../models/domain/kdf-config"; export abstract class KdfConfigService { - setKdfConfig: (userId: UserId, KdfConfig: KdfConfig) => Promise; - getKdfConfig: () => Promise; + abstract setKdfConfig(userId: UserId, KdfConfig: KdfConfig): Promise; + abstract getKdfConfig(): Promise; + abstract getKdfConfig$(userId: UserId): Observable; } diff --git a/libs/common/src/auth/abstractions/webauthn/webauthn-login-prf-crypto.service.abstraction.ts b/libs/common/src/auth/abstractions/webauthn/webauthn-login-prf-key.service.abstraction.ts similarity index 88% rename from libs/common/src/auth/abstractions/webauthn/webauthn-login-prf-crypto.service.abstraction.ts rename to libs/common/src/auth/abstractions/webauthn/webauthn-login-prf-key.service.abstraction.ts index b2c59e76afb..c3c09466091 100644 --- a/libs/common/src/auth/abstractions/webauthn/webauthn-login-prf-crypto.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/webauthn/webauthn-login-prf-key.service.abstraction.ts @@ -3,7 +3,7 @@ import { PrfKey } from "../../../types/key"; /** * Contains methods for all crypto operations specific to the WebAuthn login flow. */ -export abstract class WebAuthnLoginPrfCryptoServiceAbstraction { +export abstract class WebAuthnLoginPrfKeyServiceAbstraction { /** * Get the salt used to generate the PRF-output used when logging in with WebAuthn. */ diff --git a/libs/common/src/auth/models/request/registration/register-finish.request.ts b/libs/common/src/auth/models/request/registration/register-finish.request.ts index 6a36bf82139..7ffac6bfe6c 100644 --- a/libs/common/src/auth/models/request/registration/register-finish.request.ts +++ b/libs/common/src/auth/models/request/registration/register-finish.request.ts @@ -21,6 +21,8 @@ export class RegisterFinishRequest { public orgSponsoredFreeFamilyPlanToken?: string, public acceptEmergencyAccessInviteToken?: string, public acceptEmergencyAccessId?: string, + public providerInviteToken?: string, + public providerUserId?: string, // Org Invite data (only applies on web) public organizationUserId?: string, diff --git a/libs/common/src/auth/services/account.service.ts b/libs/common/src/auth/services/account.service.ts index dceec2cbf13..04a0c62dd93 100644 --- a/libs/common/src/auth/services/account.service.ts +++ b/libs/common/src/auth/services/account.service.ts @@ -45,6 +45,24 @@ const LOGGED_OUT_INFO: AccountInfo = { name: undefined, }; +/** + * An rxjs map operator that extracts the UserId from an account, or throws if the account or UserId are null. + */ +export const getUserId = map<{ id: UserId | undefined }, UserId>((account) => { + if (account?.id == null) { + throw new Error("Null account or account ID"); + } + + return account.id; +}); + +/** + * An rxjs map operator that extracts the UserId from an account, or returns undefined if the account or UserId are null. + */ +export const getOptionalUserId = map<{ id: UserId | undefined }, UserId | undefined>( + (account) => account?.id ?? undefined, +); + export class AccountServiceImplementation implements InternalAccountService { private accountsState: GlobalState>; private activeAccountIdState: GlobalState; diff --git a/libs/common/src/auth/services/auth.service.spec.ts b/libs/common/src/auth/services/auth.service.spec.ts index 9a93a4207b7..5663384714d 100644 --- a/libs/common/src/auth/services/auth.service.spec.ts +++ b/libs/common/src/auth/services/auth.service.spec.ts @@ -1,6 +1,7 @@ import { MockProxy, mock } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; +import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { FakeAccountService, makeStaticByteArray, @@ -8,7 +9,6 @@ import { trackEmissions, } from "../../../spec"; import { ApiService } from "../../abstractions/api.service"; -import { CryptoService } from "../../platform/abstractions/crypto.service"; import { MessagingService } from "../../platform/abstractions/messaging.service"; import { StateService } from "../../platform/abstractions/state.service"; import { Utils } from "../../platform/misc/utils"; @@ -25,7 +25,7 @@ describe("AuthService", () => { let accountService: FakeAccountService; let messagingService: MockProxy; - let cryptoService: MockProxy; + let keyService: MockProxy; let apiService: MockProxy; let stateService: MockProxy; let tokenService: MockProxy; @@ -36,7 +36,7 @@ describe("AuthService", () => { beforeEach(() => { accountService = mockAccountServiceWith(userId); messagingService = mock(); - cryptoService = mock(); + keyService = mock(); apiService = mock(); stateService = mock(); tokenService = mock(); @@ -44,7 +44,7 @@ describe("AuthService", () => { sut = new AuthService( accountService, messagingService, - cryptoService, + keyService, apiService, stateService, tokenService, @@ -63,7 +63,7 @@ describe("AuthService", () => { beforeEach(() => { accountService.activeAccountSubject.next(accountInfo); tokenService.hasAccessToken$.mockReturnValue(of(true)); - cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(undefined)); + keyService.getInMemoryUserKeyFor$.mockReturnValue(of(undefined)); }); it("emits LoggedOut when there is no active account", async () => { @@ -84,7 +84,7 @@ describe("AuthService", () => { it("emits LoggedOut when there is no access token but has a user key", async () => { tokenService.hasAccessToken$.mockReturnValue(of(false)); - cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(userKey)); + keyService.getInMemoryUserKeyFor$.mockReturnValue(of(userKey)); expect(await firstValueFrom(sut.activeAccountStatus$)).toEqual( AuthenticationStatus.LoggedOut, @@ -93,14 +93,14 @@ describe("AuthService", () => { it("emits Locked when there is an access token and no user key", async () => { tokenService.hasAccessToken$.mockReturnValue(of(true)); - cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(undefined)); + keyService.getInMemoryUserKeyFor$.mockReturnValue(of(undefined)); expect(await firstValueFrom(sut.activeAccountStatus$)).toEqual(AuthenticationStatus.Locked); }); it("emits Unlocked when there is an access token and user key", async () => { tokenService.hasAccessToken$.mockReturnValue(of(true)); - cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(userKey)); + keyService.getInMemoryUserKeyFor$.mockReturnValue(of(userKey)); expect(await firstValueFrom(sut.activeAccountStatus$)).toEqual(AuthenticationStatus.Unlocked); }); @@ -117,7 +117,7 @@ describe("AuthService", () => { const emissions = trackEmissions(sut.activeAccountStatus$); tokenService.hasAccessToken$.mockReturnValue(of(true)); - cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(userKey)); + keyService.getInMemoryUserKeyFor$.mockReturnValue(of(userKey)); accountService.activeAccountSubject.next(accountInfo2); expect(emissions).toEqual([AuthenticationStatus.Locked, AuthenticationStatus.Unlocked]); @@ -150,7 +150,7 @@ describe("AuthService", () => { describe("authStatusFor$", () => { beforeEach(() => { tokenService.hasAccessToken$.mockReturnValue(of(true)); - cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(undefined)); + keyService.getInMemoryUserKeyFor$.mockReturnValue(of(undefined)); }); it.each([null, undefined, "not a userId"])( @@ -172,14 +172,14 @@ describe("AuthService", () => { it("emits Locked when there is an access token and no user key", async () => { tokenService.hasAccessToken$.mockReturnValue(of(true)); - cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(undefined)); + keyService.getInMemoryUserKeyFor$.mockReturnValue(of(undefined)); expect(await firstValueFrom(sut.authStatusFor$(userId))).toEqual(AuthenticationStatus.Locked); }); it("emits Unlocked when there is an access token and user key", async () => { tokenService.hasAccessToken$.mockReturnValue(of(true)); - cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(userKey)); + keyService.getInMemoryUserKeyFor$.mockReturnValue(of(userKey)); expect(await firstValueFrom(sut.authStatusFor$(userId))).toEqual( AuthenticationStatus.Unlocked, diff --git a/libs/common/src/auth/services/auth.service.ts b/libs/common/src/auth/services/auth.service.ts index 307da55a5eb..2b8cd7919fd 100644 --- a/libs/common/src/auth/services/auth.service.ts +++ b/libs/common/src/auth/services/auth.service.ts @@ -9,8 +9,8 @@ import { switchMap, } from "rxjs"; +import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { ApiService } from "../../abstractions/api.service"; -import { CryptoService } from "../../platform/abstractions/crypto.service"; import { StateService } from "../../platform/abstractions/state.service"; import { MessageSender } from "../../platform/messaging"; import { Utils } from "../../platform/misc/utils"; @@ -27,7 +27,7 @@ export class AuthService implements AuthServiceAbstraction { constructor( protected accountService: AccountService, protected messageSender: MessageSender, - protected cryptoService: CryptoService, + protected keyService: KeyService, protected apiService: ApiService, protected stateService: StateService, private tokenService: TokenService, @@ -69,7 +69,7 @@ export class AuthService implements AuthServiceAbstraction { } return combineLatest([ - this.cryptoService.getInMemoryUserKeyFor$(userId), + this.keyService.getInMemoryUserKeyFor$(userId), this.tokenService.hasAccessToken$(userId), ]).pipe( map(([userKey, hasAccessToken]) => { diff --git a/libs/common/src/auth/services/device-trust.service.implementation.ts b/libs/common/src/auth/services/device-trust.service.implementation.ts index 178f4b06545..1738ab10bb6 100644 --- a/libs/common/src/auth/services/device-trust.service.implementation.ts +++ b/libs/common/src/auth/services/device-trust.service.implementation.ts @@ -2,10 +2,10 @@ import { firstValueFrom, map, Observable } from "rxjs"; import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; +import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { AppIdService } from "../../platform/abstractions/app-id.service"; import { ConfigService } from "../../platform/abstractions/config/config.service"; import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service"; -import { CryptoService } from "../../platform/abstractions/crypto.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { KeyGenerationService } from "../../platform/abstractions/key-generation.service"; @@ -64,7 +64,7 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { constructor( private keyGenerationService: KeyGenerationService, private cryptoFunctionService: CryptoFunctionService, - private cryptoService: CryptoService, + private keyService: KeyService, private encryptService: EncryptService, private appIdService: AppIdService, private devicesApiService: DevicesApiServiceAbstraction, @@ -124,7 +124,7 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { } // Attempt to get user key - const userKey: UserKey = await this.cryptoService.getUserKey(userId); + const userKey: UserKey = await this.keyService.getUserKey(userId); // If user key is not found, throw error if (!userKey) { @@ -175,6 +175,7 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { newUserKey: UserKey, masterPasswordHash: string, ): Promise { + this.logService.info("[Device trust rotation] Rotating device trust..."); if (!userId) { throw new Error("UserId is required. Cannot rotate device's trust."); } @@ -183,11 +184,15 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { if (currentDeviceKey == null) { // If the current device doesn't have a device key available to it, then we can't // rotate any trust at all, so early return. + this.logService.info("[Device trust rotation] No device key available to rotate trust!"); return; } // At this point of rotating their keys, they should still have their old user key in state - const oldUserKey = await firstValueFrom(this.cryptoService.userKey$(userId)); + const oldUserKey = await firstValueFrom(this.keyService.userKey$(userId)); + if (oldUserKey == newUserKey) { + this.logService.info("[Device trust rotation] Old user key is the same as the new user key."); + } const deviceIdentifier = await this.appIdService.getAppId(); const secretVerificationRequest = new SecretVerificationRequest(); @@ -229,7 +234,12 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { trustRequest.currentDevice = currentDeviceUpdateRequest; trustRequest.otherDevices = []; + this.logService.info( + "[Device trust rotation] Posting device trust update with current device:", + deviceIdentifier, + ); await this.devicesApiService.updateTrust(trustRequest, deviceIdentifier); + this.logService.info("[Device trust rotation] Device trust update posted successfully."); } async getDeviceKey(userId: UserId): Promise { diff --git a/libs/common/src/auth/services/device-trust.service.spec.ts b/libs/common/src/auth/services/device-trust.service.spec.ts index 1171ae2918a..66a91a693e5 100644 --- a/libs/common/src/auth/services/device-trust.service.spec.ts +++ b/libs/common/src/auth/services/device-trust.service.spec.ts @@ -4,6 +4,7 @@ import { BehaviorSubject, of } from "rxjs"; import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; import { UserDecryptionOptions } from "../../../../auth/src/common/models/domain/user-decryption-options"; +import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; import { FakeActiveUserState } from "../../../spec/fake-state"; import { FakeStateProvider } from "../../../spec/fake-state-provider"; @@ -11,7 +12,6 @@ import { DeviceType } from "../../enums"; import { AppIdService } from "../../platform/abstractions/app-id.service"; import { ConfigService } from "../../platform/abstractions/config/config.service"; import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service"; -import { CryptoService } from "../../platform/abstractions/crypto.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { KeyGenerationService } from "../../platform/abstractions/key-generation.service"; @@ -43,7 +43,7 @@ describe("deviceTrustService", () => { const keyGenerationService = mock(); const cryptoFunctionService = mock(); - const cryptoService = mock(); + const keyService = mock(); const encryptService = mock(); const appIdService = mock(); const devicesApiService = mock(); @@ -368,7 +368,7 @@ describe("deviceTrustService", () => { .mockResolvedValue(mockDeviceRsaKeyPair); cryptoSvcGetUserKeySpy = jest - .spyOn(cryptoService, "getUserKey") + .spyOn(keyService, "getUserKey") .mockResolvedValue(mockUserKey); cryptoSvcRsaEncryptSpy = jest @@ -623,7 +623,7 @@ describe("deviceTrustService", () => { const fakeNewUserKeyData = new Uint8Array(64); fakeNewUserKeyData.fill(FakeNewUserKeyMarker, 0, 1); fakeNewUserKey = new SymmetricCryptoKey(fakeNewUserKeyData) as UserKey; - cryptoService.userKey$.mockReturnValue(of(fakeNewUserKey)); + keyService.userKey$.mockReturnValue(of(fakeNewUserKey)); }); it("throws an error when a null user id is passed in", async () => { @@ -659,7 +659,7 @@ describe("deviceTrustService", () => { fakeOldUserKeyData.fill(FakeOldUserKeyMarker, 0, 1); // Mock the retrieval of a user key that differs from the new one passed into the method - cryptoService.userKey$.mockReturnValue( + keyService.userKey$.mockReturnValue( of(new SymmetricCryptoKey(fakeOldUserKeyData) as UserKey), ); @@ -749,7 +749,7 @@ describe("deviceTrustService", () => { return new DeviceTrustService( keyGenerationService, cryptoFunctionService, - cryptoService, + keyService, encryptService, appIdService, devicesApiService, diff --git a/libs/common/src/auth/services/kdf-config.service.ts b/libs/common/src/auth/services/kdf-config.service.ts index cfd2a3e1de0..604a186d765 100644 --- a/libs/common/src/auth/services/kdf-config.service.ts +++ b/libs/common/src/auth/services/kdf-config.service.ts @@ -1,4 +1,4 @@ -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, Observable } from "rxjs"; import { KdfType } from "../../platform/enums/kdf-type.enum"; import { KDF_CONFIG_DISK, StateProvider, UserKeyDefinition } from "../../platform/state"; @@ -38,4 +38,8 @@ export class KdfConfigService implements KdfConfigServiceAbstraction { } return state; } + + getKdfConfig$(userId: UserId): Observable { + return this.stateProvider.getUser(userId, KDF_CONFIG).state$; + } } diff --git a/libs/common/src/auth/services/key-connector.service.spec.ts b/libs/common/src/auth/services/key-connector.service.spec.ts index eb3e4cfc0e6..b1bf87693c1 100644 --- a/libs/common/src/auth/services/key-connector.service.spec.ts +++ b/libs/common/src/auth/services/key-connector.service.spec.ts @@ -1,12 +1,12 @@ import { mock } from "jest-mock-extended"; +import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec"; import { ApiService } from "../../abstractions/api.service"; import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationData } from "../../admin-console/models/data/organization.data"; import { Organization } from "../../admin-console/models/domain/organization"; import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response"; -import { CryptoService } from "../../platform/abstractions/crypto.service"; import { LogService } from "../../platform/abstractions/log.service"; import { Utils } from "../../platform/misc/utils"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; @@ -27,7 +27,7 @@ import { TokenService } from "./token.service"; describe("KeyConnectorService", () => { let keyConnectorService: KeyConnectorService; - const cryptoService = mock(); + const keyService = mock(); const apiService = mock(); const tokenService = mock(); const logService = mock(); @@ -56,7 +56,7 @@ describe("KeyConnectorService", () => { keyConnectorService = new KeyConnectorService( accountService, masterPasswordService, - cryptoService, + keyService, apiService, tokenService, logService, diff --git a/libs/common/src/auth/services/key-connector.service.ts b/libs/common/src/auth/services/key-connector.service.ts index ad9b7081cdf..111f82e6e52 100644 --- a/libs/common/src/auth/services/key-connector.service.ts +++ b/libs/common/src/auth/services/key-connector.service.ts @@ -2,12 +2,12 @@ import { firstValueFrom } from "rxjs"; import { LogoutReason } from "@bitwarden/auth/common"; +import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { ApiService } from "../../abstractions/api.service"; import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationUserType } from "../../admin-console/enums"; import { Organization } from "../../admin-console/models/domain/organization"; import { KeysRequest } from "../../models/request/keys.request"; -import { CryptoService } from "../../platform/abstractions/crypto.service"; import { KeyGenerationService } from "../../platform/abstractions/key-generation.service"; import { LogService } from "../../platform/abstractions/log.service"; import { KdfType } from "../../platform/enums/kdf-type.enum"; @@ -54,7 +54,7 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { constructor( private accountService: AccountService, private masterPasswordService: InternalMasterPasswordServiceAbstraction, - private cryptoService: CryptoService, + private keyService: KeyService, private apiService: ApiService, private tokenService: TokenService, private logService: LogService, @@ -146,7 +146,7 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { ? new PBKDF2KdfConfig(kdfIterations) : new Argon2KdfConfig(kdfIterations, kdfMemory, kdfParallelism); - const masterKey = await this.cryptoService.makeMasterKey( + const masterKey = await this.keyService.makeMasterKey( password.keyB64, await this.tokenService.getEmail(), kdfConfig, @@ -154,11 +154,11 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); await this.masterPasswordService.setMasterKey(masterKey, userId); - const userKey = await this.cryptoService.makeUserKey(masterKey); - await this.cryptoService.setUserKey(userKey[0], userId); - await this.cryptoService.setMasterKeyEncryptedUserKey(userKey[1].encryptedString, userId); + const userKey = await this.keyService.makeUserKey(masterKey); + await this.keyService.setUserKey(userKey[0], userId); + await this.keyService.setMasterKeyEncryptedUserKey(userKey[1].encryptedString, userId); - const [pubKey, privKey] = await this.cryptoService.makeKeyPair(userKey[0]); + const [pubKey, privKey] = await this.keyService.makeKeyPair(userKey[0]); try { const keyConnectorUrl = diff --git a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts index b78ef52f07f..088ce960794 100644 --- a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts +++ b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts @@ -5,9 +5,9 @@ import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { UserId } from "../../../../common/src/types/guid"; +import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationAutoEnrollStatusResponse } from "../../admin-console/models/response/organization-auto-enroll-status.response"; -import { CryptoService } from "../../platform/abstractions/crypto.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { AccountInfo, AccountService } from "../abstractions/account.service"; @@ -18,7 +18,7 @@ describe("PasswordResetEnrollmentServiceImplementation", () => { let organizationApiService: MockProxy; let accountService: MockProxy; - let cryptoService: MockProxy; + let keyService: MockProxy; let encryptService: MockProxy; let organizationUserApiService: MockProxy; let i18nService: MockProxy; @@ -28,14 +28,14 @@ describe("PasswordResetEnrollmentServiceImplementation", () => { organizationApiService = mock(); accountService = mock(); accountService.activeAccount$ = activeAccountSubject; - cryptoService = mock(); + keyService = mock(); encryptService = mock(); organizationUserApiService = mock(); i18nService = mock(); service = new PasswordResetEnrollmentServiceImplementation( organizationApiService, accountService, - cryptoService, + keyService, encryptService, organizationUserApiService, i18nService, @@ -99,7 +99,7 @@ describe("PasswordResetEnrollmentServiceImplementation", () => { }; activeAccountSubject.next(Object.assign(user1AccountInfo, { id: "userId" as UserId })); - cryptoService.getUserKey.mockResolvedValue({ key: "key" } as any); + keyService.getUserKey.mockResolvedValue({ key: "key" } as any); encryptService.rsaEncrypt.mockResolvedValue(encryptedKey as any); await service.enroll("orgId"); diff --git a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts index 7dc5f449959..9adcd0b7c17 100644 --- a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts +++ b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts @@ -6,8 +6,8 @@ import { } from "@bitwarden/admin-console/common"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction"; -import { CryptoService } from "../../platform/abstractions/crypto.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { Utils } from "../../platform/misc/utils"; import { UserKey } from "../../types/key"; @@ -20,7 +20,7 @@ export class PasswordResetEnrollmentServiceImplementation constructor( protected organizationApiService: OrganizationApiServiceAbstraction, protected accountService: AccountService, - protected cryptoService: CryptoService, + protected keyService: KeyService, protected encryptService: EncryptService, protected organizationUserApiService: OrganizationUserApiService, protected i18nService: I18nService, @@ -47,7 +47,7 @@ export class PasswordResetEnrollmentServiceImplementation userId = userId ?? (await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id)))); - userKey = userKey ?? (await this.cryptoService.getUserKey(userId)); + userKey = userKey ?? (await this.keyService.getUserKey(userId)); // RSA Encrypt user's userKey.key with organization public key const encryptedKey = await this.encryptService.rsaEncrypt(userKey.key, orgPublicKey); diff --git a/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts b/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts index 73a97cbc8bb..02cd6056efb 100644 --- a/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts +++ b/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts @@ -8,9 +8,9 @@ import { UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; +import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; import { FakeAccountService, mockAccountServiceWith } from "../../../../spec"; import { VaultTimeoutSettingsService } from "../../../abstractions/vault-timeout/vault-timeout-settings.service"; -import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; import { LogService } from "../../../platform/abstractions/log.service"; import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service"; @@ -31,7 +31,7 @@ import { UserVerificationService } from "./user-verification.service"; describe("UserVerificationService", () => { let sut: UserVerificationService; - const cryptoService = mock(); + const keyService = mock(); const masterPasswordService = mock(); const i18nService = mock(); const userVerificationApiService = mock(); @@ -50,7 +50,7 @@ describe("UserVerificationService", () => { accountService = mockAccountServiceWith(mockUserId); sut = new UserVerificationService( - cryptoService, + keyService, accountService, masterPasswordService, i18nService, @@ -132,7 +132,7 @@ describe("UserVerificationService", () => { setMasterPasswordAvailability(false); setPinAvailability("DISABLED"); vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(isBiometricsLockSet); - cryptoService.hasUserKeyStored.mockResolvedValue(isBiometricsUserKeyStored); + keyService.hasUserKeyStored.mockResolvedValue(isBiometricsUserKeyStored); platformUtilsService.supportsSecureStorage.mockReturnValue(platformSupportSecureStorage); const result = await sut.getAvailableVerificationOptions("client"); @@ -205,7 +205,7 @@ describe("UserVerificationService", () => { kdfConfigService.getKdfConfig.mockResolvedValue("kdfConfig" as unknown as KdfConfig); masterPasswordService.masterKey$.mockReturnValue(of("masterKey" as unknown as MasterKey)); - cryptoService.hashMasterKey + keyService.hashMasterKey .calledWith("password", "masterKey" as unknown as MasterKey, HashPurpose.LocalAuthorization) .mockResolvedValue("localHash"); }); @@ -216,7 +216,7 @@ describe("UserVerificationService", () => { }); it("returns if verification is successful", async () => { - cryptoService.compareAndUpdateKeyHash.mockResolvedValueOnce(true); + keyService.compareAndUpdateKeyHash.mockResolvedValueOnce(true); const result = await sut.verifyUserByMasterPassword( { @@ -227,7 +227,7 @@ describe("UserVerificationService", () => { "email", ); - expect(cryptoService.compareAndUpdateKeyHash).toHaveBeenCalled(); + expect(keyService.compareAndUpdateKeyHash).toHaveBeenCalled(); expect(masterPasswordService.setMasterKeyHash).toHaveBeenCalledWith( "localHash", mockUserId, @@ -240,7 +240,7 @@ describe("UserVerificationService", () => { }); it("throws if verification fails", async () => { - cryptoService.compareAndUpdateKeyHash.mockResolvedValueOnce(false); + keyService.compareAndUpdateKeyHash.mockResolvedValueOnce(false); await expect( sut.verifyUserByMasterPassword( @@ -253,7 +253,7 @@ describe("UserVerificationService", () => { ), ).rejects.toThrow("Invalid master password"); - expect(cryptoService.compareAndUpdateKeyHash).toHaveBeenCalled(); + expect(keyService.compareAndUpdateKeyHash).toHaveBeenCalled(); expect(masterPasswordService.setMasterKeyHash).not.toHaveBeenCalledWith(); expect(masterPasswordService.setMasterKey).not.toHaveBeenCalledWith(); }); @@ -265,7 +265,7 @@ describe("UserVerificationService", () => { }); it("returns if verification is successful", async () => { - cryptoService.hashMasterKey + keyService.hashMasterKey .calledWith( "password", "masterKey" as unknown as MasterKey, @@ -285,7 +285,7 @@ describe("UserVerificationService", () => { "email", ); - expect(cryptoService.compareAndUpdateKeyHash).not.toHaveBeenCalled(); + expect(keyService.compareAndUpdateKeyHash).not.toHaveBeenCalled(); expect(masterPasswordService.setMasterKeyHash).toHaveBeenCalledWith( "localHash", mockUserId, @@ -298,7 +298,7 @@ describe("UserVerificationService", () => { }); it("throws if verification fails", async () => { - cryptoService.hashMasterKey + keyService.hashMasterKey .calledWith( "password", "masterKey" as unknown as MasterKey, @@ -318,7 +318,7 @@ describe("UserVerificationService", () => { ), ).rejects.toThrow("Invalid master password"); - expect(cryptoService.compareAndUpdateKeyHash).not.toHaveBeenCalled(); + expect(keyService.compareAndUpdateKeyHash).not.toHaveBeenCalled(); expect(masterPasswordService.setMasterKeyHash).not.toHaveBeenCalledWith(); expect(masterPasswordService.setMasterKey).not.toHaveBeenCalledWith(); }); @@ -380,7 +380,7 @@ describe("UserVerificationService", () => { it("throws if master key cannot be created", async () => { kdfConfigService.getKdfConfig.mockResolvedValueOnce("kdfConfig" as unknown as KdfConfig); masterPasswordService.masterKey$.mockReturnValueOnce(of(null)); - cryptoService.makeMasterKey.mockResolvedValueOnce(null); + keyService.makeMasterKey.mockResolvedValueOnce(null); await expect( sut.verifyUserByMasterPassword( diff --git a/libs/common/src/auth/services/user-verification/user-verification.service.ts b/libs/common/src/auth/services/user-verification/user-verification.service.ts index 3b133891c95..b31ba59c983 100644 --- a/libs/common/src/auth/services/user-verification/user-verification.service.ts +++ b/libs/common/src/auth/services/user-verification/user-verification.service.ts @@ -3,8 +3,8 @@ import { firstValueFrom, map } from "rxjs"; import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; import { PinServiceAbstraction } from "../../../../../auth/src/common/abstractions/pin.service.abstraction"; +import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../../abstractions/vault-timeout/vault-timeout-settings.service"; -import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; import { LogService } from "../../../platform/abstractions/log.service"; import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service"; @@ -39,7 +39,7 @@ import { */ export class UserVerificationService implements UserVerificationServiceAbstraction { constructor( - private cryptoService: CryptoService, + private keyService: KeyService, private accountService: AccountService, private masterPasswordService: InternalMasterPasswordServiceAbstraction, private i18nService: I18nService, @@ -66,7 +66,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti this.hasMasterPasswordAndMasterKeyHash(userId), this.pinService.isPinDecryptionAvailable(userId), this.vaultTimeoutSettingsService.isBiometricLockSet(userId), - this.cryptoService.hasUserKeyStored(KeySuffixOptions.Biometric, userId), + this.keyService.hasUserKeyStored(KeySuffixOptions.Biometric, userId), ]); // note: we do not need to check this.platformUtilsService.supportsBiometric() because @@ -119,7 +119,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti ); let masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); if (!masterKey && !alreadyHashed) { - masterKey = await this.cryptoService.makeMasterKey( + masterKey = await this.keyService.makeMasterKey( verification.secret, email, await this.kdfConfigService.getKdfConfig(), @@ -127,7 +127,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti } request.masterPasswordHash = alreadyHashed ? verification.secret - : await this.cryptoService.hashMasterKey(verification.secret, masterKey); + : await this.keyService.hashMasterKey(verification.secret, masterKey); } return request; @@ -196,7 +196,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti let masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); if (!masterKey) { - masterKey = await this.cryptoService.makeMasterKey(verification.secret, email, kdfConfig); + masterKey = await this.keyService.makeMasterKey(verification.secret, email, kdfConfig); } if (!masterKey) { @@ -206,7 +206,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti let policyOptions: MasterPasswordPolicyResponse | null; // Client-side verification if (await this.hasMasterPasswordAndMasterKeyHash(userId)) { - const passwordValid = await this.cryptoService.compareAndUpdateKeyHash( + const passwordValid = await this.keyService.compareAndUpdateKeyHash( verification.secret, masterKey, ); @@ -217,7 +217,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti } else { // Server-side verification const request = new SecretVerificationRequest(); - const serverKeyHash = await this.cryptoService.hashMasterKey( + const serverKeyHash = await this.keyService.hashMasterKey( verification.secret, masterKey, HashPurpose.ServerAuthorization, @@ -230,7 +230,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti } } - const localKeyHash = await this.cryptoService.hashMasterKey( + const localKeyHash = await this.keyService.hashMasterKey( verification.secret, masterKey, HashPurpose.LocalAuthorization, @@ -254,7 +254,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti let userKey: UserKey; // Biometrics crashes and doesn't return a value if the user cancels the prompt try { - userKey = await this.cryptoService.getUserKeyFromStorage(KeySuffixOptions.Biometric); + userKey = await this.keyService.getUserKeyFromStorage(KeySuffixOptions.Biometric); } catch (e) { this.logService.error(`Biometrics User Verification failed: ${e.message}`); // So, any failures should be treated as a failed verification diff --git a/libs/common/src/auth/services/webauthn-login/webauthn-login-prf-crypto.service.spec.ts b/libs/common/src/auth/services/webauthn-login/webauthn-login-prf-key.service.spec.ts similarity index 78% rename from libs/common/src/auth/services/webauthn-login/webauthn-login-prf-crypto.service.spec.ts rename to libs/common/src/auth/services/webauthn-login/webauthn-login-prf-key.service.spec.ts index 96eb466b206..f1fe07a996f 100644 --- a/libs/common/src/auth/services/webauthn-login/webauthn-login-prf-crypto.service.spec.ts +++ b/libs/common/src/auth/services/webauthn-login/webauthn-login-prf-key.service.spec.ts @@ -2,15 +2,15 @@ import { mock, MockProxy } from "jest-mock-extended"; import { CryptoFunctionService } from "../../../platform/abstractions/crypto-function.service"; -import { WebAuthnLoginPrfCryptoService } from "./webauthn-login-prf-crypto.service"; +import { WebAuthnLoginPrfKeyService } from "./webauthn-login-prf-key.service"; -describe("WebAuthnLoginPrfCryptoService", () => { +describe("WebAuthnLoginPrfKeyService", () => { let cryptoFunctionService: MockProxy; - let service: WebAuthnLoginPrfCryptoService; + let service: WebAuthnLoginPrfKeyService; beforeEach(() => { cryptoFunctionService = mock(); - service = new WebAuthnLoginPrfCryptoService(cryptoFunctionService); + service = new WebAuthnLoginPrfKeyService(cryptoFunctionService); }); describe("createSymmetricKeyFromPrf", () => { diff --git a/libs/common/src/auth/services/webauthn-login/webauthn-login-prf-crypto.service.ts b/libs/common/src/auth/services/webauthn-login/webauthn-login-prf-key.service.ts similarity index 82% rename from libs/common/src/auth/services/webauthn-login/webauthn-login-prf-crypto.service.ts rename to libs/common/src/auth/services/webauthn-login/webauthn-login-prf-key.service.ts index 29032cd5877..92cc03e7592 100644 --- a/libs/common/src/auth/services/webauthn-login/webauthn-login-prf-crypto.service.ts +++ b/libs/common/src/auth/services/webauthn-login/webauthn-login-prf-key.service.ts @@ -1,11 +1,11 @@ import { CryptoFunctionService } from "../../../platform/abstractions/crypto-function.service"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { PrfKey } from "../../../types/key"; -import { WebAuthnLoginPrfCryptoServiceAbstraction } from "../../abstractions/webauthn/webauthn-login-prf-crypto.service.abstraction"; +import { WebAuthnLoginPrfKeyServiceAbstraction } from "../../abstractions/webauthn/webauthn-login-prf-key.service.abstraction"; const LoginWithPrfSalt = "passwordless-login"; -export class WebAuthnLoginPrfCryptoService implements WebAuthnLoginPrfCryptoServiceAbstraction { +export class WebAuthnLoginPrfKeyService implements WebAuthnLoginPrfKeyServiceAbstraction { constructor(private cryptoFunctionService: CryptoFunctionService) {} async getLoginWithPrfSalt(): Promise { diff --git a/libs/common/src/auth/services/webauthn-login/webauthn-login.service.spec.ts b/libs/common/src/auth/services/webauthn-login/webauthn-login.service.spec.ts index 1c7f045461b..10444062349 100644 --- a/libs/common/src/auth/services/webauthn-login/webauthn-login.service.spec.ts +++ b/libs/common/src/auth/services/webauthn-login/webauthn-login.service.spec.ts @@ -7,7 +7,7 @@ import { Utils } from "../../../platform/misc/utils"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { PrfKey } from "../../../types/key"; import { WebAuthnLoginApiServiceAbstraction } from "../../abstractions/webauthn/webauthn-login-api.service.abstraction"; -import { WebAuthnLoginPrfCryptoServiceAbstraction } from "../../abstractions/webauthn/webauthn-login-prf-crypto.service.abstraction"; +import { WebAuthnLoginPrfKeyServiceAbstraction } from "../../abstractions/webauthn/webauthn-login-prf-key.service.abstraction"; import { AuthResult } from "../../models/domain/auth-result"; import { WebAuthnLoginCredentialAssertionOptionsView } from "../../models/view/webauthn-login/webauthn-login-credential-assertion-options.view"; import { WebAuthnLoginCredentialAssertionView } from "../../models/view/webauthn-login/webauthn-login-credential-assertion.view"; @@ -21,7 +21,7 @@ describe("WebAuthnLoginService", () => { const webAuthnLoginApiService = mock(); const loginStrategyService = mock(); - const webAuthnLoginPrfCryptoService = mock(); + const webAuthnLoginPrfKeyService = mock(); const navigatorCredentials = mock(); const logService = mock(); @@ -72,7 +72,7 @@ describe("WebAuthnLoginService", () => { return new WebAuthnLoginService( webAuthnLoginApiService, loginStrategyService, - webAuthnLoginPrfCryptoService, + webAuthnLoginPrfKeyService, window, logService, ); @@ -141,8 +141,8 @@ describe("WebAuthnLoginService", () => { publicKeyCredential.getClientExtensionResults().prf?.results?.first; const prfKey = new SymmetricCryptoKey(new Uint8Array(prfResult)) as PrfKey; - webAuthnLoginPrfCryptoService.getLoginWithPrfSalt.mockResolvedValue(saltArrayBuffer); - webAuthnLoginPrfCryptoService.createSymmetricKeyFromPrf.mockResolvedValue(prfKey); + webAuthnLoginPrfKeyService.getLoginWithPrfSalt.mockResolvedValue(saltArrayBuffer); + webAuthnLoginPrfKeyService.createSymmetricKeyFromPrf.mockResolvedValue(prfKey); // Mock implementations navigatorCredentials.get.mockResolvedValue(publicKeyCredential); @@ -152,7 +152,7 @@ describe("WebAuthnLoginService", () => { // Assert - expect(webAuthnLoginPrfCryptoService.getLoginWithPrfSalt).toHaveBeenCalled(); + expect(webAuthnLoginPrfKeyService.getLoginWithPrfSalt).toHaveBeenCalled(); expect(navigatorCredentials.get).toHaveBeenCalledWith( expect.objectContaining({ @@ -169,9 +169,7 @@ describe("WebAuthnLoginService", () => { }), ); - expect(webAuthnLoginPrfCryptoService.createSymmetricKeyFromPrf).toHaveBeenCalledWith( - prfResult, - ); + expect(webAuthnLoginPrfKeyService.createSymmetricKeyFromPrf).toHaveBeenCalledWith(prfResult); expect(result).toBeInstanceOf(WebAuthnLoginCredentialAssertionView); expect(result.token).toEqual(credentialAssertionOptions.token); diff --git a/libs/common/src/auth/services/webauthn-login/webauthn-login.service.ts b/libs/common/src/auth/services/webauthn-login/webauthn-login.service.ts index 7fca20e6159..41f4994fab0 100644 --- a/libs/common/src/auth/services/webauthn-login/webauthn-login.service.ts +++ b/libs/common/src/auth/services/webauthn-login/webauthn-login.service.ts @@ -3,7 +3,7 @@ import { LoginStrategyServiceAbstraction, WebAuthnLoginCredentials } from "@bitw import { LogService } from "../../../platform/abstractions/log.service"; import { PrfKey } from "../../../types/key"; import { WebAuthnLoginApiServiceAbstraction } from "../../abstractions/webauthn/webauthn-login-api.service.abstraction"; -import { WebAuthnLoginPrfCryptoServiceAbstraction } from "../../abstractions/webauthn/webauthn-login-prf-crypto.service.abstraction"; +import { WebAuthnLoginPrfKeyServiceAbstraction } from "../../abstractions/webauthn/webauthn-login-prf-key.service.abstraction"; import { WebAuthnLoginServiceAbstraction } from "../../abstractions/webauthn/webauthn-login.service.abstraction"; import { AuthResult } from "../../models/domain/auth-result"; import { WebAuthnLoginCredentialAssertionOptionsView } from "../../models/view/webauthn-login/webauthn-login-credential-assertion-options.view"; @@ -17,7 +17,7 @@ export class WebAuthnLoginService implements WebAuthnLoginServiceAbstraction { constructor( private webAuthnLoginApiService: WebAuthnLoginApiServiceAbstraction, private loginStrategyService: LoginStrategyServiceAbstraction, - private webAuthnLoginPrfCryptoService: WebAuthnLoginPrfCryptoServiceAbstraction, + private webAuthnLoginPrfKeyService: WebAuthnLoginPrfKeyServiceAbstraction, private window: Window, private logService?: LogService, ) { @@ -37,7 +37,7 @@ export class WebAuthnLoginService implements WebAuthnLoginServiceAbstraction { }; // TODO: Remove `any` when typescript typings add support for PRF nativeOptions.publicKey.extensions = { - prf: { eval: { first: await this.webAuthnLoginPrfCryptoService.getLoginWithPrfSalt() } }, + prf: { eval: { first: await this.webAuthnLoginPrfKeyService.getLoginWithPrfSalt() } }, } as any; try { @@ -50,7 +50,7 @@ export class WebAuthnLoginService implements WebAuthnLoginServiceAbstraction { let symmetricPrfKey: PrfKey | undefined; if (prfResult != undefined) { symmetricPrfKey = - await this.webAuthnLoginPrfCryptoService.createSymmetricKeyFromPrf(prfResult); + await this.webAuthnLoginPrfKeyService.createSymmetricKeyFromPrf(prfResult); } const deviceResponse = new WebAuthnLoginAssertionResponseRequest(response); diff --git a/libs/common/src/autofill/constants/index.ts b/libs/common/src/autofill/constants/index.ts index 9333fa23368..62ad10e9a90 100644 --- a/libs/common/src/autofill/constants/index.ts +++ b/libs/common/src/autofill/constants/index.ts @@ -24,6 +24,7 @@ export const EVENTS = { MOUSEENTER: "mouseenter", MOUSELEAVE: "mouseleave", MOUSEUP: "mouseup", + MOUSEOUT: "mouseout", SUBMIT: "submit", } as const; diff --git a/libs/common/src/billing/models/response/organization-billing-metadata.response.ts b/libs/common/src/billing/models/response/organization-billing-metadata.response.ts index 33d7907fa88..4831d290698 100644 --- a/libs/common/src/billing/models/response/organization-billing-metadata.response.ts +++ b/libs/common/src/billing/models/response/organization-billing-metadata.response.ts @@ -1,10 +1,12 @@ import { BaseResponse } from "../../../models/response/base.response"; export class OrganizationBillingMetadataResponse extends BaseResponse { + isEligibleForSelfHost: boolean; isOnSecretsManagerStandalone: boolean; constructor(response: any) { super(response); + this.isEligibleForSelfHost = this.getResponseProperty("IsEligibleForSelfHost"); this.isOnSecretsManagerStandalone = this.getResponseProperty("IsOnSecretsManagerStandalone"); } } diff --git a/libs/common/src/billing/services/organization-billing.service.ts b/libs/common/src/billing/services/organization-billing.service.ts index 6b326472c97..eebea0ca74e 100644 --- a/libs/common/src/billing/services/organization-billing.service.ts +++ b/libs/common/src/billing/services/organization-billing.service.ts @@ -1,9 +1,9 @@ +import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { ApiService } from "../../abstractions/api.service"; import { OrganizationApiServiceAbstraction as OrganizationApiService } from "../../admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request"; import { OrganizationKeysRequest } from "../../admin-console/models/request/organization-keys.request"; import { OrganizationResponse } from "../../admin-console/models/response/organization.response"; -import { CryptoService } from "../../platform/abstractions/crypto.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { EncString } from "../../platform/models/domain/enc-string"; @@ -28,7 +28,7 @@ interface OrganizationKeys { export class OrganizationBillingService implements OrganizationBillingServiceAbstraction { constructor( private apiService: ApiService, - private cryptoService: CryptoService, + private keyService: KeyService, private encryptService: EncryptService, private i18nService: I18nService, private organizationApiService: OrganizationApiService, @@ -78,8 +78,8 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs } private async makeOrganizationKeys(): Promise { - const [encryptedKey, key] = await this.cryptoService.makeOrgKey(); - const [publicKey, encryptedPrivateKey] = await this.cryptoService.makeKeyPair(key); + const [encryptedKey, key] = await this.keyService.makeOrgKey(); + const [publicKey, encryptedPrivateKey] = await this.keyService.makeKeyPair(key); const encryptedCollectionName = await this.encryptService.encrypt( this.i18nService.t("defaultCollection"), key, diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index fd3833d10e3..84cf5ed521e 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -20,7 +20,7 @@ export enum FeatureFlag { EnableTimeThreshold = "PM-5864-dollar-threshold", InlineMenuPositioningImprovements = "inline-menu-positioning-improvements", ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner", - AC2828_ProviderPortalMembersPage = "AC-2828_provider-portal-members-page", + VaultBulkManagementAction = "vault-bulk-management-action", IdpAutoSubmitLogin = "idp-auto-submit-login", UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh", EnableUpgradePasswordManagerSub = "AC-2708-upgrade-password-manager-sub", @@ -33,7 +33,6 @@ export enum FeatureFlag { CipherKeyEncryption = "cipher-key-encryption", VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint", PM11901_RefactorSelfHostingLicenseUploader = "PM-11901-refactor-self-hosting-license-uploader", - Pm3478RefactorOrganizationUserApi = "pm-3478-refactor-organizationuser-api", AccessIntelligence = "pm-13227-access-intelligence", Pm13322AddPolicyDefinitions = "pm-13322-add-policy-definitions", LimitCollectionCreationDeletionSplit = "pm-10863-limit-collection-creation-deletion-split", @@ -67,7 +66,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.EnableTimeThreshold]: FALSE, [FeatureFlag.InlineMenuPositioningImprovements]: FALSE, [FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE, - [FeatureFlag.AC2828_ProviderPortalMembersPage]: FALSE, + [FeatureFlag.VaultBulkManagementAction]: FALSE, [FeatureFlag.IdpAutoSubmitLogin]: FALSE, [FeatureFlag.UnauthenticatedExtensionUIRefresh]: FALSE, [FeatureFlag.EnableUpgradePasswordManagerSub]: FALSE, @@ -80,7 +79,6 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.CipherKeyEncryption]: FALSE, [FeatureFlag.VerifiedSsoDomainEndpoint]: FALSE, [FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader]: FALSE, - [FeatureFlag.Pm3478RefactorOrganizationUserApi]: FALSE, [FeatureFlag.AccessIntelligence]: FALSE, [FeatureFlag.Pm13322AddPolicyDefinitions]: FALSE, [FeatureFlag.LimitCollectionCreationDeletionSplit]: FALSE, diff --git a/libs/common/src/key-management/abstractions/process-reload.service.ts b/libs/common/src/key-management/abstractions/process-reload.service.ts new file mode 100644 index 00000000000..e46c1e23199 --- /dev/null +++ b/libs/common/src/key-management/abstractions/process-reload.service.ts @@ -0,0 +1,6 @@ +import { AuthService } from "../../auth/abstractions/auth.service"; + +export abstract class ProcessReloadServiceAbstraction { + abstract startProcessReload(authService: AuthService): Promise; + abstract cancelProcessReload(): void; +} diff --git a/libs/common/src/key-management/services/process-reload.service.ts b/libs/common/src/key-management/services/process-reload.service.ts new file mode 100644 index 00000000000..2f25d63b0fd --- /dev/null +++ b/libs/common/src/key-management/services/process-reload.service.ts @@ -0,0 +1,106 @@ +import { firstValueFrom, map, timeout } from "rxjs"; + +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { BiometricStateService } from "@bitwarden/key-management"; + +import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions"; +import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; +import { AccountService } from "../../auth/abstractions/account.service"; +import { AuthService } from "../../auth/abstractions/auth.service"; +import { AuthenticationStatus } from "../../auth/enums/authentication-status"; +import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; +import { UserId } from "../../types/guid"; +import { ProcessReloadServiceAbstraction } from "../abstractions/process-reload.service"; + +export class ProcessReloadService implements ProcessReloadServiceAbstraction { + private reloadInterval: any = null; + + constructor( + private pinService: PinServiceAbstraction, + private messagingService: MessagingService, + private reloadCallback: () => Promise = null, + private vaultTimeoutSettingsService: VaultTimeoutSettingsService, + private biometricStateService: BiometricStateService, + private accountService: AccountService, + ) {} + + async startProcessReload(authService: AuthService): Promise { + const accounts = await firstValueFrom(this.accountService.accounts$); + if (accounts != null) { + const keys = Object.keys(accounts); + if (keys.length > 0) { + for (const userId of keys) { + let status = await firstValueFrom(authService.authStatusFor$(userId as UserId)); + status = await authService.getAuthStatus(userId); + if (status === AuthenticationStatus.Unlocked) { + return; + } + } + } + } + + // A reloadInterval has already been set and is executing + if (this.reloadInterval != null) { + return; + } + + // If there is an active user, check if they have a pinKeyEncryptedUserKeyEphemeral. If so, prevent process reload upon lock. + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + if (userId != null) { + const ephemeralPin = await this.pinService.getPinKeyEncryptedUserKeyEphemeral(userId); + if (ephemeralPin != null) { + return; + } + } + + this.cancelProcessReload(); + await this.executeProcessReload(); + } + + private async executeProcessReload() { + const biometricLockedFingerprintValidated = await firstValueFrom( + this.biometricStateService.fingerprintValidated$, + ); + if (!biometricLockedFingerprintValidated) { + clearInterval(this.reloadInterval); + this.reloadInterval = null; + + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe( + map((a) => a?.id), + timeout(500), + ), + ); + // Replace current active user if they will be logged out on reload + if (activeUserId != null) { + const timeoutAction = await firstValueFrom( + this.vaultTimeoutSettingsService + .getVaultTimeoutActionByUserId$(activeUserId) + .pipe(timeout(500)), // safety feature to avoid this call hanging and stopping process reload from clearing memory + ); + if (timeoutAction === VaultTimeoutAction.LogOut) { + const nextUser = await firstValueFrom( + this.accountService.nextUpAccount$.pipe(map((account) => account?.id ?? null)), + ); + await this.accountService.switchAccount(nextUser); + } + } + + this.messagingService.send("reloadProcess"); + if (this.reloadCallback != null) { + await this.reloadCallback(); + } + return; + } + if (this.reloadInterval == null) { + this.reloadInterval = setInterval(async () => await this.executeProcessReload(), 1000); + } + } + + cancelProcessReload(): void { + if (this.reloadInterval != null) { + clearInterval(this.reloadInterval); + this.reloadInterval = null; + } + } +} diff --git a/libs/common/src/platform/abstractions/encrypt.service.ts b/libs/common/src/platform/abstractions/encrypt.service.ts index c70042e4186..5b28b98803b 100644 --- a/libs/common/src/platform/abstractions/encrypt.service.ts +++ b/libs/common/src/platform/abstractions/encrypt.service.ts @@ -8,7 +8,11 @@ import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; export abstract class EncryptService { abstract encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise; abstract encryptToBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise; - abstract decryptToUtf8(encString: EncString, key: SymmetricCryptoKey): Promise; + abstract decryptToUtf8( + encString: EncString, + key: SymmetricCryptoKey, + decryptContext?: string, + ): Promise; abstract decryptToBytes(encThing: Encrypted, key: SymmetricCryptoKey): Promise; abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise; abstract rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise; diff --git a/libs/common/src/platform/abstractions/sdk/sdk.service.ts b/libs/common/src/platform/abstractions/sdk/sdk.service.ts index 360f2e91a76..04be8a992d0 100644 --- a/libs/common/src/platform/abstractions/sdk/sdk.service.ts +++ b/libs/common/src/platform/abstractions/sdk/sdk.service.ts @@ -2,9 +2,32 @@ import { Observable } from "rxjs"; import { BitwardenClient } from "@bitwarden/sdk-internal"; +import { UserId } from "../../../types/guid"; + export abstract class SdkService { - client$: Observable; + /** + * Check if the SDK is supported in the current environment. + */ supported$: Observable; + /** + * Retrieve a client initialized without a user. + * This client can only be used for operations that don't require a user context. + */ + client$: Observable; + + /** + * Retrieve a client initialized for a specific user. + * This client can be used for operations that require a user context, such as retrieving ciphers + * and operations involving crypto. It can also be used for operations that don't require a user context. + * + * **WARNING:** Do not use `firstValueFrom(userClient$)`! Any operations on the client must be done within the observable. + * The client will be destroyed when the observable is no longer subscribed to. + * Please let platform know if you need a client that is not destroyed when the observable is no longer subscribed to. + * + * @param userId + */ + abstract userClient$(userId: UserId): Observable; + abstract failedToInitialize(): Promise; } diff --git a/libs/common/src/platform/abstractions/system.service.ts b/libs/common/src/platform/abstractions/system.service.ts index 204e336fbf4..7a34a313528 100644 --- a/libs/common/src/platform/abstractions/system.service.ts +++ b/libs/common/src/platform/abstractions/system.service.ts @@ -1,8 +1,4 @@ -import { AuthService } from "../../auth/abstractions/auth.service"; - export abstract class SystemService { - abstract startProcessReload(authService: AuthService): Promise; - abstract cancelProcessReload(): void; abstract clearClipboard(clipboardValue: string, timeoutMs?: number): Promise; abstract clearPendingClipboard(): Promise; } diff --git a/libs/common/src/platform/enums/encryption-type.enum.ts b/libs/common/src/platform/enums/encryption-type.enum.ts index b4ecd780499..a0ffe679279 100644 --- a/libs/common/src/platform/enums/encryption-type.enum.ts +++ b/libs/common/src/platform/enums/encryption-type.enum.ts @@ -8,6 +8,14 @@ export enum EncryptionType { Rsa2048_OaepSha1_HmacSha256_B64 = 6, } +export function encryptionTypeToString(encryptionType: EncryptionType): string { + if (encryptionType in EncryptionType) { + return EncryptionType[encryptionType]; + } else { + return "Unknown encryption type " + encryptionType; + } +} + /** The expected number of parts to a serialized EncString of the given encryption type. * For example, an EncString of type AesCbc256_B64 will have 2 parts, and an EncString of type * AesCbc128_HmacSha256_B64 will have 3 parts. diff --git a/libs/common/src/platform/misc/utils.ts b/libs/common/src/platform/misc/utils.ts index 326ed5e8e8d..a7cc05bbf64 100644 --- a/libs/common/src/platform/misc/utils.ts +++ b/libs/common/src/platform/misc/utils.ts @@ -6,7 +6,7 @@ import { Observable, of, switchMap } from "rxjs"; import { getHostname, parse } from "tldts"; import { Merge } from "type-fest"; -import { CryptoService } from "../abstractions/crypto.service"; +import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { EncryptService } from "../abstractions/encrypt.service"; import { I18nService } from "../abstractions/i18n.service"; @@ -18,7 +18,7 @@ declare global { } interface BitwardenContainerService { - getCryptoService: () => CryptoService; + getKeyService: () => KeyService; getEncryptService: () => EncryptService; } diff --git a/libs/common/src/platform/models/domain/enc-string.spec.ts b/libs/common/src/platform/models/domain/enc-string.spec.ts index 39d58831772..85108a9609b 100644 --- a/libs/common/src/platform/models/domain/enc-string.spec.ts +++ b/libs/common/src/platform/models/domain/enc-string.spec.ts @@ -1,10 +1,10 @@ import { mock, MockProxy } from "jest-mock-extended"; +import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; import { makeEncString, makeStaticByteArray } from "../../../../spec"; import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { UserKey, OrgKey } from "../../../types/key"; -import { CryptoService } from "../../abstractions/crypto.service"; import { EncryptionType } from "../../enums"; import { Utils } from "../../misc/utils"; import { ContainerService } from "../../services/container.service"; @@ -81,9 +81,9 @@ describe("EncString", () => { describe("decrypt", () => { const encString = new EncString(EncryptionType.Rsa2048_OaepSha256_B64, "data"); - const cryptoService = mock(); - cryptoService.hasUserKey.mockResolvedValue(true); - cryptoService.getUserKeyWithLegacySupport.mockResolvedValue( + const keyService = mock(); + keyService.hasUserKey.mockResolvedValue(true); + keyService.getUserKeyWithLegacySupport.mockResolvedValue( new SymmetricCryptoKey(makeStaticByteArray(32)) as UserKey, ); @@ -94,7 +94,7 @@ describe("EncString", () => { beforeEach(() => { (window as any).bitwardenContainerService = new ContainerService( - cryptoService, + keyService, encryptService, ); }); @@ -117,7 +117,7 @@ describe("EncString", () => { describe("decryptWithKey", () => { const encString = new EncString(EncryptionType.Rsa2048_OaepSha256_B64, "data"); - const cryptoService = mock(); + const keyService = mock(); const encryptService = mock(); encryptService.decryptToUtf8 .calledWith(encString, expect.anything()) @@ -140,10 +140,7 @@ describe("EncString", () => { } beforeEach(() => { - (window as any).bitwardenContainerService = new ContainerService( - cryptoService, - encryptService, - ); + (window as any).bitwardenContainerService = new ContainerService(keyService, encryptService); }); it("decrypts using the provided key and encryptService", async () => { @@ -152,7 +149,7 @@ describe("EncString", () => { const key = new SymmetricCryptoKey(makeStaticByteArray(32)); await encString.decryptWithKey(key, encryptService); - expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encString, key); + expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encString, key, "domain-withkey"); }); it("fails to decrypt when key is null", async () => { @@ -321,28 +318,22 @@ describe("EncString", () => { }); describe("decrypt", () => { - let cryptoService: MockProxy; + let keyService: MockProxy; let encryptService: MockProxy; let encString: EncString; beforeEach(() => { - cryptoService = mock(); + keyService = mock(); encryptService = mock(); encString = new EncString(null); - (window as any).bitwardenContainerService = new ContainerService( - cryptoService, - encryptService, - ); + (window as any).bitwardenContainerService = new ContainerService(keyService, encryptService); }); it("handles value it can't decrypt", async () => { encryptService.decryptToUtf8.mockRejectedValue("error"); - (window as any).bitwardenContainerService = new ContainerService( - cryptoService, - encryptService, - ); + (window as any).bitwardenContainerService = new ContainerService(keyService, encryptService); const decrypted = await encString.decrypt(null); @@ -354,35 +345,43 @@ describe("EncString", () => { }); }); - it("uses provided key without depending on CryptoService", async () => { + it("uses provided key without depending on KeyService", async () => { const key = mock(); await encString.decrypt(null, key); - expect(cryptoService.getUserKeyWithLegacySupport).not.toHaveBeenCalled(); - expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encString, key); + expect(keyService.getUserKeyWithLegacySupport).not.toHaveBeenCalled(); + expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encString, key, "provided-key"); }); it("gets an organization key if required", async () => { const orgKey = mock(); - cryptoService.getOrgKey.calledWith("orgId").mockResolvedValue(orgKey); + keyService.getOrgKey.calledWith("orgId").mockResolvedValue(orgKey); await encString.decrypt("orgId", null); - expect(cryptoService.getOrgKey).toHaveBeenCalledWith("orgId"); - expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encString, orgKey); + expect(keyService.getOrgKey).toHaveBeenCalledWith("orgId"); + expect(encryptService.decryptToUtf8).toHaveBeenCalledWith( + encString, + orgKey, + "domain-orgkey-orgId", + ); }); it("gets the user's decryption key if required", async () => { const userKey = mock(); - cryptoService.getUserKeyWithLegacySupport.mockResolvedValue(userKey); + keyService.getUserKeyWithLegacySupport.mockResolvedValue(userKey); await encString.decrypt(null, null); - expect(cryptoService.getUserKeyWithLegacySupport).toHaveBeenCalledWith(); - expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encString, userKey); + expect(keyService.getUserKeyWithLegacySupport).toHaveBeenCalledWith(); + expect(encryptService.decryptToUtf8).toHaveBeenCalledWith( + encString, + userKey, + "domain-withlegacysupport-masterkey", + ); }); }); diff --git a/libs/common/src/platform/models/domain/enc-string.ts b/libs/common/src/platform/models/domain/enc-string.ts index 0b0a597acd3..6f01f46439c 100644 --- a/libs/common/src/platform/models/domain/enc-string.ts +++ b/libs/common/src/platform/models/domain/enc-string.ts @@ -159,16 +159,27 @@ export class EncString implements Encrypted { return this.decryptedValue; } + let keyContext = "provided-key"; try { if (key == null) { key = await this.getKeyForDecryption(orgId); + keyContext = orgId == null ? `domain-orgkey-${orgId}` : "domain-userkey|masterkey"; + if (orgId != null) { + keyContext = `domain-orgkey-${orgId}`; + } else { + const cryptoService = Utils.getContainerService().getKeyService(); + keyContext = + (await cryptoService.getUserKey()) == null + ? "domain-withlegacysupport-masterkey" + : "domain-withlegacysupport-userkey"; + } } if (key == null) { throw new Error("No key to decrypt EncString with orgId " + orgId); } const encryptService = Utils.getContainerService().getEncryptService(); - this.decryptedValue = await encryptService.decryptToUtf8(this, key); + this.decryptedValue = await encryptService.decryptToUtf8(this, key, keyContext); } catch (e) { this.decryptedValue = DECRYPT_ERROR; } @@ -181,7 +192,7 @@ export class EncString implements Encrypted { throw new Error("No key to decrypt EncString"); } - this.decryptedValue = await encryptService.decryptToUtf8(this, key); + this.decryptedValue = await encryptService.decryptToUtf8(this, key, "domain-withkey"); } catch (e) { this.decryptedValue = DECRYPT_ERROR; } @@ -189,10 +200,10 @@ export class EncString implements Encrypted { return this.decryptedValue; } private async getKeyForDecryption(orgId: string) { - const cryptoService = Utils.getContainerService().getCryptoService(); + const keyService = Utils.getContainerService().getKeyService(); return orgId != null - ? await cryptoService.getOrgKey(orgId) - : await cryptoService.getUserKeyWithLegacySupport(); + ? await keyService.getOrgKey(orgId) + : await keyService.getUserKeyWithLegacySupport(); } } diff --git a/libs/common/src/platform/services/container.service.ts b/libs/common/src/platform/services/container.service.ts index 2e0748a32ab..6022e097ab0 100644 --- a/libs/common/src/platform/services/container.service.ts +++ b/libs/common/src/platform/services/container.service.ts @@ -1,9 +1,9 @@ -import { CryptoService } from "../abstractions/crypto.service"; +import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { EncryptService } from "../abstractions/encrypt.service"; export class ContainerService { constructor( - private cryptoService: CryptoService, + private keyService: KeyService, private encryptService: EncryptService, ) {} @@ -14,13 +14,13 @@ export class ContainerService { } /** - * @throws Will throw if CryptoService was not instantiated and provided to the ContainerService constructor + * @throws Will throw if KeyService was not instantiated and provided to the ContainerService constructor */ - getCryptoService(): CryptoService { - if (this.cryptoService == null) { - throw new Error("ContainerService.cryptoService not initialized."); + getKeyService(): KeyService { + if (this.keyService == null) { + throw new Error("ContainerService.keyService not initialized."); } - return this.cryptoService; + return this.keyService; } /** diff --git a/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts b/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts index 681972e7e4b..137d67ca0f0 100644 --- a/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts +++ b/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts @@ -2,7 +2,7 @@ import { Utils } from "../../../platform/misc/utils"; import { CryptoFunctionService } from "../../abstractions/crypto-function.service"; import { EncryptService } from "../../abstractions/encrypt.service"; import { LogService } from "../../abstractions/log.service"; -import { EncryptionType } from "../../enums"; +import { EncryptionType, encryptionTypeToString as encryptionTypeName } from "../../enums"; import { Decryptable } from "../../interfaces/decryptable.interface"; import { Encrypted } from "../../interfaces/encrypted"; import { InitializerMetadata } from "../../interfaces/initializer-metadata.interface"; @@ -63,20 +63,37 @@ export class EncryptServiceImplementation implements EncryptService { return new EncArrayBuffer(encBytes); } - async decryptToUtf8(encString: EncString, key: SymmetricCryptoKey): Promise { + async decryptToUtf8( + encString: EncString, + key: SymmetricCryptoKey, + decryptContext: string = "no context", + ): Promise { if (key == null) { throw new Error("No key provided for decryption."); } key = this.resolveLegacyKey(key, encString); + // DO NOT REMOVE OR MOVE. This prevents downgrade to mac-less CBC, which would compromise integrity and confidentiality. if (key.macKey != null && encString?.mac == null) { - this.logService.error("MAC required but not provided."); + this.logService.error( + "[Encrypt service] Key has mac key but payload is missing mac bytes. Key type " + + encryptionTypeName(key.encType) + + "Payload type " + + encryptionTypeName(encString.encryptionType), + "Decrypt context: " + decryptContext, + ); return null; } if (key.encType !== encString.encryptionType) { - this.logService.error("Key encryption type does not match payload encryption type."); + this.logService.error( + "[Encrypt service] Key encryption type does not match payload encryption type. Key type " + + encryptionTypeName(key.encType) + + "Payload type " + + encryptionTypeName(encString.encryptionType), + "Decrypt context: " + decryptContext, + ); return null; } @@ -94,7 +111,14 @@ export class EncryptServiceImplementation implements EncryptService { ); const macsEqual = await this.cryptoFunctionService.compareFast(fastParams.mac, computedMac); if (!macsEqual) { - this.logMacFailed("MAC comparison failed. Key or payload has changed."); + this.logMacFailed( + "[Encrypt service] MAC comparison failed. Key or payload has changed. Key type " + + encryptionTypeName(key.encType) + + "Payload type " + + encryptionTypeName(encString.encryptionType) + + " Decrypt context: " + + decryptContext, + ); return null; } } @@ -113,13 +137,24 @@ export class EncryptServiceImplementation implements EncryptService { key = this.resolveLegacyKey(key, encThing); + // DO NOT REMOVE OR MOVE. This prevents downgrade to mac-less CBC, which would compromise integrity and confidentiality. if (key.macKey != null && encThing.macBytes == null) { - this.logService.error("MAC required but not provided."); + this.logService.error( + "[Encrypt service] Key has mac key but payload is missing mac bytes. Key type " + + encryptionTypeName(key.encType) + + " Payload type " + + encryptionTypeName(encThing.encryptionType), + ); return null; } if (key.encType !== encThing.encryptionType) { - this.logService.error("Key encryption type does not match payload encryption type."); + this.logService.error( + "[Encrypt service] Key encryption type does not match payload encryption type. Key type " + + encryptionTypeName(key.encType) + + " Payload type " + + encryptionTypeName(encThing.encryptionType), + ); return null; } @@ -129,13 +164,25 @@ export class EncryptServiceImplementation implements EncryptService { macData.set(new Uint8Array(encThing.dataBytes), encThing.ivBytes.byteLength); const computedMac = await this.cryptoFunctionService.hmac(macData, key.macKey, "sha256"); if (computedMac === null) { - this.logMacFailed("Failed to compute MAC."); + this.logMacFailed( + "[Encrypt service] Failed to compute MAC." + + " Key type " + + encryptionTypeName(key.encType) + + " Payload type " + + encryptionTypeName(encThing.encryptionType), + ); return null; } const macsMatch = await this.cryptoFunctionService.compare(encThing.macBytes, computedMac); if (!macsMatch) { - this.logMacFailed("MAC comparison failed. Key or payload has changed."); + this.logMacFailed( + "[Encrypt service] MAC comparison failed. Key or payload has changed." + + " Key type " + + encryptionTypeName(key.encType) + + " Payload type " + + encryptionTypeName(encThing.encryptionType), + ); return null; } } @@ -164,7 +211,7 @@ export class EncryptServiceImplementation implements EncryptService { async rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise { if (data == null) { - throw new Error("No data provided for decryption."); + throw new Error("[Encrypt service] rsaDecrypt: No data provided for decryption."); } let algorithm: "sha1" | "sha256"; @@ -182,7 +229,7 @@ export class EncryptServiceImplementation implements EncryptService { } if (privateKey == null) { - throw new Error("No private key provided for decryption."); + throw new Error("[Encrypt service] rsaDecrypt: No private key provided for decryption."); } return this.cryptoFunctionService.rsaDecrypt(data.dataBytes, privateKey, algorithm); diff --git a/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts b/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts new file mode 100644 index 00000000000..ff82b3aa764 --- /dev/null +++ b/libs/common/src/platform/services/sdk/default-sdk.service.spec.ts @@ -0,0 +1,132 @@ +import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject, firstValueFrom, of } from "rxjs"; + +import { KeyService } from "@bitwarden/key-management"; +import { BitwardenClient } from "@bitwarden/sdk-internal"; + +import { ApiService } from "../../../abstractions/api.service"; +import { AccountInfo, AccountService } from "../../../auth/abstractions/account.service"; +import { KdfConfigService } from "../../../auth/abstractions/kdf-config.service"; +import { PBKDF2KdfConfig } from "../../../auth/models/domain/kdf-config"; +import { UserId } from "../../../types/guid"; +import { UserKey } from "../../../types/key"; +import { Environment, EnvironmentService } from "../../abstractions/environment.service"; +import { PlatformUtilsService } from "../../abstractions/platform-utils.service"; +import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory"; +import { EncryptedString } from "../../models/domain/enc-string"; +import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; + +import { DefaultSdkService } from "./default-sdk.service"; + +describe("DefaultSdkService", () => { + describe("userClient$", () => { + let sdkClientFactory!: MockProxy; + let environmentService!: MockProxy; + let platformUtilsService!: MockProxy; + let accountService!: MockProxy; + let kdfConfigService!: MockProxy; + let keyService!: MockProxy; + let apiService!: MockProxy; + let service!: DefaultSdkService; + + let mockClient!: MockProxy; + + beforeEach(() => { + sdkClientFactory = mock(); + environmentService = mock(); + platformUtilsService = mock(); + accountService = mock(); + kdfConfigService = mock(); + keyService = mock(); + apiService = mock(); + + // Can't use `of(mock())` for some reason + environmentService.environment$ = new BehaviorSubject(mock()); + + service = new DefaultSdkService( + sdkClientFactory, + environmentService, + platformUtilsService, + accountService, + kdfConfigService, + keyService, + apiService, + ); + + mockClient = mock(); + mockClient.crypto.mockReturnValue(mock()); + sdkClientFactory.createSdkClient.mockResolvedValue(mockClient); + }); + + describe("given the user is logged in", () => { + const userId = "user-id" as UserId; + + beforeEach(() => { + accountService.accounts$ = of({ + [userId]: { email: "email", emailVerified: true, name: "name" } as AccountInfo, + }); + kdfConfigService.getKdfConfig$ + .calledWith(userId) + .mockReturnValue(of(new PBKDF2KdfConfig())); + keyService.userKey$ + .calledWith(userId) + .mockReturnValue(of(new SymmetricCryptoKey(new Uint8Array(64)) as UserKey)); + keyService.userEncryptedPrivateKey$ + .calledWith(userId) + .mockReturnValue(of("private-key" as EncryptedString)); + keyService.encryptedOrgKeys$.calledWith(userId).mockReturnValue(of({})); + }); + + it("creates an SDK client when called the first time", async () => { + const result = await firstValueFrom(service.userClient$(userId)); + + expect(result).toBe(mockClient); + expect(sdkClientFactory.createSdkClient).toHaveBeenCalled(); + }); + + it("does not create an SDK client when called the second time with same userId", async () => { + const subject_1 = new BehaviorSubject(undefined); + const subject_2 = new BehaviorSubject(undefined); + + // Use subjects to ensure the subscription is kept alive + service.userClient$(userId).subscribe(subject_1); + service.userClient$(userId).subscribe(subject_2); + + // Wait for the next tick to ensure all async operations are done + await new Promise(process.nextTick); + + expect(subject_1.value).toBe(mockClient); + expect(subject_2.value).toBe(mockClient); + expect(sdkClientFactory.createSdkClient).toHaveBeenCalledTimes(1); + }); + + it("destroys the SDK client when all subscriptions are closed", async () => { + const subject_1 = new BehaviorSubject(undefined); + const subject_2 = new BehaviorSubject(undefined); + const subscription_1 = service.userClient$(userId).subscribe(subject_1); + const subscription_2 = service.userClient$(userId).subscribe(subject_2); + await new Promise(process.nextTick); + + subscription_1.unsubscribe(); + subscription_2.unsubscribe(); + + expect(mockClient.free).toHaveBeenCalledTimes(1); + }); + + it("destroys the SDK client when the userKey is unset (i.e. lock or logout)", async () => { + const userKey$ = new BehaviorSubject(new SymmetricCryptoKey(new Uint8Array(64)) as UserKey); + keyService.userKey$.calledWith(userId).mockReturnValue(userKey$); + + const subject = new BehaviorSubject(undefined); + service.userClient$(userId).subscribe(subject); + await new Promise(process.nextTick); + + userKey$.next(undefined); + await new Promise(process.nextTick); + + expect(mockClient.free).toHaveBeenCalledTimes(1); + expect(subject.value).toBe(undefined); + }); + }); + }); +}); diff --git a/libs/common/src/platform/services/sdk/default-sdk.service.ts b/libs/common/src/platform/services/sdk/default-sdk.service.ts index d4a9cfeb7ed..a1617315448 100644 --- a/libs/common/src/platform/services/sdk/default-sdk.service.ts +++ b/libs/common/src/platform/services/sdk/default-sdk.service.ts @@ -1,24 +1,45 @@ -import { concatMap, firstValueFrom, shareReplay } from "rxjs"; +import { + combineLatest, + concatMap, + firstValueFrom, + Observable, + shareReplay, + map, + distinctUntilChanged, + tap, + switchMap, +} from "rxjs"; -import { LogLevel, DeviceType as SdkDeviceType } from "@bitwarden/sdk-internal"; +import { KeyService } from "@bitwarden/key-management"; +import { + BitwardenClient, + ClientSettings, + LogLevel, + DeviceType as SdkDeviceType, +} from "@bitwarden/sdk-internal"; import { ApiService } from "../../../abstractions/api.service"; +import { EncryptedOrganizationKeyData } from "../../../admin-console/models/data/encrypted-organization-key.data"; +import { AccountInfo, AccountService } from "../../../auth/abstractions/account.service"; +import { KdfConfigService } from "../../../auth/abstractions/kdf-config.service"; +import { KdfConfig } from "../../../auth/models/domain/kdf-config"; import { DeviceType } from "../../../enums/device-type.enum"; -import { EnvironmentService } from "../../abstractions/environment.service"; +import { OrganizationId, UserId } from "../../../types/guid"; +import { UserKey } from "../../../types/key"; +import { Environment, EnvironmentService } from "../../abstractions/environment.service"; import { PlatformUtilsService } from "../../abstractions/platform-utils.service"; import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory"; import { SdkService } from "../../abstractions/sdk/sdk.service"; +import { KdfType } from "../../enums"; +import { compareValues } from "../../misc/compare-values"; +import { EncryptedString } from "../../models/domain/enc-string"; export class DefaultSdkService implements SdkService { + private sdkClientCache = new Map>(); + client$ = this.environmentService.environment$.pipe( concatMap(async (env) => { - const settings = { - apiUrl: env.getApiUrl(), - identityUrl: env.getIdentityUrl(), - deviceType: this.toDevice(this.platformUtilsService.getDevice()), - userAgent: this.userAgent ?? navigator.userAgent, - }; - + const settings = this.toSettings(env); return await this.sdkClientFactory.createSdkClient(settings, LogLevel.Info); }), shareReplay({ refCount: true, bufferSize: 1 }), @@ -34,10 +55,81 @@ export class DefaultSdkService implements SdkService { private sdkClientFactory: SdkClientFactory, private environmentService: EnvironmentService, private platformUtilsService: PlatformUtilsService, + private accountService: AccountService, + private kdfConfigService: KdfConfigService, + private keyService: KeyService, private apiService: ApiService, // Yes we shouldn't import ApiService, but it's temporary private userAgent: string = null, ) {} + userClient$(userId: UserId): Observable { + // TODO: Figure out what happens when the user logs out + if (this.sdkClientCache.has(userId)) { + return this.sdkClientCache.get(userId); + } + + const account$ = this.accountService.accounts$.pipe( + map((accounts) => accounts[userId]), + distinctUntilChanged(), + ); + const kdfParams$ = this.kdfConfigService.getKdfConfig$(userId).pipe(distinctUntilChanged()); + const privateKey$ = this.keyService + .userEncryptedPrivateKey$(userId) + .pipe(distinctUntilChanged()); + const userKey$ = this.keyService.userKey$(userId).pipe(distinctUntilChanged()); + const orgKeys$ = this.keyService.encryptedOrgKeys$(userId).pipe( + distinctUntilChanged(compareValues), // The upstream observable emits different objects with the same values + ); + + const client$ = combineLatest([ + this.environmentService.environment$, + account$, + kdfParams$, + privateKey$, + userKey$, + orgKeys$, + ]).pipe( + // switchMap is required to allow the clean-up logic to be executed when `combineLatest` emits a new value. + switchMap(([env, account, kdfParams, privateKey, userKey, orgKeys]) => { + // Create our own observable to be able to implement clean-up logic + return new Observable((subscriber) => { + let client: BitwardenClient; + + const createAndInitializeClient = async () => { + if (privateKey == null || userKey == null) { + return undefined; + } + + const settings = this.toSettings(env); + client = await this.sdkClientFactory.createSdkClient(settings, LogLevel.Info); + + await this.initializeClient(client, account, kdfParams, privateKey, userKey, orgKeys); + + return client; + }; + + createAndInitializeClient() + .then((c) => { + client = c; + subscriber.next(c); + }) + .catch((e) => { + subscriber.error(e); + }); + + return () => client?.free(); + }); + }), + tap({ + finalize: () => this.sdkClientCache.delete(userId), + }), + shareReplay({ refCount: true, bufferSize: 1 }), + ); + + this.sdkClientCache.set(userId, client$); + return client$; + } + async failedToInitialize(): Promise { // Only log on cloud instances if ( @@ -52,6 +144,52 @@ export class DefaultSdkService implements SdkService { }); } + private async initializeClient( + client: BitwardenClient, + account: AccountInfo, + kdfParams: KdfConfig, + privateKey: EncryptedString, + userKey: UserKey, + orgKeys?: Record, + ) { + await client.crypto().initialize_user_crypto({ + email: account.email, + method: { decryptedKey: { decrypted_user_key: userKey.keyB64 } }, + kdfParams: + kdfParams.kdfType === KdfType.PBKDF2_SHA256 + ? { + pBKDF2: { iterations: kdfParams.iterations }, + } + : { + argon2id: { + iterations: kdfParams.iterations, + memory: kdfParams.memory, + parallelism: kdfParams.parallelism, + }, + }, + privateKey, + }); + + // We initialize the org crypto even if the org_keys are + // null to make sure any existing org keys are cleared. + await client.crypto().initialize_org_crypto({ + organizationKeys: new Map( + Object.entries(orgKeys ?? {}) + .filter(([_, v]) => v.type === "organization") + .map(([k, v]) => [k, v.key]), + ), + }); + } + + private toSettings(env: Environment): ClientSettings { + return { + apiUrl: env.getApiUrl(), + identityUrl: env.getIdentityUrl(), + deviceType: this.toDevice(this.platformUtilsService.getDevice()), + userAgent: this.userAgent ?? navigator.userAgent, + }; + } + private toDevice(device: DeviceType): SdkDeviceType { switch (device) { case DeviceType.Android: diff --git a/libs/common/src/platform/services/system.service.ts b/libs/common/src/platform/services/system.service.ts index 357737391c2..03e96af75b5 100644 --- a/libs/common/src/platform/services/system.service.ts +++ b/libs/common/src/platform/services/system.service.ts @@ -1,16 +1,6 @@ -import { firstValueFrom, map, Subscription, timeout } from "rxjs"; +import { firstValueFrom, Subscription } from "rxjs"; -import { BiometricStateService } from "@bitwarden/key-management"; - -import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions"; -import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; -import { AccountService } from "../../auth/abstractions/account.service"; -import { AuthService } from "../../auth/abstractions/auth.service"; -import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { AutofillSettingsServiceAbstraction } from "../../autofill/services/autofill-settings.service"; -import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; -import { UserId } from "../../types/guid"; -import { MessagingService } from "../abstractions/messaging.service"; import { PlatformUtilsService } from "../abstractions/platform-utils.service"; import { SystemService as SystemServiceAbstraction } from "../abstractions/system.service"; import { Utils } from "../misc/utils"; @@ -18,19 +8,12 @@ import { ScheduledTaskNames } from "../scheduling/scheduled-task-name.enum"; import { TaskSchedulerService } from "../scheduling/task-scheduler.service"; export class SystemService implements SystemServiceAbstraction { - private reloadInterval: any = null; private clearClipboardTimeoutSubscription: Subscription; private clearClipboardTimeoutFunction: () => Promise = null; constructor( - private pinService: PinServiceAbstraction, - private messagingService: MessagingService, private platformUtilsService: PlatformUtilsService, - private reloadCallback: () => Promise = null, private autofillSettingsService: AutofillSettingsServiceAbstraction, - private vaultTimeoutSettingsService: VaultTimeoutSettingsService, - private biometricStateService: BiometricStateService, - private accountService: AccountService, private taskSchedulerService: TaskSchedulerService, ) { this.taskSchedulerService.registerTaskHandler( @@ -39,86 +22,6 @@ export class SystemService implements SystemServiceAbstraction { ); } - async startProcessReload(authService: AuthService): Promise { - const accounts = await firstValueFrom(this.accountService.accounts$); - if (accounts != null) { - const keys = Object.keys(accounts); - if (keys.length > 0) { - for (const userId of keys) { - let status = await firstValueFrom(authService.authStatusFor$(userId as UserId)); - status = await authService.getAuthStatus(userId); - if (status === AuthenticationStatus.Unlocked) { - return; - } - } - } - } - - // A reloadInterval has already been set and is executing - if (this.reloadInterval != null) { - return; - } - - // If there is an active user, check if they have a pinKeyEncryptedUserKeyEphemeral. If so, prevent process reload upon lock. - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - if (userId != null) { - const ephemeralPin = await this.pinService.getPinKeyEncryptedUserKeyEphemeral(userId); - if (ephemeralPin != null) { - return; - } - } - - this.cancelProcessReload(); - await this.executeProcessReload(); - } - - private async executeProcessReload() { - const biometricLockedFingerprintValidated = await firstValueFrom( - this.biometricStateService.fingerprintValidated$, - ); - if (!biometricLockedFingerprintValidated) { - clearInterval(this.reloadInterval); - this.reloadInterval = null; - - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe( - map((a) => a?.id), - timeout(500), - ), - ); - // Replace current active user if they will be logged out on reload - if (activeUserId != null) { - const timeoutAction = await firstValueFrom( - this.vaultTimeoutSettingsService - .getVaultTimeoutActionByUserId$(activeUserId) - .pipe(timeout(500)), // safety feature to avoid this call hanging and stopping process reload from clearing memory - ); - if (timeoutAction === VaultTimeoutAction.LogOut) { - const nextUser = await firstValueFrom( - this.accountService.nextUpAccount$.pipe(map((account) => account?.id ?? null)), - ); - await this.accountService.switchAccount(nextUser); - } - } - - this.messagingService.send("reloadProcess"); - if (this.reloadCallback != null) { - await this.reloadCallback(); - } - return; - } - if (this.reloadInterval == null) { - this.reloadInterval = setInterval(async () => await this.executeProcessReload(), 1000); - } - } - - cancelProcessReload(): void { - if (this.reloadInterval != null) { - clearInterval(this.reloadInterval); - this.reloadInterval = null; - } - } - async clearClipboard(clipboardValue: string, timeoutMs: number = null): Promise { this.clearClipboardTimeoutSubscription?.unsubscribe(); diff --git a/libs/common/src/platform/services/user-auto-unlock-key.service.spec.ts b/libs/common/src/platform/services/user-auto-unlock-key.service.spec.ts index f0d60158c18..23a8ba3138b 100644 --- a/libs/common/src/platform/services/user-auto-unlock-key.service.spec.ts +++ b/libs/common/src/platform/services/user-auto-unlock-key.service.spec.ts @@ -1,5 +1,6 @@ import { mock } from "jest-mock-extended"; +import { DefaultKeyService } from "../../../../key-management/src/key.service"; import { CsprngArray } from "../../types/csprng"; import { UserId } from "../../types/guid"; import { UserKey } from "../../types/key"; @@ -7,7 +8,6 @@ import { KeySuffixOptions } from "../enums"; import { Utils } from "../misc/utils"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; -import { CryptoService } from "./crypto.service"; import { UserAutoUnlockKeyService } from "./user-auto-unlock-key.service"; describe("UserAutoUnlockKeyService", () => { @@ -15,10 +15,10 @@ describe("UserAutoUnlockKeyService", () => { const mockUserId = Utils.newGuid() as UserId; - const cryptoService = mock(); + const keyService = mock(); beforeEach(() => { - userAutoUnlockKeyService = new UserAutoUnlockKeyService(cryptoService); + userAutoUnlockKeyService = new UserAutoUnlockKeyService(keyService); }); describe("setUserKeyInMemoryIfAutoUserKeySet", () => { @@ -27,25 +27,22 @@ describe("UserAutoUnlockKeyService", () => { await (userAutoUnlockKeyService as any).setUserKeyInMemoryIfAutoUserKeySet(null); // Assert - expect(cryptoService.getUserKeyFromStorage).not.toHaveBeenCalled(); - expect(cryptoService.setUserKey).not.toHaveBeenCalled(); + expect(keyService.getUserKeyFromStorage).not.toHaveBeenCalled(); + expect(keyService.setUserKey).not.toHaveBeenCalled(); }); it("does nothing if the autoUserKey is null", async () => { // Arrange const userId = mockUserId; - cryptoService.getUserKeyFromStorage.mockResolvedValue(null); + keyService.getUserKeyFromStorage.mockResolvedValue(null); // Act await (userAutoUnlockKeyService as any).setUserKeyInMemoryIfAutoUserKeySet(userId); // Assert - expect(cryptoService.getUserKeyFromStorage).toHaveBeenCalledWith( - KeySuffixOptions.Auto, - userId, - ); - expect(cryptoService.setUserKey).not.toHaveBeenCalled(); + expect(keyService.getUserKeyFromStorage).toHaveBeenCalledWith(KeySuffixOptions.Auto, userId); + expect(keyService.setUserKey).not.toHaveBeenCalled(); }); it("sets the user key in memory if the autoUserKey is not null", async () => { @@ -55,17 +52,14 @@ describe("UserAutoUnlockKeyService", () => { const mockRandomBytes = new Uint8Array(64) as CsprngArray; const mockAutoUserKey: UserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey; - cryptoService.getUserKeyFromStorage.mockResolvedValue(mockAutoUserKey); + keyService.getUserKeyFromStorage.mockResolvedValue(mockAutoUserKey); // Act await (userAutoUnlockKeyService as any).setUserKeyInMemoryIfAutoUserKeySet(userId); // Assert - expect(cryptoService.getUserKeyFromStorage).toHaveBeenCalledWith( - KeySuffixOptions.Auto, - userId, - ); - expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockAutoUserKey, userId); + expect(keyService.getUserKeyFromStorage).toHaveBeenCalledWith(KeySuffixOptions.Auto, userId); + expect(keyService.setUserKey).toHaveBeenCalledWith(mockAutoUserKey, userId); }); }); }); diff --git a/libs/common/src/platform/services/user-auto-unlock-key.service.ts b/libs/common/src/platform/services/user-auto-unlock-key.service.ts index b4a154133c1..abb8993c39c 100644 --- a/libs/common/src/platform/services/user-auto-unlock-key.service.ts +++ b/libs/common/src/platform/services/user-auto-unlock-key.service.ts @@ -1,15 +1,15 @@ +import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { UserId } from "../../types/guid"; -import { CryptoService } from "../abstractions/crypto.service"; import { KeySuffixOptions } from "../enums"; -// TODO: this is a half measure improvement which allows us to reduce some side effects today (cryptoService.getUserKey setting user key in memory if auto key exists) -// but ideally, in the future, we would be able to put this logic into the cryptoService +// TODO: this is a half measure improvement which allows us to reduce some side effects today (keyService.getUserKey setting user key in memory if auto key exists) +// but ideally, in the future, we would be able to put this logic into the keyService // after the vault timeout settings service is transitioned to state provider so that // the getUserKey logic can simply go to the correct location based on the vault timeout settings // similar to the TokenService (it would either go to secure storage for the auto user key or memory for the user key) export class UserAutoUnlockKeyService { - constructor(private cryptoService: CryptoService) {} + constructor(private keyService: KeyService) {} /** * The presence of the user key in memory dictates whether the user's vault is locked or unlocked. @@ -23,16 +23,13 @@ export class UserAutoUnlockKeyService { return false; } - const autoUserKey = await this.cryptoService.getUserKeyFromStorage( - KeySuffixOptions.Auto, - userId, - ); + const autoUserKey = await this.keyService.getUserKeyFromStorage(KeySuffixOptions.Auto, userId); if (autoUserKey == null) { return false; } - await this.cryptoService.setUserKey(autoUserKey, userId); + await this.keyService.setUserKey(autoUserKey, userId); return true; } } diff --git a/libs/common/src/platform/sync/default-sync.service.ts b/libs/common/src/platform/sync/default-sync.service.ts index 66a6c8e3503..eaf804d2866 100644 --- a/libs/common/src/platform/sync/default-sync.service.ts +++ b/libs/common/src/platform/sync/default-sync.service.ts @@ -8,6 +8,7 @@ import { import { UserDecryptionOptionsServiceAbstraction } from "../../../../auth/src/common/abstractions"; import { LogoutReason } from "../../../../auth/src/common/types"; +import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { ApiService } from "../../abstractions/api.service"; import { InternalOrganizationServiceAbstraction } from "../../admin-console/abstractions/organization/organization.service.abstraction"; import { InternalPolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; @@ -41,7 +42,6 @@ import { CipherData } from "../../vault/models/data/cipher.data"; import { FolderData } from "../../vault/models/data/folder.data"; import { CipherResponse } from "../../vault/models/response/cipher.response"; import { FolderResponse } from "../../vault/models/response/folder.response"; -import { CryptoService } from "../abstractions/crypto.service"; import { LogService } from "../abstractions/log.service"; import { StateService } from "../abstractions/state.service"; import { MessageSender } from "../messaging"; @@ -60,7 +60,7 @@ export class DefaultSyncService extends CoreSyncService { private domainSettingsService: DomainSettingsService, folderService: InternalFolderService, cipherService: CipherService, - private cryptoService: CryptoService, + private keyService: KeyService, collectionService: CollectionService, messageSender: MessageSender, private policyService: InternalPolicyService, @@ -178,10 +178,10 @@ export class DefaultSyncService extends CoreSyncService { throw new Error("Stamp has changed"); } - await this.cryptoService.setMasterKeyEncryptedUserKey(response.key, response.id); - await this.cryptoService.setPrivateKey(response.privateKey, response.id); - await this.cryptoService.setProviderKeys(response.providers, response.id); - await this.cryptoService.setOrgKeys( + await this.keyService.setMasterKeyEncryptedUserKey(response.key, response.id); + await this.keyService.setPrivateKey(response.privateKey, response.id); + await this.keyService.setProviderKeys(response.providers, response.id); + await this.keyService.setOrgKeys( response.organizations, response.providerOrganizations, response.id, diff --git a/libs/common/src/services/notifications.service.ts b/libs/common/src/services/notifications.service.ts index 8e6a664a0af..d443193c9b7 100644 --- a/libs/common/src/services/notifications.service.ts +++ b/libs/common/src/services/notifications.service.ts @@ -193,6 +193,7 @@ export class NotificationsService implements NotificationsServiceAbstraction { break; case NotificationType.LogOut: if (isAuthenticated) { + this.logService.info("[Notifications Service] Received logout notification"); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.logoutCallback("logoutNotification"); diff --git a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts index d90388f866f..540f26bba2d 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts @@ -10,13 +10,13 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { UserId } from "@bitwarden/common/types/guid"; import { BiometricStateService } from "@bitwarden/key-management"; +import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { FakeAccountService, mockAccountServiceWith, FakeStateProvider } from "../../../spec"; import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; import { Policy } from "../../admin-console/models/domain/policy"; import { TokenService } from "../../auth/abstractions/token.service"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; -import { CryptoService } from "../../platform/abstractions/crypto.service"; import { LogService } from "../../platform/abstractions/log.service"; import { VAULT_TIMEOUT, @@ -30,7 +30,7 @@ describe("VaultTimeoutSettingsService", () => { let accountService: FakeAccountService; let pinService: MockProxy; let userDecryptionOptionsService: MockProxy; - let cryptoService: MockProxy; + let keyService: MockProxy; let tokenService: MockProxy; let policyService: MockProxy; const biometricStateService = mock(); @@ -46,7 +46,7 @@ describe("VaultTimeoutSettingsService", () => { accountService = mockAccountServiceWith(mockUserId); pinService = mock(); userDecryptionOptionsService = mock(); - cryptoService = mock(); + keyService = mock(); tokenService = mock(); policyService = mock(); @@ -342,7 +342,7 @@ describe("VaultTimeoutSettingsService", () => { stateProvider.singleUser.getFake(mockUserId, VAULT_TIMEOUT).nextMock, ).toHaveBeenCalledWith(timeout); - expect(cryptoService.refreshAdditionalKeys).toHaveBeenCalled(); + expect(keyService.refreshAdditionalKeys).toHaveBeenCalled(); }); it("should clear the tokens when the timeout is not never and the action is log out", async () => { @@ -377,7 +377,7 @@ describe("VaultTimeoutSettingsService", () => { accountService, pinService, userDecryptionOptionsService, - cryptoService, + keyService, tokenService, policyService, biometricStateService, diff --git a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts index a90842b208c..a1bc93144b7 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts @@ -19,6 +19,7 @@ import { } from "@bitwarden/auth/common"; import { BiometricStateService } from "@bitwarden/key-management"; +import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "../../admin-console/enums"; @@ -26,7 +27,6 @@ import { Policy } from "../../admin-console/models/domain/policy"; import { AccountService } from "../../auth/abstractions/account.service"; import { TokenService } from "../../auth/abstractions/token.service"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; -import { CryptoService } from "../../platform/abstractions/crypto.service"; import { LogService } from "../../platform/abstractions/log.service"; import { StateProvider } from "../../platform/state"; import { UserId } from "../../types/guid"; @@ -39,7 +39,7 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA private accountService: AccountService, private pinService: PinServiceAbstraction, private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, - private cryptoService: CryptoService, + private keyService: KeyService, private tokenService: TokenService, private policyService: PolicyService, private biometricStateService: BiometricStateService, @@ -87,7 +87,7 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA clientSecret, ]); - await this.cryptoService.refreshAdditionalKeys(); + await this.keyService.refreshAdditionalKeys(); } availableVaultTimeoutActions$(userId?: string): Observable { @@ -287,7 +287,7 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA } async clear(userId?: string): Promise { - await this.cryptoService.clearPinKeys(userId); + await this.keyService.clearPinKeys(userId); } private async userHasMasterPassword(userId: string): Promise { diff --git a/libs/common/src/tools/dependencies.ts b/libs/common/src/tools/dependencies.ts index 8b860591d54..84e2f53fa29 100644 --- a/libs/common/src/tools/dependencies.ts +++ b/libs/common/src/tools/dependencies.ts @@ -3,6 +3,8 @@ import { Observable } from "rxjs"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { UserId } from "@bitwarden/common/types/guid"; +import { UserEncryptor } from "./state/user-encryptor.abstraction"; + /** error emitted when the `SingleUserDependency` changes Ids */ export type UserChangedError = { /** the userId pinned by the single user dependency */ @@ -45,7 +47,35 @@ export type UserDependency = { userId$: Observable; }; -/** A pattern for types that depend upon a fixed userid and return +/** Decorates a type to indicate the user, if any, that the type is usable only by + * a specific user. + */ +export type UserBound = { [P in K]: T } & { + /** The user to which T is bound. */ + userId: UserId; +}; + +/** A pattern for types that depend upon a fixed-key encryptor and return + * an observable. + * + * Consumers of this dependency should emit a `UserChangedError` if + * the bound UserId changes or if the encryptor changes. If + * `singleUserEncryptor$` completes, the consumer should complete + * once all events received prior to the completion event are + * finished processing. The consumer should, where possible, + * prioritize these events in order to complete as soon as possible. + * If `singleUserEncryptor$` emits an unrecoverable error, the consumer + * should also emit the error. + */ +export type SingleUserEncryptorDependency = { + /** A stream that emits an encryptor when subscribed and the user key + * is available, and completes when the user key is no longer available. + * The stream should not emit null or undefined. + */ + singleUserEncryptor$: Observable>; +}; + +/** A pattern for types that depend upon a fixed-value userid and return * an observable. * * Consumers of this dependency should emit a `UserChangedError` if diff --git a/libs/common/src/tools/integration/integration-id.ts b/libs/common/src/tools/integration/integration-id.ts index 46b81c3c4c0..a15db143ee1 100644 --- a/libs/common/src/tools/integration/integration-id.ts +++ b/libs/common/src/tools/integration/integration-id.ts @@ -1,7 +1,13 @@ import { Opaque } from "type-fest"; +export const IntegrationIds = [ + "anonaddy", + "duckduckgo", + "fastmail", + "firefoxrelay", + "forwardemail", + "simplelogin", +] as const; + /** Identifies a vendor integrated into bitwarden */ -export type IntegrationId = Opaque< - "anonaddy" | "duckduckgo" | "fastmail" | "firefoxrelay" | "forwardemail" | "simplelogin", - "IntegrationId" ->; +export type IntegrationId = Opaque<(typeof IntegrationIds)[number], "IntegrationId">; diff --git a/libs/common/src/tools/private-classifier.ts b/libs/common/src/tools/private-classifier.ts new file mode 100644 index 00000000000..f9648504b76 --- /dev/null +++ b/libs/common/src/tools/private-classifier.ts @@ -0,0 +1,31 @@ +import { Jsonify } from "type-fest"; + +import { Classifier } from "@bitwarden/common/tools/state/classifier"; + +export class PrivateClassifier implements Classifier, Data> { + constructor(private keys: (keyof Jsonify)[] = undefined) {} + + classify(value: Data): { disclosed: Jsonify>; secret: Jsonify } { + const pickMe = JSON.parse(JSON.stringify(value)); + const keys: (keyof Jsonify)[] = this.keys ?? (Object.keys(pickMe) as any); + + const picked: Partial> = {}; + for (const key of keys) { + picked[key] = pickMe[key]; + } + const secret = picked as Jsonify; + + return { disclosed: null, secret }; + } + + declassify(_disclosed: Jsonify>, secret: Jsonify) { + const result: Partial> = {}; + const keys: (keyof Jsonify)[] = this.keys ?? (Object.keys(secret) as any); + + for (const key of keys) { + result[key] = secret[key]; + } + + return result as Jsonify; + } +} diff --git a/libs/common/src/tools/public-classifier.ts b/libs/common/src/tools/public-classifier.ts new file mode 100644 index 00000000000..82396f1c169 --- /dev/null +++ b/libs/common/src/tools/public-classifier.ts @@ -0,0 +1,29 @@ +import { Jsonify } from "type-fest"; + +import { Classifier } from "@bitwarden/common/tools/state/classifier"; + +export class PublicClassifier implements Classifier> { + constructor(private keys: (keyof Jsonify)[]) {} + + classify(value: Data): { disclosed: Jsonify; secret: Jsonify> } { + const pickMe = JSON.parse(JSON.stringify(value)); + + const picked: Partial> = {}; + for (const key of this.keys) { + picked[key] = pickMe[key]; + } + const disclosed = picked as Jsonify; + + return { disclosed, secret: null }; + } + + declassify(disclosed: Jsonify, _secret: Jsonify>) { + const result: Partial> = {}; + + for (const key of this.keys) { + result[key] = disclosed[key]; + } + + return result as Jsonify; + } +} diff --git a/libs/common/src/tools/rx.spec.ts b/libs/common/src/tools/rx.spec.ts index 8a2c1e38f5c..f6932f01dc1 100644 --- a/libs/common/src/tools/rx.spec.ts +++ b/libs/common/src/tools/rx.spec.ts @@ -2,11 +2,18 @@ * include structuredClone in test environment. * @jest-environment ../../../../shared/test.environment.ts */ -import { of, firstValueFrom } from "rxjs"; +import { of, firstValueFrom, Subject, tap, EmptyError } from "rxjs"; import { awaitAsync, trackEmissions } from "../../spec"; -import { distinctIfShallowMatch, reduceCollection } from "./rx"; +import { + anyComplete, + distinctIfShallowMatch, + on, + ready, + reduceCollection, + withLatestReady, +} from "./rx"; describe("reduceCollection", () => { it.each([[null], [undefined], [[]]])( @@ -84,3 +91,488 @@ describe("distinctIfShallowMatch", () => { expect(result).toEqual([{ foo: true, bar: true }]); }); }); + +describe("anyComplete", () => { + it("emits true when its input completes", () => { + const input$ = new Subject(); + + const emissions: boolean[] = []; + anyComplete(input$).subscribe((e) => emissions.push(e)); + input$.complete(); + + expect(emissions).toEqual([true]); + }); + + it("completes when its input is already complete", () => { + const input = new Subject(); + input.complete(); + + let completed = false; + anyComplete(input).subscribe({ complete: () => (completed = true) }); + + expect(completed).toBe(true); + }); + + it("completes when any input completes", () => { + const input$ = new Subject(); + const completing$ = new Subject(); + + let completed = false; + anyComplete([input$, completing$]).subscribe({ complete: () => (completed = true) }); + completing$.complete(); + + expect(completed).toBe(true); + }); + + it("ignores emissions", () => { + const input$ = new Subject(); + + const emissions: boolean[] = []; + anyComplete(input$).subscribe((e) => emissions.push(e)); + input$.next(1); + input$.next(2); + input$.complete(); + + expect(emissions).toEqual([true]); + }); + + it("forwards errors", () => { + const input$ = new Subject(); + const expected = { some: "error" }; + + let error = null; + anyComplete(input$).subscribe({ error: (e: unknown) => (error = e) }); + input$.error(expected); + + expect(error).toEqual(expected); + }); +}); + +describe("ready", () => { + it("connects when subscribed", () => { + const watch$ = new Subject(); + let connected = false; + const source$ = new Subject().pipe(tap({ subscribe: () => (connected = true) })); + + // precondition: ready$ should be cold + const ready$ = source$.pipe(ready(watch$)); + expect(connected).toBe(false); + + ready$.subscribe(); + + expect(connected).toBe(true); + }); + + it("suppresses source emissions until its watch emits", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const ready$ = source$.pipe(ready(watch$)); + const results: number[] = []; + ready$.subscribe((n) => results.push(n)); + + // precondition: no emissions + source$.next(1); + expect(results).toEqual([]); + + watch$.next(); + + expect(results).toEqual([1]); + }); + + it("suppresses source emissions until all watches emit", () => { + const watchA$ = new Subject(); + const watchB$ = new Subject(); + const source$ = new Subject(); + const ready$ = source$.pipe(ready([watchA$, watchB$])); + const results: number[] = []; + ready$.subscribe((n) => results.push(n)); + + // preconditions: no emissions + source$.next(1); + expect(results).toEqual([]); + watchA$.next(); + expect(results).toEqual([]); + + watchB$.next(); + + expect(results).toEqual([1]); + }); + + it("emits the last source emission when its watch emits", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const ready$ = source$.pipe(ready(watch$)); + const results: number[] = []; + ready$.subscribe((n) => results.push(n)); + + // precondition: no emissions + source$.next(1); + expect(results).toEqual([]); + + source$.next(2); + watch$.next(); + + expect(results).toEqual([2]); + }); + + it("emits all source emissions after its watch emits", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const ready$ = source$.pipe(ready(watch$)); + const results: number[] = []; + ready$.subscribe((n) => results.push(n)); + + watch$.next(); + source$.next(1); + source$.next(2); + + expect(results).toEqual([1, 2]); + }); + + it("ignores repeated watch emissions", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const ready$ = source$.pipe(ready(watch$)); + const results: number[] = []; + ready$.subscribe((n) => results.push(n)); + + watch$.next(); + source$.next(1); + watch$.next(); + source$.next(2); + watch$.next(); + + expect(results).toEqual([1, 2]); + }); + + it("completes when its source completes", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const ready$ = source$.pipe(ready(watch$)); + let completed = false; + ready$.subscribe({ complete: () => (completed = true) }); + + source$.complete(); + + expect(completed).toBeTruthy(); + }); + + it("errors when its source errors", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const ready$ = source$.pipe(ready(watch$)); + const expected = { some: "error" }; + let error = null; + ready$.subscribe({ error: (e: unknown) => (error = e) }); + + source$.error(expected); + + expect(error).toEqual(expected); + }); + + it("errors when its watch errors", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const ready$ = source$.pipe(ready(watch$)); + const expected = { some: "error" }; + let error = null; + ready$.subscribe({ error: (e: unknown) => (error = e) }); + + watch$.error(expected); + + expect(error).toEqual(expected); + }); + + it("errors when its watch completes before emitting", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const ready$ = source$.pipe(ready(watch$)); + let error = null; + ready$.subscribe({ error: (e: unknown) => (error = e) }); + + watch$.complete(); + + expect(error).toBeInstanceOf(EmptyError); + }); +}); + +describe("withLatestReady", () => { + it("connects when subscribed", () => { + const watch$ = new Subject(); + let connected = false; + const source$ = new Subject().pipe(tap({ subscribe: () => (connected = true) })); + + // precondition: ready$ should be cold + const ready$ = source$.pipe(withLatestReady(watch$)); + expect(connected).toBe(false); + + ready$.subscribe(); + + expect(connected).toBe(true); + }); + + it("suppresses source emissions until its watch emits", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const ready$ = source$.pipe(withLatestReady(watch$)); + const results: [number, string][] = []; + ready$.subscribe((n) => results.push(n)); + + // precondition: no emissions + source$.next(1); + expect(results).toEqual([]); + + watch$.next("watch"); + + expect(results).toEqual([[1, "watch"]]); + }); + + it("emits the last source emission when its watch emits", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const ready$ = source$.pipe(withLatestReady(watch$)); + const results: [number, string][] = []; + ready$.subscribe((n) => results.push(n)); + + // precondition: no emissions + source$.next(1); + expect(results).toEqual([]); + + source$.next(2); + watch$.next("watch"); + + expect(results).toEqual([[2, "watch"]]); + }); + + it("emits all source emissions after its watch emits", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const ready$ = source$.pipe(withLatestReady(watch$)); + const results: [number, string][] = []; + ready$.subscribe((n) => results.push(n)); + + watch$.next("watch"); + source$.next(1); + source$.next(2); + + expect(results).toEqual([ + [1, "watch"], + [2, "watch"], + ]); + }); + + it("appends the latest watch emission", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const ready$ = source$.pipe(withLatestReady(watch$)); + const results: [number, string][] = []; + ready$.subscribe((n) => results.push(n)); + + watch$.next("ignored"); + watch$.next("watch"); + source$.next(1); + watch$.next("ignored"); + watch$.next("watch"); + source$.next(2); + + expect(results).toEqual([ + [1, "watch"], + [2, "watch"], + ]); + }); + + it("completes when its source completes", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const ready$ = source$.pipe(withLatestReady(watch$)); + let completed = false; + ready$.subscribe({ complete: () => (completed = true) }); + + source$.complete(); + + expect(completed).toBeTruthy(); + }); + + it("errors when its source errors", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const ready$ = source$.pipe(withLatestReady(watch$)); + const expected = { some: "error" }; + let error = null; + ready$.subscribe({ error: (e: unknown) => (error = e) }); + + source$.error(expected); + + expect(error).toEqual(expected); + }); + + it("errors when its watch errors", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const ready$ = source$.pipe(withLatestReady(watch$)); + const expected = { some: "error" }; + let error = null; + ready$.subscribe({ error: (e: unknown) => (error = e) }); + + watch$.error(expected); + + expect(error).toEqual(expected); + }); + + it("errors when its watch completes before emitting", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const ready$ = source$.pipe(withLatestReady(watch$)); + let error = null; + ready$.subscribe({ error: (e: unknown) => (error = e) }); + + watch$.complete(); + + expect(error).toBeInstanceOf(EmptyError); + }); +}); + +describe("on", () => { + it("connects when subscribed", () => { + const watch$ = new Subject(); + let connected = false; + const source$ = new Subject().pipe(tap({ subscribe: () => (connected = true) })); + + // precondition: on$ should be cold + const on$ = source$.pipe(on(watch$)); + expect(connected).toBeFalsy(); + + on$.subscribe(); + + expect(connected).toBeTruthy(); + }); + + it("suppresses source emissions until `on` emits", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const results: number[] = []; + source$.pipe(on(watch$)).subscribe((n) => results.push(n)); + + // precondition: on$ should be cold + source$.next(1); + expect(results).toEqual([]); + + watch$.next(); + + expect(results).toEqual([1]); + }); + + it("repeats source emissions when `on` emits", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const results: number[] = []; + source$.pipe(on(watch$)).subscribe((n) => results.push(n)); + source$.next(1); + + watch$.next(); + watch$.next(); + + expect(results).toEqual([1, 1]); + }); + + it("updates source emissions when `on` emits", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const results: number[] = []; + source$.pipe(on(watch$)).subscribe((n) => results.push(n)); + + source$.next(1); + watch$.next(); + source$.next(2); + watch$.next(); + + expect(results).toEqual([1, 2]); + }); + + it("emits a value when `on` emits before the source is ready", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const results: number[] = []; + source$.pipe(on(watch$)).subscribe((n) => results.push(n)); + + watch$.next(); + source$.next(1); + + expect(results).toEqual([1]); + }); + + it("ignores repeated `on` emissions before the source is ready", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const results: number[] = []; + source$.pipe(on(watch$)).subscribe((n) => results.push(n)); + + watch$.next(); + watch$.next(); + source$.next(1); + + expect(results).toEqual([1]); + }); + + it("emits only the latest source emission when `on` emits", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const results: number[] = []; + source$.pipe(on(watch$)).subscribe((n) => results.push(n)); + source$.next(1); + + watch$.next(); + + source$.next(2); + source$.next(3); + watch$.next(); + + expect(results).toEqual([1, 3]); + }); + + it("completes when its source completes", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + let complete: boolean = false; + source$.pipe(on(watch$)).subscribe({ complete: () => (complete = true) }); + + source$.complete(); + + expect(complete).toBeTruthy(); + }); + + it("completes when its watch completes", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + let complete: boolean = false; + source$.pipe(on(watch$)).subscribe({ complete: () => (complete = true) }); + + watch$.complete(); + + expect(complete).toBeTruthy(); + }); + + it("errors when its source errors", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const expected = { some: "error" }; + let error = null; + source$.pipe(on(watch$)).subscribe({ error: (e: unknown) => (error = e) }); + + source$.error(expected); + + expect(error).toEqual(expected); + }); + + it("errors when its watch errors", () => { + const watch$ = new Subject(); + const source$ = new Subject(); + const expected = { some: "error" }; + let error = null; + source$.pipe(on(watch$)).subscribe({ error: (e: unknown) => (error = e) }); + + watch$.error(expected); + + expect(error).toEqual(expected); + }); +}); diff --git a/libs/common/src/tools/rx.ts b/libs/common/src/tools/rx.ts index d2c5747a882..d5d0b499ff2 100644 --- a/libs/common/src/tools/rx.ts +++ b/libs/common/src/tools/rx.ts @@ -1,4 +1,21 @@ -import { map, distinctUntilChanged, OperatorFunction } from "rxjs"; +import { + map, + distinctUntilChanged, + OperatorFunction, + Observable, + ignoreElements, + endWith, + race, + pipe, + connect, + ReplaySubject, + concat, + zip, + first, + takeUntil, + withLatestFrom, + concatMap, +} from "rxjs"; /** * An observable operator that reduces an emitted collection to a single object, @@ -36,3 +53,109 @@ export function distinctIfShallowMatch(): OperatorFunction { return isDistinct; }); } + +/** Create an observable that, once subscribed, emits `true` then completes when + * any input completes. If an input is already complete when the subscription + * occurs, it emits immediately. + * @param watch$ the observable(s) to watch for completion; if an array is passed, + * null and undefined members are ignored. If `watch$` is empty, `anyComplete` + * will never complete. + * @returns An observable that emits `true` when any of its inputs + * complete. The observable forwards the first error from its input. + * @remarks This method is particularly useful in combination with `takeUntil` and + * streams that are not guaranteed to complete on their own. + */ +export function anyComplete(watch$: Observable | Observable[]): Observable { + if (Array.isArray(watch$)) { + const completes$ = watch$ + .filter((w$) => !!w$) + .map((w$) => w$.pipe(ignoreElements(), endWith(true))); + const completed$ = race(completes$); + return completed$; + } else { + return watch$.pipe(ignoreElements(), endWith(true)); + } +} + +/** + * Create an observable that delays the input stream until all watches have + * emitted a value. The watched values are not included in the source stream. + * The last emission from the source is output when all the watches have + * emitted at least once. + * @param watch$ the observable(s) to watch for readiness. If `watch$` is empty, + * `ready` will never emit. + * @returns An observable that emits when the source stream emits. The observable + * errors if one of its watches completes before emitting. It also errors if one + * of its watches errors. + */ +export function ready(watch$: Observable | Observable[]) { + const watching$ = Array.isArray(watch$) ? watch$ : [watch$]; + return pipe( + connect>((source$) => { + // this subscription is safe because `source$` connects only after there + // is an external subscriber. + const source = new ReplaySubject(1); + source$.subscribe(source); + + // `concat` is subscribed immediately after it's returned, at which point + // `zip` blocks until all items in `watching$` are ready. If that occurs + // after `source$` is hot, then the replay subject sends the last-captured + // emission through immediately. Otherwise, `ready` waits for the next + // emission + return concat(zip(watching$).pipe(first(), ignoreElements()), source).pipe( + takeUntil(anyComplete(source)), + ); + }), + ); +} + +export function withLatestReady( + watch$: Observable, +): OperatorFunction { + return connect((source$) => { + // these subscriptions are safe because `source$` connects only after there + // is an external subscriber. + const source = new ReplaySubject(1); + source$.subscribe(source); + const watch = new ReplaySubject(1); + watch$.subscribe(watch); + + // `concat` is subscribed immediately after it's returned, at which point + // `zip` blocks until all items in `watching$` are ready. If that occurs + // after `source$` is hot, then the replay subject sends the last-captured + // emission through immediately. Otherwise, `ready` waits for the next + // emission + return concat(zip(watch).pipe(first(), ignoreElements()), source).pipe( + withLatestFrom(watch), + takeUntil(anyComplete(source)), + ); + }); +} + +/** + * Create an observable that emits the latest value of the source stream + * when `watch$` emits. If `watch$` emits before the stream emits, then + * an emission occurs as soon as a value becomes ready. + * @param watch$ the observable that triggers emissions + * @returns An observable that emits when `watch$` emits. The observable + * errors if its source stream errors. It also errors if `on` errors. It + * completes if its watch completes. + * + * @remarks This works like `audit`, but it repeats emissions when + * watch$ fires. + */ +export function on(watch$: Observable) { + return pipe( + connect>((source$) => { + const source = new ReplaySubject(1); + source$.subscribe(source); + + return watch$ + .pipe( + ready(source), + concatMap(() => source.pipe(first())), + ) + .pipe(takeUntil(anyComplete(source))); + }), + ); +} diff --git a/libs/common/src/tools/send/models/domain/send.spec.ts b/libs/common/src/tools/send/models/domain/send.spec.ts index 5b1d7e73dae..74c0e77b394 100644 --- a/libs/common/src/tools/send/models/domain/send.spec.ts +++ b/libs/common/src/tools/send/models/domain/send.spec.ts @@ -3,8 +3,8 @@ import { mock } from "jest-mock-extended"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { UserKey } from "@bitwarden/common/types/key"; +import { KeyService } from "../../../../../../key-management/src/abstractions/key.service"; import { makeStaticByteArray, mockEnc } from "../../../../../spec"; -import { CryptoService } from "../../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../../platform/abstractions/encrypt.service"; import { ContainerService } from "../../../../platform/services/container.service"; import { SendType } from "../../enums/send-type"; @@ -111,14 +111,14 @@ describe("Send", () => { send.hideEmail = true; const encryptService = mock(); - const cryptoService = mock(); + const keyService = mock(); encryptService.decryptToBytes .calledWith(send.key, userKey) .mockResolvedValue(makeStaticByteArray(32)); - cryptoService.makeSendKey.mockResolvedValue("cryptoKey" as any); - cryptoService.getUserKey.mockResolvedValue(userKey); + keyService.makeSendKey.mockResolvedValue("cryptoKey" as any); + keyService.getUserKey.mockResolvedValue(userKey); - (window as any).bitwardenContainerService = new ContainerService(cryptoService, encryptService); + (window as any).bitwardenContainerService = new ContainerService(keyService, encryptService); const view = await send.decrypt(); diff --git a/libs/common/src/tools/send/models/domain/send.ts b/libs/common/src/tools/send/models/domain/send.ts index 41d1fecc10b..6e53813a368 100644 --- a/libs/common/src/tools/send/models/domain/send.ts +++ b/libs/common/src/tools/send/models/domain/send.ts @@ -72,13 +72,13 @@ export class Send extends Domain { async decrypt(): Promise { const model = new SendView(this); - const cryptoService = Utils.getContainerService().getCryptoService(); + const keyService = Utils.getContainerService().getKeyService(); const encryptService = Utils.getContainerService().getEncryptService(); try { - const sendKeyEncryptionKey = await cryptoService.getUserKey(); + const sendKeyEncryptionKey = await keyService.getUserKey(); model.key = await encryptService.decryptToBytes(this.key, sendKeyEncryptionKey); - model.cryptoKey = await cryptoService.makeSendKey(model.key); + model.cryptoKey = await keyService.makeSendKey(model.key); } catch (e) { // TODO: error? } diff --git a/libs/common/src/tools/send/services/send.service.abstraction.ts b/libs/common/src/tools/send/services/send.service.abstraction.ts index 4fa927942c1..866a661b4a4 100644 --- a/libs/common/src/tools/send/services/send.service.abstraction.ts +++ b/libs/common/src/tools/send/services/send.service.abstraction.ts @@ -1,6 +1,6 @@ import { Observable } from "rxjs"; -import { UserKeyRotationDataProvider } from "@bitwarden/auth/common"; +import { UserKeyRotationDataProvider } from "@bitwarden/key-management"; import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; diff --git a/libs/common/src/tools/send/services/send.service.spec.ts b/libs/common/src/tools/send/services/send.service.spec.ts index 5743eff481b..5aca3a4b5c9 100644 --- a/libs/common/src/tools/send/services/send.service.spec.ts +++ b/libs/common/src/tools/send/services/send.service.spec.ts @@ -4,6 +4,7 @@ import { firstValueFrom, of } from "rxjs"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { SelfHostedEnvironment } from "@bitwarden/common/platform/services/default-environment.service"; +import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; import { FakeAccountService, FakeActiveUserState, @@ -11,7 +12,6 @@ import { awaitAsync, mockAccountServiceWith, } from "../../../../spec"; -import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service"; @@ -40,7 +40,7 @@ import { } from "./test-data/send-tests.data"; describe("SendService", () => { - const cryptoService = mock(); + const keyService = mock(); const i18nService = mock(); const keyGenerationService = mock(); const encryptService = mock(); @@ -65,7 +65,7 @@ describe("SendService", () => { get: () => of(new SelfHostedEnvironment({ webVault: "https://example.com" })), }); - (window as any).bitwardenContainerService = new ContainerService(cryptoService, encryptService); + (window as any).bitwardenContainerService = new ContainerService(keyService, encryptService); accountService.activeAccountSubject.next({ id: mockUserId, @@ -84,7 +84,7 @@ describe("SendService", () => { decryptedState.nextState([testSendViewData("1", "Test Send")]); sendService = new SendService( - cryptoService, + keyService, i18nService, keyGenerationService, sendStateProvider, diff --git a/libs/common/src/tools/send/services/send.service.ts b/libs/common/src/tools/send/services/send.service.ts index 25937e7da1f..3ba1cb92e2c 100644 --- a/libs/common/src/tools/send/services/send.service.ts +++ b/libs/common/src/tools/send/services/send.service.ts @@ -1,7 +1,7 @@ import { Observable, concatMap, distinctUntilChanged, firstValueFrom, map } from "rxjs"; +import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; import { PBKDF2KdfConfig } from "../../../auth/models/domain/kdf-config"; -import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service"; @@ -37,7 +37,7 @@ export class SendService implements InternalSendServiceAbstraction { ); constructor( - private cryptoService: CryptoService, + private keyService: KeyService, private i18nService: I18nService, private keyGenerationService: KeyGenerationService, private stateProvider: SendStateProvider, @@ -77,7 +77,7 @@ export class SendService implements InternalSendServiceAbstraction { send.password = passwordKey.keyB64; } if (key == null) { - key = await this.cryptoService.getUserKey(); + key = await this.keyService.getUserKey(); } send.key = await this.encryptService.encrypt(model.key, key); send.name = await this.encryptService.encrypt(model.name, model.cryptoKey); @@ -197,7 +197,7 @@ export class SendService implements InternalSendServiceAbstraction { } decSends = []; - const hasKey = await this.cryptoService.hasUserKey(); + const hasKey = await this.keyService.hasUserKey(); if (!hasKey) { throw new Error("No user key found."); } @@ -322,7 +322,7 @@ export class SendService implements InternalSendServiceAbstraction { key: SymmetricCryptoKey, ): Promise<[EncString, EncArrayBuffer]> { if (key == null) { - key = await this.cryptoService.getUserKey(); + key = await this.keyService.getUserKey(); } const encFileName = await this.encryptService.encrypt(fileName, key); const encFileData = await this.encryptService.encryptToBytes(new Uint8Array(data), key); diff --git a/libs/common/src/tools/state/classified-format.ts b/libs/common/src/tools/state/classified-format.ts index 93147a0fb53..26aca0197c5 100644 --- a/libs/common/src/tools/state/classified-format.ts +++ b/libs/common/src/tools/state/classified-format.ts @@ -17,3 +17,9 @@ export type ClassifiedFormat = { */ readonly disclosed: Jsonify; }; + +export function isClassifiedFormat( + value: any, +): value is ClassifiedFormat { + return "id" in value && "secret" in value && "disclosed" in value; +} diff --git a/libs/common/src/tools/state/identity-state-constraint.ts b/libs/common/src/tools/state/identity-state-constraint.ts index ff7712b9091..df33dad543a 100644 --- a/libs/common/src/tools/state/identity-state-constraint.ts +++ b/libs/common/src/tools/state/identity-state-constraint.ts @@ -1,4 +1,11 @@ -import { Constraints, StateConstraints } from "../types"; +import { BehaviorSubject, Observable } from "rxjs"; + +import { + Constraints, + DynamicStateConstraints, + StateConstraints, + SubjectConstraints, +} from "../types"; // The constraints type shares the properties of the state, // but never has any members @@ -9,16 +16,31 @@ const EMPTY_CONSTRAINTS = new Proxy(Object.freeze({}), { }); /** A constraint that does nothing. */ -export class IdentityConstraint implements StateConstraints { +export class IdentityConstraint + implements StateConstraints, DynamicStateConstraints +{ /** Instantiate the identity constraint */ constructor() {} readonly constraints: Readonly> = EMPTY_CONSTRAINTS; + calibrate() { + return this; + } + adjust(state: State) { return state; } + fix(state: State) { return state; } } + +/** Emits a constraint that does not alter the input state. */ +export function unconstrained$(): Observable> { + const identity = new IdentityConstraint(); + const constraints$ = new BehaviorSubject(identity); + + return constraints$; +} diff --git a/libs/common/src/tools/state/object-key.ts b/libs/common/src/tools/state/object-key.ts new file mode 100644 index 00000000000..88365d5cbd1 --- /dev/null +++ b/libs/common/src/tools/state/object-key.ts @@ -0,0 +1,53 @@ +import { UserKeyDefinition, UserKeyDefinitionOptions } from "../../platform/state"; +// eslint-disable-next-line -- `StateDefinition` used as a type +import type { StateDefinition } from "../../platform/state/state-definition"; + +import { ClassifiedFormat } from "./classified-format"; +import { Classifier } from "./classifier"; + +/** A key for storing JavaScript objects (`{ an: "example" }`) + * in a UserStateSubject. + */ +// FIXME: promote to class: `ObjectConfiguration`. +// The class receives `encryptor`, `prepareNext`, `adjust`, and `fix` +// From `UserStateSubject`. `UserStateSubject` keeps `classify` and +// `declassify`. The class should also include serialization +// facilities (to be used in place of JSON.parse/stringify) in it's +// options. Also allow swap between "classifier" and "classification"; the +// latter is a list of properties/arguments to the specific classifier in-use. +export type ObjectKey> = { + target: "object"; + key: string; + state: StateDefinition; + classifier: Classifier; + format: "plain" | "classified"; + options: UserKeyDefinitionOptions; +}; + +export function isObjectKey(key: any): key is ObjectKey { + return key.target === "object" && "format" in key && "classifier" in key; +} + +export function toUserKeyDefinition( + key: ObjectKey, +) { + if (key.format === "plain") { + const plain = new UserKeyDefinition(key.state, key.key, key.options); + + return plain; + } else if (key.format === "classified") { + const classified = new UserKeyDefinition>( + key.state, + key.key, + { + cleanupDelayMs: key.options.cleanupDelayMs, + deserializer: (jsonValue) => jsonValue as ClassifiedFormat, + clearOn: key.options.clearOn, + }, + ); + + return classified; + } else { + throw new Error(`unknown format: ${key.format}`); + } +} diff --git a/libs/common/src/tools/state/state-constraints-dependency.ts b/libs/common/src/tools/state/state-constraints-dependency.ts index 66bac636bd7..427ff42e7a4 100644 --- a/libs/common/src/tools/state/state-constraints-dependency.ts +++ b/libs/common/src/tools/state/state-constraints-dependency.ts @@ -1,6 +1,6 @@ import { Observable } from "rxjs"; -import { DynamicStateConstraints, StateConstraints } from "../types"; +import { DynamicStateConstraints, StateConstraints, SubjectConstraints } from "../types"; /** A pattern for types that depend upon a dynamic set of constraints. * @@ -10,12 +10,12 @@ import { DynamicStateConstraints, StateConstraints } from "../types"; * last-emitted constraints. If `constraints$` completes, the consumer should * continue using the last-emitted constraints. */ -export type StateConstraintsDependency = { +export type SubjectConstraintsDependency = { /** A stream that emits constraints when subscribed and when the * constraints change. The stream should not emit `null` or * `undefined`. */ - constraints$: Observable | DynamicStateConstraints>; + constraints$: Observable>; }; /** Returns `true` if the input constraint is a `DynamicStateConstraints`. diff --git a/libs/common/src/tools/state/user-state-subject-dependencies.ts b/libs/common/src/tools/state/user-state-subject-dependencies.ts index 7f36ab7cae8..0ba842334bf 100644 --- a/libs/common/src/tools/state/user-state-subject-dependencies.ts +++ b/libs/common/src/tools/state/user-state-subject-dependencies.ts @@ -1,15 +1,23 @@ -import { Simplify } from "type-fest"; +import { RequireExactlyOne, Simplify } from "type-fest"; -import { Dependencies, SingleUserDependency, WhenDependency } from "../dependencies"; +import { + Dependencies, + SingleUserDependency, + SingleUserEncryptorDependency, + WhenDependency, +} from "../dependencies"; -import { StateConstraintsDependency } from "./state-constraints-dependency"; +import { SubjectConstraintsDependency } from "./state-constraints-dependency"; /** dependencies accepted by the user state subject */ export type UserStateSubjectDependencies = Simplify< - SingleUserDependency & + RequireExactlyOne< + SingleUserDependency & SingleUserEncryptorDependency, + "singleUserEncryptor$" | "singleUserId$" + > & Partial & Partial> & - Partial> & { + Partial> & { /** Compute the next stored value. If this is not set, values * provided to `next` unconditionally override state. * @param current the value stored in state diff --git a/libs/common/src/tools/state/user-state-subject.spec.ts b/libs/common/src/tools/state/user-state-subject.spec.ts index 73971da4ef9..9f5475df9de 100644 --- a/libs/common/src/tools/state/user-state-subject.spec.ts +++ b/libs/common/src/tools/state/user-state-subject.spec.ts @@ -1,14 +1,50 @@ import { BehaviorSubject, of, Subject } from "rxjs"; +import { GENERATOR_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state"; import { UserId } from "@bitwarden/common/types/guid"; import { awaitAsync, FakeSingleUserState, ObservableTracker } from "../../../spec"; +import { UserBound } from "../dependencies"; +import { PrivateClassifier } from "../private-classifier"; import { StateConstraints } from "../types"; +import { ClassifiedFormat } from "./classified-format"; +import { ObjectKey } from "./object-key"; +import { UserEncryptor } from "./user-encryptor.abstraction"; import { UserStateSubject } from "./user-state-subject"; const SomeUser = "some user" as UserId; type TestType = { foo: string }; +const SomeKey = new UserKeyDefinition(GENERATOR_DISK, "TestKey", { + deserializer: (d) => d as TestType, + clearOn: [], +}); + +const SomeObjectKey = { + target: "object", + key: "TestObjectKey", + state: GENERATOR_DISK, + classifier: new PrivateClassifier(), + format: "classified", + options: { + deserializer: (d) => d as TestType, + clearOn: ["logout"], + }, +} satisfies ObjectKey; + +const SomeEncryptor: UserEncryptor = { + userId: SomeUser, + + encrypt(secret) { + const tmp: any = secret; + return Promise.resolve({ foo: `encrypt(${tmp.foo})` } as any); + }, + + decrypt(secret) { + const tmp: any = JSON.parse(secret.encryptedString); + return Promise.resolve({ foo: `decrypt(${tmp.foo})` } as any); + }, +}; function fooMaxLength(maxLength: number): StateConstraints { return Object.freeze({ @@ -43,7 +79,11 @@ describe("UserStateSubject", () => { const singleUserId$ = new BehaviorSubject(SomeUser); const nextValue = jest.fn((_, next) => next); const when$ = new BehaviorSubject(true); - const subject = new UserStateSubject(state, { singleUserId$, nextValue, when$ }); + const subject = new UserStateSubject(SomeKey, () => state, { + singleUserId$, + nextValue, + when$, + }); // the interleaved await asyncs are only necessary b/c `nextValue` is called asynchronously subject.next({ foo: "next" }); @@ -65,7 +105,11 @@ describe("UserStateSubject", () => { const singleUserId$ = new BehaviorSubject(SomeUser); const nextValue = jest.fn((_, next) => next); const when$ = new BehaviorSubject(true); - const subject = new UserStateSubject(state, { singleUserId$, nextValue, when$ }); + const subject = new UserStateSubject(SomeKey, () => state, { + singleUserId$, + nextValue, + when$, + }); // the interleaved await asyncs are only necessary b/c `nextValue` is called asynchronously subject.next({ foo: "next" }); @@ -79,11 +123,35 @@ describe("UserStateSubject", () => { expect(nextValue).toHaveBeenCalledTimes(1); }); + it("ignores repeated singleUserEncryptor$ emissions", async () => { + // this test looks for `nextValue` because a subscription isn't necessary for + // the subject to update + const initialValue: TestType = { foo: "init" }; + const state = new FakeSingleUserState(SomeUser, initialValue); + const nextValue = jest.fn((_, next) => next); + const singleUserEncryptor$ = new BehaviorSubject({ userId: SomeUser, encryptor: null }); + const subject = new UserStateSubject(SomeKey, () => state, { + nextValue, + singleUserEncryptor$, + }); + + // the interleaved await asyncs are only necessary b/c `nextValue` is called asynchronously + subject.next({ foo: "next" }); + await awaitAsync(); + singleUserEncryptor$.next({ userId: SomeUser, encryptor: null }); + await awaitAsync(); + singleUserEncryptor$.next({ userId: SomeUser, encryptor: null }); + singleUserEncryptor$.next({ userId: SomeUser, encryptor: null }); + await awaitAsync(); + + expect(nextValue).toHaveBeenCalledTimes(1); + }); + it("waits for constraints$", async () => { const state = new FakeSingleUserState(SomeUser, { foo: "init" }); const singleUserId$ = new BehaviorSubject(SomeUser); const constraints$ = new Subject>(); - const subject = new UserStateSubject(state, { singleUserId$, constraints$ }); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$, constraints$ }); const tracker = new ObservableTracker(subject); constraints$.next(fooMaxLength(3)); @@ -91,13 +159,28 @@ describe("UserStateSubject", () => { expect(initResult).toEqual({ foo: "ini" }); }); + + it("waits for singleUserEncryptor$", async () => { + const state = new FakeSingleUserState>>( + SomeUser, + { id: null, secret: '{"foo":"init"}', disclosed: {} }, + ); + const singleUserEncryptor$ = new Subject>(); + const subject = new UserStateSubject(SomeObjectKey, () => state, { singleUserEncryptor$ }); + const tracker = new ObservableTracker(subject); + + singleUserEncryptor$.next({ userId: SomeUser, encryptor: SomeEncryptor }); + const [initResult] = await tracker.pauseUntilReceived(1); + + expect(initResult).toEqual({ foo: "decrypt(init)" }); + }); }); describe("next", () => { it("emits the next value", async () => { const state = new FakeSingleUserState(SomeUser, { foo: "init" }); const singleUserId$ = new BehaviorSubject(SomeUser); - const subject = new UserStateSubject(state, { singleUserId$ }); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$ }); const expected: TestType = { foo: "next" }; let actual: TestType = null; @@ -114,7 +197,7 @@ describe("UserStateSubject", () => { const initialState = { foo: "init" }; const state = new FakeSingleUserState(SomeUser, initialState); const singleUserId$ = new BehaviorSubject(SomeUser); - const subject = new UserStateSubject(state, { singleUserId$ }); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$ }); let actual: TestType = null; subject.subscribe((value) => { @@ -132,7 +215,7 @@ describe("UserStateSubject", () => { const state = new FakeSingleUserState(SomeUser, initialValue); const singleUserId$ = new BehaviorSubject(SomeUser); const shouldUpdate = jest.fn(() => true); - const subject = new UserStateSubject(state, { singleUserId$, shouldUpdate }); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$, shouldUpdate }); const nextVal: TestType = { foo: "next" }; subject.next(nextVal); @@ -147,7 +230,7 @@ describe("UserStateSubject", () => { const singleUserId$ = new BehaviorSubject(SomeUser); const shouldUpdate = jest.fn(() => true); const dependencyValue = { bar: "dependency" }; - const subject = new UserStateSubject(state, { + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$, shouldUpdate, dependencies$: of(dependencyValue), @@ -165,7 +248,7 @@ describe("UserStateSubject", () => { const state = new FakeSingleUserState(SomeUser, initialValue); const singleUserId$ = new BehaviorSubject(SomeUser); const shouldUpdate = jest.fn(() => true); - const subject = new UserStateSubject(state, { singleUserId$, shouldUpdate }); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$, shouldUpdate }); const expected: TestType = { foo: "next" }; let actual: TestType = null; @@ -183,7 +266,7 @@ describe("UserStateSubject", () => { const state = new FakeSingleUserState(SomeUser, initialValue); const singleUserId$ = new BehaviorSubject(SomeUser); const shouldUpdate = jest.fn(() => false); - const subject = new UserStateSubject(state, { singleUserId$, shouldUpdate }); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$, shouldUpdate }); subject.next({ foo: "next" }); await awaitAsync(); @@ -200,7 +283,7 @@ describe("UserStateSubject", () => { const state = new FakeSingleUserState(SomeUser, initialValue); const singleUserId$ = new BehaviorSubject(SomeUser); const nextValue = jest.fn((_, next) => next); - const subject = new UserStateSubject(state, { singleUserId$, nextValue }); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$, nextValue }); const nextVal: TestType = { foo: "next" }; subject.next(nextVal); @@ -215,7 +298,7 @@ describe("UserStateSubject", () => { const singleUserId$ = new BehaviorSubject(SomeUser); const nextValue = jest.fn((_, next) => next); const dependencyValue = { bar: "dependency" }; - const subject = new UserStateSubject(state, { + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$, nextValue, dependencies$: of(dependencyValue), @@ -236,7 +319,11 @@ describe("UserStateSubject", () => { const singleUserId$ = new BehaviorSubject(SomeUser); const nextValue = jest.fn((_, next) => next); const when$ = new BehaviorSubject(true); - const subject = new UserStateSubject(state, { singleUserId$, nextValue, when$ }); + const subject = new UserStateSubject(SomeKey, () => state, { + singleUserId$, + nextValue, + when$, + }); const nextVal: TestType = { foo: "next" }; subject.next(nextVal); @@ -253,7 +340,11 @@ describe("UserStateSubject", () => { const singleUserId$ = new BehaviorSubject(SomeUser); const nextValue = jest.fn((_, next) => next); const when$ = new BehaviorSubject(false); - const subject = new UserStateSubject(state, { singleUserId$, nextValue, when$ }); + const subject = new UserStateSubject(SomeKey, () => state, { + singleUserId$, + nextValue, + when$, + }); const nextVal: TestType = { foo: "next" }; subject.next(nextVal); @@ -265,42 +356,52 @@ describe("UserStateSubject", () => { expect(nextValue).toHaveBeenCalled(); }); - it("waits to evaluate nextValue until singleUserId$ emits", async () => { - // this test looks for `nextValue` because a subscription isn't necessary for + it("waits to evaluate `UserState.update` until singleUserId$ emits", async () => { + // this test looks for `nextMock` because a subscription isn't necessary for // the subject to update. const initialValue: TestType = { foo: "init" }; const state = new FakeSingleUserState(SomeUser, initialValue); const singleUserId$ = new Subject(); - const nextValue = jest.fn((_, next) => next); - const subject = new UserStateSubject(state, { singleUserId$, nextValue }); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$ }); + // precondition: subject doesn't update after `next` const nextVal: TestType = { foo: "next" }; subject.next(nextVal); await awaitAsync(); - expect(nextValue).not.toHaveBeenCalled(); + expect(state.nextMock).not.toHaveBeenCalled(); + singleUserId$.next(SomeUser); await awaitAsync(); - expect(nextValue).toHaveBeenCalled(); + expect(state.nextMock).toHaveBeenCalledWith({ foo: "next" }); }); - it("applies constraints$ on init", async () => { - const state = new FakeSingleUserState(SomeUser, { foo: "init" }); - const singleUserId$ = new BehaviorSubject(SomeUser); - const constraints$ = new BehaviorSubject(fooMaxLength(2)); - const subject = new UserStateSubject(state, { singleUserId$, constraints$ }); - const tracker = new ObservableTracker(subject); + it("waits to evaluate `UserState.update` until singleUserEncryptor$ emits", async () => { + const state = new FakeSingleUserState>>( + SomeUser, + { id: null, secret: '{"foo":"init"}', disclosed: null }, + ); + const singleUserEncryptor$ = new Subject>(); + const subject = new UserStateSubject(SomeObjectKey, () => state, { singleUserEncryptor$ }); - const [result] = await tracker.pauseUntilReceived(1); + // precondition: subject doesn't update after `next` + const nextVal: TestType = { foo: "next" }; + subject.next(nextVal); + await awaitAsync(); + expect(state.nextMock).not.toHaveBeenCalled(); - expect(result).toEqual({ foo: "in" }); + singleUserEncryptor$.next({ userId: SomeUser, encryptor: SomeEncryptor }); + await awaitAsync(); + + const encrypted = { foo: "encrypt(next)" }; + expect(state.nextMock).toHaveBeenCalledWith({ id: null, secret: encrypted, disclosed: null }); }); it("applies dynamic constraints", async () => { const state = new FakeSingleUserState(SomeUser, { foo: "init" }); const singleUserId$ = new BehaviorSubject(SomeUser); const constraints$ = new BehaviorSubject(DynamicFooMaxLength); - const subject = new UserStateSubject(state, { singleUserId$, constraints$ }); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$, constraints$ }); const tracker = new ObservableTracker(subject); const expected: TestType = { foo: "next" }; const emission = tracker.expectEmission(); @@ -311,24 +412,11 @@ describe("UserStateSubject", () => { expect(actual).toEqual({ foo: "" }); }); - it("applies constraints$ on constraints$ emission", async () => { - const state = new FakeSingleUserState(SomeUser, { foo: "init" }); - const singleUserId$ = new BehaviorSubject(SomeUser); - const constraints$ = new BehaviorSubject(fooMaxLength(2)); - const subject = new UserStateSubject(state, { singleUserId$, constraints$ }); - const tracker = new ObservableTracker(subject); - - constraints$.next(fooMaxLength(1)); - const [, result] = await tracker.pauseUntilReceived(2); - - expect(result).toEqual({ foo: "i" }); - }); - it("applies constraints$ on next", async () => { const state = new FakeSingleUserState(SomeUser, { foo: "init" }); const singleUserId$ = new BehaviorSubject(SomeUser); const constraints$ = new BehaviorSubject(fooMaxLength(2)); - const subject = new UserStateSubject(state, { singleUserId$, constraints$ }); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$, constraints$ }); const tracker = new ObservableTracker(subject); subject.next({ foo: "next" }); @@ -341,7 +429,7 @@ describe("UserStateSubject", () => { const state = new FakeSingleUserState(SomeUser, { foo: "init" }); const singleUserId$ = new BehaviorSubject(SomeUser); const constraints$ = new BehaviorSubject(fooMaxLength(2)); - const subject = new UserStateSubject(state, { singleUserId$, constraints$ }); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$, constraints$ }); const tracker = new ObservableTracker(subject); constraints$.next(fooMaxLength(3)); @@ -355,13 +443,17 @@ describe("UserStateSubject", () => { const state = new FakeSingleUserState(SomeUser, { foo: "init" }); const singleUserId$ = new BehaviorSubject(SomeUser); const constraints$ = new Subject>(); - const subject = new UserStateSubject(state, { singleUserId$, constraints$ }); - const tracker = new ObservableTracker(subject); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$, constraints$ }); + const results: any[] = []; + subject.subscribe((r) => { + results.push(r); + }); subject.next({ foo: "next" }); constraints$.next(fooMaxLength(3)); + await awaitAsync(); // `init` is also waiting and is processed before `next` - const [, nextResult] = await tracker.pauseUntilReceived(2); + const [, nextResult] = results; expect(nextResult).toEqual({ foo: "nex" }); }); @@ -370,7 +462,7 @@ describe("UserStateSubject", () => { const state = new FakeSingleUserState(SomeUser, { foo: "init" }); const singleUserId$ = new BehaviorSubject(SomeUser); const constraints$ = new BehaviorSubject(fooMaxLength(3)); - const subject = new UserStateSubject(state, { singleUserId$, constraints$ }); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$, constraints$ }); const tracker = new ObservableTracker(subject); constraints$.error({ some: "error" }); @@ -384,7 +476,7 @@ describe("UserStateSubject", () => { const state = new FakeSingleUserState(SomeUser, { foo: "init" }); const singleUserId$ = new BehaviorSubject(SomeUser); const constraints$ = new BehaviorSubject(fooMaxLength(3)); - const subject = new UserStateSubject(state, { singleUserId$, constraints$ }); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$, constraints$ }); const tracker = new ObservableTracker(subject); constraints$.complete(); @@ -399,7 +491,7 @@ describe("UserStateSubject", () => { it("emits errors", async () => { const state = new FakeSingleUserState(SomeUser, { foo: "init" }); const singleUserId$ = new BehaviorSubject(SomeUser); - const subject = new UserStateSubject(state, { singleUserId$ }); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$ }); const expected: TestType = { foo: "error" }; let actual: TestType = null; @@ -418,7 +510,7 @@ describe("UserStateSubject", () => { const initialState = { foo: "init" }; const state = new FakeSingleUserState(SomeUser, initialState); const singleUserId$ = new BehaviorSubject(SomeUser); - const subject = new UserStateSubject(state, { singleUserId$ }); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$ }); let actual: TestType = null; subject.subscribe({ @@ -437,7 +529,7 @@ describe("UserStateSubject", () => { const initialState = { foo: "init" }; const state = new FakeSingleUserState(SomeUser, initialState); const singleUserId$ = new BehaviorSubject(SomeUser); - const subject = new UserStateSubject(state, { singleUserId$ }); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$ }); let shouldNotRun = false; subject.subscribe({ @@ -457,7 +549,7 @@ describe("UserStateSubject", () => { it("emits completes", async () => { const state = new FakeSingleUserState(SomeUser, { foo: "init" }); const singleUserId$ = new BehaviorSubject(SomeUser); - const subject = new UserStateSubject(state, { singleUserId$ }); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$ }); let actual = false; subject.subscribe({ @@ -475,7 +567,7 @@ describe("UserStateSubject", () => { const initialState = { foo: "init" }; const state = new FakeSingleUserState(SomeUser, initialState); const singleUserId$ = new BehaviorSubject(SomeUser); - const subject = new UserStateSubject(state, { singleUserId$ }); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$ }); let shouldNotRun = false; subject.subscribe({ @@ -496,7 +588,7 @@ describe("UserStateSubject", () => { const initialState = { foo: "init" }; const state = new FakeSingleUserState(SomeUser, initialState); const singleUserId$ = new BehaviorSubject(SomeUser); - const subject = new UserStateSubject(state, { singleUserId$ }); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$ }); let timesRun = 0; subject.subscribe({ @@ -513,11 +605,36 @@ describe("UserStateSubject", () => { }); describe("subscribe", () => { + it("applies constraints$ on init", async () => { + const state = new FakeSingleUserState(SomeUser, { foo: "init" }); + const singleUserId$ = new BehaviorSubject(SomeUser); + const constraints$ = new BehaviorSubject(fooMaxLength(2)); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$, constraints$ }); + const tracker = new ObservableTracker(subject); + + const [result] = await tracker.pauseUntilReceived(1); + + expect(result).toEqual({ foo: "in" }); + }); + + it("applies constraints$ on constraints$ emission", async () => { + const state = new FakeSingleUserState(SomeUser, { foo: "init" }); + const singleUserId$ = new BehaviorSubject(SomeUser); + const constraints$ = new BehaviorSubject(fooMaxLength(2)); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$, constraints$ }); + const tracker = new ObservableTracker(subject); + + constraints$.next(fooMaxLength(1)); + const [, result] = await tracker.pauseUntilReceived(2); + + expect(result).toEqual({ foo: "i" }); + }); + it("completes when singleUserId$ completes", async () => { const initialValue: TestType = { foo: "init" }; const state = new FakeSingleUserState(SomeUser, initialValue); const singleUserId$ = new BehaviorSubject(SomeUser); - const subject = new UserStateSubject(state, { singleUserId$ }); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$ }); let actual = false; subject.subscribe({ @@ -531,12 +648,32 @@ describe("UserStateSubject", () => { expect(actual).toBeTruthy(); }); + it("completes when singleUserId$ completes", async () => { + const state = new FakeSingleUserState>>( + SomeUser, + { id: null, secret: '{"foo":"init"}', disclosed: null }, + ); + const singleUserEncryptor$ = new Subject>(); + const subject = new UserStateSubject(SomeObjectKey, () => state, { singleUserEncryptor$ }); + + let actual = false; + subject.subscribe({ + complete: () => { + actual = true; + }, + }); + singleUserEncryptor$.complete(); + await awaitAsync(); + + expect(actual).toBeTruthy(); + }); + it("completes when when$ completes", async () => { const initialValue: TestType = { foo: "init" }; const state = new FakeSingleUserState(SomeUser, initialValue); const singleUserId$ = new BehaviorSubject(SomeUser); const when$ = new BehaviorSubject(true); - const subject = new UserStateSubject(state, { singleUserId$, when$ }); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$, when$ }); let actual = false; subject.subscribe({ @@ -557,7 +694,7 @@ describe("UserStateSubject", () => { const initialValue: TestType = { foo: "init" }; const state = new FakeSingleUserState(SomeUser, initialValue); const singleUserId$ = new BehaviorSubject(SomeUser); - const subject = new UserStateSubject(state, { singleUserId$ }); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$ }); const errorUserId = "error" as UserId; let error = false; @@ -572,11 +709,32 @@ describe("UserStateSubject", () => { expect(error).toEqual({ expectedUserId: SomeUser, actualUserId: errorUserId }); }); + it("errors when singleUserEncryptor$ changes", async () => { + const state = new FakeSingleUserState>>( + SomeUser, + { id: null, secret: '{"foo":"init"}', disclosed: null }, + ); + const singleUserEncryptor$ = new Subject>(); + const subject = new UserStateSubject(SomeObjectKey, () => state, { singleUserEncryptor$ }); + const errorUserId = "error" as UserId; + + let error = false; + subject.subscribe({ + error: (e: unknown) => { + error = e as any; + }, + }); + singleUserEncryptor$.next({ userId: errorUserId, encryptor: SomeEncryptor }); + await awaitAsync(); + + expect(error).toEqual({ expectedUserId: SomeUser, actualUserId: errorUserId }); + }); + it("errors when singleUserId$ errors", async () => { const initialValue: TestType = { foo: "init" }; const state = new FakeSingleUserState(SomeUser, initialValue); const singleUserId$ = new BehaviorSubject(SomeUser); - const subject = new UserStateSubject(state, { singleUserId$ }); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$ }); const expected = { error: "description" }; let actual = false; @@ -591,12 +749,31 @@ describe("UserStateSubject", () => { expect(actual).toEqual(expected); }); + it("errors when singleUserEncryptor$ errors", async () => { + const initialValue: TestType = { foo: "init" }; + const state = new FakeSingleUserState(SomeUser, initialValue); + const singleUserEncryptor$ = new Subject>(); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserEncryptor$ }); + const expected = { error: "description" }; + + let actual = false; + subject.subscribe({ + error: (e: unknown) => { + actual = e as any; + }, + }); + singleUserEncryptor$.error(expected); + await awaitAsync(); + + expect(actual).toEqual(expected); + }); + it("errors when when$ errors", async () => { const initialValue: TestType = { foo: "init" }; const state = new FakeSingleUserState(SomeUser, initialValue); const singleUserId$ = new BehaviorSubject(SomeUser); const when$ = new BehaviorSubject(true); - const subject = new UserStateSubject(state, { singleUserId$, when$ }); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$, when$ }); const expected = { error: "description" }; let actual = false; @@ -616,7 +793,7 @@ describe("UserStateSubject", () => { it("returns the userId to which the subject is bound", () => { const state = new FakeSingleUserState(SomeUser, { foo: "init" }); const singleUserId$ = new Subject(); - const subject = new UserStateSubject(state, { singleUserId$ }); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$ }); expect(subject.userId).toEqual(SomeUser); }); @@ -626,7 +803,7 @@ describe("UserStateSubject", () => { it("emits the next value with an empty constraint", async () => { const state = new FakeSingleUserState(SomeUser, { foo: "init" }); const singleUserId$ = new BehaviorSubject(SomeUser); - const subject = new UserStateSubject(state, { singleUserId$ }); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$ }); const tracker = new ObservableTracker(subject.withConstraints$); const expected: TestType = { foo: "next" }; const emission = tracker.expectEmission(); @@ -642,7 +819,7 @@ describe("UserStateSubject", () => { const initialState = { foo: "init" }; const state = new FakeSingleUserState(SomeUser, initialState); const singleUserId$ = new BehaviorSubject(SomeUser); - const subject = new UserStateSubject(state, { singleUserId$ }); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$ }); const tracker = new ObservableTracker(subject.withConstraints$); subject.complete(); @@ -657,7 +834,7 @@ describe("UserStateSubject", () => { const state = new FakeSingleUserState(SomeUser, { foo: "init" }); const singleUserId$ = new BehaviorSubject(SomeUser); const constraints$ = new BehaviorSubject(fooMaxLength(2)); - const subject = new UserStateSubject(state, { singleUserId$, constraints$ }); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$, constraints$ }); const tracker = new ObservableTracker(subject.withConstraints$); const expected = fooMaxLength(1); const emission = tracker.expectEmission(); @@ -673,7 +850,7 @@ describe("UserStateSubject", () => { const state = new FakeSingleUserState(SomeUser, { foo: "init" }); const singleUserId$ = new BehaviorSubject(SomeUser); const constraints$ = new BehaviorSubject(DynamicFooMaxLength); - const subject = new UserStateSubject(state, { singleUserId$, constraints$ }); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$, constraints$ }); const tracker = new ObservableTracker(subject.withConstraints$); const expected: TestType = { foo: "next" }; const emission = tracker.expectEmission(); @@ -690,7 +867,7 @@ describe("UserStateSubject", () => { const singleUserId$ = new BehaviorSubject(SomeUser); const expected = fooMaxLength(2); const constraints$ = new BehaviorSubject(expected); - const subject = new UserStateSubject(state, { singleUserId$, constraints$ }); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$, constraints$ }); const tracker = new ObservableTracker(subject.withConstraints$); const emission = tracker.expectEmission(); @@ -705,7 +882,7 @@ describe("UserStateSubject", () => { const state = new FakeSingleUserState(SomeUser, { foo: "init" }); const singleUserId$ = new BehaviorSubject(SomeUser); const constraints$ = new BehaviorSubject(fooMaxLength(2)); - const subject = new UserStateSubject(state, { singleUserId$, constraints$ }); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$, constraints$ }); const tracker = new ObservableTracker(subject.withConstraints$); const expected = fooMaxLength(3); constraints$.next(expected); @@ -722,7 +899,7 @@ describe("UserStateSubject", () => { const state = new FakeSingleUserState(SomeUser, { foo: "init" }); const singleUserId$ = new BehaviorSubject(SomeUser); const constraints$ = new Subject>(); - const subject = new UserStateSubject(state, { singleUserId$, constraints$ }); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$, constraints$ }); const tracker = new ObservableTracker(subject.withConstraints$); const expected = fooMaxLength(3); @@ -740,7 +917,7 @@ describe("UserStateSubject", () => { const singleUserId$ = new BehaviorSubject(SomeUser); const expected = fooMaxLength(3); const constraints$ = new BehaviorSubject(expected); - const subject = new UserStateSubject(state, { singleUserId$, constraints$ }); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$, constraints$ }); const tracker = new ObservableTracker(subject.withConstraints$); constraints$.error({ some: "error" }); @@ -756,7 +933,7 @@ describe("UserStateSubject", () => { const singleUserId$ = new BehaviorSubject(SomeUser); const expected = fooMaxLength(3); const constraints$ = new BehaviorSubject(expected); - const subject = new UserStateSubject(state, { singleUserId$, constraints$ }); + const subject = new UserStateSubject(SomeKey, () => state, { singleUserId$, constraints$ }); const tracker = new ObservableTracker(subject.withConstraints$); constraints$.complete(); diff --git a/libs/common/src/tools/state/user-state-subject.ts b/libs/common/src/tools/state/user-state-subject.ts index 61a9e87c686..89f19ac3c73 100644 --- a/libs/common/src/tools/state/user-state-subject.ts +++ b/libs/common/src/tools/state/user-state-subject.ts @@ -5,15 +5,10 @@ import { ReplaySubject, filter, map, - Subject, takeUntil, pairwise, - combineLatest, distinctUntilChanged, BehaviorSubject, - race, - ignoreElements, - endWith, startWith, Observable, Subscription, @@ -22,16 +17,32 @@ import { combineLatestWith, catchError, EMPTY, + concatMap, + OperatorFunction, + pipe, + first, + withLatestFrom, + scan, + skip, } from "rxjs"; -import { SingleUserState } from "@bitwarden/common/platform/state"; +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { SingleUserState, UserKeyDefinition } from "@bitwarden/common/platform/state"; +import { UserId } from "@bitwarden/common/types/guid"; -import { WithConstraints } from "../types"; +import { UserBound } from "../dependencies"; +import { anyComplete, ready, withLatestReady } from "../rx"; +import { Constraints, SubjectConstraints, WithConstraints } from "../types"; -import { IdentityConstraint } from "./identity-state-constraint"; +import { ClassifiedFormat, isClassifiedFormat } from "./classified-format"; +import { unconstrained$ } from "./identity-state-constraint"; +import { isObjectKey, ObjectKey, toUserKeyDefinition } from "./object-key"; import { isDynamic } from "./state-constraints-dependency"; +import { UserEncryptor } from "./user-encryptor.abstraction"; import { UserStateSubjectDependencies } from "./user-state-subject-dependencies"; +type Constrained = { constraints: Readonly>; state: State }; + /** * Adapt a state provider to an rxjs subject. * @@ -44,14 +55,20 @@ import { UserStateSubjectDependencies } from "./user-state-subject-dependencies" * @template State the state stored by the subject * @template Dependencies use-specific dependencies provided by the user. */ -export class UserStateSubject +export class UserStateSubject< + State extends object, + Secret = State, + Disclosed = never, + Dependencies = null, + > extends Observable implements SubjectLike { /** - * Instantiates the user state subject - * @param state the backing store of the subject - * @param dependencies tailor the subject's behavior for a particular + * Instantiates the user state subject bound to a persistent backing store + * @param key identifies the persistent backing store + * @param getState creates a persistent backing store using a key + * @param context tailor the subject's behavior for a particular * purpose. * @param dependencies.when$ blocks updates to the state subject until * this becomes true. When this occurs, only the last-received update @@ -61,91 +78,304 @@ export class UserStateSubject * is available. */ constructor( - private state: SingleUserState, - private dependencies: UserStateSubjectDependencies, + private key: UserKeyDefinition | ObjectKey, + getState: (key: UserKeyDefinition) => SingleUserState, + private context: UserStateSubjectDependencies, ) { super(); + if (isObjectKey(this.key)) { + // classification and encryption only supported with `ObjectKey` + this.objectKey = this.key; + this.stateKey = toUserKeyDefinition(this.key); + this.state = getState(this.stateKey); + } else { + // raw state access granted with `UserKeyDefinition` + this.objectKey = null; + this.stateKey = this.key as UserKeyDefinition; + this.state = getState(this.stateKey); + } + // normalize dependencies - const when$ = (this.dependencies.when$ ?? new BehaviorSubject(true)).pipe( - distinctUntilChanged(), + const when$ = (this.context.when$ ?? new BehaviorSubject(true)).pipe(distinctUntilChanged()); + + // manage dependencies through replay subjects since `UserStateSubject` + // reads them in multiple places + const encryptor$ = new ReplaySubject(1); + const { singleUserId$, singleUserEncryptor$ } = this.context; + this.encryptor(singleUserEncryptor$ ?? singleUserId$).subscribe(encryptor$); + + const constraints$ = new ReplaySubject>(1); + (this.context.constraints$ ?? unconstrained$()) + .pipe( + // FIXME: this should probably log that an error occurred + catchError(() => EMPTY), + ) + .subscribe(constraints$); + + const dependencies$ = new ReplaySubject(1); + if (this.context.dependencies$) { + this.context.dependencies$.subscribe(dependencies$); + } else { + dependencies$.next(null); + } + + // wire output before input so that output normalizes the current state + // before any `next` value is processed + this.outputSubscription = this.state.state$ + .pipe(this.declassify(encryptor$), this.adjust(combineLatestWith(constraints$))) + .subscribe(this.output); + + const last$ = new ReplaySubject(1); + this.output + .pipe( + last(), + map((o) => o.state), + ) + .subscribe(last$); + + // the update stream simulates the stateProvider's "shouldUpdate" + // functionality & applies policy + const updates$ = concat( + this.input.pipe( + this.when(when$), + this.adjust(withLatestReady(constraints$)), + this.prepareUpdate(this, dependencies$), + ), + // when the output subscription completes, its last-emitted value + // loops around to the input for finalization + last$.pipe(this.fix(constraints$), this.prepareUpdate(last$, dependencies$)), ); - const userIdAvailable$ = this.dependencies.singleUserId$.pipe( - startWith(state.userId), + + // classification/encryption bound to the input subscription's lifetime + // to ensure that `fix` has access to the encryptor key + // + // FIXME: this should probably timeout when a lock occurs + this.inputSubscription = updates$ + .pipe(this.classify(encryptor$), takeUntil(anyComplete([when$, this.input, encryptor$]))) + .subscribe({ + next: (state) => this.onNext(state), + error: (e: unknown) => this.onError(e), + complete: () => this.onComplete(), + }); + } + + private stateKey: UserKeyDefinition; + private objectKey: ObjectKey; + + private encryptor( + singleUserEncryptor$: Observable | UserId>, + ): Observable { + return singleUserEncryptor$.pipe( + // normalize inputs + map((maybe): UserBound<"encryptor", UserEncryptor> => { + if (typeof maybe === "object" && "encryptor" in maybe) { + return maybe; + } else if (typeof maybe === "string") { + return { encryptor: null, userId: maybe as UserId }; + } else { + throw new Error(`Invalid encryptor input received for ${this.key.key}.`); + } + }), + // fail the stream if the state desyncs from the bound userId + startWith({ userId: this.state.userId, encryptor: null } as UserBound< + "encryptor", + UserEncryptor + >), pairwise(), - map(([expectedUserId, actualUserId]) => { - if (expectedUserId === actualUserId) { - return true; + map(([expected, actual]) => { + if (expected.userId === actual.userId) { + return actual; } else { - throw { expectedUserId, actualUserId }; + throw { + expectedUserId: expected.userId, + actualUserId: actual.userId, + }; } }), + // reduce emissions to when encryptor changes distinctUntilChanged(), + map(({ encryptor }) => encryptor), ); - const constraints$ = ( - this.dependencies.constraints$ ?? new BehaviorSubject(new IdentityConstraint()) - ).pipe( - // FIXME: this should probably log that an error occurred - catchError(() => EMPTY), + } + + private when(when$: Observable): OperatorFunction { + return pipe( + combineLatestWith(when$.pipe(distinctUntilChanged())), + filter(([_, when]) => !!when), + map(([input]) => input), ); + } + + private prepareUpdate( + init$: Observable, + dependencies$: Observable, + ): OperatorFunction, State> { + return (input$) => + concat( + // `init$` becomes the accumulator for `scan` + init$.pipe( + first(), + map((init) => [init, null] as const), + ), + input$.pipe( + map((constrained) => constrained.state), + withLatestFrom(dependencies$), + ), + ).pipe( + // scan only emits values that can cause updates + scan(([prev], [pending, dependencies]) => { + const shouldUpdate = this.context.shouldUpdate?.(prev, pending, dependencies) ?? true; + if (shouldUpdate) { + // actual update + const next = this.context.nextValue?.(prev, pending, dependencies) ?? pending; + return [next, dependencies]; + } else { + // false update + return [prev, null]; + } + }), + // the first emission primes `scan`s aggregator + skip(1), + map(([state]) => state), + + // clean up false updates + distinctUntilChanged(), + ); + } - // normalize input in case this `UserStateSubject` is not the only - // observer of the backing store - const input$ = combineLatest([this.input, constraints$]).pipe( - map(([input, constraints]) => { - const calibration = isDynamic(constraints) ? constraints.calibrate(input) : constraints; - const state = calibration.adjust(input); - return state; + private adjust( + withConstraints: OperatorFunction]>, + ): OperatorFunction> { + return pipe( + // how constraints are blended with incoming emissions varies: + // * `output` needs to emit when constraints update + // * `input` needs to wait until a message flows through the pipe + withConstraints, + map(([loadedState, constraints]) => { + // bypass nulls + if (!loadedState) { + return { + constraints: {} as Constraints, + state: null, + } satisfies Constrained; + } + + const calibration = isDynamic(constraints) + ? constraints.calibrate(loadedState) + : constraints; + const adjusted = calibration.adjust(loadedState); + + return { + constraints: calibration.constraints, + state: adjusted, + }; }), ); + } - // when the output subscription completes, its last-emitted value - // loops around to the input for finalization - const finalize$ = this.pipe( - last(), + private fix( + constraints$: Observable>, + ): OperatorFunction> { + return pipe( combineLatestWith(constraints$), - map(([output, constraints]) => { - const calibration = isDynamic(constraints) ? constraints.calibrate(output) : constraints; - const state = calibration.fix(output); - return state; + map(([loadedState, constraints]) => { + const calibration = isDynamic(constraints) + ? constraints.calibrate(loadedState) + : constraints; + const fixed = calibration.fix(loadedState); + + return { + constraints: calibration.constraints, + state: fixed, + }; }), ); - const updates$ = concat(input$, finalize$); + } - // observe completion - const whenComplete$ = when$.pipe(ignoreElements(), endWith(true)); - const inputComplete$ = this.input.pipe(ignoreElements(), endWith(true)); - const userIdComplete$ = this.dependencies.singleUserId$.pipe(ignoreElements(), endWith(true)); - const completion$ = race(whenComplete$, inputComplete$, userIdComplete$); + private declassify(encryptor$: Observable): OperatorFunction { + // short-circuit if they key lacks encryption support + if (!this.objectKey || this.objectKey.format === "plain") { + return (input$) => input$ as Observable; + } - // wire output before input so that output normalizes the current state - // before any `next` value is processed - this.outputSubscription = this.state.state$ - .pipe( - combineLatestWith(constraints$), - map(([rawState, constraints]) => { - const calibration = isDynamic(constraints) - ? constraints.calibrate(rawState) - : constraints; - const state = calibration.adjust(rawState); - return { - constraints: calibration.constraints, - state, - }; + // if the key supports encryption, enable encryptor support + if (this.objectKey && this.objectKey.format === "classified") { + return pipe( + combineLatestWith(encryptor$), + concatMap(async ([input, encryptor]) => { + // pass through null values + if (input === null || input === undefined) { + return null; + } + + // fail fast if the format is incorrect + if (!isClassifiedFormat(input)) { + throw new Error(`Cannot declassify ${this.key.key}; unknown format.`); + } + + // decrypt classified data + const { secret, disclosed } = input; + const encrypted = EncString.fromJSON(secret); + const decryptedSecret = await encryptor.decrypt(encrypted); + + // assemble into proper state + const declassified = this.objectKey.classifier.declassify(disclosed, decryptedSecret); + const state = this.objectKey.options.deserializer(declassified); + + return state; }), - ) - .subscribe(this.output); - this.inputSubscription = combineLatest([updates$, when$, userIdAvailable$]) - .pipe( - filter(([_, when]) => when), - map(([state]) => state), - takeUntil(completion$), - ) - .subscribe({ - next: (r) => this.onNext(r), - error: (e: unknown) => this.onError(e), - complete: () => this.onComplete(), - }); + ); + } + + throw new Error(`unknown serialization format: ${this.objectKey.format}`); + } + + private classify(encryptor$: Observable): OperatorFunction { + // short-circuit if they key lacks encryption support; `encryptor` is + // readied to preserve `dependencies.singleUserId$` emission contract + if (!this.objectKey || this.objectKey.format === "plain") { + return pipe( + ready(encryptor$), + map((input) => input as unknown), + ); + } + + // if the key supports encryption, enable encryptor support + if (this.objectKey && this.objectKey.format === "classified") { + return pipe( + withLatestReady(encryptor$), + concatMap(async ([input, encryptor]) => { + // fail fast if there's no value + if (input === null || input === undefined) { + return null; + } + + // split data by classification level + const serialized = JSON.parse(JSON.stringify(input)); + const classified = this.objectKey.classifier.classify(serialized); + + // protect data + const encrypted = await encryptor.encrypt(classified.secret); + const secret = JSON.parse(JSON.stringify(encrypted)); + + // wrap result in classified format envelope for storage + const envelope = { + id: null as void, + secret, + disclosed: classified.disclosed, + } satisfies ClassifiedFormat; + + // deliberate type erasure; the type is restored during `declassify` + return envelope as unknown; + }), + ); + } + + // FIXME: add "encrypted" format --> key contains encryption logic + // CONSIDER: should "classified format" algorithm be embedded in subject keys...? + + throw new Error(`unknown serialization format: ${this.objectKey.format}`); } /** The userId to which the subject is bound. @@ -177,7 +407,8 @@ export class UserStateSubject // using subjects to ensure the right semantics are followed; // if greater efficiency becomes desirable, consider implementing // `SubjectLike` directly - private input = new Subject(); + private input = new ReplaySubject(1); + private state: SingleUserState; private readonly output = new ReplaySubject>(1); /** A stream containing settings and their last-applied constraints. */ @@ -188,25 +419,8 @@ export class UserStateSubject private inputSubscription: Unsubscribable; private outputSubscription: Unsubscribable; - private onNext(value: State) { - const nextValue = this.dependencies.nextValue ?? ((_: State, next: State) => next); - const shouldUpdate = this.dependencies.shouldUpdate ?? ((_: State) => true); - - this.state - .update( - (state, dependencies) => { - const next = nextValue(state, value, dependencies); - return next; - }, - { - shouldUpdate(current, dependencies) { - const update = shouldUpdate(current, value, dependencies); - return update; - }, - combineLatestWith: this.dependencies.dependencies$, - }, - ) - .catch((e: any) => this.onError(e)); + private onNext(value: unknown) { + this.state.update(() => value).catch((e: any) => this.onError(e)); } private onError(value: any) { @@ -232,8 +446,8 @@ export class UserStateSubject private dispose() { if (!this.isDisposed) { // clean up internal subscriptions - this.inputSubscription.unsubscribe(); - this.outputSubscription.unsubscribe(); + this.inputSubscription?.unsubscribe(); + this.outputSubscription?.unsubscribe(); this.inputSubscription = null; this.outputSubscription = null; diff --git a/libs/common/src/tools/types.ts b/libs/common/src/tools/types.ts index ec1903e6225..9b746924278 100644 --- a/libs/common/src/tools/types.ts +++ b/libs/common/src/tools/types.ts @@ -1,5 +1,7 @@ import { Simplify } from "type-fest"; +import { IntegrationId } from "./integration"; + /** Constraints that are shared by all primitive field types */ type PrimitiveConstraint = { /** `true` indicates the field is required; otherwise the field is optional */ @@ -129,6 +131,8 @@ export type StateConstraints = { fix: (state: State) => State; }; +export type SubjectConstraints = StateConstraints | DynamicStateConstraints; + /** Options that provide contextual information about the application state * when a generator is invoked. */ @@ -144,4 +148,7 @@ export type VaultItemRequest = { /** Options that provide contextual information about the application state * when a generator is invoked. */ -export type GenerationRequest = Partial; +export type GenerationRequest = Partial & + Partial<{ + integration: IntegrationId | null; + }>; diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index f0e19a21342..444c922fe31 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -1,7 +1,7 @@ import { Observable } from "rxjs"; -import { UserKeyRotationDataProvider } from "@bitwarden/auth/common"; import { LocalData } from "@bitwarden/common/vault/models/data/local.data"; +import { UserKeyRotationDataProvider } from "@bitwarden/key-management"; import { UriMatchStrategySetting } from "../../models/domain/domain-service"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; diff --git a/libs/common/src/vault/abstractions/folder/folder.service.abstraction.ts b/libs/common/src/vault/abstractions/folder/folder.service.abstraction.ts index 857915ddb80..df21b136f41 100644 --- a/libs/common/src/vault/abstractions/folder/folder.service.abstraction.ts +++ b/libs/common/src/vault/abstractions/folder/folder.service.abstraction.ts @@ -1,6 +1,6 @@ import { Observable } from "rxjs"; -import { UserKeyRotationDataProvider } from "@bitwarden/auth/common"; +import { UserKeyRotationDataProvider } from "@bitwarden/key-management"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { UserId } from "../../../types/guid"; diff --git a/libs/common/src/vault/models/domain/attachment.spec.ts b/libs/common/src/vault/models/domain/attachment.spec.ts index 690866e173a..14dec8dea0c 100644 --- a/libs/common/src/vault/models/domain/attachment.spec.ts +++ b/libs/common/src/vault/models/domain/attachment.spec.ts @@ -1,7 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; +import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec"; -import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; @@ -57,17 +57,14 @@ describe("Attachment", () => { }); describe("decrypt", () => { - let cryptoService: MockProxy; + let keyService: MockProxy; let encryptService: MockProxy; beforeEach(() => { - cryptoService = mock(); + keyService = mock(); encryptService = mock(); - (window as any).bitwardenContainerService = new ContainerService( - cryptoService, - encryptService, - ); + (window as any).bitwardenContainerService = new ContainerService(keyService, encryptService); }); it("expected output", async () => { @@ -101,32 +98,32 @@ describe("Attachment", () => { attachment.key = mock(); }); - it("uses the provided key without depending on CryptoService", async () => { + it("uses the provided key without depending on KeyService", async () => { const providedKey = mock(); await attachment.decrypt(null, providedKey); - expect(cryptoService.getUserKeyWithLegacySupport).not.toHaveBeenCalled(); + expect(keyService.getUserKeyWithLegacySupport).not.toHaveBeenCalled(); expect(encryptService.decryptToBytes).toHaveBeenCalledWith(attachment.key, providedKey); }); it("gets an organization key if required", async () => { const orgKey = mock(); - cryptoService.getOrgKey.calledWith("orgId").mockResolvedValue(orgKey); + keyService.getOrgKey.calledWith("orgId").mockResolvedValue(orgKey); await attachment.decrypt("orgId", null); - expect(cryptoService.getOrgKey).toHaveBeenCalledWith("orgId"); + expect(keyService.getOrgKey).toHaveBeenCalledWith("orgId"); expect(encryptService.decryptToBytes).toHaveBeenCalledWith(attachment.key, orgKey); }); it("gets the user's decryption key if required", async () => { const userKey = mock(); - cryptoService.getUserKeyWithLegacySupport.mockResolvedValue(userKey); + keyService.getUserKeyWithLegacySupport.mockResolvedValue(userKey); await attachment.decrypt(null, null); - expect(cryptoService.getUserKeyWithLegacySupport).toHaveBeenCalled(); + expect(keyService.getUserKeyWithLegacySupport).toHaveBeenCalled(); expect(encryptService.decryptToBytes).toHaveBeenCalledWith(attachment.key, userKey); }); }); diff --git a/libs/common/src/vault/models/domain/attachment.ts b/libs/common/src/vault/models/domain/attachment.ts index 7a234fa21ce..117b3b26e92 100644 --- a/libs/common/src/vault/models/domain/attachment.ts +++ b/libs/common/src/vault/models/domain/attachment.ts @@ -68,10 +68,10 @@ export class Attachment extends Domain { } private async getKeyForDecryption(orgId: string) { - const cryptoService = Utils.getContainerService().getCryptoService(); + const keyService = Utils.getContainerService().getKeyService(); return orgId != null - ? await cryptoService.getOrgKey(orgId) - : await cryptoService.getUserKeyWithLegacySupport(); + ? await keyService.getOrgKey(orgId) + : await keyService.getUserKeyWithLegacySupport(); } toAttachmentData(): AttachmentData { diff --git a/libs/common/src/vault/models/domain/cipher.spec.ts b/libs/common/src/vault/models/domain/cipher.spec.ts index f10884b55ae..509a17a8a0e 100644 --- a/libs/common/src/vault/models/domain/cipher.spec.ts +++ b/libs/common/src/vault/models/domain/cipher.spec.ts @@ -3,9 +3,9 @@ import { Jsonify } from "type-fest"; import { UserId } from "@bitwarden/common/types/guid"; +import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec/utils"; import { UriMatchStrategy } from "../../../models/domain/domain-service"; -import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { EncString } from "../../../platform/models/domain/enc-string"; import { ContainerService } from "../../../platform/services/container.service"; @@ -237,16 +237,13 @@ describe("Cipher DTO", () => { login.decrypt.mockResolvedValue(loginView); cipher.login = login; - const cryptoService = mock(); + const keyService = mock(); const encryptService = mock(); const cipherService = mock(); encryptService.decryptToBytes.mockResolvedValue(makeStaticByteArray(64)); - (window as any).bitwardenContainerService = new ContainerService( - cryptoService, - encryptService, - ); + (window as any).bitwardenContainerService = new ContainerService(keyService, encryptService); const cipherView = await cipher.decrypt( await cipherService.getKeyForCipherKeyDecryption(cipher, mockUserId), @@ -357,16 +354,13 @@ describe("Cipher DTO", () => { cipher.secureNote.type = SecureNoteType.Generic; cipher.key = mockEnc("EncKey"); - const cryptoService = mock(); + const keyService = mock(); const encryptService = mock(); const cipherService = mock(); encryptService.decryptToBytes.mockResolvedValue(makeStaticByteArray(64)); - (window as any).bitwardenContainerService = new ContainerService( - cryptoService, - encryptService, - ); + (window as any).bitwardenContainerService = new ContainerService(keyService, encryptService); const cipherView = await cipher.decrypt( await cipherService.getKeyForCipherKeyDecryption(cipher, mockUserId), @@ -495,16 +489,13 @@ describe("Cipher DTO", () => { card.decrypt.mockResolvedValue(cardView); cipher.card = card; - const cryptoService = mock(); + const keyService = mock(); const encryptService = mock(); const cipherService = mock(); encryptService.decryptToBytes.mockResolvedValue(makeStaticByteArray(64)); - (window as any).bitwardenContainerService = new ContainerService( - cryptoService, - encryptService, - ); + (window as any).bitwardenContainerService = new ContainerService(keyService, encryptService); const cipherView = await cipher.decrypt( await cipherService.getKeyForCipherKeyDecryption(cipher, mockUserId), @@ -657,16 +648,13 @@ describe("Cipher DTO", () => { identity.decrypt.mockResolvedValue(identityView); cipher.identity = identity; - const cryptoService = mock(); + const keyService = mock(); const encryptService = mock(); const cipherService = mock(); encryptService.decryptToBytes.mockResolvedValue(makeStaticByteArray(64)); - (window as any).bitwardenContainerService = new ContainerService( - cryptoService, - encryptService, - ); + (window as any).bitwardenContainerService = new ContainerService(keyService, encryptService); const cipherView = await cipher.decrypt( await cipherService.getKeyForCipherKeyDecryption(cipher, mockUserId), diff --git a/libs/common/src/vault/models/domain/login-uri.spec.ts b/libs/common/src/vault/models/domain/login-uri.spec.ts index c42b0cc9d12..a1ecb473597 100644 --- a/libs/common/src/vault/models/domain/login-uri.spec.ts +++ b/libs/common/src/vault/models/domain/login-uri.spec.ts @@ -70,7 +70,7 @@ describe("LoginUri", () => { encryptService = mock(); global.bitwardenContainerService = { getEncryptService: () => encryptService, - getCryptoService: () => null, + getKeyService: () => null, }; }); diff --git a/libs/common/src/vault/models/domain/login-uri.ts b/libs/common/src/vault/models/domain/login-uri.ts index 0a0c5765a34..e5943929f2d 100644 --- a/libs/common/src/vault/models/domain/login-uri.ts +++ b/libs/common/src/vault/models/domain/login-uri.ts @@ -47,8 +47,8 @@ export class LoginUri extends Domain { return false; } - const cryptoService = Utils.getContainerService().getEncryptService(); - const localChecksum = await cryptoService.hash(clearTextUri, "sha256"); + const keyService = Utils.getContainerService().getEncryptService(); + const localChecksum = await keyService.hash(clearTextUri, "sha256"); const remoteChecksum = await this.uriChecksum.decrypt(orgId, encKey); return remoteChecksum === localChecksum; diff --git a/libs/common/src/vault/models/view/cipher.view.ts b/libs/common/src/vault/models/view/cipher.view.ts index 028b582db26..3ea3f109be1 100644 --- a/libs/common/src/vault/models/view/cipher.view.ts +++ b/libs/common/src/vault/models/view/cipher.view.ts @@ -2,9 +2,8 @@ import { View } from "../../../models/view/view"; import { InitializerMetadata } from "../../../platform/interfaces/initializer-metadata.interface"; import { InitializerKey } from "../../../platform/services/cryptography/initializer-key"; import { DeepJsonify } from "../../../types/deep-jsonify"; -import { LinkedIdType } from "../../enums"; +import { CipherType, LinkedIdType } from "../../enums"; import { CipherRepromptType } from "../../enums/cipher-reprompt-type"; -import { CipherType } from "../../enums/cipher-type"; import { LocalData } from "../data/local.data"; import { Cipher } from "../domain/cipher"; @@ -132,6 +131,13 @@ export class CipherView implements View, InitializerMetadata { ); } + /** + * Determines if the cipher can be launched in a new browser tab. + */ + get canLaunch(): boolean { + return this.type === CipherType.Login && this.login.canLaunch; + } + linkedFieldValue(id: LinkedIdType) { const linkedFieldOption = this.linkedFieldOptions?.get(id); if (linkedFieldOption == null) { diff --git a/libs/common/src/vault/services/cipher-authorization.service.spec.ts b/libs/common/src/vault/services/cipher-authorization.service.spec.ts new file mode 100644 index 00000000000..cccd29ad697 --- /dev/null +++ b/libs/common/src/vault/services/cipher-authorization.service.spec.ts @@ -0,0 +1,275 @@ +import { mock } from "jest-mock-extended"; +import { firstValueFrom, of } from "rxjs"; + +import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { CollectionId } from "@bitwarden/common/types/guid"; + +import { CipherView } from "../models/view/cipher.view"; + +import { + CipherAuthorizationService, + DefaultCipherAuthorizationService, +} from "./cipher-authorization.service"; + +describe("CipherAuthorizationService", () => { + let cipherAuthorizationService: CipherAuthorizationService; + + const mockCollectionService = mock(); + const mockOrganizationService = mock(); + + // Mock factories + const createMockCipher = ( + organizationId: string | null, + collectionIds: string[], + edit: boolean = true, + ) => ({ + organizationId, + collectionIds, + edit, + }); + + const createMockCollection = (id: string, manage: boolean) => ({ + id, + manage, + }); + + const createMockOrganization = ({ + allowAdminAccessToAllCollectionItems = false, + canEditAllCiphers = false, + canEditUnassignedCiphers = false, + isAdmin = false, + editAnyCollection = false, + } = {}) => ({ + allowAdminAccessToAllCollectionItems, + canEditAllCiphers, + canEditUnassignedCiphers, + isAdmin, + permissions: { + editAnyCollection, + }, + }); + + beforeEach(() => { + jest.clearAllMocks(); + cipherAuthorizationService = new DefaultCipherAuthorizationService( + mockCollectionService, + mockOrganizationService, + ); + }); + + describe("canDeleteCipher$", () => { + it("should return true if cipher has no organizationId", (done) => { + const cipher = createMockCipher(null, []) as CipherView; + + cipherAuthorizationService.canDeleteCipher$(cipher).subscribe((result) => { + expect(result).toBe(true); + done(); + }); + }); + + it("should return true if isAdminConsoleAction is true and cipher is unassigned", (done) => { + const cipher = createMockCipher("org1", []) as CipherView; + const organization = createMockOrganization({ canEditUnassignedCiphers: true }); + mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + + cipherAuthorizationService.canDeleteCipher$(cipher, [], true).subscribe((result) => { + expect(result).toBe(true); + done(); + }); + }); + + it("should return true if isAdminConsoleAction is true and user can edit all ciphers in the org", (done) => { + const cipher = createMockCipher("org1", ["col1"]) as CipherView; + const organization = createMockOrganization({ canEditAllCiphers: true }); + mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + + cipherAuthorizationService.canDeleteCipher$(cipher, [], true).subscribe((result) => { + expect(result).toBe(true); + expect(mockOrganizationService.get$).toHaveBeenCalledWith("org1"); + done(); + }); + }); + + it("should return false if isAdminConsoleAction is true but user does not have permission to edit unassigned ciphers", (done) => { + const cipher = createMockCipher("org1", []) as CipherView; + const organization = createMockOrganization({ canEditUnassignedCiphers: false }); + mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + + cipherAuthorizationService.canDeleteCipher$(cipher, [], true).subscribe((result) => { + expect(result).toBe(false); + done(); + }); + }); + + it("should return true if activeCollectionId is provided and has manage permission", (done) => { + const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView; + const activeCollectionId = "col1" as CollectionId; + const org = createMockOrganization(); + mockOrganizationService.get$.mockReturnValue(of(org as Organization)); + + const allCollections = [ + createMockCollection("col1", true), + createMockCollection("col2", false), + ]; + mockCollectionService.decryptedCollectionViews$.mockReturnValue( + of(allCollections as CollectionView[]), + ); + + cipherAuthorizationService + .canDeleteCipher$(cipher, [activeCollectionId]) + .subscribe((result) => { + expect(result).toBe(true); + expect(mockCollectionService.decryptedCollectionViews$).toHaveBeenCalledWith([ + "col1", + "col2", + ] as CollectionId[]); + done(); + }); + }); + + it("should return false if activeCollectionId is provided and manage permission is not present", (done) => { + const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView; + const activeCollectionId = "col1" as CollectionId; + const org = createMockOrganization(); + mockOrganizationService.get$.mockReturnValue(of(org as Organization)); + + const allCollections = [ + createMockCollection("col1", false), + createMockCollection("col2", true), + ]; + mockCollectionService.decryptedCollectionViews$.mockReturnValue( + of(allCollections as CollectionView[]), + ); + + cipherAuthorizationService + .canDeleteCipher$(cipher, [activeCollectionId]) + .subscribe((result) => { + expect(result).toBe(false); + expect(mockCollectionService.decryptedCollectionViews$).toHaveBeenCalledWith([ + "col1", + "col2", + ] as CollectionId[]); + done(); + }); + }); + + it("should return true if any collection has manage permission", (done) => { + const cipher = createMockCipher("org1", ["col1", "col2", "col3"]) as CipherView; + const org = createMockOrganization(); + mockOrganizationService.get$.mockReturnValue(of(org as Organization)); + + const allCollections = [ + createMockCollection("col1", false), + createMockCollection("col2", true), + createMockCollection("col3", false), + ]; + mockCollectionService.decryptedCollectionViews$.mockReturnValue( + of(allCollections as CollectionView[]), + ); + + cipherAuthorizationService.canDeleteCipher$(cipher).subscribe((result) => { + expect(result).toBe(true); + expect(mockCollectionService.decryptedCollectionViews$).toHaveBeenCalledWith([ + "col1", + "col2", + "col3", + ] as CollectionId[]); + done(); + }); + }); + + it("should return false if no collection has manage permission", (done) => { + const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView; + const org = createMockOrganization(); + mockOrganizationService.get$.mockReturnValue(of(org as Organization)); + + const allCollections = [ + createMockCollection("col1", false), + createMockCollection("col2", false), + ]; + mockCollectionService.decryptedCollectionViews$.mockReturnValue( + of(allCollections as CollectionView[]), + ); + + cipherAuthorizationService.canDeleteCipher$(cipher).subscribe((result) => { + expect(result).toBe(false); + expect(mockCollectionService.decryptedCollectionViews$).toHaveBeenCalledWith([ + "col1", + "col2", + ] as CollectionId[]); + done(); + }); + }); + }); + + describe("canCloneCipher$", () => { + it("should return true if cipher has no organizationId", async () => { + const cipher = createMockCipher(null, []) as CipherView; + + const result = await firstValueFrom(cipherAuthorizationService.canCloneCipher$(cipher)); + expect(result).toBe(true); + }); + + describe("isAdminConsoleAction is true", () => { + it("should return true for admin users", async () => { + const cipher = createMockCipher("org1", []) as CipherView; + const organization = createMockOrganization({ isAdmin: true }); + mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + + const result = await firstValueFrom( + cipherAuthorizationService.canCloneCipher$(cipher, true), + ); + expect(result).toBe(true); + }); + + it("should return true for custom user with canEditAnyCollection", async () => { + const cipher = createMockCipher("org1", []) as CipherView; + const organization = createMockOrganization({ editAnyCollection: true }); + mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + + const result = await firstValueFrom( + cipherAuthorizationService.canCloneCipher$(cipher, true), + ); + expect(result).toBe(true); + }); + }); + + describe("isAdminConsoleAction is false", () => { + it("should return true if at least one cipher collection has manage permission", async () => { + const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView; + const organization = createMockOrganization(); + mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + + const allCollections = [ + createMockCollection("col1", true), + createMockCollection("col2", false), + ]; + mockCollectionService.decryptedCollectionViews$.mockReturnValue( + of(allCollections as CollectionView[]), + ); + + const result = await firstValueFrom(cipherAuthorizationService.canCloneCipher$(cipher)); + expect(result).toBe(true); + }); + + it("should return false if no collection has manage permission", async () => { + const cipher = createMockCipher("org1", ["col1", "col2"]) as CipherView; + const organization = createMockOrganization(); + mockOrganizationService.get$.mockReturnValue(of(organization as Organization)); + + const allCollections = [ + createMockCollection("col1", false), + createMockCollection("col2", false), + ]; + mockCollectionService.decryptedCollectionViews$.mockReturnValue( + of(allCollections as CollectionView[]), + ); + + const result = await firstValueFrom(cipherAuthorizationService.canCloneCipher$(cipher)); + expect(result).toBe(false); + }); + }); + }); +}); diff --git a/libs/common/src/vault/services/cipher-authorization.service.ts b/libs/common/src/vault/services/cipher-authorization.service.ts new file mode 100644 index 00000000000..eb6211848ae --- /dev/null +++ b/libs/common/src/vault/services/cipher-authorization.service.ts @@ -0,0 +1,122 @@ +import { map, Observable, of, shareReplay, switchMap } from "rxjs"; + +import { CollectionService } from "@bitwarden/admin-console/common"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { CollectionId } from "@bitwarden/common/types/guid"; + +import { Cipher } from "../models/domain/cipher"; +import { CipherView } from "../models/view/cipher.view"; + +/** + * Represents either a cipher or a cipher view. + */ +type CipherLike = Cipher | CipherView; + +/** + * Service for managing user cipher authorization. + */ +export abstract class CipherAuthorizationService { + /** + * Determines if the user can delete the specified cipher. + * + * @param {CipherLike} cipher - The cipher object to evaluate for deletion permissions. + * @param {CollectionId[]} [allowedCollections] - Optional. The selected collection id from the vault filter. + * @param {boolean} isAdminConsoleAction - Optional. A flag indicating if the action is being performed from the admin console. + * + * @returns {Observable} - An observable that emits a boolean value indicating if the user can delete the cipher. + */ + canDeleteCipher$: ( + cipher: CipherLike, + allowedCollections?: CollectionId[], + isAdminConsoleAction?: boolean, + ) => Observable; + + /** + * Determines if the user can clone the specified cipher. + * + * @param {CipherLike} cipher - The cipher object to evaluate for cloning permissions. + * @param {boolean} isAdminConsoleAction - Optional. A flag indicating if the action is being performed from the admin console. + * + * @returns {Observable} - An observable that emits a boolean value indicating if the user can clone the cipher. + */ + canCloneCipher$: (cipher: CipherLike, isAdminConsoleAction?: boolean) => Observable; +} + +/** + * {@link CipherAuthorizationService} + */ +export class DefaultCipherAuthorizationService implements CipherAuthorizationService { + constructor( + private collectionService: CollectionService, + private organizationService: OrganizationService, + ) {} + + /** + * + * {@link CipherAuthorizationService.canDeleteCipher$} + */ + canDeleteCipher$( + cipher: CipherLike, + allowedCollections?: CollectionId[], + isAdminConsoleAction?: boolean, + ): Observable { + if (cipher.organizationId == null) { + return of(true); + } + + return this.organizationService.get$(cipher.organizationId).pipe( + switchMap((organization) => { + if (isAdminConsoleAction) { + // If the user is an admin, they can delete an unassigned cipher + if (!cipher.collectionIds || cipher.collectionIds.length === 0) { + return of(organization?.canEditUnassignedCiphers === true); + } + + if (organization?.canEditAllCiphers) { + return of(true); + } + } + + return this.collectionService + .decryptedCollectionViews$(cipher.collectionIds as CollectionId[]) + .pipe( + map((allCollections) => { + const shouldFilter = allowedCollections?.some(Boolean); + + const collections = shouldFilter + ? allCollections.filter((c) => allowedCollections.includes(c.id as CollectionId)) + : allCollections; + + return collections.some((collection) => collection.manage); + }), + ); + }), + ); + } + + /** + * {@link CipherAuthorizationService.canCloneCipher$} + */ + canCloneCipher$(cipher: CipherLike, isAdminConsoleAction?: boolean): Observable { + if (cipher.organizationId == null) { + return of(true); + } + + return this.organizationService.get$(cipher.organizationId).pipe( + switchMap((organization) => { + // Admins and custom users can always clone when in the Admin Console + if ( + isAdminConsoleAction && + (organization.isAdmin || organization.permissions?.editAnyCollection) + ) { + return of(true); + } + + return this.collectionService + .decryptedCollectionViews$(cipher.collectionIds as CollectionId[]) + .pipe(map((allCollections) => allCollections.some((collection) => collection.manage))); + }), + shareReplay({ bufferSize: 1, refCount: false }), + ); + } +} diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index 3e8ec843fd8..961bc03bbb0 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -3,6 +3,10 @@ import { BehaviorSubject, map, of } from "rxjs"; import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service"; +import { + CipherDecryptionKeys, + KeyService, +} from "../../../../key-management/src/abstractions/key.service"; import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; import { FakeStateProvider } from "../../../spec/fake-state-provider"; import { makeStaticByteArray } from "../../../spec/utils"; @@ -12,7 +16,6 @@ import { AutofillSettingsService } from "../../autofill/services/autofill-settin import { DomainSettingsService } from "../../autofill/services/domain-settings.service"; import { UriMatchStrategy } from "../../models/domain/domain-service"; import { ConfigService } from "../../platform/abstractions/config/config.service"; -import { CipherDecryptionKeys, CryptoService } from "../../platform/abstractions/crypto.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { StateService } from "../../platform/abstractions/state.service"; @@ -107,7 +110,7 @@ const mockUserId = Utils.newGuid() as UserId; let accountService: FakeAccountService; describe("Cipher Service", () => { - const cryptoService = mock(); + const keyService = mock(); const stateService = mock(); const autofillSettingsService = mock(); const domainSettingsService = mock(); @@ -130,10 +133,10 @@ describe("Cipher Service", () => { encryptService.encryptToBytes.mockReturnValue(Promise.resolve(ENCRYPTED_BYTES)); encryptService.encrypt.mockReturnValue(Promise.resolve(new EncString(ENCRYPTED_TEXT))); - (window as any).bitwardenContainerService = new ContainerService(cryptoService, encryptService); + (window as any).bitwardenContainerService = new ContainerService(keyService, encryptService); cipherService = new CipherService( - cryptoService, + keyService, domainSettingsService, apiService, i18nService, @@ -159,10 +162,10 @@ describe("Cipher Service", () => { it("should upload encrypted file contents with save attachments", async () => { const fileName = "filename"; const fileData = new Uint8Array(10); - cryptoService.getOrgKey.mockReturnValue( + keyService.getOrgKey.mockReturnValue( Promise.resolve(new SymmetricCryptoKey(new Uint8Array(32)) as OrgKey), ); - cryptoService.makeDataEncKey.mockReturnValue( + keyService.makeDataEncKey.mockReturnValue( Promise.resolve(new SymmetricCryptoKey(new Uint8Array(32))), ); @@ -271,7 +274,7 @@ describe("Cipher Service", () => { encryptService.decryptToBytes.mockReturnValue(Promise.resolve(makeStaticByteArray(64))); configService.checkServerMeetsVersionRequirement$.mockReturnValue(of(true)); - cryptoService.makeCipherKey.mockReturnValue( + keyService.makeCipherKey.mockReturnValue( Promise.resolve(new SymmetricCryptoKey(makeStaticByteArray(64)) as CipherKey), ); encryptService.encrypt.mockImplementation(encryptText); @@ -286,7 +289,7 @@ describe("Cipher Service", () => { { uri: "uri", match: UriMatchStrategy.RegularExpression } as LoginUriView, ]; - cryptoService.getOrgKey.mockReturnValue( + keyService.getOrgKey.mockReturnValue( Promise.resolve(new SymmetricCryptoKey(new Uint8Array(32)) as OrgKey), ); @@ -306,7 +309,7 @@ describe("Cipher Service", () => { it("is null when feature flag is false", async () => { configService.getFeatureFlag.mockResolvedValue(false); - cryptoService.getOrgKey.mockReturnValue( + keyService.getOrgKey.mockReturnValue( Promise.resolve(new SymmetricCryptoKey(new Uint8Array(32)) as OrgKey), ); const cipher = await cipherService.encrypt(cipherView, userId); @@ -330,7 +333,7 @@ describe("Cipher Service", () => { it("is not called when feature flag is false", async () => { configService.getFeatureFlag.mockResolvedValue(false); - cryptoService.getOrgKey.mockReturnValue( + keyService.getOrgKey.mockReturnValue( Promise.resolve(new SymmetricCryptoKey(new Uint8Array(32)) as OrgKey), ); @@ -341,7 +344,7 @@ describe("Cipher Service", () => { it("is called when feature flag is true", async () => { configService.getFeatureFlag.mockResolvedValue(true); - cryptoService.getOrgKey.mockReturnValue( + keyService.getOrgKey.mockReturnValue( Promise.resolve(new SymmetricCryptoKey(new Uint8Array(32)) as OrgKey), ); @@ -368,7 +371,7 @@ describe("Cipher Service", () => { const keys = { userKey: originalUserKey, } as CipherDecryptionKeys; - cryptoService.cipherDecryptionKeys$.mockReturnValue(of(keys)); + keyService.cipherDecryptionKeys$.mockReturnValue(of(keys)); const cipher1 = new CipherView(cipherObj); cipher1.id = "Cipher 1"; @@ -387,7 +390,7 @@ describe("Cipher Service", () => { encryptedKey = new EncString("Re-encrypted Cipher Key"); encryptService.encrypt.mockResolvedValue(encryptedKey); - cryptoService.makeCipherKey.mockResolvedValue( + keyService.makeCipherKey.mockResolvedValue( new SymmetricCryptoKey(new Uint8Array(32)) as CipherKey, ); }); diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index a7377a93eec..154042601e9 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -15,6 +15,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service"; +import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { ApiService } from "../../abstractions/api.service"; import { SearchService } from "../../abstractions/search.service"; import { AutofillSettingsServiceAbstraction } from "../../autofill/services/autofill-settings.service"; @@ -24,7 +25,6 @@ import { ErrorResponse } from "../../models/response/error.response"; import { ListResponse } from "../../models/response/list.response"; import { View } from "../../models/view/view"; import { ConfigService } from "../../platform/abstractions/config/config.service"; -import { CryptoService } from "../../platform/abstractions/crypto.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { StateService } from "../../platform/abstractions/state.service"; @@ -112,7 +112,7 @@ export class CipherService implements CipherServiceAbstraction { private addEditCipherInfoState: ActiveUserState; constructor( - private cryptoService: CryptoService, + private keyService: KeyService, private domainSettingsService: DomainSettingsService, private apiService: ApiService, private i18nService: I18nService, @@ -400,7 +400,7 @@ export class CipherService implements CipherServiceAbstraction { } private async decryptCiphers(ciphers: Cipher[], userId: UserId) { - const keys = await firstValueFrom(this.cryptoService.cipherDecryptionKeys$(userId, true)); + const keys = await firstValueFrom(this.keyService.cipherDecryptionKeys$(userId, true)); if (keys == null || (keys.userKey == null && Object.keys(keys.orgKeys).length === 0)) { // return early if there are no keys to decrypt with @@ -550,7 +550,7 @@ export class CipherService implements CipherServiceAbstraction { } const ciphers = response.data.map((cr) => new Cipher(new CipherData(cr))); - const key = await this.cryptoService.getOrgKey(organizationId); + const key = await this.keyService.getOrgKey(organizationId); let decCiphers: CipherView[] = []; if (await this.configService.getFeatureFlag(FeatureFlag.PM4154_BulkEncryptionService)) { decCiphers = await this.bulkEncryptService.decryptItems(ciphers, key); @@ -650,14 +650,11 @@ export class CipherService implements CipherServiceAbstraction { ciphersLocalData = {}; } - const cipherId = id as CipherId; - if (ciphersLocalData[cipherId]) { - ciphersLocalData[cipherId].lastLaunched = new Date().getTime(); - } else { - ciphersLocalData[cipherId] = { - lastUsedDate: new Date().getTime(), - }; - } + const currentTime = new Date().getTime(); + ciphersLocalData[id as CipherId] = { + lastLaunched: currentTime, + lastUsedDate: currentTime, + }; await this.localDataState.update(() => ciphersLocalData); @@ -851,7 +848,7 @@ export class CipherService implements CipherServiceAbstraction { const encFileName = await this.encryptService.encrypt(filename, cipherEncKey); - const dataEncKey = await this.cryptoService.makeDataEncKey(cipherEncKey); + const dataEncKey = await this.keyService.makeDataEncKey(cipherEncKey); const encData = await this.encryptService.encryptToBytes(new Uint8Array(data), dataEncKey[0]); const response = await this.cipherFileUploadService.upload( @@ -1248,8 +1245,8 @@ export class CipherService implements CipherServiceAbstraction { async getKeyForCipherKeyDecryption(cipher: Cipher, userId: UserId): Promise { return ( - (await this.cryptoService.getOrgKey(cipher.organizationId)) || - ((await this.cryptoService.getUserKeyWithLegacySupport(userId)) as UserKey) + (await this.keyService.getOrgKey(cipher.organizationId)) || + ((await this.keyService.getUserKeyWithLegacySupport(userId)) as UserKey) ); } @@ -1297,7 +1294,7 @@ export class CipherService implements CipherServiceAbstraction { // In the case of a cipher that is being shared with an organization, we want to decrypt the // cipher key with the user's key and then re-encrypt it with the organization's key. private async encryptSharedCipher(model: CipherView, userId: UserId): Promise { - const keyForCipherKeyDecryption = await this.cryptoService.getUserKeyWithLegacySupport(userId); + const keyForCipherKeyDecryption = await this.keyService.getUserKeyWithLegacySupport(userId); return await this.encrypt(model, userId, null, keyForCipherKeyDecryption); } @@ -1374,14 +1371,14 @@ export class CipherService implements CipherServiceAbstraction { const encBuf = await EncArrayBuffer.fromResponse(attachmentResponse); const activeUserId = await firstValueFrom(this.accountService.activeAccount$); - const userKey = await this.cryptoService.getUserKeyWithLegacySupport(activeUserId.id); + const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId.id); const decBuf = await this.encryptService.decryptToBytes(encBuf, userKey); let encKey: UserKey | OrgKey; - encKey = await this.cryptoService.getOrgKey(organizationId); - encKey ||= (await this.cryptoService.getUserKeyWithLegacySupport()) as UserKey; + encKey = await this.keyService.getOrgKey(organizationId); + encKey ||= (await this.keyService.getUserKeyWithLegacySupport()) as UserKey; - const dataEncKey = await this.cryptoService.makeDataEncKey(encKey); + const dataEncKey = await this.keyService.makeDataEncKey(encKey); const encFileName = await this.encryptService.encrypt(attachmentView.fileName, encKey); const encData = await this.encryptService.encryptToBytes(new Uint8Array(decBuf), dataEncKey[0]); @@ -1682,7 +1679,7 @@ export class CipherService implements CipherServiceAbstraction { // First, we get the key for cipher key encryption, in its decrypted form let decryptedCipherKey: SymmetricCryptoKey; if (cipher.key == null) { - decryptedCipherKey = await this.cryptoService.makeCipherKey(); + decryptedCipherKey = await this.keyService.makeCipherKey(); } else { decryptedCipherKey = new SymmetricCryptoKey( await this.encryptService.decryptToBytes(cipher.key, keyForCipherKeyDecryption), diff --git a/libs/common/src/vault/services/folder/folder.service.spec.ts b/libs/common/src/vault/services/folder/folder.service.spec.ts index 05e1cdebc93..193d0e85e61 100644 --- a/libs/common/src/vault/services/folder/folder.service.spec.ts +++ b/libs/common/src/vault/services/folder/folder.service.spec.ts @@ -1,11 +1,11 @@ import { mock, MockProxy } from "jest-mock-extended"; import { firstValueFrom } from "rxjs"; +import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; import { makeStaticByteArray } from "../../../../spec"; import { FakeAccountService, mockAccountServiceWith } from "../../../../spec/fake-account-service"; import { FakeActiveUserState } from "../../../../spec/fake-state"; import { FakeStateProvider } from "../../../../spec/fake-state-provider"; -import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; import { Utils } from "../../../platform/misc/utils"; @@ -22,7 +22,7 @@ import { FOLDER_ENCRYPTED_FOLDERS } from "../key-state/folder.state"; describe("Folder Service", () => { let folderService: FolderService; - let cryptoService: MockProxy; + let keyService: MockProxy; let encryptService: MockProxy; let i18nService: MockProxy; let cipherService: MockProxy; @@ -33,7 +33,7 @@ describe("Folder Service", () => { let folderState: FakeActiveUserState>; beforeEach(() => { - cryptoService = mock(); + keyService = mock(); encryptService = mock(); i18nService = mock(); cipherService = mock(); @@ -43,14 +43,14 @@ describe("Folder Service", () => { i18nService.collator = new Intl.Collator("en"); - cryptoService.hasUserKey.mockResolvedValue(true); - cryptoService.getUserKeyWithLegacySupport.mockResolvedValue( + keyService.hasUserKey.mockResolvedValue(true); + keyService.getUserKeyWithLegacySupport.mockResolvedValue( new SymmetricCryptoKey(makeStaticByteArray(32)) as UserKey, ); encryptService.decryptToUtf8.mockResolvedValue("DEC"); folderService = new FolderService( - cryptoService, + keyService, encryptService, i18nService, cipherService, diff --git a/libs/common/src/vault/services/folder/folder.service.ts b/libs/common/src/vault/services/folder/folder.service.ts index 2adbc8c6d0e..2a76e82f3b7 100644 --- a/libs/common/src/vault/services/folder/folder.service.ts +++ b/libs/common/src/vault/services/folder/folder.service.ts @@ -2,7 +2,7 @@ import { Observable, firstValueFrom, map, shareReplay } from "rxjs"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; -import { CryptoService } from "../../../platform/abstractions/crypto.service"; +import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; import { Utils } from "../../../platform/misc/utils"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; @@ -26,7 +26,7 @@ export class FolderService implements InternalFolderServiceAbstraction { private decryptedFoldersState: DerivedState; constructor( - private cryptoService: CryptoService, + private keyService: KeyService, private encryptService: EncryptService, private i18nService: I18nService, private cipherService: CipherService, @@ -36,7 +36,7 @@ export class FolderService implements InternalFolderServiceAbstraction { this.decryptedFoldersState = this.stateProvider.getDerived( this.encryptedFoldersState.state$, FOLDER_DECRYPTED_FOLDERS, - { folderService: this, cryptoService: this.cryptoService }, + { folderService: this, keyService: this.keyService }, ); this.folders$ = this.encryptedFoldersState.state$.pipe( diff --git a/libs/common/src/vault/services/key-state/folder.state.spec.ts b/libs/common/src/vault/services/key-state/folder.state.spec.ts index 072372f55d8..ece66b5d451 100644 --- a/libs/common/src/vault/services/key-state/folder.state.spec.ts +++ b/libs/common/src/vault/services/key-state/folder.state.spec.ts @@ -1,6 +1,6 @@ import { mock } from "jest-mock-extended"; -import { CryptoService } from "../../../platform/abstractions/crypto.service"; +import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; import { FolderService } from "../../abstractions/folder/folder.service.abstraction"; import { FolderData } from "../../models/data/folder.data"; import { Folder } from "../../models/domain/folder"; @@ -31,7 +31,7 @@ describe("encrypted folders", () => { }); describe("derived decrypted folders", () => { - const cryptoService = mock(); + const keyService = mock(); const folderService = mock(); const sut = FOLDER_DECRYPTED_FOLDERS; let data: FolderData; @@ -64,13 +64,13 @@ describe("derived decrypted folders", () => { it("should derive encrypted folders", async () => { const folderViewMock = new FolderView(new Folder(data)); - cryptoService.hasUserKey.mockResolvedValue(true); + keyService.hasUserKey.mockResolvedValue(true); folderService.decryptFolders.mockResolvedValue([folderViewMock]); const encryptedFoldersState = { id: data }; const derivedStateResult = await sut.derive(encryptedFoldersState, { folderService, - cryptoService, + keyService, }); expect(derivedStateResult).toEqual([folderViewMock]); diff --git a/libs/common/src/vault/services/key-state/folder.state.ts b/libs/common/src/vault/services/key-state/folder.state.ts index 1a45c88d6f2..7262d72d58e 100644 --- a/libs/common/src/vault/services/key-state/folder.state.ts +++ b/libs/common/src/vault/services/key-state/folder.state.ts @@ -1,6 +1,6 @@ import { Jsonify } from "type-fest"; -import { CryptoService } from "../../../platform/abstractions/crypto.service"; +import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; import { DeriveDefinition, FOLDER_DISK, UserKeyDefinition } from "../../../platform/state"; import { FolderService } from "../../abstractions/folder/folder.service.abstraction"; import { FolderData } from "../../models/data/folder.data"; @@ -19,13 +19,13 @@ export const FOLDER_ENCRYPTED_FOLDERS = UserKeyDefinition.record( export const FOLDER_DECRYPTED_FOLDERS = DeriveDefinition.from< Record, FolderView[], - { folderService: FolderService; cryptoService: CryptoService } + { folderService: FolderService; keyService: KeyService } >(FOLDER_ENCRYPTED_FOLDERS, { deserializer: (obj) => obj.map((f) => FolderView.fromJSON(f)), - derive: async (from, { folderService, cryptoService }) => { + derive: async (from, { folderService, keyService }) => { const folders = Object.values(from || {}).map((f) => new Folder(f)); - if (await cryptoService.hasUserKey()) { + if (await keyService.hasUserKey()) { return await folderService.decryptFolders(folders); } else { return []; diff --git a/libs/common/tsconfig.json b/libs/common/tsconfig.json index 11cdb4e44c0..99c58f3cf24 100644 --- a/libs/common/tsconfig.json +++ b/libs/common/tsconfig.json @@ -1,5 +1,12 @@ { "extends": "../shared/tsconfig.libs", - "include": ["src", "spec", "./custom-matchers.d.ts"], + "include": [ + "src", + "spec", + "./custom-matchers.d.ts", + "../key-management/src/key.service.spec.ts", + "../key-management/src/key.service.ts", + "../key-management/src/abstractions/key.service.ts" + ], "exclude": ["node_modules", "dist"] } diff --git a/libs/components/src/async-actions/in-forms.mdx b/libs/components/src/async-actions/in-forms.mdx index e0715fed419..6b1ab864248 100644 --- a/libs/components/src/async-actions/in-forms.mdx +++ b/libs/components/src/async-actions/in-forms.mdx @@ -42,7 +42,7 @@ class Component { return; } - await this.cryptoService.encrypt(/* ... */); + await this.keyService.encrypt(/* ... */); // `formGroup.invalid` will always return `true` here diff --git a/libs/components/src/icon/icon.mdx b/libs/components/src/icon/icon.mdx index d8b881b7e86..fc1c4cd3d57 100644 --- a/libs/components/src/icon/icon.mdx +++ b/libs/components/src/icon/icon.mdx @@ -67,7 +67,14 @@ import * as stories from "./icon.stories"; - Example: `--color-art-primary` corresponds to `tw-stroke-art-primary` or `tw-fill-art-primary`. -6. **Import your SVG const** anywhere you want to use the SVG. +6. **Remove any hardcoded width or height attributes** if your SVG has a configured + [viewBox](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/viewBox) attribute in order + to allow the SVG to scale to fit its container. + + - **Note:** Scaling is required for any SVG used as an + [AnonLayout](?path=/docs/auth-anon-layout--docs) `pageIcon`. + +7. **Import your SVG const** anywhere you want to use the SVG. - **Angular Component Example:** @@ -95,5 +102,5 @@ import * as stories from "./icon.stories"; ``` -7. **Ensure your SVG renders properly** according to Figma in both light and dark modes on a client +8. **Ensure your SVG renders properly** according to Figma in both light and dark modes on a client which supports multiple style modes. diff --git a/libs/components/src/table/table-data-source.ts b/libs/components/src/table/table-data-source.ts index 36f58667632..6501c9bffbd 100644 --- a/libs/components/src/table/table-data-source.ts +++ b/libs/components/src/table/table-data-source.ts @@ -74,7 +74,7 @@ export class TableDataSource extends DataSource { } } - connect(): Observable { + connect(): Observable { if (!this._renderChangesSubscription) { this.updateChangeSubscription(); } diff --git a/libs/components/src/table/table-scroll.component.html b/libs/components/src/table/table-scroll.component.html new file mode 100644 index 00000000000..26b06ee0e5c --- /dev/null +++ b/libs/components/src/table/table-scroll.component.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + +
+
diff --git a/libs/components/src/table/table-scroll.component.ts b/libs/components/src/table/table-scroll.component.ts new file mode 100644 index 00000000000..77647133bc0 --- /dev/null +++ b/libs/components/src/table/table-scroll.component.ts @@ -0,0 +1,92 @@ +import { + AfterContentChecked, + Component, + ContentChild, + Input, + OnDestroy, + TemplateRef, + Directive, + NgZone, + AfterViewInit, + ElementRef, + TrackByFunction, +} from "@angular/core"; + +import { TableComponent } from "./table.component"; + +/** + * Helper directive for defining the row template. + * + * ```html + * + * {{ row.id }} + * + * ``` + */ +@Directive({ + selector: "[bitRowDef]", + standalone: true, +}) +export class BitRowDef { + constructor(public template: TemplateRef) {} +} + +/** + * Scrollable table component. + * + * Utilizes virtual scrolling to render large datasets. + */ +@Component({ + selector: "bit-table-scroll", + templateUrl: "./table-scroll.component.html", + providers: [{ provide: TableComponent, useExisting: TableScrollComponent }], +}) +export class TableScrollComponent + extends TableComponent + implements AfterContentChecked, AfterViewInit, OnDestroy +{ + /** The size of the rows in the list (in pixels). */ + @Input({ required: true }) rowSize: number; + + /** Optional trackBy function. */ + @Input() trackBy: TrackByFunction | undefined; + + @ContentChild(BitRowDef) protected rowDef: BitRowDef; + + /** + * Height of the thead element (in pixels). + * + * Used to increase the table's total height to avoid items being cut off. + */ + protected headerHeight = 0; + + /** + * Observer for table header, applies padding on resize. + */ + private headerObserver: ResizeObserver; + + constructor( + private zone: NgZone, + private el: ElementRef, + ) { + super(); + } + + ngAfterViewInit(): void { + this.headerObserver = new ResizeObserver((entries) => { + this.zone.run(() => { + this.headerHeight = entries[0].contentRect.height; + }); + }); + + this.headerObserver.observe(this.el.nativeElement.querySelector("thead")); + } + + override ngOnDestroy(): void { + super.ngOnDestroy(); + + if (this.headerObserver) { + this.headerObserver.disconnect(); + } + } +} diff --git a/libs/components/src/table/table.component.html b/libs/components/src/table/table.component.html index 09b035826c9..6615d0cb42a 100644 --- a/libs/components/src/table/table.component.html +++ b/libs/components/src/table/table.component.html @@ -6,7 +6,7 @@ diff --git a/libs/components/src/table/table.component.ts b/libs/components/src/table/table.component.ts index b4d6d1931d1..16f7a01b7c8 100644 --- a/libs/components/src/table/table.component.ts +++ b/libs/components/src/table/table.component.ts @@ -30,7 +30,7 @@ export class TableComponent implements OnDestroy, AfterContentChecked { @ContentChild(TableBodyDirective) templateVariable: TableBodyDirective; - protected rows: Observable; + protected rows$: Observable; private _initialized = false; @@ -50,7 +50,7 @@ export class TableComponent implements OnDestroy, AfterContentChecked { this._initialized = true; const dataStream = this.dataSource.connect(); - this.rows = dataStream; + this.rows$ = dataStream; } } diff --git a/libs/components/src/table/table.mdx b/libs/components/src/table/table.mdx index f08691f7640..3f28dd93b68 100644 --- a/libs/components/src/table/table.mdx +++ b/libs/components/src/table/table.mdx @@ -141,28 +141,28 @@ dataSource.filter = "search value"; ### Virtual Scrolling It's heavily adviced to use virtual scrolling if you expect the table to have any significant amount -of data. This is easily done by wrapping the table in the `cdk-virtual-scroll-viewport` component, -specify a `itemSize`, set `scrollWindow` to `true` and replace `*ngFor` with `*cdkVirtualFor`. +of data. This is done by using the `bit-table-scroll` component instead of the `bit-table` +component. This component behaves slightly different from the `bit-table` component. Instead of +using the `*ngFor` directive to render the rows, you provide a `bitRowDef` template that will be +used for rendering the rows. + +Due to limitations in the Angular Component Dev Kit you must provide an `rowSize` which corresponds +to the height of each row. If the height of the rows are not uniform, you should set an explicit row +height and align vertically. ```html - - - - - Id - Name - Other - - - - - {{ r.id }} - {{ r.name }} - {{ r.other }} - - - - + + + Id + Name + Other + + + {{ row.id }} + {{ row.name }} + {{ row.other }} + + ``` ## Accessibility diff --git a/libs/components/src/table/table.module.ts b/libs/components/src/table/table.module.ts index 753e4362e6f..1f1b705c69e 100644 --- a/libs/components/src/table/table.module.ts +++ b/libs/components/src/table/table.module.ts @@ -1,20 +1,31 @@ +import { ScrollingModule } from "@angular/cdk/scrolling"; import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; import { CellDirective } from "./cell.directive"; import { RowDirective } from "./row.directive"; import { SortableComponent } from "./sortable.component"; +import { BitRowDef, TableScrollComponent } from "./table-scroll.component"; import { TableBodyDirective, TableComponent } from "./table.component"; @NgModule({ - imports: [CommonModule], + imports: [CommonModule, ScrollingModule, BitRowDef], declarations: [ + CellDirective, + RowDirective, + SortableComponent, + TableBodyDirective, TableComponent, + TableScrollComponent, + ], + exports: [ + BitRowDef, CellDirective, RowDirective, SortableComponent, TableBodyDirective, + TableComponent, + TableScrollComponent, ], - exports: [TableComponent, CellDirective, RowDirective, SortableComponent, TableBodyDirective], }) export class TableModule {} diff --git a/libs/components/src/table/table.stories.ts b/libs/components/src/table/table.stories.ts index 8fa3f5559df..4ebc3045d13 100644 --- a/libs/components/src/table/table.stories.ts +++ b/libs/components/src/table/table.stories.ts @@ -1,4 +1,3 @@ -import { ScrollingModule } from "@angular/cdk/scrolling"; import { Meta, moduleMetadata, StoryObj } from "@storybook/angular"; import { countries } from "../form/countries"; @@ -10,7 +9,7 @@ export default { title: "Component Library/Table", decorators: [ moduleMetadata({ - imports: [TableModule, ScrollingModule], + imports: [TableModule], }), ], argTypes: { @@ -114,26 +113,21 @@ export const Scrollable: Story = { props: { dataSource: data2, sortFn: (a: any, b: any) => a.id - b.id, + trackBy: (index: number, item: any) => item.id, }, template: ` - - - - - Id - Name - Other - - - - - {{ r.id }} - {{ r.name }} - {{ r.other }} - - - - + + + Id + Name + Other + + + {{ row.id }} + {{ row.name }} + {{ row.other }} + + `, }), }; @@ -151,22 +145,16 @@ export const Filterable: Story = { }, template: ` - - - - - Name - Value - - - - - {{ r.name }} - {{ r.value }} - - - - + + + Name + Value + + + {{ row.name }} + {{ row.value }} + + `, }), }; diff --git a/libs/importer/spec/bitwarden-password-protected-importer.spec.ts b/libs/importer/spec/bitwarden-password-protected-importer.spec.ts index e5100e49900..d15aa61c8a7 100644 --- a/libs/importer/spec/bitwarden-password-protected-importer.spec.ts +++ b/libs/importer/spec/bitwarden-password-protected-importer.spec.ts @@ -2,12 +2,12 @@ import { mock, MockProxy } from "jest-mock-extended"; import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { KdfType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { KeyService } from "@bitwarden/key-management"; import { BitwardenPasswordProtectedImporter, @@ -19,7 +19,7 @@ import { emptyUnencryptedExport } from "./test-data/bitwarden-json/unencrypted.j describe("BitwardenPasswordProtectedImporter", () => { let importer: BitwardenPasswordProtectedImporter; - let cryptoService: MockProxy; + let keyService: MockProxy; let encryptService: MockProxy; let i18nService: MockProxy; let cipherService: MockProxy; @@ -31,7 +31,7 @@ describe("BitwardenPasswordProtectedImporter", () => { }; beforeEach(() => { - cryptoService = mock(); + keyService = mock(); encryptService = mock(); i18nService = mock(); cipherService = mock(); @@ -39,7 +39,7 @@ describe("BitwardenPasswordProtectedImporter", () => { accountService = mock(); importer = new BitwardenPasswordProtectedImporter( - cryptoService, + keyService, encryptService, i18nService, cipherService, diff --git a/libs/importer/spec/keepassx-csv-importer.spec.ts b/libs/importer/spec/keepassx-csv-importer.spec.ts new file mode 100644 index 00000000000..0b3d729d9de --- /dev/null +++ b/libs/importer/spec/keepassx-csv-importer.spec.ts @@ -0,0 +1,42 @@ +import { KeePassXCsvImporter } from "../src/importers"; + +import { keepassxTestData } from "./test-data/keepassx-csv/testdata.csv"; + +describe("KeePassX CSV Importer", () => { + let importer: KeePassXCsvImporter; + + beforeEach(() => { + importer = new KeePassXCsvImporter(); + }); + + describe("given login data", () => { + it("should parse login data when provided valid CSV", async () => { + const result = await importer.parse(keepassxTestData); + expect(result != null).toBe(true); + + const cipher = result.ciphers.shift(); + expect(cipher.name).toEqual("Example Entry"); + expect(cipher.login.username).toEqual("testuser"); + expect(cipher.login.password).toEqual("password123"); + expect(cipher.login.uris.length).toEqual(1); + const uriView = cipher.login.uris.shift(); + expect(uriView.uri).toEqual("https://example.com"); + expect(cipher.notes).toEqual("Some notes"); + }); + + it("should import TOTP when present in the CSV", async () => { + const result = await importer.parse(keepassxTestData); + expect(result != null).toBe(true); + + const cipher = result.ciphers.pop(); + expect(cipher.name).toEqual("Another Entry"); + expect(cipher.login.username).toEqual("anotheruser"); + expect(cipher.login.password).toEqual("anotherpassword"); + expect(cipher.login.uris.length).toEqual(1); + const uriView = cipher.login.uris.shift(); + expect(uriView.uri).toEqual("https://another.com"); + expect(cipher.notes).toEqual("Another set of notes"); + expect(cipher.login.totp).toEqual("otpauth://totp/Another?secret=ABCD1234EFGH5678"); + }); + }); +}); diff --git a/libs/importer/spec/test-data/keepassx-csv/testdata.csv.ts b/libs/importer/spec/test-data/keepassx-csv/testdata.csv.ts new file mode 100644 index 00000000000..99eb99b993a --- /dev/null +++ b/libs/importer/spec/test-data/keepassx-csv/testdata.csv.ts @@ -0,0 +1,3 @@ +export const keepassxTestData = `Title,Username,Password,URL,Notes,TOTP +Example Entry,testuser,password123,https://example.com,Some notes, +Another Entry,anotheruser,anotherpassword,https://another.com,Another set of notes,otpauth://totp/Another?secret=ABCD1234EFGH5678`; diff --git a/libs/importer/src/components/import.component.ts b/libs/importer/src/components/import.component.ts index 9be0428c865..1ffe2728b05 100644 --- a/libs/importer/src/components/import.component.ts +++ b/libs/importer/src/components/import.component.ts @@ -30,7 +30,6 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { ClientType } from "@bitwarden/common/enums"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -56,6 +55,7 @@ import { SelectModule, ToastService, } from "@bitwarden/components"; +import { KeyService } from "@bitwarden/key-management"; import { ImportOption, ImportResult, ImportType } from "../models"; import { @@ -88,7 +88,7 @@ const safeProviders: SafeProvider[] = [ ImportApiServiceAbstraction, I18nService, CollectionService, - CryptoService, + KeyService, EncryptService, PinServiceAbstraction, AccountService, diff --git a/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts b/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts index 160a55e6f28..42033b3d61a 100644 --- a/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts +++ b/libs/importer/src/importers/bitwarden/bitwarden-json-importer.ts @@ -8,7 +8,6 @@ import { CollectionWithIdExport, FolderWithIdExport, } from "@bitwarden/common/models/export"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; @@ -16,6 +15,7 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym import { OrganizationId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; +import { KeyService } from "@bitwarden/key-management"; import { BitwardenEncryptedIndividualJsonExport, BitwardenEncryptedOrgJsonExport, @@ -32,7 +32,7 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer { private result: ImportResult; protected constructor( - protected cryptoService: CryptoService, + protected keyService: KeyService, protected encryptService: EncryptService, protected i18nService: I18nService, protected cipherService: CipherService, @@ -63,11 +63,11 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer { results: BitwardenEncryptedIndividualJsonExport | BitwardenEncryptedOrgJsonExport, ) { if (results.encKeyValidation_DO_NOT_EDIT != null) { - let keyForDecryption: SymmetricCryptoKey = await this.cryptoService.getOrgKey( + let keyForDecryption: SymmetricCryptoKey = await this.keyService.getOrgKey( this.organizationId, ); if (keyForDecryption == null) { - keyForDecryption = await this.cryptoService.getUserKeyWithLegacySupport(); + keyForDecryption = await this.keyService.getUserKeyWithLegacySupport(); } const encKeyValidation = new EncString(results.encKeyValidation_DO_NOT_EDIT); const encKeyValidationDecrypt = await this.encryptService.decryptToUtf8( @@ -210,8 +210,8 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer { if (data.encrypted) { const collection = CollectionWithIdExport.toDomain(c); collection.organizationId = this.organizationId; - collectionView = await firstValueFrom(this.cryptoService.activeUserOrgKeys$).then( - (orgKeys) => collection.decrypt(orgKeys[c.organizationId as OrganizationId]), + collectionView = await firstValueFrom(this.keyService.activeUserOrgKeys$).then((orgKeys) => + collection.decrypt(orgKeys[c.organizationId as OrganizationId]), ); } else { collectionView = CollectionWithIdExport.toView(c); diff --git a/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.ts b/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.ts index 35a0ec0f22c..fa19e3c0001 100644 --- a/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.ts +++ b/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.ts @@ -5,13 +5,13 @@ import { KdfConfig, PBKDF2KdfConfig, } from "@bitwarden/common/auth/models/domain/kdf-config"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { KdfType } from "@bitwarden/common/platform/enums"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { KeyService } from "@bitwarden/key-management"; import { BitwardenPasswordProtectedFileFormat } from "@bitwarden/vault-export-core"; import { ImportResult } from "../../models/import-result"; @@ -23,7 +23,7 @@ export class BitwardenPasswordProtectedImporter extends BitwardenJsonImporter im private key: SymmetricCryptoKey; constructor( - cryptoService: CryptoService, + keyService: KeyService, encryptService: EncryptService, i18nService: I18nService, cipherService: CipherService, @@ -31,7 +31,7 @@ export class BitwardenPasswordProtectedImporter extends BitwardenJsonImporter im accountService: AccountService, private promptForPassword_callback: () => Promise, ) { - super(cryptoService, encryptService, i18nService, cipherService, pinService, accountService); + super(keyService, encryptService, i18nService, cipherService, pinService, accountService); } async parse(data: string): Promise { diff --git a/libs/importer/src/importers/keepassx-csv-importer.ts b/libs/importer/src/importers/keepassx-csv-importer.ts index 4047a49d572..03aa18cecba 100644 --- a/libs/importer/src/importers/keepassx-csv-importer.ts +++ b/libs/importer/src/importers/keepassx-csv-importer.ts @@ -30,6 +30,8 @@ export class KeePassXCsvImporter extends BaseImporter implements Importer { cipher.login.username = this.getValueOrDefault(value.Username); cipher.login.password = this.getValueOrDefault(value.Password); cipher.login.uris = this.makeUriArray(value.URL); + cipher.login.totp = this.getValueOrDefault(value.TOTP); + this.cleanupCipher(cipher); result.ciphers.push(cipher); }); diff --git a/libs/importer/src/services/import.service.spec.ts b/libs/importer/src/services/import.service.spec.ts index c221e15aa7f..8b497beac93 100644 --- a/libs/importer/src/services/import.service.spec.ts +++ b/libs/importer/src/services/import.service.spec.ts @@ -3,7 +3,6 @@ import { mock, MockProxy } from "jest-mock-extended"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -11,6 +10,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; +import { KeyService } from "@bitwarden/key-management"; import { BitwardenPasswordProtectedImporter } from "../importers/bitwarden/bitwarden-password-protected-importer"; import { Importer } from "../importers/importer"; @@ -26,7 +26,7 @@ describe("ImportService", () => { let importApiService: MockProxy; let i18nService: MockProxy; let collectionService: MockProxy; - let cryptoService: MockProxy; + let keyService: MockProxy; let encryptService: MockProxy; let pinService: MockProxy; let accountService: MockProxy; @@ -37,7 +37,7 @@ describe("ImportService", () => { importApiService = mock(); i18nService = mock(); collectionService = mock(); - cryptoService = mock(); + keyService = mock(); encryptService = mock(); pinService = mock(); @@ -47,7 +47,7 @@ describe("ImportService", () => { importApiService, i18nService, collectionService, - cryptoService, + keyService, encryptService, pinService, accountService, diff --git a/libs/importer/src/services/import.service.ts b/libs/importer/src/services/import.service.ts index 1e983aa3d1b..17695c29d57 100644 --- a/libs/importer/src/services/import.service.ts +++ b/libs/importer/src/services/import.service.ts @@ -11,7 +11,6 @@ import { ImportCiphersRequest } from "@bitwarden/common/models/request/import-ci import { ImportOrganizationCiphersRequest } from "@bitwarden/common/models/request/import-organization-ciphers.request"; import { KvpRequest } from "@bitwarden/common/models/request/kvp.request"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -22,6 +21,7 @@ import { CipherRequest } from "@bitwarden/common/vault/models/request/cipher.req import { FolderWithIdRequest } from "@bitwarden/common/vault/models/request/folder-with-id.request"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; +import { KeyService } from "@bitwarden/key-management"; import { AscendoCsvImporter, @@ -106,7 +106,7 @@ export class ImportService implements ImportServiceAbstraction { private importApiService: ImportApiServiceAbstraction, private i18nService: I18nService, private collectionService: CollectionService, - private cryptoService: CryptoService, + private keyService: KeyService, private encryptService: EncryptService, private pinService: PinServiceAbstraction, private accountService: AccountService, @@ -210,7 +210,7 @@ export class ImportService implements ImportServiceAbstraction { case "bitwardenjson": case "bitwardenpasswordprotected": return new BitwardenPasswordProtectedImporter( - this.cryptoService, + this.keyService, this.encryptService, this.i18nService, this.cipherService, @@ -349,7 +349,7 @@ export class ImportService implements ImportServiceAbstraction { const c = await this.cipherService.encrypt(importResult.ciphers[i], activeUserId); request.ciphers.push(new CipherRequest(c)); } - const userKey = await this.cryptoService.getUserKeyWithLegacySupport(activeUserId); + const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId); if (importResult.folders != null) { for (let i = 0; i < importResult.folders.length; i++) { const f = await this.folderService.encrypt(importResult.folders[i], userKey); diff --git a/libs/common/src/platform/abstractions/crypto.service.ts b/libs/key-management/src/abstractions/key.service.ts similarity index 88% rename from libs/common/src/platform/abstractions/crypto.service.ts rename to libs/key-management/src/abstractions/key.service.ts index 020cfb81754..55ffea9db79 100644 --- a/libs/common/src/platform/abstractions/crypto.service.ts +++ b/libs/key-management/src/abstractions/key.service.ts @@ -1,10 +1,15 @@ import { Observable } from "rxjs"; -import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response"; -import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response"; -import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response"; -import { KdfConfig } from "../../auth/models/domain/kdf-config"; -import { OrganizationId, UserId } from "../../types/guid"; +import { EncryptedOrganizationKeyData } from "@bitwarden/common/admin-console/models/data/encrypted-organization-key.data"; + +import { ProfileOrganizationResponse } from "../../../common/src/admin-console/models/response/profile-organization.response"; +import { ProfileProviderOrganizationResponse } from "../../../common/src/admin-console/models/response/profile-provider-organization.response"; +import { ProfileProviderResponse } from "../../../common/src/admin-console/models/response/profile-provider.response"; +import { KdfConfig } from "../../../common/src/auth/models/domain/kdf-config"; +import { KeySuffixOptions, HashPurpose } from "../../../common/src/platform/enums"; +import { EncryptedString, EncString } from "../../../common/src/platform/models/domain/enc-string"; +import { SymmetricCryptoKey } from "../../../common/src/platform/models/domain/symmetric-crypto-key"; +import { OrganizationId, UserId } from "../../../common/src/types/guid"; import { UserKey, MasterKey, @@ -13,10 +18,7 @@ import { CipherKey, UserPrivateKey, UserPublicKey, -} from "../../types/key"; -import { KeySuffixOptions, HashPurpose } from "../enums"; -import { EncString } from "../models/domain/enc-string"; -import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; +} from "../../../common/src/types/key"; export class UserPrivateKeyDecryptionFailedError extends Error { constructor() { @@ -39,7 +41,7 @@ export type CipherDecryptionKeys = { orgKeys: Record; }; -export abstract class CryptoService { +export abstract class KeyService { /** * Retrieves a stream of the given users {@see UserKey} values. Can emit null if the user does not have a user key, e.g. the user * is in a locked or logged out state. @@ -288,6 +290,17 @@ export abstract class CryptoService { */ abstract userPrivateKey$(userId: UserId): Observable; + /** + * Gets an observable stream of the given users encrypted private key, will emit null if the user + * doesn't have an encrypted private key at all. + * + * @param userId The user id of the user to get the data for. + * + * @deprecated Temporary function to allow the SDK to be initialized after the login process, it + * will be removed when auth has been migrated to the SDK. + */ + abstract userEncryptedPrivateKey$(userId: UserId): Observable; + /** * Gets an observable stream of the given users decrypted private key with legacy support, * will emit null if the user doesn't have a UserKey to decrypt the encrypted private key @@ -381,6 +394,18 @@ export abstract class CryptoService { */ abstract orgKeys$(userId: UserId): Observable | null>; + /** + * Gets an observable stream of the given users encrypted organisation keys. + * + * @param userId The user id of the user to get the data for. + * + * @deprecated Temporary function to allow the SDK to be initialized after the login process, it + * will be removed when auth has been migrated to the SDK. + */ + abstract encryptedOrgKeys$( + userId: UserId, + ): Observable>; + /** * Gets an observable stream of the users public key. If the user is does not have * a {@link UserKey} or {@link UserPrivateKey} that is decryptable, this will emit null. diff --git a/libs/auth/src/common/abstractions/user-key-rotation-data-provider.abstraction.ts b/libs/key-management/src/abstractions/user-key-rotation-data-provider.abstraction.ts similarity index 100% rename from libs/auth/src/common/abstractions/user-key-rotation-data-provider.abstraction.ts rename to libs/key-management/src/abstractions/user-key-rotation-data-provider.abstraction.ts diff --git a/libs/key-management/src/index.ts b/libs/key-management/src/index.ts index 298ffd145fd..5ad96ddeba7 100644 --- a/libs/key-management/src/index.ts +++ b/libs/key-management/src/index.ts @@ -4,3 +4,7 @@ export { } from "./biometrics/biometric-state.service"; export { BiometricsService } from "./biometrics/biometric.service"; export * from "./biometrics/biometric.state"; + +export { KeyService } from "./abstractions/key.service"; +export { DefaultKeyService } from "./key.service"; +export { UserKeyRotationDataProvider } from "./abstractions/user-key-rotation-data-provider.abstraction"; diff --git a/libs/common/src/platform/services/crypto.service.spec.ts b/libs/key-management/src/key.service.spec.ts similarity index 80% rename from libs/common/src/platform/services/crypto.service.spec.ts rename to libs/key-management/src/key.service.spec.ts index 769e6942b05..263779f59b3 100644 --- a/libs/common/src/platform/services/crypto.service.spec.ts +++ b/libs/key-management/src/key.service.spec.ts @@ -1,48 +1,48 @@ import { mock } from "jest-mock-extended"; import { bufferCount, firstValueFrom, lastValueFrom, of, take, tap } from "rxjs"; -import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions"; +import { PinServiceAbstraction } from "../../auth/src/common/abstractions"; import { awaitAsync, makeEncString, makeStaticByteArray, makeSymmetricCryptoKey, -} from "../../../spec"; -import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; -import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state"; -import { FakeStateProvider } from "../../../spec/fake-state-provider"; -import { EncryptedOrganizationKeyData } from "../../admin-console/models/data/encrypted-organization-key.data"; -import { KdfConfigService } from "../../auth/abstractions/kdf-config.service"; -import { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service"; -import { VAULT_TIMEOUT } from "../../services/vault-timeout/vault-timeout-settings.state"; -import { CsprngArray } from "../../types/csprng"; -import { OrganizationId, UserId } from "../../types/guid"; -import { UserKey, MasterKey } from "../../types/key"; -import { VaultTimeoutStringType } from "../../types/vault-timeout.type"; -import { CryptoFunctionService } from "../abstractions/crypto-function.service"; -import { UserPrivateKeyDecryptionFailedError } from "../abstractions/crypto.service"; -import { EncryptService } from "../abstractions/encrypt.service"; -import { KeyGenerationService } from "../abstractions/key-generation.service"; -import { LogService } from "../abstractions/log.service"; -import { PlatformUtilsService } from "../abstractions/platform-utils.service"; -import { StateService } from "../abstractions/state.service"; -import { Encrypted } from "../interfaces/encrypted"; -import { Utils } from "../misc/utils"; -import { EncString, EncryptedString } from "../models/domain/enc-string"; -import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; -import { CryptoService } from "../services/crypto.service"; -import { UserKeyDefinition } from "../state"; - -import { USER_ENCRYPTED_ORGANIZATION_KEYS } from "./key-state/org-keys.state"; -import { USER_ENCRYPTED_PROVIDER_KEYS } from "./key-state/provider-keys.state"; +} from "../../common/spec"; +import { FakeAccountService, mockAccountServiceWith } from "../../common/spec/fake-account-service"; +import { FakeActiveUserState, FakeSingleUserState } from "../../common/spec/fake-state"; +import { FakeStateProvider } from "../../common/spec/fake-state-provider"; +import { EncryptedOrganizationKeyData } from "../../common/src/admin-console/models/data/encrypted-organization-key.data"; +import { KdfConfigService } from "../../common/src/auth/abstractions/kdf-config.service"; +import { FakeMasterPasswordService } from "../../common/src/auth/services/master-password/fake-master-password.service"; +import { CryptoFunctionService } from "../../common/src/platform/abstractions/crypto-function.service"; +import { EncryptService } from "../../common/src/platform/abstractions/encrypt.service"; +import { KeyGenerationService } from "../../common/src/platform/abstractions/key-generation.service"; +import { LogService } from "../../common/src/platform/abstractions/log.service"; +import { PlatformUtilsService } from "../../common/src/platform/abstractions/platform-utils.service"; +import { StateService } from "../../common/src/platform/abstractions/state.service"; +import { Encrypted } from "../../common/src/platform/interfaces/encrypted"; +import { Utils } from "../../common/src/platform/misc/utils"; +import { EncString, EncryptedString } from "../../common/src/platform/models/domain/enc-string"; +import { SymmetricCryptoKey } from "../../common/src/platform/models/domain/symmetric-crypto-key"; +import { USER_ENCRYPTED_ORGANIZATION_KEYS } from "../../common/src/platform/services/key-state/org-keys.state"; +import { USER_ENCRYPTED_PROVIDER_KEYS } from "../../common/src/platform/services/key-state/provider-keys.state"; import { USER_ENCRYPTED_PRIVATE_KEY, USER_EVER_HAD_USER_KEY, USER_KEY, -} from "./key-state/user-key.state"; +} from "../../common/src/platform/services/key-state/user-key.state"; +import { UserKeyDefinition } from "../../common/src/platform/state"; +import { VAULT_TIMEOUT } from "../../common/src/services/vault-timeout/vault-timeout-settings.state"; +import { CsprngArray } from "../../common/src/types/csprng"; +import { OrganizationId, UserId } from "../../common/src/types/guid"; +import { UserKey, MasterKey } from "../../common/src/types/key"; +import { VaultTimeoutStringType } from "../../common/src/types/vault-timeout.type"; -describe("cryptoService", () => { - let cryptoService: CryptoService; +import { UserPrivateKeyDecryptionFailedError } from "./abstractions/key.service"; +import { DefaultKeyService } from "./key.service"; + +describe("keyService", () => { + let keyService: DefaultKeyService; const pinService = mock(); const keyGenerationService = mock(); @@ -63,7 +63,7 @@ describe("cryptoService", () => { masterPasswordService = new FakeMasterPasswordService(); stateProvider = new FakeStateProvider(accountService); - cryptoService = new CryptoService( + keyService = new DefaultKeyService( pinService, masterPasswordService, keyGenerationService, @@ -83,7 +83,7 @@ describe("cryptoService", () => { }); it("instantiates", () => { - expect(cryptoService).not.toBeFalsy(); + expect(keyService).not.toBeFalsy(); }); describe("getUserKey", () => { @@ -95,7 +95,7 @@ describe("cryptoService", () => { }); it("retrieves the key state of the requested user", async () => { - await cryptoService.getUserKey(mockUserId); + await keyService.getUserKey(mockUserId); expect(stateProvider.mock.getUserState$).toHaveBeenCalledWith(USER_KEY, mockUserId); }); @@ -103,13 +103,13 @@ describe("cryptoService", () => { it("returns the User Key if available", async () => { stateProvider.singleUser.getFake(mockUserId, USER_KEY).nextState(mockUserKey); - const userKey = await cryptoService.getUserKey(mockUserId); + const userKey = await keyService.getUserKey(mockUserId); expect(userKey).toEqual(mockUserKey); }); it("returns nullish if the user key is not set", async () => { - const userKey = await cryptoService.getUserKey(mockUserId); + const userKey = await keyService.getUserKey(mockUserId); expect(userKey).toBeFalsy(); }); @@ -129,12 +129,12 @@ describe("cryptoService", () => { stateProvider.singleUser .getFake(mockUserId, USER_KEY) .nextState(hasKey ? mockUserKey : null); - expect(await cryptoService[method](mockUserId)).toBe(hasKey); + expect(await keyService[method](mockUserId)).toBe(hasKey); }); it("returns false when no active userId is set", async () => { accountService.activeAccountSubject.next(null); - expect(await cryptoService[method]()).toBe(false); + expect(await keyService[method]()).toBe(false); }); it.each([true, false])( @@ -144,7 +144,7 @@ describe("cryptoService", () => { stateProvider.singleUser .getFake(mockUserId, USER_KEY) .nextState(hasKey ? mockUserKey : null); - expect(await cryptoService[method]()).toBe(hasKey); + expect(await keyService[method]()).toBe(hasKey); }, ); }, @@ -165,9 +165,9 @@ describe("cryptoService", () => { it("returns the User Key if available", async () => { stateProvider.singleUser.getFake(mockUserId, USER_KEY).nextState(mockUserKey); - const getKeySpy = jest.spyOn(cryptoService, "getUserKey"); + const getKeySpy = jest.spyOn(keyService, "getUserKey"); - const userKey = await cryptoService.getUserKeyWithLegacySupport(mockUserId); + const userKey = await keyService.getUserKeyWithLegacySupport(mockUserId); expect(getKeySpy).toHaveBeenCalledWith(mockUserId); expect(getMasterKey).not.toHaveBeenCalled(); @@ -178,7 +178,7 @@ describe("cryptoService", () => { it("returns the user's master key when User Key is not available", async () => { masterPasswordService.masterKeySubject.next(mockMasterKey); - const userKey = await cryptoService.getUserKeyWithLegacySupport(mockUserId); + const userKey = await keyService.getUserKeyWithLegacySupport(mockUserId); expect(getMasterKey).toHaveBeenCalledWith(mockUserId); expect(userKey).toEqual(mockMasterKey); @@ -195,19 +195,19 @@ describe("cryptoService", () => { it("should return true when stored value is true", async () => { everHadUserKeyState.nextState(true); - expect(await firstValueFrom(cryptoService.everHadUserKey$)).toBe(true); + expect(await firstValueFrom(keyService.everHadUserKey$)).toBe(true); }); it("should return false when stored value is false", async () => { everHadUserKeyState.nextState(false); - expect(await firstValueFrom(cryptoService.everHadUserKey$)).toBe(false); + expect(await firstValueFrom(keyService.everHadUserKey$)).toBe(false); }); it("should return false when stored value is null", async () => { everHadUserKeyState.nextState(null); - expect(await firstValueFrom(cryptoService.everHadUserKey$)).toBe(false); + expect(await firstValueFrom(keyService.everHadUserKey$)).toBe(false); }); }); @@ -225,7 +225,7 @@ describe("cryptoService", () => { }); it("should set everHadUserKey if key is not null to true", async () => { - await cryptoService.setUserKey(mockUserKey, mockUserId); + await keyService.setUserKey(mockUserKey, mockUserId); expect(await firstValueFrom(everHadUserKeyState.state$)).toBe(true); }); @@ -234,7 +234,7 @@ describe("cryptoService", () => { it("sets an Auto key if vault timeout is set to 'never'", async () => { await stateProvider.setUserState(VAULT_TIMEOUT, VaultTimeoutStringType.Never, mockUserId); - await cryptoService.setUserKey(mockUserKey, mockUserId); + await keyService.setUserKey(mockUserKey, mockUserId); expect(stateService.setUserKeyAutoUnlock).toHaveBeenCalledWith(mockUserKey.keyB64, { userId: mockUserId, @@ -244,7 +244,7 @@ describe("cryptoService", () => { it("clears the Auto key if vault timeout is set to anything other than null", async () => { await stateProvider.setUserState(VAULT_TIMEOUT, 10, mockUserId); - await cryptoService.setUserKey(mockUserKey, mockUserId); + await keyService.setUserKey(mockUserKey, mockUserId); expect(stateService.setUserKeyAutoUnlock).toHaveBeenCalledWith(null, { userId: mockUserId, @@ -252,7 +252,7 @@ describe("cryptoService", () => { }); it("clears the old deprecated Auto key whenever a User Key is set", async () => { - await cryptoService.setUserKey(mockUserKey, mockUserId); + await keyService.setUserKey(mockUserKey, mockUserId); expect(stateService.setCryptoMasterKeyAuto).toHaveBeenCalledWith(null, { userId: mockUserId, @@ -261,13 +261,11 @@ describe("cryptoService", () => { }); it("throws if key is null", async () => { - await expect(cryptoService.setUserKey(null, mockUserId)).rejects.toThrow("No key provided."); + await expect(keyService.setUserKey(null, mockUserId)).rejects.toThrow("No key provided."); }); it("throws if userId is null", async () => { - await expect(cryptoService.setUserKey(mockUserKey, null)).rejects.toThrow( - "No userId provided.", - ); + await expect(keyService.setUserKey(mockUserKey, null)).rejects.toThrow("No userId provided."); }); describe("Pin Key refresh", () => { @@ -285,7 +283,7 @@ describe("cryptoService", () => { mockPinKeyEncryptedUserKey, ); - await cryptoService.setUserKey(mockUserKey, mockUserId); + await keyService.setUserKey(mockUserKey, mockUserId); expect(pinService.storePinKeyEncryptedUserKey).toHaveBeenCalledWith( mockPinKeyEncryptedUserKey, @@ -299,7 +297,7 @@ describe("cryptoService", () => { pinService.getUserKeyEncryptedPin.mockResolvedValue(mockUserKeyEncryptedPin); pinService.getPinKeyEncryptedUserKeyPersistent.mockResolvedValue(null); - await cryptoService.setUserKey(mockUserKey, mockUserId); + await keyService.setUserKey(mockUserKey, mockUserId); expect(pinService.storePinKeyEncryptedUserKey).toHaveBeenCalledWith( mockPinKeyEncryptedUserKey, @@ -311,7 +309,7 @@ describe("cryptoService", () => { it("clears the pinKeyEncryptedUserKeyPersistent and pinKeyEncryptedUserKeyEphemeral if the UserKeyEncryptedPin is not set", async () => { pinService.getUserKeyEncryptedPin.mockResolvedValue(null); - await cryptoService.setUserKey(mockUserKey, mockUserId); + await keyService.setUserKey(mockUserKey, mockUserId); expect(pinService.clearPinKeyEncryptedUserKeyPersistent).toHaveBeenCalledWith(mockUserId); expect(pinService.clearPinKeyEncryptedUserKeyEphemeral).toHaveBeenCalledWith(mockUserId); @@ -338,19 +336,19 @@ describe("cryptoService", () => { }); it("throws if userKey is null", async () => { - await expect(cryptoService.setUserKeys(null, mockEncPrivateKey, mockUserId)).rejects.toThrow( + await expect(keyService.setUserKeys(null, mockEncPrivateKey, mockUserId)).rejects.toThrow( "No userKey provided.", ); }); it("throws if encPrivateKey is null", async () => { - await expect(cryptoService.setUserKeys(mockUserKey, null, mockUserId)).rejects.toThrow( + await expect(keyService.setUserKeys(mockUserKey, null, mockUserId)).rejects.toThrow( "No encPrivateKey provided.", ); }); it("throws if userId is null", async () => { - await expect(cryptoService.setUserKeys(mockUserKey, mockEncPrivateKey, null)).rejects.toThrow( + await expect(keyService.setUserKeys(mockUserKey, mockEncPrivateKey, null)).rejects.toThrow( "No userId provided.", ); }); @@ -359,15 +357,15 @@ describe("cryptoService", () => { encryptService.decryptToBytes.mockResolvedValue(null); await expect( - cryptoService.setUserKeys(mockUserKey, mockEncPrivateKey, mockUserId), + keyService.setUserKeys(mockUserKey, mockEncPrivateKey, mockUserId), ).rejects.toThrow(UserPrivateKeyDecryptionFailedError); }); // We already have tests for setUserKey, so we just need to test that the correct methods are called it("calls setUserKey with the userKey and userId", async () => { - const setUserKeySpy = jest.spyOn(cryptoService, "setUserKey"); + const setUserKeySpy = jest.spyOn(keyService, "setUserKey"); - await cryptoService.setUserKeys(mockUserKey, mockEncPrivateKey, mockUserId); + await keyService.setUserKeys(mockUserKey, mockEncPrivateKey, mockUserId); expect(setUserKeySpy).toHaveBeenCalledWith(mockUserKey, mockUserId); }); @@ -375,9 +373,9 @@ describe("cryptoService", () => { // We already have tests for setPrivateKey, so we just need to test that the correct methods are called // TODO: Move those tests into here since `setPrivateKey` will be converted to a private method it("calls setPrivateKey with the encPrivateKey and userId", async () => { - const setEncryptedPrivateKeySpy = jest.spyOn(cryptoService, "setPrivateKey"); + const setEncryptedPrivateKeySpy = jest.spyOn(keyService, "setPrivateKey"); - await cryptoService.setUserKeys(mockUserKey, mockEncPrivateKey, mockUserId); + await keyService.setUserKeys(mockUserKey, mockEncPrivateKey, mockUserId); expect(setEncryptedPrivateKeySpy).toHaveBeenCalledWith(mockEncPrivateKey, mockUserId); }); @@ -388,7 +386,7 @@ describe("cryptoService", () => { let callCount = 0; stateProvider.activeUserId$ = stateProvider.activeUserId$.pipe(tap(() => callCount++)); - await cryptoService.clearKeys(null); + await keyService.clearKeys(null); expect(callCount).toBe(1); // revert to the original state @@ -402,7 +400,7 @@ describe("cryptoService", () => { USER_KEY, ])("key removal", (key: UserKeyDefinition) => { it(`clears ${key.key} for active user when unspecified`, async () => { - await cryptoService.clearKeys(null); + await keyService.clearKeys(null); const encryptedOrgKeyState = stateProvider.singleUser.getFake(mockUserId, key); expect(encryptedOrgKeyState.nextMock).toHaveBeenCalledTimes(1); @@ -411,7 +409,7 @@ describe("cryptoService", () => { it(`clears ${key.key} for the specified user when specified`, async () => { const userId = "someOtherUser" as UserId; - await cryptoService.clearKeys(userId); + await keyService.clearKeys(userId); const encryptedOrgKeyState = stateProvider.singleUser.getFake(userId, key); expect(encryptedOrgKeyState.nextMock).toHaveBeenCalledTimes(1); @@ -458,7 +456,7 @@ describe("cryptoService", () => { const fakeUserPublicKey = makeStaticByteArray(10, 2); cryptoFunctionService.rsaExtractPublicKey.mockResolvedValue(fakeUserPublicKey); - const userPrivateKey = await firstValueFrom(cryptoService.userPrivateKey$(mockUserId)); + const userPrivateKey = await firstValueFrom(keyService.userPrivateKey$(mockUserId)); expect(encryptService.decryptToBytes).toHaveBeenCalledWith( fakeEncryptedUserPrivateKey, @@ -471,7 +469,7 @@ describe("cryptoService", () => { it("returns null user private key when no user key is found", async () => { setupKeys({ makeMasterKey: false, makeUserKey: false }); - const userPrivateKey = await firstValueFrom(cryptoService.userPrivateKey$(mockUserId)); + const userPrivateKey = await firstValueFrom(keyService.userPrivateKey$(mockUserId)); expect(encryptService.decryptToBytes).not.toHaveBeenCalled(); @@ -487,7 +485,7 @@ describe("cryptoService", () => { ); encryptedUserPrivateKeyState.nextState(null); - const userPrivateKey = await firstValueFrom(cryptoService.userPrivateKey$(mockUserId)); + const userPrivateKey = await firstValueFrom(keyService.userPrivateKey$(mockUserId)); expect(userPrivateKey).toBeFalsy(); }); }); @@ -568,7 +566,7 @@ describe("cryptoService", () => { encryptedPrivateKey: makeEncString("privateKey"), }); - const decryptionKeys = await firstValueFrom(cryptoService.cipherDecryptionKeys$(mockUserId)); + const decryptionKeys = await firstValueFrom(keyService.cipherDecryptionKeys$(mockUserId)); expect(decryptionKeys).not.toBeNull(); expect(decryptionKeys.userKey).not.toBeNull(); @@ -584,7 +582,7 @@ describe("cryptoService", () => { }, }); - const decryptionKeys = await firstValueFrom(cryptoService.cipherDecryptionKeys$(mockUserId)); + const decryptionKeys = await firstValueFrom(keyService.cipherDecryptionKeys$(mockUserId)); expect(decryptionKeys).not.toBeNull(); expect(decryptionKeys.userKey).not.toBeNull(); @@ -605,7 +603,7 @@ describe("cryptoService", () => { providerKeys: {}, }); - const decryptionKeys = await firstValueFrom(cryptoService.cipherDecryptionKeys$(mockUserId)); + const decryptionKeys = await firstValueFrom(keyService.cipherDecryptionKeys$(mockUserId)); expect(decryptionKeys).not.toBeNull(); expect(decryptionKeys.userKey).not.toBeNull(); @@ -634,7 +632,7 @@ describe("cryptoService", () => { }, }); - const decryptionKeys = await firstValueFrom(cryptoService.cipherDecryptionKeys$(mockUserId)); + const decryptionKeys = await firstValueFrom(keyService.cipherDecryptionKeys$(mockUserId)); expect(decryptionKeys).not.toBeNull(); expect(decryptionKeys.userKey).not.toBeNull(); @@ -653,7 +651,7 @@ describe("cryptoService", () => { it("returns a stream that pays attention to updates of all data", async () => { // Start listening until there have been 6 emissions const promise = lastValueFrom( - cryptoService.cipherDecryptionKeys$(mockUserId).pipe(bufferCount(6), take(1)), + keyService.cipherDecryptionKeys$(mockUserId).pipe(bufferCount(6), take(1)), ); // User has their UserKey set diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/key-management/src/key.service.ts similarity index 90% rename from libs/common/src/platform/services/crypto.service.ts rename to libs/key-management/src/key.service.ts index 6b2afdb9806..b12db176cec 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/key-management/src/key.service.ts @@ -10,20 +10,39 @@ import { switchMap, } from "rxjs"; -import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions"; -import { EncryptedOrganizationKeyData } from "../../admin-console/models/data/encrypted-organization-key.data"; -import { BaseEncryptedOrganizationKey } from "../../admin-console/models/domain/encrypted-organization-key"; -import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response"; -import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response"; -import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response"; -import { AccountService } from "../../auth/abstractions/account.service"; -import { KdfConfigService } from "../../auth/abstractions/kdf-config.service"; -import { InternalMasterPasswordServiceAbstraction } from "../../auth/abstractions/master-password.service.abstraction"; -import { KdfConfig } from "../../auth/models/domain/kdf-config"; -import { Utils } from "../../platform/misc/utils"; -import { VAULT_TIMEOUT } from "../../services/vault-timeout/vault-timeout-settings.state"; -import { CsprngArray } from "../../types/csprng"; -import { OrganizationId, ProviderId, UserId } from "../../types/guid"; +import { PinServiceAbstraction } from "../../auth/src/common/abstractions"; +import { EncryptedOrganizationKeyData } from "../../common/src/admin-console/models/data/encrypted-organization-key.data"; +import { BaseEncryptedOrganizationKey } from "../../common/src/admin-console/models/domain/encrypted-organization-key"; +import { ProfileOrganizationResponse } from "../../common/src/admin-console/models/response/profile-organization.response"; +import { ProfileProviderOrganizationResponse } from "../../common/src/admin-console/models/response/profile-provider-organization.response"; +import { ProfileProviderResponse } from "../../common/src/admin-console/models/response/profile-provider.response"; +import { AccountService } from "../../common/src/auth/abstractions/account.service"; +import { KdfConfigService } from "../../common/src/auth/abstractions/kdf-config.service"; +import { InternalMasterPasswordServiceAbstraction } from "../../common/src/auth/abstractions/master-password.service.abstraction"; +import { KdfConfig } from "../../common/src/auth/models/domain/kdf-config"; +import { CryptoFunctionService } from "../../common/src/platform/abstractions/crypto-function.service"; +import { EncryptService } from "../../common/src/platform/abstractions/encrypt.service"; +import { KeyGenerationService } from "../../common/src/platform/abstractions/key-generation.service"; +import { LogService } from "../../common/src/platform/abstractions/log.service"; +import { PlatformUtilsService } from "../../common/src/platform/abstractions/platform-utils.service"; +import { StateService } from "../../common/src/platform/abstractions/state.service"; +import { KeySuffixOptions, HashPurpose } from "../../common/src/platform/enums"; +import { convertValues } from "../../common/src/platform/misc/convert-values"; +import { Utils } from "../../common/src/platform/misc/utils"; +import { EFFLongWordList } from "../../common/src/platform/misc/wordlist"; +import { EncString, EncryptedString } from "../../common/src/platform/models/domain/enc-string"; +import { SymmetricCryptoKey } from "../../common/src/platform/models/domain/symmetric-crypto-key"; +import { USER_ENCRYPTED_ORGANIZATION_KEYS } from "../../common/src/platform/services/key-state/org-keys.state"; +import { USER_ENCRYPTED_PROVIDER_KEYS } from "../../common/src/platform/services/key-state/provider-keys.state"; +import { + USER_ENCRYPTED_PRIVATE_KEY, + USER_EVER_HAD_USER_KEY, + USER_KEY, +} from "../../common/src/platform/services/key-state/user-key.state"; +import { ActiveUserState, StateProvider } from "../../common/src/platform/state"; +import { VAULT_TIMEOUT } from "../../common/src/services/vault-timeout/vault-timeout-settings.state"; +import { CsprngArray } from "../../common/src/types/csprng"; +import { OrganizationId, ProviderId, UserId } from "../../common/src/types/guid"; import { OrgKey, UserKey, @@ -32,35 +51,16 @@ import { CipherKey, UserPrivateKey, UserPublicKey, -} from "../../types/key"; -import { VaultTimeoutStringType } from "../../types/vault-timeout.type"; -import { CryptoFunctionService } from "../abstractions/crypto-function.service"; +} from "../../common/src/types/key"; +import { VaultTimeoutStringType } from "../../common/src/types/vault-timeout.type"; + import { CipherDecryptionKeys, - CryptoService as CryptoServiceAbstraction, + KeyService as KeyServiceAbstraction, UserPrivateKeyDecryptionFailedError, -} from "../abstractions/crypto.service"; -import { EncryptService } from "../abstractions/encrypt.service"; -import { KeyGenerationService } from "../abstractions/key-generation.service"; -import { LogService } from "../abstractions/log.service"; -import { PlatformUtilsService } from "../abstractions/platform-utils.service"; -import { StateService } from "../abstractions/state.service"; -import { KeySuffixOptions, HashPurpose } from "../enums"; -import { convertValues } from "../misc/convert-values"; -import { EFFLongWordList } from "../misc/wordlist"; -import { EncString, EncryptedString } from "../models/domain/enc-string"; -import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; -import { ActiveUserState, StateProvider } from "../state"; - -import { USER_ENCRYPTED_ORGANIZATION_KEYS } from "./key-state/org-keys.state"; -import { USER_ENCRYPTED_PROVIDER_KEYS } from "./key-state/provider-keys.state"; -import { - USER_ENCRYPTED_PRIVATE_KEY, - USER_EVER_HAD_USER_KEY, - USER_KEY, -} from "./key-state/user-key.state"; +} from "./abstractions/key.service"; -export class CryptoService implements CryptoServiceAbstraction { +export class DefaultKeyService implements KeyServiceAbstraction { private readonly activeUserEverHadUserKey: ActiveUserState; readonly everHadUserKey$: Observable; @@ -841,6 +841,10 @@ export class CryptoService implements CryptoServiceAbstraction { return this.userPrivateKeyHelper$(userId, false).pipe(map((keys) => keys?.userPrivateKey)); } + userEncryptedPrivateKey$(userId: UserId): Observable { + return this.stateProvider.getUser(userId, USER_ENCRYPTED_PRIVATE_KEY).state$; + } + userPrivateKeyWithLegacySupport$(userId: UserId): Observable { return this.userPrivateKeyHelper$(userId, true).pipe(map((keys) => keys?.userPrivateKey)); } @@ -929,6 +933,12 @@ export class CryptoService implements CryptoServiceAbstraction { return this.cipherDecryptionKeys$(userId, true).pipe(map((keys) => keys?.orgKeys)); } + encryptedOrgKeys$( + userId: UserId, + ): Observable> { + return this.stateProvider.getUser(userId, USER_ENCRYPTED_ORGANIZATION_KEYS).state$; + } + cipherDecryptionKeys$( userId: UserId, legacySupport: boolean = false, diff --git a/libs/tools/card/src/card.component.ts b/libs/tools/card/src/card.component.ts index 9305246c581..85db7eaa7b3 100644 --- a/libs/tools/card/src/card.component.ts +++ b/libs/tools/card/src/card.component.ts @@ -11,7 +11,7 @@ import { TypographyModule } from "@bitwarden/components"; imports: [CommonModule, TypographyModule, JslibModule], host: { class: - "tw-box-border tw-bg-background tw-block tw-text-main tw-border-solid tw-border tw-border-secondary-300 tw-border [&:not(bit-layout_*)]:tw-rounded-lg tw-p-6", + "tw-box-border tw-bg-background tw-block tw-text-main tw-border-solid tw-border tw-border-secondary-300 tw-border [&:not(bit-layout_*)]:tw-rounded-lg tw-rounded-lg tw-p-6", }, }) export class CardComponent { diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts index 1a66fe92256..d264991ae40 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts @@ -10,7 +10,6 @@ import { } from "@bitwarden/common/auth/models/domain/kdf-config"; import { CipherWithIdExport } from "@bitwarden/common/models/export/cipher-with-ids.export"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { KdfType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -25,6 +24,7 @@ import { Login } from "@bitwarden/common/vault/models/domain/login"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; +import { KeyService } from "@bitwarden/key-management"; import { BuildTestObject, GetUniqueString } from "../../../../../../common/spec"; @@ -152,7 +152,7 @@ describe("VaultExportService", () => { let cipherService: MockProxy; let pinService: MockProxy; let folderService: MockProxy; - let cryptoService: MockProxy; + let keyService: MockProxy; let encryptService: MockProxy; let kdfConfigService: MockProxy; let accountService: MockProxy; @@ -162,12 +162,12 @@ describe("VaultExportService", () => { cipherService = mock(); pinService = mock(); folderService = mock(); - cryptoService = mock(); + keyService = mock(); encryptService = mock(); kdfConfigService = mock(); accountService = mock(); - cryptoService.userKey$.mockReturnValue(new BehaviorSubject("mockOriginalUserKey" as any)); + keyService.userKey$.mockReturnValue(new BehaviorSubject("mockOriginalUserKey" as any)); const userId = "" as UserId; const accountInfo: AccountInfo = { @@ -187,7 +187,7 @@ describe("VaultExportService", () => { folderService, cipherService, pinService, - cryptoService, + keyService, encryptService, cryptoFunctionService, kdfConfigService, diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts index d6d37b28ac7..04dba1299d7 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts @@ -6,7 +6,6 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { CipherWithIdExport, FolderWithIdExport } from "@bitwarden/common/models/export"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -16,6 +15,7 @@ import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { Folder } from "@bitwarden/common/vault/models/domain/folder"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; +import { KeyService } from "@bitwarden/key-management"; import { BitwardenCsvIndividualExportType, @@ -35,7 +35,7 @@ export class IndividualVaultExportService private folderService: FolderService, private cipherService: CipherService, pinService: PinServiceAbstraction, - private cryptoService: CryptoService, + private keyService: KeyService, encryptService: EncryptService, cryptoFunctionService: CryptoFunctionService, kdfConfigService: KdfConfigService, @@ -104,7 +104,7 @@ export class IndividualVaultExportService const activeUserId = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); - const userKey = await this.cryptoService.getUserKeyWithLegacySupport(activeUserId); + const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId); const encKeyValidation = await this.encryptService.encrypt(Utils.newGuid(), userKey); const jsonDoc: BitwardenEncryptedIndividualJsonExport = { diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts index 28ffa8e83dc..4e23a0ed25c 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts @@ -14,7 +14,6 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { CipherWithIdExport, CollectionWithIdExport } from "@bitwarden/common/models/export"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { OrganizationId } from "@bitwarden/common/types/guid"; @@ -23,6 +22,7 @@ import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { KeyService } from "@bitwarden/key-management"; import { BitwardenCsvOrgExportType, @@ -42,7 +42,7 @@ export class OrganizationVaultExportService private cipherService: CipherService, private apiService: ApiService, pinService: PinServiceAbstraction, - private cryptoService: CryptoService, + private keyService: KeyService, encryptService: EncryptService, cryptoFunctionService: CryptoFunctionService, private collectionService: CollectionService, @@ -105,7 +105,7 @@ export class OrganizationVaultExportService exportData.collections.forEach((c) => { const collection = new Collection(new CollectionData(c as CollectionDetailsResponse)); exportPromises.push( - firstValueFrom(this.cryptoService.activeUserOrgKeys$) + firstValueFrom(this.keyService.activeUserOrgKeys$) .then((keys) => collection.decrypt(keys[organizationId as OrganizationId])) .then((decCol) => { decCollections.push(decCol); @@ -245,7 +245,7 @@ export class OrganizationVaultExportService collections: Collection[], ciphers: Cipher[], ): Promise { - const orgKey = await this.cryptoService.getOrgKey(organizationId); + const orgKey = await this.keyService.getOrgKey(organizationId); const encKeyValidation = await this.encryptService.encrypt(Utils.newGuid(), orgKey); const jsonDoc: BitwardenEncryptedOrgJsonExport = { diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts index 7e93c78fc51..525e769957f 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts @@ -10,7 +10,6 @@ import { } from "@bitwarden/common/auth/models/domain/kdf-config"; import { CipherWithIdExport } from "@bitwarden/common/models/export/cipher-with-ids.export"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { KdfType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -25,6 +24,7 @@ import { Login } from "@bitwarden/common/vault/models/domain/login"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; +import { KeyService } from "@bitwarden/key-management"; import { BuildTestObject, GetUniqueString } from "../../../../../../common/spec"; @@ -152,7 +152,7 @@ describe("VaultExportService", () => { let cipherService: MockProxy; let pinService: MockProxy; let folderService: MockProxy; - let cryptoService: MockProxy; + let keyService: MockProxy; let encryptService: MockProxy; let accountService: MockProxy; let kdfConfigService: MockProxy; @@ -162,7 +162,7 @@ describe("VaultExportService", () => { cipherService = mock(); pinService = mock(); folderService = mock(); - cryptoService = mock(); + keyService = mock(); encryptService = mock(); accountService = mock(); @@ -172,7 +172,7 @@ describe("VaultExportService", () => { folderService.getAllFromState.mockResolvedValue(UserFolders); kdfConfigService.getKdfConfig.mockResolvedValue(DEFAULT_KDF_CONFIG); encryptService.encrypt.mockResolvedValue(new EncString("encrypted")); - cryptoService.userKey$.mockReturnValue(new BehaviorSubject("mockOriginalUserKey" as any)); + keyService.userKey$.mockReturnValue(new BehaviorSubject("mockOriginalUserKey" as any)); const userId = "" as UserId; const accountInfo: AccountInfo = { email: "", @@ -186,7 +186,7 @@ describe("VaultExportService", () => { folderService, cipherService, pinService, - cryptoService, + keyService, encryptService, cryptoFunctionService, kdfConfigService, diff --git a/libs/tools/generator/components/src/credential-generator-history.component.html b/libs/tools/generator/components/src/credential-generator-history.component.html index 2b8802b9327..c42d6a12729 100644 --- a/libs/tools/generator/components/src/credential-generator-history.component.html +++ b/libs/tools/generator/components/src/credential-generator-history.component.html @@ -1,7 +1,10 @@
-

{{ credential.credential }}

+ {{ credential.generationDate | date: "medium" }} diff --git a/libs/tools/generator/components/src/credential-generator-history.component.ts b/libs/tools/generator/components/src/credential-generator-history.component.ts index 2f76027a941..bcedd91babf 100644 --- a/libs/tools/generator/components/src/credential-generator-history.component.ts +++ b/libs/tools/generator/components/src/credential-generator-history.component.ts @@ -9,6 +9,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { UserId } from "@bitwarden/common/types/guid"; import { CardComponent, + ColorPasswordModule, IconButtonModule, NoItemsModule, SectionComponent, @@ -21,6 +22,7 @@ import { GeneratedCredential, GeneratorHistoryService } from "@bitwarden/generat selector: "bit-credential-generator-history", templateUrl: "credential-generator-history.component.html", imports: [ + ColorPasswordModule, CommonModule, IconButtonModule, NoItemsModule, diff --git a/libs/tools/generator/components/src/credential-generator.component.html b/libs/tools/generator/components/src/credential-generator.component.html index b174349ecef..f580b75f1ba 100644 --- a/libs/tools/generator/components/src/credential-generator.component.html +++ b/libs/tools/generator/components/src/credential-generator.component.html @@ -3,7 +3,7 @@ fullWidth class="tw-mb-4" [selected]="(root$ | async).nav" - (selectedChange)="onRootChanged($event)" + (selectedChange)="onRootChanged({ nav: $event })" attr.aria-label="{{ 'type' | i18n }}" > @@ -16,61 +16,88 @@
- + [valueLabel]="credentialTypeLabel$ | async" + >
-
{{ "options" | i18n }}
+

{{ "options" | i18n }}

-
+ {{ "type" | i18n }} - + + {{ credentialTypeHint$ | async }}
+
+ + {{ "service" | i18n }} + + + +
+
diff --git a/libs/tools/generator/components/src/credential-generator.component.ts b/libs/tools/generator/components/src/credential-generator.component.ts index 359c7505c54..579d196a7a6 100644 --- a/libs/tools/generator/components/src/credential-generator.component.ts +++ b/libs/tools/generator/components/src/credential-generator.component.ts @@ -2,11 +2,12 @@ import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } fro import { FormBuilder } from "@angular/forms"; import { BehaviorSubject, - concat, + catchError, + combineLatest, + combineLatestWith, distinctUntilChanged, filter, map, - of, ReplaySubject, Subject, switchMap, @@ -16,25 +17,33 @@ import { import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { IntegrationId } from "@bitwarden/common/tools/integration"; import { UserId } from "@bitwarden/common/types/guid"; +import { ToastService } from "@bitwarden/components"; import { Option } from "@bitwarden/components/src/select/option"; import { + AlgorithmInfo, CredentialAlgorithm, CredentialCategory, - CredentialGeneratorInfo, CredentialGeneratorService, GeneratedCredential, Generators, + getForwarderConfiguration, isEmailAlgorithm, + isForwarderIntegration, isPasswordAlgorithm, + isSameAlgorithm, isUsernameAlgorithm, - PasswordAlgorithm, + toCredentialGeneratorConfiguration, } from "@bitwarden/generator-core"; +import { GeneratorHistoryService } from "@bitwarden/generator-history"; -/** root category that drills into username and email categories */ +// constants used to identify navigation selections that are not +// generator algorithms const IDENTIFIER = "identifier"; -/** options available for the top-level navigation */ -type RootNavValue = PasswordAlgorithm | typeof IDENTIFIER; +const FORWARDER = "forwarder"; +const NONE_SELECTED = "none"; @Component({ selector: "tools-credential-generator", @@ -43,6 +52,9 @@ type RootNavValue = PasswordAlgorithm | typeof IDENTIFIER; export class CredentialGeneratorComponent implements OnInit, OnDestroy { constructor( private generatorService: CredentialGeneratorService, + private generatorHistoryService: GeneratorHistoryService, + private toastService: ToastService, + private logService: LogService, private i18nService: I18nService, private accountService: AccountService, private zone: NgZone, @@ -59,21 +71,25 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { @Output() readonly onGenerated = new EventEmitter(); - protected root$ = new BehaviorSubject<{ nav: RootNavValue }>({ + protected root$ = new BehaviorSubject<{ nav: string }>({ nav: null, }); - protected onRootChanged(nav: RootNavValue) { + protected onRootChanged(value: { nav: string }) { // prevent subscription cycle - if (this.root$.value.nav !== nav) { + if (this.root$.value.nav !== value.nav) { this.zone.run(() => { - this.root$.next({ nav }); + this.root$.next(value); }); } } protected username = this.formBuilder.group({ - nav: [null as CredentialAlgorithm], + nav: [null as string], + }); + + protected forwarder = this.formBuilder.group({ + nav: [null as string], }); async ngOnInit() { @@ -92,16 +108,29 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { this.generatorService .algorithms$(["email", "username"], { userId$: this.userId$ }) .pipe( - map((algorithms) => this.toOptions(algorithms)), + map((algorithms) => { + const usernames = algorithms.filter((a) => !isForwarderIntegration(a.id)); + const usernameOptions = this.toOptions(usernames); + usernameOptions.push({ value: FORWARDER, label: this.i18nService.t("forwardedEmail") }); + + const forwarders = algorithms.filter((a) => isForwarderIntegration(a.id)); + const forwarderOptions = this.toOptions(forwarders); + forwarderOptions.unshift({ value: NONE_SELECTED, label: this.i18nService.t("select") }); + + return [usernameOptions, forwarderOptions] as const; + }), takeUntil(this.destroyed), ) - .subscribe(this.usernameOptions$); + .subscribe(([usernames, forwarders]) => { + this.usernameOptions$.next(usernames); + this.forwarderOptions$.next(forwarders); + }); this.generatorService .algorithms$("password", { userId$: this.userId$ }) .pipe( map((algorithms) => { - const options = this.toOptions(algorithms) as Option[]; + const options = this.toOptions(algorithms); options.push({ value: IDENTIFIER, label: this.i18nService.t("username") }); return options; }), @@ -111,7 +140,7 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { this.algorithm$ .pipe( - map((a) => a?.descriptionKey && this.i18nService.t(a?.descriptionKey)), + map((a) => a?.description), takeUntil(this.destroyed), ) .subscribe((hint) => { @@ -124,7 +153,7 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { this.algorithm$ .pipe( - map((a) => a.category), + map((a) => a?.category), distinctUntilChanged(), takeUntil(this.destroyed), ) @@ -139,10 +168,32 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { // wire up the generator this.algorithm$ .pipe( + filter((algorithm) => !!algorithm), switchMap((algorithm) => this.typeToGenerator$(algorithm.id)), + catchError((error: unknown, generator) => { + if (typeof error === "string") { + this.toastService.showToast({ + message: error, + variant: "error", + title: "", + }); + } else { + this.logService.error(error); + } + + // continue with origin stream + return generator; + }), + withLatestFrom(this.userId$), takeUntil(this.destroyed), ) - .subscribe((generated) => { + .subscribe(([generated, userId]) => { + this.generatorHistoryService + .track(userId, generated.credential, generated.category, generated.generationDate) + .catch((e: unknown) => { + this.logService.error(e); + }); + // update subjects within the angular zone so that the // template bindings refresh immediately this.zone.run(() => { @@ -151,35 +202,116 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { }); }); - // assume the last-visible generator algorithm is the user's preferred one - const preferences = await this.generatorService.preferences({ singleUserId$: this.userId$ }); + // normalize cascade selections; introduce subjects to allow changes + // from user selections and changes from preference updates to + // update the template + type CascadeValue = { nav: string; algorithm?: CredentialAlgorithm }; + const activeRoot$ = new Subject(); + const activeIdentifier$ = new Subject(); + const activeForwarder$ = new Subject(); + this.root$ .pipe( - filter(({ nav }) => !!nav), - switchMap((root) => { - if (root.nav === IDENTIFIER) { - return concat(of(this.username.value), this.username.valueChanges); + map( + (root): CascadeValue => + root.nav === IDENTIFIER + ? { nav: root.nav } + : { nav: root.nav, algorithm: JSON.parse(root.nav) }, + ), + takeUntil(this.destroyed), + ) + .subscribe(activeRoot$); + + this.username.valueChanges + .pipe( + map( + (username): CascadeValue => + username.nav === FORWARDER + ? { nav: username.nav } + : { nav: username.nav, algorithm: JSON.parse(username.nav) }, + ), + takeUntil(this.destroyed), + ) + .subscribe(activeIdentifier$); + + this.forwarder.valueChanges + .pipe( + map( + (forwarder): CascadeValue => + forwarder.nav === NONE_SELECTED + ? { nav: forwarder.nav } + : { nav: forwarder.nav, algorithm: JSON.parse(forwarder.nav) }, + ), + takeUntil(this.destroyed), + ) + .subscribe(activeForwarder$); + + // update forwarder cascade visibility + combineLatest([activeRoot$, activeIdentifier$, activeForwarder$]) + .pipe( + map(([root, username, forwarder]) => { + const showForwarder = !root.algorithm && !username.algorithm; + const forwarderId = + showForwarder && isForwarderIntegration(forwarder.algorithm) + ? forwarder.algorithm.forwarder + : null; + return [showForwarder, forwarderId] as const; + }), + distinctUntilChanged((prev, next) => prev[0] === next[0] && prev[1] === next[1]), + takeUntil(this.destroyed), + ) + .subscribe(([showForwarder, forwarderId]) => { + // update subjects within the angular zone so that the + // template bindings refresh immediately + this.zone.run(() => { + this.showForwarder$.next(showForwarder); + this.forwarderId$.next(forwarderId); + }); + }); + + // update active algorithm + combineLatest([activeRoot$, activeIdentifier$, activeForwarder$]) + .pipe( + map(([root, username, forwarder]) => { + const selection = root.algorithm ?? username.algorithm ?? forwarder.algorithm; + if (selection) { + return this.generatorService.algorithm(selection); } else { - return of(root as { nav: PasswordAlgorithm }); + return null; } }), - filter(({ nav }) => !!nav), + distinctUntilChanged((prev, next) => isSameAlgorithm(prev?.id, next?.id)), + takeUntil(this.destroyed), + ) + .subscribe((algorithm) => { + // update subjects within the angular zone so that the + // template bindings refresh immediately + this.zone.run(() => { + this.algorithm$.next(algorithm); + }); + }); + + // assume the last-selected generator algorithm is the user's preferred one + const preferences = await this.generatorService.preferences({ singleUserId$: this.userId$ }); + this.algorithm$ + .pipe( + filter((algorithm) => !!algorithm), withLatestFrom(preferences), takeUntil(this.destroyed), ) - .subscribe(([{ nav: algorithm }, preference]) => { + .subscribe(([algorithm, preference]) => { function setPreference(category: CredentialCategory) { const p = preference[category]; - p.algorithm = algorithm; + p.algorithm = algorithm.id; p.updated = new Date(); } // `is*Algorithm` decides `algorithm`'s type, which flows into `setPreference` - if (isEmailAlgorithm(algorithm)) { + if (isEmailAlgorithm(algorithm.id)) { setPreference("email"); - } else if (isUsernameAlgorithm(algorithm)) { + } else if (isUsernameAlgorithm(algorithm.id)) { setPreference("username"); - } else if (isPasswordAlgorithm(algorithm)) { + } else if (isPasswordAlgorithm(algorithm.id)) { setPreference("password"); } else { return; @@ -189,34 +321,74 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { }); // populate the form with the user's preferences to kick off interactivity - preferences.pipe(takeUntil(this.destroyed)).subscribe(({ email, username, password }) => { - // the last preference set by the user "wins" - const userNav = email.updated > username.updated ? email : username; - const rootNav: any = userNav.updated > password.updated ? IDENTIFIER : password.algorithm; - const credentialType = rootNav === IDENTIFIER ? userNav.algorithm : password.algorithm; - - // update navigation; break subscription loop - this.onRootChanged(rootNav); - this.username.setValue({ nav: userNav.algorithm }, { emitEvent: false }); - - // load algorithm metadata - const algorithm = this.generatorService.algorithm(credentialType); + preferences + .pipe( + map(({ email, username, password }) => { + const forwarderPref = isForwarderIntegration(email.algorithm) ? email : null; + const usernamePref = email.updated > username.updated ? email : username; + + // inject drilldown flags + const forwarderNav = !forwarderPref + ? NONE_SELECTED + : JSON.stringify(forwarderPref.algorithm); + const userNav = forwarderPref ? FORWARDER : JSON.stringify(usernamePref.algorithm); + const rootNav = + usernamePref.updated > password.updated + ? IDENTIFIER + : JSON.stringify(password.algorithm); + + // construct cascade metadata + const cascade = { + root: { + selection: { nav: rootNav }, + active: { + nav: rootNav, + algorithm: rootNav === IDENTIFIER ? null : password.algorithm, + } as CascadeValue, + }, + username: { + selection: { nav: userNav }, + active: { + nav: userNav, + algorithm: forwarderPref ? null : usernamePref.algorithm, + }, + }, + forwarder: { + selection: { nav: forwarderNav }, + active: { + nav: forwarderNav, + algorithm: forwarderPref?.algorithm, + }, + }, + }; + + return cascade; + }), + takeUntil(this.destroyed), + ) + .subscribe(({ root, username, forwarder }) => { + // update navigation; break subscription loop + this.onRootChanged(root.selection); + this.username.setValue(username.selection, { emitEvent: false }); + this.forwarder.setValue(forwarder.selection, { emitEvent: false }); + + // update cascade visibility + activeRoot$.next(root.active); + activeIdentifier$.next(username.active); + activeForwarder$.next(forwarder.active); + }); - // update subjects within the angular zone so that the - // template bindings refresh immediately + // automatically regenerate when the algorithm switches if the algorithm + // allows it; otherwise set a placeholder + this.algorithm$.pipe(takeUntil(this.destroyed)).subscribe((a) => { this.zone.run(() => { - this.algorithm$.next(algorithm); + if (!a || a.onlyOnRequest) { + this.value$.next("-"); + } else { + this.generate("autogenerate"); + } }); }); - - // generate on load unless the generator prohibits it - this.algorithm$ - .pipe( - distinctUntilChanged((prev, next) => prev.id === next.id), - filter((a) => !a.onlyOnRequest), - takeUntil(this.destroyed), - ) - .subscribe(() => this.generate$.next()); } private typeToGenerator$(type: CredentialAlgorithm) { @@ -240,20 +412,69 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { case "passphrase": return this.generatorService.generate$(Generators.passphrase, dependencies); + } - default: - throw new Error(`Invalid generator type: "${type}"`); + if (isForwarderIntegration(type)) { + const forwarder = getForwarderConfiguration(type.forwarder); + const configuration = toCredentialGeneratorConfiguration(forwarder); + const generator = this.generatorService.generate$(configuration, dependencies); + return generator; } + + throw new Error(`Invalid generator type: "${type}"`); } + /** Lists the top-level credential types supported by the component. + * @remarks This is string-typed because angular doesn't support + * structural equality for objects, which prevents `CredentialAlgorithm` + * from being selectable within a dropdown when its value contains a + * `ForwarderIntegration`. + */ + protected rootOptions$ = new BehaviorSubject[]>([]); + + /** Lists the credential types of the username algorithm box. */ + protected usernameOptions$ = new BehaviorSubject[]>([]); + /** Lists the credential types of the username algorithm box. */ - protected usernameOptions$ = new BehaviorSubject[]>([]); + protected forwarderOptions$ = new BehaviorSubject[]>([]); - /** Lists the top-level credential types supported by the component. */ - protected rootOptions$ = new BehaviorSubject[]>([]); + /** Tracks the currently selected forwarder. */ + protected forwarderId$ = new BehaviorSubject(null); + + /** Tracks forwarder control visibility */ + protected showForwarder$ = new BehaviorSubject(false); /** tracks the currently selected credential type */ - protected algorithm$ = new ReplaySubject(1); + protected algorithm$ = new ReplaySubject(1); + + protected showAlgorithm$ = this.algorithm$.pipe( + combineLatestWith(this.showForwarder$), + map(([algorithm, showForwarder]) => (showForwarder ? null : algorithm)), + ); + + /** + * Emits the copy button aria-label respective of the selected credential type + */ + protected credentialTypeCopyLabel$ = this.algorithm$.pipe( + filter((algorithm) => !!algorithm), + map(({ copy }) => copy), + ); + + /** + * Emits the generate button aria-label respective of the selected credential type + */ + protected credentialTypeGenerateLabel$ = this.algorithm$.pipe( + filter((algorithm) => !!algorithm), + map(({ generate }) => generate), + ); + + /** + * Emits the copy credential toast respective of the selected credential type + */ + protected credentialTypeLabel$ = this.algorithm$.pipe( + filter((algorithm) => !!algorithm), + map(({ generatedValue }) => generatedValue), + ); /** Emits hint key for the currently selected credential type */ protected credentialTypeHint$ = new ReplaySubject(1); @@ -268,12 +489,20 @@ export class CredentialGeneratorComponent implements OnInit, OnDestroy { protected readonly userId$ = new BehaviorSubject(null); /** Emits when a new credential is requested */ - protected readonly generate$ = new Subject(); + private readonly generate$ = new Subject(); + + /** Request a new value from the generator + * @param requestor a label used to trace generation request + * origin in the debugger. + */ + protected generate(requestor: string) { + this.generate$.next(requestor); + } - private toOptions(algorithms: CredentialGeneratorInfo[]) { - const options: Option[] = algorithms.map((algorithm) => ({ - value: algorithm.id, - label: this.i18nService.t(algorithm.nameKey), + private toOptions(algorithms: AlgorithmInfo[]) { + const options: Option[] = algorithms.map((algorithm) => ({ + value: JSON.stringify(algorithm.id), + label: algorithm.name, })); return options; diff --git a/libs/tools/generator/components/src/empty-credential-history.component.html b/libs/tools/generator/components/src/empty-credential-history.component.html index ee28dc58958..f7f35fe9362 100644 --- a/libs/tools/generator/components/src/empty-credential-history.component.html +++ b/libs/tools/generator/components/src/empty-credential-history.component.html @@ -1,7 +1,7 @@
-

{{ "noPasswordsToShow" | i18n }}

-
{{ "noRecentlyGeneratedPassword" | i18n }}
+

{{ "nothingToShow" | i18n }}

+
{{ "nothingGeneratedRecently" | i18n }}
diff --git a/libs/tools/generator/components/src/forwarder-settings.component.html b/libs/tools/generator/components/src/forwarder-settings.component.html new file mode 100644 index 00000000000..64566fa9562 --- /dev/null +++ b/libs/tools/generator/components/src/forwarder-settings.component.html @@ -0,0 +1,16 @@ +
+ + {{ "forwarderDomainName" | i18n }} + + {{ "forwarderDomainNameHint" | i18n }} + + + {{ "apiKey" | i18n }} + + + + + {{ "selfHostBaseUrl" | i18n }} + + +
diff --git a/libs/tools/generator/components/src/forwarder-settings.component.ts b/libs/tools/generator/components/src/forwarder-settings.component.ts new file mode 100644 index 00000000000..67e93c611ee --- /dev/null +++ b/libs/tools/generator/components/src/forwarder-settings.component.ts @@ -0,0 +1,197 @@ +import { + Component, + EventEmitter, + Input, + OnChanges, + OnDestroy, + OnInit, + Output, + SimpleChanges, +} from "@angular/core"; +import { FormBuilder } from "@angular/forms"; +import { + BehaviorSubject, + concatMap, + map, + ReplaySubject, + skip, + Subject, + switchAll, + switchMap, + takeUntil, + withLatestFrom, +} from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { IntegrationId } from "@bitwarden/common/tools/integration"; +import { UserId } from "@bitwarden/common/types/guid"; +import { + CredentialGeneratorConfiguration, + CredentialGeneratorService, + getForwarderConfiguration, + NoPolicy, + toCredentialGeneratorConfiguration, +} from "@bitwarden/generator-core"; + +import { completeOnAccountSwitch, toValidators } from "./util"; + +const Controls = Object.freeze({ + domain: "domain", + token: "token", + baseUrl: "baseUrl", +}); + +/** Options group for forwarder integrations */ +@Component({ + selector: "tools-forwarder-settings", + templateUrl: "forwarder-settings.component.html", +}) +export class ForwarderSettingsComponent implements OnInit, OnChanges, OnDestroy { + /** Instantiates the component + * @param accountService queries user availability + * @param generatorService settings and policy logic + * @param formBuilder reactive form controls + */ + constructor( + private formBuilder: FormBuilder, + private generatorService: CredentialGeneratorService, + private accountService: AccountService, + ) {} + + /** Binds the component to a specific user's settings. + * When this input is not provided, the form binds to the active + * user + */ + @Input() + userId: UserId | null; + + @Input({ required: true }) + forwarder: IntegrationId; + + /** Emits settings updates and completes if the settings become unavailable. + * @remarks this does not emit the initial settings. If you would like + * to receive live settings updates including the initial update, + * use `CredentialGeneratorService.settings$(...)` instead. + */ + @Output() + readonly onUpdated = new EventEmitter(); + + /** The template's control bindings */ + protected settings = this.formBuilder.group({ + [Controls.domain]: [""], + [Controls.token]: [""], + [Controls.baseUrl]: [""], + }); + + private forwarderId$ = new ReplaySubject(1); + + async ngOnInit() { + const singleUserId$ = this.singleUserId$(); + + const forwarder$ = new ReplaySubject>(1); + this.forwarderId$ + .pipe( + map((id) => getForwarderConfiguration(id)), + // type erasure necessary because the configuration properties are + // determined dynamically at runtime + // FIXME: this can be eliminated by unifying the forwarder settings types; + // see `ForwarderConfiguration<...>` for details. + map((forwarder) => toCredentialGeneratorConfiguration(forwarder)), + takeUntil(this.destroyed$), + ) + .subscribe((forwarder) => { + this.displayDomain = forwarder.request.includes("domain"); + this.displayToken = forwarder.request.includes("token"); + this.displayBaseUrl = forwarder.request.includes("baseUrl"); + + forwarder$.next(forwarder); + }); + + const settings$$ = forwarder$.pipe( + concatMap((forwarder) => this.generatorService.settings(forwarder, { singleUserId$ })), + ); + + // bind settings to the reactive form + settings$$.pipe(switchAll(), takeUntil(this.destroyed$)).subscribe((settings) => { + // skips reactive event emissions to break a subscription cycle + this.settings.patchValue(settings as any, { emitEvent: false }); + }); + + // bind policy to the reactive form + forwarder$ + .pipe( + switchMap((forwarder) => { + const constraints$ = this.generatorService + .policy$(forwarder, { userId$: singleUserId$ }) + .pipe(map(({ constraints }) => [constraints, forwarder] as const)); + + return constraints$; + }), + takeUntil(this.destroyed$), + ) + .subscribe(([constraints, forwarder]) => { + for (const name in Controls) { + const control = this.settings.get(name); + if (forwarder.request.includes(name as any)) { + control.enable({ emitEvent: false }); + control.setValidators( + // the configuration's type erasure affects `toValidators` as well + toValidators(name, forwarder, constraints), + ); + } else { + control.disable({ emitEvent: false }); + control.clearValidators(); + } + } + + this.settings.updateValueAndValidity({ emitEvent: false }); + }); + + // the first emission is the current value; subsequent emissions are updates + settings$$ + .pipe( + map((settings$) => settings$.pipe(skip(1))), + switchAll(), + takeUntil(this.destroyed$), + ) + .subscribe(this.onUpdated); + + // now that outputs are set up, connect inputs + this.settings.valueChanges + .pipe(withLatestFrom(settings$$), takeUntil(this.destroyed$)) + .subscribe(([value, settings]) => { + settings.next(value); + }); + } + + ngOnChanges(changes: SimpleChanges): void { + this.refresh$.complete(); + if ("forwarder" in changes) { + this.forwarderId$.next(this.forwarder); + } + } + + protected displayDomain: boolean; + protected displayToken: boolean; + protected displayBaseUrl: boolean; + + private singleUserId$() { + // FIXME: this branch should probably scan for the user and make sure + // the account is unlocked + if (this.userId) { + return new BehaviorSubject(this.userId as UserId).asObservable(); + } + + return this.accountService.activeAccount$.pipe( + completeOnAccountSwitch(), + takeUntil(this.destroyed$), + ); + } + + private readonly refresh$ = new Subject(); + + private readonly destroyed$ = new Subject(); + ngOnDestroy(): void { + this.destroyed$.complete(); + } +} diff --git a/libs/tools/generator/components/src/generator.module.ts b/libs/tools/generator/components/src/generator.module.ts index 96622774a3f..2d1cedca400 100644 --- a/libs/tools/generator/components/src/generator.module.ts +++ b/libs/tools/generator/components/src/generator.module.ts @@ -5,8 +5,10 @@ import { ReactiveFormsModule } from "@angular/forms"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { SafeInjectionToken } from "@bitwarden/angular/services/injection-tokens"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { StateProvider } from "@bitwarden/common/platform/state"; import { CardComponent, @@ -27,9 +29,11 @@ import { CredentialGeneratorService, Randomizer, } from "@bitwarden/generator-core"; +import { KeyService } from "@bitwarden/key-management"; import { CatchallSettingsComponent } from "./catchall-settings.component"; import { CredentialGeneratorComponent } from "./credential-generator.component"; +import { ForwarderSettingsComponent } from "./forwarder-settings.component"; import { PassphraseSettingsComponent } from "./passphrase-settings.component"; import { PasswordGeneratorComponent } from "./password-generator.component"; import { PasswordSettingsComponent } from "./password-settings.component"; @@ -62,23 +66,32 @@ const RANDOMIZER = new SafeInjectionToken("Randomizer"); safeProvider({ provide: RANDOMIZER, useFactory: createRandomizer, - deps: [CryptoService], + deps: [KeyService], }), safeProvider({ provide: CredentialGeneratorService, useClass: CredentialGeneratorService, - deps: [RANDOMIZER, StateProvider, PolicyService], + deps: [ + RANDOMIZER, + StateProvider, + PolicyService, + ApiService, + I18nService, + EncryptService, + KeyService, + ], }), ], declarations: [ CatchallSettingsComponent, CredentialGeneratorComponent, + ForwarderSettingsComponent, SubaddressSettingsComponent, - UsernameSettingsComponent, PasswordGeneratorComponent, - PasswordSettingsComponent, PassphraseSettingsComponent, + PasswordSettingsComponent, UsernameGeneratorComponent, + UsernameSettingsComponent, ], exports: [CredentialGeneratorComponent, PasswordGeneratorComponent, UsernameGeneratorComponent], }) diff --git a/libs/tools/generator/components/src/passphrase-settings.component.html b/libs/tools/generator/components/src/passphrase-settings.component.html index c40df97c69c..d089de7a07b 100644 --- a/libs/tools/generator/components/src/passphrase-settings.component.html +++ b/libs/tools/generator/components/src/passphrase-settings.component.html @@ -1,4 +1,4 @@ - +
{{ "options" | i18n }}
@@ -7,14 +7,8 @@
{{ "options" | i18n }}
{{ "numWords" | i18n }} - + + {{ numWordsBoundariesHint$ | async }}
diff --git a/libs/tools/generator/components/src/passphrase-settings.component.ts b/libs/tools/generator/components/src/passphrase-settings.component.ts index 25e028210cc..d65e897f4e1 100644 --- a/libs/tools/generator/components/src/passphrase-settings.component.ts +++ b/libs/tools/generator/components/src/passphrase-settings.component.ts @@ -1,8 +1,10 @@ +import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { OnInit, Input, Output, EventEmitter, Component, OnDestroy } from "@angular/core"; import { FormBuilder } from "@angular/forms"; -import { BehaviorSubject, skip, takeUntil, Subject } from "rxjs"; +import { BehaviorSubject, skip, takeUntil, Subject, ReplaySubject } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { UserId } from "@bitwarden/common/types/guid"; import { Generators, @@ -28,11 +30,13 @@ export class PassphraseSettingsComponent implements OnInit, OnDestroy { /** Instantiates the component * @param accountService queries user availability * @param generatorService settings and policy logic + * @param i18nService localize hints * @param formBuilder reactive form controls */ constructor( private formBuilder: FormBuilder, private generatorService: CredentialGeneratorService, + private i18nService: I18nService, private accountService: AccountService, ) {} @@ -47,6 +51,9 @@ export class PassphraseSettingsComponent implements OnInit, OnDestroy { @Input() showHeader: boolean = true; + /** Removes bottom margin from `bit-section` */ + @Input({ transform: coerceBooleanProperty }) disableMargin = false; + /** Emits settings updates and completes if the settings become unavailable. * @remarks this does not emit the initial settings. If you would like * to receive live settings updates including the initial update, @@ -87,28 +94,33 @@ export class PassphraseSettingsComponent implements OnInit, OnDestroy { .get(Controls.wordSeparator) .setValidators(toValidators(Controls.wordSeparator, Generators.passphrase, constraints)); - // forward word boundaries to the template (can't do it through the rx form) - this.minNumWords = constraints.numWords.min; - this.maxNumWords = constraints.numWords.max; + this.settings.updateValueAndValidity({ emitEvent: false }); + this.policyInEffect = constraints.policyInEffect; this.toggleEnabled(Controls.capitalize, !constraints.capitalize?.readonly); this.toggleEnabled(Controls.includeNumber, !constraints.includeNumber?.readonly); + + const boundariesHint = this.i18nService.t( + "generatorBoundariesHint", + constraints.numWords.min, + constraints.numWords.max, + ); + this.numWordsBoundariesHint.next(boundariesHint); }); // now that outputs are set up, connect inputs this.settings.valueChanges.pipe(takeUntil(this.destroyed$)).subscribe(settings); } - /** attribute binding for numWords[min] */ - protected minNumWords: number; - - /** attribute binding for numWords[max] */ - protected maxNumWords: number; - /** display binding for enterprise policy notice */ protected policyInEffect: boolean; + private numWordsBoundariesHint = new ReplaySubject(1); + + /** display binding for min/max constraints of `numWords` */ + protected numWordsBoundariesHint$ = this.numWordsBoundariesHint.asObservable(); + private toggleEnabled(setting: keyof typeof Controls, enabled: boolean) { if (enabled) { this.settings.get(setting).enable({ emitEvent: false }); diff --git a/libs/tools/generator/components/src/password-generator.component.html b/libs/tools/generator/components/src/password-generator.component.html index 9a33aa143ec..6726df30855 100644 --- a/libs/tools/generator/components/src/password-generator.component.html +++ b/libs/tools/generator/components/src/password-generator.component.html @@ -14,29 +14,37 @@
- + [valueLabel]="credentialTypeLabel$ | async" + >
diff --git a/libs/tools/generator/components/src/password-generator.component.ts b/libs/tools/generator/components/src/password-generator.component.ts index bf33c7cfca9..8566edf4664 100644 --- a/libs/tools/generator/components/src/password-generator.component.ts +++ b/libs/tools/generator/components/src/password-generator.component.ts @@ -1,6 +1,8 @@ +import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core"; import { BehaviorSubject, + catchError, distinctUntilChanged, filter, map, @@ -13,17 +15,20 @@ import { import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { UserId } from "@bitwarden/common/types/guid"; +import { ToastService } from "@bitwarden/components"; import { Option } from "@bitwarden/components/src/select/option"; import { CredentialGeneratorService, Generators, PasswordAlgorithm, GeneratedCredential, - CredentialGeneratorInfo, CredentialAlgorithm, isPasswordAlgorithm, + AlgorithmInfo, } from "@bitwarden/generator-core"; +import { GeneratorHistoryService } from "@bitwarden/generator-history"; /** Options group for passwords */ @Component({ @@ -33,6 +38,9 @@ import { export class PasswordGeneratorComponent implements OnInit, OnDestroy { constructor( private generatorService: CredentialGeneratorService, + private generatorHistoryService: GeneratorHistoryService, + private toastService: ToastService, + private logService: LogService, private i18nService: I18nService, private accountService: AccountService, private zone: NgZone, @@ -45,6 +53,9 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { @Input() userId: UserId | null; + /** Removes bottom margin, passed to downstream components */ + @Input({ transform: coerceBooleanProperty }) disableMargin = false; + /** tracks the currently selected credential type */ protected credentialType$ = new BehaviorSubject(null); @@ -55,7 +66,15 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { protected readonly userId$ = new BehaviorSubject(null); /** Emits when a new credential is requested */ - protected readonly generate$ = new Subject(); + private readonly generate$ = new Subject(); + + /** Request a new value from the generator + * @param requestor a label used to trace generation request + * origin in the debugger. + */ + protected generate(requestor: string) { + this.generate$.next(requestor); + } /** Tracks changes to the selected credential type * @param type the new credential type @@ -97,10 +116,32 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { // wire up the generator this.algorithm$ .pipe( + filter((algorithm) => !!algorithm), switchMap((algorithm) => this.typeToGenerator$(algorithm.id)), + catchError((error: unknown, generator) => { + if (typeof error === "string") { + this.toastService.showToast({ + message: error, + variant: "error", + title: "", + }); + } else { + this.logService.error(error); + } + + // continue with origin stream + return generator; + }), + withLatestFrom(this.userId$), takeUntil(this.destroyed), ) - .subscribe((generated) => { + .subscribe(([generated, userId]) => { + this.generatorHistoryService + .track(userId, generated.credential, generated.category, generated.generationDate) + .catch((e: unknown) => { + this.logService.error(e); + }); + // update subjects within the angular zone so that the // template bindings refresh immediately this.zone.run(() => { @@ -150,7 +191,7 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { filter((a) => !a.onlyOnRequest), takeUntil(this.destroyed), ) - .subscribe(() => this.generate$.next()); + .subscribe(() => this.generate("autogenerate")); } private typeToGenerator$(type: CredentialAlgorithm) { @@ -174,12 +215,36 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { protected passwordOptions$ = new BehaviorSubject[]>([]); /** tracks the currently selected credential type */ - protected algorithm$ = new ReplaySubject(1); + protected algorithm$ = new ReplaySubject(1); + + /** + * Emits the copy button aria-label respective of the selected credential type + */ + protected credentialTypeCopyLabel$ = this.algorithm$.pipe( + filter((algorithm) => !!algorithm), + map(({ copy }) => copy), + ); + + /** + * Emits the generate button aria-label respective of the selected credential type + */ + protected credentialTypeGenerateLabel$ = this.algorithm$.pipe( + filter((algorithm) => !!algorithm), + map(({ generate }) => generate), + ); + + /** + * Emits the copy credential toast respective of the selected credential type + */ + protected credentialTypeLabel$ = this.algorithm$.pipe( + filter((algorithm) => !!algorithm), + map(({ generatedValue }) => generatedValue), + ); - private toOptions(algorithms: CredentialGeneratorInfo[]) { + private toOptions(algorithms: AlgorithmInfo[]) { const options: Option[] = algorithms.map((algorithm) => ({ value: algorithm.id, - label: this.i18nService.t(algorithm.nameKey), + label: this.i18nService.t(algorithm.name), })); return options; diff --git a/libs/tools/generator/components/src/password-settings.component.html b/libs/tools/generator/components/src/password-settings.component.html index fcafc789049..aa12a3247c3 100644 --- a/libs/tools/generator/components/src/password-settings.component.html +++ b/libs/tools/generator/components/src/password-settings.component.html @@ -1,4 +1,4 @@ - +

{{ "options" | i18n }}

@@ -7,13 +7,8 @@

{{ "options" | i18n }}

{{ "length" | i18n }} - + + {{ lengthBoundariesHint$ | async }} @@ -57,23 +52,11 @@

{{ "options" | i18n }}

{{ "minNumbers" | i18n }} - + {{ "minSpecial" | i18n }} - +
diff --git a/libs/tools/generator/components/src/password-settings.component.ts b/libs/tools/generator/components/src/password-settings.component.ts index 9466c81a0f4..6e9d106b71a 100644 --- a/libs/tools/generator/components/src/password-settings.component.ts +++ b/libs/tools/generator/components/src/password-settings.component.ts @@ -1,8 +1,10 @@ +import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { OnInit, Input, Output, EventEmitter, Component, OnDestroy } from "@angular/core"; import { FormBuilder } from "@angular/forms"; -import { BehaviorSubject, takeUntil, Subject, map, filter, tap, debounceTime, skip } from "rxjs"; +import { BehaviorSubject, takeUntil, Subject, map, filter, tap, skip, ReplaySubject } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { UserId } from "@bitwarden/common/types/guid"; import { Generators, @@ -32,11 +34,13 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy { /** Instantiates the component * @param accountService queries user availability * @param generatorService settings and policy logic + * @param i18nService localize hints * @param formBuilder reactive form controls */ constructor( private formBuilder: FormBuilder, private generatorService: CredentialGeneratorService, + private i18nService: I18nService, private accountService: AccountService, ) {} @@ -55,6 +59,9 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy { @Input() waitMs: number = 100; + /** Removes bottom margin from `bit-section` */ + @Input({ transform: coerceBooleanProperty }) disableMargin = false; + /** Emits settings updates and completes if the settings become unavailable. * @remarks this does not emit the initial settings. If you would like * to receive live settings updates including the initial update, @@ -128,14 +135,6 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy { toValidators(Controls.minSpecial, Generators.password, constraints), ); - // forward word boundaries to the template (can't do it through the rx form) - this.minLength = constraints.length.min; - this.maxLength = constraints.length.max; - this.minMinNumber = constraints.minNumber.min; - this.maxMinNumber = constraints.minNumber.max; - this.minMinSpecial = constraints.minSpecial.min; - this.maxMinSpecial = constraints.minSpecial.max; - this.policyInEffect = constraints.policyInEffect; const toggles = [ @@ -151,6 +150,13 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy { for (const [control, enabled] of toggles) { this.toggleEnabled(control, enabled); } + + const boundariesHint = this.i18nService.t( + "generatorBoundariesHint", + constraints.length.min, + constraints.length.max, + ); + this.lengthBoundariesHint.next(boundariesHint); }); // cascade selections between checkboxes and spinboxes @@ -197,9 +203,6 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy { // now that outputs are set up, connect inputs this.settings.valueChanges .pipe( - // debounce ensures rapid edits to a field, such as partial edits to a - // spinbox or rapid button clicks don't emit spurious generator updates - debounceTime(this.waitMs), map((settings) => { // interface is "avoid" while storage is "include" const s: any = { ...settings }; @@ -212,27 +215,14 @@ export class PasswordSettingsComponent implements OnInit, OnDestroy { .subscribe(settings); } - /** attribute binding for length[min] */ - protected minLength: number; - - /** attribute binding for length[max] */ - protected maxLength: number; - - /** attribute binding for minNumber[min] */ - protected minMinNumber: number; - - /** attribute binding for minNumber[max] */ - protected maxMinNumber: number; - - /** attribute binding for minSpecial[min] */ - protected minMinSpecial: number; - - /** attribute binding for minSpecial[max] */ - protected maxMinSpecial: number; - /** display binding for enterprise policy notice */ protected policyInEffect: boolean; + private lengthBoundariesHint = new ReplaySubject(1); + + /** display binding for min/max constraints of `length` */ + protected lengthBoundariesHint$ = this.lengthBoundariesHint.asObservable(); + private toggleEnabled(setting: keyof typeof Controls, enabled: boolean) { if (enabled) { this.settings.get(setting).enable({ emitEvent: false }); diff --git a/libs/tools/generator/components/src/subaddress-settings.component.ts b/libs/tools/generator/components/src/subaddress-settings.component.ts index 30db8dc657d..bd6ca899db7 100644 --- a/libs/tools/generator/components/src/subaddress-settings.component.ts +++ b/libs/tools/generator/components/src/subaddress-settings.component.ts @@ -1,6 +1,6 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { FormBuilder } from "@angular/forms"; -import { BehaviorSubject, skip, Subject, takeUntil } from "rxjs"; +import { BehaviorSubject, map, skip, Subject, takeUntil, withLatestFrom } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { UserId } from "@bitwarden/common/types/guid"; @@ -53,9 +53,23 @@ export class SubaddressSettingsComponent implements OnInit, OnDestroy { const singleUserId$ = this.singleUserId$(); const settings = await this.generatorService.settings(Generators.subaddress, { singleUserId$ }); - settings.pipe(takeUntil(this.destroyed$)).subscribe((s) => { - this.settings.patchValue(s, { emitEvent: false }); - }); + settings + .pipe( + withLatestFrom(this.accountService.activeAccount$), + map(([settings, activeAccount]) => { + // if the subaddress isn't specified, copy it from + // the user's settings + if ((settings.subaddressEmail ?? "").length < 1) { + settings.subaddressEmail = activeAccount.email; + } + + return settings; + }), + takeUntil(this.destroyed$), + ) + .subscribe((s) => { + this.settings.patchValue(s, { emitEvent: false }); + }); // the first emission is the current value; subsequent emissions are updates settings.pipe(skip(1), takeUntil(this.destroyed$)).subscribe(this.onUpdated); diff --git a/libs/tools/generator/components/src/username-generator.component.html b/libs/tools/generator/components/src/username-generator.component.html index 6425cb7a38f..36aaae57ce2 100644 --- a/libs/tools/generator/components/src/username-generator.component.html +++ b/libs/tools/generator/components/src/username-generator.component.html @@ -3,49 +3,78 @@
-
- + -
{{ "options" | i18n }}
+

{{ "options" | i18n }}

-
+
-
+ {{ "type" | i18n }} - + + {{ credentialTypeHint$ | async }}
+
+ + {{ "service" | i18n }} + + + +
+
diff --git a/libs/tools/generator/components/src/username-generator.component.ts b/libs/tools/generator/components/src/username-generator.component.ts index 767c73c398a..6518ee51ed8 100644 --- a/libs/tools/generator/components/src/username-generator.component.ts +++ b/libs/tools/generator/components/src/username-generator.component.ts @@ -1,7 +1,11 @@ +import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core"; import { FormBuilder } from "@angular/forms"; import { BehaviorSubject, + catchError, + combineLatest, + combineLatestWith, distinctUntilChanged, filter, map, @@ -14,17 +18,30 @@ import { import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { IntegrationId } from "@bitwarden/common/tools/integration"; import { UserId } from "@bitwarden/common/types/guid"; +import { ToastService } from "@bitwarden/components"; import { Option } from "@bitwarden/components/src/select/option"; import { + AlgorithmInfo, CredentialAlgorithm, - CredentialGeneratorInfo, CredentialGeneratorService, GeneratedCredential, Generators, + getForwarderConfiguration, isEmailAlgorithm, + isForwarderIntegration, + isSameAlgorithm, isUsernameAlgorithm, + toCredentialGeneratorConfiguration, } from "@bitwarden/generator-core"; +import { GeneratorHistoryService } from "@bitwarden/generator-history"; + +// constants used to identify navigation selections that are not +// generator algorithms +const FORWARDER = "forwarder"; +const NONE_SELECTED = "none"; /** Component that generates usernames and emails */ @Component({ @@ -41,6 +58,9 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { */ constructor( private generatorService: CredentialGeneratorService, + private generatorHistoryService: GeneratorHistoryService, + private toastService: ToastService, + private logService: LogService, private i18nService: I18nService, private accountService: AccountService, private zone: NgZone, @@ -57,9 +77,16 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { @Output() readonly onGenerated = new EventEmitter(); + /** Removes bottom margin from internal elements */ + @Input({ transform: coerceBooleanProperty }) disableMargin = false; + /** Tracks the selected generation algorithm */ - protected credential = this.formBuilder.group({ - type: [null as CredentialAlgorithm], + protected username = this.formBuilder.group({ + nav: [null as string], + }); + + protected forwarder = this.formBuilder.group({ + nav: [null as string], }); async ngOnInit() { @@ -78,14 +105,27 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { this.generatorService .algorithms$(["email", "username"], { userId$: this.userId$ }) .pipe( - map((algorithms) => this.toOptions(algorithms)), + map((algorithms) => { + const usernames = algorithms.filter((a) => !isForwarderIntegration(a.id)); + const usernameOptions = this.toOptions(usernames); + usernameOptions.push({ value: FORWARDER, label: this.i18nService.t("forwardedEmail") }); + + const forwarders = algorithms.filter((a) => isForwarderIntegration(a.id)); + const forwarderOptions = this.toOptions(forwarders); + forwarderOptions.unshift({ value: NONE_SELECTED, label: this.i18nService.t("select") }); + + return [usernameOptions, forwarderOptions] as const; + }), takeUntil(this.destroyed), ) - .subscribe(this.typeOptions$); + .subscribe(([usernames, forwarders]) => { + this.typeOptions$.next(usernames); + this.forwarderOptions$.next(forwarders); + }); this.algorithm$ .pipe( - map((a) => a?.descriptionKey && this.i18nService.t(a?.descriptionKey)), + map((a) => a?.description), takeUntil(this.destroyed), ) .subscribe((hint) => { @@ -99,10 +139,32 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { // wire up the generator this.algorithm$ .pipe( + filter((algorithm) => !!algorithm), switchMap((algorithm) => this.typeToGenerator$(algorithm.id)), + catchError((error: unknown, generator) => { + if (typeof error === "string") { + this.toastService.showToast({ + message: error, + variant: "error", + title: "", + }); + } else { + this.logService.error(error); + } + + // continue with origin stream + return generator; + }), + withLatestFrom(this.userId$), takeUntil(this.destroyed), ) - .subscribe((generated) => { + .subscribe(([generated, userId]) => { + this.generatorHistoryService + .track(userId, generated.credential, generated.category, generated.generationDate) + .catch((e: unknown) => { + this.logService.error(e); + }); + // update subjects within the angular zone so that the // template bindings refresh immediately this.zone.run(() => { @@ -111,20 +173,96 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { }); }); + // normalize cascade selections; introduce subjects to allow changes + // from user selections and changes from preference updates to + // update the template + type CascadeValue = { nav: string; algorithm?: CredentialAlgorithm }; + const activeIdentifier$ = new Subject(); + const activeForwarder$ = new Subject(); + + this.username.valueChanges + .pipe( + map( + (username): CascadeValue => + username.nav === FORWARDER + ? { nav: username.nav } + : { nav: username.nav, algorithm: JSON.parse(username.nav) }, + ), + takeUntil(this.destroyed), + ) + .subscribe(activeIdentifier$); + + this.forwarder.valueChanges + .pipe( + map( + (forwarder): CascadeValue => + forwarder.nav === NONE_SELECTED + ? { nav: forwarder.nav } + : { nav: forwarder.nav, algorithm: JSON.parse(forwarder.nav) }, + ), + takeUntil(this.destroyed), + ) + .subscribe(activeForwarder$); + + // update forwarder cascade visibility + combineLatest([activeIdentifier$, activeForwarder$]) + .pipe( + map(([username, forwarder]) => { + const showForwarder = !username.algorithm; + const forwarderId = + showForwarder && isForwarderIntegration(forwarder.algorithm) + ? forwarder.algorithm.forwarder + : null; + return [showForwarder, forwarderId] as const; + }), + distinctUntilChanged((prev, next) => prev[0] === next[0] && prev[1] === next[1]), + takeUntil(this.destroyed), + ) + .subscribe(([showForwarder, forwarderId]) => { + // update subjects within the angular zone so that the + // template bindings refresh immediately + this.zone.run(() => { + this.showForwarder$.next(showForwarder); + this.forwarderId$.next(forwarderId); + }); + }); + + // update active algorithm + combineLatest([activeIdentifier$, activeForwarder$]) + .pipe( + map(([username, forwarder]) => { + const selection = username.algorithm ?? forwarder.algorithm; + if (selection) { + return this.generatorService.algorithm(selection); + } else { + return null; + } + }), + distinctUntilChanged((prev, next) => isSameAlgorithm(prev?.id, next?.id)), + takeUntil(this.destroyed), + ) + .subscribe((algorithm) => { + // update subjects within the angular zone so that the + // template bindings refresh immediately + this.zone.run(() => { + this.algorithm$.next(algorithm); + }); + }); + // assume the last-visible generator algorithm is the user's preferred one const preferences = await this.generatorService.preferences({ singleUserId$: this.userId$ }); - this.credential.valueChanges + this.algorithm$ .pipe( - filter(({ type }) => !!type), + filter((algorithm) => !!algorithm), withLatestFrom(preferences), takeUntil(this.destroyed), ) - .subscribe(([{ type }, preference]) => { - if (isEmailAlgorithm(type)) { - preference.email.algorithm = type; + .subscribe(([algorithm, preference]) => { + if (isEmailAlgorithm(algorithm.id)) { + preference.email.algorithm = algorithm.id; preference.email.updated = new Date(); - } else if (isUsernameAlgorithm(type)) { - preference.username.algorithm = type; + } else if (isUsernameAlgorithm(algorithm.id)) { + preference.username.algorithm = algorithm.id; preference.username.updated = new Date(); } else { return; @@ -133,31 +271,61 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { preferences.next(preference); }); - // populate the form with the user's preferences to kick off interactivity - preferences.pipe(takeUntil(this.destroyed)).subscribe(({ email, username }) => { - // this generator supports email & username; the last preference - // set by the user "wins" - const preference = email.updated > username.updated ? email.algorithm : username.algorithm; + preferences + .pipe( + map(({ email, username }) => { + const forwarderPref = isForwarderIntegration(email.algorithm) ? email : null; + const usernamePref = email.updated > username.updated ? email : username; - // break subscription loop - this.credential.setValue({ type: preference }, { emitEvent: false }); + // inject drilldown flags + const forwarderNav = !forwarderPref + ? NONE_SELECTED + : JSON.stringify(forwarderPref.algorithm); + const userNav = forwarderPref ? FORWARDER : JSON.stringify(usernamePref.algorithm); - const algorithm = this.generatorService.algorithm(preference); - // update subjects within the angular zone so that the - // template bindings refresh immediately - this.zone.run(() => { - this.algorithm$.next(algorithm); - }); - }); + // construct cascade metadata + const cascade = { + username: { + selection: { nav: userNav }, + active: { + nav: userNav, + algorithm: forwarderPref ? null : usernamePref.algorithm, + }, + }, + forwarder: { + selection: { nav: forwarderNav }, + active: { + nav: forwarderNav, + algorithm: forwarderPref?.algorithm, + }, + }, + }; - // generate on load unless the generator prohibits it - this.algorithm$ - .pipe( - distinctUntilChanged((prev, next) => prev.id === next.id), - filter((a) => !a.onlyOnRequest), + return cascade; + }), takeUntil(this.destroyed), ) - .subscribe(() => this.generate$.next()); + .subscribe(({ username, forwarder }) => { + // update navigation; break subscription loop + this.username.setValue(username.selection, { emitEvent: false }); + this.forwarder.setValue(forwarder.selection, { emitEvent: false }); + + // update cascade visibility + activeIdentifier$.next(username.active); + activeForwarder$.next(forwarder.active); + }); + + // automatically regenerate when the algorithm switches if the algorithm + // allows it; otherwise set a placeholder + this.algorithm$.pipe(takeUntil(this.destroyed)).subscribe((a) => { + this.zone.run(() => { + if (!a || a.onlyOnRequest) { + this.value$.next("-"); + } else { + this.generate("autogenerate"); + } + }); + }); } private typeToGenerator$(type: CredentialAlgorithm) { @@ -175,17 +343,60 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { case "username": return this.generatorService.generate$(Generators.username, dependencies); + } - default: - throw new Error(`Invalid generator type: "${type}"`); + if (isForwarderIntegration(type)) { + const forwarder = getForwarderConfiguration(type.forwarder); + const configuration = toCredentialGeneratorConfiguration(forwarder); + return this.generatorService.generate$(configuration, dependencies); } + + throw new Error(`Invalid generator type: "${type}"`); } /** Lists the credential types supported by the component. */ - protected typeOptions$ = new BehaviorSubject[]>([]); + protected typeOptions$ = new BehaviorSubject[]>([]); + + /** Tracks the currently selected forwarder. */ + protected forwarderId$ = new BehaviorSubject(null); + + /** Lists the credential types supported by the component. */ + protected forwarderOptions$ = new BehaviorSubject[]>([]); + + /** Tracks forwarder control visibility */ + protected showForwarder$ = new BehaviorSubject(false); /** tracks the currently selected credential type */ - protected algorithm$ = new ReplaySubject(1); + protected algorithm$ = new ReplaySubject(1); + + protected showAlgorithm$ = this.algorithm$.pipe( + combineLatestWith(this.showForwarder$), + map(([algorithm, showForwarder]) => (showForwarder ? null : algorithm)), + ); + + /** + * Emits the copy button aria-label respective of the selected credential type + */ + protected credentialTypeCopyLabel$ = this.algorithm$.pipe( + filter((algorithm) => !!algorithm), + map(({ copy }) => copy), + ); + + /** + * Emits the generate button aria-label respective of the selected credential type + */ + protected credentialTypeGenerateLabel$ = this.algorithm$.pipe( + filter((algorithm) => !!algorithm), + map(({ generate }) => generate), + ); + + /** + * Emits the copy credential toast respective of the selected credential type + */ + protected credentialTypeLabel$ = this.algorithm$.pipe( + filter((algorithm) => !!algorithm), + map(({ generatedValue }) => generatedValue), + ); /** Emits hint key for the currently selected credential type */ protected credentialTypeHint$ = new ReplaySubject(1); @@ -197,12 +408,20 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { protected readonly userId$ = new BehaviorSubject(null); /** Emits when a new credential is requested */ - protected readonly generate$ = new Subject(); + private readonly generate$ = new Subject(); + + /** Request a new value from the generator + * @param requestor a label used to trace generation request + * origin in the debugger. + */ + protected generate(requestor: string) { + this.generate$.next(requestor); + } - private toOptions(algorithms: CredentialGeneratorInfo[]) { - const options: Option[] = algorithms.map((algorithm) => ({ - value: algorithm.id, - label: this.i18nService.t(algorithm.nameKey), + private toOptions(algorithms: AlgorithmInfo[]) { + const options: Option[] = algorithms.map((algorithm) => ({ + value: JSON.stringify(algorithm.id), + label: algorithm.name, })); return options; diff --git a/libs/tools/generator/components/src/util.ts b/libs/tools/generator/components/src/util.ts index 2049a285e25..d6cd4e6fbaf 100644 --- a/libs/tools/generator/components/src/util.ts +++ b/libs/tools/generator/components/src/util.ts @@ -63,7 +63,7 @@ function getConstraint( ) { if (policy && key in policy) { return policy[key] ?? config[key]; - } else if (key in config) { + } else if (config && key in config) { return config[key]; } } diff --git a/libs/tools/generator/core/src/data/default-passphrase-boundaries.ts b/libs/tools/generator/core/src/data/default-passphrase-boundaries.ts index d4aca717093..875a15051a2 100644 --- a/libs/tools/generator/core/src/data/default-passphrase-boundaries.ts +++ b/libs/tools/generator/core/src/data/default-passphrase-boundaries.ts @@ -1,6 +1,6 @@ function initializeBoundaries() { const numWords = Object.freeze({ - min: 3, + min: 6, max: 20, }); diff --git a/libs/tools/generator/core/src/data/default-passphrase-generation-options.ts b/libs/tools/generator/core/src/data/default-passphrase-generation-options.ts index 59fb6069003..b66cc74e267 100644 --- a/libs/tools/generator/core/src/data/default-passphrase-generation-options.ts +++ b/libs/tools/generator/core/src/data/default-passphrase-generation-options.ts @@ -3,7 +3,7 @@ import { PassphraseGenerationOptions } from "../types"; /** The default options for passphrase generation. */ export const DefaultPassphraseGenerationOptions: Partial = Object.freeze({ - numWords: 3, + numWords: 6, wordSeparator: "-", capitalize: false, includeNumber: false, diff --git a/libs/tools/generator/core/src/data/generator-types.ts b/libs/tools/generator/core/src/data/generator-types.ts index 6c351b82e33..e54ec34e497 100644 --- a/libs/tools/generator/core/src/data/generator-types.ts +++ b/libs/tools/generator/core/src/data/generator-types.ts @@ -5,7 +5,7 @@ export const PasswordAlgorithms = Object.freeze(["password", "passphrase"] as co export const UsernameAlgorithms = Object.freeze(["username"] as const); /** Types of email addresses that may be generated by the credential generator */ -export const EmailAlgorithms = Object.freeze(["catchall", "forwarder", "subaddress"] as const); +export const EmailAlgorithms = Object.freeze(["catchall", "subaddress"] as const); /** All types of credentials that may be generated by the credential generator */ export const CredentialAlgorithms = Object.freeze([ diff --git a/libs/tools/generator/core/src/data/generators.ts b/libs/tools/generator/core/src/data/generators.ts index 2c96b0c2d39..6090fe789cb 100644 --- a/libs/tools/generator/core/src/data/generators.ts +++ b/libs/tools/generator/core/src/data/generators.ts @@ -1,9 +1,15 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { ApiSettings } from "@bitwarden/common/tools/integration/rpc"; import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint"; -import { Randomizer } from "../abstractions"; -import { EmailRandomizer, PasswordRandomizer, UsernameRandomizer } from "../engine"; +import { + EmailRandomizer, + ForwarderConfiguration, + PasswordRandomizer, + UsernameRandomizer, +} from "../engine"; +import { Forwarder } from "../engine/forwarder"; import { DefaultPolicyEvaluator, DynamicPasswordPolicyConstraints, @@ -25,6 +31,7 @@ import { CredentialGenerator, CredentialGeneratorConfiguration, EffUsernameGenerationOptions, + GeneratorDependencyProvider, NoPolicy, PassphraseGenerationOptions, PassphraseGeneratorPolicy, @@ -45,10 +52,16 @@ const PASSPHRASE = Object.freeze({ id: "passphrase", category: "password", nameKey: "passphrase", + generateKey: "generatePassphrase", + generatedValueKey: "passphrase", + copyKey: "copyPassphrase", onlyOnRequest: false, + request: [], engine: { - create(randomizer: Randomizer): CredentialGenerator { - return new PasswordRandomizer(randomizer); + create( + dependencies: GeneratorDependencyProvider, + ): CredentialGenerator { + return new PasswordRandomizer(dependencies.randomizer); }, }, settings: { @@ -82,10 +95,16 @@ const PASSWORD = Object.freeze({ id: "password", category: "password", nameKey: "password", + generateKey: "generatePassword", + generatedValueKey: "password", + copyKey: "copyPassword", onlyOnRequest: false, + request: [], engine: { - create(randomizer: Randomizer): CredentialGenerator { - return new PasswordRandomizer(randomizer); + create( + dependencies: GeneratorDependencyProvider, + ): CredentialGenerator { + return new PasswordRandomizer(dependencies.randomizer); }, }, settings: { @@ -127,10 +146,16 @@ const USERNAME = Object.freeze({ id: "username", category: "username", nameKey: "randomWord", + generateKey: "generateUsername", + generatedValueKey: "username", + copyKey: "copyUsername", onlyOnRequest: false, + request: [], engine: { - create(randomizer: Randomizer): CredentialGenerator { - return new UsernameRandomizer(randomizer); + create( + dependencies: GeneratorDependencyProvider, + ): CredentialGenerator { + return new UsernameRandomizer(dependencies.randomizer); }, }, settings: { @@ -158,10 +183,16 @@ const CATCHALL = Object.freeze({ category: "email", nameKey: "catchallEmail", descriptionKey: "catchallEmailDesc", + generateKey: "generateEmail", + generatedValueKey: "email", + copyKey: "copyEmail", onlyOnRequest: false, + request: [], engine: { - create(randomizer: Randomizer): CredentialGenerator { - return new EmailRandomizer(randomizer); + create( + dependencies: GeneratorDependencyProvider, + ): CredentialGenerator { + return new EmailRandomizer(dependencies.randomizer); }, }, settings: { @@ -189,10 +220,16 @@ const SUBADDRESS = Object.freeze({ category: "email", nameKey: "plusAddressedEmail", descriptionKey: "plusAddressedEmailDesc", + generateKey: "generateEmail", + generatedValueKey: "email", + copyKey: "copyEmail", onlyOnRequest: false, + request: [], engine: { - create(randomizer: Randomizer): CredentialGenerator { - return new EmailRandomizer(randomizer); + create( + dependencies: GeneratorDependencyProvider, + ): CredentialGenerator { + return new EmailRandomizer(dependencies.randomizer); }, }, settings: { @@ -215,6 +252,49 @@ const SUBADDRESS = Object.freeze({ }, } satisfies CredentialGeneratorConfiguration); +export function toCredentialGeneratorConfiguration( + configuration: ForwarderConfiguration, +) { + const forwarder = Object.freeze({ + id: { forwarder: configuration.id }, + category: "email", + nameKey: configuration.name, + descriptionKey: "forwardedEmailDesc", + generateKey: "generateEmail", + generatedValueKey: "email", + copyKey: "copyEmail", + onlyOnRequest: true, + request: configuration.forwarder.request, + engine: { + create(dependencies: GeneratorDependencyProvider) { + // FIXME: figure out why `configuration` fails to typecheck + const config: any = configuration; + return new Forwarder(config, dependencies.client, dependencies.i18nService); + }, + }, + settings: { + initial: configuration.forwarder.defaultSettings, + constraints: configuration.forwarder.settingsConstraints, + account: configuration.forwarder.settings, + }, + policy: { + type: PolicyType.PasswordGenerator, + disabledValue: {}, + combine(_acc: NoPolicy, _policy: Policy) { + return {}; + }, + createEvaluator(_policy: NoPolicy) { + return new DefaultPolicyEvaluator(); + }, + toConstraints(_policy: NoPolicy) { + return new IdentityConstraint(); + }, + }, + } satisfies CredentialGeneratorConfiguration); + + return forwarder; +} + /** Generator configurations */ export const Generators = Object.freeze({ /** Passphrase generator configuration */ diff --git a/libs/tools/generator/core/src/data/integrations.ts b/libs/tools/generator/core/src/data/integrations.ts index 6132891b368..71c80fc9dbe 100644 --- a/libs/tools/generator/core/src/data/integrations.ts +++ b/libs/tools/generator/core/src/data/integrations.ts @@ -1,3 +1,7 @@ +import { IntegrationId } from "@bitwarden/common/tools/integration"; +import { ApiSettings } from "@bitwarden/common/tools/integration/rpc"; + +import { ForwarderConfiguration } from "../engine"; import { AddyIo } from "../integration/addy-io"; import { DuckDuckGo } from "../integration/duck-duck-go"; import { Fastmail } from "../integration/fastmail"; @@ -5,6 +9,13 @@ import { FirefoxRelay } from "../integration/firefox-relay"; import { ForwardEmail } from "../integration/forward-email"; import { SimpleLogin } from "../integration/simple-login"; +/** Fixed list of integrations available to the application + * @example + * + * // Use `toCredentialGeneratorConfiguration(id :ForwarderIntegration)` + * // to convert an integration to a generator configuration + * const generator = toCredentialGeneratorConfiguration(Integrations.AddyIo); + */ export const Integrations = Object.freeze({ AddyIo, DuckDuckGo, @@ -13,3 +24,15 @@ export const Integrations = Object.freeze({ ForwardEmail, SimpleLogin, } as const); + +const integrations = new Map(Object.values(Integrations).map((i) => [i.id, i])); + +export function getForwarderConfiguration(id: IntegrationId): ForwarderConfiguration { + const maybeForwarder = integrations.get(id); + + if (maybeForwarder && "forwarder" in maybeForwarder) { + return maybeForwarder as ForwarderConfiguration; + } else { + return null; + } +} diff --git a/libs/tools/generator/core/src/engine/forwarder-configuration.ts b/libs/tools/generator/core/src/engine/forwarder-configuration.ts index 95c9add140a..7813f457399 100644 --- a/libs/tools/generator/core/src/engine/forwarder-configuration.ts +++ b/libs/tools/generator/core/src/engine/forwarder-configuration.ts @@ -1,11 +1,14 @@ import { UserKeyDefinition } from "@bitwarden/common/platform/state"; import { IntegrationConfiguration } from "@bitwarden/common/tools/integration/integration-configuration"; -import { ApiSettings } from "@bitwarden/common/tools/integration/rpc"; +import { ApiSettings, SelfHostedApiSettings } from "@bitwarden/common/tools/integration/rpc"; import { IntegrationRequest } from "@bitwarden/common/tools/integration/rpc/integration-request"; import { RpcConfiguration } from "@bitwarden/common/tools/integration/rpc/rpc-definition"; import { BufferedKeyDefinition } from "@bitwarden/common/tools/state/buffered-key-definition"; +import { ObjectKey } from "@bitwarden/common/tools/state/object-key"; +import { Constraints } from "@bitwarden/common/tools/types"; import { ForwarderContext } from "./forwarder-context"; +import { EmailDomainSettings, EmailPrefixSettings } from "./settings"; /** Mixin for transmitting `getAccountId` result. */ export type AccountRequest = { @@ -24,8 +27,16 @@ export type GetAccountIdRpcDef< Request extends IntegrationRequest = IntegrationRequest, > = RpcConfiguration, string>; +export type ForwarderRequestFields = keyof (ApiSettings & + SelfHostedApiSettings & + EmailDomainSettings & + EmailPrefixSettings); + /** Forwarder-specific static definition */ export type ForwarderConfiguration< + // FIXME: simply forwarder settings to an object that has all + // settings properties. The runtime dynamism should be limited + // to which have values, not which have properties listed. Settings extends ApiSettings, Request extends IntegrationRequest = IntegrationRequest, > = IntegrationConfiguration & { @@ -34,12 +45,30 @@ export type ForwarderConfiguration< /** default value of all fields */ defaultSettings: Partial; - /** forwarder settings storage */ + settingsConstraints: Constraints; + + /** Well-known fields to display on the forwarder screen */ + request: readonly ForwarderRequestFields[]; + + /** forwarder settings storage + * @deprecated use local.settings instead + */ settings: UserKeyDefinition; - /** forwarder settings import buffer; `undefined` when there is no buffer. */ + /** forwarder settings import buffer; `undefined` when there is no buffer. + * @deprecated use local.settings import + */ importBuffer?: BufferedKeyDefinition; + /** locally stored data; forwarder-partitioned */ + local: { + /** integration settings storage */ + settings: ObjectKey; + + /** plaintext import buffer - used during data migrations */ + import?: ObjectKey, Settings>; + }; + /** createForwardingEmail RPC definition */ createForwardingEmail: CreateForwardingEmailRpcDef; diff --git a/libs/tools/generator/core/src/engine/forwarder.ts b/libs/tools/generator/core/src/engine/forwarder.ts new file mode 100644 index 00000000000..523c6fdf1ec --- /dev/null +++ b/libs/tools/generator/core/src/engine/forwarder.ts @@ -0,0 +1,75 @@ +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { + ApiSettings, + IntegrationRequest, + RestClient, +} from "@bitwarden/common/tools/integration/rpc"; +import { GenerationRequest } from "@bitwarden/common/tools/types"; + +import { CredentialGenerator, GeneratedCredential } from "../types"; + +import { AccountRequest, ForwarderConfiguration } from "./forwarder-configuration"; +import { ForwarderContext } from "./forwarder-context"; +import { CreateForwardingAddressRpc, GetAccountIdRpc } from "./rpc"; + +/** Generation algorithms that query an email forwarding service to + * create anonymized email addresses. + */ +export class Forwarder implements CredentialGenerator { + /** Instantiates the email forwarder engine + * @param configuration The forwarder to query + * @param client requests data from the forwarding service + * @param i18nService localizes messages sent to the forwarding service + * and user-addressable errors + */ + constructor( + private configuration: ForwarderConfiguration, + private client: RestClient, + private i18nService: I18nService, + ) {} + + async generate(request: GenerationRequest, settings: ApiSettings) { + const requestOptions: IntegrationRequest & AccountRequest = { website: request.website }; + + const getAccount = await this.getAccountId(this.configuration, settings); + if (getAccount) { + requestOptions.accountId = await this.client.fetchJson(getAccount, requestOptions); + } + + const create = this.createForwardingAddress(this.configuration, settings); + const result = await this.client.fetchJson(create, requestOptions); + const id = { forwarder: this.configuration.id }; + + return new GeneratedCredential(result, id, Date.now()); + } + + private createContext( + configuration: ForwarderConfiguration, + settings: Settings, + ) { + return new ForwarderContext(configuration, settings, this.i18nService); + } + + private createForwardingAddress( + configuration: ForwarderConfiguration, + settings: Settings, + ) { + const context = this.createContext(configuration, settings); + const rpc = new CreateForwardingAddressRpc(configuration, context); + return rpc; + } + + private getAccountId( + configuration: ForwarderConfiguration, + settings: Settings, + ) { + if (!configuration.forwarder.getAccountId) { + return null; + } + + const context = this.createContext(configuration, settings); + const rpc = new GetAccountIdRpc(configuration, context); + + return rpc; + } +} diff --git a/libs/tools/generator/core/src/engine/index.ts b/libs/tools/generator/core/src/engine/index.ts index c3d2aefef1b..2d272e7c11b 100644 --- a/libs/tools/generator/core/src/engine/index.ts +++ b/libs/tools/generator/core/src/engine/index.ts @@ -1,4 +1,4 @@ -export { CryptoServiceRandomizer } from "./crypto-service-randomizer"; +export { KeyServiceRandomizer } from "./key-service-randomizer"; export { ForwarderConfiguration, AccountRequest } from "./forwarder-configuration"; export { ForwarderContext } from "./forwarder-context"; export * from "./settings"; diff --git a/libs/tools/generator/core/src/engine/crypto-service-randomizer.spec.ts b/libs/tools/generator/core/src/engine/key-service-randomizer.spec.ts similarity index 58% rename from libs/tools/generator/core/src/engine/crypto-service-randomizer.spec.ts rename to libs/tools/generator/core/src/engine/key-service-randomizer.spec.ts index c3908bc86a8..459a05618f9 100644 --- a/libs/tools/generator/core/src/engine/crypto-service-randomizer.spec.ts +++ b/libs/tools/generator/core/src/engine/key-service-randomizer.spec.ts @@ -1,11 +1,11 @@ import { mock } from "jest-mock-extended"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { KeyService } from "@bitwarden/key-management"; -import { CryptoServiceRandomizer } from "./crypto-service-randomizer"; +import { KeyServiceRandomizer } from "./key-service-randomizer"; -describe("CryptoServiceRandomizer", () => { - const cryptoService = mock(); +describe("KeyServiceRandomizer", () => { + const keyService = mock(); afterEach(() => { jest.resetAllMocks(); @@ -13,7 +13,7 @@ describe("CryptoServiceRandomizer", () => { describe("pick", () => { it.each([[null], [undefined], [[]]])("throws when the list is %p", async (list) => { - const randomizer = new CryptoServiceRandomizer(cryptoService); + const randomizer = new KeyServiceRandomizer(keyService); await expect(() => randomizer.pick(list)).rejects.toBeInstanceOf(Error); @@ -21,8 +21,8 @@ describe("CryptoServiceRandomizer", () => { }); it("picks an item from the list", async () => { - const randomizer = new CryptoServiceRandomizer(cryptoService); - cryptoService.randomNumber.mockResolvedValue(1); + const randomizer = new KeyServiceRandomizer(keyService); + keyService.randomNumber.mockResolvedValue(1); const result = await randomizer.pick([0, 1]); @@ -32,7 +32,7 @@ describe("CryptoServiceRandomizer", () => { describe("pickWord", () => { it.each([[null], [undefined], [[]]])("throws when the list is %p", async (list) => { - const randomizer = new CryptoServiceRandomizer(cryptoService); + const randomizer = new KeyServiceRandomizer(keyService); await expect(() => randomizer.pickWord(list)).rejects.toBeInstanceOf(Error); @@ -40,8 +40,8 @@ describe("CryptoServiceRandomizer", () => { }); it("picks a word from the list", async () => { - const randomizer = new CryptoServiceRandomizer(cryptoService); - cryptoService.randomNumber.mockResolvedValue(1); + const randomizer = new KeyServiceRandomizer(keyService); + keyService.randomNumber.mockResolvedValue(1); const result = await randomizer.pickWord(["foo", "bar"]); @@ -49,8 +49,8 @@ describe("CryptoServiceRandomizer", () => { }); it("capitalizes the word when options.titleCase is true", async () => { - const randomizer = new CryptoServiceRandomizer(cryptoService); - cryptoService.randomNumber.mockResolvedValue(1); + const randomizer = new KeyServiceRandomizer(keyService); + keyService.randomNumber.mockResolvedValue(1); const result = await randomizer.pickWord(["foo", "bar"], { titleCase: true }); @@ -58,9 +58,9 @@ describe("CryptoServiceRandomizer", () => { }); it("appends a random number when options.number is true", async () => { - const randomizer = new CryptoServiceRandomizer(cryptoService); - cryptoService.randomNumber.mockResolvedValueOnce(1); - cryptoService.randomNumber.mockResolvedValueOnce(2); + const randomizer = new KeyServiceRandomizer(keyService); + keyService.randomNumber.mockResolvedValueOnce(1); + keyService.randomNumber.mockResolvedValueOnce(2); const result = await randomizer.pickWord(["foo", "bar"], { number: true }); @@ -70,7 +70,7 @@ describe("CryptoServiceRandomizer", () => { describe("shuffle", () => { it.each([[null], [undefined], [[]]])("throws when the list is %p", async (list) => { - const randomizer = new CryptoServiceRandomizer(cryptoService); + const randomizer = new KeyServiceRandomizer(keyService); await expect(() => randomizer.shuffle(list)).rejects.toBeInstanceOf(Error); @@ -78,18 +78,18 @@ describe("CryptoServiceRandomizer", () => { }); it("returns a copy of the list without shuffling it when theres only one entry", async () => { - const randomizer = new CryptoServiceRandomizer(cryptoService); + const randomizer = new KeyServiceRandomizer(keyService); const result = await randomizer.shuffle(["foo"]); expect(result).toEqual(["foo"]); expect(result).not.toBe(["foo"]); - expect(cryptoService.randomNumber).not.toHaveBeenCalled(); + expect(keyService.randomNumber).not.toHaveBeenCalled(); }); it("shuffles the tail of the list", async () => { - const randomizer = new CryptoServiceRandomizer(cryptoService); - cryptoService.randomNumber.mockResolvedValueOnce(0); + const randomizer = new KeyServiceRandomizer(keyService); + keyService.randomNumber.mockResolvedValueOnce(0); const result = await randomizer.shuffle(["bar", "foo"]); @@ -97,9 +97,9 @@ describe("CryptoServiceRandomizer", () => { }); it("shuffles the list", async () => { - const randomizer = new CryptoServiceRandomizer(cryptoService); - cryptoService.randomNumber.mockResolvedValueOnce(0); - cryptoService.randomNumber.mockResolvedValueOnce(1); + const randomizer = new KeyServiceRandomizer(keyService); + keyService.randomNumber.mockResolvedValueOnce(0); + keyService.randomNumber.mockResolvedValueOnce(1); const result = await randomizer.shuffle(["baz", "bar", "foo"]); @@ -107,8 +107,8 @@ describe("CryptoServiceRandomizer", () => { }); it("returns the input list when options.copy is false", async () => { - const randomizer = new CryptoServiceRandomizer(cryptoService); - cryptoService.randomNumber.mockResolvedValueOnce(0); + const randomizer = new KeyServiceRandomizer(keyService); + keyService.randomNumber.mockResolvedValueOnce(0); const expectedResult = ["foo"]; const result = await randomizer.shuffle(expectedResult, { copy: false }); @@ -119,7 +119,7 @@ describe("CryptoServiceRandomizer", () => { describe("chars", () => { it("returns an empty string when the length is 0", async () => { - const randomizer = new CryptoServiceRandomizer(cryptoService); + const randomizer = new KeyServiceRandomizer(keyService); const result = await randomizer.chars(0); @@ -127,8 +127,8 @@ describe("CryptoServiceRandomizer", () => { }); it("returns an arbitrary lowercase ascii character", async () => { - const randomizer = new CryptoServiceRandomizer(cryptoService); - cryptoService.randomNumber.mockResolvedValueOnce(0); + const randomizer = new KeyServiceRandomizer(keyService); + keyService.randomNumber.mockResolvedValueOnce(0); const result = await randomizer.chars(1); @@ -136,38 +136,38 @@ describe("CryptoServiceRandomizer", () => { }); it("returns a number of ascii characters based on the length", async () => { - const randomizer = new CryptoServiceRandomizer(cryptoService); - cryptoService.randomNumber.mockResolvedValue(0); + const randomizer = new KeyServiceRandomizer(keyService); + keyService.randomNumber.mockResolvedValue(0); const result = await randomizer.chars(2); expect(result).toEqual("aa"); - expect(cryptoService.randomNumber).toHaveBeenCalledTimes(2); + expect(keyService.randomNumber).toHaveBeenCalledTimes(2); }); it("returns a new random character each time its called", async () => { - const randomizer = new CryptoServiceRandomizer(cryptoService); - cryptoService.randomNumber.mockResolvedValueOnce(0); - cryptoService.randomNumber.mockResolvedValueOnce(1); + const randomizer = new KeyServiceRandomizer(keyService); + keyService.randomNumber.mockResolvedValueOnce(0); + keyService.randomNumber.mockResolvedValueOnce(1); const resultA = await randomizer.chars(1); const resultB = await randomizer.chars(1); expect(resultA).toEqual("a"); expect(resultB).toEqual("b"); - expect(cryptoService.randomNumber).toHaveBeenCalledTimes(2); + expect(keyService.randomNumber).toHaveBeenCalledTimes(2); }); }); describe("uniform", () => { it("forwards requests to the crypto service", async () => { - const randomizer = new CryptoServiceRandomizer(cryptoService); - cryptoService.randomNumber.mockResolvedValue(5); + const randomizer = new KeyServiceRandomizer(keyService); + keyService.randomNumber.mockResolvedValue(5); const result = await randomizer.uniform(0, 5); expect(result).toBe(5); - expect(cryptoService.randomNumber).toHaveBeenCalledWith(0, 5); + expect(keyService.randomNumber).toHaveBeenCalledWith(0, 5); }); }); }); diff --git a/libs/tools/generator/core/src/engine/crypto-service-randomizer.ts b/libs/tools/generator/core/src/engine/key-service-randomizer.ts similarity index 79% rename from libs/tools/generator/core/src/engine/crypto-service-randomizer.ts rename to libs/tools/generator/core/src/engine/key-service-randomizer.ts index cc7ceef4bbe..5fc719042b7 100644 --- a/libs/tools/generator/core/src/engine/crypto-service-randomizer.ts +++ b/libs/tools/generator/core/src/engine/key-service-randomizer.ts @@ -1,14 +1,14 @@ -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { KeyService } from "@bitwarden/key-management"; import { Randomizer } from "../abstractions"; import { WordOptions } from "../types"; -/** A randomizer backed by a CryptoService. */ -export class CryptoServiceRandomizer implements Randomizer { +/** A randomizer backed by a KeyService. */ +export class KeyServiceRandomizer implements Randomizer { /** instantiates the type. - * @param crypto generates random numbers + * @param keyService generates random numbers */ - constructor(private crypto: CryptoService) {} + constructor(private keyService: KeyService) {} async pick(list: Array): Promise { const length = list?.length ?? 0; @@ -28,7 +28,7 @@ export class CryptoServiceRandomizer implements Randomizer { } if (options?.number ?? false) { - const num = await this.crypto.randomNumber(1, 9); + const num = await this.keyService.randomNumber(1, 9); word = word + num.toString(); } @@ -63,6 +63,6 @@ export class CryptoServiceRandomizer implements Randomizer { } async uniform(min: number, max: number) { - return this.crypto.randomNumber(min, max); + return this.keyService.randomNumber(min, max); } } diff --git a/libs/tools/generator/core/src/factories.ts b/libs/tools/generator/core/src/factories.ts index 6c09b8d315a..479545c78fe 100644 --- a/libs/tools/generator/core/src/factories.ts +++ b/libs/tools/generator/core/src/factories.ts @@ -1,11 +1,11 @@ // contains logic that constructs generator services dynamically given // a generator id. -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { KeyService } from "@bitwarden/key-management"; import { Randomizer } from "./abstractions"; -import { CryptoServiceRandomizer } from "./engine/crypto-service-randomizer"; +import { KeyServiceRandomizer } from "./engine/key-service-randomizer"; -export function createRandomizer(cryptoService: CryptoService): Randomizer { - return new CryptoServiceRandomizer(cryptoService); +export function createRandomizer(keyService: KeyService): Randomizer { + return new KeyServiceRandomizer(keyService); } diff --git a/libs/tools/generator/core/src/integration/addy-io.ts b/libs/tools/generator/core/src/integration/addy-io.ts index 8f594827e95..2d265ca9bfc 100644 --- a/libs/tools/generator/core/src/integration/addy-io.ts +++ b/libs/tools/generator/core/src/integration/addy-io.ts @@ -1,11 +1,18 @@ -import { GENERATOR_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state"; +import { + GENERATOR_DISK, + GENERATOR_MEMORY, + UserKeyDefinition, +} from "@bitwarden/common/platform/state"; import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration"; import { ApiSettings, IntegrationRequest, SelfHostedApiSettings, } from "@bitwarden/common/tools/integration/rpc"; +import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier"; +import { PublicClassifier } from "@bitwarden/common/tools/public-classifier"; import { BufferedKeyDefinition } from "@bitwarden/common/tools/state/buffered-key-definition"; +import { ObjectKey } from "@bitwarden/common/tools/state/object-key"; import { ForwarderConfiguration, ForwarderContext, EmailDomainSettings } from "../engine"; import { CreateForwardingEmailRpcDef } from "../engine/forwarder-configuration"; @@ -44,6 +51,40 @@ const createForwardingEmail = Object.freeze({ // forwarder configuration const forwarder = Object.freeze({ defaultSettings, + createForwardingEmail, + request: ["token", "baseUrl", "domain"], + settingsConstraints: { + token: { required: true }, + domain: { required: true }, + baseUrl: {}, + }, + local: { + settings: { + // FIXME: integration should issue keys at runtime + // based on integrationId & extension metadata + // e.g. key: "forwarder.AddyIo.local.settings", + key: "addyIoForwarder", + target: "object", + format: "classified", + classifier: new PrivateClassifier(), + state: GENERATOR_DISK, + options: { + deserializer: (value) => value, + clearOn: ["logout"], + }, + } satisfies ObjectKey, + import: { + key: "forwarder.AddyIo.local.import", + target: "object", + format: "plain", + classifier: new PublicClassifier(["token", "baseUrl", "domain"]), + state: GENERATOR_MEMORY, + options: { + deserializer: (value) => value, + clearOn: ["logout", "lock"], + }, + } satisfies ObjectKey, AddyIoSettings>, + }, settings: new UserKeyDefinition(GENERATOR_DISK, "addyIoForwarder", { deserializer: (value) => value, clearOn: [], @@ -52,7 +93,6 @@ const forwarder = Object.freeze({ deserializer: (value) => value, clearOn: ["logout"], }), - createForwardingEmail, } as const); export const AddyIo = Object.freeze({ diff --git a/libs/tools/generator/core/src/integration/duck-duck-go.ts b/libs/tools/generator/core/src/integration/duck-duck-go.ts index 0c13ac6b632..4c1d672cc60 100644 --- a/libs/tools/generator/core/src/integration/duck-duck-go.ts +++ b/libs/tools/generator/core/src/integration/duck-duck-go.ts @@ -1,7 +1,14 @@ -import { GENERATOR_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state"; +import { + GENERATOR_DISK, + GENERATOR_MEMORY, + UserKeyDefinition, +} from "@bitwarden/common/platform/state"; import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration"; import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc"; +import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier"; +import { PublicClassifier } from "@bitwarden/common/tools/public-classifier"; import { BufferedKeyDefinition } from "@bitwarden/common/tools/state/buffered-key-definition"; +import { ObjectKey } from "@bitwarden/common/tools/state/object-key"; import { ForwarderConfiguration, ForwarderContext } from "../engine"; import { CreateForwardingEmailRpcDef } from "../engine/forwarder-configuration"; @@ -36,6 +43,38 @@ const createForwardingEmail = Object.freeze({ // forwarder configuration const forwarder = Object.freeze({ defaultSettings, + createForwardingEmail, + request: ["token"], + settingsConstraints: { + token: { required: true }, + }, + local: { + settings: { + // FIXME: integration should issue keys at runtime + // based on integrationId & extension metadata + // e.g. key: "forwarder.DuckDuckGo.local.settings", + key: "duckDuckGoForwarder", + target: "object", + format: "classified", + classifier: new PrivateClassifier(), + state: GENERATOR_DISK, + options: { + deserializer: (value) => value, + clearOn: ["logout"], + }, + } satisfies ObjectKey, + import: { + key: "forwarder.DuckDuckGo.local.import", + target: "object", + format: "plain", + classifier: new PublicClassifier(["token"]), + state: GENERATOR_MEMORY, + options: { + deserializer: (value) => value, + clearOn: ["logout", "lock"], + }, + } satisfies ObjectKey, DuckDuckGoSettings>, + }, settings: new UserKeyDefinition(GENERATOR_DISK, "duckDuckGoForwarder", { deserializer: (value) => value, clearOn: [], @@ -44,7 +83,6 @@ const forwarder = Object.freeze({ deserializer: (value) => value, clearOn: ["logout"], }), - createForwardingEmail, } as const); // integration-wide configuration diff --git a/libs/tools/generator/core/src/integration/fastmail.ts b/libs/tools/generator/core/src/integration/fastmail.ts index 0987540e036..13aa8db6247 100644 --- a/libs/tools/generator/core/src/integration/fastmail.ts +++ b/libs/tools/generator/core/src/integration/fastmail.ts @@ -1,7 +1,14 @@ -import { GENERATOR_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state"; +import { + GENERATOR_DISK, + GENERATOR_MEMORY, + UserKeyDefinition, +} from "@bitwarden/common/platform/state"; import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration"; import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc"; +import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier"; +import { PublicClassifier } from "@bitwarden/common/tools/public-classifier"; import { BufferedKeyDefinition } from "@bitwarden/common/tools/state/buffered-key-definition"; +import { ObjectKey } from "@bitwarden/common/tools/state/object-key"; import { ForwarderConfiguration, @@ -101,6 +108,41 @@ const createForwardingEmail = Object.freeze({ // forwarder configuration const forwarder = Object.freeze({ defaultSettings, + createForwardingEmail, + getAccountId, + request: ["token"], + settingsConstraints: { + token: { required: true }, + domain: { required: true }, + prefix: {}, + }, + local: { + settings: { + // FIXME: integration should issue keys at runtime + // based on integrationId & extension metadata + // e.g. key: "forwarder.Fastmail.local.settings" + key: "fastmailForwarder", + target: "object", + format: "classified", + classifier: new PrivateClassifier(), + state: GENERATOR_DISK, + options: { + deserializer: (value) => value, + clearOn: ["logout"], + }, + } satisfies ObjectKey, + import: { + key: "forwarder.Fastmail.local.import", + target: "object", + format: "plain", + classifier: new PublicClassifier(["token"]), + state: GENERATOR_MEMORY, + options: { + deserializer: (value) => value, + clearOn: ["logout", "lock"], + }, + } satisfies ObjectKey, FastmailSettings>, + }, settings: new UserKeyDefinition(GENERATOR_DISK, "fastmailForwarder", { deserializer: (value) => value, clearOn: [], @@ -109,8 +151,6 @@ const forwarder = Object.freeze({ deserializer: (value) => value, clearOn: ["logout"], }), - createForwardingEmail, - getAccountId, } as const); // integration-wide configuration diff --git a/libs/tools/generator/core/src/integration/firefox-relay.ts b/libs/tools/generator/core/src/integration/firefox-relay.ts index 4feb8a0bd99..9c965a4c9cd 100644 --- a/libs/tools/generator/core/src/integration/firefox-relay.ts +++ b/libs/tools/generator/core/src/integration/firefox-relay.ts @@ -1,7 +1,14 @@ -import { GENERATOR_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state"; +import { + GENERATOR_DISK, + GENERATOR_MEMORY, + UserKeyDefinition, +} from "@bitwarden/common/platform/state"; import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration"; import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc"; +import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier"; +import { PublicClassifier } from "@bitwarden/common/tools/public-classifier"; import { BufferedKeyDefinition } from "@bitwarden/common/tools/state/buffered-key-definition"; +import { ObjectKey } from "@bitwarden/common/tools/state/object-key"; import { ForwarderConfiguration, ForwarderContext } from "../engine"; import { CreateForwardingEmailRpcDef } from "../engine/forwarder-configuration"; @@ -40,6 +47,38 @@ const createForwardingEmail = Object.freeze({ // forwarder configuration const forwarder = Object.freeze({ defaultSettings, + createForwardingEmail, + request: ["token"], + settingsConstraints: { + token: { required: true }, + }, + local: { + settings: { + // FIXME: integration should issue keys at runtime + // based on integrationId & extension metadata + // e.g. key: "forwarder.Firefox.local.settings", + key: "firefoxRelayForwarder", + target: "object", + format: "classified", + classifier: new PrivateClassifier(), + state: GENERATOR_DISK, + options: { + deserializer: (value) => value, + clearOn: ["logout"], + }, + } satisfies ObjectKey, + import: { + key: "forwarder.Firefox.local.import", + target: "object", + format: "plain", + classifier: new PublicClassifier(["token"]), + state: GENERATOR_MEMORY, + options: { + deserializer: (value) => value, + clearOn: ["logout", "lock"], + }, + } satisfies ObjectKey, FirefoxRelaySettings>, + }, settings: new UserKeyDefinition(GENERATOR_DISK, "firefoxRelayForwarder", { deserializer: (value) => value, clearOn: [], @@ -52,7 +91,6 @@ const forwarder = Object.freeze({ clearOn: ["logout"], }, ), - createForwardingEmail, } as const); // integration-wide configuration diff --git a/libs/tools/generator/core/src/integration/forward-email.ts b/libs/tools/generator/core/src/integration/forward-email.ts index c4ef21d9d30..a128159fcd6 100644 --- a/libs/tools/generator/core/src/integration/forward-email.ts +++ b/libs/tools/generator/core/src/integration/forward-email.ts @@ -1,7 +1,14 @@ -import { GENERATOR_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state"; +import { + GENERATOR_DISK, + GENERATOR_MEMORY, + UserKeyDefinition, +} from "@bitwarden/common/platform/state"; import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration"; import { ApiSettings, IntegrationRequest } from "@bitwarden/common/tools/integration/rpc"; +import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier"; +import { PublicClassifier } from "@bitwarden/common/tools/public-classifier"; import { BufferedKeyDefinition } from "@bitwarden/common/tools/state/buffered-key-definition"; +import { ObjectKey } from "@bitwarden/common/tools/state/object-key"; import { ForwarderConfiguration, ForwarderContext, EmailDomainSettings } from "../engine"; import { CreateForwardingEmailRpcDef } from "../engine/forwarder-configuration"; @@ -43,6 +50,38 @@ const createForwardingEmail = Object.freeze({ // forwarder configuration const forwarder = Object.freeze({ defaultSettings, + request: ["token", "domain"], + settingsConstraints: { + token: { required: true }, + domain: { required: true }, + }, + local: { + settings: { + // FIXME: integration should issue keys at runtime + // based on integrationId & extension metadata + // e.g. key: "forwarder.ForwardEmail.local.settings", + key: "forwardEmailForwarder", + target: "object", + format: "classified", + classifier: new PrivateClassifier(), + state: GENERATOR_DISK, + options: { + deserializer: (value) => value, + clearOn: ["logout"], + }, + } satisfies ObjectKey, + import: { + key: "forwarder.ForwardEmail.local.import", + target: "object", + format: "plain", + classifier: new PublicClassifier(["token", "domain"]), + state: GENERATOR_MEMORY, + options: { + deserializer: (value) => value, + clearOn: ["logout", "lock"], + }, + } satisfies ObjectKey, ForwardEmailSettings>, + }, settings: new UserKeyDefinition(GENERATOR_DISK, "forwardEmailForwarder", { deserializer: (value) => value, clearOn: [], diff --git a/libs/tools/generator/core/src/integration/simple-login.ts b/libs/tools/generator/core/src/integration/simple-login.ts index 88730d0578e..d4b297fc37e 100644 --- a/libs/tools/generator/core/src/integration/simple-login.ts +++ b/libs/tools/generator/core/src/integration/simple-login.ts @@ -1,11 +1,18 @@ -import { GENERATOR_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state"; +import { + GENERATOR_DISK, + GENERATOR_MEMORY, + UserKeyDefinition, +} from "@bitwarden/common/platform/state"; import { IntegrationContext, IntegrationId } from "@bitwarden/common/tools/integration"; import { ApiSettings, IntegrationRequest, SelfHostedApiSettings, } from "@bitwarden/common/tools/integration/rpc"; +import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier"; +import { PublicClassifier } from "@bitwarden/common/tools/public-classifier"; import { BufferedKeyDefinition } from "@bitwarden/common/tools/state/buffered-key-definition"; +import { ObjectKey } from "@bitwarden/common/tools/state/object-key"; import { ForwarderConfiguration, ForwarderContext } from "../engine"; import { CreateForwardingEmailRpcDef } from "../engine/forwarder-configuration"; @@ -45,6 +52,38 @@ const createForwardingEmail = Object.freeze({ // forwarder configuration const forwarder = Object.freeze({ defaultSettings, + createForwardingEmail, + request: ["token", "baseUrl"], + settingsConstraints: { + token: { required: true }, + }, + local: { + settings: { + // FIXME: integration should issue keys at runtime + // based on integrationId & extension metadata + // e.g. key: "forwarder.SimpleLogin.local.settings", + key: "simpleLoginForwarder", + target: "object", + format: "classified", + classifier: new PrivateClassifier(), + state: GENERATOR_DISK, + options: { + deserializer: (value) => value, + clearOn: ["logout"], + }, + } satisfies ObjectKey, + import: { + key: "forwarder.SimpleLogin.local.import", + target: "object", + format: "plain", + classifier: new PublicClassifier(["token", "baseUrl"]), + state: GENERATOR_MEMORY, + options: { + deserializer: (value) => value, + clearOn: ["logout", "lock"], + }, + } satisfies ObjectKey, SimpleLoginSettings>, + }, settings: new UserKeyDefinition(GENERATOR_DISK, "simpleLoginForwarder", { deserializer: (value) => value, clearOn: [], @@ -57,7 +96,6 @@ const forwarder = Object.freeze({ clearOn: ["logout"], }, ), - createForwardingEmail, } as const); // integration-wide configuration diff --git a/libs/tools/generator/core/src/policies/passphrase-generator-options-evaluator.spec.ts b/libs/tools/generator/core/src/policies/passphrase-generator-options-evaluator.spec.ts index 688315e9297..3b1eb799391 100644 --- a/libs/tools/generator/core/src/policies/passphrase-generator-options-evaluator.spec.ts +++ b/libs/tools/generator/core/src/policies/passphrase-generator-options-evaluator.spec.ts @@ -164,7 +164,7 @@ describe("Password generator options builder", () => { }, ); - it.each([3, 8, 18, 20])( + it.each([6, 8, 18, 20])( "should set `numWords` (= %i) to the input value when it is within the boundaries", (numWords) => { expect(numWords).toBeGreaterThanOrEqual(DefaultPassphraseBoundaries.numWords.min); diff --git a/libs/tools/generator/core/src/policies/passphrase-policy-constraints.spec.ts b/libs/tools/generator/core/src/policies/passphrase-policy-constraints.spec.ts index 034a8234223..7565be7936f 100644 --- a/libs/tools/generator/core/src/policies/passphrase-policy-constraints.spec.ts +++ b/libs/tools/generator/core/src/policies/passphrase-policy-constraints.spec.ts @@ -72,9 +72,9 @@ describe("PassphrasePolicyConstraints", () => { }); it.each([ - [1, 3], + [1, 6], [21, 20], - ])("fits numWords (=%p) within the default bounds (3 <= %p <= 20)", (value, expected) => { + ])("fits numWords (=%p) within the default bounds (6 <= %p <= 20)", (value, expected) => { const policy = new PassphrasePolicyConstraints(Policies.Passphrase.disabledValue); const { numWords } = policy.adjust({ ...SomeSettings, numWords: value }); @@ -83,7 +83,7 @@ describe("PassphrasePolicyConstraints", () => { }); it.each([ - [1, 4, 4], + [1, 6, 6], [21, 20, 20], ])( "fits numWords (=%p) within the policy bounds (%p <= %p <= 20)", diff --git a/libs/tools/generator/core/src/rx.spec.ts b/libs/tools/generator/core/src/rx.spec.ts deleted file mode 100644 index b98e79bb074..00000000000 --- a/libs/tools/generator/core/src/rx.spec.ts +++ /dev/null @@ -1,352 +0,0 @@ -import { EmptyError, Subject, tap } from "rxjs"; - -import { anyComplete, on, ready } from "./rx"; - -describe("anyComplete", () => { - it("emits true when its input completes", () => { - const input$ = new Subject(); - - const emissions: boolean[] = []; - anyComplete(input$).subscribe((e) => emissions.push(e)); - input$.complete(); - - expect(emissions).toEqual([true]); - }); - - it("completes when its input is already complete", () => { - const input = new Subject(); - input.complete(); - - let completed = false; - anyComplete(input).subscribe({ complete: () => (completed = true) }); - - expect(completed).toBe(true); - }); - - it("completes when any input completes", () => { - const input$ = new Subject(); - const completing$ = new Subject(); - - let completed = false; - anyComplete([input$, completing$]).subscribe({ complete: () => (completed = true) }); - completing$.complete(); - - expect(completed).toBe(true); - }); - - it("ignores emissions", () => { - const input$ = new Subject(); - - const emissions: boolean[] = []; - anyComplete(input$).subscribe((e) => emissions.push(e)); - input$.next(1); - input$.next(2); - input$.complete(); - - expect(emissions).toEqual([true]); - }); - - it("forwards errors", () => { - const input$ = new Subject(); - const expected = { some: "error" }; - - let error = null; - anyComplete(input$).subscribe({ error: (e: unknown) => (error = e) }); - input$.error(expected); - - expect(error).toEqual(expected); - }); -}); - -describe("ready", () => { - it("connects when subscribed", () => { - const watch$ = new Subject(); - let connected = false; - const source$ = new Subject().pipe(tap({ subscribe: () => (connected = true) })); - - // precondition: ready$ should be cold - const ready$ = source$.pipe(ready(watch$)); - expect(connected).toBe(false); - - ready$.subscribe(); - - expect(connected).toBe(true); - }); - - it("suppresses source emissions until its watch emits", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const ready$ = source$.pipe(ready(watch$)); - const results: number[] = []; - ready$.subscribe((n) => results.push(n)); - - // precondition: no emissions - source$.next(1); - expect(results).toEqual([]); - - watch$.next(); - - expect(results).toEqual([1]); - }); - - it("suppresses source emissions until all watches emit", () => { - const watchA$ = new Subject(); - const watchB$ = new Subject(); - const source$ = new Subject(); - const ready$ = source$.pipe(ready([watchA$, watchB$])); - const results: number[] = []; - ready$.subscribe((n) => results.push(n)); - - // preconditions: no emissions - source$.next(1); - expect(results).toEqual([]); - watchA$.next(); - expect(results).toEqual([]); - - watchB$.next(); - - expect(results).toEqual([1]); - }); - - it("emits the last source emission when its watch emits", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const ready$ = source$.pipe(ready(watch$)); - const results: number[] = []; - ready$.subscribe((n) => results.push(n)); - - // precondition: no emissions - source$.next(1); - expect(results).toEqual([]); - - source$.next(2); - watch$.next(); - - expect(results).toEqual([2]); - }); - - it("emits all source emissions after its watch emits", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const ready$ = source$.pipe(ready(watch$)); - const results: number[] = []; - ready$.subscribe((n) => results.push(n)); - - watch$.next(); - source$.next(1); - source$.next(2); - - expect(results).toEqual([1, 2]); - }); - - it("ignores repeated watch emissions", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const ready$ = source$.pipe(ready(watch$)); - const results: number[] = []; - ready$.subscribe((n) => results.push(n)); - - watch$.next(); - source$.next(1); - watch$.next(); - source$.next(2); - watch$.next(); - - expect(results).toEqual([1, 2]); - }); - - it("completes when its source completes", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const ready$ = source$.pipe(ready(watch$)); - let completed = false; - ready$.subscribe({ complete: () => (completed = true) }); - - source$.complete(); - - expect(completed).toBeTruthy(); - }); - - it("errors when its source errors", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const ready$ = source$.pipe(ready(watch$)); - const expected = { some: "error" }; - let error = null; - ready$.subscribe({ error: (e: unknown) => (error = e) }); - - source$.error(expected); - - expect(error).toEqual(expected); - }); - - it("errors when its watch errors", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const ready$ = source$.pipe(ready(watch$)); - const expected = { some: "error" }; - let error = null; - ready$.subscribe({ error: (e: unknown) => (error = e) }); - - watch$.error(expected); - - expect(error).toEqual(expected); - }); - - it("errors when its watch completes before emitting", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const ready$ = source$.pipe(ready(watch$)); - let error = null; - ready$.subscribe({ error: (e: unknown) => (error = e) }); - - watch$.complete(); - - expect(error).toBeInstanceOf(EmptyError); - }); -}); - -describe("on", () => { - it("connects when subscribed", () => { - const watch$ = new Subject(); - let connected = false; - const source$ = new Subject().pipe(tap({ subscribe: () => (connected = true) })); - - // precondition: on$ should be cold - const on$ = source$.pipe(on(watch$)); - expect(connected).toBeFalsy(); - - on$.subscribe(); - - expect(connected).toBeTruthy(); - }); - - it("suppresses source emissions until `on` emits", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const results: number[] = []; - source$.pipe(on(watch$)).subscribe((n) => results.push(n)); - - // precondition: on$ should be cold - source$.next(1); - expect(results).toEqual([]); - - watch$.next(); - - expect(results).toEqual([1]); - }); - - it("repeats source emissions when `on` emits", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const results: number[] = []; - source$.pipe(on(watch$)).subscribe((n) => results.push(n)); - source$.next(1); - - watch$.next(); - watch$.next(); - - expect(results).toEqual([1, 1]); - }); - - it("updates source emissions when `on` emits", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const results: number[] = []; - source$.pipe(on(watch$)).subscribe((n) => results.push(n)); - - source$.next(1); - watch$.next(); - source$.next(2); - watch$.next(); - - expect(results).toEqual([1, 2]); - }); - - it("emits a value when `on` emits before the source is ready", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const results: number[] = []; - source$.pipe(on(watch$)).subscribe((n) => results.push(n)); - - watch$.next(); - source$.next(1); - - expect(results).toEqual([1]); - }); - - it("ignores repeated `on` emissions before the source is ready", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const results: number[] = []; - source$.pipe(on(watch$)).subscribe((n) => results.push(n)); - - watch$.next(); - watch$.next(); - source$.next(1); - - expect(results).toEqual([1]); - }); - - it("emits only the latest source emission when `on` emits", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const results: number[] = []; - source$.pipe(on(watch$)).subscribe((n) => results.push(n)); - source$.next(1); - - watch$.next(); - - source$.next(2); - source$.next(3); - watch$.next(); - - expect(results).toEqual([1, 3]); - }); - - it("completes when its source completes", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - let complete: boolean = false; - source$.pipe(on(watch$)).subscribe({ complete: () => (complete = true) }); - - source$.complete(); - - expect(complete).toBeTruthy(); - }); - - it("completes when its watch completes", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - let complete: boolean = false; - source$.pipe(on(watch$)).subscribe({ complete: () => (complete = true) }); - - watch$.complete(); - - expect(complete).toBeTruthy(); - }); - - it("errors when its source errors", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const expected = { some: "error" }; - let error = null; - source$.pipe(on(watch$)).subscribe({ error: (e: unknown) => (error = e) }); - - source$.error(expected); - - expect(error).toEqual(expected); - }); - - it("errors when its watch errors", () => { - const watch$ = new Subject(); - const source$ = new Subject(); - const expected = { some: "error" }; - let error = null; - source$.pipe(on(watch$)).subscribe({ error: (e: unknown) => (error = e) }); - - watch$.error(expected); - - expect(error).toEqual(expected); - }); -}); diff --git a/libs/tools/generator/core/src/rx.ts b/libs/tools/generator/core/src/rx.ts index 851b6cfe7c7..070d34d37d8 100644 --- a/libs/tools/generator/core/src/rx.ts +++ b/libs/tools/generator/core/src/rx.ts @@ -1,18 +1,4 @@ -import { - concat, - concatMap, - connect, - endWith, - first, - ignoreElements, - map, - Observable, - pipe, - race, - ReplaySubject, - takeUntil, - zip, -} from "rxjs"; +import { map, pipe } from "rxjs"; import { reduceCollection, distinctIfShallowMatch } from "@bitwarden/common/tools/rx"; @@ -51,86 +37,3 @@ export function newDefaultEvaluator() { return pipe(map((_) => new DefaultPolicyEvaluator())); }; } - -/** Create an observable that, once subscribed, emits `true` then completes when - * any input completes. If an input is already complete when the subscription - * occurs, it emits immediately. - * @param watch$ the observable(s) to watch for completion; if an array is passed, - * null and undefined members are ignored. If `watch$` is empty, `anyComplete` - * will never complete. - * @returns An observable that emits `true` when any of its inputs - * complete. The observable forwards the first error from its input. - * @remarks This method is particularly useful in combination with `takeUntil` and - * streams that are not guaranteed to complete on their own. - */ -export function anyComplete(watch$: Observable | Observable[]): Observable { - if (Array.isArray(watch$)) { - const completes$ = watch$ - .filter((w$) => !!w$) - .map((w$) => w$.pipe(ignoreElements(), endWith(true))); - const completed$ = race(completes$); - return completed$; - } else { - return watch$.pipe(ignoreElements(), endWith(true)); - } -} - -/** - * Create an observable that delays the input stream until all watches have - * emitted a value. The watched values are not included in the source stream. - * The last emission from the source is output when all the watches have - * emitted at least once. - * @param watch$ the observable(s) to watch for readiness. If `watch$` is empty, - * `ready` will never emit. - * @returns An observable that emits when the source stream emits. The observable - * errors if one of its watches completes before emitting. It also errors if one - * of its watches errors. - */ -export function ready(watch$: Observable | Observable[]) { - const watching$ = Array.isArray(watch$) ? watch$ : [watch$]; - return pipe( - connect>((source$) => { - // this subscription is safe because `source$` connects only after there - // is an external subscriber. - const source = new ReplaySubject(1); - source$.subscribe(source); - - // `concat` is subscribed immediately after it's returned, at which point - // `zip` blocks until all items in `watching$` are ready. If that occurs - // after `source$` is hot, then the replay subject sends the last-captured - // emission through immediately. Otherwise, `ready` waits for the next - // emission - return concat(zip(watching$).pipe(first(), ignoreElements()), source).pipe( - takeUntil(anyComplete(source)), - ); - }), - ); -} - -/** - * Create an observable that emits the latest value of the source stream - * when `watch$` emits. If `watch$` emits before the stream emits, then - * an emission occurs as soon as a value becomes ready. - * @param watch$ the observable that triggers emissions - * @returns An observable that emits when `watch$` emits. The observable - * errors if its source stream errors. It also errors if `on` errors. It - * completes if its watch completes. - * - * @remarks This works like `audit`, but it repeats emissions when - * watch$ fires. - */ -export function on(watch$: Observable) { - return pipe( - connect>((source$) => { - const source = new ReplaySubject(1); - source$.subscribe(source); - - return watch$ - .pipe( - ready(source), - concatMap(() => source.pipe(first())), - ) - .pipe(takeUntil(anyComplete(source))); - }), - ); -} diff --git a/libs/tools/generator/core/src/services/credential-generator.service.spec.ts b/libs/tools/generator/core/src/services/credential-generator.service.spec.ts index 88f1447e98d..225745e5f95 100644 --- a/libs/tools/generator/core/src/services/credential-generator.service.spec.ts +++ b/libs/tools/generator/core/src/services/credential-generator.service.spec.ts @@ -1,12 +1,17 @@ import { mock } from "jest-mock-extended"; import { BehaviorSubject, filter, firstValueFrom, Subject } from "rxjs"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { GENERATOR_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state"; import { StateConstraints } from "@bitwarden/common/tools/types"; import { OrganizationId, PolicyId, UserId } from "@bitwarden/common/types/guid"; +import { UserKey } from "@bitwarden/common/types/key"; +import { KeyService } from "@bitwarden/key-management"; import { FakeStateProvider, @@ -67,15 +72,22 @@ const SomeTime = new Date(1); const SomeAlgorithm = "passphrase"; const SomeCategory = "password"; const SomeNameKey = "passphraseKey"; +const SomeGenerateKey = "generateKey"; +const SomeGeneratedValueKey = "generatedValueKey"; +const SomeCopyKey = "copyKey"; // fake the configuration const SomeConfiguration: CredentialGeneratorConfiguration = { id: SomeAlgorithm, category: SomeCategory, nameKey: SomeNameKey, + generateKey: SomeGenerateKey, + generatedValueKey: SomeGeneratedValueKey, + copyKey: SomeCopyKey, onlyOnRequest: false, + request: [], engine: { - create: (randomizer) => { + create: (_randomizer) => { return { generate: (request, settings) => { const credential = request.website ? `${request.website}|${settings.foo}` : settings.foo; @@ -159,10 +171,22 @@ const stateProvider = new FakeStateProvider(accountService); // fake randomizer const randomizer = mock(); +const i18nService = mock(); + +const apiService = mock(); + +const encryptService = mock(); + +const keyService = mock(); + describe("CredentialGeneratorService", () => { beforeEach(async () => { await accountService.switchAccount(SomeUser); policyService.getAll$.mockImplementation(() => new BehaviorSubject([]).asObservable()); + i18nService.t.mockImplementation((key) => key); + apiService.fetch.mockImplementation(() => Promise.resolve(mock())); + const keyAvailable = new BehaviorSubject({} as UserKey); + keyService.userKey$.mockReturnValue(keyAvailable); jest.clearAllMocks(); }); @@ -170,7 +194,15 @@ describe("CredentialGeneratorService", () => { it("emits a generation for the active user when subscribed", async () => { const settings = { foo: "value" }; await stateProvider.setUserState(SettingsKey, settings, SomeUser); - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const generated = new ObservableTracker(generator.generate$(SomeConfiguration)); const result = await generated.expectEmission(); @@ -183,7 +215,15 @@ describe("CredentialGeneratorService", () => { const anotherSettings = { foo: "another value" }; await stateProvider.setUserState(SettingsKey, someSettings, SomeUser); await stateProvider.setUserState(SettingsKey, anotherSettings, AnotherUser); - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const generated = new ObservableTracker(generator.generate$(SomeConfiguration)); await accountService.switchAccount(AnotherUser); @@ -200,7 +240,15 @@ describe("CredentialGeneratorService", () => { const someSettings = { foo: "some value" }; const anotherSettings = { foo: "another value" }; await stateProvider.setUserState(SettingsKey, someSettings, SomeUser); - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const generated = new ObservableTracker(generator.generate$(SomeConfiguration)); await stateProvider.setUserState(SettingsKey, anotherSettings, SomeUser); @@ -220,7 +268,15 @@ describe("CredentialGeneratorService", () => { it("includes `website$`'s last emitted value", async () => { const settings = { foo: "value" }; await stateProvider.setUserState(SettingsKey, settings, SomeUser); - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const website$ = new BehaviorSubject("some website"); const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { website$ })); @@ -233,7 +289,15 @@ describe("CredentialGeneratorService", () => { it("errors when `website$` errors", async () => { await stateProvider.setUserState(SettingsKey, null, SomeUser); - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const website$ = new BehaviorSubject("some website"); let error = null; @@ -250,7 +314,15 @@ describe("CredentialGeneratorService", () => { it("completes when `website$` completes", async () => { await stateProvider.setUserState(SettingsKey, null, SomeUser); - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const website$ = new BehaviorSubject("some website"); let completed = false; @@ -268,7 +340,15 @@ describe("CredentialGeneratorService", () => { it("emits a generation for a specific user when `user$` supplied", async () => { await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser); await stateProvider.setUserState(SettingsKey, { foo: "another" }, AnotherUser); - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const userId$ = new BehaviorSubject(AnotherUser).asObservable(); const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { userId$ })); @@ -280,7 +360,15 @@ describe("CredentialGeneratorService", () => { it("emits a generation for a specific user when `user$` emits", async () => { await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser); await stateProvider.setUserState(SettingsKey, { foo: "another" }, AnotherUser); - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const userId = new BehaviorSubject(SomeUser); const userId$ = userId.pipe(filter((u) => !!u)); const generated = new ObservableTracker(generator.generate$(SomeConfiguration, { userId$ })); @@ -296,7 +384,15 @@ describe("CredentialGeneratorService", () => { it("errors when `user$` errors", async () => { await stateProvider.setUserState(SettingsKey, null, SomeUser); - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const userId$ = new BehaviorSubject(SomeUser); let error = null; @@ -313,7 +409,15 @@ describe("CredentialGeneratorService", () => { it("completes when `user$` completes", async () => { await stateProvider.setUserState(SettingsKey, null, SomeUser); - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const userId$ = new BehaviorSubject(SomeUser); let completed = false; @@ -331,7 +435,15 @@ describe("CredentialGeneratorService", () => { it("emits a generation only when `on$` emits", async () => { // This test breaks from arrange/act/assert because it is testing causality await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser); - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const on$ = new Subject(); const results: any[] = []; @@ -365,7 +477,15 @@ describe("CredentialGeneratorService", () => { it("errors when `on$` errors", async () => { await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser); - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const on$ = new Subject(); let error: any = null; @@ -383,7 +503,15 @@ describe("CredentialGeneratorService", () => { it("completes when `on$` completes", async () => { await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser); - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const on$ = new Subject(); let complete = false; @@ -406,54 +534,86 @@ describe("CredentialGeneratorService", () => { describe("algorithms", () => { it("outputs password generation metadata", () => { - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const result = generator.algorithms("password"); - expect(result).toContain(Generators.password); - expect(result).toContain(Generators.passphrase); + expect(result.some((a) => a.id === Generators.password.id)).toBeTruthy(); + expect(result.some((a) => a.id === Generators.passphrase.id)).toBeTruthy(); // this test shouldn't contain entries outside of the current category - expect(result).not.toContain(Generators.username); - expect(result).not.toContain(Generators.catchall); + expect(result.some((a) => a.id === Generators.username.id)).toBeFalsy(); + expect(result.some((a) => a.id === Generators.catchall.id)).toBeFalsy(); }); it("outputs username generation metadata", () => { - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const result = generator.algorithms("username"); - expect(result).toContain(Generators.username); + expect(result.some((a) => a.id === Generators.username.id)).toBeTruthy(); // this test shouldn't contain entries outside of the current category - expect(result).not.toContain(Generators.catchall); - expect(result).not.toContain(Generators.password); + expect(result.some((a) => a.id === Generators.catchall.id)).toBeFalsy(); + expect(result.some((a) => a.id === Generators.password.id)).toBeFalsy(); }); it("outputs email generation metadata", () => { - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const result = generator.algorithms("email"); - expect(result).toContain(Generators.catchall); - expect(result).toContain(Generators.subaddress); + expect(result.some((a) => a.id === Generators.catchall.id)).toBeTruthy(); + expect(result.some((a) => a.id === Generators.subaddress.id)).toBeTruthy(); // this test shouldn't contain entries outside of the current category - expect(result).not.toContain(Generators.username); - expect(result).not.toContain(Generators.password); + expect(result.some((a) => a.id === Generators.username.id)).toBeFalsy(); + expect(result.some((a) => a.id === Generators.password.id)).toBeFalsy(); }); it("combines metadata across categories", () => { - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const result = generator.algorithms(["username", "email"]); - expect(result).toContain(Generators.username); - expect(result).toContain(Generators.catchall); - expect(result).toContain(Generators.subaddress); + expect(result.some((a) => a.id === Generators.username.id)).toBeTruthy(); + expect(result.some((a) => a.id === Generators.catchall.id)).toBeTruthy(); + expect(result.some((a) => a.id === Generators.subaddress.id)).toBeTruthy(); // this test shouldn't contain entries outside of the current categories - expect(result).not.toContain(Generators.password); + expect(result.some((a) => a.id === Generators.password.id)).toBeFalsy(); }); }); @@ -461,39 +621,71 @@ describe("CredentialGeneratorService", () => { // these tests cannot use the observable tracker because they return // data that cannot be cloned it("returns password metadata", async () => { - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const result = await firstValueFrom(generator.algorithms$("password")); - expect(result).toContain(Generators.password); - expect(result).toContain(Generators.passphrase); + expect(result.some((a) => a.id === Generators.password.id)).toBeTruthy(); + expect(result.some((a) => a.id === Generators.passphrase.id)).toBeTruthy(); }); it("returns username metadata", async () => { - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const result = await firstValueFrom(generator.algorithms$("username")); - expect(result).toContain(Generators.username); + expect(result.some((a) => a.id === Generators.username.id)).toBeTruthy(); }); it("returns email metadata", async () => { - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const result = await firstValueFrom(generator.algorithms$("email")); - expect(result).toContain(Generators.catchall); - expect(result).toContain(Generators.subaddress); + expect(result.some((a) => a.id === Generators.catchall.id)).toBeTruthy(); + expect(result.some((a) => a.id === Generators.subaddress.id)).toBeTruthy(); }); it("returns username and email metadata", async () => { - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const result = await firstValueFrom(generator.algorithms$(["username", "email"])); - expect(result).toContain(Generators.username); - expect(result).toContain(Generators.catchall); - expect(result).toContain(Generators.subaddress); + expect(result.some((a) => a.id === Generators.username.id)).toBeTruthy(); + expect(result.some((a) => a.id === Generators.catchall.id)).toBeTruthy(); + expect(result.some((a) => a.id === Generators.subaddress.id)).toBeTruthy(); }); // Subsequent tests focus on passwords and passphrases as an example of policy @@ -501,13 +693,21 @@ describe("CredentialGeneratorService", () => { it("enforces the active user's policy", async () => { const policy$ = new BehaviorSubject([passwordOverridePolicy]); policyService.getAll$.mockReturnValue(policy$); - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const result = await firstValueFrom(generator.algorithms$(["password"])); expect(policyService.getAll$).toHaveBeenCalledWith(PolicyType.PasswordGenerator, SomeUser); - expect(result).toContain(Generators.password); - expect(result).not.toContain(Generators.passphrase); + expect(result.some((a) => a.id === Generators.password.id)).toBeTruthy(); + expect(result.some((a) => a.id === Generators.passphrase.id)).toBeFalsy(); }); it("follows changes to the active user", async () => { @@ -518,7 +718,15 @@ describe("CredentialGeneratorService", () => { await accountService.switchAccount(SomeUser); policyService.getAll$.mockReturnValueOnce(new BehaviorSubject([passwordOverridePolicy])); policyService.getAll$.mockReturnValueOnce(new BehaviorSubject([passphraseOverridePolicy])); - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const results: any = []; const sub = generator.algorithms$("password").subscribe((r) => results.push(r)); @@ -533,34 +741,50 @@ describe("CredentialGeneratorService", () => { PolicyType.PasswordGenerator, SomeUser, ); - expect(someResult).toContain(Generators.password); - expect(someResult).not.toContain(Generators.passphrase); + expect(someResult.some((a: any) => a.id === Generators.password.id)).toBeTruthy(); + expect(someResult.some((a: any) => a.id === Generators.passphrase.id)).toBeFalsy(); expect(policyService.getAll$).toHaveBeenNthCalledWith( 2, PolicyType.PasswordGenerator, AnotherUser, ); - expect(anotherResult).toContain(Generators.passphrase); - expect(anotherResult).not.toContain(Generators.password); + expect(anotherResult.some((a: any) => a.id === Generators.passphrase.id)).toBeTruthy(); + expect(anotherResult.some((a: any) => a.id === Generators.password.id)).toBeFalsy(); }); it("reads an arbitrary user's settings", async () => { policyService.getAll$.mockReturnValueOnce(new BehaviorSubject([passwordOverridePolicy])); - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const userId$ = new BehaviorSubject(AnotherUser).asObservable(); const result = await firstValueFrom(generator.algorithms$("password", { userId$ })); expect(policyService.getAll$).toHaveBeenCalledWith(PolicyType.PasswordGenerator, AnotherUser); - expect(result).toContain(Generators.password); - expect(result).not.toContain(Generators.passphrase); + expect(result.some((a: any) => a.id === Generators.password.id)).toBeTruthy(); + expect(result.some((a: any) => a.id === Generators.passphrase.id)).toBeFalsy(); }); it("follows changes to the arbitrary user", async () => { policyService.getAll$.mockReturnValueOnce(new BehaviorSubject([passwordOverridePolicy])); policyService.getAll$.mockReturnValueOnce(new BehaviorSubject([passphraseOverridePolicy])); - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const userId = new BehaviorSubject(SomeUser); const userId$ = userId.asObservable(); const results: any = []; @@ -572,17 +796,25 @@ describe("CredentialGeneratorService", () => { const [someResult, anotherResult] = results; expect(policyService.getAll$).toHaveBeenCalledWith(PolicyType.PasswordGenerator, SomeUser); - expect(someResult).toContain(Generators.password); - expect(someResult).not.toContain(Generators.passphrase); + expect(someResult.some((a: any) => a.id === Generators.password.id)).toBeTruthy(); + expect(someResult.some((a: any) => a.id === Generators.passphrase.id)).toBeFalsy(); expect(policyService.getAll$).toHaveBeenCalledWith(PolicyType.PasswordGenerator, AnotherUser); - expect(anotherResult).toContain(Generators.passphrase); - expect(anotherResult).not.toContain(Generators.password); + expect(anotherResult.some((a: any) => a.id === Generators.passphrase.id)).toBeTruthy(); + expect(anotherResult.some((a: any) => a.id === Generators.password.id)).toBeFalsy(); }); it("errors when the arbitrary user's stream errors", async () => { policyService.getAll$.mockReturnValueOnce(new BehaviorSubject([passwordOverridePolicy])); - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const userId = new BehaviorSubject(SomeUser); const userId$ = userId.asObservable(); let error = null; @@ -600,7 +832,15 @@ describe("CredentialGeneratorService", () => { it("completes when the arbitrary user's stream completes", async () => { policyService.getAll$.mockReturnValueOnce(new BehaviorSubject([passwordOverridePolicy])); - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const userId = new BehaviorSubject(SomeUser); const userId$ = userId.asObservable(); let completed = false; @@ -618,7 +858,15 @@ describe("CredentialGeneratorService", () => { it("ignores repeated arbitrary user emissions", async () => { policyService.getAll$.mockReturnValueOnce(new BehaviorSubject([passwordOverridePolicy])); - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const userId = new BehaviorSubject(SomeUser); const userId$ = userId.asObservable(); let count = 0; @@ -642,7 +890,15 @@ describe("CredentialGeneratorService", () => { describe("settings$", () => { it("defaults to the configuration's initial settings if settings aren't found", async () => { await stateProvider.setUserState(SettingsKey, null, SomeUser); - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const result = await firstValueFrom(generator.settings$(SomeConfiguration)); @@ -652,7 +908,15 @@ describe("CredentialGeneratorService", () => { it("reads from the active user's configuration-defined storage", async () => { const settings = { foo: "value" }; await stateProvider.setUserState(SettingsKey, settings, SomeUser); - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const result = await firstValueFrom(generator.settings$(SomeConfiguration)); @@ -664,7 +928,15 @@ describe("CredentialGeneratorService", () => { await stateProvider.setUserState(SettingsKey, settings, SomeUser); const policy$ = new BehaviorSubject([somePolicy]); policyService.getAll$.mockReturnValue(policy$); - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const result = await firstValueFrom(generator.settings$(SomeConfiguration)); @@ -672,7 +944,7 @@ describe("CredentialGeneratorService", () => { }); it("follows changes to the active user", async () => { - // initialize local accound service and state provider because this test is sensitive + // initialize local account service and state provider because this test is sensitive // to some shared data in `FakeAccountService`. const accountService = new FakeAccountService(accounts); const stateProvider = new FakeStateProvider(accountService); @@ -681,7 +953,15 @@ describe("CredentialGeneratorService", () => { const anotherSettings = { foo: "another" }; await stateProvider.setUserState(SettingsKey, someSettings, SomeUser); await stateProvider.setUserState(SettingsKey, anotherSettings, AnotherUser); - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const results: any = []; const sub = generator.settings$(SomeConfiguration).subscribe((r) => results.push(r)); @@ -698,7 +978,15 @@ describe("CredentialGeneratorService", () => { await stateProvider.setUserState(SettingsKey, { foo: "value" }, SomeUser); const anotherSettings = { foo: "another" }; await stateProvider.setUserState(SettingsKey, anotherSettings, AnotherUser); - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const userId$ = new BehaviorSubject(AnotherUser).asObservable(); const result = await firstValueFrom(generator.settings$(SomeConfiguration, { userId$ })); @@ -711,7 +999,15 @@ describe("CredentialGeneratorService", () => { await stateProvider.setUserState(SettingsKey, someSettings, SomeUser); const anotherSettings = { foo: "another" }; await stateProvider.setUserState(SettingsKey, anotherSettings, AnotherUser); - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const userId = new BehaviorSubject(SomeUser); const userId$ = userId.asObservable(); const results: any = []; @@ -730,7 +1026,15 @@ describe("CredentialGeneratorService", () => { it("errors when the arbitrary user's stream errors", async () => { await stateProvider.setUserState(SettingsKey, null, SomeUser); - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const userId = new BehaviorSubject(SomeUser); const userId$ = userId.asObservable(); let error = null; @@ -748,7 +1052,15 @@ describe("CredentialGeneratorService", () => { it("completes when the arbitrary user's stream completes", async () => { await stateProvider.setUserState(SettingsKey, null, SomeUser); - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const userId = new BehaviorSubject(SomeUser); const userId$ = userId.asObservable(); let completed = false; @@ -766,7 +1078,15 @@ describe("CredentialGeneratorService", () => { it("ignores repeated arbitrary user emissions", async () => { await stateProvider.setUserState(SettingsKey, null, SomeUser); - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const userId = new BehaviorSubject(SomeUser); const userId$ = userId.asObservable(); let count = 0; @@ -790,7 +1110,15 @@ describe("CredentialGeneratorService", () => { describe("settings", () => { it("writes to the user's state", async () => { const singleUserId$ = new BehaviorSubject(SomeUser).asObservable(); - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const subject = await generator.settings(SomeConfiguration, { singleUserId$ }); subject.next({ foo: "next value" }); @@ -803,7 +1131,15 @@ describe("CredentialGeneratorService", () => { it("waits for the user to become available", async () => { const singleUserId = new BehaviorSubject(null); const singleUserId$ = singleUserId.asObservable(); - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); let completed = false; const promise = generator.settings(SomeConfiguration, { singleUserId$ }).then((settings) => { @@ -821,7 +1157,15 @@ describe("CredentialGeneratorService", () => { describe("policy$", () => { it("creates constraints without policy in effect when there is no policy", async () => { - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const userId$ = new BehaviorSubject(SomeUser).asObservable(); const result = await firstValueFrom(generator.policy$(SomeConfiguration, { userId$ })); @@ -830,7 +1174,15 @@ describe("CredentialGeneratorService", () => { }); it("creates constraints with policy in effect when there is a policy", async () => { - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const userId$ = new BehaviorSubject(SomeUser).asObservable(); const policy$ = new BehaviorSubject([somePolicy]); policyService.getAll$.mockReturnValue(policy$); @@ -841,7 +1193,15 @@ describe("CredentialGeneratorService", () => { }); it("follows policy emissions", async () => { - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const userId = new BehaviorSubject(SomeUser); const userId$ = userId.asObservable(); const somePolicySubject = new BehaviorSubject([somePolicy]); @@ -862,7 +1222,15 @@ describe("CredentialGeneratorService", () => { }); it("follows user emissions", async () => { - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const userId = new BehaviorSubject(SomeUser); const userId$ = userId.asObservable(); const somePolicy$ = new BehaviorSubject([somePolicy]).asObservable(); @@ -884,7 +1252,15 @@ describe("CredentialGeneratorService", () => { }); it("errors when the user errors", async () => { - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const userId = new BehaviorSubject(SomeUser); const userId$ = userId.asObservable(); const expectedError = { some: "error" }; @@ -902,7 +1278,15 @@ describe("CredentialGeneratorService", () => { }); it("completes when the user completes", async () => { - const generator = new CredentialGeneratorService(randomizer, stateProvider, policyService); + const generator = new CredentialGeneratorService( + randomizer, + stateProvider, + policyService, + apiService, + i18nService, + encryptService, + keyService, + ); const userId = new BehaviorSubject(SomeUser); const userId$ = userId.asObservable(); diff --git a/libs/tools/generator/core/src/services/credential-generator.service.ts b/libs/tools/generator/core/src/services/credential-generator.service.ts index 693ffd654dc..04413ba2c0d 100644 --- a/libs/tools/generator/core/src/services/credential-generator.service.ts +++ b/libs/tools/generator/core/src/services/credential-generator.service.ts @@ -11,38 +11,60 @@ import { ignoreElements, map, Observable, - race, share, skipUntil, switchMap, takeUntil, + takeWhile, withLatestFrom, } from "rxjs"; import { Simplify } from "type-fest"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { StateProvider } from "@bitwarden/common/platform/state"; import { OnDependency, SingleUserDependency, + UserBound, UserDependency, } from "@bitwarden/common/tools/dependencies"; -import { isDynamic } from "@bitwarden/common/tools/state/state-constraints-dependency"; +import { IntegrationId, IntegrationMetadata } from "@bitwarden/common/tools/integration"; +import { RestClient } from "@bitwarden/common/tools/integration/rpc"; +import { anyComplete } from "@bitwarden/common/tools/rx"; +import { PaddedDataPacker } from "@bitwarden/common/tools/state/padded-data-packer"; +import { UserEncryptor } from "@bitwarden/common/tools/state/user-encryptor.abstraction"; +import { UserKeyEncryptor } from "@bitwarden/common/tools/state/user-key-encryptor"; import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject"; +import { UserId } from "@bitwarden/common/types/guid"; +import { KeyService } from "@bitwarden/key-management"; import { Randomizer } from "../abstractions"; -import { Generators } from "../data"; +import { + Generators, + getForwarderConfiguration, + Integrations, + toCredentialGeneratorConfiguration, +} from "../data"; import { availableAlgorithms } from "../policies/available-algorithms-policy"; import { mapPolicyToConstraints } from "../rx"; import { CredentialAlgorithm, CredentialCategories, CredentialCategory, - CredentialGeneratorInfo, + AlgorithmInfo, CredentialPreference, + isForwarderIntegration, + ForwarderIntegration, } from "../types"; -import { CredentialGeneratorConfiguration as Configuration } from "../types/credential-generator-configuration"; +import { + CredentialGeneratorConfiguration as Configuration, + CredentialGeneratorInfo, + GeneratorDependencyProvider, +} from "../types/credential-generator-configuration"; import { GeneratorConstraints } from "../types/generator-constraints"; import { PREFERENCES } from "./credential-preferences"; @@ -59,17 +81,33 @@ type Generate$Dependencies = Simplify & Partial; + + integration$?: Observable; }; type Algorithms$Dependencies = Partial; +const OPTIONS_FRAME_SIZE = 512; + export class CredentialGeneratorService { constructor( - private randomizer: Randomizer, - private stateProvider: StateProvider, - private policyService: PolicyService, + private readonly randomizer: Randomizer, + private readonly stateProvider: StateProvider, + private readonly policyService: PolicyService, + private readonly apiService: ApiService, + private readonly i18nService: I18nService, + private readonly encryptService: EncryptService, + private readonly keyService: KeyService, ) {} + private getDependencyProvider(): GeneratorDependencyProvider { + return { + client: new RestClient(this.apiService, this.i18nService), + i18nService: this.i18nService, + randomizer: this.randomizer, + }; + } + // FIXME: the rxjs methods of this service can be a lot more resilient if // `Subjects` are introduced where sharing occurs @@ -84,18 +122,13 @@ export class CredentialGeneratorService { dependencies?: Generate$Dependencies, ) { // instantiate the engine - const engine = configuration.engine.create(this.randomizer); + const engine = configuration.engine.create(this.getDependencyProvider()); // stream blocks until all of these values are received const website$ = dependencies?.website$ ?? new BehaviorSubject(null); const request$ = website$.pipe(map((website) => ({ website }))); const settings$ = this.settings$(configuration, dependencies); - // monitor completion - const requestComplete$ = request$.pipe(ignoreElements(), endWith(true)); - const settingsComplete$ = request$.pipe(ignoreElements(), endWith(true)); - const complete$ = race(requestComplete$, settingsComplete$); - // if on$ triggers before settings are loaded, trigger as soon // as they become available. let readyOn$: Observable = null; @@ -116,7 +149,7 @@ export class CredentialGeneratorService { const generate$ = (readyOn$ ?? settings$).pipe( withLatestFrom(request$, settings$), concatMap(([, request, settings]) => engine.generate(request, settings)), - takeUntil(complete$), + takeUntil(anyComplete([request$, settings$])), ); return generate$; @@ -132,11 +165,11 @@ export class CredentialGeneratorService { algorithms$( category: CredentialCategory, dependencies?: Algorithms$Dependencies, - ): Observable; + ): Observable; algorithms$( category: CredentialCategory[], dependencies?: Algorithms$Dependencies, - ): Observable; + ): Observable; algorithms$( category: CredentialCategory | CredentialCategory[], dependencies?: Algorithms$Dependencies, @@ -163,7 +196,9 @@ export class CredentialGeneratorService { return policies$; }), map((available) => { - const filtered = algorithms.filter((c) => available.has(c.id)); + const filtered = algorithms.filter( + (c) => isForwarderIntegration(c.id) || available.has(c.id), + ); return filtered; }), ); @@ -175,24 +210,80 @@ export class CredentialGeneratorService { * @param category the category or categories of interest * @returns A list containing the requested metadata. */ - algorithms(category: CredentialCategory): CredentialGeneratorInfo[]; - algorithms(category: CredentialCategory[]): CredentialGeneratorInfo[]; - algorithms(category: CredentialCategory | CredentialCategory[]): CredentialGeneratorInfo[] { - const categories = Array.isArray(category) ? category : [category]; + algorithms(category: CredentialCategory): AlgorithmInfo[]; + algorithms(category: CredentialCategory[]): AlgorithmInfo[]; + algorithms(category: CredentialCategory | CredentialCategory[]): AlgorithmInfo[] { + const categories: CredentialCategory[] = Array.isArray(category) ? category : [category]; + const algorithms = categories - .flatMap((c) => CredentialCategories[c]) - .map((c) => (c === "forwarder" ? null : Generators[c])) + .flatMap((c) => CredentialCategories[c] as CredentialAlgorithm[]) + .map((id) => this.algorithm(id)) .filter((info) => info !== null); - return algorithms; + const forwarders = Object.keys(Integrations) + .map((key: keyof typeof Integrations) => { + const forwarder: ForwarderIntegration = { forwarder: Integrations[key].id }; + return this.algorithm(forwarder); + }) + .filter((forwarder) => categories.includes(forwarder.category)); + + return algorithms.concat(forwarders); } /** Look up the metadata for a specific generator algorithm * @param id identifies the algorithm * @returns the requested metadata, or `null` if the metadata wasn't found. */ - algorithm(id: CredentialAlgorithm): CredentialGeneratorInfo { - return (id === "forwarder" ? null : Generators[id]) ?? null; + algorithm(id: CredentialAlgorithm): AlgorithmInfo { + let generator: CredentialGeneratorInfo = null; + let integration: IntegrationMetadata = null; + + if (isForwarderIntegration(id)) { + const forwarderConfig = getForwarderConfiguration(id.forwarder); + integration = forwarderConfig; + + if (forwarderConfig) { + generator = toCredentialGeneratorConfiguration(forwarderConfig); + } + } else { + generator = Generators[id]; + } + + if (!generator) { + throw new Error(`Invalid credential algorithm: ${JSON.stringify(id)}`); + } + + const info: AlgorithmInfo = { + id: generator.id, + category: generator.category, + name: integration ? integration.name : this.i18nService.t(generator.nameKey), + generate: this.i18nService.t(generator.generateKey), + generatedValue: this.i18nService.t(generator.generatedValueKey), + copy: this.i18nService.t(generator.copyKey), + onlyOnRequest: generator.onlyOnRequest, + request: generator.request, + }; + + if (generator.descriptionKey) { + info.description = this.i18nService.t(generator.descriptionKey); + } + + return info; + } + + private encryptor$(userId: UserId) { + const packer = new PaddedDataPacker(OPTIONS_FRAME_SIZE); + const encryptor$ = this.keyService.userKey$(userId).pipe( + // complete when the account locks + takeWhile((key) => !!key), + map((key) => { + const encryptor = new UserKeyEncryptor(userId, this.encryptService, key, packer); + + return { userId, encryptor } satisfies UserBound<"encryptor", UserEncryptor>; + }), + ); + + return encryptor$; } /** Get the settings for the provided configuration @@ -208,27 +299,21 @@ export class CredentialGeneratorService { dependencies?: Settings$Dependencies, ) { const userId$ = dependencies?.userId$ ?? this.stateProvider.activeUserId$; - const completion$ = userId$.pipe(ignoreElements(), endWith(true)); + const constraints$ = this.policy$(configuration, { userId$ }); - const state$ = userId$.pipe( + const settings$ = userId$.pipe( filter((userId) => !!userId), distinctUntilChanged(), switchMap((userId) => { - const state$ = this.stateProvider - .getUserState$(configuration.settings.account, userId) - .pipe(takeUntil(completion$)); - + const state$ = new UserStateSubject( + configuration.settings.account, + (key) => this.stateProvider.getUser(userId, key), + { constraints$, singleUserEncryptor$: this.encryptor$(userId) }, + ); return state$; }), map((settings) => settings ?? structuredClone(configuration.settings.initial)), - ); - - const settings$ = combineLatest([state$, this.policy$(configuration, { userId$ })]).pipe( - map(([settings, policy]) => { - const calibration = isDynamic(policy) ? policy.calibrate(settings) : policy; - const adjusted = calibration.adjust(settings); - return adjusted; - }), + takeUntil(anyComplete(userId$)), ); return settings$; @@ -251,8 +336,11 @@ export class CredentialGeneratorService { ); // FIXME: enforce policy - const state = this.stateProvider.getUser(userId, PREFERENCES); - const subject = new UserStateSubject(state, { ...dependencies }); + const subject = new UserStateSubject( + PREFERENCES, + (key) => this.stateProvider.getUser(userId, key), + { singleUserEncryptor$: this.encryptor$(userId) }, + ); return subject; } @@ -271,10 +359,14 @@ export class CredentialGeneratorService { const userId = await firstValueFrom( dependencies.singleUserId$.pipe(filter((userId) => !!userId)), ); - const state = this.stateProvider.getUser(userId, configuration.settings.account); + const constraints$ = this.policy$(configuration, { userId$: dependencies.singleUserId$ }); - const subject = new UserStateSubject(state, { ...dependencies, constraints$ }); + const subject = new UserStateSubject( + configuration.settings.account, + (key) => this.stateProvider.getUser(userId, key), + { constraints$, singleUserEncryptor$: this.encryptor$(userId) }, + ); return subject; } diff --git a/libs/tools/generator/core/src/strategies/forwarder-generator-strategy.spec.ts b/libs/tools/generator/core/src/strategies/forwarder-generator-strategy.spec.ts index 09f3ccd87a5..f57a1e5f2b6 100644 --- a/libs/tools/generator/core/src/strategies/forwarder-generator-strategy.spec.ts +++ b/libs/tools/generator/core/src/strategies/forwarder-generator-strategy.spec.ts @@ -5,13 +5,13 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; // FIXME: use index.ts imports once policy abstractions and models // implement ADR-0002 import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { RestClient } from "@bitwarden/common/tools/integration/rpc"; import { BufferedState } from "@bitwarden/common/tools/state/buffered-state"; import { UserId } from "@bitwarden/common/types/guid"; import { UserKey } from "@bitwarden/common/types/key"; +import { KeyService } from "@bitwarden/key-management"; import { FakeStateProvider, mockAccountServiceWith } from "../../../../../common/spec"; import { AddyIo, Fastmail, FirefoxRelay } from "../integration"; @@ -30,7 +30,7 @@ const SomePolicy = mock({ describe("ForwarderGeneratorStrategy", () => { const encryptService = mock(); - const keyService = mock(); + const keyService = mock(); const stateProvider = new FakeStateProvider(mockAccountServiceWith(SomeUser)); const restClient = mock(); const i18nService = mock(); diff --git a/libs/tools/generator/core/src/strategies/forwarder-generator-strategy.ts b/libs/tools/generator/core/src/strategies/forwarder-generator-strategy.ts index 04989cce196..9163d4c3a28 100644 --- a/libs/tools/generator/core/src/strategies/forwarder-generator-strategy.ts +++ b/libs/tools/generator/core/src/strategies/forwarder-generator-strategy.ts @@ -2,7 +2,6 @@ import { filter, map } from "rxjs"; import { Jsonify } from "type-fest"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SingleUserState, StateProvider } from "@bitwarden/common/platform/state"; @@ -17,6 +16,7 @@ import { SecretKeyDefinition } from "@bitwarden/common/tools/state/secret-key-de import { SecretState } from "@bitwarden/common/tools/state/secret-state"; import { UserKeyEncryptor } from "@bitwarden/common/tools/state/user-key-encryptor"; import { UserId } from "@bitwarden/common/types/guid"; +import { KeyService } from "@bitwarden/key-management"; import { GeneratorStrategy } from "../abstractions"; import { ForwarderConfiguration, AccountRequest, ForwarderContext } from "../engine"; @@ -45,7 +45,7 @@ export class ForwarderGeneratorStrategy< private client: RestClient, private i18nService: I18nService, private readonly encryptService: EncryptService, - private readonly keyService: CryptoService, + private readonly keyService: KeyService, private stateProvider: StateProvider, ) { super(); diff --git a/libs/tools/generator/core/src/strategies/passphrase-generator-strategy.spec.ts b/libs/tools/generator/core/src/strategies/passphrase-generator-strategy.spec.ts index 6591b179fc2..5abcea82493 100644 --- a/libs/tools/generator/core/src/strategies/passphrase-generator-strategy.spec.ts +++ b/libs/tools/generator/core/src/strategies/passphrase-generator-strategy.spec.ts @@ -98,7 +98,7 @@ describe("Passphrase generation strategy", () => { const strategy = new PassphraseGeneratorStrategy(randomizer, null); const result = await strategy.generate({ - numWords: 4, + numWords: 6, capitalize: true, includeNumber: true, wordSeparator: "!", @@ -106,7 +106,7 @@ describe("Passphrase generation strategy", () => { expect(result).toEqual("passphrase"); expect(randomizer.randomEffLongWords).toHaveBeenCalledWith({ - numberOfWords: 4, + numberOfWords: 6, capitalize: true, number: true, separator: "!", @@ -135,14 +135,14 @@ describe("Passphrase generation strategy", () => { const strategy = new PassphraseGeneratorStrategy(randomizer, null); const result = await strategy.generate({ - numWords: 4, + numWords: 6, includeNumber: true, wordSeparator: "!", }); expect(result).toEqual("passphrase"); expect(randomizer.randomEffLongWords).toHaveBeenCalledWith({ - numberOfWords: 4, + numberOfWords: 6, capitalize: DefaultPassphraseGenerationOptions.capitalize, number: true, separator: "!", @@ -153,14 +153,14 @@ describe("Passphrase generation strategy", () => { const strategy = new PassphraseGeneratorStrategy(randomizer, null); const result = await strategy.generate({ - numWords: 4, + numWords: 6, capitalize: true, wordSeparator: "!", }); expect(result).toEqual("passphrase"); expect(randomizer.randomEffLongWords).toHaveBeenCalledWith({ - numberOfWords: 4, + numberOfWords: 6, capitalize: true, number: DefaultPassphraseGenerationOptions.includeNumber, separator: "!", @@ -171,14 +171,14 @@ describe("Passphrase generation strategy", () => { const strategy = new PassphraseGeneratorStrategy(randomizer, null); const result = await strategy.generate({ - numWords: 4, + numWords: 6, capitalize: true, includeNumber: true, }); expect(result).toEqual("passphrase"); expect(randomizer.randomEffLongWords).toHaveBeenCalledWith({ - numberOfWords: 4, + numberOfWords: 6, capitalize: true, number: true, separator: DefaultPassphraseGenerationOptions.wordSeparator, diff --git a/libs/tools/generator/core/src/types/credential-generator-configuration.ts b/libs/tools/generator/core/src/types/credential-generator-configuration.ts index 8302450d443..22bfa31441e 100644 --- a/libs/tools/generator/core/src/types/credential-generator-configuration.ts +++ b/libs/tools/generator/core/src/types/credential-generator-configuration.ts @@ -1,4 +1,7 @@ +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { UserKeyDefinition } from "@bitwarden/common/platform/state"; +import { RestClient } from "@bitwarden/common/tools/integration/rpc"; +import { ObjectKey } from "@bitwarden/common/tools/state/object-key"; import { Constraints } from "@bitwarden/common/tools/types"; import { Randomizer } from "../abstractions"; @@ -6,30 +9,102 @@ import { CredentialAlgorithm, CredentialCategory, PolicyConfiguration } from ".. import { CredentialGenerator } from "./credential-generator"; +export type GeneratorDependencyProvider = { + randomizer: Randomizer; + client: RestClient; + i18nService: I18nService; +}; + +export type AlgorithmInfo = { + /** Uniquely identifies the credential configuration + * @example + * // Use `isForwarderIntegration(algorithm: CredentialAlgorithm)` + * // to pattern test whether the credential describes a forwarder algorithm + * const meta : CredentialGeneratorInfo = // ... + * const { forwarder } = isForwarderIntegration(meta.id) ? credentialId : {}; + */ + id: CredentialAlgorithm; + + /** The kind of credential generated by this configuration */ + category: CredentialCategory; + + /** Localized algorithm name */ + name: string; + + /* Localized generate button label */ + generate: string; + + /* Localized copy button label */ + copy: string; + + /* Localized generated value label */ + generatedValue: string; + + /** Localized algorithm description */ + description?: string; + + /** When true, credential generation must be explicitly requested. + * @remarks this property is useful when credential generation + * carries side effects, such as configuring a service external + * to Bitwarden. + */ + onlyOnRequest: boolean; + + /** Well-known fields to display on the options panel or collect from the environment. + * @remarks: at present, this is only used by forwarders + */ + request: readonly string[]; +}; + /** Credential generator metadata common across credential generators */ export type CredentialGeneratorInfo = { /** Uniquely identifies the credential configuration + * @example + * // Use `isForwarderIntegration(algorithm: CredentialAlgorithm)` + * // to pattern test whether the credential describes a forwarder algorithm + * const meta : CredentialGeneratorInfo = // ... + * const { forwarder } = isForwarderIntegration(meta.id) ? credentialId : {}; */ id: CredentialAlgorithm; /** The kind of credential generated by this configuration */ category: CredentialCategory; - /** Key used to localize the credential name in the I18nService */ + /** Localization key for the credential name */ nameKey: string; - /** Key used to localize the credential description in the I18nService */ + /** Localization key for the credential description*/ descriptionKey?: string; + /* Localization key for the generate command label */ + generateKey: string; + + /* Localization key for the copy button label */ + copyKey: string; + + /* Localization key for describing values generated by this generator */ + generatedValueKey: string; + /** When true, credential generation must be explicitly requested. * @remarks this property is useful when credential generation * carries side effects, such as configuring a service external * to Bitwarden. */ onlyOnRequest: boolean; + + /** Well-known fields to display on the options panel or collect from the environment. + * @remarks: at present, this is only used by forwarders + */ + request: readonly string[]; }; -/** Credential generator metadata that relies upon typed setting and policy definitions. */ +/** Credential generator metadata that relies upon typed setting and policy definitions. + * @example + * // Use `isForwarderIntegration(algorithm: CredentialAlgorithm)` + * // to pattern test whether the credential describes a forwarder algorithm + * const meta : CredentialGeneratorInfo = // ... + * const { forwarder } = isForwarderIntegration(meta.id) ? credentialId : {}; + */ export type CredentialGeneratorConfiguration = CredentialGeneratorInfo & { /** An algorithm that generates credentials when ran. */ engine: { @@ -40,7 +115,7 @@ export type CredentialGeneratorConfiguration = CredentialGener // the credential generator, but engine configurations should return // the underlying type. `create` may be able to do double-duty w/ an // engine definition if `CredentialGenerator` can be made covariant. - create: (randomizer: Randomizer) => CredentialGenerator; + create: (randomizer: GeneratorDependencyProvider) => CredentialGenerator; }; /** Defines the stored parameters for credential generation */ settings: { @@ -51,7 +126,10 @@ export type CredentialGeneratorConfiguration = CredentialGener constraints: Constraints; /** storage location for account-global settings */ - account: UserKeyDefinition; + account: UserKeyDefinition | ObjectKey; + + /** storage location for *plaintext* settings imports */ + import?: UserKeyDefinition | ObjectKey, Settings>; }; /** defines how to construct policy for this settings instance */ diff --git a/libs/tools/generator/core/src/types/generator-type.ts b/libs/tools/generator/core/src/types/generator-type.ts index 59727fb98f2..5b74d17fa4a 100644 --- a/libs/tools/generator/core/src/types/generator-type.ts +++ b/libs/tools/generator/core/src/types/generator-type.ts @@ -1,3 +1,5 @@ +import { IntegrationId } from "@bitwarden/common/tools/integration"; + import { EmailAlgorithms, PasswordAlgorithms, UsernameAlgorithms } from "../data/generator-types"; /** A type of password that may be generated by the credential generator. */ @@ -9,8 +11,31 @@ export type UsernameAlgorithm = (typeof UsernameAlgorithms)[number]; /** A type of email address that may be generated by the credential generator. */ export type EmailAlgorithm = (typeof EmailAlgorithms)[number]; +export type ForwarderIntegration = { forwarder: IntegrationId }; + +/** Returns true when the input algorithm is a forwarder integration. */ +export function isForwarderIntegration( + algorithm: CredentialAlgorithm, +): algorithm is ForwarderIntegration { + return algorithm && typeof algorithm === "object" && "forwarder" in algorithm; +} + +export function isSameAlgorithm(lhs: CredentialAlgorithm, rhs: CredentialAlgorithm) { + if (lhs === rhs) { + return true; + } else if (isForwarderIntegration(lhs) && isForwarderIntegration(rhs)) { + return lhs.forwarder === rhs.forwarder; + } else { + return false; + } +} + /** A type of credential that may be generated by the credential generator. */ -export type CredentialAlgorithm = PasswordAlgorithm | UsernameAlgorithm | EmailAlgorithm; +export type CredentialAlgorithm = + | PasswordAlgorithm + | UsernameAlgorithm + | EmailAlgorithm + | ForwarderIntegration; /** Compound credential types supported by the credential generator. */ export const CredentialCategories = Object.freeze({ @@ -21,7 +46,7 @@ export const CredentialCategories = Object.freeze({ username: UsernameAlgorithms as Readonly, /** Lists algorithms in the "email" credential category */ - email: EmailAlgorithms as Readonly, + email: EmailAlgorithms as Readonly<(EmailAlgorithm | ForwarderIntegration)[]>, }); /** Returns true when the input algorithm is a password algorithm. */ @@ -40,7 +65,7 @@ export function isUsernameAlgorithm( /** Returns true when the input algorithm is an email algorithm. */ export function isEmailAlgorithm(algorithm: CredentialAlgorithm): algorithm is EmailAlgorithm { - return EmailAlgorithms.includes(algorithm as any); + return EmailAlgorithms.includes(algorithm as any) || isForwarderIntegration(algorithm); } /** A type of compound credential that may be generated by the credential generator. */ diff --git a/libs/tools/generator/core/src/types/index.ts b/libs/tools/generator/core/src/types/index.ts index 884d9760078..48272cbf602 100644 --- a/libs/tools/generator/core/src/types/index.ts +++ b/libs/tools/generator/core/src/types/index.ts @@ -1,4 +1,4 @@ -import { CredentialAlgorithm, PasswordAlgorithm } from "./generator-type"; +import { EmailAlgorithm, PasswordAlgorithm, UsernameAlgorithm } from "./generator-type"; export * from "./boundary"; export * from "./catchall-generator-options"; @@ -22,7 +22,7 @@ export * from "./word-options"; /** Provided for backwards compatibility only. * @deprecated Use one of the Algorithm types instead. */ -export type GeneratorType = CredentialAlgorithm; +export type GeneratorType = PasswordAlgorithm | UsernameAlgorithm | EmailAlgorithm; /** Provided for backwards compatibility only. * @deprecated Use one of the Algorithm types instead. diff --git a/libs/tools/generator/core/src/util.spec.ts b/libs/tools/generator/core/src/util.spec.ts index 7ffd869535b..8ed95a9f268 100644 --- a/libs/tools/generator/core/src/util.spec.ts +++ b/libs/tools/generator/core/src/util.spec.ts @@ -350,14 +350,14 @@ describe("optionsToRandomAsciiRequest", () => { describe("optionsToEffWordListRequest", () => { it("should map options", async () => { const result = optionsToEffWordListRequest({ - numWords: 4, + numWords: 6, capitalize: true, includeNumber: true, wordSeparator: "!", }); expect(result).toEqual({ - numberOfWords: 4, + numberOfWords: 6, capitalize: true, number: true, separator: "!", @@ -381,13 +381,13 @@ describe("optionsToEffWordListRequest", () => { it("should default capitalize", async () => { const result = optionsToEffWordListRequest({ - numWords: 4, + numWords: 6, includeNumber: true, wordSeparator: "!", }); expect(result).toEqual({ - numberOfWords: 4, + numberOfWords: 6, capitalize: DefaultPassphraseGenerationOptions.capitalize, number: true, separator: "!", @@ -396,13 +396,13 @@ describe("optionsToEffWordListRequest", () => { it("should default includeNumber", async () => { const result = optionsToEffWordListRequest({ - numWords: 4, + numWords: 6, capitalize: true, wordSeparator: "!", }); expect(result).toEqual({ - numberOfWords: 4, + numberOfWords: 6, capitalize: true, number: DefaultPassphraseGenerationOptions.includeNumber, separator: "!", @@ -411,13 +411,13 @@ describe("optionsToEffWordListRequest", () => { it("should default wordSeparator", async () => { const result = optionsToEffWordListRequest({ - numWords: 4, + numWords: 6, capitalize: true, includeNumber: true, }); expect(result).toEqual({ - numberOfWords: 4, + numberOfWords: 6, capitalize: true, number: true, separator: DefaultPassphraseGenerationOptions.wordSeparator, diff --git a/libs/tools/generator/extensions/history/src/generated-credential.ts b/libs/tools/generator/extensions/history/src/generated-credential.ts index 59a9623bf7e..32efb752258 100644 --- a/libs/tools/generator/extensions/history/src/generated-credential.ts +++ b/libs/tools/generator/extensions/history/src/generated-credential.ts @@ -1,6 +1,6 @@ import { Jsonify } from "type-fest"; -import { GeneratorCategory } from "./options"; +import { CredentialAlgorithm } from "@bitwarden/generator-core"; /** A credential generation result */ export class GeneratedCredential { @@ -14,7 +14,7 @@ export class GeneratedCredential { */ constructor( readonly credential: string, - readonly category: GeneratorCategory, + readonly category: CredentialAlgorithm, generationDate: Date | number, ) { if (typeof generationDate === "number") { diff --git a/libs/tools/generator/extensions/history/src/generator-history.abstraction.ts b/libs/tools/generator/extensions/history/src/generator-history.abstraction.ts index 78144c3043d..06d8741b2e0 100644 --- a/libs/tools/generator/extensions/history/src/generator-history.abstraction.ts +++ b/libs/tools/generator/extensions/history/src/generator-history.abstraction.ts @@ -1,9 +1,9 @@ import { Observable } from "rxjs"; import { UserId } from "@bitwarden/common/types/guid"; +import { CredentialAlgorithm } from "@bitwarden/generator-core"; import { GeneratedCredential } from "./generated-credential"; -import { GeneratorCategory } from "./options"; /** Tracks the history of password generations. * Each user gets their own store. @@ -27,7 +27,7 @@ export abstract class GeneratorHistoryService { track: ( userId: UserId, credential: string, - category: GeneratorCategory, + category: CredentialAlgorithm, date?: Date, ) => Promise; diff --git a/libs/tools/generator/extensions/history/src/legacy-password-history-decryptor.ts b/libs/tools/generator/extensions/history/src/legacy-password-history-decryptor.ts index 5769d79da47..6a27ad476ae 100644 --- a/libs/tools/generator/extensions/history/src/legacy-password-history-decryptor.ts +++ b/libs/tools/generator/extensions/history/src/legacy-password-history-decryptor.ts @@ -1,7 +1,7 @@ -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { UserId } from "@bitwarden/common/types/guid"; +import { KeyService } from "@bitwarden/key-management"; import { GeneratedPasswordHistory } from "./generated-password-history"; @@ -9,13 +9,13 @@ import { GeneratedPasswordHistory } from "./generated-password-history"; export class LegacyPasswordHistoryDecryptor { constructor( private userId: UserId, - private cryptoService: CryptoService, + private keyService: KeyService, private encryptService: EncryptService, ) {} /** Decrypts a password history. */ async decrypt(history: GeneratedPasswordHistory[]): Promise { - const key = await this.cryptoService.getUserKey(this.userId); + const key = await this.keyService.getUserKey(this.userId); const promises = (history ?? []).map(async (item) => { const encrypted = new EncString(item.password); diff --git a/libs/tools/generator/extensions/history/src/local-generator-history.service.spec.ts b/libs/tools/generator/extensions/history/src/local-generator-history.service.spec.ts index 1fbc956bc59..3936b03acc9 100644 --- a/libs/tools/generator/extensions/history/src/local-generator-history.service.spec.ts +++ b/libs/tools/generator/extensions/history/src/local-generator-history.service.spec.ts @@ -1,13 +1,13 @@ import { mock } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { CsprngArray } from "@bitwarden/common/types/csprng"; import { UserId } from "@bitwarden/common/types/guid"; import { UserKey } from "@bitwarden/common/types/key"; +import { KeyService } from "@bitwarden/key-management"; import { FakeStateProvider, awaitAsync, mockAccountServiceWith } from "../../../../../common/spec"; @@ -18,7 +18,7 @@ const AnotherUser = "AnotherUser" as UserId; describe("LocalGeneratorHistoryService", () => { const encryptService = mock(); - const keyService = mock(); + const keyService = mock(); const userKey = new SymmetricCryptoKey(new Uint8Array(64) as CsprngArray) as UserKey; beforeEach(() => { diff --git a/libs/tools/generator/extensions/history/src/local-generator-history.service.ts b/libs/tools/generator/extensions/history/src/local-generator-history.service.ts index 2416c84b63d..7a5743f21b1 100644 --- a/libs/tools/generator/extensions/history/src/local-generator-history.service.ts +++ b/libs/tools/generator/extensions/history/src/local-generator-history.service.ts @@ -1,6 +1,5 @@ import { filter, map } from "rxjs"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { SingleUserState, StateProvider } from "@bitwarden/common/platform/state"; import { BufferedState } from "@bitwarden/common/tools/state/buffered-state"; @@ -8,12 +7,14 @@ import { PaddedDataPacker } from "@bitwarden/common/tools/state/padded-data-pack import { SecretState } from "@bitwarden/common/tools/state/secret-state"; import { UserKeyEncryptor } from "@bitwarden/common/tools/state/user-key-encryptor"; import { UserId } from "@bitwarden/common/types/guid"; +import { CredentialAlgorithm } from "@bitwarden/generator-core"; +import { KeyService } from "@bitwarden/key-management"; import { GeneratedCredential } from "./generated-credential"; import { GeneratorHistoryService } from "./generator-history.abstraction"; import { GENERATOR_HISTORY, GENERATOR_HISTORY_BUFFER } from "./key-definitions"; import { LegacyPasswordHistoryDecryptor } from "./legacy-password-history-decryptor"; -import { GeneratorCategory, HistoryServiceOptions } from "./options"; +import { HistoryServiceOptions } from "./options"; const OPTIONS_FRAME_SIZE = 2048; @@ -23,9 +24,9 @@ const OPTIONS_FRAME_SIZE = 2048; export class LocalGeneratorHistoryService extends GeneratorHistoryService { constructor( private readonly encryptService: EncryptService, - private readonly keyService: CryptoService, + private readonly keyService: KeyService, private readonly stateProvider: StateProvider, - private readonly options: HistoryServiceOptions = { maxTotal: 100 }, + private readonly options: HistoryServiceOptions = { maxTotal: 200 }, ) { super(); } @@ -33,7 +34,12 @@ export class LocalGeneratorHistoryService extends GeneratorHistoryService { private _credentialStates = new Map>(); /** {@link GeneratorHistoryService.track} */ - track = async (userId: UserId, credential: string, category: GeneratorCategory, date?: Date) => { + track = async ( + userId: UserId, + credential: string, + category: CredentialAlgorithm, + date?: Date, + ) => { const state = this.getCredentialState(userId); let result: GeneratedCredential = null; diff --git a/libs/tools/generator/extensions/legacy/src/create-legacy-password-generation-service.ts b/libs/tools/generator/extensions/legacy/src/create-legacy-password-generation-service.ts index 8ef14a3a9eb..a76fba9759e 100644 --- a/libs/tools/generator/extensions/legacy/src/create-legacy-password-generation-service.ts +++ b/libs/tools/generator/extensions/legacy/src/create-legacy-password-generation-service.ts @@ -1,28 +1,28 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { StateProvider } from "@bitwarden/common/platform/state"; import { engine, services, strategies } from "@bitwarden/generator-core"; import { LocalGeneratorHistoryService } from "@bitwarden/generator-history"; import { DefaultGeneratorNavigationService } from "@bitwarden/generator-navigation"; +import { KeyService } from "@bitwarden/key-management"; import { LegacyPasswordGenerationService } from "./legacy-password-generation.service"; import { PasswordGenerationServiceAbstraction } from "./password-generation.service.abstraction"; const { PassphraseGeneratorStrategy, PasswordGeneratorStrategy } = strategies; -const { CryptoServiceRandomizer, PasswordRandomizer } = engine; +const { KeyServiceRandomizer, PasswordRandomizer } = engine; const DefaultGeneratorService = services.DefaultGeneratorService; export function legacyPasswordGenerationServiceFactory( encryptService: EncryptService, - cryptoService: CryptoService, + keyService: KeyService, policyService: PolicyService, accountService: AccountService, stateProvider: StateProvider, ): PasswordGenerationServiceAbstraction { - const randomizer = new CryptoServiceRandomizer(cryptoService); + const randomizer = new KeyServiceRandomizer(keyService); const passwordRandomizer = new PasswordRandomizer(randomizer); const passwords = new DefaultGeneratorService( @@ -37,7 +37,7 @@ export function legacyPasswordGenerationServiceFactory( const navigation = new DefaultGeneratorNavigationService(stateProvider, policyService); - const history = new LocalGeneratorHistoryService(encryptService, cryptoService, stateProvider); + const history = new LocalGeneratorHistoryService(encryptService, keyService, stateProvider); return new LegacyPasswordGenerationService( accountService, diff --git a/libs/tools/generator/extensions/legacy/src/create-legacy-username-generation-service.ts b/libs/tools/generator/extensions/legacy/src/create-legacy-username-generation-service.ts index 8626ef81f90..1d8a36eeb05 100644 --- a/libs/tools/generator/extensions/legacy/src/create-legacy-username-generation-service.ts +++ b/libs/tools/generator/extensions/legacy/src/create-legacy-username-generation-service.ts @@ -1,18 +1,18 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { StateProvider } from "@bitwarden/common/platform/state"; import { RestClient } from "@bitwarden/common/tools/integration/rpc"; import { engine, services, strategies, Integrations } from "@bitwarden/generator-core"; import { DefaultGeneratorNavigationService } from "@bitwarden/generator-navigation"; +import { KeyService } from "@bitwarden/key-management"; import { LegacyUsernameGenerationService } from "./legacy-username-generation.service"; import { UsernameGenerationServiceAbstraction } from "./username-generation.service.abstraction"; -const { CryptoServiceRandomizer, UsernameRandomizer, EmailRandomizer, EmailCalculator } = engine; +const { KeyServiceRandomizer, UsernameRandomizer, EmailRandomizer, EmailCalculator } = engine; const DefaultGeneratorService = services.DefaultGeneratorService; const { CatchallGeneratorStrategy, @@ -24,13 +24,13 @@ const { export function legacyUsernameGenerationServiceFactory( apiService: ApiService, i18nService: I18nService, - cryptoService: CryptoService, + keyService: KeyService, encryptService: EncryptService, policyService: PolicyService, accountService: AccountService, stateProvider: StateProvider, ): UsernameGenerationServiceAbstraction { - const randomizer = new CryptoServiceRandomizer(cryptoService); + const randomizer = new KeyServiceRandomizer(keyService); const restClient = new RestClient(apiService, i18nService); const usernameRandomizer = new UsernameRandomizer(randomizer); const emailRandomizer = new EmailRandomizer(randomizer); @@ -57,7 +57,7 @@ export function legacyUsernameGenerationServiceFactory( restClient, i18nService, encryptService, - cryptoService, + keyService, stateProvider, ), policyService, @@ -69,7 +69,7 @@ export function legacyUsernameGenerationServiceFactory( restClient, i18nService, encryptService, - cryptoService, + keyService, stateProvider, ), policyService, @@ -81,7 +81,7 @@ export function legacyUsernameGenerationServiceFactory( restClient, i18nService, encryptService, - cryptoService, + keyService, stateProvider, ), policyService, @@ -93,7 +93,7 @@ export function legacyUsernameGenerationServiceFactory( restClient, i18nService, encryptService, - cryptoService, + keyService, stateProvider, ), policyService, @@ -105,7 +105,7 @@ export function legacyUsernameGenerationServiceFactory( restClient, i18nService, encryptService, - cryptoService, + keyService, stateProvider, ), policyService, @@ -117,7 +117,7 @@ export function legacyUsernameGenerationServiceFactory( restClient, i18nService, encryptService, - cryptoService, + keyService, stateProvider, ), policyService, diff --git a/libs/tools/generator/extensions/legacy/src/legacy-password-generation.service.spec.ts b/libs/tools/generator/extensions/legacy/src/legacy-password-generation.service.spec.ts index 21fbcb26273..d932d013199 100644 --- a/libs/tools/generator/extensions/legacy/src/legacy-password-generation.service.spec.ts +++ b/libs/tools/generator/extensions/legacy/src/legacy-password-generation.service.spec.ts @@ -348,7 +348,7 @@ describe("LegacyPasswordGenerationService", () => { const innerPassphrase = createPassphraseGenerator( {}, { - minNumberWords: 5, + minNumberWords: 6, capitalize: true, includeNumber: true, }, @@ -370,7 +370,7 @@ describe("LegacyPasswordGenerationService", () => { expect(result).toBe(options); expect(result).toMatchObject({ - numWords: 5, + numWords: 6, capitalize: true, includeNumber: true, }); diff --git a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html index 4da3466f708..322fea94e3a 100644 --- a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html +++ b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html @@ -12,8 +12,7 @@

{{ "additionalOptions" | i18n }}

> - {{ "password" | i18n }} - {{ "newPassword" | i18n }} + {{ (passwordRemoved ? "newPassword" : "password") | i18n }} - {{ "sendPasswordDescV2" | i18n }} + + {{ "sendPasswordDescV3" | i18n }} { + if (!this.originalSendView || !this.originalSendView.password) { + return; + } + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "removePassword" }, + content: { key: "removePasswordConfirmation" }, + type: "warning", + }); + + if (!confirmed) { + return false; + } + + this.passwordRemoved = true; + + await this.sendApiService.removePassword(this.originalSendView.id); + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("removedPassword"), + }); + + this.originalSendView.password = null; + this.sendOptionsForm.patchValue({ + password: null, + }); + this.sendOptionsForm.get("password")?.enable(); + }; + ngOnInit() { if (this.sendFormContainer.originalSendView) { this.sendOptionsForm.patchValue({ maxAccessCount: this.sendFormContainer.originalSendView.maxAccessCount, accessCount: this.sendFormContainer.originalSendView.accessCount, - password: null, + password: this.hasPassword ? "************" : null, // 12 masked characters as a placeholder hideEmail: this.sendFormContainer.originalSendView.hideEmail, notes: this.sendFormContainer.originalSendView.notes, }); } + if (this.hasPassword) { + this.sendOptionsForm.get("password")?.disable(); + } + if (!this.config.areSendsAllowed) { this.sendOptionsForm.disable(); } diff --git a/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.html b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.html index d4c253303bd..06b0f1a55df 100644 --- a/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.html +++ b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.html @@ -6,7 +6,7 @@

{{ "sendDetails" | i18n }}

{{ "name" | i18n }} - + {{ "sendDetails" | i18n }} {{ "sendLink" | i18n }} - + + + + +
+
+

{{ "noPasswordsInList" | i18n }}

+
diff --git a/libs/vault/src/components/password-history-view/password-history-view.component.spec.ts b/libs/vault/src/components/password-history-view/password-history-view.component.spec.ts new file mode 100644 index 00000000000..8772a245821 --- /dev/null +++ b/libs/vault/src/components/password-history-view/password-history-view.component.spec.ts @@ -0,0 +1,97 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; +import { BehaviorSubject } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { ColorPasswordModule, ItemModule, ToastService } from "@bitwarden/components"; +import { ColorPasswordComponent } from "@bitwarden/components/src/color-password/color-password.component"; + +import { PasswordHistoryViewComponent } from "./password-history-view.component"; + +describe("PasswordHistoryViewComponent", () => { + let component: PasswordHistoryViewComponent; + let fixture: ComponentFixture; + + const mockCipher = { + id: "122-333-444", + type: CipherType.Login, + organizationId: "222-444-555", + } as CipherView; + + const copyToClipboard = jest.fn(); + const showToast = jest.fn(); + const activeAccount$ = new BehaviorSubject<{ id: string }>({ id: "666-444-444" }); + const mockCipherService = { + get: jest.fn().mockResolvedValue({ decrypt: jest.fn().mockResolvedValue(mockCipher) }), + getKeyForCipherKeyDecryption: jest.fn().mockResolvedValue({}), + }; + + beforeEach(async () => { + mockCipherService.get.mockClear(); + mockCipherService.getKeyForCipherKeyDecryption.mockClear(); + copyToClipboard.mockClear(); + showToast.mockClear(); + + await TestBed.configureTestingModule({ + imports: [ItemModule, ColorPasswordModule, JslibModule], + providers: [ + { provide: WINDOW, useValue: window }, + { provide: CipherService, useValue: mockCipherService }, + { provide: PlatformUtilsService, useValue: { copyToClipboard } }, + { provide: AccountService, useValue: { activeAccount$ } }, + { provide: ToastService, useValue: { showToast } }, + { provide: I18nService, useValue: { t: (key: string) => key } }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(PasswordHistoryViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("renders no history text when history does not exist", () => { + expect(fixture.debugElement.nativeElement.textContent).toBe("noPasswordsInList"); + }); + + describe("history", () => { + const password1 = { password: "bad-password-1", lastUsedDate: new Date("09/13/2004") }; + const password2 = { password: "bad-password-2", lastUsedDate: new Date("02/01/2004") }; + + beforeEach(async () => { + mockCipher.passwordHistory = [password1, password2]; + + mockCipherService.get.mockResolvedValue({ decrypt: jest.fn().mockResolvedValue(mockCipher) }); + await component.ngOnInit(); + fixture.detectChanges(); + }); + + it("renders all passwords", () => { + const passwords = fixture.debugElement.queryAll(By.directive(ColorPasswordComponent)); + + expect(passwords.map((password) => password.componentInstance.password)).toEqual([ + "bad-password-1", + "bad-password-2", + ]); + }); + + it("copies a password", () => { + const copyButton = fixture.debugElement.query(By.css("button")); + + copyButton.nativeElement.click(); + + expect(copyToClipboard).toHaveBeenCalledWith("bad-password-1", { window: window }); + expect(showToast).toHaveBeenCalledWith({ + message: "passwordCopied", + title: "", + variant: "info", + }); + }); + }); +}); diff --git a/libs/vault/src/components/password-history-view/password-history-view.component.ts b/libs/vault/src/components/password-history-view/password-history-view.component.ts new file mode 100644 index 00000000000..5e858af7275 --- /dev/null +++ b/libs/vault/src/components/password-history-view/password-history-view.component.ts @@ -0,0 +1,77 @@ +import { CommonModule } from "@angular/common"; +import { OnInit, Inject, Component, Input } from "@angular/core"; +import { firstValueFrom, map } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { CipherId, UserId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { PasswordHistoryView } from "@bitwarden/common/vault/models/view/password-history.view"; +import { + ToastService, + ItemModule, + ColorPasswordModule, + IconButtonModule, +} from "@bitwarden/components"; + +@Component({ + selector: "vault-password-history-view", + templateUrl: "./password-history-view.component.html", + standalone: true, + imports: [CommonModule, ItemModule, ColorPasswordModule, IconButtonModule, JslibModule], +}) +export class PasswordHistoryViewComponent implements OnInit { + /** + * The ID of the cipher to display the password history for. + */ + @Input({ required: true }) cipherId: CipherId; + + /** The password history for the cipher. */ + history: PasswordHistoryView[] = []; + + constructor( + @Inject(WINDOW) private win: Window, + protected cipherService: CipherService, + protected platformUtilsService: PlatformUtilsService, + protected i18nService: I18nService, + protected accountService: AccountService, + protected toastService: ToastService, + ) {} + + async ngOnInit() { + await this.init(); + } + + /** Copies a password to the clipboard. */ + copy(password: string) { + const copyOptions = this.win != null ? { window: this.win } : undefined; + this.platformUtilsService.copyToClipboard(password, copyOptions); + this.toastService.showToast({ + variant: "info", + title: "", + message: this.i18nService.t("passwordCopied"), + }); + } + + /** Retrieve the password history for the given cipher */ + protected async init() { + const cipher = await this.cipherService.get(this.cipherId); + const activeAccount = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a: { id: string | undefined }) => a)), + ); + + if (!activeAccount?.id) { + throw new Error("Active account is not available."); + } + + const activeUserId = activeAccount.id as UserId; + const decCipher = await cipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), + ); + + this.history = decCipher.passwordHistory == null ? [] : decCipher.passwordHistory; + } +} diff --git a/libs/vault/src/components/password-reprompt.component.ts b/libs/vault/src/components/password-reprompt.component.ts index dcc20f7982d..3cbdfa1416a 100644 --- a/libs/vault/src/components/password-reprompt.component.ts +++ b/libs/vault/src/components/password-reprompt.component.ts @@ -3,7 +3,6 @@ import { Component } from "@angular/core"; import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { @@ -13,6 +12,7 @@ import { FormFieldModule, IconButtonModule, } from "@bitwarden/components"; +import { KeyService } from "@bitwarden/key-management"; /** * Used to verify the user's Master Password for the "Master Password Re-prompt" feature only. @@ -38,7 +38,7 @@ export class PasswordRepromptComponent { }); constructor( - protected cryptoService: CryptoService, + protected keyService: KeyService, protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService, protected formBuilder: FormBuilder, @@ -46,11 +46,11 @@ export class PasswordRepromptComponent { ) {} submit = async () => { - const storedMasterKey = await this.cryptoService.getOrDeriveMasterKey( + const storedMasterKey = await this.keyService.getOrDeriveMasterKey( this.formGroup.value.masterPassword, ); if ( - !(await this.cryptoService.compareAndUpdateKeyHash( + !(await this.keyService.compareAndUpdateKeyHash( this.formGroup.value.masterPassword, storedMasterKey, )) diff --git a/libs/vault/src/index.ts b/libs/vault/src/index.ts index d5841c7db06..f6a95281f81 100644 --- a/libs/vault/src/index.ts +++ b/libs/vault/src/index.ts @@ -12,5 +12,6 @@ export { } from "./components/assign-collections.component"; export { DownloadAttachmentComponent } from "./components/download-attachment/download-attachment.component"; +export { PasswordHistoryViewComponent } from "./components/password-history-view/password-history-view.component"; export * as VaultIcons from "./icons"; diff --git a/package-lock.json b/package-lock.json index 0457eb1cdc8..ccf6ea3d2a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@angular/platform-browser": "16.2.12", "@angular/platform-browser-dynamic": "16.2.12", "@angular/router": "16.2.12", - "@bitwarden/sdk-internal": "0.1.3", + "@bitwarden/sdk-internal": "0.1.6", "@electron/fuses": "1.8.0", "@koa/multer": "3.0.2", "@koa/router": "13.1.0", @@ -68,7 +68,7 @@ "qrious": "4.0.2", "rxjs": "7.8.1", "tabbable": "6.2.0", - "tldts": "6.1.52", + "tldts": "6.1.56", "utf-8-validate": "6.0.4", "zone.js": "0.13.3", "zxcvbn": "4.4.2" @@ -110,18 +110,18 @@ "@types/koa-json": "2.0.23", "@types/lowdb": "1.0.15", "@types/lunr": "2.3.7", - "@types/node": "20.16.11", + "@types/node": "20.17.1", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", "@types/node-ipc": "9.2.3", - "@types/papaparse": "5.3.14", + "@types/papaparse": "5.3.15", "@types/proper-lockfile": "4.1.4", "@types/retry": "0.12.5", "@types/zxcvbn": "4.4.5", "@typescript-eslint/eslint-plugin": "7.16.1", "@typescript-eslint/parser": "7.16.1", "@webcomponents/custom-elements": "1.6.0", - "@yao-pkg/pkg": "5.14.0", + "@yao-pkg/pkg": "5.16.1", "autoprefixer": "10.4.20", "babel-loader": "9.1.3", "base64-loader": "1.0.0", @@ -146,7 +146,6 @@ "eslint-plugin-storybook": "0.8.0", "eslint-plugin-tailwindcss": "3.17.4", "gulp": "4.0.2", - "gulp-filter": "9.0.1", "gulp-if": "3.0.0", "gulp-json-editor": "2.6.0", "gulp-replace": "1.1.4", @@ -155,6 +154,7 @@ "html-webpack-injector": "1.1.4", "html-webpack-plugin": "5.6.0", "husky": "9.1.4", + "jest-extended": "^4.0.2", "jest-junit": "16.0.0", "jest-mock-extended": "3.0.7", "jest-preset-angular": "14.1.1", @@ -225,7 +225,7 @@ "papaparse": "5.4.1", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", - "tldts": "6.1.52", + "tldts": "6.1.56", "zxcvbn": "4.4.2" }, "bin": { @@ -234,7 +234,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2024.10.1", + "version": "2024.10.3", "hasInstallScript": true, "license": "GPL-3.0" }, @@ -248,7 +248,7 @@ }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2024.10.2" + "version": "2024.10.4" }, "libs/admin-console": { "name": "@bitwarden/admin-console", @@ -4696,10 +4696,9 @@ "link": true }, "node_modules/@bitwarden/sdk-internal": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.1.3.tgz", - "integrity": "sha512-zk9DyYMjylVLdljeLn3OLBcD939Hg/qMNJ2FxbyjiSKtcOcgglXgYmbcS01NRFFfM9REbn+j+2fWbQo6N+8SHw==", - "license": "SEE LICENSE IN LICENSE" + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.1.6.tgz", + "integrity": "sha512-YUOOcXnK004mAwE+vfy7AgeLYCtTyafYaXEWED3PNRaSun/a5elrAD//h2yuF9u8Dn5jg1VDkssMPpuG9+2VxA==" }, "node_modules/@bitwarden/vault": { "resolved": "libs/vault", @@ -9643,9 +9642,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.16.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz", - "integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==", + "version": "20.17.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.1.tgz", + "integrity": "sha512-j2VlPv1NnwPJbaCNv69FO/1z4lId0QmGvpT41YxitRtWlg96g/j8qcv2RKsLKe2F6OJgyXhupN1Xo17b2m139Q==", "dev": true, "license": "MIT", "dependencies": { @@ -9699,9 +9698,9 @@ } }, "node_modules/@types/papaparse": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.14.tgz", - "integrity": "sha512-LxJ4iEFcpqc6METwp9f6BV6VVc43m6MfH0VqFosHvrUgfXiFe6ww7R3itkOQ+TCK6Y+Iv/+RnnvtRZnkc5Kc9g==", + "version": "5.3.15", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.15.tgz", + "integrity": "sha512-JHe6vF6x/8Z85nCX4yFdDslN11d+1pr12E526X8WAfhadOeaOTx5AuIkvDKIBopfvlzpzkdMx4YyvSKCM9oqtw==", "dev": true, "license": "MIT", "dependencies": { @@ -11070,42 +11069,40 @@ "license": "Apache-2.0" }, "node_modules/@yao-pkg/pkg": { - "version": "5.14.0", - "resolved": "https://registry.npmjs.org/@yao-pkg/pkg/-/pkg-5.14.0.tgz", - "integrity": "sha512-34oflUyAOI64a4cc4AF3ckvS8Qqnk/ISvZ1bDBa1/JAYaaFtzAO+RlhPaU+wCHzhk6VXvZwEywJpb+SlVDTgdA==", + "version": "5.16.1", + "resolved": "https://registry.npmjs.org/@yao-pkg/pkg/-/pkg-5.16.1.tgz", + "integrity": "sha512-crUlnNFSReFNFuXDc4f3X2ignkFlc9kmEG7Bp/mJMA1jYyqR0lqjZGLgrSDYTYiNsYud8AzgA3RY1DrMdcUZWg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/generator": "7.23.0", - "@babel/parser": "7.23.0", - "@babel/types": "7.23.0", - "@yao-pkg/pkg-fetch": "3.5.11", - "chalk": "^4.1.2", - "fs-extra": "^9.1.0", - "globby": "^11.1.0", + "@babel/generator": "^7.23.0", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", + "@yao-pkg/pkg-fetch": "3.5.16", "into-stream": "^6.0.0", - "minimatch": "9.0.4", "minimist": "^1.2.6", "multistream": "^4.1.0", - "prebuild-install": "7.1.1", + "picocolors": "^1.1.0", + "picomatch": "^4.0.2", + "prebuild-install": "^7.1.1", "resolve": "^1.22.0", - "stream-meter": "^1.0.4" + "stream-meter": "^1.0.4", + "tinyglobby": "^0.2.9" }, "bin": { "pkg": "lib-es5/bin.js" } }, "node_modules/@yao-pkg/pkg-fetch": { - "version": "3.5.11", - "resolved": "https://registry.npmjs.org/@yao-pkg/pkg-fetch/-/pkg-fetch-3.5.11.tgz", - "integrity": "sha512-2tQ/1n7BLTptW6lL0pfTCnVMIxls8Jiw0/ClK1J2Fja9z2S2j4uzNL5dwGRqtvPJPn/q9i8X+Y+c4dwnMb+NOA==", + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@yao-pkg/pkg-fetch/-/pkg-fetch-3.5.16.tgz", + "integrity": "sha512-mCnZvZz0/Ylpk4TGyt34pqWJyBGYJM8c3dPoMRV8Knodv2QhcYS4iXb5kB/JNWkrRtCKukGZIKkMLXZ3TQlzPg==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "^4.1.2", - "fs-extra": "^9.1.0", "https-proxy-agent": "^5.0.0", "node-fetch": "^2.6.6", + "picocolors": "^1.1.0", "progress": "^2.0.3", "semver": "^7.3.5", "tar-fs": "^2.1.1", @@ -11127,22 +11124,6 @@ "wrap-ansi": "^7.0.0" } }, - "node_modules/@yao-pkg/pkg-fetch/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@yao-pkg/pkg-fetch/node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -11205,79 +11186,45 @@ } }, "node_modules/@yao-pkg/pkg/node_modules/@babel/generator": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", - "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.7.tgz", + "integrity": "sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.23.0", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" + "@babel/types": "^7.25.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@yao-pkg/pkg/node_modules/@babel/parser": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", - "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "node_modules/@yao-pkg/pkg/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", "dev": true, "license": "MIT", "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@yao-pkg/pkg/node_modules/@babel/types": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", - "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "jsesc": "bin/jsesc" }, "engines": { - "node": ">=6.9.0" + "node": ">=6" } }, - "node_modules/@yao-pkg/pkg/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "node_modules/@yao-pkg/pkg/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@yao-pkg/pkg/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/@yarnpkg/fslib": { @@ -12147,19 +12094,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-differ": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-4.0.0.tgz", - "integrity": "sha512-Q6VPTLMsmXZ47ENG3V+wQyZS1ZxXMxFyYzA+Z/GMrJ6yIutAIEf9wTyroTzmGjNfox9/h3GdGBCVh43GVFx4Uw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/array-each": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", @@ -20885,47 +20819,6 @@ "object.assign": "^4.1.0" } }, - "node_modules/gulp-filter": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/gulp-filter/-/gulp-filter-9.0.1.tgz", - "integrity": "sha512-knVYL8h9bfYIeft3VokVTkuaWJkQJMrFCS3yVjZQC6BGg+1dZFoeUY++B9D2X4eFpeNTx9StWK0qnDby3NO3PA==", - "dev": true, - "license": "MIT", - "dependencies": { - "multimatch": "^7.0.0", - "plugin-error": "^2.0.1", - "slash": "^5.1.0", - "streamfilter": "^3.0.0", - "to-absolute-glob": "^3.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - }, - "peerDependencies": { - "gulp": ">=4" - }, - "peerDependenciesMeta": { - "gulp": { - "optional": true - } - } - }, - "node_modules/gulp-filter/node_modules/slash": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", - "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/gulp-if": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/gulp-if/-/gulp-if-3.0.0.tgz", @@ -23986,6 +23879,27 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-extended": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jest-extended/-/jest-extended-4.0.2.tgz", + "integrity": "sha512-FH7aaPgtGYHc9mRjriS0ZEHYM5/W69tLrFTIdzm+yJgeoCmmrSB/luSfMSqWP9O29QWHPEmJ4qmU6EwsZideog==", + "dev": true, + "dependencies": { + "jest-diff": "^29.0.0", + "jest-get-type": "^29.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "jest": ">=27.2.5" + }, + "peerDependenciesMeta": { + "jest": { + "optional": true + } + } + }, "node_modules/jest-get-type": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", @@ -28423,37 +28337,6 @@ "multicast-dns": "cli.js" } }, - "node_modules/multimatch": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-7.0.0.tgz", - "integrity": "sha512-SYU3HBAdF4psHEL/+jXDKHO95/m5P2RvboHT2Y0WtTttvJLP4H/2WS9WlQPFvF6C8d6SpLw8vjCnQOnVIVOSJQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-differ": "^4.0.0", - "array-union": "^3.0.1", - "minimatch": "^9.0.3" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/multimatch/node_modules/array-union": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-3.0.1.tgz", - "integrity": "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/multistream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/multistream/-/multistream-4.1.0.tgz", @@ -35368,19 +35251,6 @@ "dev": true, "license": "MIT" }, - "node_modules/streamfilter": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/streamfilter/-/streamfilter-3.0.0.tgz", - "integrity": "sha512-kvKNfXCmUyC8lAXSSHCIXBUlo/lhsLcCU/OmzACZYpRUdtKIH68xYhm/+HI15jFJYtNJGYtCgn2wmIiExY1VwA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readable-stream": "^3.0.6" - }, - "engines": { - "node": ">=8.12.0" - } - }, "node_modules/streaming-json-stringify": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/streaming-json-stringify/-/streaming-json-stringify-3.1.0.tgz", @@ -36506,6 +36376,48 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyglobby": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.9.tgz", + "integrity": "sha512-8or1+BGEdk1Zkkw2ii16qSS7uVrQJPre5A9o/XkWPATkk23FZh/15BKFxPnlTy6vkljZxLqYCzzBMj30ZrSvjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.0", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", + "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tinyspy": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", @@ -36517,21 +36429,21 @@ } }, "node_modules/tldts": { - "version": "6.1.52", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.52.tgz", - "integrity": "sha512-fgrDJXDjbAverY6XnIt0lNfv8A0cf7maTEaZxNykLGsLG7XP+5xhjBTrt/ieAsFjAlZ+G5nmXomLcZDkxXnDzw==", + "version": "6.1.56", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.56.tgz", + "integrity": "sha512-2PT1oRZCxtsbLi5R2SQjE/v4vvgRggAtVcYj+3Rrcnu2nPZvu7m64+gDa/EsVSWd3QzEc0U0xN+rbEKsJC47kA==", "license": "MIT", "dependencies": { - "tldts-core": "^6.1.52" + "tldts-core": "^6.1.56" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "6.1.52", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.52.tgz", - "integrity": "sha512-j4OxQI5rc1Ve/4m/9o2WhWSC4jGc4uVbCINdOEJRAraCi0YqTqgMcxUx7DbmuP0G3PCixoof/RZB0Q5Kh9tagw==", + "version": "6.1.56", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.56.tgz", + "integrity": "sha512-Ihxv/Bwiyj73icTYVgBUkQ3wstlCglLoegSgl64oSrGUBX1hc7Qmf/CnrnJLaQdZrCnTaLqMYOwKMKlkfkFrxQ==", "license": "MIT" }, "node_modules/tmp": { @@ -36574,20 +36486,6 @@ "license": "BSD-3-Clause", "peer": true }, - "node_modules/to-absolute-glob": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-3.0.0.tgz", - "integrity": "sha512-loO/XEWTRqpfcpI7+Jr2RR2Umaaozx1t6OSVWtMi0oy5F/Fxg3IC+D/TToDnxyAGs7uZBGT/6XmyDUxgsObJXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-absolute": "^1.0.0", - "is-negated-glob": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", diff --git a/package.json b/package.json index 402b7c482d9..bf83d37b048 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "storybook": "ng run components:storybook", "build-storybook": "ng run components:build-storybook", "build-storybook:ci": "ng run components:build-storybook --webpack-stats-json", - "postinstall": "patch-package" + "postinstall": "patch-package && rimraf ./node_modules/@types/glob && rimraf ./node_modules/@types/minimatch" }, "workspaces": [ "apps/*", @@ -71,18 +71,18 @@ "@types/koa-json": "2.0.23", "@types/lowdb": "1.0.15", "@types/lunr": "2.3.7", - "@types/node": "20.16.11", + "@types/node": "20.17.1", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", "@types/node-ipc": "9.2.3", - "@types/papaparse": "5.3.14", + "@types/papaparse": "5.3.15", "@types/proper-lockfile": "4.1.4", "@types/retry": "0.12.5", "@types/zxcvbn": "4.4.5", "@typescript-eslint/eslint-plugin": "7.16.1", "@typescript-eslint/parser": "7.16.1", "@webcomponents/custom-elements": "1.6.0", - "@yao-pkg/pkg": "5.14.0", + "@yao-pkg/pkg": "5.16.1", "autoprefixer": "10.4.20", "babel-loader": "9.1.3", "base64-loader": "1.0.0", @@ -107,7 +107,6 @@ "eslint-plugin-storybook": "0.8.0", "eslint-plugin-tailwindcss": "3.17.4", "gulp": "4.0.2", - "gulp-filter": "9.0.1", "gulp-if": "3.0.0", "gulp-json-editor": "2.6.0", "gulp-replace": "1.1.4", @@ -116,6 +115,7 @@ "html-webpack-injector": "1.1.4", "html-webpack-plugin": "5.6.0", "husky": "9.1.4", + "jest-extended": "^4.0.2", "jest-junit": "16.0.0", "jest-mock-extended": "3.0.7", "jest-preset-angular": "14.1.1", @@ -158,7 +158,7 @@ "@angular/platform-browser": "16.2.12", "@angular/platform-browser-dynamic": "16.2.12", "@angular/router": "16.2.12", - "@bitwarden/sdk-internal": "0.1.3", + "@bitwarden/sdk-internal": "0.1.6", "@electron/fuses": "1.8.0", "@koa/multer": "3.0.2", "@koa/router": "13.1.0", @@ -202,7 +202,7 @@ "qrious": "4.0.2", "rxjs": "7.8.1", "tabbable": "6.2.0", - "tldts": "6.1.52", + "tldts": "6.1.56", "utf-8-validate": "6.0.4", "zone.js": "0.13.3", "zxcvbn": "4.4.2" @@ -212,11 +212,7 @@ "@storybook/angular": { "zone.js": "$zone.js" }, - "replacestream": "4.0.3", - "@types/minimatch": "3.0.5", - "@electron/asar": { - "@types/glob": "7.1.3" - } + "replacestream": "4.0.3" }, "lint-staged": { "*": "prettier --cache --ignore-unknown --write",