diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3faae48627b..7d216ac257e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -71,6 +71,7 @@ apps/web/src/app/platform @bitwarden/team-platform-dev libs/angular/src/platform @bitwarden/team-platform-dev libs/common/src/platform @bitwarden/team-platform-dev libs/common/spec @bitwarden/team-platform-dev +libs/common/src/state-migrations @bitwarden/team-platform-dev # Node-specifc platform files libs/node @bitwarden/team-platform-dev # Web utils used across app and connectors diff --git a/.storybook/manager.js b/.storybook/manager.js new file mode 100644 index 00000000000..89a69bf9421 --- /dev/null +++ b/.storybook/manager.js @@ -0,0 +1,63 @@ +import { addons } from "@storybook/addons"; +import { create } from "@storybook/theming/create"; + +const lightTheme = create({ + base: "light", + //logo and Title + brandTitle: "Bitwarden Component Library", + brandUrl: "/", + brandImage: + "https://github.com/bitwarden/brand/blob/51942f8d6e55e96a078a524e0f739efbf1997bcf/logos/logo-horizontal-blue.png?raw=true", + brandTarget: "_self", + + //Colors + colorPrimary: "#6D757E", + colorSecondary: "#175DDC", + + // UI + appBg: "#f9fBff", + appContentBg: "#ffffff", + appBorderColor: "#CED4DC", + + // Text colors + textColor: "#212529", + textInverseColor: "#ffffff", + + // Toolbar default and active colors + barTextColor: "#6D757E", + barSelectedColor: "#175DDC", + barBg: "#ffffff", + + // Form colors + inputBg: "#ffffff", + inputBorder: "#6D757E", + inputTextColor: "#6D757E", +}); + +const darkTheme = create({ + base: "dark", + + //logo and Title + brandTitle: "Bitwarden Component Library", + brandUrl: "/", + brandImage: + "https://github.com/bitwarden/brand/blob/51942f8d6e55e96a078a524e0f739efbf1997bcf/logos/logo-horizontal-white.png?raw=true", + brandTarget: "_self", + + //Colors + colorSecondary: "#6A99F0", + barSelectedColor: "#6A99F0", +}); + +export const getPreferredColorScheme = () => { + if (!globalThis || !globalThis.matchMedia) return "light"; + + const isDarkThemePreferred = globalThis.matchMedia("(prefers-color-scheme: dark)").matches; + if (isDarkThemePreferred) return "dark"; + + return "light"; +}; + +addons.setConfig({ + theme: getPreferredColorScheme() === "dark" ? darkTheme : lightTheme, +}); diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 096ba374f2f..7278c61d448 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -92,13 +92,13 @@ "message": "Аўтазапаўненне" }, "autoFillLogin": { - "message": "Auto-fill login" + "message": "Аўтазапаўненне лагіна" }, "autoFillCard": { - "message": "Auto-fill card" + "message": "Аўтазапаўненне карткі" }, "autoFillIdentity": { - "message": "Auto-fill identity" + "message": "Аўтазапаўненне асабістых даных" }, "generatePasswordCopied": { "message": "Генерыраваць пароль (з капіяваннем)" @@ -110,13 +110,13 @@ "message": "Няма адпаведных лагінаў." }, "noCards": { - "message": "No cards" + "message": "Няма картак" }, "noIdentities": { - "message": "No identities" + "message": "Няма прыватных даных" }, "addLoginMenu": { - "message": "Add login" + "message": "Дадаць лагін" }, "addCardMenu": { "message": "Add card" diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 216d248d62c..dfa2260546c 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -442,7 +442,7 @@ "message": "При заключване на системата" }, "onRestart": { - "message": "При повторно пускане на четеца" + "message": "При повторно пускане на браузъра" }, "never": { "message": "Никога" @@ -2191,13 +2191,13 @@ "message": "Вписването е стартирано" }, "exposedMasterPassword": { - "message": "Exposed Master Password" + "message": "Разобличена главна парола" }, "exposedMasterPasswordDesc": { "message": "Паролата е намерена в пробив на данни. Използвайте уникална парола, за да защитите вашия акаунт. Наистина ли искате да използвате слаба парола?" }, "weakAndExposedMasterPassword": { - "message": "Weak and Exposed Master Password" + "message": "Слаба и разобличена главна парола" }, "weakAndBreachedMasterPasswordDesc": { "message": "Разпозната е слаба парола, която присъства в известен случай на изтекли данни. Използвайте сложна и уникална парола, за да защитите данните си. Наистина ли искате да използвате тази парола?" @@ -2575,7 +2575,7 @@ "message": "Секретният ключ няма да бъде копиран в клонирания елемент. Искате ли да продължите с клонирането на елемента?" }, "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { - "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." + "message": "Изисква се проверка от иницииращия сайт. Тази функция все още не е внедрена за акаунти без главна парола." }, "logInWithPasskey": { "message": "Вписване със секретен ключ?" diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index 66a91b70fe5..6d321b41904 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -92,13 +92,13 @@ "message": "Auto-ispuna" }, "autoFillLogin": { - "message": "Auto-fill login" + "message": "Auto-ispuna prijave" }, "autoFillCard": { - "message": "Auto-fill card" + "message": "Auto-ispuna kartice" }, "autoFillIdentity": { - "message": "Auto-fill identity" + "message": "Auto-ispuna identiteta" }, "generatePasswordCopied": { "message": "Generiraj lozinku (i kopiraj)" @@ -110,19 +110,19 @@ "message": "Nema podudarajućih prijava" }, "noCards": { - "message": "No cards" + "message": "Nema kartica" }, "noIdentities": { - "message": "No identities" + "message": "Nema identiteta" }, "addLoginMenu": { - "message": "Add login" + "message": "Dodaj prijavu" }, "addCardMenu": { - "message": "Add card" + "message": "Dodaj karticu" }, "addIdentityMenu": { - "message": "Add identity" + "message": "Dodaj identitet" }, "unlockVaultMenu": { "message": "Otključaj svoj trezor" @@ -1254,7 +1254,7 @@ "message": "Identitet" }, "typePasskey": { - "message": "Passkey" + "message": "Pristupni ključ" }, "passwordHistory": { "message": "Povijest" @@ -1657,7 +1657,7 @@ "message": "Pravila organizacije utječu na tvoje mogućnosti vlasništva. " }, "personalOwnershipPolicyInEffectImports": { - "message": "An organization policy has blocked importing items into your individual vault." + "message": "Organizacijsko pravilo onemogućuje uvoz stavki u tvoj osobni trezor." }, "excludedDomains": { "message": "Izuzete domene" @@ -1926,11 +1926,11 @@ "message": "Odaberi mapu..." }, "orgPermissionsUpdatedMustSetPassword": { - "message": "Your organization permissions were updated, requiring you to set a master password.", + "message": "Moraš postaviti glavnu lozinku jer su dopuštenja tvoje organizacije ažurirana.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "orgRequiresYouToSetPassword": { - "message": "Your organization requires you to set a master password.", + "message": "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" }, "hours": { @@ -2458,26 +2458,26 @@ "description": "Message appearing below the autofill on load message when master password reprompt is set for a vault item." }, "importData": { - "message": "Import data", + "message": "Uvezi podatke", "description": "Used for the header of the import dialog, the import button and within the file-password-prompt" }, "importError": { - "message": "Import error" + "message": "Greška prilikom uvoza" }, "importErrorDesc": { - "message": "There was a problem with the data you tried to import. Please resolve the errors listed below in your source file and try again." + "message": "Postoji problem s podacima za uvoz. Potrebno je razriješiti doljenavedene greške u izvornoj datoteci i pokušati ponovno." }, "resolveTheErrorsBelowAndTryAgain": { - "message": "Resolve the errors below and try again." + "message": "Popravi navedene greške i pokušaj ponovo." }, "description": { - "message": "Description" + "message": "Opis" }, "importSuccess": { - "message": "Data successfully imported" + "message": "Uvoz podataka u trezor je uspio" }, "importSuccessNumberOfItems": { - "message": "A total of $AMOUNT$ items were imported.", + "message": "Ukupno je uvezeno $AMOUNT$ stavaka.", "placeholders": { "amount": { "content": "$1", @@ -2486,10 +2486,10 @@ } }, "total": { - "message": "Total" + "message": "Ukupno" }, "importWarning": { - "message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organization. Do you want to proceed?", + "message": "Uvoziš podatke u $ORGANIZATION$. Tvoji podaci možda će biti podijeljeni s članovima ove organizacije. Želiš li svejedno uvesti podatke?", "placeholders": { "organization": { "content": "$1", @@ -2498,31 +2498,31 @@ } }, "importFormatError": { - "message": "Data is not formatted correctly. Please check your import file and try again." + "message": "Podaci nisu ispravno formatirani. Provjeri uvoznu datoteku i pokušaj ponovno." }, "importNothingError": { - "message": "Nothing was imported." + "message": "Ništa nije uvezeno." }, "importEncKeyError": { - "message": "Error decrypting the exported file. Your encryption key does not match the encryption key used export the data." + "message": "Greška u dešifriranju izvozne datoteke. Ovaj ključ za šifriranje ne odgovara ključu za šifriranje korištenom pri izvozu datoteke." }, "invalidFilePassword": { - "message": "Invalid file password, please use the password you entered when you created the export file." + "message": "Nesipravna lozinka datoteke. Unesi lozinku izvozne datoteke." }, "importDestination": { - "message": "Import destination" + "message": "Odredište uvoza" }, "learnAboutImportOptions": { - "message": "Learn about your import options" + "message": "Više o mogućnostima uvoza" }, "selectImportFolder": { - "message": "Select a folder" + "message": "Odaberi mapu" }, "selectImportCollection": { - "message": "Select a collection" + "message": "Odaberi zbirku" }, "importTargetHint": { - "message": "Select this option if you want the imported file contents moved to a $DESTINATION$", + "message": "Odaberi ovu opciju ako sadržaj uvezene datoteke želiš spremiti u $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": { @@ -2532,25 +2532,25 @@ } }, "importUnassignedItemsError": { - "message": "File contains unassigned items." + "message": "Datoteka sadrži nedodijeljene stavke." }, "selectFormat": { - "message": "Select the format of the import file" + "message": "Odaberi format datoteke za uvoz" }, "selectImportFile": { - "message": "Select the import file" + "message": "Odaberi datoteku za uvoz" }, "chooseFile": { - "message": "Choose File" + "message": "Odaberi datoteku" }, "noFileChosen": { - "message": "No file chosen" + "message": "Nije odabrana datoteka" }, "orCopyPasteFileContents": { - "message": "or copy/paste the import file contents" + "message": "ili kopiraj/zalijepi sadržaj uvozne datoteke" }, "instructionsFor": { - "message": "$NAME$ Instructions", + "message": "$NAME$ upute", "description": "The title for the import tool instructions.", "placeholders": { "name": { @@ -2560,127 +2560,127 @@ } }, "confirmVaultImport": { - "message": "Confirm vault import" + "message": "Potvrdi uvoz trezora" }, "confirmVaultImportDesc": { - "message": "This file is password-protected. Please enter the file password to import data." + "message": "Ova je datoteka zaštićena lozinkom. Unesi lozinku za nastavak uvoza." }, "confirmFilePassword": { - "message": "Confirm file password" + "message": "Potvrdi lozinku datoteke" }, "passkeyNotCopied": { - "message": "Passkey will not be copied" + "message": "Pristupni ključ neće biti kopiran" }, "passkeyNotCopiedAlert": { - "message": "The passkey will not be copied to the cloned item. Do you want to continue cloning this item?" + "message": "Pristupni ključ se neće kopirati u kloniranu stavku. Želiš li nastaviti klonirati ovu stavku?" }, "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { - "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." + "message": "Ishodišna stranica zahtijeva verifikaciju. Ova značajka još nije implementirana za račune bez glavne lozinke." }, "logInWithPasskey": { - "message": "Log in with passkey?" + "message": "Prijava pristupnim ključem?" }, "passkeyAlreadyExists": { - "message": "A passkey already exists for this application." + "message": "Za ovu aplikaciju već postoji pristupni ključ." }, "noPasskeysFoundForThisApplication": { - "message": "No passkeys found for this application." + "message": "Za ovu aplikaciju nema pristupnih ključeva." }, "noMatchingPasskeyLogin": { - "message": "You do not have a matching login for this site." + "message": "Nema odgovarajuće prijavu za ovu stranicu." }, "confirm": { - "message": "Confirm" + "message": "Autoriziraj" }, "savePasskey": { - "message": "Save passkey" + "message": "Spremi pristupni ključ" }, "savePasskeyNewLogin": { - "message": "Save passkey as new login" + "message": "Spremi pristupni ključ kao novu prijavu" }, "choosePasskey": { - "message": "Choose a login to save this passkey to" + "message": "Odaberi prijavu za spremanje ovog pristupnog ključa" }, "passkeyItem": { - "message": "Passkey Item" + "message": "Stavka pristupnog ključa" }, "overwritePasskey": { - "message": "Overwrite passkey?" + "message": "Prebriši pristupni ključ?" }, "overwritePasskeyAlert": { - "message": "This item already contains a passkey. Are you sure you want to overwrite the current passkey?" + "message": "Ova stavka već sadrži pristupni ključ. Sigurno želiš prebrisati trenutni pristupni ključ?" }, "featureNotSupported": { - "message": "Feature not yet supported" + "message": "Značajka još nije podržana" }, "yourPasskeyIsLocked": { - "message": "Authentication required to use passkey. Verify your identity to continue." + "message": "Za korištenje pristpnog ključa potrebna je autentifikacija. Potvrdi svoj identitet." }, "useBrowserName": { - "message": "Use browser" + "message": "Koristi preglednik" }, "multifactorAuthenticationCancelled": { - "message": "Multifactor authentication cancelled" + "message": "Multifaktorska autentifikacija otkazana" }, "noLastPassDataFound": { - "message": "No LastPass data found" + "message": "Nisu nađeni LastPass podaci" }, "incorrectUsernameOrPassword": { - "message": "Incorrect username or password" + "message": "Neispravno korisničko ime ili lozinka" }, "multifactorAuthenticationFailed": { - "message": "Multifactor authentication failed" + "message": "Multifaktorska autentifikacija nije uspjela" }, "includeSharedFolders": { - "message": "Include shared folders" + "message": "Uključi dijeljene mape" }, "lastPassEmail": { - "message": "LastPass Email" + "message": "LastPass e-pošta" }, "importingYourAccount": { - "message": "Importing your account..." + "message": "Uvoz tvog računa..." }, "lastPassMFARequired": { - "message": "LastPass multifactor authentication required" + "message": "Potrebna LastPass multifaktorska autenfikacija" }, "lastPassMFADesc": { - "message": "Enter your one-time passcode from your authentication app" + "message": "Unesi svoj jednokratni kôd iz aplikacije za autentifikaciju" }, "lastPassOOBDesc": { - "message": "Approve the login request in your authentication app or enter a one-time passcode." + "message": "Odobri svoj zahtjev za prijavu u svojoj aplikaciji za autentifikaciju ili unesi jednokratni kôd." }, "passcode": { "message": "Passcode" }, "lastPassMasterPassword": { - "message": "LastPass master password" + "message": "LastPass glavna lozinka" }, "lastPassAuthRequired": { - "message": "LastPass authentication required" + "message": "Potrebna LastPass autentifikacija" }, "awaitingSSO": { - "message": "Awaiting SSO authentication" + "message": "Čekanje SSO autentifikacije" }, "awaitingSSODesc": { - "message": "Please continue to log in using your company credentials." + "message": "Prijavi se koristeći pristupne podatke svoje tvrtke." }, "seeDetailedInstructions": { - "message": "See detailed instructions on our help site at", + "message": "Detaljne upute za pomoć pronađi na našoj stranici za pomoć na", "description": "This is followed a by a hyperlink to the help website." }, "importDirectlyFromLastPass": { - "message": "Import directly from LastPass" + "message": "Uvezi direktno iz LastPass-a" }, "importFromCSV": { - "message": "Import from CSV" + "message": "Uvezi iz CSV-a" }, "lastPassTryAgainCheckEmail": { - "message": "Try again or look for an email from LastPass to verify it's you." + "message": "Pokušaj ponovno ili pogledaj e-poštu od LastPass-a za potvrdu." }, "collection": { - "message": "Collection" + "message": "Zbirka" }, "lastPassYubikeyDesc": { - "message": "Insert the YubiKey associated with your LastPass account into your computer's USB port, then touch its button." + "message": "Umetni YubiKey pridružen svojem LastPass računu u USB priključak račuanala, a zatim dodirni njegovu tipku." } } diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index 6f637a10dfe..1c72b15db49 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -510,7 +510,7 @@ "message": "您的登录会话已过期。" }, "logOutConfirmation": { - "message": "您确定要注销吗?" + "message": "确定要注销吗?" }, "yes": { "message": "是" @@ -540,7 +540,7 @@ "message": "文件夹已保存" }, "deleteFolderConfirmation": { - "message": "您确定要删除此文件夹吗?" + "message": "确定要删除此文件夹吗?" }, "deletedFolder": { "message": "文件夹已删除" @@ -592,13 +592,13 @@ "message": "覆盖密码" }, "overwritePasswordConfirmation": { - "message": "您确定要覆盖当前密码吗?" + "message": "确定要覆盖当前密码吗?" }, "overwriteUsername": { "message": "覆盖用户名" }, "overwriteUsernameConfirmation": { - "message": "您确定要覆盖当前用户名吗?" + "message": "确定要覆盖当前用户名吗?" }, "searchFolder": { "message": "搜索文件夹" @@ -769,7 +769,7 @@ "message": "删除附件" }, "deleteAttachmentConfirmation": { - "message": "您确定要删除此附件吗?" + "message": "确定要删除此附件吗?" }, "deletedAttachment": { "message": "附件已删除" @@ -1378,7 +1378,7 @@ "description": "ex. Date this password was updated" }, "neverLockWarning": { - "message": "您确定要使用「从不」选项吗?将锁定选项设置为「从不」会将密码库的加密密钥存储在您的设备上。如果使用此选项,您必须确保您的设备安全。" + "message": "确定要使用「从不」选项吗?将锁定选项设置为「从不」会将密码库的加密密钥存储在您的设备上。如果使用此选项,您必须确保您的设备安全。" }, "noOrganizationsList": { "message": "您没有加入任何组织。组织允许您与其他用户安全地共享项目。" @@ -1468,7 +1468,7 @@ "message": "永久删除项目" }, "permanentlyDeleteItemConfirmation": { - "message": "您确定要永久删除此项目吗?" + "message": "确定要永久删除此项目吗?" }, "permanentlyDeletedItem": { "message": "项目已永久删除" @@ -2012,7 +2012,7 @@ "message": "主密码已移除" }, "leaveOrganizationConfirmation": { - "message": "您确定要退出该组织吗?" + "message": "确定要退出该组织吗?" }, "leftOrganization": { "message": "您已经退出该组织。" @@ -2522,7 +2522,7 @@ "message": "选择一个集合" }, "importTargetHint": { - "message": "如果您希望将导入的文件内容移动到 $DESTINATION$,请选择此选项", + "message": "如果您希望将导入的文件内容移动到某个 $DESTINATION$,请选择此选项", "description": "Located as a hint under the import target. Will be appended by either folder or collection, depending if the user is importing into an individual or an organizational vault.", "placeholders": { "destination": { @@ -2608,7 +2608,7 @@ "message": "覆盖通行密钥吗?" }, "overwritePasskeyAlert": { - "message": "此项目已包含一个通行密钥。您确定要覆盖当前的通行密钥吗?" + "message": "此项目已包含一个通行密钥。确定要覆盖当前的通行密钥吗?" }, "featureNotSupported": { "message": "功能尚不被支持" @@ -2632,7 +2632,7 @@ "message": "多重身份验证失败" }, "includeSharedFolders": { - "message": "包含已共享的文件夹" + "message": "包含共享的文件夹" }, "lastPassEmail": { "message": "LastPass Email" diff --git a/apps/browser/src/autofill/content/notification-bar.ts b/apps/browser/src/autofill/content/notification-bar.ts index 29ba7ca9fff..223d6ab1ddf 100644 --- a/apps/browser/src/autofill/content/notification-bar.ts +++ b/apps/browser/src/autofill/content/notification-bar.ts @@ -3,7 +3,7 @@ import ChangePasswordRuntimeMessage from "../../background/models/changePassword import AutofillField from "../models/autofill-field"; import { WatchedForm } from "../models/watched-form"; import { FormData } from "../services/abstractions/autofill.service"; -import { UserSettings } from "../types"; +import { GlobalSettings, UserSettings } from "../types"; interface HTMLElementWithFormOpId extends HTMLElement { formOpId: string; @@ -97,6 +97,7 @@ async function loadNotificationBar() { const userSettingsStorageValue = await getFromLocalStorage(activeUserId); if (userSettingsStorageValue[activeUserId]) { const userSettings: UserSettings = userSettingsStorageValue[activeUserId].settings; + const globalSettings: GlobalSettings = await getFromLocalStorage("global"); // Do not show the notification bar on the Bitwarden vault // because they can add logins and change passwords there @@ -107,11 +108,11 @@ async function loadNotificationBar() { // show the notification bar on (for login detail collection or password change). // It is managed in the Settings > Excluded Domains page in the browser extension. // Example: '{"bitwarden.com":null}' - const excludedDomainsDict = userSettings.neverDomains; + const excludedDomainsDict = globalSettings.neverDomains; if (!excludedDomainsDict || !(window.location.hostname in excludedDomainsDict)) { // Set local disabled preferences - disabledAddLoginNotification = userSettings.disableAddLoginNotification; - disabledChangedPasswordNotification = userSettings.disableChangedPasswordNotification; + disabledAddLoginNotification = globalSettings.disableAddLoginNotification; + disabledChangedPasswordNotification = globalSettings.disableChangedPasswordNotification; if (!disabledAddLoginNotification || !disabledChangedPasswordNotification) { // If the user has not disabled both notifications, then handle the initial page change (null -> actual page) diff --git a/apps/browser/src/autofill/types/index.ts b/apps/browser/src/autofill/types/index.ts index 8a97e397477..880ce3ca1e5 100644 --- a/apps/browser/src/autofill/types/index.ts +++ b/apps/browser/src/autofill/types/index.ts @@ -1,4 +1,5 @@ import { Region } from "@bitwarden/common/platform/abstractions/environment.service"; +import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { VaultTimeoutAction } from "@bitwarden/common/src/enums/vault-timeout-action.enum"; import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; @@ -34,13 +35,15 @@ export type UserSettings = { settings: { equivalentDomains: string[][]; }; - neverDomains?: { [key: string]: any }; - disableAddLoginNotification?: boolean; - disableChangedPasswordNotification?: boolean; vaultTimeout: number; vaultTimeoutAction: VaultTimeoutAction; }; +export type GlobalSettings = Pick< + GlobalState, + "disableAddLoginNotification" | "disableChangedPasswordNotification" | "neverDomains" +>; + /** * A HTMLElement (usually a form element) with additional custom properties added by this script */ diff --git a/apps/browser/src/platform/decorators/session-sync-observable/session-syncer.spec.ts b/apps/browser/src/platform/decorators/session-sync-observable/session-syncer.spec.ts index afa66db0454..96c5a4eea55 100644 --- a/apps/browser/src/platform/decorators/session-sync-observable/session-syncer.spec.ts +++ b/apps/browser/src/platform/decorators/session-sync-observable/session-syncer.spec.ts @@ -1,4 +1,4 @@ -import { awaitAsync } from "@bitwarden/angular/../test-utils"; +import { awaitAsync } from "@bitwarden/common/../spec/utils"; import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject, ReplaySubject } from "rxjs"; diff --git a/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts b/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts index 7d6cf1390fc..77267c3e875 100644 --- a/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts +++ b/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts @@ -11,6 +11,9 @@ import { fromChromeEvent } from "../../browser/from-chrome-event"; export default abstract class AbstractChromeStorageService implements AbstractStorageService { constructor(protected chromeStorageApi: chrome.storage.StorageArea) {} + get valuesRequireDeserialization(): boolean { + return true; + } get updates$(): Observable { return fromChromeEvent(this.chromeStorageApi.onChanged).pipe( mergeMap(([changes]) => { @@ -27,7 +30,6 @@ export default abstract class AbstractChromeStorageService implements AbstractSt key: key, // For removes this property will not exist but then it will just be // undefined which is fine. - value: change.newValue, updateType: updateType, }; }); diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.ts index 188a9854c55..6366c51de3f 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.ts @@ -35,6 +35,10 @@ export class LocalBackedSessionStorageService extends AbstractMemoryStorageServi super(); } + get valuesRequireDeserialization(): boolean { + return true; + } + get updates$() { return this.updatesSubject.asObservable(); } diff --git a/apps/cli/src/platform/services/lowdb-storage.service.ts b/apps/cli/src/platform/services/lowdb-storage.service.ts index e80e94c043e..d8f8f294124 100644 --- a/apps/cli/src/platform/services/lowdb-storage.service.ts +++ b/apps/cli/src/platform/services/lowdb-storage.service.ts @@ -107,6 +107,9 @@ export class LowdbStorageService implements AbstractStorageService { this.ready = true; } + get valuesRequireDeserialization(): boolean { + return true; + } get updates$() { return this.updatesSubject.asObservable(); } @@ -133,7 +136,7 @@ export class LowdbStorageService implements AbstractStorageService { return this.lockDbFile(() => { this.readForNoCache(); this.db.set(key, obj).write(); - this.updatesSubject.next({ key, value: obj, updateType: "save" }); + this.updatesSubject.next({ key, updateType: "save" }); this.logService.debug(`Successfully wrote ${key} to db`); return; }); @@ -144,7 +147,7 @@ export class LowdbStorageService implements AbstractStorageService { return this.lockDbFile(() => { this.readForNoCache(); this.db.unset(key).write(); - this.updatesSubject.next({ key, value: null, updateType: "remove" }); + this.updatesSubject.next({ key, updateType: "remove" }); this.logService.debug(`Successfully removed ${key} from db`); return; }); diff --git a/apps/cli/src/platform/services/node-env-secure-storage.service.ts b/apps/cli/src/platform/services/node-env-secure-storage.service.ts index 364491469eb..14ea6a6bb12 100644 --- a/apps/cli/src/platform/services/node-env-secure-storage.service.ts +++ b/apps/cli/src/platform/services/node-env-secure-storage.service.ts @@ -14,6 +14,10 @@ export class NodeEnvSecureStorageService implements AbstractStorageService { private cryptoService: () => CryptoService ) {} + get valuesRequireDeserialization(): boolean { + return true; + } + get updates$() { return throwError( () => new Error("Secure storage implementations cannot have their updates subscribed to.") diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 56b6378def0..51671542588 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -537,6 +537,19 @@ export class AppComponent implements OnInit, OnDestroy { this.keyConnectorService.clear(), ]); + const preLogoutActiveUserId = this.activeUserId; + await this.stateService.clean({ userId: userBeingLoggedOut }); + + if (this.activeUserId == null) { + this.router.navigate(["login"]); + } else if (preLogoutActiveUserId !== this.activeUserId) { + this.messagingService.send("switchAccount"); + } + + await this.updateAppMenu(); + + // This must come last otherwise the logout will prematurely trigger + // a process reload before all the state service user data can be cleaned up if (userBeingLoggedOut === this.activeUserId) { this.searchService.clearIndex(); this.authService.logOut(async () => { @@ -549,17 +562,6 @@ export class AppComponent implements OnInit, OnDestroy { } }); } - - const preLogoutActiveUserId = this.activeUserId; - await this.stateService.clean({ userId: userBeingLoggedOut }); - - if (this.activeUserId == null) { - this.router.navigate(["login"]); - } else if (preLogoutActiveUserId !== this.activeUserId) { - this.messagingService.send("switchAccount"); - } - - await this.updateAppMenu(); } private async recordActivity() { diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index f0c3da705d9..793f09ac652 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -1534,11 +1534,11 @@ "message": "Postavi glavnu lozinku" }, "orgPermissionsUpdatedMustSetPassword": { - "message": "Your organization permissions were updated, requiring you to set a master password.", + "message": "Moraš postaviti glavnu lozinku jer su dopuštenja tvoje organizacije ažurirana.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "orgRequiresYouToSetPassword": { - "message": "Your organization requires you to set a master password.", + "message": "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" }, "currentMasterPass": { @@ -1668,7 +1668,7 @@ "message": "Pravila organizacije utječu na tvoje mogućnosti vlasništva." }, "personalOwnershipPolicyInEffectImports": { - "message": "An organization policy has blocked importing items into your individual vault." + "message": "Organizacijsko pravilo onemogućuje uvoz stavki u tvoj osobni trezor." }, "allSends": { "message": "Svi Sendovi", @@ -2425,38 +2425,38 @@ "message": "Podizbornik" }, "typePasskey": { - "message": "Passkey" + "message": "Pristupni ključ" }, "passkeyNotCopied": { - "message": "Passkey will not be copied" + "message": "Pristupni ključ neće biti kopiran" }, "passkeyNotCopiedAlert": { - "message": "The passkey will not be copied to the cloned item. Do you want to continue cloning this item?" + "message": "Pristupni ključ se neće kopirati u kloniranu stavku. Želiš li nastaviti klonirati ovu stavku?" }, "aliasDomain": { "message": "Alias domene" }, "importData": { - "message": "Import data", + "message": "Uvezi podatke", "description": "Used for the desktop menu item and the header of the import dialog" }, "importError": { - "message": "Import error" + "message": "Greška prilikom uvoza" }, "importErrorDesc": { - "message": "There was a problem with the data you tried to import. Please resolve the errors listed below in your source file and try again." + "message": "Postoji problem s podacima za uvoz. Potrebno je razriješiti doljenavedene greške u izvornoj datoteci i pokušati ponovno." }, "resolveTheErrorsBelowAndTryAgain": { - "message": "Resolve the errors below and try again." + "message": "Popravi navedene greške i pokušaj ponovo." }, "description": { - "message": "Description" + "message": "Opis" }, "importSuccess": { - "message": "Data successfully imported" + "message": "Uvoz podataka u trezor je uspio" }, "importSuccessNumberOfItems": { - "message": "A total of $AMOUNT$ items were imported.", + "message": "Ukupno je uvezeno $AMOUNT$ stavaka.", "placeholders": { "amount": { "content": "$1", @@ -2465,10 +2465,10 @@ } }, "total": { - "message": "Total" + "message": "Ukupno" }, "importWarning": { - "message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organization. Do you want to proceed?", + "message": "Uvoziš podatke u $ORGANIZATION$. Tvoji podaci možda će biti podijeljeni s članovima ove organizacije. Želiš li svejedno uvesti podatke?", "placeholders": { "organization": { "content": "$1", @@ -2477,31 +2477,31 @@ } }, "importFormatError": { - "message": "Data is not formatted correctly. Please check your import file and try again." + "message": "Podaci nisu ispravno formatirani. Provjeri uvoznu datoteku i pokušaj ponovno." }, "importNothingError": { - "message": "Nothing was imported." + "message": "Ništa nije uvezeno." }, "importEncKeyError": { - "message": "Error decrypting the exported file. Your encryption key does not match the encryption key used export the data." + "message": "Greška u dešifriranju izvozne datoteke. Ovaj ključ za šifriranje ne odgovara ključu za šifriranje korištenom pri izvozu datoteke." }, "invalidFilePassword": { - "message": "Invalid file password, please use the password you entered when you created the export file." + "message": "Nesipravna lozinka datoteke. Unesi lozinku izvozne datoteke." }, "importDestination": { - "message": "Import destination" + "message": "Odredište uvoza" }, "learnAboutImportOptions": { - "message": "Learn about your import options" + "message": "Više o mogućnostima uvoza" }, "selectImportFolder": { - "message": "Select a folder" + "message": "Odaberi mapu" }, "selectImportCollection": { - "message": "Select a collection" + "message": "Odaberi zbirku" }, "importTargetHint": { - "message": "Select this option if you want the imported file contents moved to a $DESTINATION$", + "message": "Odaberi ovu opciju ako sadržaj uvezene datoteke želiš spremiti u $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": { @@ -2511,25 +2511,25 @@ } }, "importUnassignedItemsError": { - "message": "File contains unassigned items." + "message": "Datoteka sadrži nedodijeljene stavke." }, "selectFormat": { - "message": "Select the format of the import file" + "message": "Odaberi format datoteke za uvoz" }, "selectImportFile": { - "message": "Select the import file" + "message": "Odaberi datoteku za uvoz" }, "chooseFile": { - "message": "Choose File" + "message": "Odaberi datoteku" }, "noFileChosen": { - "message": "No file chosen" + "message": "Nije odabrana datoteka" }, "orCopyPasteFileContents": { - "message": "or copy/paste the import file contents" + "message": "ili kopiraj/zalijepi sadržaj uvozne datoteke" }, "instructionsFor": { - "message": "$NAME$ Instructions", + "message": "$NAME$ upute", "description": "The title for the import tool instructions.", "placeholders": { "name": { @@ -2539,76 +2539,76 @@ } }, "confirmVaultImport": { - "message": "Confirm vault import" + "message": "Potvrdi uvoz trezora" }, "confirmVaultImportDesc": { - "message": "This file is password-protected. Please enter the file password to import data." + "message": "Ova je datoteka zaštićena lozinkom. Unesi lozinku za nastavak uvoza." }, "confirmFilePassword": { - "message": "Confirm file password" + "message": "Potvrdi lozinku datoteke" }, "multifactorAuthenticationCancelled": { - "message": "Multifactor authentication cancelled" + "message": "Multifaktorska autentifikacija otkazana" }, "noLastPassDataFound": { - "message": "No LastPass data found" + "message": "Nisu nađeni LastPass podaci" }, "incorrectUsernameOrPassword": { - "message": "Incorrect username or password" + "message": "Neispravno korisničko ime ili lozinka" }, "multifactorAuthenticationFailed": { - "message": "Multifactor authentication failed" + "message": "Multifaktorska autentifikacija nije uspjela" }, "includeSharedFolders": { - "message": "Include shared folders" + "message": "Uključi dijeljene mape" }, "lastPassEmail": { - "message": "LastPass Email" + "message": "LastPass e-pošta" }, "importingYourAccount": { - "message": "Importing your account..." + "message": "Uvoz tvog računa..." }, "lastPassMFARequired": { - "message": "LastPass multifactor authentication required" + "message": "Potrebna LastPass multifaktorska autenfikacija" }, "lastPassMFADesc": { - "message": "Enter your one-time passcode from your authentication app" + "message": "Unesi svoj jednokratni kôd iz aplikacije za autentifikaciju" }, "lastPassOOBDesc": { - "message": "Approve the login request in your authentication app or enter a one-time passcode." + "message": "Odobri svoj zahtjev za prijavu u svojoj aplikaciji za autentifikaciju ili unesi jednokratni kôd." }, "passcode": { "message": "Passcode" }, "lastPassMasterPassword": { - "message": "LastPass master password" + "message": "LastPass glavna lozinka" }, "lastPassAuthRequired": { - "message": "LastPass authentication required" + "message": "Potrebna LastPass autentifikacija" }, "awaitingSSO": { - "message": "Awaiting SSO authentication" + "message": "Čekanje SSO autentifikacije" }, "awaitingSSODesc": { - "message": "Please continue to log in using your company credentials." + "message": "Prijavi se koristeći pristupne podatke svoje tvrtke." }, "seeDetailedInstructions": { - "message": "See detailed instructions on our help site at", + "message": "Detaljne upute za pomoć pronađi na našoj stranici za pomoć na", "description": "This is followed a by a hyperlink to the help website." }, "importDirectlyFromLastPass": { - "message": "Import directly from LastPass" + "message": "Uvezi direktno iz LastPass-a" }, "importFromCSV": { - "message": "Import from CSV" + "message": "Uvezi iz CSV-a" }, "lastPassTryAgainCheckEmail": { - "message": "Try again or look for an email from LastPass to verify it's you." + "message": "Pokušaj ponovno ili pogledaj e-poštu od LastPass-a za potvrdu." }, "collection": { - "message": "Collection" + "message": "Zbirka" }, "lastPassYubikeyDesc": { - "message": "Insert the YubiKey associated with your LastPass account into your computer's USB port, then touch its button." + "message": "Umetni YubiKey pridružen svojem LastPass računu u USB priključak račuanala, a zatim dodirni njegovu tipku." } } diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index 203af065a6c..6fa2488cd3c 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -1534,11 +1534,11 @@ "message": "Nustatyti pagrindinį slaptažodį" }, "orgPermissionsUpdatedMustSetPassword": { - "message": "Your organization permissions were updated, requiring you to set a master password.", + "message": "Dėl organizacijos leidimų atnaujinimo, jums reikia nustatyti pagrindinį slaptažodį.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "orgRequiresYouToSetPassword": { - "message": "Your organization requires you to set a master password.", + "message": "Jūsų organizacija reikalauja nustatyti pagrindinį slaptažodį.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "currentMasterPass": { @@ -1668,7 +1668,7 @@ "message": "Organizacijos politika turi įtakos jūsų nuosavybės galimybėms." }, "personalOwnershipPolicyInEffectImports": { - "message": "An organization policy has blocked importing items into your individual vault." + "message": "Organizacijos politika blokavo elementų importavimą į jūsų individualią saugyklą." }, "allSends": { "message": "Visi Sendai", @@ -2425,38 +2425,38 @@ "message": "Submeniu" }, "typePasskey": { - "message": "Passkey" + "message": "Prieigos raktas" }, "passkeyNotCopied": { - "message": "Passkey will not be copied" + "message": "Prieigos raktas nebus kopijuotas" }, "passkeyNotCopiedAlert": { - "message": "The passkey will not be copied to the cloned item. Do you want to continue cloning this item?" + "message": "Prieigos raktas nebus nukopijuotas į nuklonuotą elementą. Ar norite tęsti šio elemento klonavimą?" }, "aliasDomain": { "message": "Alias domenas" }, "importData": { - "message": "Import data", + "message": "Importuoti duomenis", "description": "Used for the desktop menu item and the header of the import dialog" }, "importError": { - "message": "Import error" + "message": "Importavimo klaida" }, "importErrorDesc": { - "message": "There was a problem with the data you tried to import. Please resolve the errors listed below in your source file and try again." + "message": "Kilo problema su duomenimis, kuriuos bandėte importuoti. Prašome pataisyti klaidas, išvardytas apačioje esančiam pradiniame faile ir pabandyti iš naujo." }, "resolveTheErrorsBelowAndTryAgain": { - "message": "Resolve the errors below and try again." + "message": "Ištaisykite klaidas ir pabandykite iš naujo." }, "description": { - "message": "Description" + "message": "Aprašymas" }, "importSuccess": { - "message": "Data successfully imported" + "message": "Duomenys sėkmingai importuoti" }, "importSuccessNumberOfItems": { - "message": "A total of $AMOUNT$ items were imported.", + "message": "Iš viso importuoti $AMOUNT$ elementai.", "placeholders": { "amount": { "content": "$1", @@ -2465,10 +2465,10 @@ } }, "total": { - "message": "Total" + "message": "Iš viso" }, "importWarning": { - "message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organization. Do you want to proceed?", + "message": "Jūs importuojate duomenis į $ORGANIZATION$. Jūsų duomenimis gali būti pasidalinta tarp šios organizacijos narių. Ar norite tęsti?", "placeholders": { "organization": { "content": "$1", @@ -2477,31 +2477,31 @@ } }, "importFormatError": { - "message": "Data is not formatted correctly. Please check your import file and try again." + "message": "Duomenys neteisingai suformatuoti. Prašome patikrinti savo importuojamą failą ir pabandyti iš naujo." }, "importNothingError": { - "message": "Nothing was imported." + "message": "Niekas nebuvo importuota." }, "importEncKeyError": { - "message": "Error decrypting the exported file. Your encryption key does not match the encryption key used export the data." + "message": "Kilo klaida iššifruojant eksportuotą failą. Jūsų šifravimo raktas nesutampa su šifravimo raktu, naudotu eksportuoti duomenis." }, "invalidFilePassword": { - "message": "Invalid file password, please use the password you entered when you created the export file." + "message": "Netinkamas failo slaptažodis, prašome naudoti tą slaptažodį, kurį įvedėte kurdami eksportuojamą failą." }, "importDestination": { - "message": "Import destination" + "message": "Importavimo vieta" }, "learnAboutImportOptions": { - "message": "Learn about your import options" + "message": "Sužinoti apie importavimo pasirinkimus" }, "selectImportFolder": { - "message": "Select a folder" + "message": "Pasirinkti aplanką" }, "selectImportCollection": { - "message": "Select a collection" + "message": "Pasirinkti rinkinį" }, "importTargetHint": { - "message": "Select this option if you want the imported file contents moved to a $DESTINATION$", + "message": "Pasirinkite šį pasirinkimą jei norite jog importuoto failo turinys būtų perkeltas į $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": { @@ -2511,25 +2511,25 @@ } }, "importUnassignedItemsError": { - "message": "File contains unassigned items." + "message": "Faile yra nepriskirtų elementų." }, "selectFormat": { - "message": "Select the format of the import file" + "message": "Pasirinkti importuojamo failo formatą" }, "selectImportFile": { - "message": "Select the import file" + "message": "Pasirinkti importuojamą failą" }, "chooseFile": { - "message": "Choose File" + "message": "Pasirinkti failą" }, "noFileChosen": { - "message": "No file chosen" + "message": "Nepasirinktas joks failas" }, "orCopyPasteFileContents": { - "message": "or copy/paste the import file contents" + "message": "arba kopijuokite/įklijuokite importuojamo failo turinį" }, "instructionsFor": { - "message": "$NAME$ Instructions", + "message": "$NAME$ Instrukcijos", "description": "The title for the import tool instructions.", "placeholders": { "name": { @@ -2539,76 +2539,76 @@ } }, "confirmVaultImport": { - "message": "Confirm vault import" + "message": "Patvirtinti saugyklos importavimą" }, "confirmVaultImportDesc": { - "message": "This file is password-protected. Please enter the file password to import data." + "message": "Failas yra apsaugotas slaptažodžiu. Norint importuoti duomenis, prašome įvesti failo slaptažodį." }, "confirmFilePassword": { - "message": "Confirm file password" + "message": "Patvirtinti failo slaptažodį" }, "multifactorAuthenticationCancelled": { - "message": "Multifactor authentication cancelled" + "message": "Kelių veiksnių autentifikacija atšaukta" }, "noLastPassDataFound": { - "message": "No LastPass data found" + "message": "Nerasti jokie LastPass duomenys" }, "incorrectUsernameOrPassword": { - "message": "Incorrect username or password" + "message": "Neteisingas naudotojo vardas arba slaptažodis" }, "multifactorAuthenticationFailed": { - "message": "Multifactor authentication failed" + "message": "Kelių veiksnių autentifikacija nepavyko" }, "includeSharedFolders": { - "message": "Include shared folders" + "message": "Įtraukti bendrinamus aplankus" }, "lastPassEmail": { - "message": "LastPass Email" + "message": "LastPass el. paštas" }, "importingYourAccount": { - "message": "Importing your account..." + "message": "Jūsų paskyra importuojama..." }, "lastPassMFARequired": { - "message": "LastPass multifactor authentication required" + "message": "Privalomas LastPass kelių veiksnių autentifikavimas" }, "lastPassMFADesc": { - "message": "Enter your one-time passcode from your authentication app" + "message": "Įveskite savo vienkartinį prieigos kodą iš autentifikavimo programėlės" }, "lastPassOOBDesc": { - "message": "Approve the login request in your authentication app or enter a one-time passcode." + "message": "Patvirtinkite prisijungimo prašymą savo autentifikavimo programėlėje arba įveskite vienkartinį prieigos kodą." }, "passcode": { - "message": "Passcode" + "message": "Prieigos kodas" }, "lastPassMasterPassword": { - "message": "LastPass master password" + "message": "LastPass pagrindinis slaptažodis" }, "lastPassAuthRequired": { - "message": "LastPass authentication required" + "message": "Privalomas LastPass autentifikavimas" }, "awaitingSSO": { - "message": "Awaiting SSO authentication" + "message": "Laukiama SSO autentifikavimo" }, "awaitingSSODesc": { - "message": "Please continue to log in using your company credentials." + "message": "Prašome toliau prisijungti naudojant savo įmonės prisijungimo duomenis." }, "seeDetailedInstructions": { - "message": "See detailed instructions on our help site at", + "message": "Peržiūrėkite detalias instrukcijas mūsų pagalbos tinklalapyje, esančiam", "description": "This is followed a by a hyperlink to the help website." }, "importDirectlyFromLastPass": { - "message": "Import directly from LastPass" + "message": "Importuoti tiesiai iš LastPass" }, "importFromCSV": { - "message": "Import from CSV" + "message": "Importuoti iš CSV" }, "lastPassTryAgainCheckEmail": { - "message": "Try again or look for an email from LastPass to verify it's you." + "message": "Pabandykite iš naujo arba ieškokite el. laiško iš LastPass kad patvirtintumėte, jog čia jūs." }, "collection": { - "message": "Collection" + "message": "Rinkinys" }, "lastPassYubikeyDesc": { - "message": "Insert the YubiKey associated with your LastPass account into your computer's USB port, then touch its button." + "message": "Įkiškite su LastPass paskyra susietą YubiKey į kompiuterio USB jungtį, tada palieskite jo mygtuką." } } diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 7fd8c0ef933..02eca45569f 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -370,13 +370,13 @@ "message": "项目已发送到回收站" }, "overwritePasswordConfirmation": { - "message": "您确定要覆盖当前密码吗?" + "message": "确定要覆盖当前密码吗?" }, "overwriteUsername": { "message": "覆盖用户名" }, "overwriteUsernameConfirmation": { - "message": "您确定要覆盖当前用户名吗?" + "message": "确定要覆盖当前用户名吗?" }, "noneFolder": { "message": "无文件夹", @@ -461,7 +461,7 @@ "message": "附件已删除" }, "deleteAttachmentConfirmation": { - "message": "您确定要删除此附件吗?" + "message": "确定要删除此附件吗?" }, "attachmentSaved": { "message": "附件已保存" @@ -485,7 +485,7 @@ "message": "文件夹已添加" }, "deleteFolderConfirmation": { - "message": "您确定要删除此文件夹吗?" + "message": "确定要删除此文件夹吗?" }, "deletedFolder": { "message": "文件夹已删除" @@ -738,7 +738,7 @@ "message": "您的登录会话已过期。" }, "logOutConfirmation": { - "message": "您确定要注销吗?" + "message": "确定要注销吗?" }, "logOut": { "message": "注销" @@ -1472,7 +1472,7 @@ "message": "隐私条款" }, "unsavedChangesConfirmation": { - "message": "您确定要退出吗?如果您现在退出,您当前的信息不会被保存。" + "message": "确定要离开吗?如果您现在离开,您当前的信息不会被保存。" }, "unsavedChangesTitle": { "message": "更改未保存" @@ -1510,7 +1510,7 @@ "message": "永久删除项目" }, "permanentlyDeleteItemConfirmation": { - "message": "您确定要永久删除此项目吗?" + "message": "确定要永久删除此项目吗?" }, "permanentlyDeletedItem": { "message": "项目已永久删除" @@ -1948,7 +1948,7 @@ "message": "退出组织" }, "leaveOrganizationConfirmation": { - "message": "您确定要退出这个组织吗?" + "message": "确定要退出该组织吗?" }, "leftOrganization": { "message": "您已经退出该组织。" @@ -2085,7 +2085,7 @@ "message": "无法访问已停用组织中的项目。请联系您的组织所有者获取协助。" }, "neverLockWarning": { - "message": "确定要使用「从不」选项吗?将锁定选项设置为「从不」会将密码库的加密密钥存储在您的设备上。如果使用此选项,应确保您的设备得到妥善的保护。" + "message": "确定要使用「从不」选项吗?将锁定选项设置为「从不」会将密码库的加密密钥存储在您的设备上。如果使用此选项,您必须确保您的设备安全。" }, "vault": { "message": "密码库" @@ -2501,7 +2501,7 @@ "message": "选择一个集合" }, "importTargetHint": { - "message": "如果您希望将导入的文件内容移动到 $DESTINATION$,请选择此选项", + "message": "如果您希望将导入的文件内容移动到某个 $DESTINATION$,请选择此选项", "description": "Located as a hint under the import target. Will be appended by either folder or collection, depending if the user is importing into an individual or an organizational vault.", "placeholders": { "destination": { @@ -2560,7 +2560,7 @@ "message": "多重身份验证失败" }, "includeSharedFolders": { - "message": "包含已共享的文件夹" + "message": "包含共享的文件夹" }, "lastPassEmail": { "message": "LastPass Email" diff --git a/apps/desktop/src/platform/services/electron-renderer-secure-storage.service.ts b/apps/desktop/src/platform/services/electron-renderer-secure-storage.service.ts index 8d6b51cf7df..42513510ff3 100644 --- a/apps/desktop/src/platform/services/electron-renderer-secure-storage.service.ts +++ b/apps/desktop/src/platform/services/electron-renderer-secure-storage.service.ts @@ -4,6 +4,9 @@ import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/ import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; export class ElectronRendererSecureStorageService implements AbstractStorageService { + get valuesRequireDeserialization(): boolean { + return true; + } get updates$() { return throwError( () => new Error("Secure storage implementations cannot have their updates subscribed to.") diff --git a/apps/desktop/src/platform/services/electron-renderer-storage.service.ts b/apps/desktop/src/platform/services/electron-renderer-storage.service.ts index 8bb9ff72179..e81a0ca908c 100644 --- a/apps/desktop/src/platform/services/electron-renderer-storage.service.ts +++ b/apps/desktop/src/platform/services/electron-renderer-storage.service.ts @@ -8,6 +8,9 @@ import { export class ElectronRendererStorageService implements AbstractStorageService { private updatesSubject = new Subject(); + get valuesRequireDeserialization(): boolean { + return true; + } get updates$() { return this.updatesSubject.asObservable(); } @@ -22,11 +25,11 @@ export class ElectronRendererStorageService implements AbstractStorageService { async save(key: string, obj: T): Promise { await ipc.platform.storage.save(key, obj); - this.updatesSubject.next({ key, value: obj, updateType: "save" }); + this.updatesSubject.next({ key, updateType: "save" }); } async remove(key: string): Promise { await ipc.platform.storage.remove(key); - this.updatesSubject.next({ key, value: null, updateType: "remove" }); + this.updatesSubject.next({ key, updateType: "remove" }); } } diff --git a/apps/desktop/src/platform/services/electron-storage.service.ts b/apps/desktop/src/platform/services/electron-storage.service.ts index 40be8260dc3..065e3f5de04 100644 --- a/apps/desktop/src/platform/services/electron-storage.service.ts +++ b/apps/desktop/src/platform/services/electron-storage.service.ts @@ -65,6 +65,9 @@ export class ElectronStorageService implements AbstractStorageService { }); } + get valuesRequireDeserialization(): boolean { + return true; + } get updates$() { return this.updatesSubject.asObservable(); } @@ -84,13 +87,13 @@ export class ElectronStorageService implements AbstractStorageService { obj = Array.from(obj); } this.store.set(key, obj); - this.updatesSubject.next({ key, value: obj, updateType: "save" }); + this.updatesSubject.next({ key, updateType: "save" }); return Promise.resolve(); } remove(key: string): Promise { this.store.delete(key); - this.updatesSubject.next({ key, value: null, updateType: "remove" }); + this.updatesSubject.next({ key, updateType: "remove" }); return Promise.resolve(); } } diff --git a/apps/web/src/app/admin-console/organizations/tools/exposed-passwords-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/exposed-passwords-report.component.ts index 415bd6516c2..7f8e3197a54 100644 --- a/apps/web/src/app/admin-console/organizations/tools/exposed-passwords-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/exposed-passwords-report.component.ts @@ -4,7 +4,6 @@ import { ActivatedRoute } from "@angular/router"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -25,12 +24,11 @@ export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportC cipherService: CipherService, auditService: AuditService, modalService: ModalService, - messagingService: MessagingService, - private organizationService: OrganizationService, + organizationService: OrganizationService, private route: ActivatedRoute, passwordRepromptService: PasswordRepromptService ) { - super(cipherService, auditService, modalService, messagingService, passwordRepromptService); + super(cipherService, auditService, organizationService, modalService, passwordRepromptService); } async ngOnInit() { diff --git a/apps/web/src/app/admin-console/organizations/tools/inactive-two-factor-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/inactive-two-factor-report.component.ts index 4cc68b1f9d0..7d60439f3f2 100644 --- a/apps/web/src/app/admin-console/organizations/tools/inactive-two-factor-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/inactive-two-factor-report.component.ts @@ -4,7 +4,6 @@ import { ActivatedRoute } from "@angular/router"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -21,13 +20,12 @@ export class InactiveTwoFactorReportComponent extends BaseInactiveTwoFactorRepor constructor( cipherService: CipherService, modalService: ModalService, - messagingService: MessagingService, private route: ActivatedRoute, logService: LogService, passwordRepromptService: PasswordRepromptService, - private organizationService: OrganizationService + organizationService: OrganizationService ) { - super(cipherService, modalService, messagingService, logService, passwordRepromptService); + super(cipherService, organizationService, modalService, logService, passwordRepromptService); } async ngOnInit() { diff --git a/apps/web/src/app/admin-console/organizations/tools/reused-passwords-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/reused-passwords-report.component.ts index 93652239a11..c85178f7211 100644 --- a/apps/web/src/app/admin-console/organizations/tools/reused-passwords-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/reused-passwords-report.component.ts @@ -3,8 +3,6 @@ import { ActivatedRoute } from "@angular/router"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -24,13 +22,11 @@ export class ReusedPasswordsReportComponent extends BaseReusedPasswordsReportCom constructor( cipherService: CipherService, modalService: ModalService, - messagingService: MessagingService, - stateService: StateService, private route: ActivatedRoute, - private organizationService: OrganizationService, + organizationService: OrganizationService, passwordRepromptService: PasswordRepromptService ) { - super(cipherService, modalService, messagingService, stateService, passwordRepromptService); + super(cipherService, organizationService, modalService, passwordRepromptService); } async ngOnInit() { diff --git a/apps/web/src/app/admin-console/organizations/tools/unsecured-websites-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/unsecured-websites-report.component.ts index e8fbc0ed5e6..bee951063f0 100644 --- a/apps/web/src/app/admin-console/organizations/tools/unsecured-websites-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/unsecured-websites-report.component.ts @@ -3,7 +3,6 @@ import { ActivatedRoute } from "@angular/router"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -20,12 +19,11 @@ export class UnsecuredWebsitesReportComponent extends BaseUnsecuredWebsitesRepor constructor( cipherService: CipherService, modalService: ModalService, - messagingService: MessagingService, private route: ActivatedRoute, - private organizationService: OrganizationService, + organizationService: OrganizationService, passwordRepromptService: PasswordRepromptService ) { - super(cipherService, modalService, messagingService, passwordRepromptService); + super(cipherService, organizationService, modalService, passwordRepromptService); } async ngOnInit() { diff --git a/apps/web/src/app/admin-console/organizations/tools/weak-passwords-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/weak-passwords-report.component.ts index 540f71d91dc..1902d24fe7d 100644 --- a/apps/web/src/app/admin-console/organizations/tools/weak-passwords-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/weak-passwords-report.component.ts @@ -3,7 +3,6 @@ import { ActivatedRoute } from "@angular/router"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; @@ -25,16 +24,15 @@ export class WeakPasswordsReportComponent extends BaseWeakPasswordsReportCompone cipherService: CipherService, passwordStrengthService: PasswordStrengthServiceAbstraction, modalService: ModalService, - messagingService: MessagingService, private route: ActivatedRoute, - private organizationService: OrganizationService, + organizationService: OrganizationService, passwordRepromptService: PasswordRepromptService ) { super( cipherService, passwordStrengthService, + organizationService, modalService, - messagingService, passwordRepromptService ); } diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts b/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts index 4d8565dd8d7..a4ca5b75214 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/create-credential-dialog/create-credential-dialog.component.ts @@ -151,21 +151,12 @@ export class CreateCredentialDialogComponent implements OnInit { } const name = this.formGroup.value.credentialNaming.name; - try { - await this.webauthnService.saveCredential( - this.formGroup.value.credentialNaming.name, - this.pendingCredential, - keySet - ); - } catch (error) { - this.logService?.error(error); - this.platformUtilsService.showToast( - "error", - this.i18nService.t("unexpectedError"), - error.message - ); - return; - } + + await this.webauthnService.saveCredential( + this.formGroup.value.credentialNaming.name, + this.pendingCredential, + keySet + ); if (await firstValueFrom(this.hasPasskeys$)) { this.platformUtilsService.showToast( diff --git a/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.html b/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.html index 7b346ddc95d..dc55be99f1b 100644 --- a/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.html +++ b/apps/web/src/app/auth/settings/webauthn-login-settings/webauthn-login-settings.component.html @@ -1,12 +1,22 @@

{{ "loginWithPasskey" | i18n }} - {{ - "on" | i18n - }} - {{ - "off" | i18n - }} + + {{ "off" | i18n }} - {{ "ssoLoginIsRequired" | i18n }} + + + {{ + "on" | i18n + }} + {{ + "off" | i18n + }} + {{ "beta" | i18n }} @@ -56,7 +66,7 @@

{{ "passkeyLimitReachedInfo" | i18n }}

- + - - -
- {{ "sendAccessUnavailable" | i18n }} -
-
- {{ "unexpectedError" | i18n }} -
-
-

+ + {{ "viewSendHiddenEmailWarning" | i18n }} + {{ "learnMore" | i18n }}. + +

+ + + + {{ "sendAccessUnavailable" | i18n }} + + + {{ "unexpectedErrorSend" | i18n }} + +
+

{{ send.name }}


- {{ - "sendHiddenByDefault" | i18n - }} -
- -
- - +
-

{{ send.file.fileName }}

- - +
-

+

Expires: {{ expirationDate | date : "medium" }}

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

- {{ "sendAccessTaglineProductDesc" | i18n }}
+

+

+ {{ "sendAccessTaglineProductDesc" | i18n }} {{ "sendAccessTaglineLearnMore" | i18n }} - Bitwarden Send {{ "sendAccessTaglineOr" | i18n }} - {{ - "sendAccessTaglineSignUp" | i18n - }} + {{ "sendAccessTaglineSignUp" | i18n }} {{ "sendAccessTaglineTryToday" | i18n }}

diff --git a/apps/web/src/app/tools/send/access.component.ts b/apps/web/src/app/tools/send/access.component.ts index 180acfd69f7..d01bc9c52ff 100644 --- a/apps/web/src/app/tools/send/access.component.ts +++ b/apps/web/src/app/tools/send/access.component.ts @@ -1,16 +1,14 @@ import { Component, OnInit } from "@angular/core"; +import { FormBuilder } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { SEND_KDF_ITERATIONS } from "@bitwarden/common/enums"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; import { SendAccess } from "@bitwarden/common/tools/send/models/domain/send-access"; @@ -18,56 +16,65 @@ import { SendAccessRequest } from "@bitwarden/common/tools/send/models/request/s import { SendAccessResponse } from "@bitwarden/common/tools/send/models/response/send-access.response"; import { SendAccessView } from "@bitwarden/common/tools/send/models/view/send-access.view"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; +import { NoItemsModule } from "@bitwarden/components"; + +import { SharedModule } from "../../shared"; + +import { ExpiredSend } from "./icons/expired-send.icon"; +import { SendAccessFileComponent } from "./send-access-file.component"; +import { SendAccessPasswordComponent } from "./send-access-password.component"; +import { SendAccessTextComponent } from "./send-access-text.component"; @Component({ selector: "app-send-access", templateUrl: "access.component.html", + standalone: true, + imports: [ + SendAccessFileComponent, + SendAccessTextComponent, + SendAccessPasswordComponent, + SharedModule, + NoItemsModule, + ], }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil export class AccessComponent implements OnInit { - send: SendAccessView; - sendType = SendType; - downloading = false; - loading = true; - passwordRequired = false; - formPromise: Promise; - password: string; - showText = false; - unavailable = false; - error = false; - hideEmail = false; + protected send: SendAccessView; + protected sendType = SendType; + protected loading = true; + protected passwordRequired = false; + protected formPromise: Promise; + protected password: string; + protected unavailable = false; + protected error = false; + protected hideEmail = false; + protected decKey: SymmetricCryptoKey; + protected accessRequest: SendAccessRequest; + protected expiredSendIcon = ExpiredSend; + + protected formGroup = this.formBuilder.group({}); private id: string; private key: string; - private decKey: SymmetricCryptoKey; - private accessRequest: SendAccessRequest; constructor( - private i18nService: I18nService, private cryptoFunctionService: CryptoFunctionService, - private apiService: ApiService, - private platformUtilsService: PlatformUtilsService, private route: ActivatedRoute, private cryptoService: CryptoService, - private fileDownloadService: FileDownloadService, - private sendApiService: SendApiService + private sendApiService: SendApiService, + private platformUtilsService: PlatformUtilsService, + private i18nService: I18nService, + protected formBuilder: FormBuilder ) {} - get sendText() { - if (this.send == null || this.send.text == null) { - return null; - } - return this.showText ? this.send.text.text : this.send.text.maskedText; - } - - get expirationDate() { + protected get expirationDate() { if (this.send == null || this.send.expirationDate == null) { return null; } return this.send.expirationDate; } - get creatorIdentifier() { + protected get creatorIdentifier() { if (this.send == null || this.send.creatorIdentifier == null) { return null; } @@ -86,77 +93,22 @@ export class AccessComponent implements OnInit { }); } - async download() { - if (this.send == null || this.decKey == null) { - return; - } - - if (this.downloading) { - return; - } - - const downloadData = await this.sendApiService.getSendFileDownloadData( - this.send, - this.accessRequest - ); - - if (Utils.isNullOrWhitespace(downloadData.url)) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("missingSendFile")); - return; - } - - this.downloading = true; - const response = await fetch(new Request(downloadData.url, { cache: "no-store" })); - if (response.status !== 200) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); - this.downloading = false; - return; - } - - try { - const encBuf = await EncArrayBuffer.fromResponse(response); - const decBuf = await this.cryptoService.decryptFromBytes(encBuf, this.decKey); - this.fileDownloadService.download({ - fileName: this.send.file.fileName, - blobData: decBuf, - downloadMethod: "save", - }); - } catch (e) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); - } - - this.downloading = false; - } - - copyText() { - this.platformUtilsService.copyToClipboard(this.send.text.text); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("valueCopied", this.i18nService.t("sendTypeText")) - ); - } - - toggleText() { - this.showText = !this.showText; - } - - async load() { + protected load = async () => { this.unavailable = false; this.error = false; this.hideEmail = false; - const keyArray = Utils.fromUrlB64ToArray(this.key); - this.accessRequest = new SendAccessRequest(); - if (this.password != null) { - const passwordHash = await this.cryptoFunctionService.pbkdf2( - this.password, - keyArray, - "sha256", - SEND_KDF_ITERATIONS - ); - this.accessRequest.password = Utils.fromBufferToB64(passwordHash); - } try { + const keyArray = Utils.fromUrlB64ToArray(this.key); + this.accessRequest = new SendAccessRequest(); + if (this.password != null) { + const passwordHash = await this.cryptoFunctionService.pbkdf2( + this.password, + keyArray, + "sha256", + SEND_KDF_ITERATIONS + ); + this.accessRequest.password = Utils.fromBufferToB64(passwordHash); + } let sendResponse: SendAccessResponse = null; if (this.loading) { sendResponse = await this.sendApiService.postSendAccess(this.id, this.accessRequest); @@ -168,16 +120,23 @@ export class AccessComponent implements OnInit { const sendAccess = new SendAccess(sendResponse); this.decKey = await this.cryptoService.makeSendKey(keyArray); this.send = await sendAccess.decrypt(this.decKey); - this.showText = this.send.text != null ? !this.send.text.hidden : true; } catch (e) { if (e instanceof ErrorResponse) { if (e.statusCode === 401) { this.passwordRequired = true; } else if (e.statusCode === 404) { this.unavailable = true; + } else if (e.statusCode === 400) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccurred"), + e.message + ); } else { this.error = true; } + } else { + this.error = true; } } this.loading = false; @@ -186,5 +145,9 @@ export class AccessComponent implements OnInit { !this.passwordRequired && !this.loading && !this.unavailable; + }; + + protected setPassword(password: string) { + this.password = password; } } diff --git a/apps/web/src/app/tools/send/icons/expired-send.icon.ts b/apps/web/src/app/tools/send/icons/expired-send.icon.ts new file mode 100644 index 00000000000..b39cdca797d --- /dev/null +++ b/apps/web/src/app/tools/send/icons/expired-send.icon.ts @@ -0,0 +1,11 @@ +import { svgIcon } from "@bitwarden/components"; + +export const ExpiredSend = svgIcon` + + + + + + + +`; diff --git a/apps/web/src/app/tools/send/send-access-file.component.html b/apps/web/src/app/tools/send/send-access-file.component.html new file mode 100644 index 00000000000..82880407809 --- /dev/null +++ b/apps/web/src/app/tools/send/send-access-file.component.html @@ -0,0 +1,5 @@ +

{{ send.file.fileName }}

+ diff --git a/apps/web/src/app/tools/send/send-access-file.component.ts b/apps/web/src/app/tools/send/send-access-file.component.ts new file mode 100644 index 00000000000..c4e71a51629 --- /dev/null +++ b/apps/web/src/app/tools/send/send-access-file.component.ts @@ -0,0 +1,67 @@ +import { Component, Input } from "@angular/core"; + +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { SendAccessRequest } from "@bitwarden/common/tools/send/models/request/send-access.request"; +import { SendAccessView } from "@bitwarden/common/tools/send/models/view/send-access.view"; +import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; + +import { SharedModule } from "../../shared"; + +@Component({ + selector: "app-send-access-file", + templateUrl: "send-access-file.component.html", + imports: [SharedModule], + standalone: true, +}) +export class SendAccessFileComponent { + @Input() send: SendAccessView; + @Input() decKey: SymmetricCryptoKey; + @Input() accessRequest: SendAccessRequest; + constructor( + private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService, + private cryptoService: CryptoService, + private fileDownloadService: FileDownloadService, + private sendApiService: SendApiService + ) {} + + protected download = async () => { + if (this.send == null || this.decKey == null) { + return; + } + + const downloadData = await this.sendApiService.getSendFileDownloadData( + this.send, + this.accessRequest + ); + + if (Utils.isNullOrWhitespace(downloadData.url)) { + this.platformUtilsService.showToast("error", null, this.i18nService.t("missingSendFile")); + return; + } + + const response = await fetch(new Request(downloadData.url, { cache: "no-store" })); + if (response.status !== 200) { + this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + return; + } + + try { + const encBuf = await EncArrayBuffer.fromResponse(response); + const decBuf = await this.cryptoService.decryptFromBytes(encBuf, this.decKey); + this.fileDownloadService.download({ + fileName: this.send.file.fileName, + blobData: decBuf, + downloadMethod: "save", + }); + } catch (e) { + this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + } + }; +} diff --git a/apps/web/src/app/tools/send/send-access-password.component.html b/apps/web/src/app/tools/send/send-access-password.component.html new file mode 100644 index 00000000000..8bb2c306010 --- /dev/null +++ b/apps/web/src/app/tools/send/send-access-password.component.html @@ -0,0 +1,28 @@ +

{{ "sendProtectedPassword" | i18n }}

+

{{ "sendProtectedPasswordDontKnow" | i18n }}

+
+ + {{ "password" | i18n }} + + + +
+ +
+
diff --git a/apps/web/src/app/tools/send/send-access-password.component.ts b/apps/web/src/app/tools/send/send-access-password.component.ts new file mode 100644 index 00000000000..07a08fda7cd --- /dev/null +++ b/apps/web/src/app/tools/send/send-access-password.component.ts @@ -0,0 +1,36 @@ +import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { FormBuilder, Validators } from "@angular/forms"; +import { Subject, takeUntil } from "rxjs"; + +import { SharedModule } from "../../shared"; + +@Component({ + selector: "app-send-access-password", + templateUrl: "send-access-password.component.html", + imports: [SharedModule], + standalone: true, +}) +export class SendAccessPasswordComponent { + private destroy$ = new Subject(); + protected formGroup = this.formBuilder.group({ + password: ["", [Validators.required]], + }); + + @Input() loading: boolean; + @Output() setPasswordEvent = new EventEmitter(); + + constructor(private formBuilder: FormBuilder) {} + + async ngOnInit() { + this.formGroup.controls.password.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe((val) => { + this.setPasswordEvent.emit(val); + }); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } +} diff --git a/apps/web/src/app/tools/send/send-access-text.component.html b/apps/web/src/app/tools/send/send-access-text.component.html new file mode 100644 index 00000000000..ca772251146 --- /dev/null +++ b/apps/web/src/app/tools/send/send-access-text.component.html @@ -0,0 +1,26 @@ +{{ "sendHiddenByDefault" | i18n }} + + + +
+ +
+
+ +
diff --git a/apps/web/src/app/tools/send/send-access-text.component.ts b/apps/web/src/app/tools/send/send-access-text.component.ts new file mode 100644 index 00000000000..4c3c9c89675 --- /dev/null +++ b/apps/web/src/app/tools/send/send-access-text.component.ts @@ -0,0 +1,59 @@ +import { Component, Input } from "@angular/core"; +import { FormBuilder } from "@angular/forms"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SendAccessView } from "@bitwarden/common/tools/send/models/view/send-access.view"; + +import { SharedModule } from "../../shared"; + +@Component({ + selector: "app-send-access-text", + templateUrl: "send-access-text.component.html", + imports: [SharedModule], + standalone: true, +}) +export class SendAccessTextComponent { + private _send: SendAccessView = null; + protected showText = false; + + protected formGroup = this.formBuilder.group({ + sendText: [""], + }); + + constructor( + private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService, + private formBuilder: FormBuilder + ) {} + + get send(): SendAccessView { + return this._send; + } + + @Input() set send(value: SendAccessView) { + this._send = value; + this.showText = this.send.text != null ? !this.send.text.hidden : true; + + if (this.send == null || this.send.text == null) { + return; + } + + this.formGroup.controls.sendText.patchValue( + this.showText ? this.send.text.text : this.send.text.maskedText + ); + } + + protected copyText() { + this.platformUtilsService.copyToClipboard(this.send.text.text); + this.platformUtilsService.showToast( + "success", + null, + this.i18nService.t("valueCopied", this.i18nService.t("sendTypeText")) + ); + } + + protected toggleText() { + this.showText = !this.showText; + } +} diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index 707d1859d2e..d2549d6df8d 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -4218,8 +4218,8 @@ "message": "Hierdie send is by verstek versteek. U kan sy sigbaarheid wissel deur die knop hier onder.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Laai lêer af" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "The Send you are trying to access does not exist or is no longer available.", @@ -4609,8 +4609,8 @@ "message": "om dit vandag te probeer.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden-gebruiker $USER_IDENTIFIER$ het die volgende met u gedeel", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Volgende" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Gekose streekvlag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index 73fa96dbbb3..bbb7d9ba4b2 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -4218,8 +4218,8 @@ "message": "This Send is hidden by default. You can toggle its visibility using the button below.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Download file" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "The Send you are trying to access does not exist or is no longer available.", @@ -4609,8 +4609,8 @@ "message": "لتجربته اليوم.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden user $USER_IDENTIFIER$ shared the following with you", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Next" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index 8807d07e090..298c927bad1 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -4218,8 +4218,8 @@ "message": "Bu \"send\" ilkin olaraq gizlidir. Aşağıdakı düyməni istifadə edərək görünməni dəyişdirə bilərsiniz.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Faylı endir" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "Müraciət etməyə çalışdığınız Send yoxdur və ya artıq əlçatmazdır.", @@ -4609,8 +4609,8 @@ "message": "bu gün sınamaq üçün.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "$USER_IDENTIFIER$ Bitwarden istifadəçisi aşağıdakıları sizinlə paylaşdı", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Növbəti" }, + "ssoLoginIsRequired": { + "message": "SSO girişi tələb olunur" + }, "selectedRegionFlag": { "message": "Seçilmiş bölgə bayrağı" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index 464f0b0925f..578d711ac3d 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -4218,8 +4218,8 @@ "message": "Прадвызначана гэты Send схаваны. Вы можаце змяніць яго бачнасць выкарыстоўваючы кнопку ніжэй.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Спампаваць файл" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "Send да якога вы спрабуеце атрымаць доступ не існуе або больш недаступны.", @@ -4609,8 +4609,8 @@ "message": "каб паспрабаваць сёння.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Карыстальнік Bitwarden $USER_IDENTIFIER$ абагуліў з вамі", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Далей" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Сцяг выбранага рэгіёна" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index 53be0706293..5a5f821e1a5 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -2763,7 +2763,7 @@ "message": "Трезор по уеб" }, "cli": { - "message": "CLI" + "message": "ИКР" }, "bitWebVault": { "message": "Трезор по уеб" @@ -4218,8 +4218,8 @@ "message": "Стандартно изпращането е скрито. Може да промените това като натиснете бутона по-долу.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Изтеглете файл" + "downloadAttachments": { + "message": "Сваляне на прикачените файлове" }, "sendAccessUnavailable": { "message": "Изпращането, което се опитвате да достъпите, е изтрито или вече не е налично.", @@ -4609,8 +4609,8 @@ "message": "за да го пробвате още днес.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Потребителят на Битуорден — $USER_IDENTIFIER$, сподели следното с вас", + "sendAccessCreatorIdentifier": { + "message": "Членът на Битуорден — $USER_IDENTIFIER$, сподели следното с вас", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Next" }, + "ssoLoginIsRequired": { + "message": "Необходимо е вписване чрез еднократно удостоверяване" + }, "selectedRegionFlag": { "message": "Знаме на избрания регион" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Достъпът до проекта е актуализиран" + }, + "unexpectedErrorSend": { + "message": "Възникна неочаквана грешка при зареждането на това изпращане. Опитайте отново по-късно." } } diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index be343cbb0ab..8e271a0c621 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -4218,8 +4218,8 @@ "message": "This Send is hidden by default. You can toggle its visibility using the button below.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Download file" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "The Send you are trying to access does not exist or is no longer available.", @@ -4609,8 +4609,8 @@ "message": "to try it today.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden user $USER_IDENTIFIER$ shared the following with you", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Next" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index e2ee1b4f675..f4ae92cb1bd 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -4218,8 +4218,8 @@ "message": "This Send is hidden by default. You can toggle its visibility using the button below.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Download file" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "The Send you are trying to access does not exist or is no longer available.", @@ -4609,8 +4609,8 @@ "message": "to try it today.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden user $USER_IDENTIFIER$ shared the following with you", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Next" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index c5b8467d13a..7e7c4c4b114 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -4218,8 +4218,8 @@ "message": "Aquest Send està ocult per defecte. Podeu canviar la seua visibilitat mitjançant el botó següent.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Baixa el fitxer" + "downloadAttachments": { + "message": "Baixa els fitxers adjunts" }, "sendAccessUnavailable": { "message": "L'enviament al qual intenteu accedir no existeix o ja no està disponible.", @@ -4609,8 +4609,8 @@ "message": "per provar-ho hui.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "L'usuari bitwarden $USER_IDENTIFIER$ ha compartit el següent amb vosaltres", + "sendAccessCreatorIdentifier": { + "message": "El membre de Bitwarden $USER_IDENTIFIER$ us ha compartit això", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Següent" }, + "ssoLoginIsRequired": { + "message": "Inici de sessió SSO necessari" + }, "selectedRegionFlag": { "message": "Bandera de la regió seleccionada" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "S'ha actualitzat l'accés al projecte" + }, + "unexpectedErrorSend": { + "message": "S'ha produït un error inesperat en carregar aquest enviament. Torneu a provar-ho més tard." } } diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index 6105ceae85a..c567d2981e8 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -4218,8 +4218,8 @@ "message": "Tento Send je ve výchozím nastavení skrytý. Viditelnost můžete přepnout pomocí tlačítka níže.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Stáhnout soubor" + "downloadAttachments": { + "message": "Stáhnout přílohy" }, "sendAccessUnavailable": { "message": "Send, ke kterému se pokoušíte přistupovat, neexistuje nebo již není k dispozici.", @@ -4609,7 +4609,7 @@ "message": "abyste to vyzkoušeli ještě dnes.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { + "sendAccessCreatorIdentifier": { "message": "Uživatel Bitwarden $USER_IDENTIFIER$ s Vámi sdílí následující", "placeholders": { "user_identifier": { @@ -7182,6 +7182,9 @@ "next": { "message": "Další" }, + "ssoLoginIsRequired": { + "message": "Přihlášení SSO je povinné" + }, "selectedRegionFlag": { "message": "Vlajka zvoleného regionu" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Přístup k projektu byl aktualizován" + }, + "unexpectedErrorSend": { + "message": "Při načítání tohoto Sendu došlo k neočekávané chybě. Zkuste to znovu později." } } diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index c901cb95299..77e07d67954 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -4218,8 +4218,8 @@ "message": "This Send is hidden by default. You can toggle its visibility using the button below.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Download file" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "The Send you are trying to access does not exist or is no longer available.", @@ -4609,8 +4609,8 @@ "message": "to try it today.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden user $USER_IDENTIFIER$ shared the following with you", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Next" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index 19f847d13ab..c4c294b6e83 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -4218,8 +4218,8 @@ "message": "Denne Send er som standard skjult. Dens synlighed kan ændres vha. knappen nedenfor.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Download fil" + "downloadAttachments": { + "message": "Download vedhæftninger" }, "sendAccessUnavailable": { "message": "Den Send, der forsøges tilgået, findes ikke, eller er ikke længere tilgængelig.", @@ -4609,8 +4609,8 @@ "message": "for at prøve det i dag.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden-bruger $USER_IDENTIFIER$ delte flg. med dig", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden-brugeren $USER_IDENTIFIER$ delte flg. med dig", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Næste" }, + "ssoLoginIsRequired": { + "message": "SSO-login er obligatorisk" + }, "selectedRegionFlag": { "message": "Valgte områdeflag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Projektadgang opdateret" + }, + "unexpectedErrorSend": { + "message": "En uventet fejl opstod under indlæsning af denne Send. Forsøg igen senere." } } diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index be019a8d530..d6bf3cab26a 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -648,22 +648,22 @@ "message": "Benenne deinen Passkey, um ihn zu identifizieren." }, "useForVaultEncryption": { - "message": "Use for vault encryption" + "message": "Zur Tresorverschlüsselung verwenden" }, "useForVaultEncryptionInfo": { - "message": "Log in and unlock on supported devices without your master password. Follow the prompts from your browser to finalize setup." + "message": "Anmelden und Entsperren auf unterstützten Geräten ohne dein Master-Passwort. Folge den Anweisungen deines Browsers, um die Einrichtung abzuschließen." }, "useForVaultEncryptionErrorReadingPasskey": { - "message": "Error reading passkey. Try again or uncheck this option." + "message": "Fehler beim Lesen des Passkeys. Versuche es erneut oder deaktiviere diese Option." }, "encryptionNotSupported": { "message": "Verschlüsselung nicht unterstützt" }, "encryptionNotEnabled": { - "message": "Encryption not enabled" + "message": "Verschlüsselung nicht aktiviert" }, "usedForEncryption": { - "message": "Used for encryption" + "message": "Wird für die Verschlüsselung verwendet" }, "loginWithPasskeyEnabled": { "message": "Anmeldung mit Passkey aktiviert" @@ -4020,7 +4020,7 @@ "message": "All Teams Starter features, plus:" }, "chooseMonthlyOrAnnualBilling": { - "message": "Choose monthly or annual billing" + "message": "Monatliche oder jährliche Abrechnung wählen" }, "abilityToAddMoreThanNMembers": { "message": "Ability to add more than $COUNT$ members", @@ -4218,8 +4218,8 @@ "message": "Dieses Send ist standardmäßig ausgeblendet. Du kannst die Sichtbarkeit mit dem Button unten umschalten.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Datei herunterladen" + "downloadAttachments": { + "message": "Anhänge herunterladen" }, "sendAccessUnavailable": { "message": "Das Send, auf das du zugreifen möchtest, existiert nicht oder ist nicht mehr verfügbar.", @@ -4609,8 +4609,8 @@ "message": "um es heute auszuprobieren.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden Benutzer $USER_IDENTIFIER$ hat Folgendes mit dir geteilt", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden-Mitglied $USER_IDENTIFIER$ hat Folgendes mit dir geteilt", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Weiter" }, + "ssoLoginIsRequired": { + "message": "SSO-Anmeldung ist erforderlich" + }, "selectedRegionFlag": { "message": "Flagge der ausgewählten Region" }, @@ -7302,7 +7305,7 @@ "message": "Manage the collection behavior for the organization" }, "limitCollectionCreationDeletionDesc": { - "message": "Limit collection creation and deletion to owners and admins" + "message": "Sammlungserstellung und -löschung auf Eigentümer und Administratoren beschränken" }, "collectionManagementUpdated": { "message": "Collection management behavior saved" @@ -7354,10 +7357,10 @@ "message": "Beta" }, "assignCollectionAccess": { - "message": "Assign collection access" + "message": "Sammlungszugriff zuweisen" }, "editedCollections": { - "message": "Edited collections" + "message": "Bearbeitete Sammlungen" }, "baseUrl": { "message": "Server-URL" @@ -7391,6 +7394,9 @@ "description": "This is followed a by a hyperlink to the help website." }, "projectAccessUpdated": { - "message": "Project access updated" + "message": "Projektzugriff aktualisiert" + }, + "unexpectedErrorSend": { + "message": "Beim Laden dieses Sends ist ein unerwarteter Fehler aufgetreten. Versuche es später erneut." } } diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index 017c86c0eb8..d7ff3c648b6 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -4218,8 +4218,8 @@ "message": "Αυτό το send είναι κρυμμένο από προεπιλογή. Μπορείτε να αλλάξετε την ορατότητά του χρησιμοποιώντας το παρακάτω κουμπί.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Λήψη Αρχείου" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "Το Send που προσπαθείτε να αποκτήσετε πρόσβαση, δεν υπάρχει ή δεν είναι πλέον διαθέσιμο.", @@ -4609,8 +4609,8 @@ "message": "για να το δοκιμάσετε σήμερα.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Ο χρήστης Bitwarden $USER_IDENTIFIER$ κοινοποίησε τα ακόλουθα μαζί σας", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Next" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 44bb8a45222..7330e5053cd 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -4218,8 +4218,8 @@ "message": "This Send is hidden by default. You can toggle its visibility using the button below.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Download file" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "The Send you are trying to access does not exist or is no longer available.", @@ -4609,8 +4609,8 @@ "message": "to try it today.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden user $USER_IDENTIFIER$ shared the following with you", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Next" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index 8242cbc0d62..ecbabbde081 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -4218,8 +4218,8 @@ "message": "This Send is hidden by default. You can toggle its visibility using the button below.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Download file" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "The Send you are trying to access does not exist or is no longer available.", @@ -4609,8 +4609,8 @@ "message": "to try it today.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden user $USER_IDENTIFIER$ shared the following with you", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Next" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index 0a1cb6f8b7c..244c300d817 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -4218,8 +4218,8 @@ "message": "This send is hidden by default. You can toggle its visibility using the button below.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Download File" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "The Send you are trying to access does not exist or is no longer available.", @@ -4609,8 +4609,8 @@ "message": "to try it today.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden user $USER_IDENTIFIER$ shared the following with you", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Next" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index c0e529f7848..4d1e883a162 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -4218,8 +4218,8 @@ "message": "Ĉi tiu sendado estas kaŝita defaŭlte. Vi povas ŝalti ĝian videblecon per la suba butono.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Elŝuti dosieron" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "La Sendo, kiun vi provas aliri, ne ekzistas aŭ ne plu haveblas.", @@ -4609,8 +4609,8 @@ "message": "provi ĝin hodiaŭ.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden-uzanto $USER_IDENTIFIER$ dividis la jenon kun vi", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Next" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index 8f1b30d315f..26a70699bdb 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -4218,8 +4218,8 @@ "message": "Este Send está oculto por defecto. Puede cambiar su visibilidad usando el botón de abajo.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Descargar archivo" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "El envío al que está intentando acceder no existe o ya no está disponible.", @@ -4252,7 +4252,7 @@ "message": "Añadir contacto de emergencia" }, "designatedEmergencyContacts": { - "message": "Diseñado como contacto de emergencia" + "message": "Designado como contacto de emergencia" }, "noGrantedAccess": { "message": "Aún no has sido designado como un contacto de emergencia para nadie." @@ -4609,8 +4609,8 @@ "message": "pruébalo hoy.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Usuario $USER_IDENTIFIER$ de Bitwarden compartió contigo", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Siguiente" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Región seleccionada" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index 175167d6706..456ed4f3d72 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -4218,8 +4218,8 @@ "message": "See Send on vaikeseades peidetud. Saad selle nähtavust alloleva nupu abil seadistada.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Laadi fail alla" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "Send, millele üritad ligi pääseda, ei eksisteeri või see pole enam saadaval.", @@ -4609,8 +4609,8 @@ "message": "et seda ise proovida.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwardeni kasutaja $USER_IDENTIFIER$ jagas sinuga järgnevat", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Next" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index 1bae995b8a4..7c67cce7f25 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -4218,8 +4218,8 @@ "message": "Send hau modu lehenetsian ezkutatuta dago. Beheko botoia sakatuz alda dezakezu ikusgarritasuna.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Deskargatu fitxategia" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "Sartzen saiatzen ari zaren Send-a ez da existitzen edo ez dago erabilgarri.", @@ -4609,8 +4609,8 @@ "message": "gaur probatzeko.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "$USER_IDENTIFIER$ Bitwarden erabiltzaileak, ondorengoa zurekin partekatu du", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Next" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index 4007656a084..89299d8ab26 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -4218,8 +4218,8 @@ "message": "این ارسال به طور پیش‌فرض پنهان است. با استفاده از دکمه زیر می‌توانید نمایان بودن آن را تغییر دهید.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "بارگیری پرونده" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "ارسالی که می‌خواهید به آن دسترسی پیدا کنید وجود ندارد یا دیگر در دسترس نیست.", @@ -4609,8 +4609,8 @@ "message": "برای امتحان کردن امروز.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "کاربر Bitwarden $USER_IDENTIFIER$ موارد زیر را با شما به اشتراک گذاشت", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "بعدی" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "پرچم منطقه انتخاب شد" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index 82b8a23ab36..14b954f26aa 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -4218,8 +4218,8 @@ "message": "Send on oletusarvoisesti piilotettu. Voit vaihtaa sen näkyvyyttä alla olevalla painikkeella.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Lataa tiedosto" + "downloadAttachments": { + "message": "Lataa liitteet" }, "sendAccessUnavailable": { "message": "Sendiä, jota yrität avata, ei ole olemassa tai se ei ole enää käytettävissä.", @@ -4609,8 +4609,8 @@ "message": "kokeillaksesi sitä tänään.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden-käyttäjä $USER_IDENTIFIER$ jakoi kanssasi seuraavat", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden-jäsen $USER_IDENTIFIER$ jakoi sinulle seuraavat", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Seuraava" }, + "ssoLoginIsRequired": { + "message": "Kertakirjautuminen vaaditaan" + }, "selectedRegionFlag": { "message": "Valitun alueen lippu" }, @@ -7391,6 +7394,9 @@ "description": "This is followed a by a hyperlink to the help website." }, "projectAccessUpdated": { - "message": "Project access updated" + "message": "Projektin käyttöoikeudet on muutettu" + }, + "unexpectedErrorSend": { + "message": "Odottamaton virhe ladattaessa Sendiä. Yritä myöhemmin uudelleen." } } diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index 2d1f7761b29..0b79921ff0c 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -4218,8 +4218,8 @@ "message": "Ang Send na ito ay nakatago bilang default. Maaari mong i toggle ang visibility nito gamit ang pindutan sa ibaba.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Mag-download ng file" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "Ang Send na sinusubukan mong ma access ay hindi umiiral o hindi na magagamit.", @@ -4609,8 +4609,8 @@ "message": "para masubukan ngayon.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Ibinahagi sa iyo ng Bitwarden user na si $USER_IDENTIFIER$ ang sumusunod", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Next" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index f996b5fecc8..e5a5e0e9563 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -4218,8 +4218,8 @@ "message": "Ce Send est masqué par défaut. Vous pouvez changer sa visibilité en utilisant le bouton ci-dessous.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Télécharger le fichier" + "downloadAttachments": { + "message": "Télécharger les pièces jointes" }, "sendAccessUnavailable": { "message": "Le Send que vous essayez d'accéder n'existe pas ou n'est plus disponible.", @@ -4609,8 +4609,8 @@ "message": "pour l'essayer dès aujourd'hui.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "L'utilisateur Bitwarden $USER_IDENTIFIER$ a partagé ce qui suit avec vous", + "sendAccessCreatorIdentifier": { + "message": "Le membre Bitwarden $USER_IDENTIFIER$ a partagé ce qui suit avec vous", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Suivant" }, + "ssoLoginIsRequired": { + "message": "La connexion SSO est requise" + }, "selectedRegionFlag": { "message": "Drapeau de la région sélectionnée" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Accès au projet mis à jour" + }, + "unexpectedErrorSend": { + "message": "Une erreur inattendue est survenue lors du chargement de ce Send. Réessayez plus tard." } } diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index c901cb95299..77e07d67954 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -4218,8 +4218,8 @@ "message": "This Send is hidden by default. You can toggle its visibility using the button below.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Download file" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "The Send you are trying to access does not exist or is no longer available.", @@ -4609,8 +4609,8 @@ "message": "to try it today.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden user $USER_IDENTIFIER$ shared the following with you", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Next" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index 92625241db3..1ab9f723784 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -4218,8 +4218,8 @@ "message": "הSend הזה מוסתר כברירת מחדל. באפשרותך לשנות את מצב ההסתרה בעזרת הכפתור להלן.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "הורד קובץ" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "The Send you are trying to access does not exist or is no longer available.", @@ -4609,8 +4609,8 @@ "message": "to try it today.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden user $USER_IDENTIFIER$ shared the following with you", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Next" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index 4c9053bd334..d3f42f2b036 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -4218,8 +4218,8 @@ "message": "This Send is hidden by default. You can toggle its visibility using the button below.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Download file" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "The Send you are trying to access does not exist or is no longer available.", @@ -4609,8 +4609,8 @@ "message": "to try it today.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden user $USER_IDENTIFIER$ shared the following with you", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Next" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index 11f291787e6..65e7d69e90f 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -612,43 +612,43 @@ "message": "Prijava glavnom lozinkom" }, "loginWithPasskey": { - "message": "Log in with passkey" + "message": "Prijava pristupnim ključem" }, "loginWithPasskeyInfo": { "message": "Use a generated passkey that will automatically log you in without a password. Biometrics, like facial recognition or fingerprint, or another FIDO2 security method will verify your identity." }, "newPasskey": { - "message": "New passkey" + "message": "Novi pristupni ključ" }, "learnMoreAboutPasswordless": { "message": "Learn more about passwordless" }, "passkeyEnterMasterPassword": { - "message": "Enter your master password to modify log in with passkey settings." + "message": "Unesi glavnu lozinku za promjenu postavki prijave pristupnim ključem." }, "creatingPasskeyLoading": { - "message": "Creating passkey..." + "message": "Stvaranje pristupnog ključa..." }, "creatingPasskeyLoadingInfo": { - "message": "Keep this window open and follow prompts from your browser." + "message": "Ostavi ovaj prozor otvoren i prati upute u svojem pregledniku." }, "errorCreatingPasskey": { - "message": "Error creating passkey" + "message": "Greška kreiranja pristupnog ključa" }, "errorCreatingPasskeyInfo": { - "message": "There was a problem creating your passkey." + "message": "Došlo je do problema sa stvaranjem pristupnog ključa." }, "passkeySuccessfullyCreated": { - "message": "Passkey successfully created!" + "message": "Pristupni ključ je uspješno stvoren!" }, "customName": { - "message": "Custom name" + "message": "Naziv" }, "customPasskeyNameInfo": { "message": "Name your passkey to help you identify it." }, "useForVaultEncryption": { - "message": "Use for vault encryption" + "message": "Koristi enkripciju trezora" }, "useForVaultEncryptionInfo": { "message": "Log in and unlock on supported devices without your master password. Follow the prompts from your browser to finalize setup." @@ -1048,7 +1048,7 @@ "message": "Ovaj izvoz sadrži podatke trezora u nešifriranom obliku! Izvezenu datoteku se ne bi smjelo pohranjivati ili slati putem nesigurnih kanala (npr. e-poštom). Izbriši ju odmah nakon završetka korištenja." }, "exportSecretsWarningDesc": { - "message": "This export contains your secrets data in an unencrypted format. You should not store or send the exported file over unsecure channels (such as email). Delete it immediately after you are done using it." + "message": "Ovaj izvoz sadrži podatke trezora u nešifriranom obliku! Izvezenu datoteku se ne bi smjelo pohranjivati ili slati putem nesigurnih kanala (npr. e-poštom). Izbriši ju odmah nakon završetka korištenja." }, "encExportKeyWarningDesc": { "message": "Ovaj izvoz šifrira tvoje podatke koristeći ključ za šifriranje. Promijeniš li naknadno ključ za šifriranje, potrebno je ponovno napraviti izvoz jer nećeš moći dešifrirati ovu izvezenu datoteku." @@ -1063,7 +1063,7 @@ "message": "Izvezi trezor" }, "exportSecrets": { - "message": "Export secrets" + "message": "Izvezi tajne" }, "fileFormat": { "message": "Format datoteke" @@ -1394,7 +1394,7 @@ } }, "importUnassignedItemsError": { - "message": "File contains unassigned items." + "message": "Datoteka sadrži nedodijeljene stavke." }, "selectFormat": { "message": "Odaberi format datoteke za uvoz" @@ -2009,7 +2009,7 @@ "message": "1 GB šifriranog prostora za pohranu podataka." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Mogućnosti za prijavu u dva koraka kao što su YubiKey i Duo." }, "premiumSignUpEmergency": { "message": "Pristup u nuždi" @@ -3678,7 +3678,7 @@ "message": "Organizacija suspendirana" }, "secretsAccessSuspended": { - "message": "Suspended organizations cannot be accessed. Please contact your organization owner for assistance." + "message": "Stavkama u suspendiranoj Organizaciji se ne može pristupiti. Kontaktiraj vlasnika Organizacije za pomoć." }, "secretsCannotCreate": { "message": "Secrets cannot be created in suspended organizations. Please contact your organization owner for assistance." @@ -4017,7 +4017,7 @@ "message": "Sve značajke Team, plus:" }, "includeAllTeamsStarterFeatures": { - "message": "All Teams Starter features, plus:" + "message": "Sve značajke Team, plus:" }, "chooseMonthlyOrAnnualBilling": { "message": "Choose monthly or annual billing" @@ -4218,8 +4218,8 @@ "message": "Ovaj je Send zadano skriven. Moguće mu je promijeniti vidljivost.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Preuzmi datoteku" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "Send kojem pokušavaš pristupiti više ne postoji ili više nije dostupan.", @@ -4609,8 +4609,8 @@ "message": "za isprobavanje.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden korisnik $USER_IDENTIFIER$ je s tobom podijelio", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -4851,7 +4851,7 @@ "message": "Greška" }, "accountRecoveryManageUsers": { - "message": "Manage users must also be granted with the manage account recovery permission" + "message": "Upravljanje korisnicima mora također biti uključeno s dozvolom za Upravljanje ponovnim postavljanjem lozinke" }, "setupProvider": { "message": "Postavke davatelja" @@ -4988,7 +4988,7 @@ "message": "Tvoja glavna lozinka ne zadovoljava pravila ove organizacije. Kako bi se pridružio organizaciji, moraš odmah ažurirati svoju glavnu lozinku. Ako nastaviš, odjavit ćeš se iz trenutne sesije te ćeš se morati ponovno prijaviti. Aktivne sesije na drugim uređajima mogu ostati aktivne do jedan sat." }, "updateWeakMasterPasswordWarning": { - "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." + "message": "Tvoja glavna lozinka ne zadovoljava pravila ove organizacije. Za pristup trezoru moraš odmah ažurirati svoju glavnu lozinku. Ako nastaviš, odjaviti ćeš se iz trenutne sesije te ćeš se morati ponovno prijaviti. Aktivne sesije na drugim uređajima mogu ostati aktivne do jedan sat." }, "maximumVaultTimeout": { "message": "Istek trezora" @@ -5367,7 +5367,7 @@ "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Use the require single-sign-on authentication policy to require all members to log in with SSO.'" }, "ssoPolicyHelpAnchor": { - "message": "require single sign-on authentication policy", + "message": "zahtijevaj SSO autentifikaciju", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Use the require single-sign-on authentication policy to require all members to log in with SSO.'" }, "ssoPolicyHelpEnd": { @@ -7055,11 +7055,11 @@ "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { - "message": "Your organization permissions were updated, requiring you to set a master password.", + "message": "Moraš postaviti glavnu lozinku jer su dopuštenja tvoje organizacije ažurirana.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "orgRequiresYouToSetPassword": { - "message": "Your organization requires you to set a master password.", + "message": "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" }, "notFound": { @@ -7092,7 +7092,7 @@ "message": "Activate Secrets Manager" }, "yourOrganizationsFingerprint": { - "message": "Your organization's fingerprint phrase", + "message": "Jedinstvena fraza tvoje organizacije", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their organization's public key with another user, for the purposes of sharing." }, "deviceApprovals": { @@ -7102,7 +7102,7 @@ "message": "Approve login requests below to allow the requesting member to finish logging in. Unapproved requests expire after 1 week. Verify the member’s information before approving." }, "deviceInfo": { - "message": "Device info" + "message": "Informacije o uređaju" }, "time": { "message": "Vrijeme" @@ -7182,6 +7182,9 @@ "next": { "message": "Sljedeće" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Zastava odabrane regije" }, @@ -7387,10 +7390,13 @@ "message": "The passkey will not be copied to the cloned item. Do you want to continue cloning this item?" }, "seeDetailedInstructions": { - "message": "See detailed instructions on our help site at", + "message": "Detaljne upute za pomoć pronađi na našoj stranici za pomoć na", "description": "This is followed a by a hyperlink to the help website." }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index 1fcb4ebecfb..a7ef618f490 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -4218,8 +4218,8 @@ "message": "Ez a Send alapértelmezésben rejtett. Az alábbi gombbal átváltható a láthatósága.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Fájl letöltése" + "downloadAttachments": { + "message": "Mellékletek letöltése" }, "sendAccessUnavailable": { "message": "Az elérendő küldés nem létezik vagy már nem elérhető.", @@ -4609,7 +4609,7 @@ "message": "próbáljuk ki még ma.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { + "sendAccessCreatorIdentifier": { "message": "$USER_IDENTIFIER$ Bitwarden felhasználó megosztotta a következőket", "placeholders": { "user_identifier": { @@ -7182,6 +7182,9 @@ "next": { "message": "Következő" }, + "ssoLoginIsRequired": { + "message": "SSO bejelentkezés szükséges" + }, "selectedRegionFlag": { "message": "Kiválasztott régió zászló" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "A projekt elérés frissítésre került." + }, + "unexpectedErrorSend": { + "message": "Váratlan hiba történt a Send betöltésekor. Próbáljuk újra később." } } diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index 4ae055a9bb8..fa0aa9340d2 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -4218,8 +4218,8 @@ "message": "Pengiriman ini disembunyikan secara default. Anda dapat mengubah visibilitasnya menggunakan tombol di bawah ini.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Unduh berkas" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "Pengiriman yang Anda coba akses tidak ada atau tidak lagi tersedia.", @@ -4609,8 +4609,8 @@ "message": "untuk mencobanya hari ini.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Pengguna Bitwarden $USER_IDENTIFIER$ berbagi yang berikut dengan Anda", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Next" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index e3de47e8f7f..f7e9ea0ed0c 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -4218,8 +4218,8 @@ "message": "Questo Send è nascosto per impostazione predefinita. Modifica la sua visibilità usando questo pulsante.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Scarica file" + "downloadAttachments": { + "message": "Scarica allegati" }, "sendAccessUnavailable": { "message": "Il Send a cui stai provando ad accedere non esiste o non è più disponibile.", @@ -4609,8 +4609,8 @@ "message": "per provarlo oggi.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "L'utente Bitwarden $USER_IDENTIFIER$ ha condiviso questo con te", + "sendAccessCreatorIdentifier": { + "message": "Il membro di Bitwarden $USER_IDENTIFIER$ ha condiviso questo con te", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Avanti" }, + "ssoLoginIsRequired": { + "message": "Il login SSO è obbligatorio" + }, "selectedRegionFlag": { "message": "Bandiera della regione selezionata" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Accesso al progetto aggiornato" + }, + "unexpectedErrorSend": { + "message": "Si è verificato un errore imprevisto durante il caricamento di questo Send. Riprova più tardi." } } diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index f60fc39ecc7..09de6c0838a 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -4218,8 +4218,8 @@ "message": "このSendはデフォルトでは非表示になっています。下のボタンで表示・非表示が切り替え可能です。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "ファイルをダウンロード" + "downloadAttachments": { + "message": "添付ファイルをダウンロード" }, "sendAccessUnavailable": { "message": "アクセスしようとしているSendは存在しないか、利用できません。", @@ -4609,8 +4609,8 @@ "message": "試してみてください", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden ユーザー $USER_IDENTIFIER$ が共有しました", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden メンバー $USER_IDENTIFIER$ が共有しました", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "次へ" }, + "ssoLoginIsRequired": { + "message": "SSO ログインが必要です" + }, "selectedRegionFlag": { "message": "リージョン選択" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "プロジェクトへのアクセスを更新しました" + }, + "unexpectedErrorSend": { + "message": "この Send の読み込み中に予期しないエラーが発生しました。後でもう一度お試しください。" } } diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index 2b0532679a3..a3fe70ddd10 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -4218,8 +4218,8 @@ "message": "This Send is hidden by default. You can toggle its visibility using the button below.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Download file" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "The Send you are trying to access does not exist or is no longer available.", @@ -4609,8 +4609,8 @@ "message": "to try it today.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden user $USER_IDENTIFIER$ shared the following with you", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Next" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index c901cb95299..77e07d67954 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -4218,8 +4218,8 @@ "message": "This Send is hidden by default. You can toggle its visibility using the button below.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Download file" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "The Send you are trying to access does not exist or is no longer available.", @@ -4609,8 +4609,8 @@ "message": "to try it today.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden user $USER_IDENTIFIER$ shared the following with you", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Next" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index 42f24c3735b..ca6c70819bd 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -4218,8 +4218,8 @@ "message": "ಈ ಕಳುಹಿಸುವಿಕೆಯನ್ನು ಪೂರ್ವನಿಯೋಜಿತವಾಗಿ ಮರೆಮಾಡಲಾಗಿದೆ. ಕೆಳಗಿನ ಬಟನ್ ಬಳಸಿ ನೀವು ಅದರ ಗೋಚರತೆಯನ್ನು ಟಾಗಲ್ ಮಾಡಬಹುದು.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "ಫೈಲ್ ಡೌನ್‌ಲೋಡ್ ಮಾಡಿ" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "ನೀವು ಪ್ರವೇಶಿಸಲು ಪ್ರಯತ್ನಿಸುತ್ತಿರುವ ಕಳುಹಿಸುವಿಕೆಯು ಅಸ್ತಿತ್ವದಲ್ಲಿಲ್ಲ ಅಥವಾ ಇನ್ನು ಮುಂದೆ ಲಭ್ಯವಿಲ್ಲ.", @@ -4609,8 +4609,8 @@ "message": "ಇಂದು ಅದನ್ನು ಪ್ರಯತ್ನಿಸಲು.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "ಬಿಟ್‌ವಾರ್ಡೆನ್ ಬಳಕೆದಾರ $USER_IDENTIFIER$ ಈ ಕೆಳಗಿನವುಗಳನ್ನು ನಿಮ್ಮೊಂದಿಗೆ ಹಂಚಿಕೊಂಡಿದ್ದಾರೆ", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Next" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index 016247cb96e..80ac34fa7c4 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -4218,8 +4218,8 @@ "message": "이 Send는 기본적으로 숨겨져 있습니다. 아래의 버튼을 눌러 공개 여부를 전환할 수 있습니다.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "파일 다운로드" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "접근하려고 하는 Send가 존재하지 않거나 더이상 제공되지 않습니다.", @@ -4609,8 +4609,8 @@ "message": "해서 체험해보세요.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden의 $USER_IDENTIFIER$ 사용자가 다음 내용을 당신과 공유했습니다", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Next" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index 37f819a12d8..986f44caf9a 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -4218,8 +4218,8 @@ "message": "Šis Send ir paslēpts pēc noklusējuma. Tā redzamību var pārslēgt ar zemāk esošo pogu.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Lejupielādēt datni" + "downloadAttachments": { + "message": "Lejupielādēt pielikumus" }, "sendAccessUnavailable": { "message": "Send, kuram mēģini piekļūt, nepastāv vai vairs nav pieejams.", @@ -4609,8 +4609,8 @@ "message": "izmēģinātu šodien.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden lietotājs $USER_IDENTIFIER$ kopīgoja sekojošo", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden dalībnieks $USER_IDENTIFIER$ kopīgoja sekojošo", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Turpināt" }, + "ssoLoginIsRequired": { + "message": "Vienotā pieteikšanās ir nepieciešama" + }, "selectedRegionFlag": { "message": "Atlasītā apgabala karogs" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Projekta piekļuve ir atjaunināta" + }, + "unexpectedErrorSend": { + "message": "Atgadījās neparedzēta kļūda šī Send ielādēšanas laikā. Vēlāk jāmēģina vēlreiz." } } diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index 448142b52db..ebd600ce3c0 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -4218,8 +4218,8 @@ "message": "ഈ Send സ്ഥിരസ്ഥിതിയായി മറച്ചിരിക്കുന്നു. ചുവടെയുള്ള ബട്ടൺ ഉപയോഗിച്ചാൽ നിങ്ങൾക്ക് അതിന്റെ ദൃശ്യപരത ടോഗിൾ ചെയ്യാൻ കഴിയും.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "ഫയൽ ഡൗൺലോഡുചെയ്യുക" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "The Send you are trying to access does not exist or is no longer available.", @@ -4609,8 +4609,8 @@ "message": "to try it today.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden user $USER_IDENTIFIER$ shared the following with you", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Next" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index c901cb95299..77e07d67954 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -4218,8 +4218,8 @@ "message": "This Send is hidden by default. You can toggle its visibility using the button below.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Download file" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "The Send you are trying to access does not exist or is no longer available.", @@ -4609,8 +4609,8 @@ "message": "to try it today.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden user $USER_IDENTIFIER$ shared the following with you", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Next" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index c901cb95299..77e07d67954 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -4218,8 +4218,8 @@ "message": "This Send is hidden by default. You can toggle its visibility using the button below.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Download file" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "The Send you are trying to access does not exist or is no longer available.", @@ -4609,8 +4609,8 @@ "message": "to try it today.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden user $USER_IDENTIFIER$ shared the following with you", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Next" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index e8b3c693a06..6c4ff5e3117 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -4218,8 +4218,8 @@ "message": "Denne Send-en er skjult som standard. Du kan veksle synlighet ved å bruke knappen nedenfor.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Last ned fil" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "Send-en du forsøker å få tilgang til finnes ikke eller er ikke lenger tilgjengelig.", @@ -4609,8 +4609,8 @@ "message": "for å prøve det i dag.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden-bruker $USER_IDENTIFIER$ delte følgende med deg", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Next" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index 7f216d27b10..05c85aa7858 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -4218,8 +4218,8 @@ "message": "This Send is hidden by default. You can toggle its visibility using the button below.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Download file" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "The Send you are trying to access does not exist or is no longer available.", @@ -4609,8 +4609,8 @@ "message": "to try it today.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden user $USER_IDENTIFIER$ shared the following with you", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Next" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index 052420c6bdd..81222b6059e 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -4218,8 +4218,8 @@ "message": "Deze Send is standaard verborgen. Je kunt de zichtbaarheid ervan in- en uitschakelen met de knop hieronder.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Bestand downloaden" + "downloadAttachments": { + "message": "Bijlagen downloaden" }, "sendAccessUnavailable": { "message": "De Send die je probeert te benaderen is niet (langer) beschikbaar.", @@ -4609,8 +4609,8 @@ "message": "om het vandaag te proberen.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden-gebruiker $USER_IDENTIFIER$ heeft het volgende met je gedeeld", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden-lid $USER_IDENTIFIER$ heeft het volgende met je gedeeld", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Volgende" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Geselecteerde regionale vlag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Projecttoegang bijgewerkt" + }, + "unexpectedErrorSend": { + "message": "Er is een onverwachte fout opgetreden tijdens het laden van deze Send. Probeer het later opnieuw." } } diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index 3183aa3bde3..d101f348674 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -4218,8 +4218,8 @@ "message": "This Send is hidden by default. You can toggle its visibility using the button below.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Download file" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "The Send you are trying to access does not exist or is no longer available.", @@ -4609,8 +4609,8 @@ "message": "to try it today.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden user $USER_IDENTIFIER$ shared the following with you", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Next" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index c901cb95299..77e07d67954 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -4218,8 +4218,8 @@ "message": "This Send is hidden by default. You can toggle its visibility using the button below.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Download file" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "The Send you are trying to access does not exist or is no longer available.", @@ -4609,8 +4609,8 @@ "message": "to try it today.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden user $USER_IDENTIFIER$ shared the following with you", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Next" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index 8053032a08b..35fe6a505d4 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -4218,8 +4218,8 @@ "message": "Ta wysyłka jest domyślnie ukryta. Możesz zmienić jej widoczność za pomocą przycisku.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Pobierz plik" + "downloadAttachments": { + "message": "Pobierz załączniki" }, "sendAccessUnavailable": { "message": "Wysyłka nie istnieje lub nie jest już dostępna.", @@ -4609,7 +4609,7 @@ "message": ", aby ją wypróbować.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { + "sendAccessCreatorIdentifier": { "message": "Użytkownik Bitwarden $USER_IDENTIFIER$ udostępnił Tobie", "placeholders": { "user_identifier": { @@ -7182,6 +7182,9 @@ "next": { "message": "Dalej" }, + "ssoLoginIsRequired": { + "message": "Logowaniez użyciem SSO jest wymagane" + }, "selectedRegionFlag": { "message": "Flaga wybranego regionu" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index d02ce09624d..5d592f7bb77 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -4218,8 +4218,8 @@ "message": "Este Send é oculto por padrão. Você pode alternar a visibilidade usando o botão abaixo.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Baixar Arquivo" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "O Send que você está tentando acessar não existe ou não está mais disponível.", @@ -4609,8 +4609,8 @@ "message": "para testar hoje mesmo.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "O usuário $USER_IDENTIFIER$ do Bitwarden compartilhou o seguinte com você", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Avançar" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Sinalização de região selecionada" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index 4e821145254..aff9efc4b3b 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -648,7 +648,7 @@ "message": "Dê um nome à sua chave de acesso para o ajudar a identificá-la." }, "useForVaultEncryption": { - "message": "Utilização para encriptação de cofre" + "message": "Utilizar para encriptação de cofre" }, "useForVaultEncryptionInfo": { "message": "Inicie sessão e desbloqueie em dispositivos suportados sem a sua palavra-passe mestra. Siga as instruções do seu navegador para finalizar a configuração." @@ -1273,7 +1273,7 @@ "message": "Zona de perigo" }, "dangerZoneDesc": { - "message": "Cuidado, estas ações não são reversíveis!" + "message": "Cuidado, estas ações são irreversíveis!" }, "deauthorizeSessions": { "message": "Desautorizar sessões" @@ -4218,8 +4218,8 @@ "message": "Este Send está oculto por defeito. Pode alternar a sua visibilidade utilizando o botão abaixo.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Transferir ficheiro" + "downloadAttachments": { + "message": "Transferir anexos" }, "sendAccessUnavailable": { "message": "O Send a que está a tentar aceder não existe ou já não está disponível.", @@ -4609,8 +4609,8 @@ "message": "para o experimentar hoje.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "O utilizador $USER_IDENTIFIER$ do Bitwarden partilhou o seguinte consigo", + "sendAccessCreatorIdentifier": { + "message": "O membro $USER_IDENTIFIER$ do Bitwarden partilhou consigo o seguinte", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Avançar" }, + "ssoLoginIsRequired": { + "message": "É necessário um início de sessão SSO" + }, "selectedRegionFlag": { "message": "Bandeira da região selecionada" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Acesso ao projeto atualizado" + }, + "unexpectedErrorSend": { + "message": "Ocorreu um erro inesperado ao carregar este Send. Tente novamente mais tarde." } } diff --git a/apps/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index 9741a96f7f5..babd9be2e0a 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -4218,8 +4218,8 @@ "message": "Acest Send este ascuns în mod implicit. Puteți comuta vizibilitatea acestuia cu butonul de mai jos.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Descărcare fișier" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "Send-ul pe care încercați să-l accesați nu există sau nu mai este disponibil.", @@ -4609,8 +4609,8 @@ "message": "pentru a-l încerca astăzi.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Utilizatorul Bitwarden $USER_IDENTIFIER$ a partajat următoarele cu dvs.", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Next" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index 1457cd1371c..c47e7995067 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -4218,8 +4218,8 @@ "message": "Эта Send по умолчанию скрыта. Вы можете переключить ее видимость с помощью кнопки ниже.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Скачать файл" + "downloadAttachments": { + "message": "Скачать вложения" }, "sendAccessUnavailable": { "message": "Send, к которой вы пытаетесь получить доступ, больше не существует или недоступна.", @@ -4609,7 +4609,7 @@ "message": "чтобы попробовать уже сегодня.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { + "sendAccessCreatorIdentifier": { "message": "Пользователь Bitwarden $USER_IDENTIFIER$ поделился с вами следующим", "placeholders": { "user_identifier": { @@ -7182,6 +7182,9 @@ "next": { "message": "Далее" }, + "ssoLoginIsRequired": { + "message": "Требуется логин SSO" + }, "selectedRegionFlag": { "message": "Флаг выбранного региона" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Доступ к проекту обновлен" + }, + "unexpectedErrorSend": { + "message": "При загрузке этой Send произошла непредвиденная ошибка. Повторите попытку позже." } } diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index 611aec16c6a..0350e6464bf 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -4218,8 +4218,8 @@ "message": "This Send is hidden by default. You can toggle its visibility using the button below.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Download file" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "The Send you are trying to access does not exist or is no longer available.", @@ -4609,8 +4609,8 @@ "message": "to try it today.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden user $USER_IDENTIFIER$ shared the following with you", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Next" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index d6ee8d07448..effd87f2185 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -4218,8 +4218,8 @@ "message": "Tento Send je normálne skrytý. Tlačidlom nižšie môžete prepnúť jeho viditeľnosť.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Stiahnuť súbor" + "downloadAttachments": { + "message": "Stiahnuť prílohy" }, "sendAccessUnavailable": { "message": "Odoslanie, ku ktorému sa pokúšate získať prístup, neexistuje alebo už nie je k dispozícii.", @@ -4609,8 +4609,8 @@ "message": "skúste to ešte dnes.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden používateľ $USER_IDENTIFIER$ zdieľal nasledujúce s vami", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden člen $USER_IDENTIFIER$ zdieľal s vami nasledujúce", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Ďalej" }, + "ssoLoginIsRequired": { + "message": "Vyžaduje sa prihlásenie cez SSO" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Prístup k projektu aktualizovaný" + }, + "unexpectedErrorSend": { + "message": "Nastal neočakávaný problém pri načítavaní tohto Sendu. Skúste to neskôr." } } diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index e62db7873ed..b4fa9f8cb33 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -4218,8 +4218,8 @@ "message": "This Send is hidden by default. You can toggle its visibility using the button below.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Download file" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "The Send you are trying to access does not exist or is no longer available.", @@ -4609,8 +4609,8 @@ "message": "to try it today.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden user $USER_IDENTIFIER$ shared the following with you", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Next" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/sr/messages.json b/apps/web/src/locales/sr/messages.json index c8175e6f884..09c15aacff3 100644 --- a/apps/web/src/locales/sr/messages.json +++ b/apps/web/src/locales/sr/messages.json @@ -648,22 +648,22 @@ "message": "Именујте Ваш приступачни кључ за лакшу идентификацију." }, "useForVaultEncryption": { - "message": "Use for vault encryption" + "message": "Користи се за шифровање сефа" }, "useForVaultEncryptionInfo": { - "message": "Log in and unlock on supported devices without your master password. Follow the prompts from your browser to finalize setup." + "message": "Пријавите се и откључајте на подржаним уређајима без ваше главне лозинке. Пратите упутства из прегледача да бисте завршили подешавање." }, "useForVaultEncryptionErrorReadingPasskey": { - "message": "Error reading passkey. Try again or uncheck this option." + "message": "Грешка при читању кључа. Покушајте поново или опозовите избор ове опције." }, "encryptionNotSupported": { "message": "Шифровање није подржано" }, "encryptionNotEnabled": { - "message": "Encryption not enabled" + "message": "Шифрирање није омогућено" }, "usedForEncryption": { - "message": "Used for encryption" + "message": "Употребљено за шифровање" }, "loginWithPasskeyEnabled": { "message": "Пријављивање са упаљеним приступчним кључем" @@ -4218,8 +4218,8 @@ "message": "Ово Слање је подразумевано скривено. Можете да пребацујете његову видљивост помоћу дугмета испод.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Преузми датотеку" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "„Send“ које покушавате да приступите не постоји или више није доступан.", @@ -4609,8 +4609,8 @@ "message": "да пробаш одмах.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden корисник $USER_IDENTIFIER$ је поделио следеће са тобом", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Следеће" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Одабрана застава" }, @@ -7391,6 +7394,9 @@ "description": "This is followed a by a hyperlink to the help website." }, "projectAccessUpdated": { - "message": "Project access updated" + "message": "Приступ пројекту је ажуриран" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index 98d4804f28f..d5510f30b4d 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -4218,8 +4218,8 @@ "message": "This Send is hidden by default. You can toggle its visibility using the button below.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Download file" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "The Send you are trying to access does not exist or is no longer available.", @@ -4609,8 +4609,8 @@ "message": "to try it today.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden user $USER_IDENTIFIER$ shared the following with you", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Next" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index 739f2eb03ab..bb7847f07b9 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -4218,8 +4218,8 @@ "message": "Denna Send är dold som standard. Du kan växla dess synlighet med hjälp av knappen nedan.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Ladda ner fil" + "downloadAttachments": { + "message": "Ladda ner bilagor" }, "sendAccessUnavailable": { "message": "Den Send du försöker komma åt finns inte eller är inte längre tillgänglig.", @@ -4609,8 +4609,8 @@ "message": "att prova det idag.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden-användaren $USER_IDENTIFIER$ delade följande med dig", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Nästa" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "Ett oväntat fel inträffade när denna Send laddades. Försök igen senare." } } diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index c901cb95299..77e07d67954 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -4218,8 +4218,8 @@ "message": "This Send is hidden by default. You can toggle its visibility using the button below.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Download file" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "The Send you are trying to access does not exist or is no longer available.", @@ -4609,8 +4609,8 @@ "message": "to try it today.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden user $USER_IDENTIFIER$ shared the following with you", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Next" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index 36058f9aa16..d1f77892e0b 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -4218,8 +4218,8 @@ "message": "This Send is hidden by default. You can toggle its visibility using the button below.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Download file" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "The Send you are trying to access does not exist or is no longer available.", @@ -4609,8 +4609,8 @@ "message": "to try it today.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden user $USER_IDENTIFIER$ shared the following with you", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Next" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index a6694784241..1b171cbff85 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -4218,8 +4218,8 @@ "message": "Bu Send varsayılan olarak gizlidir. Aşağıdaki düğmeyi kullanarak görünürlüğünü değiştirebilirsiniz.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Dosyayı indir" + "downloadAttachments": { + "message": "Ekleri indir" }, "sendAccessUnavailable": { "message": "Erişmeye çalıştığınız Send yok veya artık mevcut değil.", @@ -4609,8 +4609,8 @@ "message": "hemen deneyin.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden kullanıcısı $USER_IDENTIFIER$ aşağıdakileri sizinle paylaştı", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden üyesi $USER_IDENTIFIER$ aşağıdakileri sizinle paylaştı", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "İleri" }, + "ssoLoginIsRequired": { + "message": "SSO girişi gereklidir" + }, "selectedRegionFlag": { "message": "Seçilen bölgenin bayrağı" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Proje erişimi güncellendi" + }, + "unexpectedErrorSend": { + "message": "Bu Gönderim yüklenirken beklenmeyen bir hata oluştu. Daha sonra tekrar deneyin." } } diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index bc1409dda76..86312163a53 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -4218,8 +4218,8 @@ "message": "Це відправлення типово приховане. Ви можете змінити його видимість кнопкою нижче.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Завантажити файл" + "downloadAttachments": { + "message": "Завантажити вкладення" }, "sendAccessUnavailable": { "message": "Відправлення, до якого ви намагаєтесь отримати доступ, не існує, або більше недоступне.", @@ -4609,8 +4609,8 @@ "message": "щоб спробувати.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Користувач Bitwarden $USER_IDENTIFIER$ ділиться з вами таким", + "sendAccessCreatorIdentifier": { + "message": "Учасник Bitwarden $USER_IDENTIFIER$ ділиться з вами таким", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "Далі" }, + "ssoLoginIsRequired": { + "message": "Потрібно увійти через SSO" + }, "selectedRegionFlag": { "message": "Прапор вибраного регіону" }, @@ -7391,6 +7394,9 @@ "description": "This is followed a by a hyperlink to the help website." }, "projectAccessUpdated": { - "message": "Project access updated" + "message": "Доступ до проєкту оновлено" + }, + "unexpectedErrorSend": { + "message": "Під час завантаження цього відправлення сталася неочікувана помилка. Повторіть спробу пізніше." } } diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index a231d73cc0b..e95c92fd387 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -67,7 +67,7 @@ "message": "Số hộ chiếu" }, "licenseNumber": { - "message": "Số giấy phép" + "message": "Số bằng lái xe" }, "email": { "message": "Email" @@ -112,7 +112,7 @@ "message": "Tháng 12" }, "title": { - "message": "Tiêu đề" + "message": "Danh xưng" }, "mr": { "message": "Ông" @@ -164,7 +164,7 @@ "description": "This describes a field that is 'linked' (related) to another field." }, "remove": { - "message": "Xoá" + "message": "Xóa" }, "unassigned": { "message": "Hủy ấn định" @@ -226,7 +226,7 @@ "message": "Kiểm tra xem mật khẩu có bị lộ không." }, "passwordExposed": { - "message": "Mật khẩu này đã bị lộ $VALUE$ lần() trong các dữ liệu vi phạm. Bạn nên thay đổi nó.", + "message": "Mật khẩu này đã bị lộ $VALUE$ lần trong các vụ rò rỉ dữ liệu. Bạn nên thay đổi nó.", "placeholders": { "value": { "content": "$1", @@ -235,7 +235,7 @@ } }, "passwordSafe": { - "message": "Mật khẩu này không được tìm thấy trong bất kỳ dữ liệu vi phạm nào được biết đến. Nó an toàn để sử dụng." + "message": "Mật khẩu này không tìm thấy trong bất kỳ vụ rò rỉ dữ liệu nào. Nó an toàn để sử dụng." }, "save": { "message": "Lưu" @@ -256,7 +256,7 @@ "message": "Yêu thích" }, "unfavorite": { - "message": "Bỏ yêu thích" + "message": "Bỏ thích" }, "edit": { "message": "Sửa" @@ -279,15 +279,15 @@ "description": "Search Card type" }, "searchIdentity": { - "message": "Tìm kiếm danh tính", + "message": "Tìm kiếm danh bạ", "description": "Search Identity type" }, "searchSecureNote": { - "message": "Tìm kiếm ghi chú an toàn", + "message": "Tìm kiếm ghi chú", "description": "Search Secure Note type" }, "searchVault": { - "message": "Tìm kiếm trong Kho" + "message": "Tìm kiếm trong kho" }, "searchMyVault": { "message": "Tìm kiếm trong kho" @@ -308,31 +308,31 @@ "message": "Yêu thích" }, "types": { - "message": "Các loại" + "message": "Loại" }, "typeLogin": { - "message": "Đăng nhập" + "message": "Thông tin đăng nhập" }, "typeCard": { "message": "Thẻ" }, "typeIdentity": { - "message": "Định danh" + "message": "Danh bạ" }, "typeSecureNote": { - "message": "Ghi chú bảo mật" + "message": "Ghi chú" }, "typeLoginPlural": { - "message": "Đăng nhập" + "message": "Thông tin đăng nhập" }, "typeCardPlural": { "message": "Thẻ" }, "typeIdentityPlural": { - "message": "Danh tính" + "message": "Danh bạ" }, "typeSecureNotePlural": { - "message": "Ghi chú bảo mật" + "message": "Ghi chú" }, "folders": { "message": "Thư mục" @@ -467,10 +467,10 @@ "message": "Vault items" }, "filter": { - "message": "Bộ Lọc" + "message": "Bộ lọc" }, "moveSelectedToOrg": { - "message": "Chuyển đã chọn tới Tổ chức" + "message": "Chuyển mục đã chọn tới Tổ chức" }, "deleteSelected": { "message": "Xóa mục đã chọn" @@ -488,16 +488,16 @@ "message": "Khởi chạy" }, "newAttachment": { - "message": "Thêm tệp đính kèm mới" + "message": "Thêm tập tin đính kèm mới" }, "deletedAttachment": { - "message": "Đã xoá tệp đính kèm" + "message": "Đã xóa tập tin đính kèm" }, "deleteAttachmentConfirmation": { - "message": "Bạn có chắc chắn muốn xóa tập tin đính kèm này?" + "message": "Bạn có chắc muốn xóa tập tin đính kèm này?" }, "attachmentSaved": { - "message": "Tệp đính kèm đã được lưu." + "message": "Đã lưu tập tin đính kèm" }, "file": { "message": "Tập tin" @@ -506,13 +506,13 @@ "message": "Chọn một tập tin." }, "maxFileSize": { - "message": "Kích thước tối đa của tệp tin là 500 MB." + "message": "Dung lượng tối đa của tập tin là 500 MB." }, "addedItem": { "message": "Đã thêm mục" }, "editedItem": { - "message": "Mục được chỉnh sửa" + "message": "Đã lưu mục" }, "movedItemToOrg": { "message": "$ITEMNAME$ đã di chuyển tới $ORGNAME$", @@ -543,10 +543,10 @@ "message": "Xóa thư mục" }, "deleteAttachment": { - "message": "Xóa tệp đính kèm" + "message": "Xóa tập tin đính kèm" }, "deleteItemConfirmation": { - "message": "Bạn có chắc bạn muốn xóa mục này?" + "message": "Bạn có chắc muốn xóa mục này?" }, "deletedItem": { "message": "Đã xóa mục" @@ -558,16 +558,16 @@ "message": "Đã di chuyển mục" }, "overwritePasswordConfirmation": { - "message": "Bạn có chắc chắn muốn ghi đè mật khẩu hiện tại không?" + "message": "Bạn có chắc muốn ghi đè mật khẩu hiện tại không?" }, "editedFolder": { - "message": "Đã chỉnh sửa thư mục" + "message": "Đã lưu thư mục" }, "addedFolder": { "message": "Đã thêm thư mục" }, "deleteFolderConfirmation": { - "message": "Bạn có chắc chắn muốn xóa thư mục này không?" + "message": "Bạn có chắc muốn xóa thư mục này?" }, "deletedFolder": { "message": "Đã xóa thư mục" @@ -585,7 +585,7 @@ "message": "Phiên đăng nhập của bạn đã hết hạn." }, "logOutConfirmation": { - "message": "Bạn có chắc chắn muốn đăng xuất không?" + "message": "Bạn có chắc muốn đăng xuất?" }, "logOut": { "message": "Đăng xuất" @@ -696,7 +696,7 @@ "message": "Tạo tài khoản" }, "newAroundHere": { - "message": "Bạn mới tới đây sao?" + "message": "Bạn mới dùng?" }, "startTrial": { "message": "Bắt đầu dùng thử" @@ -717,7 +717,7 @@ "message": "Tên của bạn" }, "yourNameDesc": { - "message": "Chúng tôi nên gọi bạn là gì nào?" + "message": "Chúng tôi nên gọi bạn là gì?" }, "masterPass": { "message": "Mật khẩu chính" @@ -747,13 +747,13 @@ "message": "Gợi ý mật khẩu" }, "enterEmailToGetHint": { - "message": "Vui lòng nhập địa chỉ email của tài khoản bạn để nhận gợi ý mật khẩu." + "message": "Nhập địa chỉ email của tài khoản bạn để nhận gợi ý mật khẩu." }, "getMasterPasswordHint": { "message": "Nhận gợi ý mật khẩu chính" }, "emailRequired": { - "message": "Cần phải có địa chỉ email." + "message": "Yêu cầu có địa chỉ email." }, "invalidEmail": { "message": "Địa chỉ email không hợp lệ." @@ -781,13 +781,13 @@ "message": "Tài khoản của bạn đã được tạo! Bạn có thể đăng nhập ngay bây giờ." }, "trialAccountCreated": { - "message": "Tài Khoản Đã Tạo Thành Công." + "message": "Đã tạo tài khoản thành công." }, "masterPassSent": { - "message": "Chúng tôi đã gửi cho bạn email với gợi ý mật khẩu chính của bạn." + "message": "Chúng tôi đã gửi cho bạn email với gợi ý mật khẩu chính." }, "unexpectedError": { - "message": "Một lỗi bất ngờ đã xảy ra." + "message": "Xảy ra một lỗi bất ngờ." }, "expirationDateError": { "message": "Please select an expiration date that is in the future." @@ -796,7 +796,7 @@ "message": "Địa chỉ email" }, "yourVaultIsLocked": { - "message": "Kho của bạn đã bị khóa. Xác minh mật khẩu chính của bạn để tiếp tục." + "message": "Kho của bạn đã bị khóa. Nhập mật khẩu chính để tiếp tục." }, "uuid": { "message": "UUID" @@ -805,7 +805,7 @@ "message": "Mở khóa" }, "loggedInAsEmailOn": { - "message": "Đã đăng nhập là $EMAIL$ trên $HOSTNAME$.", + "message": "Đã đăng nhập $EMAIL$ trên $HOSTNAME$.", "placeholders": { "email": { "content": "$1", @@ -821,37 +821,37 @@ "message": "Mật khẩu chính không hợp lệ" }, "invalidFilePassword": { - "message": "Mật khẩu tệp không hợp lệ, vui lòng sử dụng mật khẩu bạn đã nhập khi tạo tệp xuất." + "message": "Mật khẩu tập tin không hợp lệ, vui lòng sử dụng mật khẩu bạn đã nhập khi xuất tập tin." }, "lockNow": { "message": "Khóa ngay" }, "noItemsInList": { - "message": "Không có mục nào để liệt kê." + "message": "Chưa có mục nào." }, "noPermissionToViewAllCollectionItems": { - "message": "Bạn không có quyền xem tất cả các mục trong bộ sưu tập này." + "message": "Bạn không có quyền xem tất cả mục trong bộ sưu tập này." }, "noCollectionsInList": { - "message": "Không có bộ sưu tập nào để liệt kê." + "message": "Chưa có bộ sưu tập nào." }, "noGroupsInList": { - "message": "Không có nhóm nào để liệt kê." + "message": "Chưa có nhóm nào." }, "noUsersInList": { - "message": "Không có người nào để liệt kê." + "message": "Chưa có người nào." }, "noMembersInList": { "message": "Không có người nào để liệt kê." }, "noEventsInList": { - "message": "Không có sự kiện nào để liệt kê." + "message": "Chưa có sự kiện nào." }, "newOrganization": { "message": "Tổ chức mới" }, "noOrganizationsList": { - "message": "Bạn không thuộc tổ chức nào. Tổ chức sẽ cho phép bạn chia sẻ với người dùng khác một cách bảo mật." + "message": "Bạn chưa thuộc tổ chức nào. Tổ chức sẽ cho phép bạn chia sẻ các mục với người dùng khác một cách bảo mật." }, "notificationSentDevice": { "message": "Một thông báo đã được gửi đến thiết bị của bạn." @@ -866,10 +866,10 @@ } }, "enterVerificationCodeApp": { - "message": "Vui lòng nhập mã xác thực 6 chữ số từ ứng dụng xác thực của bạn." + "message": "Nhập mã xác thực 6 chữ số từ ứng dụng xác thực của bạn." }, "enterVerificationCodeEmail": { - "message": "Vui lòng nhập mã xác thực 6 chữ số được gửi tới $EMAIL$.", + "message": "Nhập mã xác thực 6 chữ số được gửi tới $EMAIL$.", "placeholders": { "email": { "content": "$1", @@ -890,31 +890,31 @@ "message": "Ghi nhớ đăng nhập" }, "sendVerificationCodeEmailAgain": { - "message": "Gửi lại email xác thực" + "message": "Gửi lại email xác minh" }, "useAnotherTwoStepMethod": { - "message": "Sử dụng phương pháp xác thực hai lớp khác" + "message": "Dùng phương pháp xác mnih hai bước khác" }, "insertYubiKey": { - "message": "Vui lòng cắm Yubikey vào cổng USB của máy tính bạn và bấm nút trên Yubikey." + "message": "Cắm YubiKey vào cổng USB trên máy tính bạn và bấm nút trên Yubikey." }, "insertU2f": { - "message": "Vui lòng cắm chìa khóa bảo mật vào cổng USB của máy tính bạn và bấm nút trên chìa khóa nếu có." + "message": "Cắm khóa bảo mật vào cổng USB trên máy tính bạn và bấm nút trên khóa, nếu có." }, "loginUnavailable": { - "message": "Đăng nhập không hoạt động" + "message": "Đăng nhập không được" }, "noTwoStepProviders": { - "message": "Tài khoản này có xác thực hai lớp, tuy nhiên, trình duyệt của bạn không hỗ trợ dịch vụ xác thực hai lớp đang sử dụng." + "message": "Tài khoản này có xác minh hai bước. Tuy nhiên, trình duyệt của bạn không hỗ trợ dịch vụ xác minh của bạn." }, "noTwoStepProviders2": { "message": "Vui lòng sử dụng trình duyệt được hỗ trợ (chẳng hạn như Chrome) và/hoặc thêm dịch vụ khác với hỗ trợ tốt hơn trên các trình duyệt (chẳng hạn như một ứng dụng xác thực)." }, "twoStepOptions": { - "message": "Tùy chọn xác thực hai lớp" + "message": "Tùy chọn xác minh hai bước" }, "recoveryCodeDesc": { - "message": "Bạn bị mất quyền truy cập vào tất cả các dịch vụ xác thực hai lớp? Sử dụng mã phục hồi của bạn để tắt tất cả các dịch vụ xác thực hai lớp của tài khoản bạn." + "message": "Bạn bị mất quyền truy cập vào tất cả các dịch vụ xác minh hai bước? Sử dụng mã phục hồi của bạn để tắt chúng trên tài khoản bạn." }, "recoveryCodeTitle": { "message": "Mã phục hồi" @@ -923,14 +923,14 @@ "message": "Ứng dụng xác thực" }, "authenticatorAppDesc": { - "message": "Sử dụng một ứng dụng xác thực (chẳng hạn như Authy hoặc Google Authenticator) để tạo các mã xác nhận theo thời gian.", + "message": "Sử dụng một ứng dụng xác thực (chẳng hạn như Authy hoặc Google Authenticator) để tạo các mã xác thực thời gian thực.", "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." }, "yubiKeyTitle": { "message": "Mật khẩu OTP YubiKey" }, "yubiKeyDesc": { - "message": "Sử dụng YubiKey để truy cập tài khoản của bạn. Hoạt động với YubiKey 4, 4 Nano, 4C và NEO." + "message": "Sử dụng YubiKey để truy cập tài khoản của bạn. Hoạt động với YubiKey 4, 5 và các thiết bị NEO." }, "duoDesc": { "message": "Xác minh với Duo Security dùng ứng dụng Duo Mobile, SMS, điện thoại, hoặc mật khẩu U2F.", @@ -959,7 +959,7 @@ "message": "Email" }, "emailDesc": { - "message": "Mã xác thực sẽ được gửi qua email cho bạn." + "message": "Mã xác minh sẽ được gửi qua email cho bạn." }, "continue": { "message": "Tiếp tục" @@ -977,10 +977,10 @@ "message": "Choose an organization that you wish to move these items to. Moving to an organization transfers ownership of the items to that organization. You will no longer be the direct owner of these items once they have been moved." }, "collectionsDesc": { - "message": "Chỉnh sửa những bộ sưu tập mà bạn sẽ chia sẻ mục này với. Chỉ những thành viên của tổ chức với quyền cho những bộ sưu tập đó mới có thể xem được mục này." + "message": "Chỉnh sửa những bộ sưu tập mà bạn có chia sẻ mục này. Chỉ những thành viên của tổ chức có quyền quản lý những bộ sưu tập đó mới có thể xem được mục này." }, "deleteSelectedItemsDesc": { - "message": "Bạn đã chọn $COUNT$ mục để xóa. Bạn có chắc bạn muốn xóa hết những mục này?", + "message": "$COUNT$ mục sẽ được chuyển vào thùng rác.", "placeholders": { "count": { "content": "$1", @@ -1001,7 +1001,7 @@ "message": "Bạn có chắc rằng bạn muốn tiếp tục?" }, "moveSelectedItemsDesc": { - "message": "Vui lòng chọn thư mục mà bạn muốn di chuyển $COUNT$ mục này tới.", + "message": "Chọn thư mục mà bạn muốn di chuyển $COUNT$ mục này tới.", "placeholders": { "count": { "content": "$1", @@ -1045,7 +1045,7 @@ "message": "Xác nhận xuất kho lưu trữ" }, "exportWarningDesc": { - "message": "Bản trích xuất này chứa dữ liệu kho bạn và không được mã hóa. Bạn không nên lưu trữ hay gửi tập tin trích xuất thông qua phương thức không an toàn (như email). Vui lòng xóa nó ngay lập tức khi bạn đã sử dụng xong." + "message": "Bản xuất này chứa dữ liệu kho bạn và không được mã hóa. Bạn không nên lưu trữ hay gửi tập tin đã xuất thông qua phương thức rủi ro (như email). Vui lòng xóa nó ngay lập tức khi bạn đã sử dụng xong." }, "exportSecretsWarningDesc": { "message": "This export contains your secrets data in an unencrypted format. You should not store or send the exported file over unsecure channels (such as email). Delete it immediately after you are done using it." @@ -1060,7 +1060,7 @@ "message": "Xuất" }, "exportVault": { - "message": "Trích xuất kho" + "message": "Xuất kho" }, "exportSecrets": { "message": "Export secrets" @@ -1075,7 +1075,7 @@ "message": "This password will be used to export and import this file" }, "confirmMasterPassword": { - "message": "Xác nhận mật khẩu chính" + "message": "Nhập lại mật khẩu chính" }, "confirmFormat": { "message": "Confirm format" @@ -1084,7 +1084,7 @@ "message": "Mật khẩu tập tin" }, "confirmFilePassword": { - "message": "Xác nhận mật khẩu tập tin" + "message": "Nhập lại mật khẩu tập tin" }, "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." @@ -1099,25 +1099,25 @@ "message": "Tài khoản bị hạn chế" }, "passwordProtected": { - "message": "Mật khẩu đã được bảo vệ" + "message": "Đã được bảo vệ mật khẩu" }, "filePasswordAndConfirmFilePasswordDoNotMatch": { - "message": "“Mật khẩu tệp” và “Xác nhận mật khẩu tệp” không khớp." + "message": "“Mật khẩu tập tin” và “Nhập lại mật khẩu tập tin” không khớp." }, "confirmVaultImport": { - "message": "Xác nhận nhập kho lưu trữ" + "message": "Xác nhận nhập kho mật khẩu" }, "confirmVaultImportDesc": { - "message": "Tập tin này được mật khẩu bảo vệ. Vui lòng nhập mật khẩu tệp để nhập dữ liệu." + "message": "Tập tin này được bảo vệ bằng mật khẩu. Vui lòng nhập mật khẩu để nhập dữ liệu." }, "exportSuccess": { - "message": "Dữ liệu kho cảu bạn đã được trích xuất." + "message": "Đã xuất dữ liệu kho cảu bạn" }, "passwordGenerator": { "message": "Tạo mật khẩu" }, "minComplexityScore": { - "message": "Điểm phức tạp tối thiểu" + "message": "Độ phức tạp tối thiểu" }, "minNumbers": { "message": "Số chữ số tối thiểu" @@ -1127,7 +1127,7 @@ "description": "Minimum special characters" }, "ambiguous": { - "message": "Tránh các ký tự không rõ ràng" + "message": "Tránh các ký tự dễ gây nhầm lẫn" }, "regeneratePassword": { "message": "Tạo lại mật khẩu" @@ -1136,21 +1136,21 @@ "message": "Độ dài" }, "uppercase": { - "message": "Chữ in hoa (A-Z)", + "message": "Chữ hoa (A-Z)", "description": "Include uppercase letters in the password generator." }, "lowercase": { - "message": "Chữ in thường (a-z)", + "message": "Chữ thường (a-z)", "description": "Include lowercase letters in the password generator." }, "numbers": { - "message": "Số (0-9)" + "message": "Chữ số (0-9)" }, "specialCharacters": { "message": "Ký tự đặc biệt (!@#$%^&*)" }, "numWords": { - "message": "Số lượng chữ" + "message": "Số lượng từ" }, "wordSeparator": { "message": "Dấu tách từ" @@ -1166,10 +1166,10 @@ "message": "Lịch sử mật khẩu" }, "noPasswordsInList": { - "message": "Không có mật khẩu để liệt kê." + "message": "Chưa có mật khẩu." }, "clear": { - "message": "Xoá", + "message": "Xóa", "description": "To clear something out. Example: To clear browser history." }, "accountUpdated": { @@ -1188,7 +1188,7 @@ "message": "Mã" }, "changeEmailDesc": { - "message": "Chúng tôi đã gửi mã xác thực tới $EMAIL$. Vui lòng kiểm tra thùng thư của bạn để nhận và nhập mã vào bên dưới để hoàn thành quá trình thay đổi địa chỉ email.", + "message": "Chúng tôi đã gửi mã xác minh tới $EMAIL$. Vui lòng kiểm tra email để nhận và nhập mã vào bên dưới để hoàn tất quá trình thay đổi địa chỉ email.", "placeholders": { "email": { "content": "$1", @@ -1197,7 +1197,7 @@ } }, "loggedOutWarning": { - "message": "Tiếp tục sẽ đăng xuất bạn ra khỏi phiên hiện tại, cần bạn phải đăng nhập lại. Những phiên trên các thiết bị khác sẽ tiếp tục có hiệu lực lên đến 1 tiếng." + "message": "Sẽ đăng xuất bạn ra khỏi phiên hiện tại, sau đó cần đăng nhập lại. Những phiên trên các thiết bị khác sẽ tiếp tục có hiệu lực lên đến 1 tiếng." }, "emailChanged": { "message": "Đã thay đổi email" @@ -1224,7 +1224,7 @@ "message": "Xác nhận mật khẩu chính mới" }, "encKeySettings": { - "message": "Cài đặt mật khẩu mã hóa" + "message": "Cài đặt mã khóa" }, "kdfAlgorithm": { "message": "Thuật toán KDF" @@ -1233,7 +1233,7 @@ "message": "Số lần KDF" }, "kdfIterationsDesc": { - "message": "Số lần KDF nhiều có thể giúp bảo vệ mật khẩu chính khỏi những cuộc tấn công cưỡng chế. Chúng tôi khuyến khích giá trị $VALUE$ hoặc cao hơn.", + "message": "Số lần KDF nhiều có thể giúp bảo vệ mật khẩu chính khỏi những cuộc tấn công. Chúng tôi khuyến khích giá trị $VALUE$ hoặc cao hơn.", "placeholders": { "value": { "content": "$1", @@ -1242,7 +1242,7 @@ } }, "kdfIterationsWarning": { - "message": "Số lần KDF quá cao có thể làm các thiết bị yếu hơn bị giật lag khi đăng nhập (hoặc mỏ khóa). Chúng tôi khuyến khích tăng $INCREMENT$ mỗi lần và thử trên các thiết bị của bạn trước.", + "message": "Số lần KDF quá cao có thể làm các thiết bị yếu hơn bị giật lag khi đăng nhập (hoặc mở khóa). Chúng tôi khuyến khích tăng $INCREMENT$ mỗi lần và thử trên các thiết bị của bạn trước.", "placeholders": { "increment": { "content": "$1", @@ -1267,25 +1267,25 @@ "message": "Thay đổi KDF" }, "encKeySettingsChanged": { - "message": "Cài đặt mật khẩu mã hóa đã thay đổi" + "message": "Đã thay đổi cài đặt mã khóa" }, "dangerZone": { "message": "Vùng nguy hiểm" }, "dangerZoneDesc": { - "message": "Vui lòng cẩn thận, những hành động này không thể được hủy bỏ!" + "message": "Cẩn thận, thao tác này không thể khôi phục!" }, "deauthorizeSessions": { - "message": "Hủy quyền phiên" + "message": "Gỡ phiên" }, "deauthorizeSessionsDesc": { - "message": "Lo lắng tài khoản của bạn bị đăng nhập trên một thiết bị khác? Tiếp tục bên dưới để hủy quyền tất cả thiết bị bạn đã sử dụng. Bước bảo mật này được khuyến khích nếu bạn đã sử dụng thiết bị công cộng hoặc thiết bị không phải của bạn. Nó cũng sẽ xóa hết những phiên đăng nhập hai bước đã được lưu." + "message": "Lo lắng tài khoản của bạn bị đăng nhập trên một thiết bị khác? Hãy gỡ phiên tất cả thiết bị bạn đã sử dụng. Bước bảo mật này được khuyến khích nếu bạn đã sử dụng thiết bị công cộng hoặc thiết bị không phải của bạn. Nó cũng sẽ xóa hết những phiên đăng nhập hai bước đã được lưu." }, "deauthorizeSessionsWarning": { - "message": "Tiếp tục sẽ đăng xuất bạn ra khỏi phiên hiện tại, cần bạn phải đăng nhập lại. Bạn cũng sẽ phải đăng nhập hai bước lại nếu bạn có đăng nhập hai bước. Những phiên đăng nhập trên các thiết bị khác sẽ tiếp tục có hiệu lực lên đến 1 tiếng." + "message": "Sẽ đăng xuất bạn ra khỏi phiên hiện tại, sau đó cần đăng nhập lại. Bạn cũng sẽ phải đăng nhập hai bước lại nếu bạn có đăng nhập hai bước. Những phiên đăng nhập trên các thiết bị khác sẽ tiếp tục có hiệu lực lên đến 1 tiếng." }, "sessionsDeauthorized": { - "message": "Tất cả phiên đăng nhập đã bị hủy" + "message": "Tất cả phiên đăng nhập đã bị gỡ" }, "purgeVault": { "message": "Xóa kho" @@ -1297,7 +1297,7 @@ "message": "Vault accessed by Provider." }, "purgeVaultDesc": { - "message": "Tiếp tục bên dưới để xóa hết tất cả mục và thư mục trong kho của bạn. Những mục thuộc về tổ chức mà bạn chia sẻ với sẽ không bị xóa." + "message": "Tiếp tục để xóa hết tất cả mục và thư mục trong kho của bạn. Những mục thuộc về tổ chức mà bạn chia sẻ sẽ không bị xóa." }, "purgeOrgVaultDesc": { "message": "Tiếp tục bên dưới để xóa hết tất cả mục trong kho của tổ chức." @@ -1312,16 +1312,16 @@ "message": "Xóa tài khoản" }, "deleteAccountDesc": { - "message": "Tiếp tục bên dưới để xóa tài khoản và dữ liệu liên quan của bạn." + "message": "Tiếp tục để xóa tài khoản và dữ liệu liên quan của bạn." }, "deleteAccountWarning": { - "message": "Việc xóa tài khoản là vĩnh viễn và không thể hoàn tác." + "message": "Tài khoản sẽ bị xóa vĩnh viễn và không thể hoàn tác." }, "accountDeleted": { "message": "Tài khoản đã được xóa" }, "accountDeletedDesc": { - "message": "Tài khoản của bạn đã được đóng và tất cả những dữ liệu liên quan đã được xóa." + "message": "Đã đóng tài khoản của bạn và tất cả những dữ liệu liên quan cũng được xóa." }, "myAccount": { "message": "Tài khoản của tôi" @@ -1366,7 +1366,7 @@ "message": "Dữ liệu không được định dạng đúng cách, vui lòng kiểm tra và thử lại." }, "importNothingError": { - "message": "Không có gì đã được nhập." + "message": "Chưa nhập gì." }, "importEncKeyError": { "message": "Error decrypting the exported file. Your encryption key does not match the encryption key used export the data." @@ -1397,7 +1397,7 @@ "message": "File contains unassigned items." }, "selectFormat": { - "message": "Chọn định dạng cho file xuất" + "message": "Chọn định dạng xuất" }, "selectImportFile": { "message": "Chọn tập tin nhập" @@ -1406,13 +1406,13 @@ "message": "Chọn tập tin" }, "noFileChosen": { - "message": "Không có tệp nào được chọn" + "message": "Chưa chọn tập tin nào" }, "orCopyPasteFileContents": { "message": "hoặc sao chép/dán để nhập nội dung file" }, "instructionsFor": { - "message": "Chỉ dẫn cho $NAME$", + "message": "Hướng dẫn dùng $NAME$", "description": "The title for the import tool instructions.", "placeholders": { "name": { @@ -1425,19 +1425,19 @@ "message": "Tùy chọn" }, "preferences": { - "message": "Tuỳ chỉnh" + "message": "Tùy chỉnh" }, "preferencesDesc": { - "message": "Tùy chỉnh trải nghiệm Kho trên web của bạn." + "message": "Tùy chỉnh Kho trên web của bạn." }, "preferencesUpdated": { - "message": "Đã lưu tuỳ chỉnh" + "message": "Đã lưu tùy chỉnh" }, "language": { "message": "Ngôn ngữ" }, "languageDesc": { - "message": "Thay đổi ngôn ngữ của kho mạng." + "message": "Thay đổi ngôn ngữ của kho trên web." }, "enableFavicon": { "message": "Hiện biểu tượng trang web" @@ -1456,7 +1456,7 @@ "message": "Mặc định" }, "domainRules": { - "message": "Quy luật tên miền" + "message": "Quy tắc tên miền" }, "domainRulesDesc": { "message": "If you have the same login across multiple different website domains, you can mark the website as \"equivalent\". \"Global\" domains are ones already created for you by Bitwarden." @@ -1492,10 +1492,10 @@ } }, "domainsUpdated": { - "message": "Tên miền được cập nhật" + "message": "Tên miền đã được cập nhật" }, "twoStepLogin": { - "message": "Xác thực 2 bước" + "message": "Đăng nhập 2-bước" }, "twoStepLoginEnforcement": { "message": "Two-step Login Enforcement" @@ -1523,7 +1523,7 @@ "message": "Setting up two-step login can permanently lock you out of your Bitwarden account. A recovery code allows you to access your account in the event that you can no longer use your normal two-step login provider (example: you lose your device). Bitwarden support will not be able to assist you if you lose access to your account. We recommend you write down or print the recovery code and keep it in a safe place." }, "viewRecoveryCode": { - "message": "Hiển thị mã khôi phục" + "message": "Xem mã khôi phục" }, "providers": { "message": "Cung cấp", @@ -1549,10 +1549,10 @@ "message": "Cần có tài khoản trả phí" }, "premiumRequiredDesc": { - "message": "Cần nâng cấp tài khoản trả phí để sử dụng chức năng này." + "message": "Cần nâng cấp tài khoản trả phí để sử dụng tính năng này." }, "youHavePremiumAccess": { - "message": "Bạn được truy cập tài khoản trả phí" + "message": "Bạn có tài khoản trả phí" }, "alreadyPremiumFromOrg": { "message": "Bạn được truy cập tài khoản trả phí vì tổ chức của bạn đã chi trả cho việc này." @@ -1576,7 +1576,7 @@ "message": "Vui lòng nhập mật khẩu chính để chỉnh sửa cài đặt đăng nhập hai bước." }, "twoStepAuthenticatorDesc": { - "message": "Làm theo hướng dẫn để thiếp lập đăng nhập hai bước bằng ứng dụng:" + "message": "Làm theo hướng dẫn để thiết lập đăng nhập 2 bước bằng ứng dụng:" }, "twoStepAuthenticatorDownloadApp": { "message": "Tải về ứng dụng xác thực hai bước" @@ -1594,52 +1594,52 @@ "message": "Thiết bị Windows" }, "twoStepAuthenticatorAppsRecommended": { - "message": "Những ứng dụng xác thực sau đây được khuyên dùng, thích cái khác cũng được, ko sao." + "message": "Những ứng dụng xác thực sau đây được khuyên dùng, nhưng những cái khác cũng ok." }, "twoStepAuthenticatorScanCode": { - "message": "Quét nã QR code bằng ứng dụng xác thực" + "message": "Quét mã QR bằng ứng dụng xác thực" }, "key": { - "message": "Chìa khóa" + "message": "Khóa" }, "twoStepAuthenticatorEnterCode": { - "message": "Vui lòng nhập mã 6 bước sinh ra từ ứng dụng xác thực" + "message": "Vui lòng nhập mã 6 chữ số được tạo từ ứng dụng xác thực" }, "twoStepAuthenticatorReaddDesc": { - "message": "Trong trường hợp bạn cần thêm thiết bị khác, ở dưới là mã QR( hoặc khóa) được yêu cầu bởi ứng dụng xác thực." + "message": "Trong trường hợp bạn cần thêm thiết bị khác, ở dưới là mã QR (hoặc khóa) được yêu cầu bởi ứng dụng xác thực." }, "twoStepDisableDesc": { - "message": "Bạn có chắc muốn vô hiệu hóa xác thực hai bước?" + "message": "Bạn có chắc muốn vô hiệu hóa xác minh hai bước?" }, "twoStepDisabled": { "message": "Xác thực hai bước bị hủy bỏ." }, "twoFactorYubikeyAdd": { - "message": "Thêm khóa Yubikey mới vào tài khoản của bạn" + "message": "Thêm khóa YubiKey mới vào tài khoản của bạn" }, "twoFactorYubikeyPlugIn": { - "message": "Cắm khóa Yubikey vào cổng USB máy tính của bạn." + "message": "Cắm khóa YubiKey vào cổng USB máy tính của bạn." }, "twoFactorYubikeySelectKey": { "message": "Select the first empty YubiKey input field below." }, "twoFactorYubikeyTouchButton": { - "message": "Chạm vào nút bấm trên Yubikey." + "message": "Chạm vào nút bấm trên YubiKey." }, "twoFactorYubikeySaveForm": { "message": "Lưu mẫu." }, "twoFactorYubikeyWarning": { - "message": "Do giới hạn của hệ điều hành, Yubikey KHÔNG thể xài hết được trên các ứng dụng Bitwarden. Bạn nên đăng ký thêm một phương pháp xác thực 2 bước khác khi mà Yubikey không xài được. Hỗ trợ hệ điều hành:" + "message": "Do giới hạn của hệ điều hành, YubiKey không thể xài hết được trên các ứng dụng Bitwarden. Bạn nên đăng ký thêm một phương pháp xác thực 2 bước khác khi mà YubiKey không xài được. Hỗ trợ hệ điều hành:" }, "twoFactorYubikeySupportUsb": { "message": "Web vault, desktop application, CLI, and all browser extensions on a device with a USB port that can accept your YubiKey." }, "twoFactorYubikeySupportMobile": { - "message": "Mobile apps on a device with NFC capabilities or a USB port that can accept your YubiKey." + "message": "Ứng dụng di động trên thiết bị có khả năng NFC hoặc cổng USB có thể chấp nhận YubiKey của bạn." }, "yubikeyX": { - "message": "Yubikey $INDEX$", + "message": "YubiKey $INDEX$", "placeholders": { "index": { "content": "$1", @@ -1672,10 +1672,10 @@ "message": "Một trong các khóa bảo mật của tôi có hỗ trợ NFC." }, "twoFactorYubikeySupportsNfcDesc": { - "message": "Nếu một trong các khóa Yubikey có hỗ trợ NFC (ví dụ như Yubikey NEO), bạn sẽ được nhắc nhở trên thiết bị di động khi sóng NFC được phát hiện." + "message": "Nếu một trong các khóa YubiKey có hỗ trợ NFC (ví dụ như Yubikey NEO), bạn sẽ được nhắc nhở trên thiết bị di động khi sóng NFC được phát hiện." }, "yubikeysUpdated": { - "message": "Đã cập nhập Yubikey" + "message": "Đã cập nhật YubiKey" }, "disableAllKeys": { "message": "Deactivate all keys" @@ -1687,7 +1687,7 @@ "message": "Integration key" }, "twoFactorDuoSecretKey": { - "message": "Mã khóa bí mật" + "message": "Khóa bí mật" }, "twoFactorDuoApiHostname": { "message": "API hostname" @@ -1702,13 +1702,13 @@ "message": "Enter the resulting 6 digit verification code from the email" }, "sendEmail": { - "message": "Gửi Email" + "message": "Gửi email" }, "twoFactorU2fAdd": { "message": "Add a FIDO U2F security key to your account" }, "removeU2fConfirmation": { - "message": "Bạn có chắc chắn muốn xóa khóa bảo mật này?" + "message": "Bạn có chắc muốn xóa khóa bảo mật này?" }, "twoFactorWebAuthnAdd": { "message": "Add a WebAuthn security key to your account" @@ -1717,10 +1717,10 @@ "message": "Read key" }, "keyCompromised": { - "message": "Chìa khóa bị lộ." + "message": "Khóa bị lộ." }, "twoFactorU2fGiveName": { - "message": "Đặt cho Yubikey một cái tên để nhận diện." + "message": "Đặt cho YubiKey một cái tên để nhận diện." }, "twoFactorU2fPlugInReadKey": { "message": "Cắm khóa bảo mật vào cổng USB và bấm nút trên khóa." @@ -1732,7 +1732,7 @@ "message": "Lưu mẫu." }, "twoFactorU2fWarning": { - "message": "Do giới hạn của hệ điều hành, FIDO U2F KHÔNG thể xài hết được trên các ứng dụng Bitwarden. Bạn nên đăng ký thêm một phương pháp xác thực 2 bước khác khi mà FIDO U2F không xài được. Hỗ trợ hệ điều hành:" + "message": "Do giới hạn của hệ điều hành, FIDO U2F không thể xài hết được trên các ứng dụng Bitwarden. Bạn nên đăng ký thêm một phương pháp xác thực 2 bước khác khi mà FIDO U2F không xài được. Hỗ trợ hệ điều hành:" }, "twoFactorU2fSupportWeb": { "message": "Web vault and browser extensions on a desktop/laptop with a U2F supported browser (Chrome, Opera, Vivaldi, or Firefox with FIDO U2F turned on)." @@ -1759,7 +1759,7 @@ "message": "You have not set up any two-step login providers yet. After you have set up a two-step login provider you can check back here for your recovery code." }, "printCode": { - "message": "In mã", + "message": "Mã in", "description": "Print 2FA recovery code" }, "reports": { @@ -1774,7 +1774,7 @@ "description": "Vault health reports can be used to evaluate the security of your Bitwarden individual or organization vault." }, "unsecuredWebsitesReport": { - "message": "Báo cáo trang web không an toàn" + "message": "Trang web không an toàn" }, "unsecuredWebsitesReportDesc": { "message": "URLs that start with http:// don’t use the best available encryption. Change the login URIs for these accounts to https:// for safer browsing." @@ -1798,7 +1798,7 @@ "message": "Inactive two-step login" }, "inactive2faReportDesc": { - "message": "Xác thực 2 bước là một bước quan trọng để bảo vệ tài khoản của bạn khỏi hacker. Nếu trang web cho phép, bạn nên kích hoạt xác thực 2 bước." + "message": "Đăng nhập hai bước sẽ thêm một lớp bảo vệ cho tài khoản của bạn. Thiết lập đăng nhập hai bước bằng trình xác thực Bitwarden cho các tài khoản này hoặc sử dụng phương pháp thay thế." }, "inactive2faFound": { "message": "Logins without two-step login found" @@ -1819,7 +1819,7 @@ "message": "Hướng dẫn" }, "exposedPasswordsReport": { - "message": "Báo cáo mật khẩu bị rò rỉ" + "message": "Mật khẩu bị rò rỉ" }, "exposedPasswordsReportDesc": { "message": "Passwords exposed in a data breach are easy targets for attackers. Change these passwords to prevent potential break-ins." @@ -1852,13 +1852,13 @@ } }, "weakPasswordsReport": { - "message": "Báo cáo mật khẩu không đảm bảo an toàn" + "message": "Mật khẩu yếu" }, "weakPasswordsReportDesc": { - "message": "Mật khẩu không an toàn có thể bị hacker và công cụ dò mật khẩu tự động đoán được dễ dàng . Chế độ tạo mật khẩu tự động của Bitwarden sẽ khắc phục vấn đề này." + "message": "Những kẻ tấn công có thể dễ dàng đoán được mật khẩu yếu. Thay đổi những mật khẩu này thành mật khẩu mạnh bằng cách sử dụng trình tạo mật khẩu." }, "weakPasswordsFound": { - "message": "Phát hiện mật khẩu không an toàn" + "message": "Phát hiện mật khẩu yếu" }, "weakPasswordsFoundDesc": { "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", @@ -1873,13 +1873,13 @@ "message": "No items in your vault have weak passwords." }, "reusedPasswordsReport": { - "message": "Báo cáo mật khẩu tái sử dụng" + "message": "Mật khẩu bị trùng" }, "reusedPasswordsReportDesc": { "message": "Reusing passwords makes it easier for attackers to break into multiple accounts. Change these passwords so that each is unique." }, "reusedPasswordsFound": { - "message": "Phát hiện mật khẩu tái sử dụng" + "message": "Phát hiện mật khẩu bị trùng" }, "reusedPasswordsFoundDesc": { "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", @@ -1903,7 +1903,7 @@ } }, "dataBreachReport": { - "message": "Báo cáo dữ liệu bị rò rĩ" + "message": "Dữ liệu bị rò rĩ" }, "breachDesc": { "message": "Breached accounts can expose your personal information. Secure breached accounts by enabling 2FA or creating a stronger password." @@ -2077,7 +2077,7 @@ "message": "# of additional GB" }, "additionalStorageIntervalDesc": { - "message": "Gói của bạn có $SIZE$ dung lượng lưu trữ tập tin có mã hóa. Bạn có thể mua thêm dung lượng với giá $PRICE$ cho mỗi GB/$INTERVAL$.", + "message": "Gói của bạn có $SIZE$ dung lượng lưu trữ tập tin mã hóa. Bạn có thể mua thêm dung lượng với giá $PRICE$ cho mỗi GB/$INTERVAL$.", "placeholders": { "size": { "content": "$1", @@ -2113,7 +2113,7 @@ "description": "Short abbreviation for 'month'" }, "paymentChargedAnnually": { - "message": "Phương thức thanh toán của bạn sẽ được thu phí ngay lập tức và sau đó sẽ định kỳ thu phí mỗi năm. Bạn có thể hủy bỏ bất cứ lúc nào." + "message": "Phương thức thanh toán của bạn sẽ được áp dụng ngay lập tức và gia hạn mỗi năm. Bạn có thể hủy bỏ bất cứ lúc nào." }, "paymentCharged": { "message": "Phương thức thanh toán của bạn sẽ được thu phí ngay lập tức và sau đó sẽ thu phí định kỳ mỗi $INTERVAL$. Bạn có thể hủy bất cứ lúc nào.", @@ -2128,58 +2128,58 @@ "message": "Your payment method will be charged for any unpaid subscriptions." }, "paymentChargedWithTrial": { - "message": "Gói của bạn đi kèm với 7 ngày dùng thử miễn phí. Phương thức thanh toán của bạn sẽ không bị tính phí cho đến khi hết thời gian dùng thử. Việc thanh toán sẽ thực hiện định kỳ mỗi $INTERVAL$. Bạn có thể hủy bỏ bất cứ lúc nào." + "message": "Gói của bạn đi kèm với 7 ngày dùng thử miễn phí. Phương thức thanh toán của bạn sẽ không bị tính phí cho đến khi hết thời gian dùng thử. Bạn có thể hủy bỏ bất cứ lúc nào." }, "paymentInformation": { - "message": "Thông Tin Thanh Toán" + "message": "Thông tin thanh toán" }, "billingInformation": { - "message": "Thông Tin Hóa Đơn" + "message": "Thông tin hóa đơn" }, "billingTrialSubLabel": { "message": "Phương thức thanh toán của bạn sẽ không bị thu phí trong 7 ngày dùng thử miễn phí." }, "creditCard": { - "message": "Thẻ Tín Dụng" + "message": "Thẻ tín dụng" }, "paypalClickSubmit": { "message": "Nhấn vào nút PayPal để đăng nhập vào tài khoản PayPal của bạn, sau đó nhấn vào nút Submit bên dưới để tiếp tục." }, "cancelSubscription": { - "message": "Hủy thuê bao" + "message": "Hủy gói" }, "subscriptionExpiration": { - "message": "Thuê bao hết hạn vào" + "message": "Gói hết hạn vào" }, "subscriptionCanceled": { - "message": "Thuê bao của bạn đã bị hủy." + "message": "Gói của bạn đã bị hủy." }, "pendingCancellation": { "message": "Đang chờ hủy" }, "subscriptionPendingCanceled": { - "message": "Thuê bao của bạn sẽ được huỷ tại cuối kì thanh toán hiện tại." + "message": "Gói của bạn sẽ được hủy vào cuối kì thanh toán hiện tại." }, "reinstateSubscription": { - "message": "Kích hoạt lại thuê bao" + "message": "Kích hoạt lại gói" }, "reinstateConfirmation": { "message": "Bạn có chắc muốn bỏ yêu cầu hủy đang chờ duyệt và kích hoạt lại thuê bao?" }, "reinstated": { - "message": "Đã kích hoạt lại thuê bao." + "message": "Đã kích hoạt lại gói." }, "cancelConfirmation": { "message": "Bạn có chắc muốn hủy không? Bạn sẽ mất hết quyền truy cập tất cả các tính năng của thuê bao này khi kì thanh toán kết thúc." }, "canceledSubscription": { - "message": "Đã hủy thuê bao" + "message": "Đã hủy gói" }, "neverExpires": { "message": "Never expires" }, "status": { - "message": "Trạng Thái" + "message": "Trạng thái" }, "nextCharge": { "message": "Next charge" @@ -2194,10 +2194,10 @@ "message": "Update license" }, "manageSubscription": { - "message": "Quản lý thuê bao" + "message": "Quản lý gói" }, "launchCloudSubscription": { - "message": "Bắt Đầu Đăng Ký Đám Mây" + "message": "Bắt đầu đăng ký Đám Mây" }, "storage": { "message": "Lưu trữ" @@ -2237,7 +2237,7 @@ "message": "Hóa đơn" }, "noInvoices": { - "message": "Không có hóa đơn." + "message": "Chưa có hóa đơn." }, "paid": { "message": "Paid", @@ -2409,7 +2409,7 @@ "message": "For businesses and other large organizations." }, "freeForever": { - "message": "Miễn Phí Mãi Mãi" + "message": "Miễn phí trọn đời" }, "includesXUsers": { "message": "includes $COUNT$ users", @@ -2565,7 +2565,7 @@ "message": "Tải ứng dụng" }, "loggedInAs": { - "message": "Đã đăng nhập là" + "message": "Đã đăng nhập" }, "eventLogs": { "message": "Event logs" @@ -2589,13 +2589,13 @@ "message": "New group" }, "addGroup": { - "message": "Thêm Nhóm" + "message": "Thêm nhóm" }, "editGroup": { - "message": "Chỉnh Sửa Nhóm" + "message": "Chỉnh sửa nhóm" }, "deleteGroupConfirmation": { - "message": "Bạn có chắc chắn muốn xóa nhóm này?" + "message": "Bạn có chắc muốn xóa nhóm này?" }, "deleteMultipleGroupsConfirmation": { "message": "Are you sure you want to delete the following $QUANTITY$ group(s)?", @@ -2781,7 +2781,7 @@ "message": "Two-step login saved" }, "disabled2fa": { - "message": "Đã tắt đăng nhập 2 bước." + "message": "Đã tắt đăng nhập 2 bước" }, "recovered2fa": { "message": "Recovered account from two-step login." @@ -3300,7 +3300,7 @@ } }, "rememberEmail": { - "message": "Ghi nhớ đăng nhập" + "message": "Ghi nhớ email" }, "recoverAccountTwoStepDesc": { "message": "If you cannot access your account through your normal two-step login methods, you can use your two-step login recovery code to turn off all two-step providers on your account." @@ -3318,7 +3318,7 @@ "message": "Nhập địa chỉ email của bạn vào bên dưới để khôi phục và xóa tài khoản của bạn." }, "deleteRecoverEmailSent": { - "message": "Nếu tài khoản của bạn có tồn tại, chúng tôi đã gửi cho bạn một email với hướng dẫn chi tiết." + "message": "Nếu tài khoản của bạn tồn tại, chúng tôi đã gửi cho bạn một email với hướng dẫn chi tiết." }, "deleteRecoverConfirmDesc": { "message": "Bạn đã yêu cầu xóa tài khoản Bitwarden của mình. Nhấn vào nút bên dưới để xác nhận." @@ -3363,21 +3363,21 @@ "message": "Organization saved" }, "taxInformation": { - "message": "Thông Tin Thuế" + "message": "Thông tin thuế" }, "taxInformationDesc": { - "message": "Đối với các khách hàng ở Mỹ, mã ZIP là bắt buộc để đáp ứng các yêu cầu về thuế bán hàng, đối với các quốc gia khác, bạn có thể tùy chọn cung cấp mã số thuế (VAT / GST) và/hoặc địa chỉ để xuất hiện trên hóa đơn của mình." + "message": "Đối với khách hàng Mỹ, mã ZIP là bắt buộc để đáp ứng các yêu cầu về thuế bán hàng, đối với các quốc gia khác, bạn có thể tùy chọn cung cấp mã số thuế (VAT/GST) và/hoặc địa chỉ để xuất hiện trên hóa đơn của mình." }, "billingPlan": { "message": "Gói", "description": "A billing plan/package. For example: Families, Teams, Enterprise, etc." }, "changeBillingPlan": { - "message": "Thay Đổi Gói", + "message": "Nâng cấp gói", "description": "A billing plan/package. For example: Families, Teams, Enterprise, etc." }, "changeBillingPlanUpgrade": { - "message": "Upgrade your account to another plan be providing the information below. Please ensure that you have an active payment method added to the account.", + "message": "Nâng cấp tài khoản của bạn lên gói khác bằng cách cung cấp thông tin bên dưới. Hãy đảm bảo rằng bạn đã thêm phương thức thanh toán đang hoạt động vào tài khoản.", "description": "A billing plan/package. For example: Families, Teams, Enterprise, etc." }, "invoiceNumber": { @@ -3391,10 +3391,10 @@ } }, "viewInvoice": { - "message": "Xem Hóa đơn" + "message": "Xem hóa đơn" }, "downloadInvoice": { - "message": "Tải Hóa đơn" + "message": "Tải xuống hóa đơn" }, "verifyBankAccount": { "message": "Xác minh tài khoản ngân hàng" @@ -3406,7 +3406,7 @@ "message": "Payment with a bank account is only available to customers in the United States. You will be required to verify your bank account. We will make two micro-deposits within the next 1-2 business days. Enter these amounts on the organization's billing page to verify the bank account." }, "verifyBankAccountFailureWarning": { - "message": "Nếu không xác nhận tài khoản ngân hàng, bạn có thể lỡ hạn thanh toán khiến cho thuê bao bị ngừng." + "message": "Nếu không xác minh tài khoản ngân hàng, bạn có thể lỡ hạn thanh toán khiến cho gói bị tạm ngừng." }, "verifiedBankAccount": { "message": "Bank account verified" @@ -3486,7 +3486,7 @@ "message": "Subscription seats" }, "subscriptionUpdated": { - "message": "Đã cập nhật thuê bao" + "message": "Đã cập nhật gói" }, "subscribedToSecretsManager": { "message": "Subscription updated. You now have access to Secrets Manager." @@ -3591,7 +3591,7 @@ "message": "Any encrypted exports that you have saved will also become invalid." }, "subscription": { - "message": "Thuê bao" + "message": "Gói" }, "loading": { "message": "Loading" @@ -3671,7 +3671,7 @@ "description": "ex. Date this item was created" }, "datePasswordUpdated": { - "message": "Mật khẩu đã cập nhật", + "message": "Mật khẩu đã được cập nhật", "description": "ex. Date this password was updated" }, "organizationIsDisabled": { @@ -3814,7 +3814,7 @@ "message": "You cannot perform this action while using an in-app purchase payment method." }, "manageSubscriptionFromStore": { - "message": "Bạn chỉ có thể quản lý thuê bao của mình từ cửa hàng ứng dụng mà bạn đã thanh toán." + "message": "Bạn chỉ có thể quản lý gói của mình từ cửa hàng ứng dụng mà bạn đã thanh toán." }, "minLength": { "message": "Minimum length" @@ -3918,7 +3918,7 @@ "message": "Tìm kiếm thùng rác" }, "permanentlyDelete": { - "message": "Xóa Vĩnh Viễn" + "message": "Xoá vĩnh viễn" }, "permanentlyDeleteSelected": { "message": "Permanently delete selected" @@ -3927,7 +3927,7 @@ "message": "Permanently delete item" }, "permanentlyDeleteItemConfirmation": { - "message": "Bạn có chắc chắn muốn xóa vĩnh viễn mục này không?" + "message": "Bạn có chắc muốn xóa vĩnh viễn mục này?" }, "permanentlyDeletedItem": { "message": "Item permanently deleted" @@ -4096,56 +4096,56 @@ "message": "Văn bản" }, "createSend": { - "message": "Tạo Send mới", + "message": "Gửi mới", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "editSend": { - "message": "Chỉnh sửa Send", + "message": "Sửa mục Gửi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "createdSend": { - "message": "Đã tạo Send", + "message": "Đã lưu mục Gửi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "editedSend": { - "message": "Đã chỉnh sửa Send", + "message": "Đã lưu mục Gửi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "deletedSend": { - "message": "Đã xóa Send", + "message": "Đã xóa mục Gửi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "deleteSend": { - "message": "Xóa Send", + "message": "Xóa mục Gửi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "deleteSendConfirmation": { - "message": "Bạn có chắc chắn muốn xóa Send này?", + "message": "Bạn có chắc muốn xóa mục Gửi này?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "whatTypeOfSend": { - "message": "Đây là loại Send gì?", + "message": "Đây là kiểu Gửi gì?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "deletionDate": { "message": "Deletion date" }, "deletionDateDesc": { - "message": "Send sẽ được xóa vĩnh viễn vào ngày và giờ được chỉ định.", + "message": "Mục Gửi sẽ được xóa vĩnh viễn vào ngày và giờ được chỉ định.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "expirationDate": { "message": "Expiration date" }, "expirationDateDesc": { - "message": "Nếu được thiết lập, truy cập vào Send này sẽ hết hạn vào ngày và giờ được chỉ định.", + "message": "Nếu được thiết lập, mục Gửi này sẽ hết hạn vào ngày và giờ được chỉ định.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "maxAccessCount": { "message": "Maximum access count" }, "maxAccessCountDesc": { - "message": "Nếu được thiết lập, khi đã đạt tới số lượng truy cập tối đa, người dùng sẽ không thể truy cập Send này nữa.", + "message": "Nếu được thiết lập, khi đã đạt tới số lượng truy cập tối đa, người dùng sẽ không thể truy cập mục Gửi này nữa.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "currentAccessCount": { @@ -4170,7 +4170,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "copySendLink": { - "message": "Sao chép liên kết Send", + "message": "Sao chép liên kết mục Gửi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "removePassword": { @@ -4190,7 +4190,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "allSends": { - "message": "Toàn bộ Send" + "message": "Tất cả mục Gửi" }, "maxAccessCountReached": { "message": "Max access count reached", @@ -4203,11 +4203,11 @@ "message": "Expired" }, "searchSends": { - "message": "Tìm kiếm trong Send", + "message": "Tìm kiếm mục Gửi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendProtectedPassword": { - "message": "Send này được bảo vệ bằng mật khẩu. Hãy nhập mật khẩu vào bên dưới để tiếp tục.", + "message": "Mục Gửi này được bảo vệ bằng mật khẩu. Hãy nhập mật khẩu vào bên dưới để tiếp tục.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendProtectedPasswordDontKnow": { @@ -4218,8 +4218,8 @@ "message": "This Send is hidden by default. You can toggle its visibility using the button below.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "Download file" + "downloadAttachments": { + "message": "Tải xuống tập tin đính kèm" }, "sendAccessUnavailable": { "message": "The Send you are trying to access does not exist or is no longer available.", @@ -4230,11 +4230,11 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "noSendsInList": { - "message": "Không có Sends trong danh sách.", + "message": "Chưa có mục Gửi.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "emergencyAccess": { - "message": "Truy Cập Khẩn Cấp" + "message": "Truy cập khẩn cấp" }, "emergencyAccessDesc": { "message": "Grant and manage emergency access for trusted contacts. Trusted contacts may request access to either View or Takeover your account in case of an emergency. Visit our help page for more information and details into how zero knowledge sharing works." @@ -4388,7 +4388,7 @@ "message": "Due to an Enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." }, "disableSend": { - "message": "Tắt Send" + "message": "Xóa Gửi" }, "disableSendPolicyDesc": { "message": "Do not allow members to create or edit Sends.", @@ -4398,11 +4398,11 @@ "message": "Organization members that can manage the organization's policies are exempt from this policy's enforcement." }, "sendDisabled": { - "message": "Đã tắt Send", + "message": "Đã loại bỏ Gửi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendDisabledWarning": { - "message": "Do chính sách doanh nghiệp, bạn chỉ có thể xóa những Send hiện có.", + "message": "Do chính sách doanh nghiệp, bạn chỉ có thể xóa những mục Gửi hiện có.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendOptions": { @@ -4457,7 +4457,7 @@ "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Custom roles is an enterprise feature. Contact our support team to upgrade your subscription'" }, "customDescNonEnterpriseEnd": { - "message": ". Hãy liên lạc với đội ngũ hỗ trợ của chúng tôi để nâng cấp thuê bao của bạn", + "message": ". Hãy liên lạc với đội ngũ hỗ trợ của chúng tôi để nâng cấp gói của bạn", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Custom roles is an enterprise feature. Contact our support team to upgrade your subscription'" }, "customNonEnterpriseError": { @@ -4518,7 +4518,7 @@ "message": "Manage SSO" }, "manageUsers": { - "message": "Quản Lý Người Dùng" + "message": "Quản lý người dùng" }, "manageAccountRecovery": { "message": "Manage account recovery" @@ -4546,7 +4546,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNameDesc": { - "message": "Một tên gợi nhớ để mô tả về Send này.", + "message": "Một tên gợi nhớ để mô tả mục Gửi này.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendTextDesc": { @@ -4563,7 +4563,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "send": { - "message": "Chia sẻ", + "message": "Gửi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendAccessTaglineProductDesc": { @@ -4609,8 +4609,8 @@ "message": "thử ngay hôm nay.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden user $USER_IDENTIFIER$ shared the following with you", + "sendAccessCreatorIdentifier": { + "message": "Thành viên Bitwarden $USER_IDENTIFIER$ đã chia sẻ với bạn", "placeholders": { "user_identifier": { "content": "$1", @@ -5297,7 +5297,7 @@ "message": "Remove sponsorship" }, "removeSponsorshipConfirmation": { - "message": "Nếu xóa người tài trợ, bạn sẽ chịu trách nhiệm cho chi phí của thuê bao này và cước phát sinh về sau. Bạn có chắc muốn tiếp tục không?" + "message": "Nếu xóa người tài trợ, bạn sẽ chịu trách nhiệm cho chi phí của gói này và cước phát sinh về sau. Bạn có chắc muốn tiếp tục?" }, "sponsorshipCreated": { "message": "Sponsorship created" @@ -5610,7 +5610,7 @@ "message": "Keys" }, "billingHistory": { - "message": "Lịch sử Thanh toán" + "message": "Lịch sử thanh toán" }, "backToReports": { "message": "Back to reports" @@ -7182,6 +7182,9 @@ "next": { "message": "Next" }, + "ssoLoginIsRequired": { + "message": "Yêu cầu đăng nhập bằng SSO" + }, "selectedRegionFlag": { "message": "Selected region flag" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "Đã xảy ra lỗi khi tải mục Gửi này. Hãy thử lại sau." } } diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index ee59470c474..2d40fe30386 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -494,7 +494,7 @@ "message": "附件已删除" }, "deleteAttachmentConfirmation": { - "message": "您确定要删除此附件吗?" + "message": "确定要删除此附件吗?" }, "attachmentSaved": { "message": "附件已保存" @@ -558,7 +558,7 @@ "message": "项目已移动" }, "overwritePasswordConfirmation": { - "message": "您确定要覆盖当前密码吗?" + "message": "确定要覆盖当前密码吗?" }, "editedFolder": { "message": "文件夹已保存" @@ -567,7 +567,7 @@ "message": "文件夹已添加" }, "deleteFolderConfirmation": { - "message": "您确定要删除此文件夹吗?" + "message": "确定要删除此文件夹吗?" }, "deletedFolder": { "message": "文件夹已删除" @@ -585,7 +585,7 @@ "message": "您的登录会话已过期。" }, "logOutConfirmation": { - "message": "您确定要注销吗?" + "message": "确定要注销吗?" }, "logOut": { "message": "注销" @@ -998,7 +998,7 @@ } }, "deleteSelectedConfirmation": { - "message": "您确定要继续吗?" + "message": "确定要继续吗?" }, "moveSelectedItemsDesc": { "message": "选择要将这 $COUNT$ 个项目移动到的文件夹。", @@ -1384,7 +1384,7 @@ "message": "选择一个集合" }, "importTargetHint": { - "message": "如果您希望将导入的文件内容移动到 $DESTINATION$,请选择此选项", + "message": "如果您希望将导入的文件内容移动到某个 $DESTINATION$,请选择此选项", "description": "Located as a hint under the import target. Will be appended by either folder or collection, depending if the user is importing into an individual or an organizational vault.", "placeholders": { "destination": { @@ -1609,7 +1609,7 @@ "message": "如果您要把它添加到其他设备,下面是您的验证器 App 所需要的二维码(或密钥)。" }, "twoStepDisableDesc": { - "message": "您确定要停用此两步登录提供程序吗?" + "message": "确定要停用此两步登录提供程序吗?" }, "twoStepDisabled": { "message": "此两步登录提供程序已停用。" @@ -1708,7 +1708,7 @@ "message": "添加一个 FIDO U2F 安全钥匙到您的帐户" }, "removeU2fConfirmation": { - "message": "您确认要删除这个安全钥匙吗?" + "message": "确认要删除此安全钥匙吗?" }, "twoFactorWebAuthnAdd": { "message": "添加一个 WebAuthn 安全钥匙到您的账户" @@ -2164,13 +2164,13 @@ "message": "恢复订阅" }, "reinstateConfirmation": { - "message": "您确定要撤销请求并恢复订阅吗?" + "message": "确定要移除待处理的取消请求并恢复订阅吗?" }, "reinstated": { "message": "您的订阅已恢复。" }, "cancelConfirmation": { - "message": "您确定要取消吗?在本次计费周期结束后,您将无法使用此订阅的所有功能。" + "message": "确定要取消吗?在本次计费周期结束后,您将无法使用此订阅的所有功能。" }, "canceledSubscription": { "message": "订阅已取消" @@ -2550,7 +2550,7 @@ "message": "退出" }, "leaveOrganizationConfirmation": { - "message": "您确定要退出这个组织吗?" + "message": "确定要退出该组织吗?" }, "leftOrganization": { "message": "您已经退出该组织。" @@ -2595,10 +2595,10 @@ "message": "编辑群组" }, "deleteGroupConfirmation": { - "message": "您确定要删除此群组吗?" + "message": "确定要删除此群组吗?" }, "deleteMultipleGroupsConfirmation": { - "message": "您确定要删除如下 $QUANTITY$ 个群组吗?", + "message": "确定要删除如下 $QUANTITY$ 个群组吗?", "placeholders": { "quantity": { "content": "$1", @@ -2607,7 +2607,7 @@ } }, "removeUserConfirmation": { - "message": "您确实要移除此用户吗?" + "message": "确实要移除此用户吗?" }, "removeOrgUserConfirmation": { "message": "移除成员后,他们将不再具有对组织数据的访问权限,并且此操作是不可逆的。要将此成员添加回组织,必须再次邀请他们并加入。" @@ -3727,7 +3727,7 @@ "message": "脆弱的主密码" }, "weakMasterPasswordDesc": { - "message": "识别到弱密码。请使用一个强密码以保护你的账户。你确定你要使用弱密码吗?" + "message": "识别到弱密码。请使用一个强密码以保护你的账户。确定要使用弱密码吗?" }, "rotateAccountEncKey": { "message": "同时轮换账户的加密密钥" @@ -3736,7 +3736,7 @@ "message": "轮换加密密钥" }, "rotateEncKeyConfirmation": { - "message": "您确定要轮换账户的加密密钥吗?" + "message": "确定要轮换账户的加密密钥吗?" }, "attachmentsNeedFix": { "message": "此项目有需要修复的旧文件附件。" @@ -3927,7 +3927,7 @@ "message": "永久删除项目" }, "permanentlyDeleteItemConfirmation": { - "message": "您确定要永久删除此项目吗?" + "message": "确定要永久删除此项目吗?" }, "permanentlyDeletedItem": { "message": "项目已永久删除" @@ -4054,7 +4054,7 @@ "message": "取消链接 SSO" }, "unlinkSsoConfirmation": { - "message": "您确定要断开该组织的 SSO 链接吗?" + "message": "确定要取消该组织的 SSO 链接吗?" }, "linkSso": { "message": "链接 SSO" @@ -4218,8 +4218,8 @@ "message": "此 Send 默认隐藏。您可使用下方的按钮切换其可见性。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "下载文件" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "您尝试访问的 Send 不存在或不再可用。", @@ -4324,7 +4324,7 @@ "message": "请求访问权限" }, "requestAccessConfirmation": { - "message": "您确定要申请紧急访问吗?这将在 $WAITTIME$ 天后或当用户手动批准请求时获得访问权限。", + "message": "确定要申请紧急访问吗?这将在 $WAITTIME$ 天后或当用户手动批准请求时获得访问权限。", "placeholders": { "waittime": { "content": "$1", @@ -4348,7 +4348,7 @@ "message": "拒绝" }, "approveAccessConfirmation": { - "message": "您确定要批准紧急访问吗?这将允许 $USER$ $ACTION$ 您的账户。", + "message": "确定要批准紧急访问吗?这将允许 $USER$ $ACTION$ 您的账户。", "placeholders": { "user": { "content": "$1", @@ -4609,8 +4609,8 @@ "message": "来马上尝试。", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden 用户 $USER_IDENTIFIER$ 与您分享了以下内容", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -4788,7 +4788,7 @@ "message": "此操作不适用于所选用户。" }, "removeUsersWarning": { - "message": "您确定要移除以下用户吗?该过程可能需要几秒钟才能完成,并且不能中断或取消。" + "message": "确定要移除以下用户吗?该过程可能需要几秒钟才能完成,并且不能中断或取消。" }, "removeOrgUsersConfirmation": { "message": "移除成员后,他们将不再具有对组织数据的访问权限,并且此操作是不可逆的。要将成员添加回组织,必须再次邀请他们并加入。该过程可能需要几秒钟才能完成,并且不能被中断或取消。" @@ -4921,7 +4921,7 @@ "message": "我的提供商" }, "addOrganizationConfirmation": { - "message": "您确定要将 $ORGANIZATION$ 添加为 $PROVIDER$ 的客户吗?", + "message": "确定要将 $ORGANIZATION$ 添加为 $PROVIDER$ 的客户吗?", "placeholders": { "organization": { "content": "$1", @@ -4970,7 +4970,7 @@ } }, "detachOrganizationConfirmation": { - "message": "您确定要分离这个组织吗?该组织将继续存在,但不再由此提供商管理。" + "message": "确定要分离这个组织吗?该组织将继续存在,但不再由此提供商管理。" }, "add": { "message": "添加" @@ -5297,7 +5297,7 @@ "message": "移除赞助" }, "removeSponsorshipConfirmation": { - "message": "移除赞助后,将由您自己负责此订阅及其相关的账单。您确定要继续吗?" + "message": "移除赞助后,将由您自己负责此订阅及其相关的账单。确定要继续吗?" }, "sponsorshipCreated": { "message": "赞助已创建" @@ -5306,7 +5306,7 @@ "message": "电子邮件已发送" }, "revokeSponsorshipConfirmation": { - "message": "移除该账户后,家庭计划赞助将在计费周期结束时到期。在现有的赞助到期之前您将无法兑换新的赞助邀请。您确定要继续吗?" + "message": "移除该账户后,家庭计划赞助将在计费周期结束时到期。在现有的赞助到期之前您将无法兑换新的赞助邀请。确定要继续吗?" }, "removeSponsorshipSuccess": { "message": "赞助已移除" @@ -5754,7 +5754,7 @@ "message": "已更新设备验证" }, "areYouSureYouWantToEnableDeviceVerificationTheVerificationCodeEmailsWillArriveAtX": { - "message": "您确定要开启设备验证吗?验证码邮件将发送到 $EMAIL$", + "message": "确定要开启设备验证吗?验证码邮件将发送到 $EMAIL$", "placeholders": { "email": { "content": "$1", @@ -6189,10 +6189,10 @@ "description": "Notifies that the selected secrets have been moved to the trash" }, "hardDeleteSecretConfirmation": { - "message": "您确定要永久删除此机密吗?" + "message": "确定要永久删除此机密吗?" }, "hardDeleteSecretsConfirmation": { - "message": "您确定要永久删除这些机密吗?" + "message": "确定要永久删除这些机密吗?" }, "hardDeletesSuccessToast": { "message": "机密已永久删除" @@ -6500,7 +6500,7 @@ "message": "移除域名" }, "removeDomainWarning": { - "message": "移除域名不能撤消。您确定要继续吗?" + "message": "移除域名不能撤消。确定要继续吗?" }, "domainRemoved": { "message": "域名已移除" @@ -6891,10 +6891,10 @@ "message": "恢复机密" }, "restoreSecretPrompt": { - "message": "您确定要恢复此机密吗?" + "message": "确定要恢复此机密吗?" }, "restoreSecretsPrompt": { - "message": "您确定要恢复这些密机密吗?" + "message": "确定要恢复这些密机密吗?" }, "secretRestoredSuccessToast": { "message": "机密已恢复" @@ -6970,7 +6970,7 @@ "description": "Notification to inform the user that a form's input can't contain only whitespace." }, "dismiss": { - "message": "取消" + "message": "忽略" }, "notAvailableForFreeOrganization": { "message": "免费组织不能使用此功能。请联系您的组织所有者寻求升级。" @@ -7182,6 +7182,9 @@ "next": { "message": "下一步" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "选择的区域旗帜" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "工程访问权限已更新" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index fb9ddb850c2..0b56f49e779 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -4218,8 +4218,8 @@ "message": "此 Send 預設為隱藏。您可使用下方的按鈕切換其可見度。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, - "downloadFile": { - "message": "下載檔案" + "downloadAttachments": { + "message": "Download attachments" }, "sendAccessUnavailable": { "message": "您嘗試存取的 Send 不存在或無法再使用。", @@ -4609,8 +4609,8 @@ "message": "現在就試試。", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more about Bitwarden Send or sign up to **try it today.**'" }, - "sendCreatorIdentifier": { - "message": "Bitwarden 使用者 $USER_IDENTIFIER$ 與您共用了以下內容", + "sendAccessCreatorIdentifier": { + "message": "Bitwarden member $USER_IDENTIFIER$ shared the following with you", "placeholders": { "user_identifier": { "content": "$1", @@ -7182,6 +7182,9 @@ "next": { "message": "下一步" }, + "ssoLoginIsRequired": { + "message": "SSO login is required" + }, "selectedRegionFlag": { "message": "選定的區域標記" }, @@ -7392,5 +7395,8 @@ }, "projectAccessUpdated": { "message": "Project access updated" + }, + "unexpectedErrorSend": { + "message": "An unexpected error has occurred while loading this Send. Try again later." } } diff --git a/libs/angular/test-utils.ts b/libs/angular/test-utils.ts deleted file mode 100644 index a2422e698fd..00000000000 --- a/libs/angular/test-utils.ts +++ /dev/null @@ -1,3 +0,0 @@ -export async function awaitAsync(ms = 0) { - await new Promise((resolve) => setTimeout(resolve, ms)); -} diff --git a/libs/common/spec/fake-storage.service.ts b/libs/common/spec/fake-storage.service.ts index 281df01533f..ba3e4613466 100644 --- a/libs/common/spec/fake-storage.service.ts +++ b/libs/common/spec/fake-storage.service.ts @@ -10,6 +10,7 @@ import { StorageOptions } from "../src/platform/models/domain/storage-options"; export class FakeStorageService implements AbstractStorageService { private store: Record; private updatesSubject = new Subject(); + private _valuesRequireDeserialization = false; /** * Returns a mock of a {@see AbstractStorageService} for asserting the expected @@ -32,6 +33,18 @@ export class FakeStorageService implements AbstractStorageService { this.store = store; } + get internalStore() { + return this.store; + } + + internalUpdateValuesRequireDeserialization(value: boolean) { + this._valuesRequireDeserialization = value; + } + + get valuesRequireDeserialization(): boolean { + return this._valuesRequireDeserialization; + } + get updates$() { return this.updatesSubject.asObservable(); } @@ -48,13 +61,13 @@ export class FakeStorageService implements AbstractStorageService { save(key: string, obj: T, options?: StorageOptions): Promise { this.mock.save(key, options); this.store[key] = obj; - this.updatesSubject.next({ key: key, value: obj, updateType: "save" }); + this.updatesSubject.next({ key: key, updateType: "save" }); return Promise.resolve(); } remove(key: string, options?: StorageOptions): Promise { this.mock.remove(key, options); delete this.store[key]; - this.updatesSubject.next({ key: key, value: undefined, updateType: "remove" }); + this.updatesSubject.next({ key: key, updateType: "remove" }); return Promise.resolve(); } } diff --git a/libs/common/spec/utils.ts b/libs/common/spec/utils.ts index 91d2033da8a..7db5711adfa 100644 --- a/libs/common/spec/utils.ts +++ b/libs/common/spec/utils.ts @@ -84,3 +84,11 @@ function clone(value: any): any { return JSON.parse(JSON.stringify(value)); } } + +export async function awaitAsync(ms = 0) { + if (ms < 1) { + await Promise.resolve(); + } else { + await new Promise((resolve) => setTimeout(resolve, ms)); + } +} diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index a40f6b36b18..8c31b6d3eb6 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -420,8 +420,8 @@ export abstract class StateService { setMainWindowSize: (value: number, options?: StorageOptions) => Promise; getMinimizeOnCopyToClipboard: (options?: StorageOptions) => Promise; setMinimizeOnCopyToClipboard: (value: boolean, options?: StorageOptions) => Promise; - getNeverDomains: (options?: StorageOptions) => Promise<{ [id: string]: any }>; - setNeverDomains: (value: { [id: string]: any }, options?: StorageOptions) => Promise; + getNeverDomains: (options?: StorageOptions) => Promise<{ [id: string]: unknown }>; + setNeverDomains: (value: { [id: string]: unknown }, options?: StorageOptions) => Promise; getNoAutoPromptBiometricsText: (options?: StorageOptions) => Promise; setNoAutoPromptBiometricsText: (value: string, options?: StorageOptions) => Promise; getOpenAtLogin: (options?: StorageOptions) => Promise; diff --git a/libs/common/src/platform/abstractions/storage.service.ts b/libs/common/src/platform/abstractions/storage.service.ts index 8beac2c1c1e..c0e3478f547 100644 --- a/libs/common/src/platform/abstractions/storage.service.ts +++ b/libs/common/src/platform/abstractions/storage.service.ts @@ -5,11 +5,11 @@ import { MemoryStorageOptions, StorageOptions } from "../models/domain/storage-o export type StorageUpdateType = "save" | "remove"; export type StorageUpdate = { key: string; - value?: unknown; updateType: StorageUpdateType; }; export abstract class AbstractStorageService { + abstract get valuesRequireDeserialization(): boolean; /** * Provides an {@link Observable} that represents a stream of updates that * have happened in this storage service or in the storage this service provides diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index 3319e4a8f85..359e765f792 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -222,12 +222,9 @@ export class AccountSettings { clearClipboard?: number; collapsedGroupings?: string[]; defaultUriMatch?: UriMatchType; - disableAddLoginNotification?: boolean; disableAutoBiometricsPrompt?: boolean; disableAutoTotpCopy?: boolean; disableBadgeCounter?: boolean; - disableChangedPasswordNotification?: boolean; - disableContextMenuItem?: boolean; disableGa?: boolean; dismissedAutoFillOnPageLoadCallout?: boolean; dontShowCardsCurrentTab?: boolean; @@ -239,7 +236,6 @@ export class AccountSettings { environmentUrls: EnvironmentUrls = new EnvironmentUrls(); equivalentDomains?: any; minimizeOnCopyToClipboard?: boolean; - neverDomains?: { [id: string]: any }; passwordGenerationOptions?: PasswordGeneratorOptions; usernameGenerationOptions?: UsernameGeneratorOptions; generatorOptions?: GeneratorOptions; diff --git a/libs/common/src/platform/models/domain/global-state.ts b/libs/common/src/platform/models/domain/global-state.ts index 30ad32124cf..ec8ccd9c03d 100644 --- a/libs/common/src/platform/models/domain/global-state.ts +++ b/libs/common/src/platform/models/domain/global-state.ts @@ -36,4 +36,8 @@ export class GlobalState { enableBrowserIntegrationFingerprint?: boolean; enableDuckDuckGoBrowserIntegration?: boolean; region?: string; + neverDomains?: { [id: string]: unknown }; + disableAddLoginNotification?: boolean; + disableChangedPasswordNotification?: boolean; + disableContextMenuItem?: boolean; } diff --git a/libs/common/src/platform/services/memory-storage.service.ts b/libs/common/src/platform/services/memory-storage.service.ts index abfc0b1597b..f3ad25d3a0b 100644 --- a/libs/common/src/platform/services/memory-storage.service.ts +++ b/libs/common/src/platform/services/memory-storage.service.ts @@ -6,6 +6,9 @@ export class MemoryStorageService extends AbstractMemoryStorageService { private store = new Map(); private updatesSubject = new Subject(); + get valuesRequireDeserialization(): boolean { + return false; + } get updates$() { return this.updatesSubject.asObservable(); } @@ -27,13 +30,13 @@ export class MemoryStorageService extends AbstractMemoryStorageService { return this.remove(key); } this.store.set(key, obj); - this.updatesSubject.next({ key, value: obj, updateType: "save" }); + this.updatesSubject.next({ key, updateType: "save" }); return Promise.resolve(); } remove(key: string): Promise { this.store.delete(key); - this.updatesSubject.next({ key, value: null, updateType: "remove" }); + this.updatesSubject.next({ key, updateType: "remove" }); return Promise.resolve(); } diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index d9dcb93a366..a006ecae3ad 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -1121,18 +1121,18 @@ export class StateService< async getDisableAddLoginNotification(options?: StorageOptions): Promise { return ( - (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.settings?.disableAddLoginNotification ?? false + (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) + ?.disableAddLoginNotification ?? false ); } async setDisableAddLoginNotification(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( + const globals = await this.getGlobals( this.reconcileOptions(options, await this.defaultOnDiskOptions()) ); - account.settings.disableAddLoginNotification = value; - await this.saveAccount( - account, + globals.disableAddLoginNotification = value; + await this.saveGlobals( + globals, this.reconcileOptions(options, await this.defaultOnDiskOptions()) ); } @@ -1193,8 +1193,8 @@ export class StateService< async getDisableChangedPasswordNotification(options?: StorageOptions): Promise { return ( - (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.settings?.disableChangedPasswordNotification ?? false + (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) + ?.disableChangedPasswordNotification ?? false ); } @@ -1202,30 +1202,30 @@ export class StateService< value: boolean, options?: StorageOptions ): Promise { - const account = await this.getAccount( + const globals = await this.getGlobals( this.reconcileOptions(options, await this.defaultOnDiskOptions()) ); - account.settings.disableChangedPasswordNotification = value; - await this.saveAccount( - account, + globals.disableChangedPasswordNotification = value; + await this.saveGlobals( + globals, this.reconcileOptions(options, await this.defaultOnDiskOptions()) ); } async getDisableContextMenuItem(options?: StorageOptions): Promise { return ( - (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.settings?.disableContextMenuItem ?? false + (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) + ?.disableContextMenuItem ?? false ); } async setDisableContextMenuItem(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( + const globals = await this.getGlobals( this.reconcileOptions(options, await this.defaultOnDiskOptions()) ); - account.settings.disableContextMenuItem = value; - await this.saveAccount( - account, + globals.disableContextMenuItem = value; + await this.saveGlobals( + globals, this.reconcileOptions(options, await this.defaultOnDiskOptions()) ); } @@ -2295,19 +2295,19 @@ export class StateService< ); } - async getNeverDomains(options?: StorageOptions): Promise<{ [id: string]: any }> { + async getNeverDomains(options?: StorageOptions): Promise<{ [id: string]: unknown }> { return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.settings?.neverDomains; + await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())) + )?.neverDomains; } - async setNeverDomains(value: { [id: string]: any }, options?: StorageOptions): Promise { - const account = await this.getAccount( + async setNeverDomains(value: { [id: string]: unknown }, options?: StorageOptions): Promise { + const globals = await this.getGlobals( this.reconcileOptions(options, await this.defaultOnDiskOptions()) ); - account.settings.neverDomains = value; - await this.saveAccount( - account, + globals.neverDomains = value; + await this.saveGlobals( + globals, this.reconcileOptions(options, await this.defaultOnDiskOptions()) ); } diff --git a/libs/common/src/platform/state/global-state.ts b/libs/common/src/platform/state/global-state.ts index 3d330668def..c14338a2fb9 100644 --- a/libs/common/src/platform/state/global-state.ts +++ b/libs/common/src/platform/state/global-state.ts @@ -1,5 +1,7 @@ import { Observable } from "rxjs"; +import { StateUpdateOptions } from "./state-update-options"; + /** * A helper object for interacting with state that is scoped to a specific domain * but is not scoped to a user. This is application wide storage. @@ -8,9 +10,16 @@ export interface GlobalState { /** * Method for allowing you to manipulate state in an additive way. * @param configureState callback for how you want manipulate this section of state + * @param options Defaults given by @see {module:state-update-options#DEFAULT_OPTIONS} + * @param options.shouldUpdate A callback for determining if you want to update state. Defaults to () => true + * @param options.combineLatestWith An observable that you want to combine with the current state for callbacks. Defaults to null + * @param options.msTimeout A timeout for how long you are willing to wait for a `combineLatestWith` option to complete. Defaults to 1000ms. Only applies if `combineLatestWith` is set. * @returns A promise that must be awaited before your next action to ensure the update has been written to state. */ - update: (configureState: (state: T) => T) => Promise; + update: ( + configureState: (state: T, dependency: TCombine) => T, + options?: StateUpdateOptions + ) => Promise; /** * An observable stream of this state, the first emission of this will be the current state on disk diff --git a/libs/common/src/platform/state/implementations/default-global-state.spec.ts b/libs/common/src/platform/state/implementations/default-global-state.spec.ts index 86dc7e16709..656b8031c6e 100644 --- a/libs/common/src/platform/state/implementations/default-global-state.spec.ts +++ b/libs/common/src/platform/state/implementations/default-global-state.spec.ts @@ -3,9 +3,10 @@ * @jest-environment ../shared/test.environment.ts */ +import { firstValueFrom, of } from "rxjs"; import { Jsonify } from "type-fest"; -import { trackEmissions } from "../../../../spec"; +import { trackEmissions, awaitAsync } from "../../../../spec"; import { FakeStorageService } from "../../../../spec/fake-storage.service"; import { KeyDefinition, globalKeyBuilder } from "../key-definition"; import { StateDefinition } from "../state-definition"; @@ -28,16 +29,15 @@ class TestState { const testStateDefinition = new StateDefinition("fake", "disk"); -const testKeyDefinition = new KeyDefinition( - testStateDefinition, - "fake", - TestState.fromJSON -); +const testKeyDefinition = new KeyDefinition(testStateDefinition, "fake", { + deserializer: TestState.fromJSON, +}); const globalKey = globalKeyBuilder(testKeyDefinition); describe("DefaultGlobalState", () => { let diskStorageService: FakeStorageService; let globalState: DefaultGlobalState; + const newData = { date: new Date() }; beforeEach(() => { diskStorageService = new FakeStorageService(); @@ -48,51 +48,154 @@ describe("DefaultGlobalState", () => { jest.resetAllMocks(); }); - it("should emit when storage updates", async () => { - const emissions = trackEmissions(globalState.state$); - const newData = { date: new Date() }; - await diskStorageService.save(globalKey, newData); + describe("state$", () => { + it("should emit when storage updates", async () => { + const emissions = trackEmissions(globalState.state$); + await diskStorageService.save(globalKey, newData); + await awaitAsync(); - expect(emissions).toEqual([ - null, // Initial value - newData, - // JSON.parse(JSON.stringify(newData)), // This is due to the way `trackEmissions` clones - ]); - }); - - it("should not emit when update key does not match", async () => { - const emissions = trackEmissions(globalState.state$); - const newData = { date: new Date() }; - await diskStorageService.save("wrong_key", newData); - - expect(emissions).toEqual( - expect.arrayContaining([ + expect(emissions).toEqual([ null, // Initial value - ]) - ); - }); - - it("should save on update", async () => { - const newData = { date: new Date() }; - const result = await globalState.update((state) => { - return newData; + newData, + ]); }); - expect(diskStorageService.mock.save).toHaveBeenCalledTimes(1); - expect(result).toEqual(newData); - }); + it("should not emit when update key does not match", async () => { + const emissions = trackEmissions(globalState.state$); + await diskStorageService.save("wrong_key", newData); - it("should emit once per update", async () => { - const emissions = trackEmissions(globalState.state$); - const newData = { date: new Date() }; - - await globalState.update((state) => { - return newData; + expect(emissions).toHaveLength(0); }); - expect(emissions).toEqual([ - null, // Initial value - newData, - ]); + it("should emit initial storage value on first subscribe", async () => { + const initialStorage: Record = {}; + initialStorage[globalKey] = TestState.fromJSON({ + date: "2022-09-21T13:14:17.648Z", + }); + diskStorageService.internalUpdateStore(initialStorage); + + const state = await firstValueFrom(globalState.state$); + expect(diskStorageService.mock.get).toHaveBeenCalledTimes(1); + expect(diskStorageService.mock.get).toHaveBeenCalledWith("global_fake_fake", undefined); + expect(state).toBeTruthy(); + }); + }); + + describe("update", () => { + it("should save on update", async () => { + const result = await globalState.update((state) => { + return newData; + }); + + expect(diskStorageService.mock.save).toHaveBeenCalledTimes(1); + expect(result).toEqual(newData); + }); + + it("should emit once per update", async () => { + const emissions = trackEmissions(globalState.state$); + await awaitAsync(); // storage updates are behind a promise + + await globalState.update((state) => { + return newData; + }); + + await awaitAsync(); + + expect(emissions).toEqual([ + null, // Initial value + newData, + ]); + }); + + it("should provided combined dependencies", async () => { + const emissions = trackEmissions(globalState.state$); + await awaitAsync(); // storage updates are behind a promise + + const combinedDependencies = { date: new Date() }; + + await globalState.update( + (state, dependencies) => { + expect(dependencies).toEqual(combinedDependencies); + return newData; + }, + { + combineLatestWith: of(combinedDependencies), + } + ); + + await awaitAsync(); + + expect(emissions).toEqual([ + null, // Initial value + newData, + ]); + }); + + it("should not update if shouldUpdate returns false", async () => { + const emissions = trackEmissions(globalState.state$); + + const result = await globalState.update( + (state) => { + return newData; + }, + { + shouldUpdate: () => false, + } + ); + + expect(diskStorageService.mock.save).not.toHaveBeenCalled(); + expect(emissions).toEqual([null]); // Initial value + expect(result).toBeUndefined(); + }); + + it("should provide the update callback with the current State", async () => { + const emissions = trackEmissions(globalState.state$); + await awaitAsync(); // storage updates are behind a promise + + // Seed with interesting data + const initialData = { date: new Date(2020, 1, 1) }; + await globalState.update((state, dependencies) => { + return initialData; + }); + + await awaitAsync(); + + await globalState.update((state) => { + expect(state).toEqual(initialData); + return newData; + }); + + await awaitAsync(); + + expect(emissions).toEqual([ + null, // Initial value + initialData, + newData, + ]); + }); + + it("should give initial state for update call", async () => { + const initialStorage: Record = {}; + const initialState = TestState.fromJSON({ + date: "2022-09-21T13:14:17.648Z", + }); + initialStorage[globalKey] = initialState; + diskStorageService.internalUpdateStore(initialStorage); + + const emissions = trackEmissions(globalState.state$); + await awaitAsync(); // storage updates are behind a promise + + const newState = { + ...initialState, + date: new Date(initialState.date.getFullYear(), initialState.date.getMonth() + 1), + }; + const actual = await globalState.update((existingState) => newState); + + await awaitAsync(); + + expect(actual).toEqual(newState); + expect(emissions).toHaveLength(2); + expect(emissions).toEqual(expect.arrayContaining([initialState, newState])); + }); }); }); diff --git a/libs/common/src/platform/state/implementations/default-global-state.ts b/libs/common/src/platform/state/implementations/default-global-state.ts index a7f65764266..d6713e14bf8 100644 --- a/libs/common/src/platform/state/implementations/default-global-state.ts +++ b/libs/common/src/platform/state/implementations/default-global-state.ts @@ -1,15 +1,29 @@ -import { BehaviorSubject, Observable, defer, filter, map, shareReplay, tap } from "rxjs"; -import { Jsonify } from "type-fest"; +import { + BehaviorSubject, + Observable, + defer, + filter, + firstValueFrom, + shareReplay, + switchMap, + tap, + timeout, +} from "rxjs"; import { AbstractStorageService } from "../../abstractions/storage.service"; import { GlobalState } from "../global-state"; import { KeyDefinition, globalKeyBuilder } from "../key-definition"; +import { StateUpdateOptions, populateOptionsWithDefault } from "../state-update-options"; + +import { getStoredValue } from "./util"; +const FAKE_DEFAULT = Symbol("fakeDefault"); export class DefaultGlobalState implements GlobalState { private storageKey: string; - private seededPromise: Promise; - protected stateSubject: BehaviorSubject = new BehaviorSubject(null); + protected stateSubject: BehaviorSubject = new BehaviorSubject< + T | typeof FAKE_DEFAULT + >(FAKE_DEFAULT); state$: Observable; @@ -19,15 +33,17 @@ export class DefaultGlobalState implements GlobalState { ) { this.storageKey = globalKeyBuilder(this.keyDefinition); - this.seededPromise = this.chosenLocation.get>(this.storageKey).then((data) => { - const serializedData = this.keyDefinition.deserializer(data); - this.stateSubject.next(serializedData); - }); - const storageUpdates$ = this.chosenLocation.updates$.pipe( filter((update) => update.key === this.storageKey), - map((update) => { - return this.keyDefinition.deserializer(update.value as Jsonify); + switchMap(async (update) => { + if (update.updateType === "remove") { + return null; + } + return await getStoredValue( + this.storageKey, + this.chosenLocation, + this.keyDefinition.deserializer + ); }), shareReplay({ bufferSize: 1, refCount: false }) ); @@ -37,24 +53,53 @@ export class DefaultGlobalState implements GlobalState { this.stateSubject.next(value); }); + this.getFromState().then((s) => { + this.stateSubject.next(s); + }); + return this.stateSubject.pipe( tap({ - complete: () => storageUpdateSubscription.unsubscribe(), + complete: () => { + storageUpdateSubscription.unsubscribe(); + }, }) ); - }); + }).pipe( + shareReplay({ refCount: false, bufferSize: 1 }), + filter((i) => i != FAKE_DEFAULT) + ); } - async update(configureState: (state: T) => T): Promise { - await this.seededPromise; - const currentState = this.stateSubject.getValue(); - const newState = configureState(currentState); + async update( + configureState: (state: T, dependency: TCombine) => T, + options: StateUpdateOptions = {} + ): Promise { + options = populateOptionsWithDefault(options); + const currentState = await this.getGuaranteedState(); + const combinedDependencies = + options.combineLatestWith != null + ? await firstValueFrom(options.combineLatestWith.pipe(timeout(options.msTimeout))) + : null; + + if (!options.shouldUpdate(currentState, combinedDependencies)) { + return; + } + + const newState = configureState(currentState, combinedDependencies); await this.chosenLocation.save(this.storageKey, newState); return newState; } + private async getGuaranteedState() { + const currentValue = this.stateSubject.getValue(); + return currentValue === FAKE_DEFAULT ? await this.getFromState() : currentValue; + } + async getFromState(): Promise { - const data = await this.chosenLocation.get>(this.storageKey); - return this.keyDefinition.deserializer(data); + return await getStoredValue( + this.storageKey, + this.chosenLocation, + this.keyDefinition.deserializer + ); } } diff --git a/libs/common/src/platform/state/implementations/default-user-state.spec.ts b/libs/common/src/platform/state/implementations/default-user-state.spec.ts index e1ab3c1a623..f5cc7e9693f 100644 --- a/libs/common/src/platform/state/implementations/default-user-state.spec.ts +++ b/libs/common/src/platform/state/implementations/default-user-state.spec.ts @@ -1,8 +1,12 @@ +/** + * need to update test environment so trackEmissions works appropriately + * @jest-environment ../shared/test.environment.ts + */ import { any, mock } from "jest-mock-extended"; -import { BehaviorSubject, firstValueFrom, timeout } from "rxjs"; +import { BehaviorSubject, firstValueFrom, of, timeout } from "rxjs"; import { Jsonify } from "type-fest"; -import { trackEmissions } from "../../../../spec"; +import { awaitAsync, trackEmissions } from "../../../../spec"; import { FakeStorageService } from "../../../../spec/fake-storage.service"; import { AccountInfo, AccountService } from "../../../auth/abstractions/account.service"; import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; @@ -29,11 +33,9 @@ class TestState { const testStateDefinition = new StateDefinition("fake", "disk"); -const testKeyDefinition = new KeyDefinition( - testStateDefinition, - "fake", - TestState.fromJSON -); +const testKeyDefinition = new KeyDefinition(testStateDefinition, "fake", { + deserializer: TestState.fromJSON, +}); describe("DefaultUserState", () => { const accountService = mock(); @@ -62,7 +64,7 @@ describe("DefaultUserState", () => { name: `Test User ${id}`, status: AuthenticationStatus.Unlocked, }); - await new Promise((resolve) => setTimeout(resolve, 1)); + await awaitAsync(); }; afterEach(() => { @@ -70,51 +72,42 @@ describe("DefaultUserState", () => { }); it("emits updates for each user switch and update", async () => { - diskStorageService.internalUpdateStore({ - "user_00000000-0000-1000-a000-000000000001_fake_fake": { - date: "2022-09-21T13:14:17.648Z", - array: ["value1", "value2"], - } as Jsonify, - "user_00000000-0000-1000-a000-000000000002_fake_fake": { - date: "2021-09-21T13:14:17.648Z", - array: ["user2_value"], - }, - }); + const user1 = "user_00000000-0000-1000-a000-000000000001_fake_fake"; + const user2 = "user_00000000-0000-1000-a000-000000000002_fake_fake"; + const state1 = { + date: new Date(2021, 0), + array: ["value1"], + }; + const state2 = { + date: new Date(2022, 0), + array: ["value2"], + }; + const initialState: Record = {}; + initialState[user1] = state1; + initialState[user2] = state2; + diskStorageService.internalUpdateStore(initialState); const emissions = trackEmissions(userState.state$); // User signs in changeActiveUser("1"); - await new Promise((resolve) => setTimeout(resolve, 1)); + await awaitAsync(); // Service does an update - await userState.update((state) => { - state.array.push("value3"); - state.date = new Date(2023, 0); - return state; - }); - await new Promise((resolve) => setTimeout(resolve, 1)); + const updatedState = { + date: new Date(2023, 0), + array: ["value3"], + }; + await userState.update(() => updatedState); + await awaitAsync(); // Emulate an account switch await changeActiveUser("2"); - expect(emissions).toHaveLength(3); - // Gotten starter user data - expect(emissions[0]).toBeTruthy(); - expect(emissions[0].array).toHaveLength(2); + expect(emissions).toEqual([state1, updatedState, state2]); - // Gotten emission for the update call - expect(emissions[1]).toBeTruthy(); - expect(emissions[1].array).toHaveLength(3); - expect(new Date(emissions[1].date).getUTCFullYear()).toBe(2023); - - // The second users data - expect(emissions[2]).toBeTruthy(); - expect(emissions[2].array).toHaveLength(1); - expect(new Date(emissions[2].date).getUTCFullYear()).toBe(2021); - - // Should only be called twice to get state, once for each user - expect(diskStorageService.mock.get).toHaveBeenCalledTimes(2); + // Should be called three time to get state, once for each user and once for the update + expect(diskStorageService.mock.get).toHaveBeenCalledTimes(3); expect(diskStorageService.mock.get).toHaveBeenNthCalledWith( 1, "user_00000000-0000-1000-a000-000000000001_fake_fake", @@ -122,6 +115,11 @@ describe("DefaultUserState", () => { ); expect(diskStorageService.mock.get).toHaveBeenNthCalledWith( 2, + "user_00000000-0000-1000-a000-000000000001_fake_fake", + any() + ); + expect(diskStorageService.mock.get).toHaveBeenNthCalledWith( + 3, "user_00000000-0000-1000-a000-000000000002_fake_fake", any() ); @@ -161,9 +159,9 @@ describe("DefaultUserState", () => { diskStorageService.internalUpdateStore({ "user_00000000-0000-1000-a000-000000000001_fake_fake": { - date: "2020-09-21T13:14:17.648Z", + date: new Date(2020, 0), array: ["testValue"], - } as Jsonify, + } as TestState, }); const promise = firstValueFrom(userState.state$.pipe(timeout(20))) @@ -233,4 +231,102 @@ describe("DefaultUserState", () => { // this value is correct. expect(emissions).toHaveLength(2); }); + + describe("update", () => { + const newData = { date: new Date(), array: ["test"] }; + beforeEach(async () => { + changeActiveUser("1"); + }); + + it("should save on update", async () => { + const result = await userState.update((state, dependencies) => { + return newData; + }); + + expect(diskStorageService.mock.save).toHaveBeenCalledTimes(1); + expect(result).toEqual(newData); + }); + + it("should emit once per update", async () => { + const emissions = trackEmissions(userState.state$); + await awaitAsync(); // Need to await for the initial value to be emitted + + await userState.update((state, dependencies) => { + return newData; + }); + await awaitAsync(); + + expect(emissions).toEqual([ + null, // initial value + newData, + ]); + }); + + it("should provide combined dependencies", async () => { + const emissions = trackEmissions(userState.state$); + await awaitAsync(); // Need to await for the initial value to be emitted + + const combinedDependencies = { date: new Date() }; + + await userState.update( + (state, dependencies) => { + expect(dependencies).toEqual(combinedDependencies); + return newData; + }, + { + combineLatestWith: of(combinedDependencies), + } + ); + await awaitAsync(); + + expect(emissions).toEqual([ + null, // initial value + newData, + ]); + }); + + it("should not update if shouldUpdate returns false", async () => { + const emissions = trackEmissions(userState.state$); + await awaitAsync(); // Need to await for the initial value to be emitted + + const result = await userState.update( + (state, dependencies) => { + return newData; + }, + { + shouldUpdate: () => false, + } + ); + + await awaitAsync(); + + expect(diskStorageService.mock.save).not.toHaveBeenCalled(); + expect(result).toBe(undefined); + expect(emissions).toEqual([null]); + }); + + it("should provide the current state to the update callback", async () => { + const emissions = trackEmissions(userState.state$); + await awaitAsync(); // Need to await for the initial value to be emitted + + // Seed with interesting data + const initialData = { date: new Date(2020, 0), array: ["value1", "value2"] }; + await userState.update((state, dependencies) => { + return initialData; + }); + + await userState.update((state, dependencies) => { + expect(state).toEqual(initialData); + return newData; + }); + + await awaitAsync(); + + expect(emissions).toEqual([ + null, // Initial value + initialData, + newData, + ]); + }); + }); }); diff --git a/libs/common/src/platform/state/implementations/default-user-state.ts b/libs/common/src/platform/state/implementations/default-user-state.ts index 10d1329d70e..19a2c420d0a 100644 --- a/libs/common/src/platform/state/implementations/default-user-state.ts +++ b/libs/common/src/platform/state/implementations/default-user-state.ts @@ -9,8 +9,8 @@ import { firstValueFrom, combineLatestWith, filter, + timeout, } from "rxjs"; -import { Jsonify } from "type-fest"; import { AccountService } from "../../../auth/abstractions/account.service"; import { UserId } from "../../../types/guid"; @@ -18,9 +18,11 @@ import { EncryptService } from "../../abstractions/encrypt.service"; import { AbstractStorageService } from "../../abstractions/storage.service"; import { DerivedUserState } from "../derived-user-state"; import { KeyDefinition, userKeyBuilder } from "../key-definition"; +import { StateUpdateOptions, populateOptionsWithDefault } from "../state-update-options"; import { Converter, UserState } from "../user-state"; import { DefaultDerivedUserState } from "./default-derived-state"; +import { getStoredValue } from "./util"; const FAKE_DEFAULT = Symbol("fakeDefault"); @@ -54,9 +56,11 @@ export class DefaultUserState implements UserState { if (key == null) { return FAKE_DEFAULT; } - const jsonData = await this.chosenStorageLocation.get>(key); - const data = keyDefinition.deserializer(jsonData); - return data; + return await getStoredValue( + key, + this.chosenStorageLocation, + this.keyDefinition.deserializer + ); }), // Share the execution shareReplay({ refCount: false, bufferSize: 1 }) @@ -65,8 +69,16 @@ export class DefaultUserState implements UserState { const storageUpdates$ = this.chosenStorageLocation.updates$.pipe( combineLatestWith(this.formattedKey$), filter(([update, key]) => key !== null && update.key === key), - map(([update]) => { - return keyDefinition.deserializer(update.value as Jsonify); + switchMap(async ([update, key]) => { + if (update.updateType === "remove") { + return null; + } + const data = await getStoredValue( + key, + this.chosenStorageLocation, + this.keyDefinition.deserializer + ); + return data; }) ); @@ -94,23 +106,53 @@ export class DefaultUserState implements UserState { .pipe(filter((value) => value != FAKE_DEFAULT)); } - async update(configureState: (state: T) => T): Promise { + async update( + configureState: (state: T, dependency: TCombine) => T, + options: StateUpdateOptions = {} + ): Promise { + options = populateOptionsWithDefault(options); const key = await this.createKey(); const currentState = await this.getGuaranteedState(key); - const newState = configureState(currentState); + const combinedDependencies = + options.combineLatestWith != null + ? await firstValueFrom(options.combineLatestWith.pipe(timeout(options.msTimeout))) + : null; + + if (!options.shouldUpdate(currentState, combinedDependencies)) { + return; + } + + const newState = configureState(currentState, combinedDependencies); await this.saveToStorage(key, newState); return newState; } - async updateFor(userId: UserId, configureState: (state: T) => T): Promise { + async updateFor( + userId: UserId, + configureState: (state: T, dependencies: TCombine) => T, + options: StateUpdateOptions = {} + ): Promise { if (userId == null) { throw new Error("Attempting to update user state, but no userId has been supplied."); } + options = populateOptionsWithDefault(options); const key = userKeyBuilder(userId, this.keyDefinition); - const currentStore = await this.chosenStorageLocation.get>(key); - const currentState = this.keyDefinition.deserializer(currentStore); - const newState = configureState(currentState); + const currentState = await getStoredValue( + key, + this.chosenStorageLocation, + this.keyDefinition.deserializer + ); + const combinedDependencies = + options.combineLatestWith != null + ? await firstValueFrom(options.combineLatestWith.pipe(timeout(options.msTimeout))) + : null; + + if (!options.shouldUpdate(currentState, combinedDependencies)) { + return; + } + + const newState = configureState(currentState, combinedDependencies); await this.saveToStorage(key, newState); return newState; @@ -118,8 +160,7 @@ export class DefaultUserState implements UserState { async getFromState(): Promise { const key = await this.createKey(); - const data = await this.chosenStorageLocation.get>(key); - return this.keyDefinition.deserializer(data); + return await getStoredValue(key, this.chosenStorageLocation, this.keyDefinition.deserializer); } createDerived(converter: Converter): DerivedUserState { @@ -140,10 +181,13 @@ export class DefaultUserState implements UserState { } private async seedInitial(key: string): Promise { - const data = await this.chosenStorageLocation.get>(key); - const serializedData = this.keyDefinition.deserializer(data); - this.stateSubject.next(serializedData); - return serializedData; + const value = await getStoredValue( + key, + this.chosenStorageLocation, + this.keyDefinition.deserializer + ); + this.stateSubject.next(value); + return value; } protected saveToStorage(key: string, data: T): Promise { diff --git a/libs/common/src/platform/state/implementations/util.spec.ts b/libs/common/src/platform/state/implementations/util.spec.ts new file mode 100644 index 00000000000..15737b0c8c9 --- /dev/null +++ b/libs/common/src/platform/state/implementations/util.spec.ts @@ -0,0 +1,50 @@ +import { FakeStorageService } from "../../../../spec/fake-storage.service"; + +import { getStoredValue } from "./util"; + +describe("getStoredValue", () => { + const key = "key"; + const deserializedValue = { value: 1 }; + const value = JSON.stringify(deserializedValue); + const deserializer = (v: string) => JSON.parse(v); + let storageService: FakeStorageService; + + beforeEach(() => { + storageService = new FakeStorageService(); + }); + + describe("when the storage service requires deserialization", () => { + beforeEach(() => { + storageService.internalUpdateValuesRequireDeserialization(true); + }); + + it("should deserialize", async () => { + storageService.save(key, value); + + const result = await getStoredValue(key, storageService, deserializer); + + expect(result).toEqual(deserializedValue); + }); + }); + describe("when the storage service does not require deserialization", () => { + beforeEach(() => { + storageService.internalUpdateValuesRequireDeserialization(false); + }); + + it("should not deserialize", async () => { + storageService.save(key, value); + + const result = await getStoredValue(key, storageService, deserializer); + + expect(result).toEqual(value); + }); + + it("should convert undefined to null", async () => { + storageService.save(key, undefined); + + const result = await getStoredValue(key, storageService, deserializer); + + expect(result).toEqual(null); + }); + }); +}); diff --git a/libs/common/src/platform/state/implementations/util.ts b/libs/common/src/platform/state/implementations/util.ts new file mode 100644 index 00000000000..60401ce523b --- /dev/null +++ b/libs/common/src/platform/state/implementations/util.ts @@ -0,0 +1,18 @@ +import { Jsonify } from "type-fest"; + +import { AbstractStorageService } from "../../abstractions/storage.service"; + +export async function getStoredValue( + key: string, + storage: AbstractStorageService, + deserializer: (jsonValue: Jsonify) => T +) { + if (storage.valuesRequireDeserialization) { + const jsonValue = await storage.get>(key); + const value = deserializer(jsonValue); + return value; + } else { + const value = await storage.get(key); + return value ?? null; + } +} diff --git a/libs/common/src/platform/state/key-definition.spec.ts b/libs/common/src/platform/state/key-definition.spec.ts new file mode 100644 index 00000000000..8ddc6900083 --- /dev/null +++ b/libs/common/src/platform/state/key-definition.spec.ts @@ -0,0 +1,81 @@ +import { Opaque } from "type-fest"; + +import { KeyDefinition } from "./key-definition"; +import { StateDefinition } from "./state-definition"; + +const fakeStateDefinition = new StateDefinition("fake", "disk"); + +type FancyString = Opaque; + +describe("KeyDefinition", () => { + describe("constructor", () => { + it("throws on undefined deserializer", () => { + expect(() => { + new KeyDefinition(fakeStateDefinition, "fake", { + deserializer: undefined, + }); + }); + }); + }); + + describe("record", () => { + it("runs custom deserializer for each record value", () => { + const recordDefinition = KeyDefinition.record(fakeStateDefinition, "fake", { + // Intentionally negate the value for testing + deserializer: (value) => !value, + }); + + expect(recordDefinition).toBeTruthy(); + expect(recordDefinition.deserializer).toBeTruthy(); + + const deserializedValue = recordDefinition.deserializer({ + test1: false, + test2: true, + }); + + expect(Object.keys(deserializedValue)).toHaveLength(2); + + // Values should have swapped from their initial value + expect(deserializedValue["test1"]).toBeTruthy(); + expect(deserializedValue["test2"]).toBeFalsy(); + }); + + it("can handle fancy string type", () => { + // This test is more of a test that I got the typescript typing correctly than actually testing any business logic + const recordDefinition = KeyDefinition.record( + fakeStateDefinition, + "fake", + { + deserializer: (value) => !value, + } + ); + + const fancyRecord = recordDefinition.deserializer( + JSON.parse(`{ "myKey": false, "mySecondKey": true }`) + ); + + expect(fancyRecord).toBeTruthy(); + expect(Object.keys(fancyRecord)).toHaveLength(2); + expect(fancyRecord["myKey" as FancyString]).toBeTruthy(); + expect(fancyRecord["mySecondKey" as FancyString]).toBeFalsy(); + }); + }); + + describe("array", () => { + it("run custom deserializer for each array element", () => { + const arrayDefinition = KeyDefinition.array(fakeStateDefinition, "fake", { + deserializer: (value) => !value, + }); + + expect(arrayDefinition).toBeTruthy(); + expect(arrayDefinition.deserializer).toBeTruthy(); + + const deserializedValue = arrayDefinition.deserializer([false, true]); + + expect(deserializedValue).toBeTruthy(); + expect(deserializedValue).toHaveLength(2); + expect(deserializedValue[0]).toBeTruthy(); + expect(deserializedValue[1]).toBeFalsy(); + }); + }); +}); diff --git a/libs/common/src/platform/state/key-definition.ts b/libs/common/src/platform/state/key-definition.ts index 91dafcb5e95..17eb1d943f1 100644 --- a/libs/common/src/platform/state/key-definition.ts +++ b/libs/common/src/platform/state/key-definition.ts @@ -5,6 +5,22 @@ import { Utils } from "../misc/utils"; import { StateDefinition } from "./state-definition"; +/** + * A set of options for customizing the behavior of a {@link KeyDefinition} + */ +type KeyDefinitionOptions = { + /** + * A function to use to safely convert your type from json to your expected type. + * + * **Important:** Your data may be serialized/deserialized at any time and this + * callback needs to be able to faithfully re-initialize from the JSON object representation of your type. + * + * @param jsonValue The JSON object representation of your state. + * @returns The fully typed version of your state. + */ + readonly deserializer: (jsonValue: Jsonify) => T; +}; + /** * KeyDefinitions describe the precise location to store data for a given piece of state. * The StateDefinition is used to describe the domain of the state, and the KeyDefinition @@ -14,30 +30,61 @@ export class KeyDefinition { /** * Creates a new instance of a KeyDefinition * @param stateDefinition The state definition for which this key belongs to. - * @param key The name of the key, this should be unique per domain - * @param deserializer A function to use to safely convert your type from json to your expected type. + * @param key The name of the key, this should be unique per domain. + * @param options A set of options to customize the behavior of {@link KeyDefinition}. All options are required. + * @param options.deserializer A function to use to safely convert your type from json to your expected type. + * Your data may be serialized/deserialized at any time and this needs callback needs to be able to faithfully re-initialize + * from the JSON object representation of your type. */ constructor( readonly stateDefinition: StateDefinition, readonly key: string, - readonly deserializer: (jsonValue: Jsonify) => T - ) {} + private readonly options: KeyDefinitionOptions + ) { + if (options.deserializer == null) { + throw new Error( + `'deserializer' is a required property on key ${stateDefinition.name} > ${key}` + ); + } + } + + /** + * Gets the deserializer configured for this {@link KeyDefinition} + */ + get deserializer() { + return this.options.deserializer; + } /** * Creates a {@link KeyDefinition} for state that is an array. * @param stateDefinition The state definition to be added to the KeyDefinition * @param key The key to be added to the KeyDefinition - * @param deserializer The deserializer for the element of the array in your state. - * @returns A {@link KeyDefinition} that contains a serializer that will run the provided deserializer for each - * element of an array **unless that array is null in which case it will return an empty list.** + * @param options The options to customize the final {@link KeyDefinition}. + * @returns A {@link KeyDefinition} initialized for arrays, the options run + * the deserializer on the provided options for each element of an array + * **unless that array is null, in which case it will return an empty list.** + * + * @example + * ```typescript + * const MY_KEY = KeyDefinition.array(MY_STATE, "key", { + * deserializer: (myJsonElement) => convertToElement(myJsonElement), + * }); + * ``` */ static array( stateDefinition: StateDefinition, key: string, - deserializer: (jsonValue: Jsonify) => T + // We have them provide options for the element of the array, depending on future options we add, this could get a little weird. + options: KeyDefinitionOptions // The array helper forces an initialValue of an empty array ) { - return new KeyDefinition(stateDefinition, key, (jsonValue) => { - return jsonValue?.map((v) => deserializer(v)) ?? []; + return new KeyDefinition(stateDefinition, key, { + ...options, + deserializer: (jsonValue) => { + if (jsonValue == null) { + return null; + } + return jsonValue.map((v) => options.deserializer(v)); + }, }); } @@ -45,32 +92,42 @@ export class KeyDefinition { * Creates a {@link KeyDefinition} for state that is a record. * @param stateDefinition The state definition to be added to the KeyDefinition * @param key The key to be added to the KeyDefinition - * @param deserializer The deserializer for the value part of a record. + * @param options The options to customize the final {@link KeyDefinition}. * @returns A {@link KeyDefinition} that contains a serializer that will run the provided deserializer for each - * value in a record and returns every key as a string **unless that record is null in which case it will return an record.** + * value in a record and returns every key as a string **unless that record is null, in which case it will return an record.** + * + * @example + * ```typescript + * const MY_KEY = KeyDefinition.record(MY_STATE, "key", { + * deserializer: (myJsonValue) => convertToValue(myJsonValue), + * }); + * ``` */ - static record( + static record( stateDefinition: StateDefinition, key: string, - deserializer: (jsonValue: Jsonify) => T + // We have them provide options for the value of the record, depending on future options we add, this could get a little weird. + options: KeyDefinitionOptions // The array helper forces an initialValue of an empty record ) { - return new KeyDefinition>(stateDefinition, key, (jsonValue) => { - const output: Record = {}; + return new KeyDefinition>(stateDefinition, key, { + ...options, + deserializer: (jsonValue) => { + if (jsonValue == null) { + return null; + } - if (jsonValue == null) { + const output: Record = {}; + for (const key in jsonValue) { + output[key] = options.deserializer((jsonValue as Record>)[key]); + } return output; - } - - for (const key in jsonValue) { - output[key] = deserializer((jsonValue as Record>)[key]); - } - return output; + }, }); } /** - * - * @returns + * Create a string that should be unique across the entire application. + * @returns A string that can be used to cache instances created via this key. */ buildCacheKey(): string { return `${this.stateDefinition.storageLocation}_${this.stateDefinition.name}_${this.key}`; diff --git a/libs/common/src/platform/state/state-update-options.ts b/libs/common/src/platform/state/state-update-options.ts new file mode 100644 index 00000000000..3a4bfed4641 --- /dev/null +++ b/libs/common/src/platform/state/state-update-options.ts @@ -0,0 +1,26 @@ +import { Observable } from "rxjs"; + +export const DEFAULT_OPTIONS = { + shouldUpdate: () => true, + combineLatestWith: null as Observable, + msTimeout: 1000, +}; + +type DefinitelyTypedDefault = Omit< + typeof DEFAULT_OPTIONS, + "shouldUpdate" | "combineLatestWith" +> & { + shouldUpdate: (state: T, dependency: TCombine) => boolean; + combineLatestWith?: Observable; +}; + +export type StateUpdateOptions = Partial>; + +export function populateOptionsWithDefault( + options: StateUpdateOptions +): StateUpdateOptions { + return { + ...(DEFAULT_OPTIONS as StateUpdateOptions), + ...options, + }; +} diff --git a/libs/common/src/platform/state/user-state.ts b/libs/common/src/platform/state/user-state.ts index 82113e37ae3..93404926dd9 100644 --- a/libs/common/src/platform/state/user-state.ts +++ b/libs/common/src/platform/state/user-state.ts @@ -4,6 +4,8 @@ import { UserId } from "../../types/guid"; import { EncryptService } from "../abstractions/encrypt.service"; import { UserKey } from "../models/domain/symmetric-crypto-key"; +import { StateUpdateOptions } from "./state-update-options"; + import { DerivedUserState } from "."; export class DeriveContext { @@ -21,16 +23,33 @@ export interface UserState { /** * Updates backing stores for the active user. * @param configureState function that takes the current state and returns the new state + * @param options Defaults to @see {module:state-update-options#DEFAULT_OPTIONS} + * @param options.shouldUpdate A callback for determining if you want to update state. Defaults to () => true + * @param options.combineLatestWith An observable that you want to combine with the current state for callbacks. Defaults to null + * @param options.msTimeout A timeout for how long you are willing to wait for a `combineLatestWith` option to complete. Defaults to 1000ms. Only applies if `combineLatestWith` is set. + * @returns The new state */ - readonly update: (configureState: (state: T) => T) => Promise; + readonly update: ( + configureState: (state: T, dependencies: TCombine) => T, + options?: StateUpdateOptions + ) => Promise; /** * Updates backing stores for the given userId, which may or may not be active. * @param userId the UserId to target the update for * @param configureState function that takes the current state for the targeted user and returns the new state + * @param options Defaults given by @see {module:state-update-options#DEFAULT_OPTIONS} + * @param options.shouldUpdate A callback for determining if you want to update state. Defaults to () => true + * @param options.combineLatestWith An observable that you want to combine with the current state for callbacks. Defaults to null + * @param options.msTimeout A timeout for how long you are willing to wait for a `combineLatestWith` option to complete. Defaults to 1000ms. Only applies if `combineLatestWith` is set. + * @returns The new state */ - readonly updateFor: (userId: UserId, configureState: (state: T) => T) => Promise; + readonly updateFor: ( + userId: UserId, + configureState: (state: T, dependencies: TCombine) => T, + options?: StateUpdateOptions + ) => Promise; /** * Creates a derives state from the current state. Derived states are always tied to the active user. diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index 483c4f2e8eb..fd1f3143910 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -11,10 +11,11 @@ import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org- import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key"; import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account"; import { MoveStateVersionMigrator } from "./migrations/8-move-state-version"; +import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-settings-to-global"; import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 2; -export const CURRENT_VERSION = 8; +export const CURRENT_VERSION = 9; export type MinVersion = typeof MIN_VERSION; export async function migrate( @@ -38,7 +39,8 @@ export async function migrate( .with(AddKeyTypeToOrgKeysMigrator, 4, 5) .with(RemoveLegacyEtmKeyMigrator, 5, 6) .with(MoveBiometricAutoPromptToAccount, 6, 7) - .with(MoveStateVersionMigrator, 7, CURRENT_VERSION) + .with(MoveStateVersionMigrator, 7, 8) + .with(MoveBrowserSettingsToGlobal, 8, CURRENT_VERSION) .migrate(migrationHelper); } diff --git a/libs/common/src/state-migrations/migration-builder.spec.ts b/libs/common/src/state-migrations/migration-builder.spec.ts index fa53544f133..154d89db1ae 100644 --- a/libs/common/src/state-migrations/migration-builder.spec.ts +++ b/libs/common/src/state-migrations/migration-builder.spec.ts @@ -17,6 +17,20 @@ describe("MigrationBuilder", () => { } } + class TestMigratorWithInstanceMethod extends Migrator<0, 1> { + private async instanceMethod(helper: MigrationHelper, value: string) { + await helper.set("test", value); + } + + async migrate(helper: MigrationHelper): Promise { + await this.instanceMethod(helper, "migrate"); + } + + async rollback(helper: MigrationHelper): Promise { + await this.instanceMethod(helper, "rollback"); + } + } + let sut: MigrationBuilder; beforeEach(() => { @@ -114,4 +128,9 @@ describe("MigrationBuilder", () => { expect(rollback).not.toBeCalled(); }); }); + + it("should be able to call instance methods", async () => { + const helper = new MigrationHelper(0, mock(), mock()); + await sut.with(TestMigratorWithInstanceMethod, 0, 1).migrate(helper); + }); }); diff --git a/libs/common/src/state-migrations/migration-builder.ts b/libs/common/src/state-migrations/migration-builder.ts index 776295a6b8f..2747183629c 100644 --- a/libs/common/src/state-migrations/migration-builder.ts +++ b/libs/common/src/state-migrations/migration-builder.ts @@ -93,7 +93,7 @@ export class MigrationBuilder { ); if (shouldMigrate) { const method = direction === "up" ? migrator.migrate : migrator.rollback; - await method(helper); + await method.bind(migrator)(helper); helper.info( `Migrator ${migrator.constructor.name} (to version ${migrator.toVersion}) migrated - ${direction}` ); diff --git a/libs/common/src/state-migrations/migrations/9-move-browser-settings-to-global.spec.ts b/libs/common/src/state-migrations/migrations/9-move-browser-settings-to-global.spec.ts new file mode 100644 index 00000000000..201c8643a90 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/9-move-browser-settings-to-global.spec.ts @@ -0,0 +1,355 @@ +import { mock } from "jest-mock-extended"; + +import { FakeStorageService } from "../../../spec/fake-storage.service"; +import { MigrationHelper } from "../migration-helper"; +import { Migrator } from "../migrator"; + +import { MoveBrowserSettingsToGlobal } from "./9-move-browser-settings-to-global"; + +type TestState = { authenticatedAccounts: string[] } & { [key: string]: unknown }; + +// This could become a helper available to anyone +const runMigrator = async >( + migrator: TMigrator, + initalData?: Record +): Promise> => { + const fakeStorageService = new FakeStorageService(initalData); + const helper = new MigrationHelper(migrator.fromVersion, fakeStorageService, mock()); + await migrator.migrate(helper); + return fakeStorageService.internalStore; +}; + +describe("MoveBrowserSettingsToGlobal", () => { + const myMigrator = new MoveBrowserSettingsToGlobal(8, 9); + + // This could be the state for a browser client who has never touched the settings or this could + // be a different client who doesn't make it possible to toggle these settings + it("doesn't set any value to global if there is no equivalent settings on the account", async () => { + const testInput: TestState = { + authenticatedAccounts: ["user1"], + global: { + theme: "system", // A real global setting that should persist after migration + }, + user1: { + settings: { + region: "Self-hosted", + }, + }, + }; + + const output = await runMigrator(myMigrator, testInput); + + // No additions to the global state + expect(output["global"]).toEqual({ + theme: "system", + }); + + // No additions to user state + expect(output["user1"]).toEqual({ + settings: { + region: "Self-hosted", + }, + }); + }); + + // This could be a user who opened up the settings page and toggled the checkbox, since this setting infers undefined + // as false this is essentially the default value. + it("sets the setting from the users settings if they have toggled the setting but placed it back to it's inferred", async () => { + const testInput: TestState = { + authenticatedAccounts: ["user1"], + global: { + theme: "system", // A real global setting that should persist after migration + }, + user1: { + settings: { + disableAddLoginNotification: false, + disableChangedPasswordNotification: false, + disableContextMenuItem: false, + neverDomains: { + "example.com": null, + }, + region: "Self-hosted", + }, + }, + }; + + const output = await runMigrator(myMigrator, testInput); + + // User settings should have moved to global + expect(output["global"]).toEqual({ + theme: "system", + disableAddLoginNotification: false, + disableChangedPasswordNotification: false, + disableContextMenuItem: false, + neverDomains: { + "example.com": null, + }, + }); + + // Migrated settings should be deleted + expect(output["user1"]).toEqual({ + settings: { region: "Self-hosted" }, + }); + }); + + // The user has set a value and it's not the default, we should respect that choice globally + it("should take the only users settings", async () => { + const testInput: TestState = { + authenticatedAccounts: ["user1"], + global: { + theme: "system", // A real global setting that should persist after migration + }, + user1: { + settings: { + disableAddLoginNotification: true, + disableChangedPasswordNotification: true, + disableContextMenuItem: true, + neverDomains: { + "example.com": null, + }, + region: "Self-hosted", + }, + }, + }; + + const output = await runMigrator(myMigrator, testInput); + + // The value for the single user value should be set to global + expect(output["global"]).toEqual({ + theme: "system", + disableAddLoginNotification: true, + disableChangedPasswordNotification: true, + disableContextMenuItem: true, + neverDomains: { + "example.com": null, + }, + }); + + expect(output["user1"]).toEqual({ + settings: { region: "Self-hosted" }, + }); + }); + + // No browser client at the time of this writing should ever have multiple authenticatedAccounts + // but in the bizzare case, we should interpret any user having the feature turned on as the value for + // all the accounts. + it("should take the false value if there are conflicting choices", async () => { + const testInput: TestState = { + authenticatedAccounts: ["user1", "user2"], + global: { + theme: "system", // A real global setting that should persist after migration + }, + user1: { + settings: { + disableAddLoginNotification: true, + disableChangedPasswordNotification: true, + disableContextMenuItem: true, + neverDomains: { + "example.com": null, + }, + region: "Self-hosted", + }, + }, + user2: { + settings: { + disableAddLoginNotification: false, + disableChangedPasswordNotification: false, + disableContextMenuItem: false, + neverDomains: { + "example2.com": null, + }, + region: "Self-hosted", + }, + }, + }; + + const output = await runMigrator(myMigrator, testInput); + + // The false settings should be respected over the true values + // neverDomains should be combined into a single object + expect(output["global"]).toEqual({ + theme: "system", + disableAddLoginNotification: false, + disableChangedPasswordNotification: false, + disableContextMenuItem: false, + neverDomains: { + "example.com": null, + "example2.com": null, + }, + }); + + expect(output["user1"]).toEqual({ + settings: { region: "Self-hosted" }, + }); + + expect(output["user2"]).toEqual({ + settings: { region: "Self-hosted" }, + }); + }); + + // Once again, no normal browser should have conflicting values at the time of this comment but: + // if one user has toggled the setting back to on and one user has never touched the setting, + // persist the false value into the global state. + it("should persist the false value if one user has that in their settings", async () => { + const testInput: TestState = { + authenticatedAccounts: ["user1", "user2"], + global: { + theme: "system", // A real global setting that should persist after migration + }, + user1: { + settings: { + region: "Self-hosted", + }, + }, + user2: { + settings: { + disableAddLoginNotification: false, + disableChangedPasswordNotification: false, + disableContextMenuItem: false, + neverDomains: { + "example.com": null, + }, + region: "Self-hosted", + }, + }, + }; + + const output = await runMigrator(myMigrator, testInput); + + // The false settings should be respected over the true values + // neverDomains should be combined into a single object + expect(output["global"]).toEqual({ + theme: "system", + disableAddLoginNotification: false, + disableChangedPasswordNotification: false, + disableContextMenuItem: false, + neverDomains: { + "example.com": null, + }, + }); + + expect(output["user1"]).toEqual({ + settings: { region: "Self-hosted" }, + }); + + expect(output["user2"]).toEqual({ + settings: { region: "Self-hosted" }, + }); + }); + + // Once again, no normal browser should have conflicting values at the time of this comment but: + // if one user has toggled the setting off and one user has never touched the setting, + // persist the false value into the global state. + it("should persist the false value from a user with no settings since undefined is inferred as false", async () => { + const testInput: TestState = { + authenticatedAccounts: ["user1", "user2"], + global: { + theme: "system", // A real global setting that should persist after migration + }, + user1: { + settings: { + region: "Self-hosted", + }, + }, + user2: { + settings: { + disableAddLoginNotification: true, + disableChangedPasswordNotification: true, + disableContextMenuItem: true, + neverDomains: { + "example.com": null, + }, + region: "Self-hosted", + }, + }, + }; + + const output = await runMigrator(myMigrator, testInput); + + // The false settings should be respected over the true values + // neverDomains should be combined into a single object + expect(output["global"]).toEqual({ + theme: "system", + disableAddLoginNotification: false, + disableChangedPasswordNotification: false, + disableContextMenuItem: false, + neverDomains: { + "example.com": null, + }, + }); + + expect(output["user1"]).toEqual({ + settings: { region: "Self-hosted" }, + }); + + expect(output["user2"]).toEqual({ + settings: { region: "Self-hosted" }, + }); + }); + + // This is more realistic, a browser user could have signed into the application and logged out, then signed + // into a different account. Pre browser account switching, the state for the user _is_ kept on disk but the account + // id of the non-current account isn't saved to the authenticatedAccounts array so we don't have a great way to + // get the state and include it in our calculations for what the global state should be. + it("only cares about users defined in authenticatedAccounts", async () => { + const testInput: TestState = { + authenticatedAccounts: ["user1"], + global: { + theme: "system", // A real global setting that should persist after migration + }, + user1: { + settings: { + disableAddLoginNotification: true, + disableChangedPasswordNotification: true, + disableContextMenuItem: true, + neverDomains: { + "example.com": null, + }, + region: "Self-hosted", + }, + }, + user2: { + settings: { + disableAddLoginNotification: false, + disableChangedPasswordNotification: false, + disableContextMenuItem: false, + neverDomains: { + "example2.com": null, + }, + region: "Self-hosted", + }, + }, + }; + + const output = await runMigrator(myMigrator, testInput); + + // The true settings should be respected over the false values because that whole users values + // shouldn't be respected. + // neverDomains should be combined into a single object + expect(output["global"]).toEqual({ + theme: "system", + disableAddLoginNotification: true, + disableChangedPasswordNotification: true, + disableContextMenuItem: true, + neverDomains: { + "example.com": null, + }, + }); + + expect(output["user1"]).toEqual({ + settings: { region: "Self-hosted" }, + }); + + expect(output["user2"]).toEqual({ + settings: { + disableAddLoginNotification: false, + disableChangedPasswordNotification: false, + disableContextMenuItem: false, + neverDomains: { + "example2.com": null, + }, + region: "Self-hosted", + }, + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/9-move-browser-settings-to-global.ts b/libs/common/src/state-migrations/migrations/9-move-browser-settings-to-global.ts new file mode 100644 index 00000000000..5273c600088 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/9-move-browser-settings-to-global.ts @@ -0,0 +1,102 @@ +import { MigrationHelper } from "../migration-helper"; +import { Migrator } from "../migrator"; + +type NeverDomains = { [id: string]: unknown }; + +type ExpectedAccountType = { + settings?: { + neverDomains?: NeverDomains; + disableAddLoginNotification?: boolean; + disableChangedPasswordNotification?: boolean; + disableContextMenuItem?: boolean; + }; +}; + +type TargetGlobalState = { + neverDomains?: NeverDomains; + disableAddLoginNotification?: boolean; + disableChangedPasswordNotification?: boolean; + disableContextMenuItem?: boolean; +}; + +export class MoveBrowserSettingsToGlobal extends Migrator<8, 9> { + // Will first check if any of the accounts have a value from the given accountSelector + // if they do have a value it will set that value into global state but if multiple + // users have differing values it will prefer the false setting, + // if all users have true then it will take true. + tryAddSetting( + accounts: { userId: string; account: ExpectedAccountType }[], + accountSelector: (account: ExpectedAccountType) => boolean | undefined, + globalSetter: (value: boolean | undefined) => void + ): void { + const hasValue = accounts.some(({ account }) => { + return accountSelector(account) !== undefined; + }); + + if (hasValue) { + const value = !accounts.some(({ account }) => { + return (accountSelector(account) ?? false) === false; + }); + + globalSetter(value); + } + } + + async migrate(helper: MigrationHelper): Promise { + const global = await helper.get("global"); + + const accounts = await helper.getAccounts(); + + const globalNeverDomainsValue = accounts.reduce((accumulator, { account }) => { + const normalizedNeverDomains = account.settings?.neverDomains ?? {}; + for (const [id, value] of Object.entries(normalizedNeverDomains)) { + accumulator ??= {}; + accumulator[id] = value; + } + return accumulator; + }, undefined as NeverDomains); + + const targetGlobalState: TargetGlobalState = {}; + + if (globalNeverDomainsValue != null) { + targetGlobalState.neverDomains = globalNeverDomainsValue; + } + + this.tryAddSetting( + accounts, + (a) => a.settings?.disableAddLoginNotification, + (v) => (targetGlobalState.disableAddLoginNotification = v) + ); + + this.tryAddSetting( + accounts, + (a) => a.settings?.disableChangedPasswordNotification, + (v) => (targetGlobalState.disableChangedPasswordNotification = v) + ); + + this.tryAddSetting( + accounts, + (a) => a.settings?.disableContextMenuItem, + (v) => (targetGlobalState.disableContextMenuItem = v) + ); + + await helper.set("global", { + ...global, + ...targetGlobalState, + }); + + await Promise.all( + accounts.map(async ({ userId, account }) => { + delete account.settings?.disableAddLoginNotification; + delete account.settings?.disableChangedPasswordNotification; + delete account.settings?.disableContextMenuItem; + delete account.settings?.neverDomains; + await helper.set(userId, account); + }) + ); + } + + rollback(helper: MigrationHelper): Promise { + throw new Error("Method not implemented."); + } +} diff --git a/libs/components/src/stories/migration.mdx b/libs/components/src/stories/migration.mdx new file mode 100644 index 00000000000..730cfdb412f --- /dev/null +++ b/libs/components/src/stories/migration.mdx @@ -0,0 +1,214 @@ +import { Meta } from "@storybook/addon-docs"; + + + +# Migrating to the Component Library + +You have been tasked with migrating a component to use the CL. What does that entail? + +## Getting Started + +Before progressing here, please ensure that... + +- You have fully setup your dev environment as described in the + [contributing docs](https://contributing.bitwarden.com/). +- You are familiar with [Angular reactive forms](https://angular.io/guide/reactive-forms). +- You are familiar with [Tailwind](https://tailwindcss.com/docs/utility-first). + +## Background + +The design of Bitwarden is in flux. At the time of writing, the frontend codebase uses a mix of +multiple UI frameworks: Bootstrap, custom "box" styles, and this component library, which is built +on top of Tailwind. In short, the "CL migration" is a move to only use the CL and remove everything +else. + +This is very important work. Centralizing around a shared design system will: + +- improve user experience by utilizing consistent patterns +- improve developer experience by reducing custom complex UI code +- improve dev & design velocity by having a central location to make UI/UX changes that impact the + entire project + +## Success Criteria + +Follow these steps to fully migrate a component. + +### Use Storybook + +Don't recreate the wheel. + +After reviewing a design, consult this Storybook to determine if there is a component built for your +usecase. Don't waste effort styling a button or building a popover menu from scratch--we already +have those. If a component isn't flexible enough or doesn't exist for your usecase, contact Will +Martin. + +### Use Tailwind + +Only use Tailwind for styling. No Bootstrap or other custom CSS is allowed. + +This is easy to verify. Bitwarden prefixes all Tailwind classes with `tw-`. If you see a class +without this prefix, it probably shouldn't be there. + +
+ Bad (Bootstrap) + ```html +
+ ``` +
+ +
+ Good (Tailwind) + ```html +
+ ``` +
+ +**Exception:** Icon font classes, prefixed with `bwi`, are allowed. + +
+ Good (Icons) + ```html + + ``` +
+ +### Use Reactive Forms + +The CL has form components that integrate with Angular's reactive forms: `bit-form-field`, +`bitSubmit`, `bit-form-control`, etc. All forms should be migrated from template-drive forms to +reactive forms to make use of these components. Review the +[form component docs](?path=/docs/component-library-form--docs). + +
+ Bad + ```html + + ... + + ``` +
+ +
+ Good + ```html +
+ ... +
+ ``` +
+ +### Dialogs + +Legacy Bootstrap modals use the `ModalService`. These should be converted to use the `DialogService` +and it's [related CL components](?path=/docs/component-library-dialogs--docs). Components that are +fully migrated should have no reference to the `ModalService`. + +1. Update the template to use CL components: + +
+ ```html + + + ``` +
+ +
+ ```html + + ... + ``` +
+ +2. Create a static `open` method on the component, that calls `DialogService.open`: + +
+ ```ts + export class FooDialogComponent { + //... + + static open(dialogService: DialogService) { + return dialogService.open(DeleteAccountComponent); + } + } + ``` + +
+ +3. If you need to pass data into the dialog, pass it to `open` as a parameter and inject + `DIALOG_DATA` into the component's constructor. + +
+ ```ts + export type FooDialogParams = { + bar: string; + } + + export class FooDialogComponent { + constructor(@Inject(DIALOG_DATA) protected params: FooDialogParams) {} + + static open(dialogService: DialogService, data: FooDialogParams) { + return dialogService.open(DeleteAccountComponent, { data }); + } + } + ``` + +
+ +4. Replace calls to `ModalService.open` or `ModalService.openViewRef` with the newly created static + `open` method: + +
`this.modalService.open(FooDialogComponent);`
+ +
`FooDialogComponent.open(this.dialogService);`
+ +## Examples + +The following examples come from accross the Bitwarden codebase. + +### 1.) AboutComponent + +Codeowner: Platform + +https://github.com/bitwarden/clients/pull/6301/files + +This migration updates a `ModalService` component to the `DialogService`. + +**Note:** Most of the internal markup of this component was unchanged, aside from the removal of +defunct Bootstrap classes. + +### 2.) Auth + +Codeowner: Auth + +https://github.com/bitwarden/clients/pull/5377 + +This PR also does some general refactoring, the main relevant change can be seen here: + +[Old template](https://github.com/bitwarden/clients/pull/5377/files#diff-4fcab9ffa4ed26904c53da3bd130e346986576f2372e90b0f66188c809f9284d) +--> +[New template](https://github.com/bitwarden/clients/pull/5377/files#diff-cb93c74c828b9b49dc7869cc0324f5f7d6609da6f72e38ac6baba6d5b6384327) + +Updates a dialog, similar to example 1, but also adds CL form components and Angular Reactive Forms. + +### 3.) AC + +Codeowner: Admin Console + +https://github.com/bitwarden/clients/pull/5417 + +Migrates dialog, form, buttons, and a table. + +### 4.) Vault + +Codeowner: Vault + +https://github.com/bitwarden/clients/pull/5648 + +Some of our components are shared between multiple clients (web, desktop, and the browser extension) +through the use of inheritance. This PR updates the _web_ template of a cross-client component to +use Tailwind and the CL, and updates the base component implementation to use reactive forms, +without updating the desktop or browser templates. + +## Questions + +Please direct any development questions to Will Martin. Thank you! diff --git a/libs/importer/src/importers/lastpass/access/vault.ts b/libs/importer/src/importers/lastpass/access/vault.ts index fc38fab8714..5f614ebb840 100644 --- a/libs/importer/src/importers/lastpass/access/vault.ts +++ b/libs/importer/src/importers/lastpass/access/vault.ts @@ -142,9 +142,11 @@ export class Vault { ); if (response.status === HttpStatusCode.Ok) { const json = await response.json(); - const k1 = json?.extensions?.LastPassK1 as string; - if (k1 != null) { - return Utils.fromB64ToArray(k1); + if (json?.extensions != null && json.extensions.length > 0) { + const k1 = json.extensions[0].LastPassK1 as string; + if (k1 != null) { + return Utils.fromB64ToArray(k1); + } } } return null;