From 5c345c9ee433f82e51d2c4a18cfbb27ab027872f Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 12 Dec 2024 07:07:50 -0500 Subject: [PATCH 01/10] [PM-15094] Update remove sponsorship modal content (#12319) * Update remove sponsorship modal content * PM-15915 --- .../settings/sponsoring-org-row.component.ts | 21 ++++++++++------ apps/web/src/locales/en/messages.json | 25 ++++++++++++++++--- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts b/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts index 59b68ceef83..b40902112c8 100644 --- a/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts +++ b/apps/web/src/app/billing/settings/sponsoring-org-row.component.ts @@ -12,7 +12,6 @@ import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { DialogService, ToastService } from "@bitwarden/components"; @Component({ @@ -35,7 +34,6 @@ export class SponsoringOrgRowComponent implements OnInit { private apiService: ApiService, private i18nService: I18nService, private logService: LogService, - private platformUtilsService: PlatformUtilsService, private dialogService: DialogService, private toastService: ToastService, private configService: ConfigService, @@ -87,14 +85,21 @@ export class SponsoringOrgRowComponent implements OnInit { }); } - get isSentAwaitingSync() { - return this.isSelfHosted && !this.sponsoringOrg.familySponsorshipLastSyncDate; - } - private async doRevokeSponsorship() { + const content = this.sponsoringOrg.familySponsorshipValidUntil + ? this.i18nService.t( + "updatedRevokeSponsorshipConfirmationForAcceptedSponsorship", + this.sponsoringOrg.familySponsorshipFriendlyName, + formatDate(this.sponsoringOrg.familySponsorshipValidUntil, "MM/dd/yyyy", this.locale), + ) + : this.i18nService.t( + "updatedRevokeSponsorshipConfirmationForSentSponsorship", + this.sponsoringOrg.familySponsorshipFriendlyName, + ); + const confirmed = await this.dialogService.openSimpleDialog({ - title: `${this.i18nService.t("remove")} ${this.sponsoringOrg.familySponsorshipFriendlyName}?`, - content: { key: "revokeSponsorshipConfirmation" }, + title: `${this.i18nService.t("removeSponsorship")}?`, + content, acceptButtonText: { key: "remove" }, type: "warning", }); diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index b1203230688..aca22376132 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -6156,9 +6156,6 @@ "emailSent": { "message": "Email sent" }, - "revokeSponsorshipConfirmation": { - "message": "After removing this account, the Families plan sponsorship will expire at the end of the billing period. You will not be able to redeem a new sponsorship offer until the existing one expires. Are you sure you want to continue?" - }, "removeSponsorshipSuccess": { "message": "Sponsorship removed" }, @@ -9959,5 +9956,27 @@ "example": "bitwarden.com" } } + }, + "updatedRevokeSponsorshipConfirmationForSentSponsorship": { + "message": "If you remove $EMAIL$, the sponsorship for this Family plan cannot be redeemed. Are you sure you want to continue?", + "placeholders": { + "email": { + "content": "$1", + "example": "sponsored@organization.com" + } + } + }, + "updatedRevokeSponsorshipConfirmationForAcceptedSponsorship": { + "message": "If you remove $EMAIL$, the sponsorship for this Family plan will end and the saved payment method will be charged $40 + applicable tax on $DATE$. You will not be able to redeem a new sponsorship until $DATE$. Are you sure you want to continue?", + "placeholders": { + "email": { + "content": "$1", + "example": "sponsored@organization.com" + }, + "date": { + "content": "$2", + "example": "12/10/2024" + } + } } } From 645d36f465fd585cadd95c82595cea6a5d1027cd Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Thu, 12 Dec 2024 13:42:44 +0100 Subject: [PATCH 02/10] fix: target workflows not triggering on pull_request_target (#12370) --- .github/workflows/build-browser-target.yml | 2 +- .github/workflows/build-cli-target.yml | 2 +- .github/workflows/build-desktop-target.yml | 2 +- .github/workflows/build-web-target.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-browser-target.yml b/.github/workflows/build-browser-target.yml index 11a268466f1..12a08cf50a3 100644 --- a/.github/workflows/build-browser-target.yml +++ b/.github/workflows/build-browser-target.yml @@ -1,7 +1,7 @@ name: Build Browser on PR Target on: - pull_request: + pull_request_target: types: [opened, synchronize] branches-ignore: - 'l10n_master' diff --git a/.github/workflows/build-cli-target.yml b/.github/workflows/build-cli-target.yml index 658d8f922ba..89f8b63b525 100644 --- a/.github/workflows/build-cli-target.yml +++ b/.github/workflows/build-cli-target.yml @@ -1,7 +1,7 @@ name: Build CLI on PR Target on: - pull_request: + pull_request_target: types: [opened, synchronize] branches-ignore: - 'l10n_master' diff --git a/.github/workflows/build-desktop-target.yml b/.github/workflows/build-desktop-target.yml index 47f85d69163..b9ea9cacb8d 100644 --- a/.github/workflows/build-desktop-target.yml +++ b/.github/workflows/build-desktop-target.yml @@ -1,7 +1,7 @@ name: Build Desktop on PR Target on: - pull_request: + pull_request_target: types: [opened, synchronize] branches-ignore: - 'l10n_master' diff --git a/.github/workflows/build-web-target.yml b/.github/workflows/build-web-target.yml index a27af0b0870..9a9cd735435 100644 --- a/.github/workflows/build-web-target.yml +++ b/.github/workflows/build-web-target.yml @@ -1,7 +1,7 @@ name: Build Web on PR Target on: - pull_request: + pull_request_target: types: [opened, synchronize] branches-ignore: - 'l10n_master' From 617469127a2e18f61d7b4f8b7b96eae5dbf354d8 Mon Sep 17 00:00:00 2001 From: Icelk Date: Thu, 12 Dec 2024 13:45:37 +0100 Subject: [PATCH 03/10] ssh agent: fix first start when no .bitwarden-ssh-agent.sock exists (#12356) Co-authored-by: Bernd Schoolmann --- apps/desktop/desktop_native/core/src/ssh_agent/unix.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/desktop/desktop_native/core/src/ssh_agent/unix.rs b/apps/desktop/desktop_native/core/src/ssh_agent/unix.rs index a74c1205b57..ae03421a425 100644 --- a/apps/desktop/desktop_native/core/src/ssh_agent/unix.rs +++ b/apps/desktop/desktop_native/core/src/ssh_agent/unix.rs @@ -65,7 +65,9 @@ impl BitwardenDesktopAgent { "[SSH Agent Native Module] Could not remove existing socket file: {}", e ); - return; + if e.kind() != std::io::ErrorKind::NotFound { + return; + } } match UnixListener::bind(sockname) { From 1b6b5d3110127829e2215b0f75356fe90465891d Mon Sep 17 00:00:00 2001 From: Github Actions Date: Thu, 12 Dec 2024 13:54:02 +0000 Subject: [PATCH 04/10] Bumped Desktop client to 2024.12.1 --- apps/desktop/package.json | 2 +- apps/desktop/src/package-lock.json | 4 ++-- apps/desktop/src/package.json | 2 +- package-lock.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index f546563ed18..101e968ad6d 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/desktop", "description": "A secure and free password manager for all of your devices.", - "version": "2024.12.0", + "version": "2024.12.1", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 0300b0b93cc..201f563db2d 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2024.12.0", + "version": "2024.12.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2024.12.0", + "version": "2024.12.1", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-napi": "file:../desktop_native/napi" diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index 9a3c56cf17c..29ee5dc47ef 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -2,7 +2,7 @@ "name": "@bitwarden/desktop", "productName": "Bitwarden", "description": "A secure and free password manager for all of your devices.", - "version": "2024.12.0", + "version": "2024.12.1", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/package-lock.json b/package-lock.json index 2da7d9e6255..ff7dac2c461 100644 --- a/package-lock.json +++ b/package-lock.json @@ -230,7 +230,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2024.12.0", + "version": "2024.12.1", "hasInstallScript": true, "license": "GPL-3.0" }, From 30c151f44a8af28b033f584a14822063cefeb12a Mon Sep 17 00:00:00 2001 From: Tom <144813356+ttalty@users.noreply.github.com> Date: Thu, 12 Dec 2024 09:55:43 -0500 Subject: [PATCH 05/10] [PM-13455] Risk insights aggregation in a new service. (#12071) * Risk insights aggregation in a new service. Initial PR. * Ignoring all non-login items and refactoring into a method * Cleaning up the documentation a little * logic for generating the report summary * application summary to list at risk applications not passwords * Adding more documentation and moving types to it's own file * Awaiting the raw data report and adding the start of the test file * Adding more test cases * Removing unnecessary file * Test cases update * Fixing memeber details test to have new member * Fixing password health tests * Moving to observables * removing commented code * commented code * Switching from ternary to if/else * nullable types * one more nullable type * Adding the fixme for strict types * moving the fixme --------- Co-authored-by: Daniel James Smith --- .../risk-insights/models/password-health.ts | 92 ++++ .../risk-insights/services/ciphers.mock.ts | 78 ++-- .../reports/risk-insights/services/index.ts | 1 + .../member-cipher-details-api.service.spec.ts | 8 +- .../services/password-health.service.spec.ts | 86 +--- .../risk-insights-report.service.spec.ts | 148 +++++++ .../services/risk-insights-report.service.ts | 395 ++++++++++++++++++ 7 files changed, 696 insertions(+), 112 deletions(-) create mode 100644 bitwarden_license/bit-common/src/tools/reports/risk-insights/models/password-health.ts create mode 100644 bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.spec.ts create mode 100644 bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.ts diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/models/password-health.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/models/password-health.ts new file mode 100644 index 00000000000..427cb06d9e0 --- /dev/null +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/models/password-health.ts @@ -0,0 +1,92 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore + +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { BadgeVariant } from "@bitwarden/components"; + +/** + * All applications report summary. The total members, + * total at risk members, application, and at risk application + * counts. Aggregated from all calculated applications + */ +export type ApplicationHealthReportSummary = { + totalMemberCount: number; + totalAtRiskMemberCount: number; + totalApplicationCount: number; + totalAtRiskApplicationCount: number; +}; + +/** + * All applications report detail. Application is the cipher + * uri. Has the at risk, password, and member information + */ +export type ApplicationHealthReportDetail = { + applicationName: string; + passwordCount: number; + atRiskPasswordCount: number; + memberCount: number; + + memberDetails: MemberDetailsFlat[]; + atRiskMemberDetails: MemberDetailsFlat[]; +}; + +/** + * Breaks the cipher health info out by uri and passes + * along the password health and member info + */ +export type CipherHealthReportUriDetail = { + cipherId: string; + reusedPasswordCount: number; + weakPasswordDetail: WeakPasswordDetail; + exposedPasswordDetail: ExposedPasswordDetail; + cipherMembers: MemberDetailsFlat[]; + trimmedUri: string; +}; + +/** + * Associates a cipher with it's essential information. + * Gets the password health details, cipher members, and + * the trimmed uris for the cipher + */ +export type CipherHealthReportDetail = CipherView & { + reusedPasswordCount: number; + weakPasswordDetail: WeakPasswordDetail; + exposedPasswordDetail: ExposedPasswordDetail; + cipherMembers: MemberDetailsFlat[]; + trimmedUris: string[]; +}; + +/** + * Weak password details containing the score + * and the score type for the label and badge + */ +export type WeakPasswordDetail = { + score: number; + detailValue: WeakPasswordScore; +} | null; + +/** + * Weak password details containing the badge and + * the label for the password score + */ +export type WeakPasswordScore = { + label: string; + badgeVariant: BadgeVariant; +} | null; + +/** + * How many times a password has been exposed + */ +export type ExposedPasswordDetail = { + exposedXTimes: number; +} | null; + +/** + * Flattened member details that associates an + * organization member to a cipher + */ +export type MemberDetailsFlat = { + userName: string; + email: string; + cipherId: string; +}; diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/ciphers.mock.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/ciphers.mock.ts index e7693e46a32..ca5cdc35b8a 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/ciphers.mock.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/ciphers.mock.ts @@ -1,10 +1,18 @@ +import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; + +const createLoginUriView = (uri: string): LoginUriView => { + const view = new LoginUriView(); + view.uri = uri; + return view; +}; + export const mockCiphers: any[] = [ { initializerKey: 1, id: "cbea34a8-bde4-46ad-9d19-b05001228ab1", organizationId: null, folderId: null, - name: "Cannot Be Edited", + name: "Weak Password Cipher", notes: null, isDeleted: false, type: 1, @@ -14,10 +22,11 @@ export const mockCiphers: any[] = [ password: "123", hasUris: true, uris: [ - { uri: "www.google.com" }, - { uri: "accounts.google.com" }, - { uri: "https://www.google.com" }, - { uri: "https://www.google.com/login" }, + createLoginUriView("101domain.com"), + createLoginUriView("www.google.com"), + createLoginUriView("accounts.google.com"), + createLoginUriView("https://www.google.com"), + createLoginUriView("https://www.google.com/login"), ], }, edit: false, @@ -31,23 +40,18 @@ export const mockCiphers: any[] = [ }, { initializerKey: 1, - id: "cbea34a8-bde4-46ad-9d19-b05001228ab2", + id: "cbea34a8-bde4-46ad-9d19-b05001228cd3", organizationId: null, folderId: null, - name: "Can Be Edited id ending 2", + name: "Strong Password Cipher", notes: null, - isDeleted: false, type: 1, favorite: false, organizationUseTotp: false, login: { - password: "123", + password: "Password!123", hasUris: true, - uris: [ - { - uri: "http://nothing.com", - }, - ], + uris: [createLoginUriView("http://example.com")], }, edit: true, viewPassword: true, @@ -60,22 +64,18 @@ export const mockCiphers: any[] = [ }, { initializerKey: 1, - id: "cbea34a8-bde4-46ad-9d19-b05001228cd3", + id: "cbea34a8-bde4-46ad-9d19-b05001228ab2", organizationId: null, folderId: null, - name: "Can Be Edited id ending 3", + name: "Strong password Cipher", notes: null, type: 1, favorite: false, organizationUseTotp: false, login: { - password: "123", hasUris: true, - uris: [ - { - uri: "http://example.com", - }, - ], + password: "Password!1234", + uris: [createLoginUriView("101domain.com")], }, edit: true, viewPassword: true, @@ -91,14 +91,15 @@ export const mockCiphers: any[] = [ id: "cbea34a8-bde4-46ad-9d19-b05001228xy4", organizationId: null, folderId: null, - name: "Can Be Edited id ending 4", + name: "Strong password Cipher", notes: null, type: 1, favorite: false, organizationUseTotp: false, login: { hasUris: true, - uris: [{ uri: "101domain.com" }], + password: "Password!123", + uris: [createLoginUriView("example.com")], }, edit: true, viewPassword: true, @@ -114,14 +115,39 @@ export const mockCiphers: any[] = [ id: "cbea34a8-bde4-46ad-9d19-b05001227nm5", organizationId: null, folderId: null, - name: "Can Be Edited id ending 5", + name: "Exposed password Cipher", notes: null, type: 1, favorite: false, organizationUseTotp: false, login: { hasUris: true, - uris: [{ uri: "123formbuilder.com" }], + password: "123", + uris: [createLoginUriView("123formbuilder.com"), createLoginUriView("www.google.com")], + }, + edit: true, + viewPassword: true, + collectionIds: [], + revisionDate: "2023-08-03T17:40:59.793Z", + creationDate: "2023-08-03T17:40:59.793Z", + deletedDate: null, + reprompt: 0, + localData: null, + }, + { + initializerKey: 1, + id: "cbea34a8-bde4-46ad-9d19-b05001227tt1", + organizationId: null, + folderId: null, + name: "Secure Co Login", + notes: null, + type: 1, + favorite: false, + organizationUseTotp: false, + login: { + hasUris: true, + password: "4gRyhhOX2Og2p0", + uris: [createLoginUriView("SecureCo.com")], }, edit: true, viewPassword: true, diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/index.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/index.ts index c7bace84e5b..e930c7666e8 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/index.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/index.ts @@ -1,2 +1,3 @@ export * from "./member-cipher-details-api.service"; export * from "./password-health.service"; +export * from "./risk-insights-report.service"; diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/member-cipher-details-api.service.spec.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/member-cipher-details-api.service.spec.ts index 872a4cdff55..d6474c2c9c4 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/member-cipher-details-api.service.spec.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/member-cipher-details-api.service.spec.ts @@ -69,6 +69,12 @@ export const mockMemberCipherDetails: any = [ "cbea34a8-bde4-46ad-9d19-b05001228xy4", ], }, + { + userName: "Mister Secure", + email: "mister.secure@secureco.com", + usesKeyConnector: true, + cipherIds: ["cbea34a8-bde4-46ad-9d19-b05001227tt1"], + }, ]; describe("Member Cipher Details API Service", () => { @@ -91,7 +97,7 @@ describe("Member Cipher Details API Service", () => { const orgId = "1234"; const result = await memberCipherDetailsApiService.getMemberCipherDetails(orgId); expect(result).not.toBeNull(); - expect(result).toHaveLength(6); + expect(result).toHaveLength(7); expect(apiService.send).toHaveBeenCalledWith( "GET", "/reports/member-cipher-details/" + orgId, diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/password-health.service.spec.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/password-health.service.spec.ts index c0f77abeb79..b81acb09bed 100644 --- a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/password-health.service.spec.ts +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/password-health.service.spec.ts @@ -3,18 +3,15 @@ import { TestBed } from "@angular/core/testing"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { mockCiphers } from "./ciphers.mock"; import { MemberCipherDetailsApiService } from "./member-cipher-details-api.service"; import { mockMemberCipherDetails } from "./member-cipher-details-api.service.spec"; import { PasswordHealthService } from "./password-health.service"; +// FIXME: Remove password-health report service after PR-15498 completion describe("PasswordHealthService", () => { let service: PasswordHealthService; - let cipherService: CipherService; - let memberCipherDetailsApiService: MemberCipherDetailsApiService; - beforeEach(() => { TestBed.configureTestingModule({ providers: [ @@ -51,8 +48,6 @@ describe("PasswordHealthService", () => { }); service = TestBed.inject(PasswordHealthService); - cipherService = TestBed.inject(CipherService); - memberCipherDetailsApiService = TestBed.inject(MemberCipherDetailsApiService); }); it("should be created", () => { @@ -67,83 +62,4 @@ describe("PasswordHealthService", () => { expect(service.exposedPasswordMap.size).toBe(0); expect(service.totalMembersMap.size).toBe(0); }); - - describe("generateReport", () => { - beforeEach(async () => { - await service.generateReport(); - }); - - it("should fetch all ciphers for the organization", () => { - expect(cipherService.getAllFromApiForOrganization).toHaveBeenCalledWith("org1"); - }); - - it("should fetch member cipher details", () => { - expect(memberCipherDetailsApiService.getMemberCipherDetails).toHaveBeenCalledWith("org1"); - }); - - it("should populate reportCiphers with ciphers that have issues", () => { - expect(service.reportCiphers.length).toBeGreaterThan(0); - }); - - it("should detect weak passwords", () => { - expect(service.passwordStrengthMap.size).toBeGreaterThan(0); - expect(service.passwordStrengthMap.get("cbea34a8-bde4-46ad-9d19-b05001228ab1")).toEqual([ - "veryWeak", - "danger", - ]); - expect(service.passwordStrengthMap.get("cbea34a8-bde4-46ad-9d19-b05001228ab2")).toEqual([ - "veryWeak", - "danger", - ]); - expect(service.passwordStrengthMap.get("cbea34a8-bde4-46ad-9d19-b05001228cd3")).toEqual([ - "veryWeak", - "danger", - ]); - }); - - it("should detect reused passwords", () => { - expect(service.passwordUseMap.get("123")).toBe(3); - }); - - it("should detect exposed passwords", () => { - expect(service.exposedPasswordMap.size).toBeGreaterThan(0); - expect(service.exposedPasswordMap.get("cbea34a8-bde4-46ad-9d19-b05001228ab1")).toBe(100); - }); - - it("should calculate total members per cipher", () => { - expect(service.totalMembersMap.size).toBeGreaterThan(0); - expect(service.totalMembersMap.get("cbea34a8-bde4-46ad-9d19-b05001228ab1")).toBe(2); - expect(service.totalMembersMap.get("cbea34a8-bde4-46ad-9d19-b05001228ab2")).toBe(4); - expect(service.totalMembersMap.get("cbea34a8-bde4-46ad-9d19-b05001228cd3")).toBe(5); - expect(service.totalMembersMap.get("cbea34a8-bde4-46ad-9d19-b05001227nm5")).toBe(4); - expect(service.totalMembersMap.get("cbea34a8-bde4-46ad-9d19-b05001227nm7")).toBe(1); - expect(service.totalMembersMap.get("cbea34a8-bde4-46ad-9d19-b05001228xy4")).toBe(6); - }); - }); - - describe("findWeakPassword", () => { - it("should add weak passwords to passwordStrengthMap", () => { - const weakCipher = mockCiphers.find((c) => c.login?.password === "123") as CipherView; - service.findWeakPassword(weakCipher); - expect(service.passwordStrengthMap.get(weakCipher.id)).toEqual(["veryWeak", "danger"]); - }); - }); - - describe("findReusedPassword", () => { - it("should detect password reuse", () => { - mockCiphers.forEach((cipher) => { - service.findReusedPassword(cipher as CipherView); - }); - const reuseCounts = Array.from(service.passwordUseMap.values()).filter((count) => count > 1); - expect(reuseCounts.length).toBeGreaterThan(0); - }); - }); - - describe("findExposedPassword", () => { - it("should add exposed passwords to exposedPasswordMap", async () => { - const exposedCipher = mockCiphers.find((c) => c.login?.password === "123") as CipherView; - await service.findExposedPassword(exposedCipher); - expect(service.exposedPasswordMap.get(exposedCipher.id)).toBe(100); - }); - }); }); diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.spec.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.spec.ts new file mode 100644 index 00000000000..7505b692a8f --- /dev/null +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.spec.ts @@ -0,0 +1,148 @@ +import { TestBed } from "@angular/core/testing"; +import { firstValueFrom } from "rxjs"; + +import { AuditService } from "@bitwarden/common/abstractions/audit.service"; +import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; + +import { mockCiphers } from "./ciphers.mock"; +import { MemberCipherDetailsApiService } from "./member-cipher-details-api.service"; +import { mockMemberCipherDetails } from "./member-cipher-details-api.service.spec"; +import { RiskInsightsReportService } from "./risk-insights-report.service"; + +describe("RiskInsightsReportService", () => { + let service: RiskInsightsReportService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + RiskInsightsReportService, + { + provide: PasswordStrengthServiceAbstraction, + useValue: { + getPasswordStrength: (password: string) => { + const score = password.length < 4 ? 1 : 4; + return { score }; + }, + }, + }, + { + provide: AuditService, + useValue: { + passwordLeaked: (password: string) => Promise.resolve(password === "123" ? 100 : 0), + }, + }, + { + provide: CipherService, + useValue: { + getAllFromApiForOrganization: jest.fn().mockResolvedValue(mockCiphers), + }, + }, + { + provide: MemberCipherDetailsApiService, + useValue: { + getMemberCipherDetails: jest.fn().mockResolvedValue(mockMemberCipherDetails), + }, + }, + ], + }); + + service = TestBed.inject(RiskInsightsReportService); + }); + + it("should generate the raw data report correctly", async () => { + const result = await firstValueFrom(service.generateRawDataReport$("orgId")); + + expect(result).toHaveLength(6); + + let testCaseResults = result.filter((x) => x.id === "cbea34a8-bde4-46ad-9d19-b05001228ab1"); + expect(testCaseResults).toHaveLength(1); + let testCase = testCaseResults[0]; + expect(testCase).toBeTruthy(); + expect(testCase.cipherMembers).toHaveLength(2); + expect(testCase.trimmedUris).toHaveLength(3); + expect(testCase.weakPasswordDetail).toBeTruthy(); + expect(testCase.exposedPasswordDetail).toBeTruthy(); + expect(testCase.reusedPasswordCount).toEqual(2); + + testCaseResults = result.filter((x) => x.id === "cbea34a8-bde4-46ad-9d19-b05001227tt1"); + expect(testCaseResults).toHaveLength(1); + testCase = testCaseResults[0]; + expect(testCase).toBeTruthy(); + expect(testCase.cipherMembers).toHaveLength(1); + expect(testCase.trimmedUris).toHaveLength(1); + expect(testCase.weakPasswordDetail).toBeFalsy(); + expect(testCase.exposedPasswordDetail).toBeFalsy(); + expect(testCase.reusedPasswordCount).toEqual(1); + }); + + it("should generate the raw data + uri report correctly", async () => { + const result = await firstValueFrom(service.generateRawDataUriReport$("orgId")); + + expect(result).toHaveLength(9); + + // Two ciphers that have google.com as their uri. There should be 2 results + const googleResults = result.filter((x) => x.trimmedUri === "google.com"); + expect(googleResults).toHaveLength(2); + + // Verify the details for one of the googles matches the password health info + // expected + const firstGoogle = googleResults.filter( + (x) => x.cipherId === "cbea34a8-bde4-46ad-9d19-b05001228ab1" && x.trimmedUri === "google.com", + )[0]; + expect(firstGoogle.weakPasswordDetail).toBeTruthy(); + expect(firstGoogle.exposedPasswordDetail).toBeTruthy(); + expect(firstGoogle.reusedPasswordCount).toEqual(2); + }); + + it("should generate applications health report data correctly", async () => { + const result = await firstValueFrom(service.generateApplicationsReport$("orgId")); + + expect(result).toHaveLength(6); + + // Two ciphers have google.com associated with them. The first cipher + // has 2 members and the second has 4. However, the 2 members in the first + // cipher are also associated with the second. The total amount of members + // should be 4 not 6 + const googleTestResults = result.filter((x) => x.applicationName === "google.com"); + expect(googleTestResults).toHaveLength(1); + const googleTest = googleTestResults[0]; + expect(googleTest.memberCount).toEqual(4); + + // Both ciphers have at risk passwords + expect(googleTest.passwordCount).toEqual(2); + + // All members are at risk since both ciphers are at risk + expect(googleTest.atRiskMemberDetails).toHaveLength(4); + expect(googleTest.atRiskPasswordCount).toEqual(2); + + // There are 2 ciphers associated with 101domain.com + const domain101TestResults = result.filter((x) => x.applicationName === "101domain.com"); + expect(domain101TestResults).toHaveLength(1); + const domain101Test = domain101TestResults[0]; + expect(domain101Test.passwordCount).toEqual(2); + + // The first cipher is at risk. The second cipher is not at risk + expect(domain101Test.atRiskPasswordCount).toEqual(1); + + // The first cipher has 2 members. The second cipher the second + // cipher has 4. One of the members in the first cipher is associated + // with the second. So there should be 5 members total. + expect(domain101Test.memberCount).toEqual(5); + + // The first cipher is at risk. The total at risk members is 2 and + // at risk password count is 1. + expect(domain101Test.atRiskMemberDetails).toHaveLength(2); + expect(domain101Test.atRiskPasswordCount).toEqual(1); + }); + + it("should generate applications summary data correctly", async () => { + const reportResult = await firstValueFrom(service.generateApplicationsReport$("orgId")); + const reportSummary = service.generateApplicationsSummary(reportResult); + + expect(reportSummary.totalMemberCount).toEqual(7); + expect(reportSummary.totalAtRiskMemberCount).toEqual(6); + expect(reportSummary.totalApplicationCount).toEqual(6); + expect(reportSummary.totalAtRiskApplicationCount).toEqual(5); + }); +}); diff --git a/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.ts b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.ts new file mode 100644 index 00000000000..f4b30735584 --- /dev/null +++ b/bitwarden_license/bit-common/src/tools/reports/risk-insights/services/risk-insights-report.service.ts @@ -0,0 +1,395 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore + +import { Injectable } from "@angular/core"; +import { concatMap, first, from, map, Observable, zip } from "rxjs"; + +import { AuditService } from "@bitwarden/common/abstractions/audit.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; + +import { + ApplicationHealthReportDetail, + ApplicationHealthReportSummary, + CipherHealthReportDetail, + CipherHealthReportUriDetail, + ExposedPasswordDetail, + MemberDetailsFlat, + WeakPasswordDetail, + WeakPasswordScore, +} from "../models/password-health"; + +import { MemberCipherDetailsApiService } from "./member-cipher-details-api.service"; + +@Injectable() +export class RiskInsightsReportService { + constructor( + private passwordStrengthService: PasswordStrengthServiceAbstraction, + private auditService: AuditService, + private cipherService: CipherService, + private memberCipherDetailsApiService: MemberCipherDetailsApiService, + ) {} + + /** + * Report data from raw cipher health data. + * Can be used in the Raw Data diagnostic tab (just exclude the members in the view) + * and can be used in the raw data + members tab when including the members in the view + * @param organizationId + * @returns Cipher health report data with members and trimmed uris + */ + generateRawDataReport$(organizationId: string): Observable { + const allCiphers$ = from(this.cipherService.getAllFromApiForOrganization(organizationId)); + const memberCiphers$ = from( + this.memberCipherDetailsApiService.getMemberCipherDetails(organizationId), + ); + + const results$ = zip(allCiphers$, memberCiphers$).pipe( + map(([allCiphers, memberCiphers]) => { + const details: MemberDetailsFlat[] = memberCiphers.flatMap((dtl) => + dtl.cipherIds.map((c) => this.getMemberDetailsFlat(dtl.userName, dtl.email, c)), + ); + return [allCiphers, details] as const; + }), + concatMap(([ciphers, flattenedDetails]) => this.getCipherDetails(ciphers, flattenedDetails)), + first(), + ); + + return results$; + } + + /** + * Report data for raw cipher health broken out into the uris + * Can be used in the raw data + members + uri diagnostic report + * @param organizationId Id of the organization + * @returns Cipher health report data flattened to the uris + */ + generateRawDataUriReport$(organizationId: string): Observable { + const cipherHealthDetails$ = this.generateRawDataReport$(organizationId); + const results$ = cipherHealthDetails$.pipe( + map((healthDetails) => this.getCipherUriDetails(healthDetails)), + first(), + ); + + return results$; + } + + /** + * Report data for the aggregation of uris to like uris and getting password/member counts, + * members, and at risk statuses. + * @param organizationId Id of the organization + * @returns The all applications health report data + */ + generateApplicationsReport$(organizationId: string): Observable { + const cipherHealthUriReport$ = this.generateRawDataUriReport$(organizationId); + const results$ = cipherHealthUriReport$.pipe( + map((uriDetails) => this.getApplicationHealthReport(uriDetails)), + first(), + ); + + return results$; + } + + /** + * Gets the summary from the application health report. Returns total members and applications as well + * as the total at risk members and at risk applications + * @param reports The previously calculated application health report data + * @returns A summary object containing report totals + */ + generateApplicationsSummary( + reports: ApplicationHealthReportDetail[], + ): ApplicationHealthReportSummary { + const totalMembers = reports.flatMap((x) => x.memberDetails); + const uniqueMembers = this.getUniqueMembers(totalMembers); + + const atRiskMembers = reports.flatMap((x) => x.atRiskMemberDetails); + const uniqueAtRiskMembers = this.getUniqueMembers(atRiskMembers); + + return { + totalMemberCount: uniqueMembers.length, + totalAtRiskMemberCount: uniqueAtRiskMembers.length, + totalApplicationCount: reports.length, + totalAtRiskApplicationCount: reports.filter((app) => app.atRiskPasswordCount > 0).length, + }; + } + + /** + * Associates the members with the ciphers they have access to. Calculates the password health. + * Finds the trimmed uris. + * @param ciphers Org ciphers + * @param memberDetails Org members + * @returns Cipher password health data with trimmed uris and associated members + */ + private async getCipherDetails( + ciphers: CipherView[], + memberDetails: MemberDetailsFlat[], + ): Promise { + const cipherHealthReports: CipherHealthReportDetail[] = []; + const passwordUseMap = new Map(); + for (const cipher of ciphers) { + if (this.validateCipher(cipher)) { + const weakPassword = this.findWeakPassword(cipher); + // Looping over all ciphers needs to happen first to determine reused passwords over all ciphers. + // Store in the set and evaluate later + if (passwordUseMap.has(cipher.login.password)) { + passwordUseMap.set( + cipher.login.password, + (passwordUseMap.get(cipher.login.password) || 0) + 1, + ); + } else { + passwordUseMap.set(cipher.login.password, 1); + } + + const exposedPassword = await this.findExposedPassword(cipher); + + // Get the cipher members + const cipherMembers = memberDetails.filter((x) => x.cipherId === cipher.id); + + // Trim uris to host name and create the cipher health report + const cipherTrimmedUris = this.getTrimmedCipherUris(cipher); + const cipherHealth = { + ...cipher, + weakPasswordDetail: weakPassword, + exposedPasswordDetail: exposedPassword, + cipherMembers: cipherMembers, + trimmedUris: cipherTrimmedUris, + } as CipherHealthReportDetail; + + cipherHealthReports.push(cipherHealth); + } + } + + // loop for reused passwords + cipherHealthReports.forEach((detail) => { + detail.reusedPasswordCount = passwordUseMap.get(detail.login.password) ?? 0; + }); + return cipherHealthReports; + } + + /** + * Flattens the cipher to trimmed uris. Used for the raw data + uri + * @param cipherHealthReport Cipher health report with uris and members + * @returns Flattened cipher health details to uri + */ + private getCipherUriDetails( + cipherHealthReport: CipherHealthReportDetail[], + ): CipherHealthReportUriDetail[] { + return cipherHealthReport.flatMap((rpt) => + rpt.trimmedUris.map((u) => this.getFlattenedCipherDetails(rpt, u)), + ); + } + + /** + * Loop through the flattened cipher to uri data. If the item exists it's values need to be updated with the new item. + * If the item is new, create and add the object with the flattened details + * @param cipherHealthUriReport Cipher and password health info broken out into their uris + * @returns Application health reports + */ + private getApplicationHealthReport( + cipherHealthUriReport: CipherHealthReportUriDetail[], + ): ApplicationHealthReportDetail[] { + const appReports: ApplicationHealthReportDetail[] = []; + cipherHealthUriReport.forEach((uri) => { + const index = appReports.findIndex((item) => item.applicationName === uri.trimmedUri); + + let atRisk: boolean = false; + if (uri.exposedPasswordDetail || uri.weakPasswordDetail || uri.reusedPasswordCount > 1) { + atRisk = true; + } + + if (index === -1) { + appReports.push(this.getApplicationReportDetail(uri, atRisk)); + } else { + appReports[index] = this.getApplicationReportDetail(uri, atRisk, appReports[index]); + } + }); + return appReports; + } + + private async findExposedPassword(cipher: CipherView): Promise { + const exposedCount = await this.auditService.passwordLeaked(cipher.login.password); + if (exposedCount > 0) { + const exposedDetail = { exposedXTimes: exposedCount } as ExposedPasswordDetail; + return exposedDetail; + } + return null; + } + + private findWeakPassword(cipher: CipherView): WeakPasswordDetail { + const hasUserName = this.isUserNameNotEmpty(cipher); + let userInput: string[] = []; + if (hasUserName) { + const atPosition = cipher.login.username.indexOf("@"); + if (atPosition > -1) { + userInput = userInput + .concat( + cipher.login.username + .substring(0, atPosition) + .trim() + .toLowerCase() + .split(/[^A-Za-z0-9]/), + ) + .filter((i) => i.length >= 3); + } else { + userInput = cipher.login.username + .trim() + .toLowerCase() + .split(/[^A-Za-z0-9]/) + .filter((i) => i.length >= 3); + } + } + const { score } = this.passwordStrengthService.getPasswordStrength( + cipher.login.password, + null, + userInput.length > 0 ? userInput : null, + ); + + if (score != null && score <= 2) { + const scoreValue = this.weakPasswordScore(score); + const weakPasswordDetail = { score: score, detailValue: scoreValue } as WeakPasswordDetail; + return weakPasswordDetail; + } + return null; + } + + private weakPasswordScore(score: number): WeakPasswordScore { + switch (score) { + case 4: + return { label: "strong", badgeVariant: "success" }; + case 3: + return { label: "good", badgeVariant: "primary" }; + case 2: + return { label: "weak", badgeVariant: "warning" }; + default: + return { label: "veryWeak", badgeVariant: "danger" }; + } + } + + /** + * Create the new application health report detail object with the details from the cipher health report uri detail object + * update or create the at risk values if the item is at risk. + * @param newUriDetail New cipher uri detail + * @param isAtRisk If the cipher has a weak, exposed, or reused password it is at risk + * @param existingUriDetail The previously processed Uri item + * @returns The new or updated application health report detail + */ + private getApplicationReportDetail( + newUriDetail: CipherHealthReportUriDetail, + isAtRisk: boolean, + existingUriDetail?: ApplicationHealthReportDetail, + ): ApplicationHealthReportDetail { + const reportDetail = { + applicationName: existingUriDetail + ? existingUriDetail.applicationName + : newUriDetail.trimmedUri, + passwordCount: existingUriDetail ? existingUriDetail.passwordCount + 1 : 1, + memberDetails: existingUriDetail + ? this.getUniqueMembers(existingUriDetail.memberDetails.concat(newUriDetail.cipherMembers)) + : newUriDetail.cipherMembers, + atRiskMemberDetails: existingUriDetail ? existingUriDetail.atRiskMemberDetails : [], + atRiskPasswordCount: existingUriDetail ? existingUriDetail.atRiskPasswordCount : 0, + } as ApplicationHealthReportDetail; + + if (isAtRisk) { + (reportDetail.atRiskPasswordCount = reportDetail.atRiskPasswordCount + 1), + (reportDetail.atRiskMemberDetails = this.getUniqueMembers( + reportDetail.atRiskMemberDetails.concat(newUriDetail.cipherMembers), + )); + } + + reportDetail.memberCount = reportDetail.memberDetails.length; + + return reportDetail; + } + + /** + * Get a distinct array of members from a combined list. Input list may contain + * duplicate members. + * @param orgMembers Input list of members + * @returns Distinct array of members + */ + private getUniqueMembers(orgMembers: MemberDetailsFlat[]): MemberDetailsFlat[] { + const existingEmails = new Set(); + const distinctUsers = orgMembers.filter((member) => { + if (existingEmails.has(member.email)) { + return false; + } + existingEmails.add(member.email); + return true; + }); + return distinctUsers; + } + + private getFlattenedCipherDetails( + detail: CipherHealthReportDetail, + uri: string, + ): CipherHealthReportUriDetail { + return { + cipherId: detail.id, + reusedPasswordCount: detail.reusedPasswordCount, + weakPasswordDetail: detail.weakPasswordDetail, + exposedPasswordDetail: detail.exposedPasswordDetail, + cipherMembers: detail.cipherMembers, + trimmedUri: uri, + }; + } + + private getMemberDetailsFlat( + userName: string, + email: string, + cipherId: string, + ): MemberDetailsFlat { + return { + userName: userName, + email: email, + cipherId: cipherId, + }; + } + + /** + * Trim the cipher uris down to get the password health application. + * The uri should only exist once after being trimmed. No duplication. + * Example: + * - Untrimmed Uris: https://gmail.com, gmail.com/login + * - Both would trim to gmail.com + * - The cipher trimmed uri list should only return on instance in the list + * @param cipher + * @returns distinct list of trimmed cipher uris + */ + private getTrimmedCipherUris(cipher: CipherView): string[] { + const cipherUris: string[] = []; + const uris = cipher.login?.uris ?? []; + uris.map((u: { uri: string }) => { + const uri = Utils.getHostname(u.uri).replace("www.", ""); + if (!cipherUris.includes(uri)) { + cipherUris.push(uri); + } + }); + return cipherUris; + } + + private isUserNameNotEmpty(c: CipherView): boolean { + return !Utils.isNullOrWhitespace(c.login.username); + } + + /** + * Validates that the cipher is a login item, has a password + * is not deleted, and the user can view the password + * @param c the input cipher + */ + private validateCipher(c: CipherView): boolean { + const { type, login, isDeleted, viewPassword } = c; + if ( + type !== CipherType.Login || + login.password == null || + login.password === "" || + isDeleted || + !viewPassword + ) { + return false; + } + return true; + } +} From bfa9cf362394d9327941d688769bceb764d822c6 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Thu, 12 Dec 2024 17:17:24 +0100 Subject: [PATCH 06/10] [PM-15545][Defect] Update trial initiation UI for new flow via trial/send-verification-email endpoint (#12256) * Add the on trial payment option on new UI * Rename variables correctly * Resolve the isTrialPaymentOptional and use observable * use firstValueFrom and remove subscribe * Resolve the selected plantype * Changes for free Org --- .../complete-trial-initiation.component.html | 11 ++- .../complete-trial-initiation.component.ts | 84 ++++++++++++++++++- 2 files changed, 89 insertions(+), 6 deletions(-) diff --git a/apps/web/src/app/auth/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.html b/apps/web/src/app/auth/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.html index 9400e512c30..416d4004260 100644 --- a/apps/web/src/app/auth/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.html +++ b/apps/web/src/app/auth/trial-initiation/complete-trial-initiation/complete-trial-initiation.component.html @@ -23,12 +23,17 @@ bitButton buttonType="primary" [disabled]="orgInfoFormGroup.controls.name.invalid" - (click)="conditionallyCreateOrganization()" + [loading]="loading && (trialPaymentOptional$ | async)" + (click)="orgNameEntrySubmit()" > - {{ "next" | i18n }} + {{ (trialPaymentOptional$ | async) ? ("startTrial" | i18n) : ("next" | i18n) }} - + (); protected readonly SubscriptionProduct = SubscriptionProduct; protected readonly ProductType = ProductType; + protected trialPaymentOptional$ = this.configService.getFeatureFlag$( + FeatureFlag.TrialPaymentOptional, + ); constructor( protected router: Router, @@ -90,6 +105,7 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy { private registrationFinishService: RegistrationFinishService, private validationService: ValidationService, private loginStrategyService: LoginStrategyServiceAbstraction, + private configService: ConfigService, ) {} async ngOnInit(): Promise { @@ -119,6 +135,7 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy { this.product = this.validProducts.includes(product) ? product : ProductType.PasswordManager; const productTierParam = parseInt(qParams.productTier) as ProductTierType; + this.productTierValue = productTierParam; /** Only show the trial stepper for a subset of types */ const showPasswordManagerStepper = this.stepperProductTypes.includes(productTierParam); @@ -185,6 +202,16 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy { } } + async orgNameEntrySubmit(): Promise { + const isTrialPaymentOptional = await firstValueFrom(this.trialPaymentOptional$); + + if (isTrialPaymentOptional) { + await this.createOrganizationOnTrial(); + } else { + await this.conditionallyCreateOrganization(); + } + } + /** Update local details from organization created event */ createdOrganization(event: OrganizationCreatedEvent) { this.orgId = event.organizationId; @@ -192,11 +219,62 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy { this.verticalStepper.next(); } + /** create an organization on trial without payment method */ + async createOrganizationOnTrial() { + this.loading = true; + let trialInitiationPath: InitiationPath = "Password Manager trial from marketing website"; + let plan: PlanInformation = { + type: this.getPlanType(), + passwordManagerSeats: 1, + }; + + if (this.product === ProductType.SecretsManager) { + trialInitiationPath = "Secrets Manager trial from marketing website"; + plan = { + ...plan, + subscribeToSecretsManager: true, + isFromSecretsManagerTrial: true, + secretsManagerSeats: 1, + }; + } + + const organization: OrganizationInformation = { + name: this.orgInfoFormGroup.value.name, + billingEmail: this.orgInfoFormGroup.value.billingEmail, + initiationPath: trialInitiationPath, + }; + + const response = await this.organizationBillingService.purchaseSubscriptionNoPaymentMethod({ + organization, + plan, + }); + + this.orgId = response?.id; + this.billingSubLabel = response.name.toString(); + this.loading = false; + this.verticalStepper.next(); + } + /** Move the user to the previous step */ previousStep() { this.verticalStepper.previous(); } + getPlanType() { + switch (this.productTier) { + case ProductTierType.Teams: + return PlanType.TeamsAnnually; + case ProductTierType.Enterprise: + return PlanType.EnterpriseAnnually; + case ProductTierType.Families: + return PlanType.FamiliesAnnually; + case ProductTierType.Free: + return PlanType.Free; + default: + return PlanType.EnterpriseAnnually; + } + } + get isSecretsManagerFree() { return this.product === ProductType.SecretsManager && this.productTier === ProductTierType.Free; } From 0df7b53bb48bce033c9a881e4592b5be6ac13803 Mon Sep 17 00:00:00 2001 From: Alec Rippberger <127791530+alec-livefront@users.noreply.github.com> Date: Thu, 12 Dec 2024 10:28:30 -0600 Subject: [PATCH 07/10] feat(sso): [PM-8114] implement SSO component UI refresh Consolidates existing SSO components into a single unified component in libs/auth, matching the new design system. This implementation: - Creates a new shared SsoComponent with extracted business logic - Adds feature flag support for unauth-ui-refresh - Updates page styling including new icons and typography - Preserves web client claimed domain logic - Maintains backwards compatibility with legacy views PM-8114 --------- Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Co-authored-by: Jared Snider --- .../extension-sso-component.service.spec.ts | 67 ++ .../login/extension-sso-component.service.ts | 34 + ...o.component.html => sso-v1.component.html} | 0 .../{sso.component.ts => sso-v1.component.ts} | 4 +- apps/browser/src/popup/app-routing.module.ts | 43 +- apps/browser/src/popup/app.module.ts | 4 +- .../src/popup/services/services.module.ts | 7 + apps/desktop/src/app/app-routing.module.ts | 31 +- apps/desktop/src/app/app.module.ts | 4 +- .../src/app/services/services.module.ts | 7 + ...o.component.html => sso-v1.component.html} | 0 .../{sso.component.ts => sso-v1.component.ts} | 4 +- .../login/web-sso-component.service.spec.ts | 36 ++ .../login/web-sso-component.service.ts | 21 + ...o.component.html => sso-v1.component.html} | 0 .../{sso.component.ts => sso-v1.component.ts} | 4 +- apps/web/src/app/core/core.module.ts | 7 + apps/web/src/app/oss-routing.module.ts | 76 ++- .../src/app/shared/loose-components.module.ts | 6 +- apps/web/src/locales/en/messages.json | 6 + .../functions/unauth-ui-refresh-route-swap.ts | 1 + .../anon-layout-wrapper.component.html | 1 + .../anon-layout-wrapper.component.ts | 7 + .../anon-layout/anon-layout.component.html | 5 +- .../anon-layout/anon-layout.component.ts | 8 + .../anon-layout/anon-layout.stories.ts | 19 + libs/auth/src/angular/icons/index.ts | 1 + libs/auth/src/angular/icons/sso-key.icon.ts | 10 + libs/auth/src/angular/index.ts | 5 + .../sso/default-sso-component.service.ts | 3 + .../src/angular/sso/sso-component.service.ts | 20 + libs/auth/src/angular/sso/sso.component.html | 18 + libs/auth/src/angular/sso/sso.component.ts | 591 ++++++++++++++++++ 33 files changed, 1005 insertions(+), 45 deletions(-) create mode 100644 apps/browser/src/auth/popup/login/extension-sso-component.service.spec.ts create mode 100644 apps/browser/src/auth/popup/login/extension-sso-component.service.ts rename apps/browser/src/auth/popup/{sso.component.html => sso-v1.component.html} (100%) rename apps/browser/src/auth/popup/{sso.component.ts => sso-v1.component.ts} (97%) rename apps/desktop/src/auth/{sso.component.html => sso-v1.component.html} (100%) rename apps/desktop/src/auth/{sso.component.ts => sso-v1.component.ts} (97%) create mode 100644 apps/web/src/app/auth/core/services/login/web-sso-component.service.spec.ts create mode 100644 apps/web/src/app/auth/core/services/login/web-sso-component.service.ts rename apps/web/src/app/auth/{sso.component.html => sso-v1.component.html} (100%) rename apps/web/src/app/auth/{sso.component.ts => sso-v1.component.ts} (98%) create mode 100644 libs/auth/src/angular/icons/sso-key.icon.ts create mode 100644 libs/auth/src/angular/sso/default-sso-component.service.ts create mode 100644 libs/auth/src/angular/sso/sso-component.service.ts create mode 100644 libs/auth/src/angular/sso/sso.component.html create mode 100644 libs/auth/src/angular/sso/sso.component.ts diff --git a/apps/browser/src/auth/popup/login/extension-sso-component.service.spec.ts b/apps/browser/src/auth/popup/login/extension-sso-component.service.spec.ts new file mode 100644 index 00000000000..7d64c4114c0 --- /dev/null +++ b/apps/browser/src/auth/popup/login/extension-sso-component.service.spec.ts @@ -0,0 +1,67 @@ +import { TestBed } from "@angular/core/testing"; +import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; + +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { + EnvironmentService, + Environment, +} from "@bitwarden/common/platform/abstractions/environment.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; + +import { BrowserApi } from "../../../platform/browser/browser-api"; + +import { ExtensionSsoComponentService } from "./extension-sso-component.service"; + +describe("ExtensionSsoComponentService", () => { + let service: ExtensionSsoComponentService; + const baseUrl = "https://vault.bitwarden.com"; + + let syncService: MockProxy; + let authService: MockProxy; + let environmentService: MockProxy; + let i18nService: MockProxy; + let logService: MockProxy; + + beforeEach(() => { + syncService = mock(); + authService = mock(); + environmentService = mock(); + i18nService = mock(); + logService = mock(); + environmentService.environment$ = new BehaviorSubject({ + getWebVaultUrl: () => baseUrl, + } as Environment); + + TestBed.configureTestingModule({ + providers: [ + { provide: SyncService, useValue: syncService }, + { provide: AuthService, useValue: authService }, + { provide: EnvironmentService, useValue: environmentService }, + { provide: I18nService, useValue: i18nService }, + { provide: LogService, useValue: logService }, + ExtensionSsoComponentService, + ], + }); + + service = TestBed.inject(ExtensionSsoComponentService); + + jest.spyOn(BrowserApi, "reloadOpenWindows").mockImplementation(); + }); + + it("creates the service", () => { + expect(service).toBeTruthy(); + }); + + describe("closeWindow", () => { + it("closes window", async () => { + const windowSpy = jest.spyOn(window, "close").mockImplementation(); + + await service.closeWindow?.(); + + expect(windowSpy).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/browser/src/auth/popup/login/extension-sso-component.service.ts b/apps/browser/src/auth/popup/login/extension-sso-component.service.ts new file mode 100644 index 00000000000..3ddc7c67f7c --- /dev/null +++ b/apps/browser/src/auth/popup/login/extension-sso-component.service.ts @@ -0,0 +1,34 @@ +import { Injectable } from "@angular/core"; + +import { DefaultSsoComponentService, SsoComponentService } from "@bitwarden/auth/angular"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; + +/** + * This service is used to handle the SSO login process for the browser extension. + */ +@Injectable() +export class ExtensionSsoComponentService + extends DefaultSsoComponentService + implements SsoComponentService +{ + constructor( + protected syncService: SyncService, + protected authService: AuthService, + protected environmentService: EnvironmentService, + protected i18nService: I18nService, + protected logService: LogService, + ) { + super(); + } + + /** + * Closes the popup window after a successful login. + */ + async closeWindow() { + window.close(); + } +} diff --git a/apps/browser/src/auth/popup/sso.component.html b/apps/browser/src/auth/popup/sso-v1.component.html similarity index 100% rename from apps/browser/src/auth/popup/sso.component.html rename to apps/browser/src/auth/popup/sso-v1.component.html diff --git a/apps/browser/src/auth/popup/sso.component.ts b/apps/browser/src/auth/popup/sso-v1.component.ts similarity index 97% rename from apps/browser/src/auth/popup/sso.component.ts rename to apps/browser/src/auth/popup/sso-v1.component.ts index 988563c2fe6..ecb743848c7 100644 --- a/apps/browser/src/auth/popup/sso.component.ts +++ b/apps/browser/src/auth/popup/sso-v1.component.ts @@ -29,9 +29,9 @@ import { BrowserApi } from "../../platform/browser/browser-api"; @Component({ selector: "app-sso", - templateUrl: "sso.component.html", + templateUrl: "sso-v1.component.html", }) -export class SsoComponent extends BaseSsoComponent { +export class SsoComponentV1 extends BaseSsoComponent { constructor( ssoLoginService: SsoLoginServiceAbstraction, loginStrategyService: LoginStrategyServiceAbstraction, diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index b158a83c566..2893647f1a6 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -39,6 +39,7 @@ import { VaultIcon, LoginDecryptionOptionsComponent, DevicesIcon, + SsoComponent, TwoFactorTimeoutIcon, } from "@bitwarden/auth/angular"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -62,7 +63,7 @@ import { RemovePasswordComponent } from "../auth/popup/remove-password.component import { SetPasswordComponent } from "../auth/popup/set-password.component"; import { AccountSecurityComponent as AccountSecurityV1Component } from "../auth/popup/settings/account-security-v1.component"; import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component"; -import { SsoComponent } from "../auth/popup/sso.component"; +import { SsoComponentV1 } from "../auth/popup/sso-v1.component"; import { TwoFactorAuthComponent } from "../auth/popup/two-factor-auth.component"; import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component"; import { TwoFactorComponent } from "../auth/popup/two-factor.component"; @@ -230,12 +231,40 @@ const routes: Routes = [ canActivate: [unauthGuardFn(unauthRouteOverrides)], data: { elevation: 1 } satisfies RouteDataProperties, }, - { - path: "sso", - component: SsoComponent, - canActivate: [unauthGuardFn(unauthRouteOverrides)], - data: { elevation: 1 } satisfies RouteDataProperties, - }, + ...unauthUiRefreshSwap( + SsoComponentV1, + ExtensionAnonLayoutWrapperComponent, + { + path: "sso", + canActivate: [unauthGuardFn(unauthRouteOverrides)], + data: { elevation: 1 } satisfies RouteDataProperties, + }, + { + path: "sso", + canActivate: [unauthGuardFn(unauthRouteOverrides)], + data: { + pageIcon: VaultIcon, + pageTitle: { + key: "enterpriseSingleSignOn", + }, + pageSubtitle: { + key: "singleSignOnEnterOrgIdentifierText", + }, + elevation: 1, + } satisfies RouteDataProperties & ExtensionAnonLayoutWrapperData, + children: [ + { path: "", component: SsoComponent }, + { + path: "", + component: EnvironmentSelectorComponent, + outlet: "environment-selector", + data: { + overlayPosition: ExtensionDefaultOverlayPosition, + } satisfies EnvironmentSelectorRouteData, + }, + ], + }, + ), { path: "set-password", component: SetPasswordComponent, diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index d637f695e81..760b43a879c 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -33,7 +33,7 @@ import { SetPasswordComponent } from "../auth/popup/set-password.component"; import { AccountSecurityComponent as AccountSecurityComponentV1 } from "../auth/popup/settings/account-security-v1.component"; import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component"; import { VaultTimeoutInputComponent } from "../auth/popup/settings/vault-timeout-input.component"; -import { SsoComponent } from "../auth/popup/sso.component"; +import { SsoComponentV1 } from "../auth/popup/sso-v1.component"; import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component"; import { TwoFactorComponent } from "../auth/popup/two-factor.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; @@ -177,7 +177,7 @@ import "../platform/popup/locales"; SettingsComponent, VaultSettingsComponent, ShareComponent, - SsoComponent, + SsoComponentV1, SyncComponent, TabsComponent, TabsV2Component, diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 6b16ccce309..7014d908ac3 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -25,6 +25,7 @@ import { AnonLayoutWrapperDataService, LoginComponentService, LockComponentService, + SsoComponentService, LoginDecryptionOptionsService, } from "@bitwarden/auth/angular"; import { LockService, LoginEmailService, PinServiceAbstraction } from "@bitwarden/auth/common"; @@ -119,6 +120,7 @@ import { PasswordRepromptService } from "@bitwarden/vault"; import { ForegroundLockService } from "../../auth/popup/accounts/foreground-lock.service"; import { ExtensionAnonLayoutWrapperDataService } from "../../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service"; import { ExtensionLoginComponentService } from "../../auth/popup/login/extension-login-component.service"; +import { ExtensionSsoComponentService } from "../../auth/popup/login/extension-sso-component.service"; import { ExtensionLoginDecryptionOptionsService } from "../../auth/popup/login-decryption-options/extension-login-decryption-options.service"; import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service"; import AutofillService from "../../autofill/services/autofill.service"; @@ -597,6 +599,11 @@ const safeProviders: SafeProvider[] = [ useExisting: PopupCompactModeService, deps: [], }), + safeProvider({ + provide: SsoComponentService, + useClass: ExtensionSsoComponentService, + deps: [SyncService, AuthService, EnvironmentService, I18nServiceAbstraction, LogService], + }), safeProvider({ provide: LoginDecryptionOptionsService, useClass: ExtensionLoginDecryptionOptionsService, diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index db9ece317c8..21dced5c2aa 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -36,6 +36,7 @@ import { VaultIcon, LoginDecryptionOptionsComponent, DevicesIcon, + SsoComponent, TwoFactorTimeoutIcon, } from "@bitwarden/auth/angular"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -51,7 +52,7 @@ import { LoginViaAuthRequestComponentV1 } from "../auth/login/login-via-auth-req import { RegisterComponent } from "../auth/register.component"; import { RemovePasswordComponent } from "../auth/remove-password.component"; import { SetPasswordComponent } from "../auth/set-password.component"; -import { SsoComponent } from "../auth/sso.component"; +import { SsoComponentV1 } from "../auth/sso-v1.component"; import { TwoFactorAuthComponent } from "../auth/two-factor-auth.component"; import { TwoFactorComponent } from "../auth/two-factor.component"; import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component"; @@ -122,7 +123,33 @@ const routes: Routes = [ }, { path: "accessibility-cookie", component: AccessibilityCookieComponent }, { path: "set-password", component: SetPasswordComponent }, - { path: "sso", component: SsoComponent }, + ...unauthUiRefreshSwap( + SsoComponentV1, + AnonLayoutWrapperComponent, + { + path: "sso", + }, + { + path: "sso", + data: { + pageIcon: VaultIcon, + pageTitle: { + key: "enterpriseSingleSignOn", + }, + pageSubtitle: { + key: "singleSignOnEnterOrgIdentifierText", + }, + } satisfies AnonLayoutWrapperData, + children: [ + { path: "", component: SsoComponent }, + { + path: "", + component: EnvironmentSelectorComponent, + outlet: "environment-selector", + }, + ], + }, + ), { path: "send", component: SendComponent, diff --git a/apps/desktop/src/app/app.module.ts b/apps/desktop/src/app/app.module.ts index d787234e8b3..5bd1c66b87c 100644 --- a/apps/desktop/src/app/app.module.ts +++ b/apps/desktop/src/app/app.module.ts @@ -18,7 +18,7 @@ import { LoginModule } from "../auth/login/login.module"; import { RegisterComponent } from "../auth/register.component"; import { RemovePasswordComponent } from "../auth/remove-password.component"; import { SetPasswordComponent } from "../auth/set-password.component"; -import { SsoComponent } from "../auth/sso.component"; +import { SsoComponentV1 } from "../auth/sso-v1.component"; import { TwoFactorOptionsComponent } from "../auth/two-factor-options.component"; import { TwoFactorComponent } from "../auth/two-factor.component"; import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component"; @@ -92,7 +92,7 @@ import { SendComponent } from "./tools/send/send.component"; SetPasswordComponent, SettingsComponent, ShareComponent, - SsoComponent, + SsoComponentV1, TwoFactorComponent, TwoFactorOptionsComponent, UpdateTempPasswordComponent, diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index a8905d5640f..ccce1e3bd7c 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -25,6 +25,8 @@ import { LoginComponentService, SetPasswordJitService, LockComponentService, + SsoComponentService, + DefaultSsoComponentService, } from "@bitwarden/auth/angular"; import { InternalUserDecryptionOptionsServiceAbstraction, @@ -361,6 +363,11 @@ const safeProviders: SafeProvider[] = [ useClass: LoginEmailService, deps: [AccountService, AuthService, StateProvider], }), + safeProvider({ + provide: SsoComponentService, + useClass: DefaultSsoComponentService, + deps: [], + }), safeProvider({ provide: LoginApprovalComponentServiceAbstraction, useClass: DesktopLoginApprovalComponentService, diff --git a/apps/desktop/src/auth/sso.component.html b/apps/desktop/src/auth/sso-v1.component.html similarity index 100% rename from apps/desktop/src/auth/sso.component.html rename to apps/desktop/src/auth/sso-v1.component.html diff --git a/apps/desktop/src/auth/sso.component.ts b/apps/desktop/src/auth/sso-v1.component.ts similarity index 97% rename from apps/desktop/src/auth/sso.component.ts rename to apps/desktop/src/auth/sso-v1.component.ts index 760eef14e80..da3139e31f7 100644 --- a/apps/desktop/src/auth/sso.component.ts +++ b/apps/desktop/src/auth/sso-v1.component.ts @@ -23,9 +23,9 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legac @Component({ selector: "app-sso", - templateUrl: "sso.component.html", + templateUrl: "sso-v1.component.html", }) -export class SsoComponent extends BaseSsoComponent { +export class SsoComponentV1 extends BaseSsoComponent { constructor( ssoLoginService: SsoLoginServiceAbstraction, loginStrategyService: LoginStrategyServiceAbstraction, diff --git a/apps/web/src/app/auth/core/services/login/web-sso-component.service.spec.ts b/apps/web/src/app/auth/core/services/login/web-sso-component.service.spec.ts new file mode 100644 index 00000000000..b178e79b329 --- /dev/null +++ b/apps/web/src/app/auth/core/services/login/web-sso-component.service.spec.ts @@ -0,0 +1,36 @@ +import { TestBed } from "@angular/core/testing"; +import { mock, MockProxy } from "jest-mock-extended"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + +import { WebSsoComponentService } from "./web-sso-component.service"; + +describe("WebSsoComponentService", () => { + let service: WebSsoComponentService; + let i18nService: MockProxy; + + beforeEach(() => { + i18nService = mock(); + + TestBed.configureTestingModule({ + providers: [WebSsoComponentService, { provide: I18nService, useValue: i18nService }], + }); + service = TestBed.inject(WebSsoComponentService); + }); + + it("creates the service", () => { + expect(service).toBeTruthy(); + }); + + describe("setDocumentCookies", () => { + it("sets ssoHandOffMessage cookie with translated message", () => { + const mockMessage = "Test SSO Message"; + i18nService.t.mockReturnValue(mockMessage); + + service.setDocumentCookies?.(); + + expect(document.cookie).toContain(`ssoHandOffMessage=${mockMessage}`); + expect(i18nService.t).toHaveBeenCalledWith("ssoHandOff"); + }); + }); +}); diff --git a/apps/web/src/app/auth/core/services/login/web-sso-component.service.ts b/apps/web/src/app/auth/core/services/login/web-sso-component.service.ts new file mode 100644 index 00000000000..f036c3f488c --- /dev/null +++ b/apps/web/src/app/auth/core/services/login/web-sso-component.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from "@angular/core"; + +import { DefaultSsoComponentService, SsoComponentService } from "@bitwarden/auth/angular"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + +/** + * This service is used to handle the SSO login process for the web client. + */ +@Injectable() +export class WebSsoComponentService + extends DefaultSsoComponentService + implements SsoComponentService +{ + constructor(private i18nService: I18nService) { + super(); + } + + setDocumentCookies() { + document.cookie = `ssoHandOffMessage=${this.i18nService.t("ssoHandOff")};SameSite=strict`; + } +} diff --git a/apps/web/src/app/auth/sso.component.html b/apps/web/src/app/auth/sso-v1.component.html similarity index 100% rename from apps/web/src/app/auth/sso.component.html rename to apps/web/src/app/auth/sso-v1.component.html diff --git a/apps/web/src/app/auth/sso.component.ts b/apps/web/src/app/auth/sso-v1.component.ts similarity index 98% rename from apps/web/src/app/auth/sso.component.ts rename to apps/web/src/app/auth/sso-v1.component.ts index 86309f5d8bf..8699ecf7b24 100644 --- a/apps/web/src/app/auth/sso.component.ts +++ b/apps/web/src/app/auth/sso-v1.component.ts @@ -35,10 +35,10 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legac @Component({ selector: "app-sso", - templateUrl: "sso.component.html", + templateUrl: "sso-v1.component.html", }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class SsoComponent extends BaseSsoComponent implements OnInit { +export class SsoComponentV1 extends BaseSsoComponent implements OnInit { protected formGroup = new FormGroup({ identifier: new FormControl(null, [Validators.required]), }); diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index e3c59e13d99..2dd1db9fdb6 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -32,6 +32,7 @@ import { LoginComponentService, LockComponentService, SetPasswordJitService, + SsoComponentService, LoginDecryptionOptionsService, } from "@bitwarden/auth/angular"; import { @@ -101,6 +102,7 @@ import { WebLockComponentService, WebLoginDecryptionOptionsService, } from "../auth"; +import { WebSsoComponentService } from "../auth/core/services/login/web-sso-component.service"; import { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service"; import { HtmlStorageService } from "../core/html-storage.service"; import { I18nService } from "../core/i18n.service"; @@ -301,6 +303,11 @@ const safeProviders: SafeProvider[] = [ useClass: LoginEmailService, deps: [AccountService, AuthService, StateProvider], }), + safeProvider({ + provide: SsoComponentService, + useClass: WebSsoComponentService, + deps: [I18nServiceAbstraction], + }), safeProvider({ provide: LoginDecryptionOptionsService, useClass: WebLoginDecryptionOptionsService, diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 8aea628ddde..1903759f959 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -29,11 +29,13 @@ import { LockIcon, TwoFactorTimeoutIcon, UserLockIcon, + SsoKeyIcon, LoginViaAuthRequestComponent, DevicesIcon, RegistrationUserAddIcon, RegistrationLockAltIcon, RegistrationExpiredLinkIcon, + SsoComponent, VaultIcon, LoginDecryptionOptionsComponent, } from "@bitwarden/auth/angular"; @@ -62,7 +64,7 @@ import { AccountComponent } from "./auth/settings/account/account.component"; import { EmergencyAccessComponent } from "./auth/settings/emergency-access/emergency-access.component"; import { EmergencyAccessViewComponent } from "./auth/settings/emergency-access/view/emergency-access-view.component"; import { SecurityRoutingModule } from "./auth/settings/security/security-routing.module"; -import { SsoComponent } from "./auth/sso.component"; +import { SsoComponentV1 } from "./auth/sso-v1.component"; import { CompleteTrialInitiationComponent } from "./auth/trial-initiation/complete-trial-initiation/complete-trial-initiation.component"; import { freeTrialTextResolver } from "./auth/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver"; import { TrialInitiationComponent } from "./auth/trial-initiation/trial-initiation.component"; @@ -430,27 +432,57 @@ const routes: Routes = [ }, ], }, - { - path: "sso", - canActivate: [unauthGuardFn()], - data: { - pageTitle: { - key: "enterpriseSingleSignOn", - }, - titleId: "enterpriseSingleSignOn", - } satisfies RouteDataProperties & AnonLayoutWrapperData, - children: [ - { - path: "", - component: SsoComponent, - }, - { - path: "", - component: EnvironmentSelectorComponent, - outlet: "environment-selector", - }, - ], - }, + ...unauthUiRefreshSwap( + SsoComponentV1, + SsoComponent, + { + path: "sso", + canActivate: [unauthGuardFn()], + data: { + pageTitle: { + key: "enterpriseSingleSignOn", + }, + titleId: "enterpriseSingleSignOn", + } satisfies RouteDataProperties & AnonLayoutWrapperData, + children: [ + { + path: "", + component: SsoComponentV1, + }, + { + path: "", + component: EnvironmentSelectorComponent, + outlet: "environment-selector", + }, + ], + }, + { + path: "sso", + canActivate: [unauthGuardFn()], + data: { + pageTitle: { + key: "singleSignOn", + }, + titleId: "enterpriseSingleSignOn", + pageSubtitle: { + key: "singleSignOnEnterOrgIdentifierText", + }, + titleAreaMaxWidth: "md", + pageIcon: SsoKeyIcon, + } satisfies RouteDataProperties & AnonLayoutWrapperData, + children: [ + { + path: "", + component: SsoComponent, + }, + { + path: "", + component: EnvironmentSelectorComponent, + outlet: "environment-selector", + }, + ], + }, + ), { path: "login", canActivate: [unauthGuardFn()], diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index 15f15e2e317..3176ac81c1a 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -50,7 +50,7 @@ import { TwoFactorSetupYubiKeyComponent } from "../auth/settings/two-factor/two- import { TwoFactorSetupComponent } from "../auth/settings/two-factor/two-factor-setup.component"; import { TwoFactorVerifyComponent } from "../auth/settings/two-factor/two-factor-verify.component"; import { UserVerificationModule } from "../auth/shared/components/user-verification"; -import { SsoComponent } from "../auth/sso.component"; +import { SsoComponentV1 } from "../auth/sso-v1.component"; import { TwoFactorOptionsComponent } from "../auth/two-factor-options.component"; import { TwoFactorComponent } from "../auth/two-factor.component"; import { UpdatePasswordComponent } from "../auth/update-password.component"; @@ -158,7 +158,7 @@ import { SharedModule } from "./shared.module"; SetPasswordComponent, SponsoredFamiliesComponent, SponsoringOrgRowComponent, - SsoComponent, + SsoComponentV1, TwoFactorSetupAuthenticatorComponent, TwoFactorComponent, TwoFactorSetupDuoComponent, @@ -225,7 +225,7 @@ import { SharedModule } from "./shared.module"; SetPasswordComponent, SponsoredFamiliesComponent, SponsoringOrgRowComponent, - SsoComponent, + SsoComponentV1, TwoFactorSetupAuthenticatorComponent, TwoFactorComponent, TwoFactorSetupDuoComponent, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index aca22376132..77b8bddd9ee 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -4739,6 +4739,12 @@ "ssoLogInWithOrgIdentifier": { "message": "Log in using your organization's single sign-on portal. Please enter your organization's SSO identifier to begin." }, + "singleSignOnEnterOrgIdentifier": { + "message": "Enter your organization's SSO identifier to begin" + }, + "singleSignOnEnterOrgIdentifierText": { + "message": "To log in with your SSO provider, enter your organization's SSO identifier to begin. You may need to enter this SSO identifier when you log in from a new device." + }, "enterpriseSingleSignOn": { "message": "Enterprise single sign-on" }, diff --git a/libs/angular/src/auth/functions/unauth-ui-refresh-route-swap.ts b/libs/angular/src/auth/functions/unauth-ui-refresh-route-swap.ts index 1146b7b40e3..b19e73a7412 100644 --- a/libs/angular/src/auth/functions/unauth-ui-refresh-route-swap.ts +++ b/libs/angular/src/auth/functions/unauth-ui-refresh-route-swap.ts @@ -15,6 +15,7 @@ import { componentRouteSwap } from "../../utils/component-route-swap"; * @param defaultComponent - The current non-refreshed component to render. * @param refreshedComponent - The new refreshed component to render. * @param options - The shared route options to apply to both components. + * @param altOptions - The alt route options to apply to the alt component. If not provided, the base options will be used. */ export function unauthUiRefreshSwap( defaultComponent: Type, diff --git a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.html b/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.html index cfd436d93ae..95b1e6cadfe 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.html +++ b/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.html @@ -4,6 +4,7 @@ [icon]="pageIcon" [showReadonlyHostname]="showReadonlyHostname" [maxWidth]="maxWidth" + [titleAreaMaxWidth]="titleAreaMaxWidth" > diff --git a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.ts b/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.ts index 95b45ffe7b3..04dc3b6dfd2 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout-wrapper.component.ts @@ -35,6 +35,10 @@ export interface AnonLayoutWrapperData { * Optional flag to set the max-width of the page. Defaults to 'md' if not provided. */ maxWidth?: "md" | "3xl"; + /** + * Optional flag to set the max-width of the title area. Defaults to null if not provided. + */ + titleAreaMaxWidth?: "md"; } @Component({ @@ -50,6 +54,7 @@ export class AnonLayoutWrapperComponent implements OnInit, OnDestroy { protected pageIcon: Icon; protected showReadonlyHostname: boolean; protected maxWidth: "md" | "3xl"; + protected titleAreaMaxWidth: "md"; constructor( private router: Router, @@ -100,6 +105,7 @@ export class AnonLayoutWrapperComponent implements OnInit, OnDestroy { this.showReadonlyHostname = Boolean(firstChildRouteData["showReadonlyHostname"]); this.maxWidth = firstChildRouteData["maxWidth"]; + this.titleAreaMaxWidth = firstChildRouteData["titleAreaMaxWidth"]; } private listenForServiceDataChanges() { @@ -157,6 +163,7 @@ export class AnonLayoutWrapperComponent implements OnInit, OnDestroy { this.pageIcon = null; this.showReadonlyHostname = null; this.maxWidth = null; + this.titleAreaMaxWidth = null; } ngOnDestroy() { diff --git a/libs/auth/src/angular/anon-layout/anon-layout.component.html b/libs/auth/src/angular/anon-layout/anon-layout.component.html index 3323b6eca08..cb3445abd96 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.component.html +++ b/libs/auth/src/angular/anon-layout/anon-layout.component.html @@ -13,7 +13,10 @@ -
+
diff --git a/libs/auth/src/angular/anon-layout/anon-layout.component.ts b/libs/auth/src/angular/anon-layout/anon-layout.component.ts index 9f3a9a0eea6..91229f38ab2 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.component.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout.component.ts @@ -34,6 +34,13 @@ export class AnonLayoutComponent implements OnInit, OnChanges { @Input() hideLogo: boolean = false; @Input() hideFooter: boolean = false; + /** + * Max width of the title area content + * + * @default null + */ + @Input() titleAreaMaxWidth?: "md"; + /** * Max width of the layout content * @@ -60,6 +67,7 @@ export class AnonLayoutComponent implements OnInit, OnChanges { async ngOnInit() { this.maxWidth = this.maxWidth ?? "md"; + this.titleAreaMaxWidth = this.titleAreaMaxWidth ?? null; this.hostname = (await firstValueFrom(this.environmentService.environment$)).getHostname(); this.version = await this.platformUtilsService.getApplicationVersion(); diff --git a/libs/auth/src/angular/anon-layout/anon-layout.stories.ts b/libs/auth/src/angular/anon-layout/anon-layout.stories.ts index 77dc082c052..27eb27c53b9 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.stories.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout.stories.ts @@ -190,3 +190,22 @@ export const HideFooter: Story = { `, }), }; + +export const WithTitleAreaMaxWidth: Story = { + render: (args) => ({ + props: { + ...args, + title: "This is a very long long title to demonstrate titleAreaMaxWidth set to 'md'", + subtitle: + "This is a very long subtitle that demonstrates how the max width container handles longer text content with the titleAreaMaxWidth input set to 'md'. Lorem ipsum dolor sit amet consectetur adipisicing elit. Expedita, quod est?", + }, + template: ` + +
+
Primary Projected Content Area (customizable)
+
Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?
+
+
+ `, + }), +}; diff --git a/libs/auth/src/angular/icons/index.ts b/libs/auth/src/angular/icons/index.ts index 05bb630fcb3..0e86ee7fc8e 100644 --- a/libs/auth/src/angular/icons/index.ts +++ b/libs/auth/src/angular/icons/index.ts @@ -10,4 +10,5 @@ export * from "./vault.icon"; export * from "./registration-user-add.icon"; export * from "./registration-lock-alt.icon"; export * from "./registration-expired-link.icon"; +export * from "./sso-key.icon"; export * from "./two-factor-timeout.icon"; diff --git a/libs/auth/src/angular/icons/sso-key.icon.ts b/libs/auth/src/angular/icons/sso-key.icon.ts new file mode 100644 index 00000000000..38ae8a66525 --- /dev/null +++ b/libs/auth/src/angular/icons/sso-key.icon.ts @@ -0,0 +1,10 @@ +import { svgIcon } from "@bitwarden/components"; + +export const SsoKeyIcon = svgIcon` + + + + + + +`; diff --git a/libs/auth/src/angular/index.ts b/libs/auth/src/angular/index.ts index a01b8849c8d..817687ef2bc 100644 --- a/libs/auth/src/angular/index.ts +++ b/libs/auth/src/angular/index.ts @@ -64,6 +64,11 @@ export * from "./lock/lock-component.service"; // vault timeout export * from "./vault-timeout-input/vault-timeout-input.component"; +// sso +export * from "./sso/sso.component"; +export * from "./sso/sso-component.service"; +export * from "./sso/default-sso-component.service"; + // self hosted environment configuration dialog export * from "./self-hosted-env-config-dialog/self-hosted-env-config-dialog.component"; diff --git a/libs/auth/src/angular/sso/default-sso-component.service.ts b/libs/auth/src/angular/sso/default-sso-component.service.ts new file mode 100644 index 00000000000..1af7fe3948a --- /dev/null +++ b/libs/auth/src/angular/sso/default-sso-component.service.ts @@ -0,0 +1,3 @@ +import { SsoComponentService } from "./sso-component.service"; + +export class DefaultSsoComponentService implements SsoComponentService {} diff --git a/libs/auth/src/angular/sso/sso-component.service.ts b/libs/auth/src/angular/sso/sso-component.service.ts new file mode 100644 index 00000000000..b5712dfacc9 --- /dev/null +++ b/libs/auth/src/angular/sso/sso-component.service.ts @@ -0,0 +1,20 @@ +import { ClientType } from "@bitwarden/common/enums"; + +export type SsoClientType = ClientType.Web | ClientType.Browser | ClientType.Desktop; + +/** + * Abstract class for SSO component services. + */ +export abstract class SsoComponentService { + /** + * Sets the cookies for the SSO component service. + * Used to pass translation messages to the SSO connector page (apps/web/src/connectors/sso.ts) during the SSO handoff process. + * See implementation in WebSsoComponentService for example usage. + */ + setDocumentCookies?(): void; + + /** + * Closes the window. + */ + closeWindow?(): Promise; +} diff --git a/libs/auth/src/angular/sso/sso.component.html b/libs/auth/src/angular/sso/sso.component.html new file mode 100644 index 00000000000..7a3fa8db973 --- /dev/null +++ b/libs/auth/src/angular/sso/sso.component.html @@ -0,0 +1,18 @@ +
+
+ + {{ "loading" | i18n }} +
+
+ + {{ "ssoIdentifier" | i18n }} + + +
+
+ +
+
+
diff --git a/libs/auth/src/angular/sso/sso.component.ts b/libs/auth/src/angular/sso/sso.component.ts new file mode 100644 index 00000000000..aad0df4e397 --- /dev/null +++ b/libs/auth/src/angular/sso/sso.component.ts @@ -0,0 +1,591 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnInit } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormControl, FormGroup, Validators, ReactiveFormsModule } from "@angular/forms"; +import { ActivatedRoute, Router, RouterModule } from "@angular/router"; +import { firstValueFrom } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { + LoginStrategyServiceAbstraction, + SsoLoginCredentials, + TrustedDeviceUserDecryptionOption, + UserDecryptionOptions, + UserDecryptionOptionsServiceAbstraction, +} from "@bitwarden/auth/common"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrgDomainApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction"; +import { OrganizationDomainSsoDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-domain/responses/organization-domain-sso-details.response"; +import { VerifiedOrganizationDomainSsoDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-domain/responses/verified-organization-domain-sso-details.response"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; +import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; +import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; +import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; +import { SsoPreValidateResponse } from "@bitwarden/common/auth/models/response/sso-pre-validate.response"; +import { ClientType, HttpStatusCode } from "@bitwarden/common/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { + AsyncActionsModule, + ButtonModule, + CheckboxModule, + FormFieldModule, + IconButtonModule, + LinkModule, + ToastService, +} from "@bitwarden/components"; +import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; + +import { SsoClientType, SsoComponentService } from "./sso-component.service"; + +interface QueryParams { + code?: string; + state?: string; + redirectUri?: string; + clientId?: string; + codeChallenge?: string; + identifier?: string; + email?: string; +} + +/** + * This component handles the SSO flow. + */ +@Component({ + standalone: true, + templateUrl: "sso.component.html", + imports: [ + AsyncActionsModule, + ButtonModule, + CheckboxModule, + CommonModule, + FormFieldModule, + IconButtonModule, + LinkModule, + JslibModule, + ReactiveFormsModule, + RouterModule, + ], +}) +export class SsoComponent implements OnInit { + protected formGroup = new FormGroup({ + identifier: new FormControl(null, [Validators.required]), + }); + + protected redirectUri: string | undefined; + protected loggingIn = false; + protected identifier: string | undefined; + protected state: string | undefined; + protected codeChallenge: string | undefined; + protected clientId: SsoClientType | undefined; + + formPromise: Promise | undefined; + initiateSsoFormPromise: Promise | undefined; + + get identifierFormControl() { + return this.formGroup.controls.identifier; + } + + constructor( + private ssoLoginService: SsoLoginServiceAbstraction, + private loginStrategyService: LoginStrategyServiceAbstraction, + private router: Router, + private i18nService: I18nService, + private route: ActivatedRoute, + private orgDomainApiService: OrgDomainApiServiceAbstraction, + private validationService: ValidationService, + private configService: ConfigService, + private platformUtilsService: PlatformUtilsService, + private apiService: ApiService, + private cryptoFunctionService: CryptoFunctionService, + private environmentService: EnvironmentService, + private passwordGenerationService: PasswordGenerationServiceAbstraction, + private logService: LogService, + private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, + private accountService: AccountService, + private toastService: ToastService, + private ssoComponentService: SsoComponentService, + private syncService: SyncService, + ) { + environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => { + this.redirectUri = env.getWebVaultUrl() + "/sso-connector.html"; + }); + + const clientType = this.platformUtilsService.getClientType(); + if (this.isValidSsoClientType(clientType)) { + this.clientId = clientType as SsoClientType; + } + } + + async ngOnInit() { + const qParams: QueryParams = await firstValueFrom(this.route.queryParams); + + // This if statement will pass on the second portion of the SSO flow + // where the user has already authenticated with the identity provider + if (this.hasCodeOrStateParams(qParams)) { + await this.handleCodeAndStateParams(qParams); + return; + } + + // This if statement will pass on the first portion of the SSO flow + if (this.hasRequiredSsoParams(qParams)) { + this.setRequiredSsoVariables(qParams); + return; + } + + if (qParams.identifier != null) { + // SSO Org Identifier in query params takes precedence over claimed domains + this.identifierFormControl.setValue(qParams.identifier); + this.loggingIn = true; + await this.submit(); + return; + } + + await this.initializeIdentifierFromEmailOrStorage(qParams); + } + + /** + * Sets the required SSO variables from the query params + * @param qParams - The query params + */ + private setRequiredSsoVariables(qParams: QueryParams): void { + this.redirectUri = qParams.redirectUri ?? ""; + this.state = qParams.state ?? ""; + this.codeChallenge = qParams.codeChallenge ?? ""; + const clientId = qParams.clientId ?? ""; + if (this.isValidSsoClientType(clientId)) { + this.clientId = clientId; + } else { + throw new Error(`Invalid SSO client type: ${qParams.clientId}`); + } + } + + /** + * Checks if the value is a valid SSO client type + * @param value - The value to check + * @returns True if the value is a valid SSO client type, otherwise false + */ + private isValidSsoClientType(value: string): value is SsoClientType { + return [ClientType.Web, ClientType.Browser, ClientType.Desktop].includes(value as ClientType); + } + + /** + * Checks if the query params have the required SSO params + * @param qParams - The query params + * @returns True if the query params have the required SSO params, false otherwise + */ + private hasRequiredSsoParams(qParams: QueryParams): boolean { + return ( + qParams.clientId != null && + qParams.redirectUri != null && + qParams.state != null && + qParams.codeChallenge != null + ); + } + + /** + * Handles the code and state params + * @param qParams - The query params + */ + private async handleCodeAndStateParams(qParams: QueryParams): Promise { + const codeVerifier = await this.ssoLoginService.getCodeVerifier(); + const state = await this.ssoLoginService.getSsoState(); + await this.ssoLoginService.setCodeVerifier(""); + await this.ssoLoginService.setSsoState(""); + + if (qParams.redirectUri != null) { + this.redirectUri = qParams.redirectUri; + } + + if ( + qParams.code != null && + codeVerifier != null && + state != null && + this.checkState(state, qParams.state ?? "") + ) { + const ssoOrganizationIdentifier = this.getOrgIdentifierFromState(qParams.state ?? ""); + await this.logIn(qParams.code, codeVerifier, ssoOrganizationIdentifier); + } + } + + /** + * Checks if the query params have a code or state + * @param qParams - The query params + * @returns True if the query params have a code or state, false otherwise + */ + private hasCodeOrStateParams(qParams: QueryParams): boolean { + return qParams.code != null && qParams.state != null; + } + + private handleGetClaimedDomainByEmailError(error: unknown): void { + if (error instanceof ErrorResponse) { + const errorResponse: ErrorResponse = error as ErrorResponse; + switch (errorResponse.statusCode) { + case HttpStatusCode.NotFound: + //this is a valid case for a domain not found + return; + + default: + this.validationService.showError(errorResponse); + break; + } + } + } + + submit = async (): Promise => { + if (this.formGroup.invalid) { + return; + } + + const autoSubmit = (await firstValueFrom(this.route.queryParams)).identifier != null; + + this.identifier = this.identifierFormControl.value ?? ""; + await this.ssoLoginService.setOrganizationSsoIdentifier(this.identifier); + this.ssoComponentService.setDocumentCookies?.(); + try { + await this.submitSso(); + } catch (error) { + if (autoSubmit) { + await this.router.navigate(["/login"]); + } else { + this.validationService.showError(error); + } + } + }; + + private async submitSso(returnUri?: string, includeUserIdentifier?: boolean) { + if (this.identifier == null || this.identifier === "") { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("ssoValidationFailed"), + message: this.i18nService.t("ssoIdentifierRequired"), + }); + return; + } + + if (this.clientId == null) { + throw new Error("Client ID is required"); + } + + this.initiateSsoFormPromise = this.apiService.preValidateSso(this.identifier); + const response = await this.initiateSsoFormPromise; + + const authorizeUrl = await this.buildAuthorizeUrl( + returnUri, + includeUserIdentifier, + response.token, + ); + this.platformUtilsService.launchUri(authorizeUrl, { sameWindow: true }); + } + + private async buildAuthorizeUrl( + returnUri?: string, + includeUserIdentifier?: boolean, + token?: string, + ): Promise { + let codeChallenge = this.codeChallenge; + let state = this.state; + + const passwordOptions = { + type: "password" as const, + length: 64, + uppercase: true, + lowercase: true, + numbers: true, + special: false, + }; + + if (codeChallenge == null) { + const codeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions); + const codeVerifierHash = await this.cryptoFunctionService.hash(codeVerifier, "sha256"); + codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash); + await this.ssoLoginService.setCodeVerifier(codeVerifier); + } + + if (state == null) { + state = await this.passwordGenerationService.generatePassword(passwordOptions); + if (returnUri) { + state += `_returnUri='${returnUri}'`; + } + } + + // Add Organization Identifier to state + state += `_identifier=${this.identifier}`; + + // Save state (regardless of new or existing) + await this.ssoLoginService.setSsoState(state); + + const env = await firstValueFrom(this.environmentService.environment$); + + let authorizeUrl = + env.getIdentityUrl() + + "/connect/authorize?" + + "client_id=" + + this.clientId + + "&redirect_uri=" + + encodeURIComponent(this.redirectUri ?? "") + + "&" + + "response_type=code&scope=api offline_access&" + + "state=" + + state + + "&code_challenge=" + + codeChallenge + + "&" + + "code_challenge_method=S256&response_mode=query&" + + "domain_hint=" + + encodeURIComponent(this.identifier ?? "") + + "&ssoToken=" + + encodeURIComponent(token ?? ""); + + if (includeUserIdentifier) { + const userIdentifier = await this.apiService.getSsoUserIdentifier(); + authorizeUrl += `&user_identifier=${encodeURIComponent(userIdentifier)}`; + } + + return authorizeUrl; + } + + private async logIn(code: string, codeVerifier: string, orgSsoIdentifier: string): Promise { + this.loggingIn = true; + try { + const email = await this.ssoLoginService.getSsoEmail(); + const redirectUri = this.redirectUri ?? ""; + const credentials = new SsoLoginCredentials( + code, + codeVerifier, + redirectUri, + orgSsoIdentifier, + email, + ); + this.formPromise = this.loginStrategyService.logIn(credentials); + const authResult = await this.formPromise; + + if (authResult.requiresTwoFactor) { + return await this.handleTwoFactorRequired(orgSsoIdentifier); + } + + // Everything after the 2FA check is considered a successful login + // Just have to figure out where to send the user + + await this.syncService.fullSync(true); + + // Save off the OrgSsoIdentifier for use in the TDE flows (or elsewhere) + // - TDE login decryption options component + // - Browser SSO on extension open + // Note: you cannot set this in state before 2FA b/c there won't be an account in state. + await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(orgSsoIdentifier); + + // Users enrolled in admin acct recovery can be forced to set a new password after + // having the admin set a temp password for them (affects TDE & standard users) + if (authResult.forcePasswordReset == ForceSetPasswordReason.AdminForcePasswordReset) { + // Weak password is not a valid scenario here b/c we cannot have evaluated a MP yet + return await this.handleForcePasswordReset(orgSsoIdentifier); + } + + // must come after 2fa check since user decryption options aren't available if 2fa is required + const userDecryptionOpts = await firstValueFrom( + this.userDecryptionOptionsService.userDecryptionOptions$, + ); + + const tdeEnabled = userDecryptionOpts.trustedDeviceOption + ? await this.isTrustedDeviceEncEnabled(userDecryptionOpts.trustedDeviceOption) + : false; + + if (tdeEnabled) { + return await this.handleTrustedDeviceEncryptionEnabled(userDecryptionOpts); + } + + // In the standard, non TDE case, a user must set password if they don't + // have one and they aren't using key connector. + // Note: TDE & Key connector are mutually exclusive org config options. + const requireSetPassword = + !userDecryptionOpts.hasMasterPassword && + userDecryptionOpts.keyConnectorOption === undefined; + + if (requireSetPassword || authResult.resetMasterPassword) { + // Change implies going no password -> password in this case + return await this.handleChangePasswordRequired(orgSsoIdentifier); + } + + // Standard SSO login success case + return await this.handleSuccessfulLogin(); + } catch (e) { + await this.handleLoginError(e); + } + } + + private async isTrustedDeviceEncEnabled( + trustedDeviceOption: TrustedDeviceUserDecryptionOption, + ): Promise { + return trustedDeviceOption !== undefined; + } + + private async handleTwoFactorRequired(orgIdentifier: string) { + await this.router.navigate(["2fa"], { + queryParams: { + identifier: orgIdentifier, + sso: "true", + }, + }); + } + + private async handleTrustedDeviceEncryptionEnabled( + userDecryptionOpts: UserDecryptionOptions, + ): Promise { + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + + if (!userId) { + return; + } + + // Tde offboarding takes precedence + if ( + !userDecryptionOpts.hasMasterPassword && + userDecryptionOpts.trustedDeviceOption?.isTdeOffboarding + ) { + await this.masterPasswordService.setForceSetPasswordReason( + ForceSetPasswordReason.TdeOffboarding, + userId, + ); + } else if ( + // If user doesn't have a MP, but has reset password permission, they must set a MP + !userDecryptionOpts.hasMasterPassword && + userDecryptionOpts.trustedDeviceOption?.hasManageResetPasswordPermission + ) { + // Set flag so that auth guard can redirect to set password screen after decryption (trusted or untrusted device) + // Note: we cannot directly navigate in this scenario as we are in a pre-decryption state, and + // if you try to set a new MP before decrypting, you will invalidate the user's data by making a new user key. + await this.masterPasswordService.setForceSetPasswordReason( + ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission, + userId, + ); + } + + if (this.ssoComponentService?.closeWindow) { + await this.ssoComponentService.closeWindow(); + } else { + await this.router.navigate(["login-initiated"]); + } + } + + private async handleChangePasswordRequired(orgIdentifier: string) { + const emailVerification = await this.configService.getFeatureFlag( + FeatureFlag.EmailVerification, + ); + + let route = "set-password"; + if (emailVerification) { + route = "set-password-jit"; + } + + await this.router.navigate([route], { + queryParams: { + identifier: orgIdentifier, + }, + }); + } + + private async handleForcePasswordReset(orgIdentifier: string) { + await this.router.navigate(["update-temp-password"], { + queryParams: { + identifier: orgIdentifier, + }, + }); + } + + private async handleSuccessfulLogin() { + await this.router.navigate(["lock"]); + } + + private async handleLoginError(e: unknown) { + this.logService.error(e); + + // TODO: Key Connector Service should pass this error message to the logout callback instead of displaying here + if (e instanceof Error && e.message === "Key Connector error") { + this.toastService.showToast({ + variant: "error", + title: "", + message: this.i18nService.t("ssoKeyConnectorError"), + }); + } + } + + private getOrgIdentifierFromState(state: string): string { + if (state === null || state === undefined) { + return ""; + } + + const stateSplit = state.split("_identifier="); + return stateSplit.length > 1 ? stateSplit[1] : ""; + } + + private checkState(state: string, checkState: string): boolean { + if (state === null || state === undefined) { + return false; + } + if (checkState === null || checkState === undefined) { + return false; + } + + const stateSplit = state.split("_identifier="); + const checkStateSplit = checkState.split("_identifier="); + return stateSplit[0] === checkStateSplit[0]; + } + + /** + * Attempts to initialize the SSO identifier from email or storage. + * Note: this flow is written for web but both browser and desktop + * redirect here on SSO button click. + * @param qParams - The query params + */ + private async initializeIdentifierFromEmailOrStorage(qParams: QueryParams): Promise { + // Check if email matches any claimed domains + if (qParams.email) { + // show loading spinner + this.loggingIn = true; + try { + if (await this.configService.getFeatureFlag(FeatureFlag.VerifiedSsoDomainEndpoint)) { + const response: ListResponse = + await this.orgDomainApiService.getVerifiedOrgDomainsByEmail(qParams.email); + + if (response.data.length > 0) { + this.identifierFormControl.setValue(response.data[0].organizationIdentifier); + await this.submit(); + return; + } + } else { + const response: OrganizationDomainSsoDetailsResponse = + await this.orgDomainApiService.getClaimedOrgDomainByEmail(qParams.email); + + if (response?.ssoAvailable && response?.verifiedDate) { + this.identifierFormControl.setValue(response.organizationIdentifier); + await this.submit(); + return; + } + } + } catch (error) { + this.handleGetClaimedDomainByEmailError(error); + } + + this.loggingIn = false; + } + + // Fallback to state svc if domain is unclaimed + const storedIdentifier = await this.ssoLoginService.getOrganizationSsoIdentifier(); + if (storedIdentifier != null) { + this.identifierFormControl.setValue(storedIdentifier); + } + } +} From 3ce89f99452a9daf0de58b062cfc46ef5bbd5982 Mon Sep 17 00:00:00 2001 From: "bw-ghapp[bot]" <178206702+bw-ghapp[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 18:07:38 +0100 Subject: [PATCH 08/10] Autosync the updated translations (#12373) Co-authored-by: bitwarden-devops-bot <106330231+bitwarden-devops-bot@users.noreply.github.com> --- apps/browser/src/_locales/ar/messages.json | 6 + apps/browser/src/_locales/az/messages.json | 10 +- apps/browser/src/_locales/be/messages.json | 6 + apps/browser/src/_locales/bg/messages.json | 6 + apps/browser/src/_locales/bn/messages.json | 6 + apps/browser/src/_locales/bs/messages.json | 6 + apps/browser/src/_locales/ca/messages.json | 6 + apps/browser/src/_locales/cs/messages.json | 6 + apps/browser/src/_locales/cy/messages.json | 6 + apps/browser/src/_locales/da/messages.json | 6 + apps/browser/src/_locales/de/messages.json | 6 + apps/browser/src/_locales/el/messages.json | 6 + apps/browser/src/_locales/en_GB/messages.json | 6 + apps/browser/src/_locales/en_IN/messages.json | 6 + apps/browser/src/_locales/es/messages.json | 6 + apps/browser/src/_locales/et/messages.json | 6 + apps/browser/src/_locales/eu/messages.json | 6 + apps/browser/src/_locales/fa/messages.json | 6 + apps/browser/src/_locales/fi/messages.json | 12 +- apps/browser/src/_locales/fil/messages.json | 6 + apps/browser/src/_locales/fr/messages.json | 6 + apps/browser/src/_locales/gl/messages.json | 6 + apps/browser/src/_locales/he/messages.json | 6 + apps/browser/src/_locales/hi/messages.json | 6 + apps/browser/src/_locales/hr/messages.json | 6 + apps/browser/src/_locales/hu/messages.json | 6 + apps/browser/src/_locales/id/messages.json | 444 +++--- apps/browser/src/_locales/it/messages.json | 6 + apps/browser/src/_locales/ja/messages.json | 6 + apps/browser/src/_locales/ka/messages.json | 6 + apps/browser/src/_locales/km/messages.json | 6 + apps/browser/src/_locales/kn/messages.json | 6 + apps/browser/src/_locales/ko/messages.json | 1312 +++++++++-------- apps/browser/src/_locales/lt/messages.json | 6 + apps/browser/src/_locales/lv/messages.json | 6 + apps/browser/src/_locales/ml/messages.json | 6 + apps/browser/src/_locales/mr/messages.json | 6 + apps/browser/src/_locales/my/messages.json | 6 + apps/browser/src/_locales/nb/messages.json | 6 + apps/browser/src/_locales/ne/messages.json | 6 + apps/browser/src/_locales/nl/messages.json | 6 + apps/browser/src/_locales/nn/messages.json | 6 + apps/browser/src/_locales/or/messages.json | 6 + apps/browser/src/_locales/pl/messages.json | 6 + apps/browser/src/_locales/pt_BR/messages.json | 6 + apps/browser/src/_locales/pt_PT/messages.json | 8 +- apps/browser/src/_locales/ro/messages.json | 6 + apps/browser/src/_locales/ru/messages.json | 6 + apps/browser/src/_locales/si/messages.json | 6 + apps/browser/src/_locales/sk/messages.json | 6 + apps/browser/src/_locales/sl/messages.json | 6 + apps/browser/src/_locales/sr/messages.json | 114 +- apps/browser/src/_locales/sv/messages.json | 6 + apps/browser/src/_locales/te/messages.json | 6 + apps/browser/src/_locales/th/messages.json | 6 + apps/browser/src/_locales/tr/messages.json | 6 + apps/browser/src/_locales/uk/messages.json | 26 +- apps/browser/src/_locales/vi/messages.json | 6 + apps/browser/src/_locales/zh_CN/messages.json | 6 + apps/browser/src/_locales/zh_TW/messages.json | 6 + apps/browser/store/locales/id/copy.resx | 58 +- apps/browser/store/locales/ko/copy.resx | 56 +- apps/browser/store/locales/sr/copy.resx | 52 +- 63 files changed, 1386 insertions(+), 1024 deletions(-) diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index f98705ce1b6..9bd69199029 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "أدخل رمز التحقق من 6 أرقام من تطبيق المصادقة الخاص بك." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "أدخل رمز التحقق المكون من 6 أرقام الذي تم إرساله إلى $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 63e0c367e84..44493df174d 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -26,7 +26,7 @@ "message": "Keçid açarı ilə giriş et" }, "useSingleSignOn": { - "message": "Tək daxil olma üsulunu istifadə et" + "message": "Vahid daxil olma üsulunu istifadə et" }, "welcomeBack": { "message": "Yenidən xoş gəlmisiniz" @@ -38,7 +38,7 @@ "message": "Bir parol təyin edərək hesabınızı yaratmağı başa çatdırın" }, "enterpriseSingleSignOn": { - "message": "Müəssisə üçün tək daxil olma" + "message": "Müəssisə üçün vahid daxil olma" }, "cancel": { "message": "İmtina" @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Kimlik doğrulayıcı tətbiqindən 6 rəqəmli doğrulama kodunu daxil edin." }, + "authenticationTimeout": { + "message": "Kimlik doğrulama vaxtı bitdi" + }, + "authenticationSessionTimedOut": { + "message": "Kimlik doğrulama seansının vaxtı bitdi. Lütfən giriş prosesini yenidən başladın." + }, "enterVerificationCodeEmail": { "message": "$EMAIL$ ünvanına göndərilən e-poçtdakı 6 rəqəmli doğrulama kodunu daxil edin.", "placeholders": { diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index b09e7374c6e..ed385cba9b1 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Увядзіце 6 лічбаў праверачнага кода з вашай праграмы аўтэнтыфікацыі." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "Увядзіце 6 лічбаў праверачнага кода, які быў адпраўлены на $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 652129f6920..6285111db29 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Въведете шестцифрения код за потвърждение от приложението за удостоверяване." }, + "authenticationTimeout": { + "message": "Време на давност за удостоверяването" + }, + "authenticationSessionTimedOut": { + "message": "Сесията за удостоверяване е изтекла. Моля, започнете отначало процеса по вписване." + }, "enterVerificationCodeEmail": { "message": "Въведете шестцифрения код за потвърждение, който е бил изпратен на $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index 3b3b4cad43a..a084fb10be7 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "আপনার প্রমাণীকরণকারী অ্যাপ থেকে ৬ সংখ্যার যাচাইকরণ কোডটি প্রবেশ করুন।" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "$EMAIL$ এ ইমেইল করা ৬ সংখ্যার যাচাই কোডটি প্রবেশ করুন।", "placeholders": { diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index a6e893887c8..70077d4e172 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Enter the 6 digit verification code from your authenticator app." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "Enter the 6 digit verification code that was emailed to $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index da8d7a3f8cf..fe4405e6c78 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Introduïu el codi de verificació de 6 dígits de l'aplicació autenticadora." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "Introduïu el codi de verificació de 6 dígits que s'ha enviat per correu electrònic a $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index 1984ba2700f..6eecda877b1 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Zadejte 6místný kód z ověřovací aplikace." }, + "authenticationTimeout": { + "message": "Časový limit ověření" + }, + "authenticationSessionTimedOut": { + "message": "Vypršel časový limit relace ověřování. Restartujte proces přihlášení." + }, "enterVerificationCodeEmail": { "message": "Zadejte 6místný kód z e-mailu, který byl zaslán na $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index c57db3b62ff..ebb94ed5816 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Enter the 6 digit verification code from your authenticator app." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "Enter the 6 digit verification code that was emailed to $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index 8c10608a298..2d8bfe44025 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Indtast den 6-cifrede verifikationskode fra din autentificerings-app." }, + "authenticationTimeout": { + "message": "Godkendelsestimeout" + }, + "authenticationSessionTimedOut": { + "message": "Godkendelsessessionen fik timeout. Genstart loginprocessen." + }, "enterVerificationCodeEmail": { "message": "Indtast den 6-cifrede verifikationskode, der blev sendt til $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index b03f5a97579..8c7aa2384f3 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Gib den 6-stelligen Verifizierungscode aus deiner Authenticator App ein." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "Gib den 6-stelligen Bestätigungscode ein, der an $EMAIL$ gesendet wurde.", "placeholders": { diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index f56db1424a6..393ef385c89 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Εισάγετε τον 6ψήφιο κωδικό από την εφαρμογή επαλήθευσης." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "Εισάγετε τον 6ψήφιο κωδικό επαλήθευσης τον οποίο λάβατε στο $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index 392c8ef7f89..6047829a755 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Enter the 6 digit verification code from your authenticator app." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "Enter the 6 digit verification code that was emailed to $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 52996939873..178234f88d4 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Enter the 6 digit verification code from your authenticator app." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "Enter the 6 digit verification code that was emailed to $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index a7e7b8cebf1..6375d60ca83 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Introduce el código de verificación de 6 dígitos de tu aplicación autenticadora." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "Introduce el código de verificación de 6 dígitos que te ha sido enviado por correo electrónico", "placeholders": { diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 020f282fb48..1e3c21871a5 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Sisesta autentimise rakendusest 6 kohaline number." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "Sisesta 6 kohaline number, mis saadeti e-posti aadressile $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index cf9054064d8..87dec3a26d6 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Sartu zure autentifikazio aplikazioaren 6 digituko egiaztatze kodea." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "Sartu $EMAIL$-era bidalitako 6 digituko egiaztatze-kodea.", "placeholders": { diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index 9f7091b3ec9..2b3a9899038 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "کد ۶ رقمی تأیید را از برنامه احراز هویت وارد کنید." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "کد ۶ رقمی تأیید را که به $EMAIL$ ایمیل شده را وارد کنید.", "placeholders": { diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index aa8adbb799d..54a5280624b 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Syötä todennussovelluksesi näyttämä kuusinumeroinen todennuskoodi." }, + "authenticationTimeout": { + "message": "Todennuksen aikakatkaisu" + }, + "authenticationSessionTimedOut": { + "message": "Todennusistunto aikakatkaistiin. Ole hyvä ja aloita kirjautumisprosessi uudelleen." + }, "enterVerificationCodeEmail": { "message": "Syötä osoitteeseen $EMAIL$ lähetetty kuusinumeroinen todennuskoodi.", "placeholders": { @@ -4890,12 +4896,12 @@ "message": "Beta" }, "extensionWidth": { - "message": "Extension width" + "message": "Laajennuksen leveys" }, "wide": { - "message": "Wide" + "message": "Leveä" }, "extraWide": { - "message": "Extra wide" + "message": "Erittäin leveä" } } diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index fe3714b6d5f..ca48eb433ff 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Ipasok ang 6 na digit na code ng pagpapatunay mula sa iyong authenticator app." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "Ipasok ang 6 na digit na code na na-email sa $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index a4f238705aa..f197c66d122 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Saisissez le code de vérification à 6 chiffres depuis votre application d'authentification." }, + "authenticationTimeout": { + "message": "Délai d'authentification dépassé" + }, + "authenticationSessionTimedOut": { + "message": "La session d'authentification a expiré. Veuillez redémarrer le processus de connexion." + }, "enterVerificationCodeEmail": { "message": "Saisissez le code de vérification à 6 chiffres qui a été envoyé par courriel à $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 845ddb08a97..af406c43c2a 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Enter the 6 digit verification code from your authenticator app." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "Enter the 6 digit verification code that was emailed to $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index 88264c26e5d..f8502ec9386 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "הכנס את קוד האימות בן 6 הספרות מאפליקציית האימות שלך." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "הכנס את קוד האימות בן 6 הספרות שנשלח ל-$EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 0d4cf1548d4..41a5cb0c68f 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "अपने ऑथेंटिकेटर ऐप से 6 डिजिट वेरिफिकेशन कोड डालें।" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "Enter the 6 digit verification code that was emailed to $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index 659a8d1b593..8449f193c8b 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Unesi 6-znamenkasti kontrolni kôd iz autentifikatorske aplikacije." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "Unesi 6-znamenkasti kontrolni kôd poslan e-poštom na $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index d11b62b6cb4..4f0d5d29546 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Add meg a 6 számjegyű ellenőrző kódot a hitelesítő alkalmazásodból." }, + "authenticationTimeout": { + "message": "Hitelesítési időkifutás" + }, + "authenticationSessionTimedOut": { + "message": "A hitelesítési munkamenet időkifutással lejárt. Indítsuk újra a bejelentkezési folyamatot." + }, "enterVerificationCodeEmail": { "message": "$EMAIL$ email címre elküldött 6 számjegyű ellenőrző kód megadása.", "placeholders": { diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 467492a2f78..3bb084a5740 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Masukkan 6 digit kode verifikasi dari aplikasi autentikasi Anda." }, + "authenticationTimeout": { + "message": "Batas waktu otentikasi" + }, + "authenticationSessionTimedOut": { + "message": "Sesi otentikasi telah berakhir. Harap mulai ulang proses masuk." + }, "enterVerificationCodeEmail": { "message": "Masukkan 6 digit kode verifikasi yang dikirim melalui email ke $EMAIL$.", "placeholders": { @@ -1687,7 +1693,7 @@ "message": "Dr" }, "mx": { - "message": "Mx" + "message": "Yth" }, "firstName": { "message": "Nama Depan" @@ -2144,7 +2150,7 @@ "message": "Kata sandi utama baru" }, "confirmNewMasterPass": { - "message": "Confirm new master password" + "message": "Konfirmasi kata sandi utama baru" }, "masterPasswordPolicyInEffect": { "message": "Satu atau lebih kebijakan organisasi membutuhkan kata sandi utama Anda untuk memenuhi persyaratan berikut:" @@ -2189,19 +2195,19 @@ "message": "Kata sandi utama Anda yang baru tidak memenuhi persyaratan kebijakan." }, "receiveMarketingEmailsV2": { - "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." + "message": "Dapatkan saran, pengumuman, dan kesempatan penelitian dari Bitwarden di kotak masuk Anda." }, "unsubscribe": { - "message": "Unsubscribe" + "message": "Berhenti berlangganan" }, "atAnyTime": { - "message": "at any time." + "message": "kapanpun." }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "Dengan melanjutkan, Anda menyetujui" }, "and": { - "message": "and" + "message": "dan" }, "acceptPolicies": { "message": "Dengan mencentang kotak ini, Anda menyetujui yang berikut:" @@ -2222,10 +2228,10 @@ "message": "Oke" }, "errorRefreshingAccessToken": { - "message": "Access Token Refresh Error" + "message": "Galat Penyegaran Token Akses" }, "errorRefreshingAccessTokenDesc": { - "message": "No refresh token or API keys found. Please try logging out and logging back in." + "message": "Tidak ada token penyegaran atau kunci API yang ditemukan. Harap coba keluar dan masuk kembali." }, "desktopSyncVerificationTitle": { "message": "Verifikasi sinkronisasi desktop" @@ -2264,10 +2270,10 @@ "message": "Akun tidak cocok" }, "nativeMessagingWrongUserKeyTitle": { - "message": "Biometric key missmatch" + "message": "Kunci biometrik tidak cocok" }, "nativeMessagingWrongUserKeyDesc": { - "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + "message": "Gagal membuka dengan biometrik. Kunci rahasia biometrik gagal membuka brankas. Harap coba atur biometrik lagi." }, "biometricsNotEnabledTitle": { "message": "Biometrik tidak diaktifkan" @@ -2282,10 +2288,10 @@ "message": "Biometrik peramban tidak didukung di perangkat ini." }, "biometricsNotUnlockedTitle": { - "message": "User locked or logged out" + "message": "Pengguna terkunci atau telah keluar" }, "biometricsNotUnlockedDesc": { - "message": "Please unlock this user in the desktop application and try again." + "message": "Harap buka kunci pengguna ini di aplikasi desktop dan coba kembali." }, "biometricsNotAvailableTitle": { "message": "Buka dengan biometrik tidak tersedia" @@ -2518,7 +2524,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendPasswordDescV3": { - "message": "Add an optional password for recipients to access this Send.", + "message": "Tambahkan kata sandi tidak wajib untuk penerima untuk mengakses Send ini.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2818,7 +2824,7 @@ "message": "Tidak ada pengidentifikasi unik yang ditemukan." }, "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", + "message": "$ORGANIZATION$ menggunakan SSO dengan server kunci yang dihosting sendiri. Kata sandi utama tidak lagi diperlukan untuk masuk untuk anggota organisasi ini.", "placeholders": { "organization": { "content": "$1", @@ -2842,16 +2848,16 @@ "message": "Anda telah keluar dari organisasi." }, "toggleCharacterCount": { - "message": "Toggle character count" + "message": "Saklar hitung karakter" }, "sessionTimeout": { - "message": "Your session has timed out. Please go back and try logging in again." + "message": "Sesi Anda telah berakhir. Harap kembali dan coba masuk lagi." }, "exportingPersonalVaultTitle": { - "message": "Exporting individual vault" + "message": "Mengekspor brankas individu" }, "exportingIndividualVaultDescription": { - "message": "Only the individual vault items associated with $EMAIL$ will be exported. Organization vault items will not be included. Only vault item information will be exported and will not include associated attachments.", + "message": "Hanya benda-benda brankas perorangan yang terkait dengan $EMAIL$ yang akan diekspor. Benda-benda brankas organisasi tidak akan disertakan. Hanya informasi benda brankas yang akan diekspor dan tidak menyertakan lampiran yang terkait.", "placeholders": { "email": { "content": "$1", @@ -2860,10 +2866,10 @@ } }, "exportingOrganizationVaultTitle": { - "message": "Exporting organization vault" + "message": "Mengekspor brankas organisasi" }, "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "message": "Hanya brankas organisasi yang terkait dengan $ORGANIZATION$ yang akan diekspor. Benda-benda di brankas perorangan atau organisasi lainnya tidak akan disertakan.", "placeholders": { "organization": { "content": "$1", @@ -2881,10 +2887,10 @@ "message": "Buat nama pengguna baru" }, "generateEmail": { - "message": "Generate email" + "message": "Buat email" }, "spinboxBoundariesHint": { - "message": "Value must be between $MIN$ and $MAX$.", + "message": "Nilai harus ada di antara $MIN$ dan $MAX$.", "description": "Explains spin box minimum and maximum values to the user", "placeholders": { "min": { @@ -2898,7 +2904,7 @@ } }, "passwordLengthRecommendationHint": { - "message": " Use $RECOMMENDED$ characters or more to generate a strong password.", + "message": " Gunakan $RECOMMENDED$ karakter atau lebih untuk menghasilkan kata sandi yang kuat.", "description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -2908,7 +2914,7 @@ } }, "passphraseNumWordsRecommendationHint": { - "message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.", + "message": " Gunakan $RECOMMENDED$ kata atau lebih untuk menghasilkan frasa sandi yang kuat.", "description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -2921,17 +2927,17 @@ "message": "Jenis nama pengguna" }, "plusAddressedEmail": { - "message": "Plus addressed email", + "message": "Surel dengan alamat plus", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" }, "plusAddressedEmailDesc": { - "message": "Use your email provider's sub-addressing capabilities." + "message": "Gunakan kemampuan sub-addressing penyedia surel Anda." }, "catchallEmail": { - "message": "Catch-all email" + "message": "Surel tangkap-semua" }, "catchallEmailDesc": { - "message": "Use your domain's configured catch-all inbox." + "message": "Gunakan pengaturan kotak masuk tangkap-semua milik domain Anda." }, "random": { "message": "Acak" @@ -2949,24 +2955,24 @@ "message": "Jenis kata sandi" }, "service": { - "message": "Service" + "message": "Layanan" }, "forwardedEmail": { - "message": "Forwarded email alias" + "message": "Alias surel yang diteruskan" }, "forwardedEmailDesc": { - "message": "Generate an email alias with an external forwarding service." + "message": "Buat alias surel dengan layanan penerusan eksternal." }, "forwarderDomainName": { - "message": "Email domain", + "message": "Domain surel", "description": "Labels the domain name email forwarder service option" }, "forwarderDomainNameHint": { - "message": "Choose a domain that is supported by the selected service", + "message": "Pilih domain yang didukung oleh layanan terpilih", "description": "Guidance provided for email forwarding services that support multiple email domains." }, "forwarderError": { - "message": "$SERVICENAME$ error: $ERRORMESSAGE$", + "message": "Galat $SERVICENAME$: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", "placeholders": { "servicename": { @@ -2980,11 +2986,11 @@ } }, "forwarderGeneratedBy": { - "message": "Generated by Bitwarden.", + "message": "Dibuat oleh Bitwarden.", "description": "Displayed with the address on the forwarding service's configuration screen." }, "forwarderGeneratedByWithWebsite": { - "message": "Website: $WEBSITE$. Generated by Bitwarden.", + "message": "Situs web: $WEBSITE$. Dibuat oleh Bitwarden.", "description": "Displayed with the address on the forwarding service's configuration screen.", "placeholders": { "WEBSITE": { @@ -2994,7 +3000,7 @@ } }, "forwaderInvalidToken": { - "message": "Invalid $SERVICENAME$ API token", + "message": "Token API $SERVICENAME$ tidak valid", "description": "Displayed when the user's API token is empty or rejected by the forwarding service.", "placeholders": { "servicename": { @@ -3004,7 +3010,7 @@ } }, "forwaderInvalidTokenWithMessage": { - "message": "Invalid $SERVICENAME$ API token: $ERRORMESSAGE$", + "message": "Token API $SERVICENAME$ tidak valid: $ERRORMESSAGE$", "description": "Displayed when the user's API token is rejected by the forwarding service with an error message.", "placeholders": { "servicename": { @@ -3018,7 +3024,7 @@ } }, "forwarderNoAccountId": { - "message": "Unable to obtain $SERVICENAME$ masked email account ID.", + "message": "Gagal mendapatkan akun ID surel bertopeng dari $SERVICENAME$.", "description": "Displayed when the forwarding service fails to return an account ID.", "placeholders": { "servicename": { @@ -3028,7 +3034,7 @@ } }, "forwarderNoDomain": { - "message": "Invalid $SERVICENAME$ domain.", + "message": "Domain $SERVICENAME$ tidak valid.", "description": "Displayed when the domain is empty or domain authorization failed at the forwarding service.", "placeholders": { "servicename": { @@ -3038,7 +3044,7 @@ } }, "forwarderNoUrl": { - "message": "Invalid $SERVICENAME$ url.", + "message": "URL $SERVICENAME$ tidak valid.", "description": "Displayed when the url of the forwarding service wasn't supplied.", "placeholders": { "servicename": { @@ -3048,7 +3054,7 @@ } }, "forwarderUnknownError": { - "message": "Unknown $SERVICENAME$ error occurred.", + "message": "Terjadi galat yang tidak diketahui dari $SERVICENAME$.", "description": "Displayed when the forwarding service failed due to an unknown error.", "placeholders": { "servicename": { @@ -3058,7 +3064,7 @@ } }, "forwarderUnknownForwarder": { - "message": "Unknown forwarder: '$SERVICENAME$'.", + "message": "Penerus tidak diketahui: '$SERVICENAME$'.", "description": "Displayed when the forwarding service is not supported.", "placeholders": { "servicename": { @@ -3068,20 +3074,20 @@ } }, "hostname": { - "message": "Hostname", + "message": "Nama host", "description": "Part of a URL." }, "apiAccessToken": { - "message": "API Access Token" + "message": "Token Akses API" }, "apiKey": { - "message": "API Key" + "message": "Kunci API" }, "ssoKeyConnectorError": { - "message": "Key connector error: make sure key connector is available and working correctly." + "message": "Galat kunci penyambung: pastikan kunci penyambung tersedia dan bekerja dengan benar." }, "premiumSubcriptionRequired": { - "message": "Premium subscription required" + "message": "Langganan premium diperlukan" }, "organizationIsDisabled": { "message": "Organisasi ditangguhkan." @@ -3279,115 +3285,115 @@ "message": "Buka di jendela baru" }, "rememberThisDeviceToMakeFutureLoginsSeamless": { - "message": "Remember this device to make future logins seamless" + "message": "Ingat perangkat ini untuk membuat login berikutnya lebih lancar" }, "deviceApprovalRequired": { - "message": "Device approval required. Select an approval option below:" + "message": "Persetujuan perangkat diperlukan. Pilih sebuah pilihan persetujuan berikut:" }, "deviceApprovalRequiredV2": { - "message": "Device approval required" + "message": "Persetujuan perangkat diperlukan" }, "selectAnApprovalOptionBelow": { - "message": "Select an approval option below" + "message": "Pilih sebuah pilihan persetujuan berikut" }, "rememberThisDevice": { - "message": "Remember this device" + "message": "Ingat perangkat ini" }, "uncheckIfPublicDevice": { - "message": "Uncheck if using a public device" + "message": "Batalkan centang jika menggunakan perangkat umum" }, "approveFromYourOtherDevice": { - "message": "Approve from your other device" + "message": "Setujui dari perangkat lain milik Anda" }, "requestAdminApproval": { - "message": "Request admin approval" + "message": "Minta persetujuan admin" }, "approveWithMasterPassword": { - "message": "Approve with master password" + "message": "Setujui dengan kata sandi utama" }, "ssoIdentifierRequired": { - "message": "Organization SSO identifier is required." + "message": "Pengenal SSO organisasi diperlukan." }, "creatingAccountOn": { - "message": "Creating account on" + "message": "Membuat akun pada" }, "checkYourEmail": { - "message": "Check your email" + "message": "Periksa surel Anda" }, "followTheLinkInTheEmailSentTo": { - "message": "Follow the link in the email sent to" + "message": "Ikuti tautan pada surel yang telah dikirim" }, "andContinueCreatingYourAccount": { - "message": "and continue creating your account." + "message": "dan lanjutkan membuat akun Anda." }, "noEmail": { - "message": "No email?" + "message": "Tidak punya surel?" }, "goBack": { - "message": "Go back" + "message": "Kembali" }, "toEditYourEmailAddress": { - "message": "to edit your email address." + "message": "untuk menyunting alamat surel Anda." }, "eu": { "message": "EU", "description": "European Union" }, "accessDenied": { - "message": "Access denied. You do not have permission to view this page." + "message": "Akses ditolak. Anda tidak mempunyai izin untuk melihat halaman ini." }, "general": { - "message": "General" + "message": "Umum" }, "display": { - "message": "Display" + "message": "Tampilan" }, "accountSuccessfullyCreated": { - "message": "Account successfully created!" + "message": "Akun berhasil dibuat!" }, "adminApprovalRequested": { - "message": "Admin approval requested" + "message": "Persetujuan admin telah diminta" }, "adminApprovalRequestSentToAdmins": { - "message": "Your request has been sent to your admin." + "message": "Permintaan Anda telah dikirim ke admin Anda." }, "youWillBeNotifiedOnceApproved": { - "message": "You will be notified once approved." + "message": "Anda akan diberitahu setelah disetujui." }, "troubleLoggingIn": { - "message": "Trouble logging in?" + "message": "Kesulitan masuk?" }, "loginApproved": { - "message": "Login approved" + "message": "Login disetujui" }, "userEmailMissing": { - "message": "User email missing" + "message": "Surel pengguna hilang" }, "activeUserEmailNotFoundLoggingYouOut": { - "message": "Active user email not found. Logging you out." + "message": "Surel pengguna yang aktif tidak ditemukan. Mengeluarkan Anda." }, "deviceTrusted": { - "message": "Device trusted" + "message": "Perangkat dipercaya" }, "sendsNoItemsTitle": { - "message": "No active Sends", + "message": "Tidak ada Send yang aktif", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendsNoItemsMessage": { - "message": "Use Send to securely share encrypted information with anyone.", + "message": "Gunakan Send untuk membagikan informasi terenkripsi secara aman dengan siapapun.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "inputRequired": { - "message": "Input is required." + "message": "Masukan ini harus diisi." }, "required": { - "message": "required" + "message": "wajib diisi" }, "search": { - "message": "Search" + "message": "Cari" }, "inputMinLength": { - "message": "Input must be at least $COUNT$ characters long.", + "message": "Masukan sekurang-kurangnya $COUNT$ karakter.", "placeholders": { "count": { "content": "$1", @@ -3396,7 +3402,7 @@ } }, "inputMaxLength": { - "message": "Input must not exceed $COUNT$ characters in length.", + "message": "Masukan tidak boleh melebihi $COUNT$ karakter.", "placeholders": { "count": { "content": "$1", @@ -3405,7 +3411,7 @@ } }, "inputForbiddenCharacters": { - "message": "The following characters are not allowed: $CHARACTERS$", + "message": "Karakter berikut tidak diperbolehkan: $CHARACTERS$", "placeholders": { "characters": { "content": "$1", @@ -3414,7 +3420,7 @@ } }, "inputMinValue": { - "message": "Input value must be at least $MIN$.", + "message": "Nilai masukan sekurang-kurangnya $MIN$.", "placeholders": { "min": { "content": "$1", @@ -3423,7 +3429,7 @@ } }, "inputMaxValue": { - "message": "Input value must not exceed $MAX$.", + "message": "Nilai masukan tidak boleh melebihi $MAX$.", "placeholders": { "max": { "content": "$1", @@ -3432,17 +3438,17 @@ } }, "multipleInputEmails": { - "message": "1 or more emails are invalid" + "message": "1 atau lebih surel tidak valid" }, "inputTrimValidator": { - "message": "Input must not contain only whitespace.", + "message": "Masukan tidak boleh berisi hanya spasi kosong.", "description": "Notification to inform the user that a form's input can't contain only whitespace." }, "inputEmail": { - "message": "Input is not an email address." + "message": "Masukan bukan sebuah alamat surel." }, "fieldsNeedAttention": { - "message": "$COUNT$ field(s) above need your attention.", + "message": "$COUNT$ bidang di atas memerlukan perhatian Anda.", "placeholders": { "count": { "content": "$1", @@ -3451,10 +3457,10 @@ } }, "singleFieldNeedsAttention": { - "message": "1 field needs your attention." + "message": "1 bidang memerlukan perhatian Anda." }, "multipleFieldsNeedAttention": { - "message": "$COUNT$ fields need your attention.", + "message": "$COUNT$ bidang memerlukan perhatian Anda.", "placeholders": { "count": { "content": "$1", @@ -3463,10 +3469,10 @@ } }, "selectPlaceholder": { - "message": "-- Select --" + "message": "-- Pilih --" }, "multiSelectPlaceholder": { - "message": "-- Type to filter --" + "message": "-- Ketik untuk menyaring --" }, "multiSelectLoading": { "message": "Mengambil pilihan..." @@ -3663,40 +3669,40 @@ "message": "Diperlukan verifikasi untuk tindakan ini. Atur PIN untuk melanjutkan." }, "setPin": { - "message": "Set PIN" + "message": "Atur PIN" }, "verifyWithBiometrics": { - "message": "Verify with biometrics" + "message": "Verifikasi dengan biometrik" }, "awaitingConfirmation": { - "message": "Awaiting confirmation" + "message": "Menunggu konfirmasi" }, "couldNotCompleteBiometrics": { - "message": "Could not complete biometrics." + "message": "Tidak dapat melengkapi biometrik." }, "needADifferentMethod": { - "message": "Need a different method?" + "message": "Perlu cara lain?" }, "useMasterPassword": { - "message": "Use master password" + "message": "Gunakan kata sandi utama" }, "usePin": { - "message": "Use PIN" + "message": "Gunakan PIN" }, "useBiometrics": { - "message": "Use biometrics" + "message": "Gunakan biometrik" }, "enterVerificationCodeSentToEmail": { - "message": "Enter the verification code that was sent to your email." + "message": "Masukkan kode verifikasi yang dikirim ke surel Anda." }, "resendCode": { - "message": "Resend code" + "message": "Kirim ulang kode" }, "total": { - "message": "Total" + "message": "Jumlah" }, "importWarning": { - "message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organization. Do you want to proceed?", + "message": "Anda mengimpor data ke $ORGANIZATION$. Data Anda dapat dibagikan dengan anggota organisasi ini. Apakah Anda ingin melanjutkan?", "placeholders": { "organization": { "content": "$1", @@ -3705,49 +3711,49 @@ } }, "duoHealthCheckResultsInNullAuthUrlError": { - "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + "message": "Gagal menyambungkan dengan layanan Duo. Gunakan cara masuk dua-langkah lainnya atau hubungi Duo untuk mendapatkan panduan." }, "launchDuoAndFollowStepsToFinishLoggingIn": { - "message": "Launch Duo and follow the steps to finish logging in." + "message": "Luncurkan Duo dan ikuti langkah-langkah untuk menyelesaikan masuk." }, "duoRequiredForAccount": { - "message": "Duo two-step login is required for your account." + "message": "Login dua-langkah Duo diperlukan untuk akun Anda." }, "popoutTheExtensionToCompleteLogin": { - "message": "Popout the extension to complete login." + "message": "Sembulkan ekstensi untuk melengkapi login." }, "popoutExtension": { - "message": "Popout extension" + "message": "Sembulkan ekstensi" }, "launchDuo": { - "message": "Launch Duo" + "message": "Luncurkan Duo" }, "importFormatError": { - "message": "Data is not formatted correctly. Please check your import file and try again." + "message": "Data tidak diformat dengan benar. Harap periksa berkas impor Anda dan coba lagi." }, "importNothingError": { - "message": "Nothing was imported." + "message": "Tidak ada yang diimpor." }, "importEncKeyError": { - "message": "Error decrypting the exported file. Your encryption key does not match the encryption key used export the data." + "message": "Tidak dapat mendekripsi berkas yang diekspor. Kunci enkripsi Anda tidak cocok dengan kunci enkripsi yang digunakan untuk mengekspor data tersebut." }, "invalidFilePassword": { - "message": "Invalid file password, please use the password you entered when you created the export file." + "message": "Kata sandi berkas tidak valid, harap menggunakan kata sandi yang Anda masukkan saat Anda membuat berkas ekspor." }, "destination": { - "message": "Destination" + "message": "Tujuan" }, "learnAboutImportOptions": { - "message": "Learn about your import options" + "message": "Pelajari tentang pilihan impor Anda" }, "selectImportFolder": { - "message": "Select a folder" + "message": "Pilih folder" }, "selectImportCollection": { - "message": "Select a collection" + "message": "Pilih koleksi" }, "importTargetHint": { - "message": "Select this option if you want the imported file contents moved to a $DESTINATION$", + "message": "Pilih pilihan ini jika Anda ingin isi dari berkas yang diimpor dipindah ke $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": { @@ -3757,25 +3763,25 @@ } }, "importUnassignedItemsError": { - "message": "File contains unassigned items." + "message": "Berkas berisi benda-benda yang belum ditetapkan." }, "selectFormat": { - "message": "Select the format of the import file" + "message": "Pilih format untuk berkas yang diimpor" }, "selectImportFile": { - "message": "Select the import file" + "message": "Pilih berkas yang akan diimpor" }, "chooseFile": { - "message": "Choose File" + "message": "Pilih Berkas" }, "noFileChosen": { - "message": "No file chosen" + "message": "Tidak ada berkas yang dipilih" }, "orCopyPasteFileContents": { - "message": "or copy/paste the import file contents" + "message": "atau salin/tempel isi berkas yang diimpor" }, "instructionsFor": { - "message": "$NAME$ Instructions", + "message": "Petunjuk $NAME$", "description": "The title for the import tool instructions.", "placeholders": { "name": { @@ -3785,52 +3791,52 @@ } }, "confirmVaultImport": { - "message": "Confirm vault import" + "message": "Konfirmasi impor brankas" }, "confirmVaultImportDesc": { - "message": "This file is password-protected. Please enter the file password to import data." + "message": "Berkas ini dilindungi oleh kata sandi. Masukkan kata sandi berkas untuk mengimpor data." }, "confirmFilePassword": { - "message": "Confirm file password" + "message": "Konfirmasi kata sandi berkas" }, "exportSuccess": { - "message": "Vault data exported" + "message": "Data brankas berhasil diekspor" }, "typePasskey": { - "message": "Passkey" + "message": "Kunci sandi" }, "accessing": { - "message": "Accessing" + "message": "Sedang mengakses" }, "loggedInExclamation": { - "message": "Logged in!" + "message": "Sudah masuk!" }, "passkeyNotCopied": { - "message": "Passkey will not be copied" + "message": "Kunci sandi tidak akan disalin" }, "passkeyNotCopiedAlert": { - "message": "The passkey will not be copied to the cloned item. Do you want to continue cloning this item?" + "message": "Kunci sandi tidak akan disalin ke benda yang digandakan. Apakah Anda ingin melanjutkan menggandakan benda ini?" }, "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { - "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." + "message": "Verifikasi diperlukan oleh situs yang menyelenggarakan. Fitur ini belum diterapkan untuk akun tanpa kata sandi utama." }, "logInWithPasskeyQuestion": { - "message": "Log in with passkey?" + "message": "Masuk dengan kunci sandi?" }, "passkeyAlreadyExists": { - "message": "A passkey already exists for this application." + "message": "Kunci sandi sudah ada untuk aplikasi ini." }, "noPasskeysFoundForThisApplication": { - "message": "No passkeys found for this application." + "message": "Tidak ada kunci sandi yang ditemukan untuk aplikasi ini." }, "noMatchingPasskeyLogin": { - "message": "You do not have a matching login for this site." + "message": "Anda tidak memiliki login yang cocok untuk situs ini." }, "noMatchingLoginsForSite": { - "message": "No matching logins for this site" + "message": "Tidak ada login yang cocok untuk situs ini" }, "searchSavePasskeyNewLogin": { - "message": "Search or save passkey as new login" + "message": "Cari atau simpan kunci sandi sebagai login baru" }, "confirm": { "message": "Konfirmasi" @@ -3991,99 +3997,99 @@ "description": "Label indicating the most common import formats" }, "confirmContinueToBrowserSettingsTitle": { - "message": "Continue to browser settings?", + "message": "Lanjutkan ke pengaturan peramban?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" }, "confirmContinueToHelpCenter": { - "message": "Continue to Help Center?", + "message": "Lanjutkan ke Pusat Bantuan?", "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" }, "confirmContinueToHelpCenterPasswordManagementContent": { - "message": "Change your browser's autofill and password management settings.", + "message": "Ganti pengaturan isi otomatis dan pengelolaan kata sandi peramban Anda.", "description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser password management settings" }, "confirmContinueToHelpCenterKeyboardShortcutsContent": { - "message": "You can view and set extension shortcuts in your browser's settings.", + "message": "Anda dapat melihat dan mengatur pintasan ekstensi di pengaturan peramban Anda.", "description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser keyboard shortcut settings" }, "confirmContinueToBrowserPasswordManagementSettingsContent": { - "message": "Change your browser's autofill and password management settings.", + "message": "Ganti pengaturan isi otomatis dan pengelolaan kata sandi peramban Anda.", "description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page" }, "confirmContinueToBrowserKeyboardShortcutSettingsContent": { - "message": "You can view and set extension shortcuts in your browser's settings.", + "message": "Anda dapat melihat dan mengatur pintasan ekstensi di pengaturan peramban Anda.", "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" }, "overrideDefaultBrowserAutofillTitle": { - "message": "Make Bitwarden your default password manager?", + "message": "Jadikan Bitwarden sebagai pengelola kata sandi bawaan Anda?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", + "message": "Mengabaikan pilihan ini dapat mengakibatkan perseteruan antara saran isi otomatis Bitwarden dengan peramban Anda.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { - "message": "Make Bitwarden your default password manager", + "message": "Jadikan Bitwarden sebagai pengelola kata sandi bawaan Anda", "description": "Label for the setting that allows overriding the default browser autofill settings" }, "privacyPermissionAdditionNotGrantedTitle": { - "message": "Unable to set Bitwarden as the default password manager", + "message": "Tidak dapat mengatur Bitwarden sebagai pengelola kata sandi bawaan", "description": "Title for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "privacyPermissionAdditionNotGrantedDescription": { - "message": "You must grant browser privacy permissions to Bitwarden to set it as the default password manager.", + "message": "Anda harus mengizinkan perizinan privasi peramban kepada Bitwarden untuk mengaturnya sebagai pengelola kata sandi bawaan.", "description": "Description for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "makeDefault": { - "message": "Make default", + "message": "Jadikan bawaan", "description": "Button text for the setting that allows overriding the default browser autofill settings" }, "saveCipherAttemptSuccess": { - "message": "Credentials saved successfully!", + "message": "Kredensial berhasil disimpan!", "description": "Notification message for when saving credentials has succeeded." }, "passwordSaved": { - "message": "Password saved!", + "message": "Kata sandi telah disimpan!", "description": "Notification message for when saving credentials has succeeded." }, "updateCipherAttemptSuccess": { - "message": "Credentials updated successfully!", + "message": "Kredensial berhasil diperbarui!", "description": "Notification message for when updating credentials has succeeded." }, "passwordUpdated": { - "message": "Password updated!", + "message": "Kata sandi telah diperbarui!", "description": "Notification message for when updating credentials has succeeded." }, "saveCipherAttemptFailed": { - "message": "Error saving credentials. Check console for details.", + "message": "Gagal menyimpan kredensial. Periksa konsol untuk rinciannya.", "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "Berhasil" }, "removePasskey": { - "message": "Remove passkey" + "message": "Hapus kunci sandi" }, "passkeyRemoved": { - "message": "Passkey removed" + "message": "Kunci sandi dihapus" }, "autofillSuggestions": { - "message": "Autofill suggestions" + "message": "Saran isi otomatis" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to autofill" + "message": "Simpan benda login untuk situs ini ke isi otomatis" }, "yourVaultIsEmpty": { - "message": "Your vault is empty" + "message": "Brankas Anda kosong" }, "noItemsMatchSearch": { - "message": "No items match your search" + "message": "Tidak ada benda yang cocok dengan pencarian Anda" }, "clearFiltersOrTryAnother": { - "message": "Clear filters or try another search term" + "message": "Bersihkan penyaringan atau coba cari kata lainnya" }, "copyInfoTitle": { - "message": "Copy info - $ITEMNAME$", + "message": "Menyalin info - $ITEMNAME$", "description": "Title for a button that opens a menu with options to copy information from an item.", "placeholders": { "itemname": { @@ -4093,7 +4099,7 @@ } }, "copyNoteTitle": { - "message": "Copy Note - $ITEMNAME$", + "message": "Menyalin Catatan - $ITEMNAME$", "description": "Title for a button copies a note to the clipboard.", "placeholders": { "itemname": { @@ -4103,7 +4109,7 @@ } }, "moreOptionsLabel": { - "message": "More options, $ITEMNAME$", + "message": "Pilihan lainnya, $ITEMNAME$", "description": "Aria label for a button that opens a menu with more options for an item.", "placeholders": { "itemname": { @@ -4113,7 +4119,7 @@ } }, "moreOptionsTitle": { - "message": "More options - $ITEMNAME$", + "message": "Pilihan lainnya - $ITEMNAME$", "description": "Title for a button that opens a menu with more options for an item.", "placeholders": { "itemname": { @@ -4123,7 +4129,7 @@ } }, "viewItemTitle": { - "message": "View item - $ITEMNAME$", + "message": "LIhat benda - $ITEMNAME$", "description": "Title for a link that opens a view for an item.", "placeholders": { "itemname": { @@ -4133,7 +4139,7 @@ } }, "autofillTitle": { - "message": "Autofill - $ITEMNAME$", + "message": "Isi otomatis - $ITEMNAME$", "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { @@ -4143,40 +4149,40 @@ } }, "noValuesToCopy": { - "message": "No values to copy" + "message": "Tidak ada nilai untuk disalin" }, "assignToCollections": { - "message": "Assign to collections" + "message": "Menempatkan ke koleksi" }, "copyEmail": { - "message": "Copy email" + "message": "Salin surel" }, "copyPhone": { - "message": "Copy phone" + "message": "Salin nomor telepon" }, "copyAddress": { - "message": "Copy address" + "message": "Salin alamat" }, "adminConsole": { - "message": "Admin Console" + "message": "Konsol Admin" }, "accountSecurity": { - "message": "Account security" + "message": "Keamanan akun" }, "notifications": { - "message": "Notifications" + "message": "Pemberitahuan" }, "appearance": { - "message": "Appearance" + "message": "Tampilan" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "Gagal menetapkan ke koleksi yang dituju." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "Gagal menetapkan ke folder yang dituju." }, "viewItemsIn": { - "message": "View items in $NAME$", + "message": "Lihat benda-benda di $NAME$", "description": "Button to view the contents of a folder or collection", "placeholders": { "name": { @@ -4186,7 +4192,7 @@ } }, "backTo": { - "message": "Back to $NAME$", + "message": "Kembali ke $NAME$", "description": "Navigate back to a previous folder or collection", "placeholders": { "name": { @@ -4196,10 +4202,10 @@ } }, "new": { - "message": "New" + "message": "Baru" }, "removeItem": { - "message": "Remove $NAME$", + "message": "Buang $NAME$", "description": "Remove a selected option, such as a folder or collection", "placeholders": { "name": { @@ -4209,16 +4215,16 @@ } }, "itemsWithNoFolder": { - "message": "Items with no folder" + "message": "Benda-benda tanpa folder" }, "itemDetails": { - "message": "Item details" + "message": "Rincian benda" }, "itemName": { - "message": "Item name" + "message": "Nama benda" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Anda tidak dapat menghapus koleksi dengan izin hanya lihat: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -4227,47 +4233,47 @@ } }, "organizationIsDeactivated": { - "message": "Organization is deactivated" + "message": "Organisasi dinonaktifkan" }, "owner": { - "message": "Owner" + "message": "Pemilik" }, "selfOwnershipLabel": { - "message": "You", + "message": "Anda", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { - "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." + "message": "Benda-benda di organisasi yang dinonaktifkan tidak dapat diakses. Hubungi pemilik organisasi Anda untuk mendapatkan panduan." }, "additionalInformation": { - "message": "Additional information" + "message": "Informasi tambahan" }, "itemHistory": { - "message": "Item history" + "message": "Riwayat benda" }, "lastEdited": { - "message": "Last edited" + "message": "Terakhir disunting" }, "ownerYou": { - "message": "Owner: You" + "message": "Pemilik: Anda" }, "linked": { - "message": "Linked" + "message": "Terkait" }, "copySuccessful": { - "message": "Copy Successful" + "message": "Berhasil Disalin" }, "upload": { - "message": "Upload" + "message": "Unggah" }, "addAttachment": { - "message": "Add attachment" + "message": "Tambahkan lampiran" }, "maxFileSizeSansPunctuation": { - "message": "Maximum file size is 500 MB" + "message": "Ukuran berkas maksimal adalah 500 MB" }, "deleteAttachmentName": { - "message": "Delete attachment $NAME$", + "message": "Hapus lampiran $NAME$", "placeholders": { "name": { "content": "$1", @@ -4276,7 +4282,7 @@ } }, "downloadAttachmentName": { - "message": "Download $NAME$", + "message": "Unduh $NAME$", "placeholders": { "name": { "content": "$1", @@ -4285,25 +4291,25 @@ } }, "permanentlyDeleteAttachmentConfirmation": { - "message": "Are you sure you want to permanently delete this attachment?" + "message": "Apakah Anda yakin ingin menghapus lampiran ini selamanya?" }, "premium": { "message": "Premium" }, "freeOrgsCannotUseAttachments": { - "message": "Free organizations cannot use attachments" + "message": "Organisasi gratis tidak dapat menggunakan lampiran" }, "filters": { - "message": "Filters" + "message": "Penyaring" }, "filterVault": { - "message": "Filter vault" + "message": "Penyaring brankas" }, "filterApplied": { - "message": "One filter applied" + "message": "Satu saringan diterapkan" }, "filterAppliedPlural": { - "message": "$COUNT$ filters applied", + "message": "$COUNT$ saringan diterapkan", "placeholders": { "count": { "content": "$1", @@ -4312,10 +4318,10 @@ } }, "personalDetails": { - "message": "Personal details" + "message": "Rincian pribadi" }, "identification": { - "message": "Identification" + "message": "Pengenalan" }, "contactInfo": { "message": "Contact info" diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index e11b793399b..f4cefd2c381 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Inserisci il codice di verifica a 6 cifre dalla tua app di autenticazione." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "Inserisci il codice di verifica a 6 cifre inviato a $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index d9908835c8f..e0298ac9c24 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "認証アプリに表示された6桁の認証コードを入力してください。" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "$EMAIL$に送信された6桁の認証コードを入力してください。", "placeholders": { diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index 8b92a9ff340..d867b5bbc84 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Enter the 6 digit verification code from your authenticator app." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "Enter the 6 digit verification code that was emailed to $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index 6f26673abcd..29ef49db698 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Enter the 6 digit verification code from your authenticator app." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "Enter the 6 digit verification code that was emailed to $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 6d15ddc9068..56dd7a0ded6 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "ನಿಮ್ಮ ದೃಢೀಕರಣ ಅಪ್ಲಿಕೇಶನ್‌ನಿಂದ 6 ಅಂಕಿಯ ಪರಿಶೀಲನಾ ಕೋಡ್ ಅನ್ನು ನಮೂದಿಸಿ." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "$EMAIL$ಗೆ ಇಮೇಲ್ ಮಾಡಲಾದ 6 ಅಂಕಿಯ ಪರಿಶೀಲನಾ ಕೋಡ್ ಅನ್ನು ನಮೂದಿಸಿ.", "placeholders": { diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index 0b527149d15..67bb5244e8a 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -20,16 +20,16 @@ "message": "계정 만들기" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Bitwarden을 처음 이용하시나요?" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "패스키를 사용하여 로그인하기" }, "useSingleSignOn": { - "message": "Use single sign-on" + "message": "통합인증(SSO) 사용하기" }, "welcomeBack": { - "message": "Welcome back" + "message": "돌아온 것을 환영합니다." }, "setAStrongPassword": { "message": "비밀번호 설정" @@ -81,10 +81,10 @@ "message": "마스터 비밀번호 힌트 (선택)" }, "joinOrganization": { - "message": "Join organization" + "message": "\"조직\"에 가입하기" }, "joinOrganizationName": { - "message": "Join $ORGANIZATIONNAME$", + "message": "$ORGANIZATIONNAME$에 참가하기", "placeholders": { "organizationName": { "content": "$1", @@ -93,7 +93,7 @@ } }, "finishJoiningThisOrganizationBySettingAMasterPassword": { - "message": "Finish joining this organization by setting a master password." + "message": "마지막으로, 마스터 비밀번호를 설정하여 조직에 참가하십시오" }, "tab": { "message": "탭" @@ -120,7 +120,7 @@ "message": "비밀번호 복사" }, "copyPassphrase": { - "message": "Copy passphrase" + "message": "암호 복사" }, "copyNote": { "message": "메모 복사" @@ -153,16 +153,16 @@ "message": "운전면허 번호 복사" }, "copyPrivateKey": { - "message": "Copy private key" + "message": "개인 키 복사" }, "copyPublicKey": { - "message": "Copy public key" + "message": "공개 키 복사" }, "copyFingerprint": { - "message": "Copy fingerprint" + "message": "핑거프린트 복사" }, "copyCustomField": { - "message": "Copy $FIELD$", + "message": "$FIELD$ 복사", "placeholders": { "field": { "content": "$1", @@ -171,13 +171,13 @@ } }, "copyWebsite": { - "message": "Copy website" + "message": "웹사이트 복사" }, "copyNotes": { - "message": "Copy notes" + "message": "노트 복사" }, "fill": { - "message": "Fill", + "message": "채우기", "description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." }, "autoFill": { @@ -232,16 +232,16 @@ "message": "항목 추가" }, "accountEmail": { - "message": "Account email" + "message": "계정 이메일" }, "requestHint": { - "message": "Request hint" + "message": "힌트 요청" }, "requestPasswordHint": { - "message": "Request password hint" + "message": "마스터 비밀번호 힌트 얻기" }, "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": { - "message": "Enter your account email address and your password hint will be sent to you" + "message": "계정 이메일 주소를 입력하세요. 그 주소로 비밀번호 힌트가 전송될 것 입니다." }, "passwordHint": { "message": "비밀번호 힌트" @@ -274,7 +274,7 @@ "message": "마스터 비밀번호 변경" }, "continueToWebApp": { - "message": "웹 앱에서 계속하시겠용?" + "message": "웹 앱에서 계속하시겠나요?" }, "continueToWebAppDesc": { "message": "웹 앱에서 Bitwarden 계정의 더 많은 기능을 탐색해보세요." @@ -289,7 +289,7 @@ "message": "브라우저 확장 스토어로 이동하시겠습니까?" }, "continueToBrowserExtensionStoreDesc": { - "message": "Help others find out if Bitwarden is right for them. Visit your browser's extension store and leave a rating now." + "message": "다른 사람들이 Bitwarden이 적합한지 알 수 있도록 도와주세요. 당신의 브라우저 확장 스토어로 방문하여 별점을 남겨주세요." }, "changeMasterPasswordOnWebConfirmation": { "message": "Bitwarden 웹 앱에서 마스터 비밀번호를 변경할 수 있습니다." @@ -315,37 +315,37 @@ "message": "정보" }, "moreFromBitwarden": { - "message": "More from Bitwarden" + "message": "Bitwarden에 대한 더 많은 정보" }, "continueToBitwardenDotCom": { "message": "bitwarden.com 으로 이동할까요?" }, "bitwardenForBusiness": { - "message": "Bitwarden for Business" + "message": "비지니스용 Bitwarden" }, "bitwardenAuthenticator": { - "message": "Bitwarden Authenticator" + "message": "Bitwarden 인증 도구" }, "continueToAuthenticatorPageDesc": { - "message": "Bitwarden Authenticator allows you to store authenticator keys and generate TOTP codes for 2-step verification flows. Learn more on the bitwarden.com website" + "message": "Bitwarden 인증 도구를 사용하면, 인증키를 저장하고, 2단계 인증을 위한 TOTP 코드를 생성할 수 있습니다. 자세한 내용은 bitwarden.com 사이트에서 확인해주세요." }, "bitwardenSecretsManager": { - "message": "Bitwarden Secrets Manager" + "message": "Bitwarden 보안 매니저" }, "continueToSecretsManagerPageDesc": { - "message": "Securely store, manage, and share developer secrets with Bitwarden Secrets Manager. Learn more on the bitwarden.com website." + "message": "Bitwarden 보안 매니저를 이용하여, 개발자의 기밀을 안전하게 저장하고, 관리하고, 공유하세요. 자세한 내용은 bitwarden.com 사이트에서 확인해주요." }, "passwordlessDotDev": { - "message": "Passwordless.dev" + "message": "Passwardless.dev" }, "continueToPasswordlessDotDevPageDesc": { - "message": "Create smooth and secure login experiences free from traditional passwords with Passwordless.dev. Learn more on the bitwarden.com website." + "message": "Passwardless.dev와 함께, 기존의 비밀번호 로그인 방식으로 부터 벗어나, 매끄럽고 안전한 로그인 경험을 만들어보세요. 자세한 내용은 bitwarden.com 사이트에서 확인해주요" }, "freeBitwardenFamilies": { - "message": "Free Bitwarden Families" + "message": "무료 bitwarden 가족 플랜" }, "freeBitwardenFamiliesPageDesc": { - "message": "You are eligible for Free Bitwarden Families. Redeem this offer today in the web app." + "message": "무료 Bitwarden 가족 플랜을 이용하실 수 있습니다. 오늘 웹앱에서 이 혜택을 사용하세요." }, "version": { "message": "버전" @@ -366,22 +366,22 @@ "message": "폴더 편집" }, "newFolder": { - "message": "New folder" + "message": "새 폴더" }, "folderName": { - "message": "Folder name" + "message": "폴더 이름" }, "folderHintText": { - "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + "message": "상위 폴더 이름 뒤에 \"/\"를 추가하여 폴더를 계층적으로 구성합니다. 예: Social/Forums" }, "noFoldersAdded": { - "message": "No folders added" + "message": "추가된 폴더가 없습니다." }, "createFoldersToOrganize": { - "message": "Create folders to organize your vault items" + "message": "폴더를 만들어 보관함의 항목들을 정리해보세요" }, "deleteFolderPermanently": { - "message": "Are you sure you want to permanently delete this folder?" + "message": "정말로 이 폴더를 영구적으로 삭제하시겠습니까?" }, "deleteFolder": { "message": "폴더 삭제" @@ -424,7 +424,7 @@ "message": "유일무이하고 강력한 비밀번호를 자동으로 생성합니다." }, "bitWebVaultApp": { - "message": "Bitwarden web app" + "message": "Bitwarden 웹 앱" }, "importItems": { "message": "항목 가져오기" @@ -436,7 +436,7 @@ "message": "비밀번호 생성" }, "generatePassphrase": { - "message": "Generate passphrase" + "message": "암호 생성" }, "regeneratePassword": { "message": "비밀번호 재생성" @@ -467,11 +467,11 @@ "description": "deprecated. Use specialCharactersLabel instead." }, "include": { - "message": "Include", + "message": "포함", "description": "Card header for password generator include block" }, "uppercaseDescription": { - "message": "Include uppercase characters", + "message": "대문자 포함", "description": "Tooltip for the password generator uppercase character checkbox" }, "uppercaseLabel": { @@ -479,7 +479,7 @@ "description": "Label for the password generator uppercase character checkbox" }, "lowercaseDescription": { - "message": "Include lowercase characters", + "message": "소문자 포함", "description": "Full description for the password generator lowercase character checkbox" }, "lowercaseLabel": { @@ -487,7 +487,7 @@ "description": "Label for the password generator lowercase character checkbox" }, "numbersDescription": { - "message": "Include numbers", + "message": "숫자 포함", "description": "Full description for the password generator numbers checkbox" }, "numbersLabel": { @@ -495,7 +495,7 @@ "description": "Label for the password generator numbers checkbox" }, "specialCharactersDescription": { - "message": "Include special characters", + "message": "특수 문자 포함", "description": "Full description for the password generator special characters checkbox" }, "specialCharactersLabel": { @@ -526,11 +526,11 @@ "description": "deprecated. Use avoidAmbiguous instead." }, "avoidAmbiguous": { - "message": "Avoid ambiguous characters", + "message": "모호한 문자 사용 안 함", "description": "Label for the avoid ambiguous characters checkbox." }, "generatorPolicyInEffect": { - "message": "Enterprise policy requirements have been applied to your generator options.", + "message": "기업 정책에 따른 요구사항들이 당신의 생성기 옵션들에 적용되었습니다.", "description": "Indicates that a policy limits the credential generator screen." }, "searchVault": { @@ -567,16 +567,16 @@ "message": "즐겨찾기 해제" }, "itemAddedToFavorites": { - "message": "Item added to favorites" + "message": "항목이 즐겨찾기에 추가되었습니다." }, "itemRemovedFromFavorites": { - "message": "Item removed from favorites" + "message": "항목이 즐겨찾기에서 삭제되었습니다." }, "notes": { "message": "메모" }, "privateNote": { - "message": "Private note" + "message": "개인 메모" }, "note": { "message": "메모" @@ -600,7 +600,7 @@ "message": "웹사이트 열기" }, "launchWebsiteName": { - "message": "Launch website $ITEMNAME$", + "message": "$ITEMNAME$ 웹사이드 열기", "placeholders": { "itemname": { "content": "$1", @@ -633,7 +633,7 @@ "message": "세션 만료" }, "vaultTimeoutHeader": { - "message": "Vault timeout" + "message": "보관함 시간초과" }, "otherOptions": { "message": "기타 옵션" @@ -654,13 +654,13 @@ "message": "보관함이 잠겨 있습니다. 마스터 비밀번호를 입력하여 계속하세요." }, "yourVaultIsLockedV2": { - "message": "Your vault is locked" + "message": "당신의 보관함이 잠겼습니다." }, "yourAccountIsLocked": { - "message": "Your account is locked" + "message": "당신의 계정이 잠겼습니다." }, "or": { - "message": "or" + "message": "또는" }, "unlock": { "message": "잠금 해제" @@ -685,7 +685,7 @@ "message": "보관함 시간 제한" }, "vaultTimeout1": { - "message": "Timeout" + "message": "시간초과" }, "lockNow": { "message": "지금 잠그기" @@ -739,16 +739,16 @@ "message": "보안" }, "confirmMasterPassword": { - "message": "Confirm master password" + "message": "마스터 비밀번호 확정" }, "masterPassword": { - "message": "Master password" + "message": "마스터 비밀번호" }, "masterPassImportant": { - "message": "Your master password cannot be recovered if you forget it!" + "message": "마스터 비밀번호는 잊어버려도 복구할 수 없습니다!" }, "masterPassHintLabel": { - "message": "Master password hint" + "message": "마스터 비밀번호 힌트" }, "errorOccurred": { "message": "오류가 발생했습니다" @@ -782,10 +782,10 @@ "message": "계정 생성이 완료되었습니다! 이제 로그인하실 수 있습니다." }, "newAccountCreated2": { - "message": "Your new account has been created!" + "message": "계정 생성이 완료되었습니다!" }, "youHaveBeenLoggedIn": { - "message": "You have been logged in!" + "message": "로그인이 이미 되어있습니다." }, "youSuccessfullyLoggedIn": { "message": "로그인에 성공했습니다." @@ -800,7 +800,7 @@ "message": "인증 코드는 반드시 입력해야 합니다." }, "webauthnCancelOrTimeout": { - "message": "The authentication was cancelled or took too long. Please try again." + "message": "인증이 너무 오래 걸리거나 취소되었습니다. 다시 시도하여 주십시오." }, "invalidVerificationCode": { "message": "유효하지 않은 확인 코드" @@ -828,16 +828,16 @@ "message": "현재 웹페이지에서 QR 코드 스캔하기" }, "totpHelperTitle": { - "message": "Make 2-step verification seamless" + "message": "간편하게 2단계 인증을 만들기" }, "totpHelper": { - "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + "message": "Bitwarden은 2단계 인증 코드들을 저장하고, 채워넣을 수 있습니다. 키를 복사하여 이 필드에 붙여넣으세요." }, "totpHelperWithCapture": { - "message": "Bitwarden can store and fill 2-step verification codes. Select the camera icon to take a screenshot of this website's authenticator QR code, or copy and paste the key into this field." + "message": "Bitwarden은 2단계 인증 코드들을 저장하고, 채워넣을 수 있습니다. 카메라 아이콘을 선택하고, 이 웹사이드의 인증 도구 QR코드를 스크린샷을 찍거나, 키를 복사하여 이 필드에 붙여넣으세요." }, "learnMoreAboutAuthenticators": { - "message": "Learn more about authenticators" + "message": "인증 도구에 대해 더 알아보기" }, "copyTOTP": { "message": "인증서 키 (TOTP) 복사" @@ -846,7 +846,7 @@ "message": "로그아웃됨" }, "loggedOutDesc": { - "message": "You have been logged out of your account." + "message": "계정이 로그아웃 되었습니다." }, "loginExpired": { "message": "로그인 세션이 만료되었습니다." @@ -855,19 +855,19 @@ "message": "로그인" }, "logInToBitwarden": { - "message": "Log in to Bitwarden" + "message": "Bitwarden에 로그인" }, "restartRegistration": { - "message": "Restart registration" + "message": "등록 재시작" }, "expiredLink": { "message": "만료된 링크" }, "pleaseRestartRegistrationOrTryLoggingIn": { - "message": "Please restart registration or try logging in." + "message": "등록 재시작 혹은 다시 로그인을 해주시길 바랍니다" }, "youMayAlreadyHaveAnAccount": { - "message": "You may already have an account" + "message": "계정을 이미 가지고 계실수도 있습니다." }, "logOutConfirmation": { "message": "정말 로그아웃하시겠습니까?" @@ -891,10 +891,10 @@ "message": "2단계 인증은 보안 키, 인증 앱, SMS, 전화 통화 등의 다른 기기로 사용자의 로그인 시도를 검증하여 사용자의 계정을 더욱 안전하게 만듭니다. 2단계 인증은 bitwarden.com 웹 보관함에서 활성화할 수 있습니다. 지금 웹 사이트를 방문하시겠습니까?" }, "twoStepLoginConfirmationContent": { - "message": "Make your account more secure by setting up two-step login in the Bitwarden web app." + "message": "Bitwarden 웹 앱에 2단계 인증을 설정하여, 당신의 계정을 좀 더 안전하게 만드세요." }, "twoStepLoginConfirmationTitle": { - "message": "Continue to web app?" + "message": "웹 앱으로 진행하나요?" }, "editedFolder": { "message": "폴더 편집함" @@ -981,7 +981,7 @@ "message": "로그인을 추가할 건지 물어보기" }, "vaultSaveOptionsTitle": { - "message": "Save to vault options" + "message": "보관함 옵션들을 저장하기" }, "addLoginNotificationDesc": { "message": "\"로그인 추가 알림\"을 사용하면 새 로그인을 사용할 때마다 보관함에 그 로그인을 추가할 것인지 물어봅니다." @@ -990,22 +990,22 @@ "message": "보관함에 항목이 없을 경우 추가하라는 메시지를 표시합니다. 모든 로그인된 계정에 적용됩니다." }, "showCardsInVaultView": { - "message": "Show cards as Autofill suggestions on Vault view" + "message": "보관함 보기에서 카드 자동완성 제안를 표시" }, "showCardsCurrentTab": { "message": "탭 페이지에 카드 표시" }, "showCardsCurrentTabDesc": { - "message": "List card items on the Tab page for easy autofill." + "message": "간편한 자동완성을 위해 탭에 카드 항목들을 나열" }, "showIdentitiesInVaultView": { - "message": "Show identities as Autofill suggestions on Vault view" + "message": "보관함 보기에서 신원들의 자동완성 제안을 표시" }, "showIdentitiesCurrentTab": { - "message": "Show identities on Tab page" + "message": "탭 페이지에 신원들을 표시" }, "showIdentitiesCurrentTabDesc": { - "message": "List identity items on the Tab page for easy autofill." + "message": "간편한 자동완성을 위해 탭에 신원 항목들을 나열" }, "clearClipboard": { "message": "클립보드 비우기", @@ -1043,7 +1043,7 @@ "message": "예, 지금 변경하겠습니다." }, "notificationUnlockDesc": { - "message": "Unlock your Bitwarden vault to complete the autofill request." + "message": "Bitwarden 보관함을 잠금 해제 하여 자동완성 요청을 완료하세요." }, "notificationUnlock": { "message": "잠금 해제" @@ -1052,13 +1052,13 @@ "message": "추가 옵션" }, "enableContextMenuItem": { - "message": "Show context menu options" + "message": "문맥 매뉴 옵션 표시" }, "contextMenuItemDesc": { - "message": "Use a secondary click to access password generation and matching logins for the website." + "message": "우클릭을 사용하여, 비밀번호 생성과 웹사이트 로그인 매칭에 접근하세요" }, "contextMenuItemDescAlt": { - "message": "Use a secondary click to access password generation and matching logins for the website. Applies to all logged in accounts." + "message": "우클릭을 사용하여, 웹사이트의 비밀번호 생성과 사용가능한 로그인들에 접근하세요. 모든 로그인 된 계정에 적용됩니다." }, "defaultUriMatchDetection": { "message": "기본 URI 일치 인식", @@ -1089,7 +1089,7 @@ "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, "exportFrom": { - "message": "Export from" + "message": "~(으)로부터 내보내기" }, "exportVault": { "message": "보관함 내보내기" @@ -1098,19 +1098,19 @@ "message": "파일 형식" }, "fileEncryptedExportWarningDesc": { - "message": "This file export will be password protected and require the file password to decrypt." + "message": "이 파일 내보내기는 비밀번호로 보호될 것이며, 파일을 해독하기 위해서는 파일 비밀번호가 필요합니다." }, "filePassword": { "message": "파일 비밀번호" }, "exportPasswordDescription": { - "message": "This password will be used to export and import this file" + "message": "이 비밀번호는 이 파일을 파일 내보내거나, 가져오는데 사용됩니다." }, "accountRestrictedOptionDescription": { - "message": "Use your account encryption key, derived from your account's username and Master Password, to encrypt the export and restrict import to only the current Bitwarden account." + "message": "계정의 사용자 이름과 마스터 비밀번호에서 파생된 계정 암호화 키를 사용하여 내보내기를 암호화하고, 현재 Bitwarden계정으로 가져오기를 제한해보세요. " }, "passwordProtectedOptionDescription": { - "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + "message": "파일 비밀번호를 설정하여, 내보내기를 암호화하고, 해독에 그 파일 비밀번호를 사용하는 Bitwarden계정에 가져오세요." }, "exportTypeHeading": { "message": "내보내기 유형" @@ -1119,14 +1119,14 @@ "message": "계정 제한됨" }, "filePasswordAndConfirmFilePasswordDoNotMatch": { - "message": "“File password” and “Confirm file password“ do not match." + "message": "파일 비밀번호와 파일 비밀번호 확인이 일치하지 않습니다." }, "warning": { "message": "경고", "description": "WARNING (should stay in capitalized letters if the language permits)" }, "warningCapitalized": { - "message": "Warning", + "message": "경고", "description": "Warning (should maintain locale-relevant capitalization)" }, "confirmVaultExport": { @@ -1148,7 +1148,7 @@ "message": "공유됨" }, "bitwardenForBusinessPageDesc": { - "message": "Bitwarden for Business allows you to share your vault items with others by using an organization. Learn more on the bitwarden.com website." + "message": "비지니스용 Bitwarden은 조직을 사용하여 보관함 항목들을 다른 사람과 공유할 수 있게 해줍니다. 자세한 내용은 bitwarden.com 사이트에서 확인해주세요" }, "moveToOrganization": { "message": "조직으로 이동하기" @@ -1209,7 +1209,7 @@ "message": "파일" }, "fileToShare": { - "message": "File to share" + "message": "공유할 파일" }, "selectFile": { "message": "파일을 선택하세요." @@ -1221,7 +1221,7 @@ "message": "기능 사용할 수 없음" }, "encryptionKeyMigrationRequired": { - "message": "Encryption key migration required. Please login through the web vault to update your encryption key." + "message": "암호화 키 마이그레이션이 필요합니다. 웹 볼트를 통해 로그인하여 암호화 키를 업데이트하세요." }, "premiumMembership": { "message": "프리미엄 멤버십" @@ -1245,10 +1245,10 @@ "message": "1GB의 암호화된 파일 저장소." }, "premiumSignUpEmergency": { - "message": "Emergency access." + "message": "비상 접근" }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "YubiKey나 Duo와 같은 독점적인 2단계 로그인 옵션" }, "ppremiumSignUpReports": { "message": "보관함을 안전하게 유지하기 위한 암호 위생, 계정 상태, 데이터 유출 보고서" @@ -1269,7 +1269,7 @@ "message": "bitwarden.com 웹 보관함에서 프리미엄 멤버십을 구입할 수 있습니다. 지금 웹 사이트를 방문하시겠습니까?" }, "premiumPurchaseAlertV2": { - "message": "You can purchase Premium from your account settings on the Bitwarden web app." + "message": "Bitwarden 웹 앱의 계정 설정에서 프리미엄에 대한 결제를 할 수 있습니다." }, "premiumCurrentMember": { "message": "프리미엄 사용자입니다!" @@ -1278,7 +1278,7 @@ "message": "Bitwarden을 지원해 주셔서 감사합니다." }, "premiumFeatures": { - "message": "Upgrade to Premium and receive:" + "message": "프리미엄으로 업그래이드 하고 받기: " }, "premiumPrice": { "message": "이 모든 기능을 연 $PRICE$에 이용하실 수 있습니다!", @@ -1290,7 +1290,7 @@ } }, "premiumPriceV2": { - "message": "All for just $PRICE$ per year!", + "message": "이 모든 기능을 연 $PRICE$에 이용하실 수 있습니다!", "placeholders": { "price": { "content": "$1", @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "인증 앱에서 6자리 인증 코드를 입력하세요." }, + "authenticationTimeout": { + "message": "인증 시간 초과" + }, + "authenticationSessionTimedOut": { + "message": "인증 세션 시간이 초과 되었습니다. 다시 로그인을 시작해주세요." + }, "enterVerificationCodeEmail": { "message": "$EMAIL$ 주소로 전송된 6자리 인증 코드를 입력하세요.", "placeholders": { @@ -1383,17 +1389,17 @@ "message": "인증 앱" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "Bitwarden 인증같은 인증 앱을 통해 코드를 생성하여 입력해주세요", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP Security Key" + "message": "YubiKey OTP 보안 키" }, "yubiKeyDesc": { "message": "YubiKey를 사용하여 사용자의 계정에 접근합니다. YubiKey 4, 4 Nano, 4C 및 NEO 기기를 사용할 수 있습니다." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "Duo Security에서 생성한 코드를 입력하세요", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1410,7 +1416,7 @@ "message": "이메일" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "이메일로 전송된 코드를 입력하세요." }, "selfHostedEnvironment": { "message": "자체 호스팅 환경" @@ -1419,13 +1425,13 @@ "message": "온-프레미스 Bitwarden이 호스팅되고 있는 서버의 기본 URL을 지정하세요." }, "selfHostedBaseUrlHint": { - "message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com" + "message": "온-프레미스 Bitwarden이 호스팅되고 있는 서버의 기본 URL을 지정하세요. 예: https://bitwarden.company.com" }, "selfHostedCustomEnvHeader": { - "message": "For advanced configuration, you can specify the base URL of each service independently." + "message": "고급 구성의 경우 각 서비스의 기본 URL을 독립적으로 지정할 수 있습니다." }, "selfHostedEnvFormInvalid": { - "message": "You must add either the base Server URL or at least one custom environment." + "message": "기본 서버 URL이나 최소한 하나의 사용자 지정 환경을 추가해야 합니다." }, "customEnvironment": { "message": "사용자 지정 환경" @@ -1437,7 +1443,7 @@ "message": "서버 URL" }, "selfHostBaseUrl": { - "message": "Self-host server URL", + "message": "자체 호스트 서버 URL", "description": "Label for field requesting a self-hosted integration service URL" }, "apiUrl": { @@ -1463,28 +1469,28 @@ "description": "Represents the message for allowing the user to enable the autofill overlay" }, "autofillSuggestionsSectionTitle": { - "message": "Autofill suggestions" + "message": "자동 완성 제안" }, "showInlineMenuLabel": { - "message": "Show autofill suggestions on form fields" + "message": "양식 필드에 자동 완성 제안 표시" }, "showInlineMenuIdentitiesLabel": { - "message": "Display identities as suggestions" + "message": "신원를 제안으로 표시" }, "showInlineMenuCardsLabel": { - "message": "Display cards as suggestions" + "message": "카드를 제안으로 표시" }, "showInlineMenuOnIconSelectionLabel": { - "message": "Display suggestions when icon is selected" + "message": "아이콘을 선택하면 제안이 표시됩니다." }, "showInlineMenuOnFormFieldsDescAlt": { - "message": "Applies to all logged in accounts." + "message": "로그인한 모든 계정에 적용" }, "turnOffBrowserBuiltInPasswordManagerSettings": { "message": "충돌을 방지하기 위해 브라우저의 기본 암호 관리 설정을 해제합니다." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { - "message": "Edit browser settings." + "message": "브라우저 설정 편집" }, "autofillOverlayVisibilityOff": { "message": "끄기", @@ -1499,7 +1505,7 @@ "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, "enableAutoFillOnPageLoadSectionTitle": { - "message": "Autofill on page load" + "message": "페이지 로드 시 자동 완성" }, "enableAutoFillOnPageLoad": { "message": "페이지 로드 시 자동 완성 사용" @@ -1511,10 +1517,10 @@ "message": "취약하거나 신뢰할 수 없는 웹사이트 페이지 로드 시 자동 완성이 악용될 수 있습니다." }, "learnMoreAboutAutofillOnPageLoadLinkText": { - "message": "Learn more about risks" + "message": "위험에 대해 자세히 알아보기" }, "learnMoreAboutAutofill": { - "message": "Learn more about autofill" + "message": "자동 완정에 대해 자세히 할아보기" }, "defaultAutoFillOnPageLoad": { "message": "로그인 항목에 대한 기본 자동 완성 설정" @@ -1541,13 +1547,13 @@ "message": "사이드바에서 보관함 열기" }, "commandAutofillLoginDesc": { - "message": "Autofill the last used login for the current website" + "message": "현재 웹사이트에 마지막으로 사용된 로그인을 자동 채우기" }, "commandAutofillCardDesc": { - "message": "Autofill the last used card for the current website" + "message": "현재 웹사이트에 마지막으로 사용된 카드를 자동 채우기" }, "commandAutofillIdentityDesc": { - "message": "Autofill the last used identity for the current website" + "message": "현재 웹사이트에 마지막으로 사용된 신원을 자동 채우기" }, "commandGeneratePasswordDesc": { "message": "새 무작위 비밀번호를 만들고 클립보드에 복사합니다." @@ -1580,7 +1586,7 @@ "message": "참 / 거짓" }, "cfTypeCheckbox": { - "message": "Checkbox" + "message": "체크박스" }, "cfTypeLinked": { "message": "연결됨", @@ -1600,7 +1606,7 @@ "message": "웹사이트 아이콘 표시하기" }, "faviconDesc": { - "message": "Show a recognizable image next to each login." + "message": "로그인 정보 옆에 식별용 이미지를 표시합니다." }, "faviconDescAlt": { "message": "각 로그인 정보 옆에 인식할 수 있는 이미지를 표시합니다. 모든 로그인된 계정에 적용됩니다." @@ -1765,10 +1771,10 @@ "message": "신원" }, "typeSshKey": { - "message": "SSH key" + "message": "SSH 키" }, "newItemHeader": { - "message": "New $TYPE$", + "message": "새 $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1777,7 +1783,7 @@ } }, "editItemHeader": { - "message": "Edit $TYPE$", + "message": "$TYPE$ 수정", "placeholders": { "type": { "content": "$1", @@ -1786,7 +1792,7 @@ } }, "viewItemHeader": { - "message": "View $TYPE$", + "message": "$TYPE$ 보기", "placeholders": { "type": { "content": "$1", @@ -1798,13 +1804,13 @@ "message": "비밀번호 변경 기록" }, "generatorHistory": { - "message": "Generator history" + "message": "생성기 기록" }, "clearGeneratorHistoryTitle": { - "message": "Clear generator history" + "message": "생성기 기록 지우기" }, "cleargGeneratorHistoryDescription": { - "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + "message": "계속하면 모든 항목이 생성기 기록에서 영구적으로 삭제됩니다. 계속하시겠습니까?" }, "back": { "message": "뒤로" @@ -1813,7 +1819,7 @@ "message": "컬렉션" }, "nCollections": { - "message": "$COUNT$ collections", + "message": "$COUNT$ 컬렉션", "placeholders": { "count": { "content": "$1", @@ -1843,7 +1849,7 @@ "message": "보안 메모" }, "sshKeys": { - "message": "SSH Keys" + "message": "SSH 키" }, "clear": { "message": "삭제", @@ -1869,7 +1875,7 @@ "description": "Domain name. Ex. website.com" }, "baseDomainOptionRecommended": { - "message": "Base domain (recommended)", + "message": "기본 도메인 (추천)", "description": "Domain name. Ex. website.com" }, "domainName": { @@ -1923,13 +1929,13 @@ "message": "비밀번호가 없습니다." }, "clearHistory": { - "message": "Clear history" + "message": "기록 지우기" }, "nothingToShow": { - "message": "Nothing to show" + "message": "항목 없음" }, "nothingGeneratedRecently": { - "message": "You haven't generated anything recently" + "message": "최근에 생성한 것이 없습니다" }, "remove": { "message": "제거" @@ -1990,16 +1996,16 @@ "message": "PIN 코드를 사용하여 잠금 해제" }, "setYourPinTitle": { - "message": "Set PIN" + "message": "PIN 설정" }, "setYourPinButton": { - "message": "Set PIN" + "message": "PIN 설정" }, "setYourPinCode": { "message": "Bitwarden 잠금해제에 사용될 PIN 코드를 설정합니다. 이 애플리케이션에서 완전히 로그아웃할 경우 PIN 설정이 초기화됩니다." }, "setYourPinCode1": { - "message": "Your PIN will be used to unlock Bitwarden instead of your master password. Your PIN will reset if you ever fully log out of Bitwarden." + "message": "PIN은 마스터 비밀번호 대신 Bitwarden 잠금해제에 사용됩니다. Bitwarden에서 완전히 로그아웃하면 PIN이 재설정됩니다." }, "pinRequired": { "message": "PIN 코드가 필요합니다." @@ -2014,7 +2020,7 @@ "message": "생체 인식을 사용하여 잠금 해제" }, "unlockWithMasterPassword": { - "message": "Unlock with master password" + "message": "마스터 비밀번호로 잠금 해제" }, "awaitDesktop": { "message": "데스크톱으로부터의 확인을 대기 중" @@ -2026,7 +2032,7 @@ "message": "브라우저 다시 시작 시 마스터 비밀번호로 잠금" }, "lockWithMasterPassOnRestart1": { - "message": "Require master password on browser restart" + "message": "브라우저 다시 시작 시 마스터 비밀번호가 필요합니다" }, "selectOneCollection": { "message": "반드시 하나 이상의 컬렉션을 선택해야 합니다." @@ -2041,33 +2047,33 @@ "message": "하나 이상의 단체 정책이 생성기 규칙에 영항을 미치고 있습니다." }, "passwordGenerator": { - "message": "Password generator" + "message": "비밀번호 생성기" }, "usernameGenerator": { - "message": "Username generator" + "message": "사용자 이름 생성기" }, "useThisPassword": { - "message": "Use this password" + "message": "이 비밀번호 사용" }, "useThisUsername": { - "message": "Use this username" + "message": "이 사용자 이름 사용" }, "securePasswordGenerated": { - "message": "Secure password generated! Don't forget to also update your password on the website." + "message": "보안 비밀번호가 생성되었습니다! 웹사이트에서 비밀번호를 업데이트하는 것도 잊지 마세요." }, "useGeneratorHelpTextPartOne": { - "message": "Use the generator", + "message": "생성기를 사용하세요", "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" }, "useGeneratorHelpTextPartTwo": { - "message": "to create a strong unique password", + "message": "강력한 고유 비밀번호를 만들기 위해서는", "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" }, "vaultTimeoutAction": { "message": "보관함 시간 제한 초과시 동작" }, "vaultTimeoutAction1": { - "message": "Timeout action" + "message": "시간초과 시 행동" }, "lock": { "message": "잠금", @@ -2096,7 +2102,7 @@ "message": "복원된 항목" }, "alreadyHaveAccount": { - "message": "Already have an account?" + "message": "이미 계정이 있으신가요?" }, "vaultTimeoutLogOutConfirmation": { "message": "로그아웃하면 보관함에 대한 모든 접근이 제거되며 시간 제한을 초과하면 온라인 인증을 요구합니다. 정말로 이 설정을 사용하시겠습니까?" @@ -2108,7 +2114,7 @@ "message": "자동 완성 및 저장" }, "fillAndSave": { - "message": "Fill and save" + "message": "채우기 및 저장" }, "autoFillSuccessAndSavedUri": { "message": "항목을 자동 완성하고 URI를 저장함" @@ -2117,16 +2123,16 @@ "message": "항목을 자동 완성함" }, "insecurePageWarning": { - "message": "Warning: This is an unsecured HTTP page, and any information you submit can potentially be seen and changed by others. This Login was originally saved on a secure (HTTPS) page." + "message": "경고: 이 페이지는 보안이 해제된 HTTP 페이지이며, 제출한 모든 정보는 다른 사람이 보고 변경할 수 있습니다. 이 로그인은 원래 보안(HTTPS) 페이지에 저장되었습니다." }, "insecurePageWarningFillPrompt": { - "message": "Do you still wish to fill this login?" + "message": "여전히 이 로그인을 채우시겠습니까?" }, "autofillIframeWarning": { - "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to autofill anyway, or Cancel to stop." + "message": "양식은 저장된 로그인의 URI가 아닌 다른 도메인에서 호스팅됩니다. 그래도 자동 완성을 사용하시려면 OK, 아니라면 취소 버튼을 선택해주세요." }, "autofillIframeWarningTip": { - "message": "To prevent this warning in the future, save this URI, $HOSTNAME$, to your Bitwarden login item for this site.", + "message": "향후 이 경고를 방지하려면 이 URI인 $HOSTname$(을)를 Bitwarden로그인 항목에 저장하세요.", "placeholders": { "hostname": { "content": "$1", @@ -2189,25 +2195,25 @@ "message": "새 마스터 비밀번호가 정책 요구 사항을 따르지 않습니다." }, "receiveMarketingEmailsV2": { - "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." + "message": "Email 받은 편지함을 통해 Bitwarden의 조언, 공지사항 및 연구 기회들을 얻어보세요" }, "unsubscribe": { - "message": "Unsubscribe" + "message": "구독 취소" }, "atAnyTime": { - "message": "at any time." + "message": "언제든지" }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "계속하면 다음에 동의하게 됩니다" }, "and": { - "message": "and" + "message": "그리고" }, "acceptPolicies": { "message": "이 박스를 체크하면 다음에 동의하는 것으로 간주됩니다:" }, "acceptPoliciesRequired": { - "message": "Terms of Service and Privacy Policy have not been acknowledged." + "message": "서비스 약관 및 개인 정보 보호 정책을 확인하지 않았습니다." }, "termsOfService": { "message": "서비스 약관" @@ -2222,10 +2228,10 @@ "message": "확인" }, "errorRefreshingAccessToken": { - "message": "Access Token Refresh Error" + "message": "엑세스 토큰 새로고침 오류" }, "errorRefreshingAccessTokenDesc": { - "message": "No refresh token or API keys found. Please try logging out and logging back in." + "message": "새로 고침 토큰이나 API 키를 찾을 수 없습니다. 로그아웃하고 다시 로그인해 주세요" }, "desktopSyncVerificationTitle": { "message": "데스크톱과의 동기화 인증" @@ -2264,10 +2270,10 @@ "message": "계정이 일치하지 않음" }, "nativeMessagingWrongUserKeyTitle": { - "message": "Biometric key missmatch" + "message": "생체인식 키 불일치" }, "nativeMessagingWrongUserKeyDesc": { - "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + "message": "생체 인식 잠금 해제에 실패했습니다. 생체 인식 비밀 키가 보관함 잠금 해제에 실패했습니다. 생체 인식을 다시 설정해 보세요." }, "biometricsNotEnabledTitle": { "message": "생체 인식이 활성화되지 않음" @@ -2282,22 +2288,22 @@ "message": "이 기기에서는 생체 인식이 지원되지 않습니다." }, "biometricsNotUnlockedTitle": { - "message": "User locked or logged out" + "message": "사용자 잠금 또는 로그아웃" }, "biometricsNotUnlockedDesc": { - "message": "Please unlock this user in the desktop application and try again." + "message": "데스크톱 애플리케이션에서 이 사용자의 잠금을 해제하고 다시 시도해 주세요." }, "biometricsNotAvailableTitle": { - "message": "Biometric unlock unavailable" + "message": "생체 인식 잠금 해제 사용 불가" }, "biometricsNotAvailableDesc": { - "message": "Biometric unlock is currently unavailable. Please try again later." + "message": "생체 인식 잠금 해제는 현재 사용할 수 없습니다. 나중에 다시 시도해 주세요." }, "biometricsFailedTitle": { - "message": "Biometrics failed" + "message": "생체 인식 실패" }, "biometricsFailedDesc": { - "message": "Biometrics cannot be completed, consider using a master password or logging out. If this persists, please contact Bitwarden support." + "message": "생체 인식을 완료할 수 없습니다. 마스터 비밀번호를 사용하거나 로그아웃하는 것을 고려하세요. 이 문제가 계속되면 Bitwarden 지원팀에 문의해 주세요." }, "nativeMessaginPermissionErrorTitle": { "message": "권한이 부여되지 않음" @@ -2318,10 +2324,10 @@ "message": "조직의 정책이 소유권 설정에 영향을 미치고 있습니다." }, "personalOwnershipPolicyInEffectImports": { - "message": "An organization policy has blocked importing items into your individual vault." + "message": "조직 정책으로 인해 개별 보관함으로 항목을 가져오는 것이 차단되었습니다." }, "domainsTitle": { - "message": "Domains", + "message": "도메인", "description": "A category title describing the concept of web domains" }, "excludedDomains": { @@ -2331,10 +2337,10 @@ "message": "Bitwarden은 이 도메인들에 대해 로그인 정보를 저장할 것인지 묻지 않습니다. 페이지를 새로고침해야 변경된 내용이 적용됩니다." }, "excludedDomainsDescAlt": { - "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." + "message": "BItwarden은 로그인한 모든 계정에 대해 이러한 도메인에 대한 로그인 세부 정보를 저장하도록 요청하지 않습니다. 변경 사항을 적용하려면 페이지를 새로 고쳐야 합니다" }, "websiteItemLabel": { - "message": "Website $number$ (URI)", + "message": "웹사이트 $number$ (URI)", "placeholders": { "number": { "content": "$1", @@ -2352,17 +2358,17 @@ } }, "excludedDomainsSavedSuccess": { - "message": "Excluded domain changes saved" + "message": "제외된 도메인 변경 사항 저장됨" }, "limitSendViews": { - "message": "Limit views" + "message": "제한 보기" }, "limitSendViewsHint": { - "message": "No one can view this Send after the limit is reached.", + "message": "제한에 도달한 후에는 아무도 이 전송을 볼 수 없습니다.", "description": "Displayed under the limit views field on Send" }, "limitSendViewsCount": { - "message": "$ACCESSCOUNT$ views left", + "message": "남은 $ACCESSCOUNT$ 횟수", "description": "Displayed under the limit views field on Send", "placeholders": { "accessCount": { @@ -2372,26 +2378,26 @@ } }, "send": { - "message": "Send", + "message": "보내기", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendDetails": { - "message": "Send details", + "message": "보내기 세부 정보", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "searchSends": { - "message": "Send 검색", + "message": " Send 검색", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "addSend": { - "message": "Send 추가", + "message": " Send 추가", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendTypeText": { "message": "텍스트" }, "sendTypeTextToShare": { - "message": "Text to share" + "message": "공유할 텍스트" }, "sendTypeFile": { "message": "파일" @@ -2401,7 +2407,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "hideTextByDefault": { - "message": "Hide text by default" + "message": "기본적으로 텍스트 숨기기" }, "maxAccessCountReached": { "message": "최대 접근 횟수 도달", @@ -2417,10 +2423,10 @@ "message": "비밀번호로 보호됨" }, "copyLink": { - "message": "Copy link" + "message": "링크 복사" }, "copySendLink": { - "message": "Send 링크 복사", + "message": " Send 링크 복사", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "removePassword": { @@ -2433,7 +2439,7 @@ "message": "비밀번호 제거함" }, "deletedSend": { - "message": "Send 삭제함", + "message": " Send 삭제함", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendLink": { @@ -2447,27 +2453,27 @@ "message": "비밀번호를 제거하시겠습니까?" }, "deleteSend": { - "message": "Send 삭제", + "message": " Send 삭제", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "deleteSendConfirmation": { - "message": "정말 이 Send를 삭제하시겠습니까?", + "message": "정말 이 Send를 삭제하시겠습니까?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "deleteSendPermanentConfirmation": { - "message": "Are you sure you want to permanently delete this Send?", + "message": "이 Send을 영구적으로 삭제하시겠습니까?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "editSend": { - "message": "Send 편집", + "message": " Send 편집", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendTypeHeader": { - "message": "어떤 유형의 Send인가요?", + "message": "어떤 유형의 Send인가요?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNameDesc": { - "message": "이 Send의 이름", + "message": "이 Send을 설명할 이름", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendFileDesc": { @@ -2477,11 +2483,11 @@ "message": "삭제 날짜" }, "deletionDateDesc": { - "message": "이 Send가 정해진 일시에 영구적으로 삭제됩니다.", + "message": "이 Send가 정해진 일시에 영구적으로 삭제됩니다.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "deletionDateDescV2": { - "message": "The Send will be permanently deleted on this date.", + "message": "이 Send가 이 날짜에 영구적으로 삭제됩니다.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "expirationDate": { @@ -2495,7 +2501,7 @@ "message": "1일" }, "days": { - "message": "$DAYS$일", + "message": "$DAYS$ 일", "placeholders": { "days": { "content": "$1", @@ -2518,7 +2524,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendPasswordDescV3": { - "message": "Add an optional password for recipients to access this Send.", + "message": "수신자가 이 Send에 액세스할 수 있도록 비밀번호 옵션를 추가합니다.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2563,15 +2569,15 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "createdSendSuccessfully": { - "message": "Send created successfully!", + "message": "Send가 성공적으로 생성되었습니다!", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInHoursSingle": { - "message": "The Send will be available to anyone with the link for the next 1 hour.", + "message": "이 Send는 링크가 있는 누구나 향후 1시간 동안 이용할 수 있습니다.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInHours": { - "message": "The Send will be available to anyone with the link for the next $HOURS$ hours.", + "message": "이 전송은 링크가 있는 누구나 향후 $HOURS$ 시간 동안 이용할 수 있습니다.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "hours": { @@ -2581,11 +2587,11 @@ } }, "sendExpiresInDaysSingle": { - "message": "The Send will be available to anyone with the link for the next 1 day.", + "message": "이 Send은 향후 1일 동안 링크가 있는 누구나 이용할 수 있습니다.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInDays": { - "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "message": "이 Send은 향후 $DAYS$일 동안 링크가 있는 누구나 이용할 수 있습니다.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "days": { @@ -2595,19 +2601,19 @@ } }, "sendLinkCopied": { - "message": "Send link copied", + "message": "Send 링크 복사됨", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "editedSend": { - "message": "Send 수정함", + "message": "Send 수정됨", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendFilePopoutDialogText": { - "message": "Pop out extension?", + "message": "확장자를 새 창에서 열까요?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendFilePopoutDialogDesc": { - "message": "To create a file Send, you need to pop out the extension to a new window.", + "message": "파일 Send를 만들려면, 새 창으로 확장자를 열어야 합니다.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendLinuxChromiumFileWarning": { @@ -2620,7 +2626,7 @@ "message": "Safari에서 파일을 선택할 경우, 이 배너를 클릭하여 확장 프로그램을 새 창에서 여세요." }, "popOut": { - "message": "Pop out" + "message": "새 창에서 열기" }, "sendFileCalloutHeader": { "message": "시작하기 전에" @@ -2656,7 +2662,7 @@ "message": "받는 사람으로부터 나의 이메일 주소 숨기기" }, "hideYourEmail": { - "message": "Hide your email address from viewers." + "message": "사람들로부터 이메일 주소를 숨기세요." }, "sendOptionsPolicyInEffect": { "message": "하나 이상의 단체 정책이 Send 설정에 영향을 미치고 있습니다." @@ -2674,7 +2680,7 @@ "message": "이메일 인증 필요함" }, "emailVerifiedV2": { - "message": "Email verified" + "message": "이메일 인증됨" }, "emailVerificationRequiredDesc": { "message": "이 기능을 사용하려면 이메일 인증이 필요합니다. 웹 보관함에서 이메일을 인증할 수 있습니다." @@ -2689,10 +2695,10 @@ "message": "최근에 조직 관리자가 마스터 비밀번호를 변경했습니다. 보관함에 액세스하려면 지금 업데이트해야 합니다. 계속하면 현재 세션에서 로그아웃되며 다시 로그인해야 합니다. 다른 장치의 활성 세션은 최대 1시간 동안 계속 활성 상태로 유지될 수 있습니다." }, "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": "마스터 비밀번호가 조직 정책 중 하나 이상을 충족하지 못합니다. 보관함에 액세스하려면, 지금 마스터 비밀번호를 업데이트해야 합니다. 계속 진행하면 현재 세션에서 로그아웃되므로, 다시 로그인해야 합니다. 다른 장치에서 활성 세션은 최대 1시간 동안 계속 활성 상태로 유지될 수 있습니다." }, "tdeDisabledMasterPasswordRequired": { - "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + "message": "조직에서 신뢰할 수 있는 장치 암호화를 비활성화했습니다. 보관함에 접근하려면 마스터 비밀번호를 설정하세요." }, "resetPasswordPolicyAutoEnroll": { "message": "자동 등록" @@ -2708,15 +2714,15 @@ "description": "Used as a message within the notification bar when no folders are found" }, "orgPermissionsUpdatedMustSetPassword": { - "message": "Your organization permissions were updated, requiring you to set a master password.", + "message": "조직 권한이 업데이트되어 마스터 비밀번호를 설정해야 합니다.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "orgRequiresYouToSetPassword": { - "message": "Your organization requires you to set a master password.", + "message": "당신의 조직은 마스터 비밀번호를 설정해야 합니다.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "cardMetrics": { - "message": "out of $TOTAL$", + "message": "&TOTAL% 중에서", "placeholders": { "total": { "content": "$1", @@ -2735,7 +2741,7 @@ "message": "분" }, "vaultTimeoutPolicyAffectingOptions": { - "message": "Enterprise policy requirements have been applied to your timeout options" + "message": "타임아웃 옵션에 기업의 정책 요구 사항이 적용되었습니다" }, "vaultTimeoutPolicyInEffect": { "message": "조직 정책이 보관함 제한 시간에 영향을 미치고 있습니다. 최대 허용 보관함 제한 시간은 $HOURS$시간 $MINUTES$분입니다", @@ -2751,7 +2757,7 @@ } }, "vaultTimeoutPolicyInEffect1": { - "message": "$HOURS$ hour(s) and $MINUTES$ minute(s) maximum.", + "message": "최대 $HOURS$시간 $MINUTES$분", "placeholders": { "hours": { "content": "$1", @@ -2764,7 +2770,7 @@ } }, "vaultTimeoutPolicyMaximumError": { - "message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum", + "message": "타임아웃이 조직에서 설정한 제한을 초과합니다: 최대 $HOURS$시간 $MINUTES$분", "placeholders": { "hours": { "content": "$1", @@ -2777,7 +2783,7 @@ } }, "vaultTimeoutPolicyWithActionInEffect": { - "message": "Your organization policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s). Your vault timeout action is set to $ACTION$.", + "message": "조직 정책이 보관함 타임아웃에 영향을 미치고 있습니다. 최대 허용 보관함 타임아웃은 최대 $HOURS$시간 $MINUTES$분입니다. 보관함 타임아웃 작업은 $ACTION$으로 설정되어 있습니다.", "placeholders": { "hours": { "content": "$1", @@ -2794,7 +2800,7 @@ } }, "vaultTimeoutActionPolicyInEffect": { - "message": "Your organization policies have set your vault timeout action to $ACTION$.", + "message": "조직 정책에 따라 보관함 타임아웃 작업이 $ACTION$으로 설정되었습니다.", "placeholders": { "action": { "content": "$1", @@ -2803,7 +2809,7 @@ } }, "vaultTimeoutTooLarge": { - "message": "Your vault timeout exceeds the restrictions set by your organization." + "message": "보관함 시간 초과가 조직에서 설정한 제한을 초과합니다." }, "vaultExportDisabled": { "message": "보관함 내보내기 비활성화됨" @@ -2851,7 +2857,7 @@ "message": "개인 보관함을 내보내는 중" }, "exportingIndividualVaultDescription": { - "message": "Only the individual vault items associated with $EMAIL$ will be exported. Organization vault items will not be included. Only vault item information will be exported and will not include associated attachments.", + "message": "$EMAIL$과 관련된 개별 보관함 항목만 내보냅니다. 조직 보관함 항목은 포함되지 않습니다. 보관함 항목 정보만 내보내며 관련 첨부 파일은 포함되지 않습니다", "placeholders": { "email": { "content": "$1", @@ -2860,10 +2866,10 @@ } }, "exportingOrganizationVaultTitle": { - "message": "Exporting organization vault" + "message": "조직 보관함을 내보내는 중" }, "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "message": "$Organization$와 관련된 조직 보관함만 내보냅니다. 개별 보관함이나 다른 조직의 항목은 포함되지 않습니다", "placeholders": { "organization": { "content": "$1", @@ -2881,10 +2887,10 @@ "message": "아이디 생성" }, "generateEmail": { - "message": "Generate email" + "message": "이메일 생성" }, "spinboxBoundariesHint": { - "message": "Value must be between $MIN$ and $MAX$.", + "message": "값은 $MIN$과 $MAX$ 사이여야 합니다", "description": "Explains spin box minimum and maximum values to the user", "placeholders": { "min": { @@ -2898,7 +2904,7 @@ } }, "passwordLengthRecommendationHint": { - "message": " Use $RECOMMENDED$ characters or more to generate a strong password.", + "message": "강력한 비밀번호를 생성하려면 $RECORDENED$ 문자 이상을 사용하세요", "description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -2908,7 +2914,7 @@ } }, "passphraseNumWordsRecommendationHint": { - "message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.", + "message": "강력한 암호를 생성하려면 $RECORDENED$ 단어 이상을 사용하세요.", "description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -2921,17 +2927,17 @@ "message": "아이디 유형" }, "plusAddressedEmail": { - "message": "Plus addressed email", + "message": "추가 이메일", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" }, "plusAddressedEmailDesc": { - "message": "Use your email provider's sub-addressing capabilities." + "message": "이메일 제공업체의 하위 주소 지정 기능을 사용하세요." }, "catchallEmail": { - "message": "Catch-all email" + "message": "Catch-all 이메일 (도메인 상의 어떤 주소로도 전송된 이메일을 받을 수 있는 주소)" }, "catchallEmailDesc": { - "message": "Use your domain's configured catch-all inbox." + "message": "catch-all이 설정된 내 도메인의 메일함을 사용하세요." }, "random": { "message": "무작위" @@ -2955,18 +2961,18 @@ "message": "포워딩된 이메일 별칭" }, "forwardedEmailDesc": { - "message": "Generate an email alias with an external forwarding service." + "message": "외부 포워딩 서비스를 사용해서 이메일 주소 별칭을 만들어보세요." }, "forwarderDomainName": { - "message": "Email domain", + "message": "이메일 도메인", "description": "Labels the domain name email forwarder service option" }, "forwarderDomainNameHint": { - "message": "Choose a domain that is supported by the selected service", + "message": "선택한 서비스에서 지원하는 도메인 선택", "description": "Guidance provided for email forwarding services that support multiple email domains." }, "forwarderError": { - "message": "$SERVICENAME$ error: $ERRORMESSAGE$", + "message": "$SERVICENAME$ 오류: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", "placeholders": { "servicename": { @@ -2980,11 +2986,11 @@ } }, "forwarderGeneratedBy": { - "message": "Generated by Bitwarden.", + "message": "Bitwarden에서 생성됨", "description": "Displayed with the address on the forwarding service's configuration screen." }, "forwarderGeneratedByWithWebsite": { - "message": "Website: $WEBSITE$. Generated by Bitwarden.", + "message": "웹사이트: $WEBSITE$. Bitwarden에서 생성됨", "description": "Displayed with the address on the forwarding service's configuration screen.", "placeholders": { "WEBSITE": { @@ -2994,7 +3000,7 @@ } }, "forwaderInvalidToken": { - "message": "Invalid $SERVICENAME$ API token", + "message": "잘못된 $SERVICENAME$ API 토큰\n", "description": "Displayed when the user's API token is empty or rejected by the forwarding service.", "placeholders": { "servicename": { @@ -3004,7 +3010,7 @@ } }, "forwaderInvalidTokenWithMessage": { - "message": "Invalid $SERVICENAME$ API token: $ERRORMESSAGE$", + "message": "잘못된 $ServiceNAME$ API 토큰: $ERRORMESSAGE$", "description": "Displayed when the user's API token is rejected by the forwarding service with an error message.", "placeholders": { "servicename": { @@ -3018,7 +3024,7 @@ } }, "forwarderNoAccountId": { - "message": "Unable to obtain $SERVICENAME$ masked email account ID.", + "message": "$SERVICNAME$ 마스크된 이메일 계정 ID를 얻을 수 없습니다.", "description": "Displayed when the forwarding service fails to return an account ID.", "placeholders": { "servicename": { @@ -3028,7 +3034,7 @@ } }, "forwarderNoDomain": { - "message": "Invalid $SERVICENAME$ domain.", + "message": "잘못된 $SERVICNAME$ 도메인.", "description": "Displayed when the domain is empty or domain authorization failed at the forwarding service.", "placeholders": { "servicename": { @@ -3038,7 +3044,7 @@ } }, "forwarderNoUrl": { - "message": "Invalid $SERVICENAME$ url.", + "message": "잘못된 $SERVICNAME$ URL", "description": "Displayed when the url of the forwarding service wasn't supplied.", "placeholders": { "servicename": { @@ -3048,7 +3054,7 @@ } }, "forwarderUnknownError": { - "message": "Unknown $SERVICENAME$ error occurred.", + "message": "알 수 없는 $SERVICNAME$ 오류가 발생했습니다.", "description": "Displayed when the forwarding service failed due to an unknown error.", "placeholders": { "servicename": { @@ -3058,7 +3064,7 @@ } }, "forwarderUnknownForwarder": { - "message": "Unknown forwarder: '$SERVICENAME$'.", + "message": "알 수 없는 포워더: '$SERVICNAME$'.", "description": "Displayed when the forwarding service is not supported.", "placeholders": { "servicename": { @@ -3084,13 +3090,13 @@ "message": "프리미엄 구독이 필요합니다" }, "organizationIsDisabled": { - "message": "Organization suspended." + "message": "조직이 중지됨" }, "disabledOrganizationFilterError": { - "message": "Items in suspended Organizations cannot be accessed. Contact your Organization owner for assistance." + "message": "중단된 조직의 항목에 액세스할 수 없습니다. 조직 소유자에게 도움을 요청하세요." }, "loggingInTo": { - "message": "Logging in to $DOMAIN$", + "message": "$DOMAIN$(으)로 로그인", "placeholders": { "domain": { "content": "$1", @@ -3099,13 +3105,13 @@ } }, "settingsEdited": { - "message": "Settings have been edited" + "message": "설정이 편집되었습니다" }, "environmentEditedClick": { - "message": "Click here" + "message": "여기를 클릭하세요." }, "environmentEditedReset": { - "message": "to reset to pre-configured settings" + "message": "사전 구성된 설정으로 재설정하려면" }, "serverVersion": { "message": "서버 버전" @@ -3117,7 +3123,7 @@ "message": "제 3자" }, "thirdPartyServerMessage": { - "message": "Connected to third-party server implementation, $SERVERNAME$. Please verify bugs using the official server, or report them to the third-party server.", + "message": "제 3자 서버 구현에 연결되었습니다. $SERVERNAME$. 공식 서버를 사용하여 버그를 확인하거나 타사 서버에 보고해 주세요.", "placeholders": { "servername": { "content": "$1", @@ -3126,7 +3132,7 @@ } }, "lastSeenOn": { - "message": "last seen on: $DATE$", + "message": "확인된 날짜: $DATE$", "placeholders": { "date": { "content": "$1", @@ -3135,10 +3141,10 @@ } }, "loginWithMasterPassword": { - "message": "Log in with master password" + "message": "마스터 비밀번호로 로그인" }, "loggingInAs": { - "message": "Logging in as" + "message": "다음으로 로그인 중" }, "notYou": { "message": "본인이 아닌가요?" @@ -3150,67 +3156,67 @@ "message": "이메일 기억하기" }, "loginWithDevice": { - "message": "Log in with device" + "message": "기기로 로그인" }, "loginWithDeviceEnabledInfo": { - "message": "Log in with device must be set up in the settings of the Bitwarden app. Need another option?" + "message": "기기로 로그인하려면 Bitwarden 모바일 앱 설정에서 설정해야 합니다. 다른 방식이 필요하신가요?" }, "fingerprintPhraseHeader": { - "message": "Fingerprint phrase" + "message": "지문 구절" }, "fingerprintMatchInfo": { - "message": "Please make sure your vault is unlocked and the Fingerprint phrase matches on the other device." + "message": "반드시 보관함이 잠금 해제되었고, 지문 구절이 다른 기기에서 일치하는지 확인해주세요." }, "resendNotification": { - "message": "Resend notification" + "message": "알림 다시 보내기" }, "viewAllLogInOptions": { - "message": "View all log in options" + "message": "모든 로그인 방식 보기" }, "viewAllLoginOptions": { - "message": "View all log in options" + "message": "모든 로그인 방식 보기" }, "notificationSentDevice": { - "message": "A notification has been sent to your device." + "message": "기기에 알림이 전송되었습니다." }, "aNotificationWasSentToYourDevice": { - "message": "A notification was sent to your device" + "message": "기기에 알림이 전송되었습니다." }, "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" + "message": "반드시 계정이 잠금 해제되었고, 지문 구절이 다른 기기에서 일치하는지 확인해주세요." }, "youWillBeNotifiedOnceTheRequestIsApproved": { - "message": "You will be notified once the request is approved" + "message": "요청이 승인되면 알림을 받게 됩니다" }, "needAnotherOptionV1": { - "message": "Need another option?" + "message": "다른 옵션이 필요하신가요?" }, "loginInitiated": { - "message": "Login initiated" + "message": "로그인 시작" }, "exposedMasterPassword": { - "message": "Exposed Master Password" + "message": "노출된 마스터 비밀번호" }, "exposedMasterPasswordDesc": { - "message": "Password found in a data breach. Use a unique password to protect your account. Are you sure you want to use an exposed password?" + "message": "데이터 유출이 된 비밀번호임이 발견되었습니다. 계정을 보호하려면 고유한 비밀번호를 사용하세요. 노출된 비밀번호를 사용하시겠습니까?" }, "weakAndExposedMasterPassword": { - "message": "Weak and Exposed Master Password" + "message": "취약하고 노출된 마스터 비밀번호" }, "weakAndBreachedMasterPasswordDesc": { - "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" + "message": "데이터 유출이 된 약한 비밀번호임이 발견되었습니다. 계정을 보호하려면 강력하고 고유한 비밀번호를 사용하세요. 이 비밀번호를 사용하시겠습니까?" }, "checkForBreaches": { - "message": "Check known data breaches for this password" + "message": "이 비밀번호에 대한 알려진 데이터 유출 확인\n" }, "important": { - "message": "Important:" + "message": "중요:" }, "masterPasswordHint": { - "message": "Your master password cannot be recovered if you forget it!" + "message": "마스터 비밀번호를 잊어버리면 복구할 수 없습니다!\n" }, "characterMinimum": { - "message": "$LENGTH$ character minimum", + "message": "최소 $LENGTH$ 문자", "placeholders": { "length": { "content": "$1", @@ -3219,13 +3225,13 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Your organization policies have turned on autofill on page load." + "message": "조직 정책에 따라, 페이지 로드 시 자동 완성 기능을 켰습니다." }, "howToAutofill": { - "message": "How to autofill" + "message": "자동 완성 사용법" }, "autofillSelectInfoWithCommand": { - "message": "Select an item from this screen, use the shortcut $COMMAND$, or explore other options in settings.", + "message": "이 화면에서 항목을 선택하거나, 바로 가기 $COMMAND$를 사용하거나, 설정의 다른 옵션을 탐색하세요.", "placeholders": { "command": { "content": "$1", @@ -3234,31 +3240,31 @@ } }, "autofillSelectInfoWithoutCommand": { - "message": "Select an item from this screen, or explore other options in settings." + "message": "이 화면에서 항목을 선택하거나 설정의 다른 옵션을 탐색하세요." }, "gotIt": { - "message": "Got it" + "message": "이해했습니다" }, "autofillSettings": { "message": "자동 완성 설정" }, "autofillKeyboardShortcutSectionTitle": { - "message": "Autofill shortcut" + "message": "자동 완성 바로가기" }, "autofillKeyboardShortcutUpdateLabel": { - "message": "Change shortcut" + "message": "바로가기 변경" }, "autofillKeyboardManagerShortcutsLabel": { - "message": "Manage shortcuts" + "message": "바로가기 관리" }, "autofillShortcut": { "message": "자동 완성 키보드 단축키" }, "autofillLoginShortcutNotSet": { - "message": "The autofill login shortcut is not set. Change this in the browser's settings." + "message": "자동 채우기 로그인 바로 가기가 설정되어 있지 않습니다. 브라우저 설정에서 이 항목을 변경해주세요." }, "autofillLoginShortcutText": { - "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", + "message": "자동 채우기 로그인 바로 가기는 $COMMAND$입니다. 브라우저 설정의 모든 바로 가기를 관리하세요.", "placeholders": { "command": { "content": "$1", @@ -3267,7 +3273,7 @@ } }, "autofillShortcutTextSafari": { - "message": "Default autofill shortcut: $COMMAND$.", + "message": "기본 자동 완성 바로 가기: $COMMAND$.", "placeholders": { "command": { "content": "$1", @@ -3276,65 +3282,65 @@ } }, "opensInANewWindow": { - "message": "Opens in a new window" + "message": "새 창에서 열립니다" }, "rememberThisDeviceToMakeFutureLoginsSeamless": { - "message": "Remember this device to make future logins seamless" + "message": "향후 로그인을 원활하게 하기 위해 이 기기 기억하기" }, "deviceApprovalRequired": { - "message": "Device approval required. Select an approval option below:" + "message": "기기 승인이 필요합니다. 아래에서 승인 옵션을 선택하세요:" }, "deviceApprovalRequiredV2": { - "message": "Device approval required" + "message": "기기 승인이 필요합니다." }, "selectAnApprovalOptionBelow": { - "message": "Select an approval option below" + "message": "아래에서 승인 옵션을 선택하세요" }, "rememberThisDevice": { "message": "이 기기 기억하기" }, "uncheckIfPublicDevice": { - "message": "Uncheck if using a public device" + "message": "공용 기기를 사용하는 경우 체크 해제" }, "approveFromYourOtherDevice": { - "message": "Approve from your other device" + "message": "다른 장치에서 승인" }, "requestAdminApproval": { - "message": "관리자 승인 필요" + "message": "관리자 인증 필요" }, "approveWithMasterPassword": { - "message": "Approve with master password" + "message": "마스터 비밀번호로 승인" }, "ssoIdentifierRequired": { - "message": "Organization SSO identifier is required." + "message": "조직의 SSO 식별자가 필요합니다" }, "creatingAccountOn": { - "message": "Creating account on" + "message": "계정 만들기" }, "checkYourEmail": { - "message": "Check your email" + "message": "이메일을 확인해주세요" }, "followTheLinkInTheEmailSentTo": { - "message": "Follow the link in the email sent to" + "message": "이메일로 전송한 링크를 통해" }, "andContinueCreatingYourAccount": { - "message": "and continue creating your account." + "message": "계정을 계속 생성하세요." }, "noEmail": { - "message": "No email?" + "message": "이메일이 전송되지 않았나요?" }, "goBack": { - "message": "Go back" + "message": "뒤로 돌아가서" }, "toEditYourEmailAddress": { - "message": "to edit your email address." + "message": "이메일 주소를 수정하기" }, "eu": { "message": "EU", "description": "European Union" }, "accessDenied": { - "message": "Access denied. You do not have permission to view this page." + "message": "접근이 거부되었습니다. 이 페이지를 볼 권한이 없습니다." }, "general": { "message": "일반" @@ -3349,45 +3355,45 @@ "message": "관리자 승인 필요" }, "adminApprovalRequestSentToAdmins": { - "message": "Your request has been sent to your admin." + "message": "요청이 관리자에게 전송되었습니다." }, "youWillBeNotifiedOnceApproved": { - "message": "You will be notified once approved." + "message": "승인되면 알림을 받게 됩니다." }, "troubleLoggingIn": { - "message": "Trouble logging in?" + "message": "로그인에 문제가 있나요?" }, "loginApproved": { - "message": "Login approved" + "message": "로그인 승인됨" }, "userEmailMissing": { - "message": "User email missing" + "message": "사용자 이메일 누락" }, "activeUserEmailNotFoundLoggingYouOut": { - "message": "Active user email not found. Logging you out." + "message": "활성화된 사용자의 이메일을 찾을 수 없습니다. 로그아웃합니다." }, "deviceTrusted": { - "message": "Device trusted" + "message": "신뢰할 수 있는 장치" }, "sendsNoItemsTitle": { - "message": "No active Sends", + "message": "활성화된 Send없음", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendsNoItemsMessage": { - "message": "Use Send to securely share encrypted information with anyone.", + "message": "Send를 사용하여 암호화된 정보를 어느 사람과도 안전하게 공유합니다.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "inputRequired": { - "message": "Input is required." + "message": "입력이 필요합니다." }, "required": { - "message": "required" + "message": "필수" }, "search": { "message": "검색" }, "inputMinLength": { - "message": "Input must be at least $COUNT$ characters long.", + "message": "입력은 최소한 $COUNT$자 이상이어야 합니다.", "placeholders": { "count": { "content": "$1", @@ -3396,7 +3402,7 @@ } }, "inputMaxLength": { - "message": "Input must not exceed $COUNT$ characters in length.", + "message": "입력 길이는 $COUNT$자를 초과해서는 안 됩니다.", "placeholders": { "count": { "content": "$1", @@ -3405,7 +3411,7 @@ } }, "inputForbiddenCharacters": { - "message": "The following characters are not allowed: $CHARACTERS$", + "message": "다음 문자는 허용되지 않습니다: $CHARACTRS$", "placeholders": { "characters": { "content": "$1", @@ -3414,7 +3420,7 @@ } }, "inputMinValue": { - "message": "Input value must be at least $MIN$.", + "message": "입력 값은 최소 $MIN$자 이상이어야 합니다.", "placeholders": { "min": { "content": "$1", @@ -3423,7 +3429,7 @@ } }, "inputMaxValue": { - "message": "Input value must not exceed $MAX$.", + "message": "입력 값은 $MAX$ 자를 초과해서는 안 됩니다.", "placeholders": { "max": { "content": "$1", @@ -3435,14 +3441,14 @@ "message": "하나 이상의 이메일이 유효하지 않습니다." }, "inputTrimValidator": { - "message": "Input must not contain only whitespace.", + "message": "입력에는 공백만 포함해서는 안 됩니다.", "description": "Notification to inform the user that a form's input can't contain only whitespace." }, "inputEmail": { - "message": "Input is not an email address." + "message": "입력이 이메일 주소가 아닙니다" }, "fieldsNeedAttention": { - "message": "$COUNT$ field(s) above need your attention.", + "message": "위의 $COUNT$ 필드에 주의가 필요합니다", "placeholders": { "count": { "content": "$1", @@ -3451,10 +3457,10 @@ } }, "singleFieldNeedsAttention": { - "message": "1 field needs your attention." + "message": "1개의 필드가 주의가 필요합니다." }, "multipleFieldsNeedAttention": { - "message": "$COUNT$ fields need your attention.", + "message": "$COUNT$ 개의 필드가 주의가 필요합니다.", "placeholders": { "count": { "content": "$1", @@ -3463,22 +3469,22 @@ } }, "selectPlaceholder": { - "message": "-- Select --" + "message": "-- 선택 --" }, "multiSelectPlaceholder": { - "message": "-- Type to filter --" + "message": "- 필터링할 유형 --" }, "multiSelectLoading": { - "message": "Retrieving options..." + "message": "옵션을 검색하는 중..." }, "multiSelectNotFound": { - "message": "No items found" + "message": "항목을 찾을 수 없습니다" }, "multiSelectClearAll": { - "message": "Clear all" + "message": "모두 지우기" }, "plusNMore": { - "message": "+ $QUANTITY$ more", + "message": "+ $QUANITY$개 더보기", "placeholders": { "quantity": { "content": "$1", @@ -3487,30 +3493,30 @@ } }, "submenu": { - "message": "Submenu" + "message": "하위 메뉴" }, "toggleCollapse": { - "message": "Toggle collapse", + "message": "토글이 붕괴됨", "description": "Toggling an expand/collapse state." }, "filelessImport": { - "message": "Import your data to Bitwarden?", + "message": "데이터를 Bitwarden으로 가져오시겠습니까?", "description": "Default notification title for triggering a fileless import." }, "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", + "message": "LastPass 데이터를 보호하고 Bitwarden으로 가져오시겠습니까?", "description": "LastPass specific notification title for triggering a fileless import." }, "lpCancelFilelessImport": { - "message": "Save as unencrypted file", + "message": "암호화되지 않은 파일로 저장", "description": "LastPass specific notification button text for cancelling a fileless import." }, "startFilelessImport": { - "message": "Import to Bitwarden", + "message": "Bitwarden으로 가져오기", "description": "Notification button text for starting a fileless import." }, "importing": { - "message": "Importing...", + "message": "가져오는 중...", "description": "Notification message for when an import is in progress." }, "dataSuccessfullyImported": { @@ -3518,52 +3524,52 @@ "description": "Notification message for when an import has completed successfully." }, "dataImportFailed": { - "message": "Error importing. Check console for details.", + "message": "가져오는 중 오류가 발생했습니다. 자세한 내용은 콘솔을 확인하세요.", "description": "Notification message for when an import has failed." }, "importNetworkError": { - "message": "Network error encountered during import.", + "message": "가져오기 중에 네트워크 오류가 발생했습니다.", "description": "Notification message for when an import has failed due to a network error." }, "aliasDomain": { - "message": "Alias domain" + "message": "도메인 별칭" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Items with master password re-prompt cannot be autofilled on page load. Autofill on page load turned off.", + "message": "마스터 비밀번호 재 요청이 있는 항목은 페이지 로드에서 자동으로 채울 수 없습니다. 페이지 로드의 자동 완성이 꺼졌습니다.", "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Autofill on page load set to use default setting.", + "message": "페이지 로드 시 자동 완성이 기본 설정을 사용하도록 설정되었습니다.", "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { - "message": "Turn off master password re-prompt to edit this field", + "message": "마스터 암호 재 요청을 해제하여 이 필드를 편집합니다", "description": "Message appearing below the autofill on load message when master password reprompt is set for a vault item." }, "toggleSideNavigation": { - "message": "Toggle side navigation" + "message": "사이드 내비게이션 전환" }, "skipToContent": { - "message": "Skip to content" + "message": "콘텐츠로 건너뛰기" }, "bitwardenOverlayButton": { - "message": "Bitwarden autofill menu button", + "message": "Bitwarden 자동 완성 메뉴 버튼", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden autofill menu", + "message": "Bitwarden 자동 완성메뉴 전환", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden autofill menu", + "message": "Bitwarden 자동 완성 매뉴", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { - "message": "Unlock your account to view matching logins", + "message": "일치하는 로그인을 보기위해 계정을 잠금해제하세요", "description": "Text to display in overlay when the account is locked." }, "unlockYourAccountToViewAutofillSuggestions": { - "message": "Unlock your account to view autofill suggestions", + "message": "계정 잠금을 해제하여 자동 채우기 제안 보기", "description": "Text to display in overlay when the account is locked." }, "unlockAccount": { @@ -3571,19 +3577,19 @@ "description": "Button text to display in overlay when the account is locked." }, "unlockAccountAria": { - "message": "Unlock your account, opens in a new window", + "message": "계정 잠금을 해제하기, 새 창에서 열립니다", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, "fillCredentialsFor": { - "message": "Fill credentials for", + "message": "자격 증명 채우기", "description": "Screen reader text for when overlay item is in focused" }, "partialUsername": { - "message": "Partial username", + "message": "부분적인 사용자 이름", "description": "Screen reader text for when a login item is focused where a partial username is displayed. SR will announce this phrase before reading the text of the partial username" }, "noItemsToShow": { - "message": "No items to show", + "message": "표시할 항목 없음", "description": "Text to show in overlay if there are no matching items" }, "newItem": { @@ -3591,64 +3597,64 @@ "description": "Button text to display in overlay when there are no matching items" }, "addNewVaultItem": { - "message": "Add new vault item", + "message": "새 보관함 항목 추가", "description": "Screen reader text (aria-label) for new item button in overlay" }, "newLogin": { - "message": "New login", + "message": "새 로그인", "description": "Button text to display within inline menu when there are no matching items on a login field" }, "addNewLoginItemAria": { - "message": "Add new vault login item, opens in a new window", + "message": "새 보관함 로그인 항목 추가, 새 창에서 열립니다", "description": "Screen reader text (aria-label) for new login button within inline menu" }, "newCard": { - "message": "New card", + "message": "새 카드", "description": "Button text to display within inline menu when there are no matching items on a credit card field" }, "addNewCardItemAria": { - "message": "Add new vault card item, opens in a new window", + "message": "새 보관함 카드 항목 추가, 새 창에서 열립니다", "description": "Screen reader text (aria-label) for new card button within inline menu" }, "newIdentity": { - "message": "New identity", + "message": "신규 ID", "description": "Button text to display within inline menu when there are no matching items on an identity field" }, "addNewIdentityItemAria": { - "message": "Add new vault identity item, opens in a new window", + "message": "새 보관함 ID 항목 추가, 새 창에서 열립니다", "description": "Screen reader text (aria-label) for new identity button within inline menu" }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden autofill menu available. Press the down arrow key to select.", + "message": "Bitwarden 자동 완성 메뉴를 사용할 수 있습니다. 아래쪽 화살표 키를 눌러 선택하세요.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { - "message": "Turn on" + "message": "켜기" }, "ignore": { - "message": "Ignore" + "message": "무시하기" }, "importData": { - "message": "Import data", + "message": "데이터 가져오기", "description": "Used for the header of the import dialog, the import button and within the file-password-prompt" }, "importError": { - "message": "Import error" + "message": "가져오기 오류" }, "importErrorDesc": { - "message": "There was a problem with the data you tried to import. Please resolve the errors listed below in your source file and try again." + "message": "가져오려고 하는 데이터에 문제가 있습니다. 아래에 표시된 파일의 오류를 해결한 뒤 다시 시도해 주세요." }, "resolveTheErrorsBelowAndTryAgain": { - "message": "Resolve the errors below and try again." + "message": "아래 오류를 해결하고 다시 시도하세요." }, "description": { - "message": "Description" + "message": "설명" }, "importSuccess": { - "message": "Data successfully imported" + "message": "데이터 가져오기 성공" }, "importSuccessNumberOfItems": { - "message": "A total of $AMOUNT$ items were imported.", + "message": "가져온 항목의 총 개수", "placeholders": { "amount": { "content": "$1", @@ -3660,43 +3666,43 @@ "message": "다시 시도" }, "verificationRequiredForActionSetPinToContinue": { - "message": "Verification required for this action. Set a PIN to continue." + "message": "이 작업을 수행하려면 증명이 필요합니다. 계속하려면 PIN을 설정하세요." }, "setPin": { - "message": "Set PIN" + "message": "PIN 설정하기" }, "verifyWithBiometrics": { - "message": "Verify with biometrics" + "message": "생체 인식을 사용하여 증명하기" }, "awaitingConfirmation": { - "message": "Awaiting confirmation" + "message": "확인 대기 중" }, "couldNotCompleteBiometrics": { - "message": "Could not complete biometrics." + "message": "생체 인식을 완료할 수 없습니다." }, "needADifferentMethod": { - "message": "Need a different method?" + "message": "다른 방법이 필요하신가요?" }, "useMasterPassword": { - "message": "Use master password" + "message": "마스터 비밀번호를 사용하기" }, "usePin": { - "message": "Use PIN" + "message": "PIN 사용하기" }, "useBiometrics": { - "message": "Use biometrics" + "message": "생체 인식 사용하기" }, "enterVerificationCodeSentToEmail": { - "message": "Enter the verification code that was sent to your email." + "message": "이메일로 전송된 인증 코드를 입력해주세요" }, "resendCode": { - "message": "Resend code" + "message": "코드 재전송" }, "total": { - "message": "Total" + "message": "합계" }, "importWarning": { - "message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organization. Do you want to proceed?", + "message": "데이터를 $ORganization$로 가져오고 있습니다. 데이터를 이 조직의 구성원들과 공유할 수 있습니다. 계속 진행하시겠습니까?", "placeholders": { "organization": { "content": "$1", @@ -3705,19 +3711,19 @@ } }, "duoHealthCheckResultsInNullAuthUrlError": { - "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + "message": "Duo 서비스 연결 중 오류가 발생했습니다. 다른 2단계 로그인 방법을 사용하거나 Duo에 문의하여 도움을 받으세요." }, "launchDuoAndFollowStepsToFinishLoggingIn": { - "message": "Launch Duo and follow the steps to finish logging in." + "message": "듀오를 실행하고 단계를 따라 로그인을 완료하세요" }, "duoRequiredForAccount": { - "message": "Duo two-step login is required for your account." + "message": "계정에 Duo 2단계 로그인이 필요합니다." }, "popoutTheExtensionToCompleteLogin": { - "message": "Popout the extension to complete login." + "message": "확장 프로그램을 실행하여 로그인을 완료합니다." }, "popoutExtension": { - "message": "Popout extension" + "message": "확장 프로그램을 새 창에서 열기" }, "launchDuo": { "message": "Duo 실행" @@ -3729,25 +3735,25 @@ "message": "아무것도 가져오지 못했습니다." }, "importEncKeyError": { - "message": "Error decrypting the exported file. Your encryption key does not match the encryption key used export the data." + "message": "내보내려는 파일을 복호화하던 중 오류가 발생했습니다. 암호화 키가 내보내려는 데이터를 암호화한 키와 일치하지 않습니다." }, "invalidFilePassword": { - "message": "Invalid file password, please use the password you entered when you created the export file." + "message": "파일 비밀번호가 잘못되었습니다. 내보내기 파일을 만들 때 입력한 비밀번호를 사용해 주세요." }, "destination": { - "message": "Destination" + "message": "수신자" }, "learnAboutImportOptions": { - "message": "Learn about your import options" + "message": "가져오기 옵션 알아보기" }, "selectImportFolder": { - "message": "Select a folder" + "message": "폴더 선택" }, "selectImportCollection": { - "message": "Select a collection" + "message": "컬렉션 선택" }, "importTargetHint": { - "message": "Select this option if you want the imported file contents moved to a $DESTINATION$", + "message": "가져온 파일의 내용을 $DESTINATION$로 이동하려면 이 옵션을 선택하세요.", "description": "Located as a hint under the import target. Will be appended by either folder or collection, depending if the user is importing into an individual or an organizational vault.", "placeholders": { "destination": { @@ -3757,25 +3763,25 @@ } }, "importUnassignedItemsError": { - "message": "File contains unassigned items." + "message": "파일에 할당되지 않은 항목이 포함되어 있습니다." }, "selectFormat": { - "message": "Select the format of the import file" + "message": "불러올 파일의 포맷 선택" }, "selectImportFile": { - "message": "Select the import file" + "message": "불러올 파일 선택" }, "chooseFile": { - "message": "Choose File" + "message": "파일 선택" }, "noFileChosen": { - "message": "No file chosen" + "message": "선택된 파일 없음" }, "orCopyPasteFileContents": { - "message": "or copy/paste the import file contents" + "message": "또는 가져온 파일 내용 복사/붙여넣기" }, "instructionsFor": { - "message": "$NAME$ Instructions", + "message": "$NAME$ 지침", "description": "The title for the import tool instructions.", "placeholders": { "name": { @@ -3785,25 +3791,25 @@ } }, "confirmVaultImport": { - "message": "Confirm vault import" + "message": "보관함 가져오기 확인" }, "confirmVaultImportDesc": { - "message": "This file is password-protected. Please enter the file password to import data." + "message": "이 파일은 비밀번호로 보호받고 있습니다. 데이터를 가져오려면 파일 비밀번호를 입력하세요." }, "confirmFilePassword": { - "message": "Confirm file password" + "message": "파일 비밀번호 확인" }, "exportSuccess": { - "message": "Vault data exported" + "message": "보관함 데이터 내보내짐" }, "typePasskey": { "message": "패스키" }, "accessing": { - "message": "Accessing" + "message": "접근 중" }, "loggedInExclamation": { - "message": "Logged in!" + "message": "로그인 완료!" }, "passkeyNotCopied": { "message": "패스키가 복사되지 않습니다" @@ -3815,7 +3821,7 @@ "message": "사이트에서 인증을 요구합니다. 이 기능은 비밀번호가 없는 계정에서는 아직 지원하지 않습니다." }, "logInWithPasskeyQuestion": { - "message": "Log in with passkey?" + "message": "패스키로 로그인하시겠어요?" }, "passkeyAlreadyExists": { "message": "이미 이 애플리케이션에 해당하는 패스키가 있습니다." @@ -3824,16 +3830,16 @@ "message": "이 애플리케이션에 대한 패스키를 찾을 수 없습니다." }, "noMatchingPasskeyLogin": { - "message": "사이트와 일치하는 로그인이 없습니다." + "message": "이 사이트와 일치하는 로그인이 없습니다." }, "noMatchingLoginsForSite": { - "message": "No matching logins for this site" + "message": "사이트와 일치하는 로그인 없음" }, "searchSavePasskeyNewLogin": { - "message": "Search or save passkey as new login" + "message": "패스키를 새 로그인으로 검색 또는 저장" }, "confirm": { - "message": "Confirm" + "message": "확인" }, "savePasskey": { "message": "패스키 저장" @@ -3842,10 +3848,10 @@ "message": "새 로그인으로 패스키 저장" }, "chooseCipherForPasskeySave": { - "message": "Choose a login to save this passkey to" + "message": "패스키를 저장할 로그인 선택하기" }, "chooseCipherForPasskeyAuth": { - "message": "Choose a passkey to log in with" + "message": "로그인할 패스키 선택" }, "passkeyItem": { "message": "패스키 항목" @@ -3857,128 +3863,128 @@ "message": "이 항목은 이미 패스키가 있습니다. 정말로 현재 패스키를 덮어쓰시겠어요?" }, "featureNotSupported": { - "message": "Feature not yet supported" + "message": "아직 지원되지 않는 기능" }, "yourPasskeyIsLocked": { "message": "패스키를 사용하려면 인증이 필요합니다. 인증을 진행해주세요." }, "multifactorAuthenticationCancelled": { - "message": "Multifactor authentication cancelled" + "message": "멀티팩터 인증이 취소되었습니다" }, "noLastPassDataFound": { - "message": "No LastPass data found" + "message": "LastPass 데이터를 찾을 수 없습니다" }, "incorrectUsernameOrPassword": { - "message": "Incorrect username or password" + "message": "잘못된 사용자 이름 또는 비밀번호 입니다." }, "incorrectPassword": { - "message": "Incorrect password" + "message": "잘못된 비밀번호입니다" }, "incorrectCode": { - "message": "Incorrect code" + "message": "잘못된 코드입니다." }, "incorrectPin": { - "message": "Incorrect PIN" + "message": "올바르지 않은 PIN입니다." }, "multifactorAuthenticationFailed": { - "message": "Multifactor authentication failed" + "message": "멀티팩터 인증 실패" }, "includeSharedFolders": { - "message": "Include shared folders" + "message": "공유 폴더 포함" }, "lastPassEmail": { - "message": "LastPass Email" + "message": "LastPass 이메일" }, "importingYourAccount": { - "message": "Importing your account..." + "message": "계정 가져오기 중..." }, "lastPassMFARequired": { - "message": "LastPass multifactor authentication required" + "message": "LastPass 멀티팩터 인증 필요" }, "lastPassMFADesc": { - "message": "Enter your one-time passcode from your authentication app" + "message": "인증 앱에서 일회용 비밀번호 입력하기" }, "lastPassOOBDesc": { - "message": "Approve the login request in your authentication app or enter a one-time passcode." + "message": "인증 앱에서 로그인 요청을 승인하거나 일회용 비밀번호를 입력하세요" }, "passcode": { - "message": "Passcode" + "message": "비밀번호" }, "lastPassMasterPassword": { - "message": "LastPass master password" + "message": "LastPass 마스터 비밀번호" }, "lastPassAuthRequired": { - "message": "LastPass authentication required" + "message": "LastPass 인증 필요" }, "awaitingSSO": { - "message": "Awaiting SSO authentication" + "message": "SSO 인증 대기 중" }, "awaitingSSODesc": { - "message": "Please continue to log in using your company credentials." + "message": "회사 자격 증명을 사용하여 계속 로그인해 주세요." }, "seeDetailedInstructions": { - "message": "See detailed instructions on our help site at", + "message": "도움말 사이트에서 자세한 지침을 확인하세요", "description": "This is followed a by a hyperlink to the help website." }, "importDirectlyFromLastPass": { - "message": "Import directly from LastPass" + "message": "LastPass에서 직접 가져오기" }, "importFromCSV": { - "message": "Import from CSV" + "message": "CSV에서 가져오기" }, "lastPassTryAgainCheckEmail": { - "message": "Try again or look for an email from LastPass to verify it's you." + "message": "다시 시도하거나 LastPass에서 이메일을 찾아 사용자임을 증명하세요." }, "collection": { - "message": "Collection" + "message": "컬렉션" }, "lastPassYubikeyDesc": { - "message": "Insert the YubiKey associated with your LastPass account into your computer's USB port, then touch its button." + "message": "LastPass 계정과 연결된 YubiKey를 컴퓨터의 USB 포트에 삽입한 다음 버튼을 누릅니다." }, "switchAccount": { - "message": "Switch account" + "message": "계정 전환" }, "switchAccounts": { - "message": "Switch accounts" + "message": "계정 전환" }, "switchToAccount": { - "message": "Switch to account" + "message": "계정 전환" }, "activeAccount": { - "message": "Active account" + "message": "계정 활성화" }, "availableAccounts": { - "message": "Available accounts" + "message": "사용 가능한 계정" }, "accountLimitReached": { - "message": "Account limit reached. Log out of an account to add another." + "message": "계정 개수 제한에 도달했습니다. 추가로 로그인하려면 다른 계정을 로그아웃 해주세요." }, "active": { - "message": "active" + "message": "활성" }, "locked": { - "message": "locked" + "message": "잠김" }, "unlocked": { - "message": "unlocked" + "message": "잠금 해제됨" }, "server": { - "message": "server" + "message": "서버" }, "hostedAt": { - "message": "hosted at" + "message": "호스팅된" }, "useDeviceOrHardwareKey": { - "message": "Use your device or hardware key" + "message": "기기또는 하드웨어 키를 사용하세요" }, "justOnce": { - "message": "Just once" + "message": "한 번만 알림" }, "alwaysForThisSite": { - "message": "Always for this site" + "message": "항상 이 사이트에 대해" }, "domainAddedToExcludedDomains": { - "message": "$DOMAIN$ added to excluded domains.", + "message": "제외된 도메인에 $DOMAIN$이 추가되었습니다.", "placeholders": { "domain": { "content": "$1", @@ -3987,31 +3993,31 @@ } }, "commonImportFormats": { - "message": "Common formats", + "message": "일반적인 형식", "description": "Label indicating the most common import formats" }, "confirmContinueToBrowserSettingsTitle": { - "message": "Continue to browser settings?", + "message": "브라우저 설정으로 이동하시겠습니까?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" }, "confirmContinueToHelpCenter": { - "message": "Continue to Help Center?", + "message": "도움말 센터로 이동하시겠습니까?", "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" }, "confirmContinueToHelpCenterPasswordManagementContent": { - "message": "Change your browser's autofill and password management settings.", + "message": "브라우저의 자동 완성 및 비밀번호 관리 설정을 변경합니다.", "description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser password management settings" }, "confirmContinueToHelpCenterKeyboardShortcutsContent": { - "message": "You can view and set extension shortcuts in your browser's settings.", + "message": "브라우저 설정에서 확장 단축키를 보고, 설정할 수 있습니다.", "description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser keyboard shortcut settings" }, "confirmContinueToBrowserPasswordManagementSettingsContent": { - "message": "Change your browser's autofill and password management settings.", + "message": "브라우저의 자동 채우기 및 비밀번호 관리 설정을 변경합니다.", "description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page" }, "confirmContinueToBrowserKeyboardShortcutSettingsContent": { - "message": "You can view and set extension shortcuts in your browser's settings.", + "message": "브라우저 설정에서 확장 단축키를 보고, 설정할 수 있습니다.", "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" }, "overrideDefaultBrowserAutofillTitle": { @@ -4019,7 +4025,7 @@ "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", + "message": "이 옵션을 무시하면 Bitwarden 자동 완성 제안과 브라우저 간에 충돌이 발생할 수 있습니다", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -4027,39 +4033,39 @@ "description": "Label for the setting that allows overriding the default browser autofill settings" }, "privacyPermissionAdditionNotGrantedTitle": { - "message": "Unable to set Bitwarden as the default password manager", + "message": "Bitwarden을 기본 비밀번호 관리자로 설정할 수 없습니다", "description": "Title for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "privacyPermissionAdditionNotGrantedDescription": { - "message": "You must grant browser privacy permissions to Bitwarden to set it as the default password manager.", + "message": "기본 비밀번호 관리자로 설정하려면 Bitwarden에게 브라우저 개인정보 보호 권한을 부여해야 합니다.", "description": "Description for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "makeDefault": { - "message": "Make default", + "message": "기본값으로 만들기", "description": "Button text for the setting that allows overriding the default browser autofill settings" }, "saveCipherAttemptSuccess": { - "message": "Credentials saved successfully!", + "message": "자격 증명이 성공적으로 저장됨!", "description": "Notification message for when saving credentials has succeeded." }, "passwordSaved": { - "message": "Password saved!", + "message": "비밀번호 저장됨!", "description": "Notification message for when saving credentials has succeeded." }, "updateCipherAttemptSuccess": { - "message": "Credentials updated successfully!", + "message": "자격 증명이 성공적으로 업데이트됨!", "description": "Notification message for when updating credentials has succeeded." }, "passwordUpdated": { - "message": "Password updated!", + "message": "비밀번호 업데이트됨!", "description": "Notification message for when updating credentials has succeeded." }, "saveCipherAttemptFailed": { - "message": "Error saving credentials. Check console for details.", + "message": "자격 증명 저장 중 오류가 발생했습니다. 자세한 내용은 콘솔을 확인하세요.", "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "성공" }, "removePasskey": { "message": "패스키 제거" @@ -4068,22 +4074,22 @@ "message": "패스키 제거됨" }, "autofillSuggestions": { - "message": "Autofill suggestions" + "message": "자동 완성 제안" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to autofill" + "message": "이 사이트에서 자동으로 작성할 로그인 항목 저장" }, "yourVaultIsEmpty": { - "message": "Your vault is empty" + "message": "당신의 보관함이 비어있습니다" }, "noItemsMatchSearch": { - "message": "No items match your search" + "message": "사이트와 일치하는 항목 없음" }, "clearFiltersOrTryAnother": { - "message": "Clear filters or try another search term" + "message": "필터 지우기 또는 다른 검색어 시도" }, "copyInfoTitle": { - "message": "Copy info - $ITEMNAME$", + "message": "정보 복사 - $ITEMNAME$", "description": "Title for a button that opens a menu with options to copy information from an item.", "placeholders": { "itemname": { @@ -4093,7 +4099,7 @@ } }, "copyNoteTitle": { - "message": "Copy Note - $ITEMNAME$", + "message": "메모 복사 - $ITEMNAME$", "description": "Title for a button copies a note to the clipboard.", "placeholders": { "itemname": { @@ -4103,7 +4109,7 @@ } }, "moreOptionsLabel": { - "message": "More options, $ITEMNAME$", + "message": "$ITEMNAME$ 의 다른 옵션", "description": "Aria label for a button that opens a menu with more options for an item.", "placeholders": { "itemname": { @@ -4113,7 +4119,7 @@ } }, "moreOptionsTitle": { - "message": "More options - $ITEMNAME$", + "message": "다른 옵션 - $ITEMNAME$", "description": "Title for a button that opens a menu with more options for an item.", "placeholders": { "itemname": { @@ -4123,7 +4129,7 @@ } }, "viewItemTitle": { - "message": "View item - $ITEMNAME$", + "message": "항목 보기 - $ITEMNAME$", "description": "Title for a link that opens a view for an item.", "placeholders": { "itemname": { @@ -4133,7 +4139,7 @@ } }, "autofillTitle": { - "message": "Autofill - $ITEMNAME$", + "message": "자동 완성 - $ITEMNAME$", "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { @@ -4143,22 +4149,22 @@ } }, "noValuesToCopy": { - "message": "No values to copy" + "message": "복사할 값이 없습니다" }, "assignToCollections": { - "message": "Assign to collections" + "message": "컬렉션에 할당하기" }, "copyEmail": { - "message": "Copy email" + "message": "이메일 복사하기" }, "copyPhone": { - "message": "Copy phone" + "message": "전화번호 복사하기" }, "copyAddress": { - "message": "Copy address" + "message": "주소 복사하기" }, "adminConsole": { - "message": "Admin Console" + "message": "관리자 콘솔" }, "accountSecurity": { "message": "계정 보안" @@ -4170,13 +4176,13 @@ "message": "화면 스타일" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "대상 컬렉션을 할당하는 중 오류가 발생했습니다." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "대상 폴더를 할당하는 중 오류가 발생했습니다." }, "viewItemsIn": { - "message": "View items in $NAME$", + "message": "$NAME$에서 항목 보기", "description": "Button to view the contents of a folder or collection", "placeholders": { "name": { @@ -4186,7 +4192,7 @@ } }, "backTo": { - "message": "Back to $NAME$", + "message": "다시 $NAME$로 돌아가기", "description": "Navigate back to a previous folder or collection", "placeholders": { "name": { @@ -4196,10 +4202,10 @@ } }, "new": { - "message": "New" + "message": "새 항목" }, "removeItem": { - "message": "Remove $NAME$", + "message": "$NAME$ 제거", "description": "Remove a selected option, such as a folder or collection", "placeholders": { "name": { @@ -4209,16 +4215,16 @@ } }, "itemsWithNoFolder": { - "message": "Items with no folder" + "message": "폴더가 없는 항목" }, "itemDetails": { - "message": "Item details" + "message": "항목 세부사항" }, "itemName": { - "message": "Item name" + "message": "항목 이름" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "보기 권한만 있는 컬렉션은 제거할 수 없습니다: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -4227,47 +4233,47 @@ } }, "organizationIsDeactivated": { - "message": "Organization is deactivated" + "message": "조직이 비활성화되었습니다" }, "owner": { - "message": "Owner" + "message": "소유자" }, "selfOwnershipLabel": { - "message": "You", + "message": "당신", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { - "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." + "message": "비활성화된 조직의 항목에 액세스할 수 없습니다. 조직 소유자에게 도움을 요청하세요." }, "additionalInformation": { - "message": "Additional information" + "message": "추가 정보" }, "itemHistory": { - "message": "Item history" + "message": "항목 기록" }, "lastEdited": { - "message": "Last edited" + "message": "최근 수정 날짜:" }, "ownerYou": { - "message": "Owner: You" + "message": "소유자: 당신" }, "linked": { - "message": "Linked" + "message": "연결됨" }, "copySuccessful": { - "message": "Copy Successful" + "message": "복사 성공" }, "upload": { - "message": "Upload" + "message": "업로드" }, "addAttachment": { - "message": "Add attachment" + "message": "첨부파일 추가" }, "maxFileSizeSansPunctuation": { - "message": "Maximum file size is 500 MB" + "message": "최대 파일 크기는 500MB입니다." }, "deleteAttachmentName": { - "message": "Delete attachment $NAME$", + "message": "첨부파일 $NAME$ 삭제", "placeholders": { "name": { "content": "$1", @@ -4276,7 +4282,7 @@ } }, "downloadAttachmentName": { - "message": "Download $NAME$", + "message": "$NAME$ 다운로드", "placeholders": { "name": { "content": "$1", @@ -4285,25 +4291,25 @@ } }, "permanentlyDeleteAttachmentConfirmation": { - "message": "Are you sure you want to permanently delete this attachment?" + "message": "정말로 이 첨부파일을 영구적으로 삭제하시겠습니까?" }, "premium": { - "message": "Premium" + "message": "프리미엄" }, "freeOrgsCannotUseAttachments": { - "message": "Free organizations cannot use attachments" + "message": "무료 조직에서는 첨부 파일을 사용할 수 없습니다." }, "filters": { - "message": "Filters" + "message": "필터" }, "filterVault": { - "message": "Filter vault" + "message": "보관함 필터링" }, "filterApplied": { - "message": "One filter applied" + "message": "필터 1개가 적용되었습니다" }, "filterAppliedPlural": { - "message": "$COUNT$ filters applied", + "message": "$COUNT$개의 필터가 적용되었습니다", "placeholders": { "count": { "content": "$1", @@ -4312,16 +4318,16 @@ } }, "personalDetails": { - "message": "Personal details" + "message": "개인 정보" }, "identification": { - "message": "Identification" + "message": "본인 확인" }, "contactInfo": { - "message": "Contact info" + "message": "연락처 정보" }, "downloadAttachment": { - "message": "Download - $ITEMNAME$", + "message": "다운로드 - $ITEMNAME$", "placeholders": { "itemname": { "content": "$1", @@ -4330,23 +4336,23 @@ } }, "cardNumberEndsWith": { - "message": "card number ends with", + "message": "카드 번호는 다음으로 끝납니다", "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." }, "loginCredentials": { - "message": "Login credentials" + "message": "로그인 정보" }, "authenticatorKey": { - "message": "Authenticator key" + "message": "인증 키" }, "autofillOptions": { - "message": "Autofill options" + "message": "자동 완성 옵션" }, "websiteUri": { - "message": "Website (URI)" + "message": "웹사이트 (URI)" }, "websiteUriCount": { - "message": "Website (URI) $COUNT$", + "message": "웹사이트 (URI) $COUNT$", "description": "Label for an input field that contains a website URI. The input field is part of a list of fields, and the count indicates the position of the field in the list.", "placeholders": { "count": { @@ -4356,16 +4362,16 @@ } }, "websiteAdded": { - "message": "Website added" + "message": "웹사이트 추가됨" }, "addWebsite": { - "message": "Add website" + "message": "웹사이트 추가" }, "deleteWebsite": { - "message": "Delete website" + "message": "웹사이트 삭제" }, "defaultLabel": { - "message": "Default ($VALUE$)", + "message": "기본값 ($VALUE$)", "description": "A label that indicates the default value for a field with the current default value in parentheses.", "placeholders": { "value": { @@ -4375,7 +4381,7 @@ } }, "showMatchDetection": { - "message": "Show match detection $WEBSITE$", + "message": "$WEBSITE$ 일치 인식 보이기", "placeholders": { "website": { "content": "$1", @@ -4384,7 +4390,7 @@ } }, "hideMatchDetection": { - "message": "Hide match detection $WEBSITE$", + "message": "$WEBSITE$ 일치 인식 숨기기", "placeholders": { "website": { "content": "$1", @@ -4393,19 +4399,19 @@ } }, "autoFillOnPageLoad": { - "message": "Autofill on page load?" + "message": "페이지 로드 시 자동 완성을 할까요?" }, "cardExpiredTitle": { - "message": "Expired card" + "message": "만료된 카드" }, "cardExpiredMessage": { - "message": "If you've renewed it, update the card's information" + "message": "갱신한 경우, 카드 정보를 업데이트합니다" }, "cardDetails": { - "message": "Card details" + "message": "카드 상세정보" }, "cardBrandDetails": { - "message": "$BRAND$ details", + "message": "$BRAND$ 상세정보", "placeholders": { "brand": { "content": "$1", @@ -4417,40 +4423,40 @@ "message": "애니메이션 활성화" }, "showAnimations": { - "message": "Show animations" + "message": "애니메이션 표시" }, "addAccount": { - "message": "Add account" + "message": "계정 추가" }, "loading": { - "message": "Loading" + "message": "불러오는 중" }, "data": { - "message": "Data" + "message": "데이터" }, "passkeys": { - "message": "Passkeys", + "message": "패스키", "description": "A section header for a list of passkeys." }, "passwords": { - "message": "Passwords", + "message": "비밀번호", "description": "A section header for a list of passwords." }, "logInWithPasskeyAriaLabel": { - "message": "Log in with passkey", + "message": "패스키로 로그인", "description": "ARIA label for the inline menu button that logs in with a passkey." }, "assign": { - "message": "Assign" + "message": "할당" }, "bulkCollectionAssignmentDialogDescriptionSingular": { - "message": "Only organization members with access to these collections will be able to see the item." + "message": "이 컬렉션에 액세스할 수 있는 조직 구성원만 해당 항목을 볼 수 있습니다." }, "bulkCollectionAssignmentDialogDescriptionPlural": { - "message": "Only organization members with access to these collections will be able to see the items." + "message": "이 컬렉션에 액세스할 수 있는 조직 구성원만 해당 항목들을 볼 수 있습니다." }, "bulkCollectionAssignmentWarning": { - "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "message": "$TOTAL_COUNT$ 항목들을 선택했습니다. 편집 권한이 없기 때문에 항목들의 $READONLY_COUNT$를 업데이트할 수 없습니다.", "placeholders": { "total_count": { "content": "$1", @@ -4462,37 +4468,37 @@ } }, "addField": { - "message": "Add field" + "message": "필드 추가" }, "add": { - "message": "Add" + "message": "추가" }, "fieldType": { - "message": "Field type" + "message": "필드 유형" }, "fieldLabel": { - "message": "Field label" + "message": "필드 레이블" }, "textHelpText": { - "message": "Use text fields for data like security questions" + "message": "보안 질문과 같은 데이터에 텍스트 필드를 사용하세요" }, "hiddenHelpText": { - "message": "Use hidden fields for sensitive data like a password" + "message": "비밀번호와 같은 중요한 데이터의 경우 숨겨진 필드를 사용하세요." }, "checkBoxHelpText": { - "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + "message": "이메일 기억과 같이 양식의 체크박스를 자동으로 채우려면 체크박스들을 사용하세요" }, "linkedHelpText": { - "message": "Use a linked field when you are experiencing autofill issues for a specific website." + "message": "특정 웹사이트에 대한 자동 채우기 문제가 발생할 때는, 연결 필드를 사용하세요" }, "linkedLabelHelpText": { - "message": "Enter the the field's html id, name, aria-label, or placeholder." + "message": "필드의 html ID, 이름, aria-label 또는 플레이스홀더를 입력하세요" }, "editField": { - "message": "Edit field" + "message": "필드 편집" }, "editFieldLabel": { - "message": "Edit $LABEL$", + "message": "$LABEL$ 편집", "placeholders": { "label": { "content": "$1", @@ -4501,7 +4507,7 @@ } }, "deleteCustomField": { - "message": "Delete $LABEL$", + "message": "$LABEL$ 삭제", "placeholders": { "label": { "content": "$1", @@ -4510,7 +4516,7 @@ } }, "fieldAdded": { - "message": "$LABEL$ added", + "message": "$LABEL$ 추가됨", "placeholders": { "label": { "content": "$1", @@ -4519,7 +4525,7 @@ } }, "reorderToggleButton": { - "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "message": "$LABEL$을 재정렬합니다. 화살표 키를 사용하여 항목을 위나 아래로 이동할 수 있습니다.", "placeholders": { "label": { "content": "$1", @@ -4528,7 +4534,7 @@ } }, "reorderFieldUp": { - "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "message": "$LABEL$을 위로 이동했습니다. 위치: $INDEX$ / $LENGTH$", "placeholders": { "label": { "content": "$1", @@ -4545,13 +4551,13 @@ } }, "selectCollectionsToAssign": { - "message": "Select collections to assign" + "message": "할당할 컬렉션을 선택하세요" }, "personalItemTransferWarningSingular": { - "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + "message": "1개 항목이 선택한 조직으로 영구적으로 전송됩니다. 더 이상 이 항목을 소유하지 않습니다." }, "personalItemsTransferWarningPlural": { - "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "message": "$PERSONAL_ITEMS_COUNT$ 개 항목들이 선택한 조직으로 영구적으로 전송됩니다. 더 이상 이 항목들을 소유하지 않습니다.", "placeholders": { "personal_items_count": { "content": "$1", @@ -4560,7 +4566,7 @@ } }, "personalItemWithOrgTransferWarningSingular": { - "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "message": "1개 항목이 $ORG$으로 영구적으로 전송됩니다. 더 이상 이 항목을 소유하지 않습니다.", "placeholders": { "org": { "content": "$1", @@ -4569,7 +4575,7 @@ } }, "personalItemsWithOrgTransferWarningPlural": { - "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "message": "$PERSONAL_ITEMS_COUNT$ 개 항목들이 $ORG$으로 영구적으로 전송됩니다. 더 이상 이 항목들을 소유하지 않습니다.", "placeholders": { "personal_items_count": { "content": "$1", @@ -4582,13 +4588,13 @@ } }, "successfullyAssignedCollections": { - "message": "Successfully assigned collections" + "message": "성공적으로 컬렉션을 할당했습니다" }, "nothingSelected": { - "message": "You have not selected anything." + "message": "아무것도 선택하지 않았습니다." }, "movedItemsToOrg": { - "message": "Selected items moved to $ORGNAME$", + "message": "선택한 항목이 $ORGNAME$(으)로 이동됨", "placeholders": { "orgname": { "content": "$1", @@ -4597,7 +4603,7 @@ } }, "itemsMovedToOrg": { - "message": "Items moved to $ORGNAME$", + "message": "항목들이 $ORGNAME$로 이동했습니다", "placeholders": { "orgname": { "content": "$1", @@ -4606,7 +4612,7 @@ } }, "itemMovedToOrg": { - "message": "Item moved to $ORGNAME$", + "message": "항목이 $ORGNAME$로 이동했습니다", "placeholders": { "orgname": { "content": "$1", @@ -4615,7 +4621,7 @@ } }, "reorderFieldDown": { - "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "message": "$LABEL$을 아래로 이동했습니다. 위치: $INDEX$ / $LENGTH$", "placeholders": { "label": { "content": "$1", @@ -4632,49 +4638,49 @@ } }, "itemLocation": { - "message": "Item Location" + "message": "항목 위치" }, "fileSend": { - "message": "File Send" + "message": "파일 Send" }, "fileSends": { - "message": "File Sends" + "message": "파일 Send" }, "textSend": { - "message": "Text Send" + "message": "텍스트 Send" }, "textSends": { - "message": "Text Sends" + "message": "텍스트 Send" }, "bitwardenNewLook": { - "message": "Bitwarden has a new look!" + "message": "Bitwarden이 새로운 모습으로 돌아왔습니다!" }, "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" + "message": "보관함 탭에서 자동 완성하고 검색하는 것이 그 어느 때보다 쉽고 직관적입니다. 둘러보세요!" }, "accountActions": { - "message": "Account actions" + "message": "계정 작업" }, "showNumberOfAutofillSuggestions": { - "message": "Show number of login autofill suggestions on extension icon" + "message": "확장 아이콘에 로그인 자동 완성 제안 수 표시" }, "systemDefault": { - "message": "System default" + "message": "시스템 기본 설정" }, "enterprisePolicyRequirementsApplied": { - "message": "Enterprise policy requirements have been applied to this setting" + "message": "기업 정책에 따른 요구사항들이 옵션들에 적용되었습니다." }, "sshPrivateKey": { - "message": "Private key" + "message": "개인 키" }, "sshPublicKey": { - "message": "Public key" + "message": "공개 키" }, "sshFingerprint": { - "message": "Fingerprint" + "message": "지문" }, "sshKeyAlgorithm": { - "message": "Key type" + "message": "키 유형" }, "sshKeyAlgorithmED25519": { "message": "ED25519" @@ -4689,213 +4695,213 @@ "message": "RSA 4096-Bit" }, "retry": { - "message": "Retry" + "message": "재시도" }, "vaultCustomTimeoutMinimum": { - "message": "Minimum custom timeout is 1 minute." + "message": "최소 사용자 지정 시간 초과는 1분입니다." }, "additionalContentAvailable": { - "message": "Additional content is available" + "message": "추가 콘텐츠를 사용할 수 있습니다" }, "fileSavedToDevice": { - "message": "File saved to device. Manage from your device downloads." + "message": "파일을 장치에 저장했습니다. 장치 다운로드로 관리할 수 있습니다." }, "showCharacterCount": { - "message": "Show character count" + "message": "글자 수 표시하기" }, "hideCharacterCount": { - "message": "Hide character count" + "message": "글자 수 숨기기" }, "itemsInTrash": { - "message": "Items in trash" + "message": "휴지통에 있는 항목" }, "noItemsInTrash": { - "message": "No items in trash" + "message": "휴지통에 항목이 없습니다." }, "noItemsInTrashDesc": { - "message": "Items you delete will appear here and be permanently deleted after 30 days" + "message": "삭제한 항목은 여기에 표시되며 30일 후 영구적으로 삭제됩니다." }, "trashWarning": { - "message": "Items that have been in trash more than 30 days will automatically be deleted" + "message": "30일 이상 휴지통에 보관된 항목은 자동으로 삭제됩니다." }, "restore": { - "message": "Restore" + "message": "복원" }, "deleteForever": { - "message": "Delete forever" + "message": "영구 삭제하기" }, "noEditPermissions": { - "message": "You don't have permission to edit this item" + "message": "아이템을 수정할 권한이 없습니다." }, "authenticating": { - "message": "Authenticating" + "message": "인증 중" }, "fillGeneratedPassword": { - "message": "Fill generated password", + "message": "생성된 비밀번호를 입력하세요", "description": "Heading for the password generator within the inline menu" }, "passwordRegenerated": { - "message": "Password regenerated", + "message": "비밀번호가 재생성되었습니다.", "description": "Notification message for when a password has been regenerated" }, "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "message": "Bitwarden에 로그인을 저장하시겠습니까?", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { - "message": "Space", + "message": "스페이스", "description": "Represents the space key in screen reader content as a readable word" }, "tildeCharacterDescriptor": { - "message": "Tilde", + "message": "물결표(~)", "description": "Represents the ~ key in screen reader content as a readable word" }, "backtickCharacterDescriptor": { - "message": "Backtick", + "message": "백틱(`)", "description": "Represents the ` key in screen reader content as a readable word" }, "exclamationCharacterDescriptor": { - "message": "Exclamation mark", + "message": "느낌표 (!)", "description": "Represents the ! key in screen reader content as a readable word" }, "atSignCharacterDescriptor": { - "message": "At sign", + "message": "골뱅이표 (@)", "description": "Represents the @ key in screen reader content as a readable word" }, "hashSignCharacterDescriptor": { - "message": "Hash sign", + "message": "해시 기호 (#)", "description": "Represents the # key in screen reader content as a readable word" }, "dollarSignCharacterDescriptor": { - "message": "Dollar sign", + "message": "달러 기호 ($)", "description": "Represents the $ key in screen reader content as a readable word" }, "percentSignCharacterDescriptor": { - "message": "Percent sign", + "message": "퍼센트 기호 (%)", "description": "Represents the % key in screen reader content as a readable word" }, "caretCharacterDescriptor": { - "message": "Caret", + "message": "캐럿 기호 (^)", "description": "Represents the ^ key in screen reader content as a readable word" }, "ampersandCharacterDescriptor": { - "message": "Ampersand", + "message": "앰퍼샌드 기호 (&)", "description": "Represents the & key in screen reader content as a readable word" }, "asteriskCharacterDescriptor": { - "message": "Asterisk", + "message": "별표 (*)", "description": "Represents the * key in screen reader content as a readable word" }, "parenLeftCharacterDescriptor": { - "message": "Left parenthesis", + "message": "왼쪽 소괄호 ' ( '", "description": "Represents the ( key in screen reader content as a readable word" }, "parenRightCharacterDescriptor": { - "message": "Right parenthesis", + "message": "오른쪽 소괄호 ' ) '", "description": "Represents the ) key in screen reader content as a readable word" }, "hyphenCharacterDescriptor": { - "message": "Underscore", + "message": "밑줄( _ )", "description": "Represents the _ key in screen reader content as a readable word" }, "underscoreCharacterDescriptor": { - "message": "Hyphen", + "message": "붙임표 ( - )", "description": "Represents the - key in screen reader content as a readable word" }, "plusCharacterDescriptor": { - "message": "Plus", + "message": "더하기 기호 ( + )", "description": "Represents the + key in screen reader content as a readable word" }, "equalsCharacterDescriptor": { - "message": "Equals", + "message": "등호 ( = )", "description": "Represents the = key in screen reader content as a readable word" }, "braceLeftCharacterDescriptor": { - "message": "Left brace", + "message": "왼쪽 중괄호 ' { '", "description": "Represents the { key in screen reader content as a readable word" }, "braceRightCharacterDescriptor": { - "message": "Right brace", + "message": "오른쪽 중괄호 ' } '", "description": "Represents the } key in screen reader content as a readable word" }, "bracketLeftCharacterDescriptor": { - "message": "Left bracket", + "message": "왼쪽 대괄호 ' [ '", "description": "Represents the [ key in screen reader content as a readable word" }, "bracketRightCharacterDescriptor": { - "message": "Right bracket", + "message": "오른쪽 대괄호 ' ] '", "description": "Represents the ] key in screen reader content as a readable word" }, "pipeCharacterDescriptor": { - "message": "Pipe", + "message": "파이프 기호 ( | )", "description": "Represents the | key in screen reader content as a readable word" }, "backSlashCharacterDescriptor": { - "message": "Back slash", + "message": "백슬래시 ( \\ )", "description": "Represents the back slash key in screen reader content as a readable word" }, "colonCharacterDescriptor": { - "message": "Colon", + "message": "콜론 ( : )", "description": "Represents the : key in screen reader content as a readable word" }, "semicolonCharacterDescriptor": { - "message": "Semicolon", + "message": "세미콜론( ; )", "description": "Represents the ; key in screen reader content as a readable word" }, "doubleQuoteCharacterDescriptor": { - "message": "Double quote", + "message": "쌍 따옴표 ( \" )", "description": "Represents the double quote key in screen reader content as a readable word" }, "singleQuoteCharacterDescriptor": { - "message": "Single quote", + "message": "홑 따옴표 ( ' )", "description": "Represents the ' key in screen reader content as a readable word" }, "lessThanCharacterDescriptor": { - "message": "Less than", + "message": "보다 작음 ( < )", "description": "Represents the < key in screen reader content as a readable word" }, "greaterThanCharacterDescriptor": { - "message": "Greater than", + "message": "보다 큰 ( > )", "description": "Represents the > key in screen reader content as a readable word" }, "commaCharacterDescriptor": { - "message": "Comma", + "message": "쉼표( , )", "description": "Represents the , key in screen reader content as a readable word" }, "periodCharacterDescriptor": { - "message": "Period", + "message": "마침표 ( . )", "description": "Represents the . key in screen reader content as a readable word" }, "questionCharacterDescriptor": { - "message": "Question mark", + "message": "물음표 ( ? )", "description": "Represents the ? key in screen reader content as a readable word" }, "forwardSlashCharacterDescriptor": { - "message": "Forward slash", + "message": "슬래시 ( / )", "description": "Represents the / key in screen reader content as a readable word" }, "lowercaseAriaLabel": { - "message": "Lowercase" + "message": "소문자" }, "uppercaseAriaLabel": { - "message": "Uppercase" + "message": "대문자" }, "generatedPassword": { - "message": "Generated password" + "message": "비밀번호 생성" }, "compactMode": { - "message": "Compact mode" + "message": "컴팩트 모드\n" }, "beta": { - "message": "Beta" + "message": "베타" }, "extensionWidth": { - "message": "Extension width" + "message": "확장 폭" }, "wide": { - "message": "Wide" + "message": "넓게" }, "extraWide": { - "message": "Extra wide" + "message": "매우 넓게" } } diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 3318bd8df11..80c7de95fb2 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Įvesk 6 skaitmenų patvirtinimo kodą iš tavo autentifikavimo aplikacijos." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "Įvesk 6 skaitmenų prisijungimo kodą, kuris buvo išsiųstas $EMAIL$ el. paštu.", "placeholders": { diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index cb0f299dcb7..927fa6daf45 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Jāievada 6 ciparu apstiprinājuma kods no autentificētāja lietotnes." }, + "authenticationTimeout": { + "message": "Autentificēšanās noildze" + }, + "authenticationSessionTimedOut": { + "message": "Iestājās autentificēšanās sesijas noildze. Lūgums sākt pieteikšanos no jauna." + }, "enterVerificationCodeEmail": { "message": "Jāievada 6 ciparu apstiprinājuma kods, kas tika nosūtīts uz $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index f5a2e244fc5..79db10b1aba 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "നിങ്ങളുടെ ഓതന്റിക്കേറ്റർ അപ്ലിക്കേഷനിൽ നിന്ന് 6 അക്ക സ്ഥിരീകരണ കോഡ് നൽകുക." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "$EMAIL$ൽ ഇമെയിൽ ചെയ്ത 6 അക്ക സ്ഥിരീകരണ കോഡ് നൽകുക", "placeholders": { diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index bd7a1c755ec..21e8e657915 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Enter the 6 digit verification code from your authenticator app." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "Enter the 6 digit verification code that was emailed to $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index 6f26673abcd..29ef49db698 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Enter the 6 digit verification code from your authenticator app." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "Enter the 6 digit verification code that was emailed to $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 027923d0509..c6218e89556 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Skriv inn den 6-sifrede verifiseringskoden som står på din autentiseringsapp." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "Skriv inn den 6-sifrede verifiseringskoden som ble sendt til", "placeholders": { diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index 6f26673abcd..29ef49db698 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Enter the 6 digit verification code from your authenticator app." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "Enter the 6 digit verification code that was emailed to $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index 6ed66273305..f31970d889e 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Voer de 6-cijferige verificatiecode uit je authenticatie-app in." }, + "authenticationTimeout": { + "message": "Authenticatie-timeout" + }, + "authenticationSessionTimedOut": { + "message": "De verificatiesessie is verlopen. Start het inlogproces opnieuw op." + }, "enterVerificationCodeEmail": { "message": "Voer de 6-cijferige verificatiecode in die via e-mail is verstuurd naar $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index 6f26673abcd..29ef49db698 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Enter the 6 digit verification code from your authenticator app." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "Enter the 6 digit verification code that was emailed to $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index 6f26673abcd..29ef49db698 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Enter the 6 digit verification code from your authenticator app." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "Enter the 6 digit verification code that was emailed to $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index dc5b84eb6e5..46f8fde985b 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Wpisz 6-cyfrowy kod weryfikacyjny z aplikacji uwierzytelniającej." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "Wpisz 6-cyfrowy kod weryfikacyjny, który został przesłany na adres $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 79c87bbda09..4c7f45090c7 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Insira o código de verificação de 6 dígitos do seu aplicativo de autenticação." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "Insira o código de verificação de 6 dígitos que foi enviado por e-mail para $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 46d0fee21c4..7782c59eef7 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -1269,7 +1269,7 @@ "message": "Pode adquirir uma subscrição Premium no cofre web em bitwarden.com. Pretende visitar o site agora?" }, "premiumPurchaseAlertV2": { - "message": "Pode adquirir o Premium a partir das definições da sua conta na aplicação Web do Bitwarden." + "message": "Pode adquirir o Premium a partir das definições da sua conta na aplicação Web Bitwarden." }, "premiumCurrentMember": { "message": "É um membro Premium!" @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Introduza o código de verificação de 6 dígitos da sua aplicação de autenticação." }, + "authenticationTimeout": { + "message": "Tempo limite de autenticação" + }, + "authenticationSessionTimedOut": { + "message": "A sessão de autenticação expirou. Por favor, reinicie o processo de início de sessão." + }, "enterVerificationCodeEmail": { "message": "Introduza o código de verificação de 6 dígitos que foi enviado por e-mail para $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index c2f145087c5..cda8bedba62 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Introducere cod de verificare din 6 cifre din aplicația de autentificare." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "Introducere cod de verificare din 6 cifre care a fost trimis prin e-mail la $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index 7eb6d55cf5b..3a2e026ccda 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Введите 6-значный код подтверждения из вашего приложения-аутентификатора." }, + "authenticationTimeout": { + "message": "Таймаут аутентификации" + }, + "authenticationSessionTimedOut": { + "message": "Сеанс аутентификации завершился по времени. Пожалуйста, попробуйте войти еще раз." + }, "enterVerificationCodeEmail": { "message": "Введите 6-значный код подтверждения, который был отправлен на $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index 42be772294c..09d6911e8e9 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "ඔබගේ සත්යාපන යෙදුමෙන් 6 ඉලක්කම් සත්යාපන කේතය ඇතුළත් කරන්න." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "$EMAIL$වෙත ඊමේල් කරන ලද 6 ඉලක්කම් සත්යාපන කේතය ඇතුළත් කරන්න.", "placeholders": { diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index 9a720ea2891..5c8fa388f05 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Zadajte 6-miestny verifikačný kód z vašej overovacej aplikácie." }, + "authenticationTimeout": { + "message": "Časový limit overenia" + }, + "authenticationSessionTimedOut": { + "message": "Relácia overovania skončila. Znovu spustite proces prihlásenia." + }, "enterVerificationCodeEmail": { "message": "Zadajte 6-miestny verifikačný kód, ktorý vám bol zaslaný emailom", "placeholders": { diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index b5dadd4340f..c069146d72a 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Vnesite 6-mestno verifikacijsko kodo iz svoje aplikacije za avtentikacijo." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "Vnesite 6-mestno verifikacijsko kodo, ki vam je bila poslana na $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index e7759897a81..abf4dc835ef 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -1126,7 +1126,7 @@ "description": "WARNING (should stay in capitalized letters if the language permits)" }, "warningCapitalized": { - "message": "Warning", + "message": "Упозорење", "description": "Warning (should maintain locale-relevant capitalization)" }, "confirmVaultExport": { @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Унесите шестоцифрени верификациони код из апликације за утврђивање аутентичности." }, + "authenticationTimeout": { + "message": "Истекло је време аутентификације" + }, + "authenticationSessionTimedOut": { + "message": "Истекло је време сесије за аутентификацију. Молим вас покрените процес пријаве поново." + }, "enterVerificationCodeEmail": { "message": "Унесите шестоцифрени верификациони код који је послан на $EMAIL$.", "placeholders": { @@ -1768,7 +1774,7 @@ "message": "SSH кључ" }, "newItemHeader": { - "message": "New $TYPE$", + "message": "Нови $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1813,7 +1819,7 @@ "message": "Колекције" }, "nCollections": { - "message": "$COUNT$ collections", + "message": "$COUNT$ колекција", "placeholders": { "count": { "content": "$1", @@ -1843,7 +1849,7 @@ "message": "Сигурносне белешке" }, "sshKeys": { - "message": "SSH Keys" + "message": "SSH Кључеви" }, "clear": { "message": "Очисти", @@ -2571,7 +2577,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInHours": { - "message": "The Send will be available to anyone with the link for the next $HOURS$ hours.", + "message": "Send ће бити доступан свакоме са везом у наредних $HOURS$ часова.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "hours": { @@ -2585,7 +2591,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendExpiresInDays": { - "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "message": "Send ће бити доступан свакоме са везом у наредних $DAYS$ дана.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", "placeholders": { "days": { @@ -2884,7 +2890,7 @@ "message": "Генеришите имејл" }, "spinboxBoundariesHint": { - "message": "Value must be between $MIN$ and $MAX$.", + "message": "Вредност мора бити између $MIN$ и $MAX$.", "description": "Explains spin box minimum and maximum values to the user", "placeholders": { "min": { @@ -2898,7 +2904,7 @@ } }, "passwordLengthRecommendationHint": { - "message": " Use $RECOMMENDED$ characters or more to generate a strong password.", + "message": " Употребити $RECOMMENDED$ знакова или више да бисте генерисали јаку лозинку.", "description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -2908,7 +2914,7 @@ } }, "passphraseNumWordsRecommendationHint": { - "message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.", + "message": " Употребити $RECOMMENDED$ речи или више да бисте генерисали јаку приступну фразу.", "description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -3165,7 +3171,7 @@ "message": "Поново послати обавештење" }, "viewAllLogInOptions": { - "message": "View all log in options" + "message": "Погледајте сав извештај у опције" }, "viewAllLoginOptions": { "message": "Погледајте сав извештај у опције" @@ -3174,16 +3180,16 @@ "message": "Обавештење је послато на ваш уређај." }, "aNotificationWasSentToYourDevice": { - "message": "A notification was sent to your device" + "message": "Обавештење је послато на ваш уређај" }, "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" + "message": "Уверите се да је ваш налог откључан и да се фраза отиска подудара на другом уређају" }, "youWillBeNotifiedOnceTheRequestIsApproved": { - "message": "You will be notified once the request is approved" + "message": "Бићете обавештени када захтев буде одобрен" }, "needAnotherOptionV1": { - "message": "Need another option?" + "message": "Треба Вам друга опције?" }, "loginInitiated": { "message": "Пријава је покренута" @@ -3279,16 +3285,16 @@ "message": "Отвара се у новом прозору" }, "rememberThisDeviceToMakeFutureLoginsSeamless": { - "message": "Remember this device to make future logins seamless" + "message": "Запамтити овај уређај да би будуће пријаве биле беспрекорне" }, "deviceApprovalRequired": { "message": "Потребно је одобрење уређаја. Изаберите опцију одобрења испод:" }, "deviceApprovalRequiredV2": { - "message": "Device approval required" + "message": "Потребно је одобрење уређаја" }, "selectAnApprovalOptionBelow": { - "message": "Select an approval option below" + "message": "Изаберите опцију одобрења у наставку" }, "rememberThisDevice": { "message": "Запамти овај уређај" @@ -3364,7 +3370,7 @@ "message": "Недостаје имејл корисника" }, "activeUserEmailNotFoundLoggingYouOut": { - "message": "Active user email not found. Logging you out." + "message": "Имејл активног корисника није пронађен. Одјављивање." }, "deviceTrusted": { "message": "Уређај поуздан" @@ -3803,7 +3809,7 @@ "message": "Приступ" }, "loggedInExclamation": { - "message": "Logged in!" + "message": "Пријављено!" }, "passkeyNotCopied": { "message": "Приступни кључ неће бити копиран" @@ -4297,13 +4303,13 @@ "message": "Филтери" }, "filterVault": { - "message": "Filter vault" + "message": "Филтер сефа" }, "filterApplied": { - "message": "One filter applied" + "message": "Примењен је један филтер" }, "filterAppliedPlural": { - "message": "$COUNT$ filters applied", + "message": "Примењени су $COUNT$ филтера", "placeholders": { "count": { "content": "$1", @@ -4775,55 +4781,55 @@ "description": "Represents the % key in screen reader content as a readable word" }, "caretCharacterDescriptor": { - "message": "Caret", + "message": "Знак за уметање", "description": "Represents the ^ key in screen reader content as a readable word" }, "ampersandCharacterDescriptor": { - "message": "Ampersand", + "message": "Знак Ampersand", "description": "Represents the & key in screen reader content as a readable word" }, "asteriskCharacterDescriptor": { - "message": "Asterisk", + "message": "Знак звездица", "description": "Represents the * key in screen reader content as a readable word" }, "parenLeftCharacterDescriptor": { - "message": "Left parenthesis", + "message": "Отворена заграда", "description": "Represents the ( key in screen reader content as a readable word" }, "parenRightCharacterDescriptor": { - "message": "Right parenthesis", + "message": "Затворена заграда", "description": "Represents the ) key in screen reader content as a readable word" }, "hyphenCharacterDescriptor": { - "message": "Underscore", + "message": "Доња црта", "description": "Represents the _ key in screen reader content as a readable word" }, "underscoreCharacterDescriptor": { - "message": "Hyphen", + "message": "Цртица", "description": "Represents the - key in screen reader content as a readable word" }, "plusCharacterDescriptor": { - "message": "Plus", + "message": "Плус", "description": "Represents the + key in screen reader content as a readable word" }, "equalsCharacterDescriptor": { - "message": "Equals", + "message": "Једнако", "description": "Represents the = key in screen reader content as a readable word" }, "braceLeftCharacterDescriptor": { - "message": "Left brace", + "message": "Лева велика заграда", "description": "Represents the { key in screen reader content as a readable word" }, "braceRightCharacterDescriptor": { - "message": "Right brace", + "message": "Десна велика заграда", "description": "Represents the } key in screen reader content as a readable word" }, "bracketLeftCharacterDescriptor": { - "message": "Left bracket", + "message": "Лева заграда", "description": "Represents the [ key in screen reader content as a readable word" }, "bracketRightCharacterDescriptor": { - "message": "Right bracket", + "message": "Десна заграда", "description": "Represents the ] key in screen reader content as a readable word" }, "pipeCharacterDescriptor": { @@ -4831,71 +4837,71 @@ "description": "Represents the | key in screen reader content as a readable word" }, "backSlashCharacterDescriptor": { - "message": "Back slash", + "message": "Задња коса црта", "description": "Represents the back slash key in screen reader content as a readable word" }, "colonCharacterDescriptor": { - "message": "Colon", + "message": "Две тачке", "description": "Represents the : key in screen reader content as a readable word" }, "semicolonCharacterDescriptor": { - "message": "Semicolon", + "message": "Тачка-запета", "description": "Represents the ; key in screen reader content as a readable word" }, "doubleQuoteCharacterDescriptor": { - "message": "Double quote", + "message": "Двоструки наводници", "description": "Represents the double quote key in screen reader content as a readable word" }, "singleQuoteCharacterDescriptor": { - "message": "Single quote", + "message": "Један наводник", "description": "Represents the ' key in screen reader content as a readable word" }, "lessThanCharacterDescriptor": { - "message": "Less than", + "message": "Мање од", "description": "Represents the < key in screen reader content as a readable word" }, "greaterThanCharacterDescriptor": { - "message": "Greater than", + "message": "Веће од", "description": "Represents the > key in screen reader content as a readable word" }, "commaCharacterDescriptor": { - "message": "Comma", + "message": "Зарез", "description": "Represents the , key in screen reader content as a readable word" }, "periodCharacterDescriptor": { - "message": "Period", + "message": "Тачка", "description": "Represents the . key in screen reader content as a readable word" }, "questionCharacterDescriptor": { - "message": "Question mark", + "message": "Упитник", "description": "Represents the ? key in screen reader content as a readable word" }, "forwardSlashCharacterDescriptor": { - "message": "Forward slash", + "message": "Коса црта", "description": "Represents the / key in screen reader content as a readable word" }, "lowercaseAriaLabel": { - "message": "Lowercase" + "message": "Мала слова" }, "uppercaseAriaLabel": { - "message": "Uppercase" + "message": "Велика слова" }, "generatedPassword": { - "message": "Generated password" + "message": "Генерисана лозинка" }, "compactMode": { - "message": "Compact mode" + "message": "Компактни режим" }, "beta": { - "message": "Beta" + "message": "Бета" }, "extensionWidth": { - "message": "Extension width" + "message": "Ширина додатка" }, "wide": { - "message": "Wide" + "message": "Широко" }, "extraWide": { - "message": "Extra wide" + "message": "Врло широко" } } diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 2bbf1e133e7..0f35df9e96c 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Ange den 6-siffriga verifieringskoden från din autentiseringsapp." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "Ange den 6-siffriga verifieringskoden som skickades till $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index 6f26673abcd..29ef49db698 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Enter the 6 digit verification code from your authenticator app." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "Enter the 6 digit verification code that was emailed to $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 76d3e0cf540..60f629aee9b 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Enter the 6 digit verification code from your authenticator app." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "Enter the 6 digit verification code that was emailed to $EMAIL$.", "placeholders": { diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 1d54e9865b8..c4f183cfad5 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Kimlik doğrulama uygulamanızdaki 6 haneli doğrulama kodunu girin." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "$EMAIL$ adresine e-postayla gönderdiğimiz 6 haneli doğrulama kodunu girin.", "placeholders": { diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index 3ffc1f375fd..75d65f7a0fe 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -1126,7 +1126,7 @@ "description": "WARNING (should stay in capitalized letters if the language permits)" }, "warningCapitalized": { - "message": "Warning", + "message": "Попередження", "description": "Warning (should maintain locale-relevant capitalization)" }, "confirmVaultExport": { @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Введіть 6-значний код підтвердження з програми автентифікації." }, + "authenticationTimeout": { + "message": "Час очікування автентифікації" + }, + "authenticationSessionTimedOut": { + "message": "Час очікування сеансу автентифікації завершився. Перезапустіть процес входу в систему." + }, "enterVerificationCodeEmail": { "message": "Введіть 6-значний код підтвердження, надісланий на $EMAIL$.", "placeholders": { @@ -1843,7 +1849,7 @@ "message": "Захищені нотатки" }, "sshKeys": { - "message": "SSH Keys" + "message": "Ключі SSH" }, "clear": { "message": "Стерти", @@ -3279,16 +3285,16 @@ "message": "Відкривається у новому вікні" }, "rememberThisDeviceToMakeFutureLoginsSeamless": { - "message": "Remember this device to make future logins seamless" + "message": "Запам'ятайте цей пристрій, щоб спростити майбутні входи в систему" }, "deviceApprovalRequired": { "message": "Необхідне підтвердження пристрою. Виберіть варіант підтвердження нижче:" }, "deviceApprovalRequiredV2": { - "message": "Device approval required" + "message": "Потрібне підтвердження пристрою" }, "selectAnApprovalOptionBelow": { - "message": "Select an approval option below" + "message": "Виберіть варіант підтвердження нижче" }, "rememberThisDevice": { "message": "Запам'ятати цей пристрій" @@ -3364,7 +3370,7 @@ "message": "Немає адреси електронної пошти" }, "activeUserEmailNotFoundLoggingYouOut": { - "message": "Active user email not found. Logging you out." + "message": "Адресу е-пошти активного користувача не знайдено. Виконується вихід із системи." }, "deviceTrusted": { "message": "Довірений пристрій" @@ -3803,7 +3809,7 @@ "message": "Доступ" }, "loggedInExclamation": { - "message": "Logged in!" + "message": "Ви увійшли!" }, "passkeyNotCopied": { "message": "Ключ доступу не буде скопійовано" @@ -4890,12 +4896,12 @@ "message": "Бета" }, "extensionWidth": { - "message": "Extension width" + "message": "Ширина вікна розширення" }, "wide": { - "message": "Wide" + "message": "Широке" }, "extraWide": { - "message": "Extra wide" + "message": "Дуже широке" } } diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 88adbc3a53b..22a130c909e 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "Nhập mã xác nhận 6 chữ số từ ứng dụng xác thực của bạn." }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "Nhập mã xác nhận 6 chữ số đã được gửi tới email", "placeholders": { diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index 05b71990c1d..4890ded83bd 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "请输入您的验证器 App 中的 6 位数验证码。" }, + "authenticationTimeout": { + "message": "身份验证超时" + }, + "authenticationSessionTimedOut": { + "message": "身份验证会话超时。请重新启动登录过程。" + }, "enterVerificationCodeEmail": { "message": "请输入发送给电子邮件 $EMAIL$ 的 6 位数验证码。", "placeholders": { diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index fd71e2de563..60bdf76cabf 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -1319,6 +1319,12 @@ "enterVerificationCodeApp": { "message": "輸入驗證器應用程式提供的 6 位數驗證碼。" }, + "authenticationTimeout": { + "message": "Authentication timeout" + }, + "authenticationSessionTimedOut": { + "message": "The authentication session timed out. Please restart the login process." + }, "enterVerificationCodeEmail": { "message": "輸入已傳送至 $EMAIL$ 的 6 位數驗證碼。", "placeholders": { diff --git a/apps/browser/store/locales/id/copy.resx b/apps/browser/store/locales/id/copy.resx index b0791fa3b1f..2d64b4ca542 100644 --- a/apps/browser/store/locales/id/copy.resx +++ b/apps/browser/store/locales/id/copy.resx @@ -118,58 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden Password Manager + Pengelola Sandi Bitwarden - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + Di rumah, di kantor, atau di perjalanan, Bitwarden mengamankan semua kata sandi, kunci sandi, dan informasi sensitif Anda dengan mudah. - Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! + Dikenal sebagai pengelola sandi terbaik oleh PCMag, WIRED, The Verge, CNET, G2, dan lainnya! -SECURE YOUR DIGITAL LIFE -Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. +AMANKAN KEHIDUPAN DIGITAL ANDA +Amankan kehidupan digital Anda dan dapatkan perlindungan dari peretasan data dengan membuat dan menyimpan kata sandi yang unik dan kuat untuk setiap akun. Rawat semuanya dalam brankas kata sandi terenkripsi dari ujung-ke-ujung yang hanya Anda saja yang dapat mengakses. -ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE -Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. +AKSES DATA ANDA, DI MANA SAJA, KAPAN SAJA, DI PERANGKAT APAPUN +Kelola, simpan, amankan, dan bagikan tanpa batas dengan mudah kata sandi antar perangkat tak terbatas dan tanpa batasan. -EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE -Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. +SETIAP ORANG SEBAIKNYA MEMILIKI PERALATAN UNTUK TETAP AMAN KETIKA DARING +Gunakan Bitwarden secara gratis tanpa iklan atau menjual data. Bitwarden percaya setiap orang sebaiknya memiliki kemampuan untuk tetap aman ketika daring. Rencana premium menawarkan akses ke fitur-fitur yang lebih lanjut. -EMPOWER YOUR TEAMS WITH BITWARDEN -Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. +BERDAYAKAN TIM ANDA DENGAN BITWARDEN +Rencana untuk Teams dan Enterprise datang dengan kemampuan bisnis profesional. Beberapa contoh termasuk pemaduan SSO, hosting mandiri, pemaduan direktori dan pembekalan SCIM, kebijakan global, akses API, log kejadian, dan banyak lagi. -Use Bitwarden to secure your workforce and share sensitive information with colleagues. +Gunakan Bitwarden untuk mengamankan tenaga kerja Anda dan membagikan informasi sensitif kepada rekan kerja. -More reasons to choose Bitwarden: +Alasan lebih lanjut untuk memilih Bitwarden: -World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Enkripsi Kelas Dunia +Kata sandi dilindungi dengan enkripsi ujung-ke-ujung yang lebih lanjut (AES-256 bit, tanda pagar bergaram, dan PBKDF2 SHA-256) sehingga data Anda tetap aman dan privat. -3rd-party Audits -Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. +Audit Pihak Ketiga +Bitwarden secara rutin melakukan audit keamanan yang dilakukan pihak ketiga secara menyeluruh dengan perusahaan keamanan terkemuka. Audit tahunan ini termasuk penilaian sumber kode dan pengujian penembusan antar IP, server, dan aplikasi web Bitwarden. -Advanced 2FA -Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. +2FA Terdepan +Amankan login Anda dengan pengotentikasi pihak ketiga, kode yang dikirim ke surel, atau pengenal WebAuthn FIDO2 seperti kunci keamanan perangkat keras atau kunci sandi. Bitwarden Send -Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. +Salurkan data ke orang lain secara langsung sembari menjaga keamanan dari ujung-ke-ujung dan membatasi paparan. -Built-in Generator -Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. +Pembuat Bawaan +Buat kata sandi yang panjang, rumit, dan beda serta nama pengguna unik untuk setiap situs yang Anda kunjungi. Padukan dengan nama lain surel untuk privasi lebih lanjut. -Global Translations -Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. +Terjemahan Global +Terjemahan Bitwarden hadir dalam lebih dari 60 bahasa, diterjemahkan oleh komunitas global melalui Crowdin. -Cross-Platform Applications -Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. +Aplikasi Lintas Platform +Amankan dan bagikan data sensitif dalam Brankas Bitwarden Anda dari sebarang peramban, ponsel, atau sistem operasi desktop, dan lebih banyak lagi. -Bitwarden secures more than just passwords -End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! +Bitwarden mengamankan lebih dari sekedar kata sandi +Solusi pengelolaan pengenal terenkripsi ujung-ke-ujung dari Bitwarden memberdayakan organisasi untuk mengamankan segalanya, termasuk rahasia pengembang dan pengalaman kunci sandi. Kunjungi bitwarden.com untuk mempelajari lebih lanjut tentang Pengelola Rahasia Bitwarden dan passwordless.dev Bitwarden! - At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. + Di rumah, di kantor, atau di perjalanan, Bitwarden mengamankan semua kata sandi, kunci sandi, dan informasi sensitif Anda dengan mudah. Sinkronkan dan akses brankas Anda dari beberapa perangkat diff --git a/apps/browser/store/locales/ko/copy.resx b/apps/browser/store/locales/ko/copy.resx index a2fc4e19858..de6ef1c370d 100644 --- a/apps/browser/store/locales/ko/copy.resx +++ b/apps/browser/store/locales/ko/copy.resx @@ -124,48 +124,50 @@ 집에서도, 직장에서도, 이동 중에도 Bitwarden은 비밀번호, 패스키, 민감 정보를 쉽게 보호합니다. - Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! + PCMag, WIRED, The Verge, CNET, G2 등에서 최고의 비밀번호 관리자 선정! -SECURE YOUR DIGITAL LIFE -Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. +디지털 라이프를 안전하게 보호하세요 +모든 계정을 위한 강력하고 고유한 비밀번호를 생성하고 저장하여, 데이터 유출로부터 안전하게 보호하세요. 오직 사용자만 접근할 수 있는 엔드투엔드 방식으로 암호화된 비밀번호 보관함에서 모든 것을 관리하세요. -ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE -Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. +어디서든, 언제든, 어떤 기기에서든 접근 가능 +무제한의 비밀번호들을 관리, 저장, 보호, 공유하며 무제한의 기기에서 손쉽게 이용하세요. -EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE -Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. +모두가 온라인 안전을 위한 도구를 가져야 합니다. +광고나 데이터 판매 없이 Bitwarden을 무료로 이용하세요. Bitwarden은 모두가 안전한 온라인 환경을 누릴 권리가 있다고 믿습니다. 프리미엄 플랜을 통해 고급 기능도 이용 가능합니다. -EMPOWER YOUR TEAMS WITH BITWARDEN -Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. +Bitwarden으로 팀을 강화하세요 +팀 및 사업용 플랜은 전문 비즈니스 기능을 제공합니다. 예를 들어 SSO 통합, 자체 호스팅, 디렉토리 통합 및 SCIM 프로비저닝, 글로벌 정책, API 접근, 이벤트 로그 등을 포함합니다. +(SSO: 1회 사용자 인증으로 다수의 앱, 웹에 접근, 인증할 수 있는 통합 로그인 솔루션) -Use Bitwarden to secure your workforce and share sensitive information with colleagues. +Bitwarden을 사용하여 직원을 보호하고 동료들과 민감한 정보를 안전하게 공유하세요. -More reasons to choose Bitwarden: +Bitwarden을 선택해야 하는 이유 -World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +세계적 수준의 암호화 +고급 엔드 투 엔드 암호화(AES-256 비트, 솔팅된 해시 태그, PBKDF2 SHA-256)로 비밀번호를 보호하여 데이터의 보안과 개인 정보를 유지합니다. -3rd-party Audits -Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. +제3자를 통한 보안 감사 +Bitwarden은 저명한 보안 회사와 함께 정기적인 제3자 보안 감사를 수행합니다. 연례 감사에는 소스 코드 평가와 Bitwarden IP, 서버, 웹 애플리케이션에 대한 침투 테스트가 포함됩니다. -Advanced 2FA -Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. +고급 2단계 인증(2FA) +타사 인증 앱, 이메일 코드 또는 하드웨어 보안 키나 패스키와 같은 FIDO2 WebAuthn 자격 증명을 통해 로그인 보안을 강화하세요. -Bitwarden Send -Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. +Bitwarden의 데이터 전송 방식 +엔드투엔드 암호화를 유지하면서 데이터를 직접 다른 사람에게 전송하여 노출을 최소화합니다. -Built-in Generator -Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. +내장된 비밀번호 및 사용자 이름 생성자 +긴, 복잡하고 고유한 비밀번호와 각 사이트에 사용할 고유 사용자 이름을 생성하세요. 이메일명 제공업체와 통합하여 추가적인 개인 정보 보호를 제공합니다. -Global Translations -Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. +글로벌 번역 +Bitwarden은 Crowdin 글로벌 커뮤니티를 통해 한국어를 포함한 60개 이상의 언어로 번역되어 있습니다. -Cross-Platform Applications -Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. +크로스 플랫폼 애플리케이션 +모든 브라우저, 모바일 기기, 데스크톱 OS 등 다양한 환경에서 Bitwarden 보관함 내의 중요한 데이터를 안전하게 관리하고 공유하세요. -Bitwarden secures more than just passwords -End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! +비밀번호 이상의 보안을 제공합니다 +Bitwarden의 엔드투엔드 암호화된 신용 증명 관리 솔루션은 개발자 비밀과 패스키 경험을 포함한 모든 것을 보호하도록 조직을 지원합니다. +Bitwarden 의 Secrets Manager 및Passwordless.dev에 대해 자세히 알아보려면 Bitwarden.com 을 방문하세요! diff --git a/apps/browser/store/locales/sr/copy.resx b/apps/browser/store/locales/sr/copy.resx index a7657997a92..2e737523856 100644 --- a/apps/browser/store/locales/sr/copy.resx +++ b/apps/browser/store/locales/sr/copy.resx @@ -124,48 +124,48 @@ Било где, Bitwarden лако обезбеђује све ваше лозинке, приступне кључеве и осетљиве информације. - Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! + Препознат као најбољи руковалац лозинкама од стране PCMag, WIRED, The Verge, CNET, G2, и других! -SECURE YOUR DIGITAL LIFE -Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. +ОСИГУРАЈТЕ ВАШ ДИГИТАЛНИ ЖИВОТ +Осигурајте ваш дигитални живот и заштитите се против цурења података генерисањем и чувањем јединствених, јаких лозинки за сваки ваш налог. Држите све унутар потпуно екнриптованог трезора којем само ви имате приступ. -ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE -Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. +ПРИСТУПИТЕ ВАШИМ ПОДАЦИМА, БИЛО ГДЕ, НА БИЛО КОМ УРЕЂАЈУ +Са лакоћом управљајте, складиштите, штитите и делите неограничен број лозинки на неограниченом броју уређаја без органичења -EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE -Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. +СВАКО БИ ТРЕБАО ДА ИМА АЛАТЕ ДА БИ ОСТАО БЕЗБЕДАН НА МРЕЖИ +Користите Bitwarden бесплатно без реклама или продаје података. Bitwarden верује да свако треба да има способност да остане безбедан на мрежи. Премијум планови нуде приступ напредним могућностима. -EMPOWER YOUR TEAMS WITH BITWARDEN -Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. +ОСНАЖИТЕ ВАШЕ ТИМОВЕ СА BITWARDEN-ОМ +Планови за тимове и пословно окружење долазе са професионалним пословним могућностима. Неки од примера укључују SSO интеграцију, самостално хостовање, интеграцију са директоријумом и SCIM провизионисање, глобалне полисе, API приступ, записе догађаја и још више. -Use Bitwarden to secure your workforce and share sensitive information with colleagues. +Користите Bitwarden да би сте обезбедили вашу радну снагу и делили осетљиве информације са колегама. -More reasons to choose Bitwarden: +Додатни разлози да изаберете Bitwarden: -World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +Енкрипција светске класе +Лозинке су заштићене са напредном потпуном енкрипцијом (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) тако да ваши подаци остају сигурни и приватни. -3rd-party Audits -Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. +Ревизије треће стране +Bitwarden редовно спроводи опсежне безбедносне ревизије трећих страна заједно са препознатим безбедносним фирмама. Ове годишње ревизије укључују процене изворног кода и тестирање пробојности Bitwarden-ових ИП адреса, сервера и веб апликација. -Advanced 2FA -Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. +Напредна двофакторска аутентификација +Обезбедите ваше пријаве са аутентификатором треће стране, кодовима послатим на е-пошту, или FIDO2 WebAuthn акредитивима као што су хардверски кључ или фраза. Bitwarden Send -Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. +Одашиљите податке директно другима док одржавате потпуну енкриптовану безбедност и ограничавате изалагање. -Built-in Generator -Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. +Уграђени генератор +Правите дугачке, комплексне и посебне лозинке и јединствена корисничка имена за сваки сајт који посећујете. Интегришите са провајдерима алијаса е-пошти за додатну приватност. -Global Translations -Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. +Глобална превођења +Bitwarden преводи постоје за више од 60 језика, преведени од стране глобалне заједнице уз помоћ Crowdin-а. -Cross-Platform Applications -Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. +Апликације за разне платформе +Обезбедите и делите осетљиве податке у оквиру Bitwarden трезора из било којег прегледача, мобилног уређаја или оперативног система и више. -Bitwarden secures more than just passwords -End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! +Bitwarden обезбеђује више од обичних лозинки +Потпуно енкриптовано решење за управљање акредитивима од Bitwarden-а оснажава организације да обезбеде све, укључујући тајне девелопера и фразе. Посетите Bitwarden.com да би сте сазнали више о Bitwarden руководиоцу тајнама и Bitwarden Passwordless.dev! From 7c8b9db58f15841c07436505d412ec65646b9cde Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Thu, 12 Dec 2024 12:22:55 -0500 Subject: [PATCH 09/10] Revert workflow changes (#12376) * Revert "fix: target workflows not triggering on pull_request_target (#12370)" This reverts commit 645d36f465fd585cadd95c82595cea6a5d1027cd. * Revert "[PM-15126] Tighten scope of our client build pipelines to remove reliance on secrets (#12243)" This reverts commit f8c33ea04be4052a383c6f1ce84bca6ff3a07256. --- .github/CODEOWNERS | 4 -- .github/workflows/build-browser-target.yml | 39 ------------- .github/workflows/build-browser.yml | 18 +++--- .github/workflows/build-cli-target.yml | 39 ------------- .github/workflows/build-cli.yml | 27 ++++----- .github/workflows/build-desktop-target.yml | 38 ------------ .github/workflows/build-desktop.yml | 68 +++++----------------- .github/workflows/build-web-target.yml | 41 ------------- .github/workflows/build-web.yml | 28 +++------ .github/workflows/lint.yml | 10 +--- 10 files changed, 43 insertions(+), 269 deletions(-) delete mode 100644 .github/workflows/build-browser-target.yml delete mode 100644 .github/workflows/build-cli-target.yml delete mode 100644 .github/workflows/build-desktop-target.yml delete mode 100644 .github/workflows/build-web-target.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e9360c73ab9..99bea676bfb 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -85,13 +85,9 @@ apps/web/src/app/shared @bitwarden/team-platform-dev apps/web/src/translation-constants.ts @bitwarden/team-platform-dev # Workflows .github/workflows/brew-bump-desktop.yml @bitwarden/team-platform-dev -.github/workflows/build-browser-target.yml @bitwarden/team-platform-dev .github/workflows/build-browser.yml @bitwarden/team-platform-dev -.github/workflows/build-cli-target.yml @bitwarden/team-platform-dev .github/workflows/build-cli.yml @bitwarden/team-platform-dev -.github/workflows/build-desktop-target.yml @bitwarden/team-platform-dev .github/workflows/build-desktop.yml @bitwarden/team-platform-dev -.github/workflows/build-web-target.yml @bitwarden/team-platform-dev .github/workflows/build-web.yml @bitwarden/team-platform-dev .github/workflows/chromatic.yml @bitwarden/team-platform-dev .github/workflows/lint.yml @bitwarden/team-platform-dev diff --git a/.github/workflows/build-browser-target.yml b/.github/workflows/build-browser-target.yml deleted file mode 100644 index 12a08cf50a3..00000000000 --- a/.github/workflows/build-browser-target.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Build Browser on PR Target - -on: - pull_request_target: - types: [opened, synchronize] - branches-ignore: - - 'l10n_master' - - 'cf-pages' - paths: - - 'apps/browser/**' - - 'libs/**' - - '*' - - '!*.md' - - '!*.txt' - workflow_call: - inputs: {} - workflow_dispatch: - inputs: - sdk_branch: - description: "Custom SDK branch" - required: false - type: string - -defaults: - run: - shell: bash - -jobs: - check-run: - name: Check PR run - uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main - - run-workflow: - name: Run Build Browser on PR Target - needs: check-run - if: ${{ github.event.pull_request.head.repo.full_name != github.repository }} - uses: ./.github/workflows/build-browser.yml - secrets: inherit - diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index 56a980bf0f9..7740e418e7b 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -1,7 +1,7 @@ name: Build Browser on: - pull_request: + pull_request_target: types: [opened, synchronize] branches-ignore: - 'l10n_master' @@ -38,14 +38,19 @@ defaults: shell: bash jobs: + check-run: + name: Check PR run + uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main + setup: name: Setup runs-on: ubuntu-22.04 + needs: + - check-run outputs: repo_url: ${{ steps.gen_vars.outputs.repo_url }} adj_build_number: ${{ steps.gen_vars.outputs.adj_build_number }} node_version: ${{ steps.retrieve-node-version.outputs.node_version }} - has_secrets: ${{ steps.check-secrets.outputs.has_secrets }} steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -69,14 +74,6 @@ jobs: NODE_VERSION=${NODE_NVMRC/v/''} echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT - - name: Check secrets - id: check-secrets - env: - AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - run: | - has_secrets=${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL != '' }} - echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT - locales-test: name: Locales Test @@ -284,7 +281,6 @@ jobs: needs: - setup - locales-test - if: ${{ needs.setup.outputs.has_secrets == 'true' }} env: _BUILD_NUMBER: ${{ needs.setup.outputs.adj_build_number }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} diff --git a/.github/workflows/build-cli-target.yml b/.github/workflows/build-cli-target.yml deleted file mode 100644 index 89f8b63b525..00000000000 --- a/.github/workflows/build-cli-target.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Build CLI on PR Target - -on: - pull_request_target: - types: [opened, synchronize] - branches-ignore: - - 'l10n_master' - - 'cf-pages' - paths: - - 'apps/cli/**' - - 'libs/**' - - '*' - - '!*.md' - - '!*.txt' - - '.github/workflows/build-cli.yml' - - 'bitwarden_license/bit-cli/**' - workflow_dispatch: - inputs: - sdk_branch: - description: "Custom SDK branch" - required: false - type: string - -defaults: - run: - shell: bash - -jobs: - check-run: - name: Check PR run - uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main - - run-workflow: - name: Run Build CLI on PR Target - needs: check-run - if: ${{ github.event.pull_request.head.repo.full_name != github.repository }} - uses: ./.github/workflows/build-cli.yml - secrets: inherit - diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 35970a8b307..d480879fb15 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -1,7 +1,7 @@ name: Build CLI on: - pull_request: + pull_request_target: types: [opened, synchronize] branches-ignore: - 'l10n_master' @@ -27,8 +27,6 @@ on: - '!*.txt' - '.github/workflows/build-cli.yml' - 'bitwarden_license/bit-cli/**' - workflow_call: - inputs: {} workflow_dispatch: inputs: sdk_branch: @@ -41,13 +39,18 @@ defaults: working-directory: apps/cli jobs: + check-run: + name: Check PR run + uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main + setup: name: Setup runs-on: ubuntu-22.04 + needs: + - check-run outputs: package_version: ${{ steps.retrieve-package-version.outputs.package_version }} node_version: ${{ steps.retrieve-node-version.outputs.node_version }} - has_secrets: ${{ steps.check-secrets.outputs.has_secrets }} steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -68,14 +71,6 @@ jobs: NODE_VERSION=${NODE_NVMRC/v/''} echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT - - name: Check secrets - id: check-secrets - env: - AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - run: | - has_secrets=${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL != '' }} - echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT - cli: name: CLI ${{ matrix.os.base }} - ${{ matrix.license_type.readable }} strategy: @@ -122,7 +117,7 @@ jobs: working-directory: ./ - name: Download SDK Artifacts - if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} + if: ${{ inputs.sdk_branch != '' }} uses: bitwarden/gh-actions/download-artifacts@main with: github_token: ${{secrets.GITHUB_TOKEN}} @@ -135,7 +130,7 @@ jobs: if_no_artifact_found: fail - name: Override SDK - if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} + if: ${{ inputs.sdk_branch != '' }} working-directory: ./ run: | ls -l ../ @@ -277,7 +272,7 @@ jobs: working-directory: ./ - name: Download SDK Artifacts - if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} + if: ${{ inputs.sdk_branch != '' }} uses: bitwarden/gh-actions/download-artifacts@main with: github_token: ${{secrets.GITHUB_TOKEN}} @@ -290,7 +285,7 @@ jobs: if_no_artifact_found: fail - name: Override SDK - if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} + if: ${{ inputs.sdk_branch != '' }} working-directory: ./ run: | ls -l ../ diff --git a/.github/workflows/build-desktop-target.yml b/.github/workflows/build-desktop-target.yml deleted file mode 100644 index b9ea9cacb8d..00000000000 --- a/.github/workflows/build-desktop-target.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Build Desktop on PR Target - -on: - pull_request_target: - types: [opened, synchronize] - branches-ignore: - - 'l10n_master' - - 'cf-pages' - paths: - - 'apps/desktop/**' - - 'libs/**' - - '*' - - '!*.md' - - '!*.txt' - - '.github/workflows/build-desktop.yml' - workflow_dispatch: - inputs: - sdk_branch: - description: "Custom SDK branch" - required: false - type: string - -defaults: - run: - shell: bash - -jobs: - check-run: - name: Check PR run - uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main - - run-workflow: - name: Run Build Desktop on PR Target - needs: check-run - if: ${{ github.event.pull_request.head.repo.full_name != github.repository }} - uses: ./.github/workflows/build-desktop.yml - secrets: inherit - diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index e35dee54e08..bc9bdec396a 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -1,7 +1,7 @@ name: Build Desktop on: - pull_request: + pull_request_target: types: [opened, synchronize] branches-ignore: - 'l10n_master' @@ -25,8 +25,6 @@ on: - '!*.md' - '!*.txt' - '.github/workflows/build-desktop.yml' - workflow_call: - inputs: {} workflow_dispatch: inputs: sdk_branch: @@ -39,9 +37,15 @@ defaults: shell: bash jobs: + check-run: + name: Check PR run + uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main + electron-verify: name: Verify Electron Version runs-on: ubuntu-22.04 + needs: + - check-run steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -63,6 +67,8 @@ jobs: setup: name: Setup runs-on: ubuntu-22.04 + needs: + - check-run outputs: package_version: ${{ steps.retrieve-version.outputs.package_version }} release_channel: ${{ steps.release-channel.outputs.channel }} @@ -70,7 +76,6 @@ jobs: rc_branch_exists: ${{ steps.branch-check.outputs.rc_branch_exists }} hotfix_branch_exists: ${{ steps.branch-check.outputs.hotfix_branch_exists }} node_version: ${{ steps.retrieve-node-version.outputs.node_version }} - has_secrets: ${{ steps.check-secrets.outputs.has_secrets }} defaults: run: working-directory: apps/desktop @@ -133,14 +138,6 @@ jobs: NODE_VERSION=${NODE_NVMRC/v/''} echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT - - name: Check secrets - id: check-secrets - env: - AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - run: | - has_secrets=${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL != '' }} - echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT - linux: name: Linux Build # Note, before updating the ubuntu version of the workflow, ensure the snap base image @@ -336,14 +333,12 @@ jobs: rustup show - name: Login to Azure - if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 with: creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - name: Retrieve secrets id: retrieve-secrets - if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: bitwarden/gh-actions/get-keyvault-secrets@main with: keyvault: "bitwarden-ci" @@ -358,7 +353,7 @@ jobs: working-directory: ./ - name: Download SDK Artifacts - if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} + if: ${{ inputs.sdk_branch != '' }} uses: bitwarden/gh-actions/download-artifacts@main with: github_token: ${{secrets.GITHUB_TOKEN}} @@ -371,7 +366,7 @@ jobs: if_no_artifact_found: fail - name: Override SDK - if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} + if: ${{ inputs.sdk_branch != '' }} working-directory: ./ run: | ls -l ../ @@ -391,17 +386,7 @@ jobs: working-directory: apps/desktop/desktop_native run: node build.js cross-platform - - name: Build - run: | - npm run build - - - name: Pack - if: ${{ needs.setup.outputs.has_secrets == 'false' }} - run: | - npm run pack:win - - - name: Pack & Sign (dev) - if: ${{ needs.setup.outputs.has_secrets == 'true' }} + - name: Build & Sign (dev) env: ELECTRON_BUILDER_SIGN: 1 SIGNING_VAULT_URL: ${{ steps.retrieve-secrets.outputs.code-signing-vault-url }} @@ -410,10 +395,10 @@ jobs: SIGNING_CLIENT_SECRET: ${{ steps.retrieve-secrets.outputs.code-signing-client-secret }} SIGNING_CERT_NAME: ${{ steps.retrieve-secrets.outputs.code-signing-cert-name }} run: | + npm run build npm run pack:win - name: Rename appx files for store - if: ${{ needs.setup.outputs.has_secrets == 'true' }} run: | Copy-Item "./dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx" ` -Destination "./dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx" @@ -423,7 +408,6 @@ jobs: -Destination "./dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx" - name: Package for Chocolatey - if: ${{ needs.setup.outputs.has_secrets == 'true' }} run: | Copy-Item -Path ./stores/chocolatey -Destination ./dist/chocolatey -Recurse Copy-Item -Path ./dist/nsis-web/Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe ` @@ -435,7 +419,6 @@ jobs: choco pack ./dist/chocolatey/bitwarden.nuspec --version "$env:_PACKAGE_VERSION" --out ./dist/chocolatey - name: Fix NSIS artifact names for auto-updater - if: ${{ needs.setup.outputs.has_secrets == 'true' }} run: | Rename-Item -Path .\dist\nsis-web\Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z ` -NewName bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z @@ -452,7 +435,6 @@ jobs: if-no-files-found: error - name: Upload installer exe artifact - if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe @@ -460,7 +442,6 @@ jobs: if-no-files-found: error - name: Upload appx ia32 artifact - if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx @@ -468,7 +449,6 @@ jobs: if-no-files-found: error - name: Upload store appx ia32 artifact - if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx @@ -476,7 +456,6 @@ jobs: if-no-files-found: error - name: Upload NSIS ia32 artifact - if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z @@ -484,7 +463,6 @@ jobs: if-no-files-found: error - name: Upload appx x64 artifact - if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx @@ -492,7 +470,6 @@ jobs: if-no-files-found: error - name: Upload store appx x64 artifact - if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx @@ -500,7 +477,6 @@ jobs: if-no-files-found: error - name: Upload NSIS x64 artifact - if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z @@ -508,7 +484,6 @@ jobs: if-no-files-found: error - name: Upload appx ARM64 artifact - if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx @@ -516,7 +491,6 @@ jobs: if-no-files-found: error - name: Upload store appx ARM64 artifact - if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx @@ -524,7 +498,6 @@ jobs: if-no-files-found: error - name: Upload NSIS ARM64 artifact - if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z @@ -532,7 +505,6 @@ jobs: if-no-files-found: error - name: Upload nupkg artifact - if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: bitwarden.${{ env._PACKAGE_VERSION }}.nupkg @@ -540,7 +512,6 @@ jobs: if-no-files-found: error - name: Upload auto-update artifact - if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: ${{ needs.setup.outputs.release_channel }}.yml @@ -603,13 +574,11 @@ jobs: key: ${{ runner.os }}-${{ github.run_id }}-safari-extension - name: Login to Azure - if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 with: creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - name: Download Provisioning Profiles secrets - if: ${{ needs.setup.outputs.has_secrets == 'true' }} env: ACCOUNT_NAME: bitwardenci CONTAINER_NAME: profiles @@ -622,7 +591,6 @@ jobs: --output none - name: Get certificates - if: ${{ needs.setup.outputs.has_secrets == 'true' }} run: | mkdir -p $HOME/certificates @@ -645,7 +613,6 @@ jobs: jq -r .value | base64 -d > $HOME/certificates/macdev-cert.p12 - name: Set up keychain - if: ${{ needs.setup.outputs.has_secrets == 'true' }} env: KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} run: | @@ -675,7 +642,6 @@ jobs: security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEYCHAIN_PASSWORD build.keychain - name: Set up provisioning profiles - if: ${{ needs.setup.outputs.has_secrets == 'true' }} run: | cp $HOME/secrets/bitwarden_desktop_appstore.provisionprofile \ $GITHUB_WORKSPACE/apps/desktop/bitwarden_desktop_appstore.provisionprofile @@ -695,7 +661,7 @@ jobs: working-directory: ./ - name: Download SDK Artifacts - if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} + if: ${{ inputs.sdk_branch != '' }} uses: bitwarden/gh-actions/download-artifacts@main with: github_token: ${{secrets.GITHUB_TOKEN}} @@ -708,7 +674,7 @@ jobs: if_no_artifact_found: fail - name: Override SDK - if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} + if: ${{ inputs.sdk_branch != '' }} working-directory: ./ run: | ls -l ../ @@ -735,7 +701,6 @@ jobs: browser-build: name: Browser Build needs: setup - if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: ./.github/workflows/build-browser.yml secrets: inherit @@ -743,7 +708,6 @@ jobs: macos-package-github: name: MacOS Package GitHub Release Assets runs-on: macos-13 - if: ${{ needs.setup.outputs.has_secrets == 'true' }} needs: - browser-build - macos-build @@ -985,7 +949,6 @@ jobs: macos-package-mas: name: MacOS Package Prod Release Asset runs-on: macos-13 - if: ${{ needs.setup.outputs.has_secrets == 'true' }} needs: - browser-build - macos-build @@ -1253,7 +1216,6 @@ jobs: macos-package-dev: name: MacOS Package Dev Release Asset runs-on: macos-13 - if: ${{ needs.setup.outputs.has_secrets == 'true' }} needs: - browser-build - macos-build diff --git a/.github/workflows/build-web-target.yml b/.github/workflows/build-web-target.yml deleted file mode 100644 index 9a9cd735435..00000000000 --- a/.github/workflows/build-web-target.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Build Web on PR Target - -on: - pull_request_target: - types: [opened, synchronize] - branches-ignore: - - 'l10n_master' - - 'cf-pages' - paths: - - 'apps/web/**' - - 'libs/**' - - '*' - - '!*.md' - - '!*.txt' - - '.github/workflows/build-web.yml' - workflow_dispatch: - inputs: - custom_tag_extension: - description: "Custom image tag extension" - required: false - sdk_branch: - description: "Custom SDK branch" - required: false - type: string - -defaults: - run: - shell: bash - -jobs: - check-run: - name: Check PR run - uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main - - run-workflow: - name: Run Build Web on PR Target - needs: check-run - if: ${{ github.event.pull_request.head.repo.full_name != github.repository }} - uses: ./.github/workflows/build-web.yml - secrets: inherit - diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index 2360f876826..6e5e11c3361 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -1,7 +1,7 @@ name: Build Web on: - pull_request: + pull_request_target: types: [opened, synchronize] branches-ignore: - 'l10n_master' @@ -27,8 +27,6 @@ on: - '.github/workflows/build-web.yml' release: types: [published] - workflow_call: - inputs: {} workflow_dispatch: inputs: custom_tag_extension: @@ -43,13 +41,18 @@ env: _AZ_REGISTRY: bitwardenprod.azurecr.io jobs: + check-run: + name: Check PR run + uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main + setup: name: Setup runs-on: ubuntu-22.04 + needs: + - check-run outputs: version: ${{ steps.version.outputs.value }} node_version: ${{ steps.retrieve-node-version.outputs.node_version }} - has_secrets: ${{ steps.check-secrets.outputs.has_secrets }} steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -67,14 +70,6 @@ jobs: NODE_VERSION=${NODE_NVMRC/v/''} echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT - - name: Check secrets - id: check-secrets - env: - AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - run: | - has_secrets=${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL != '' }} - echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT - build-artifacts: name: Build artifacts runs-on: ubuntu-22.04 @@ -133,7 +128,7 @@ jobs: run: npm ci - name: Download SDK Artifacts - if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} + if: ${{ inputs.sdk_branch != '' }} uses: bitwarden/gh-actions/download-artifacts@main with: github_token: ${{secrets.GITHUB_TOKEN}} @@ -146,7 +141,7 @@ jobs: if_no_artifact_found: fail - name: Override SDK - if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }} + if: ${{ inputs.sdk_branch != '' }} working-directory: ./ run: | ls -l ../ @@ -215,23 +210,19 @@ jobs: ########## ACRs ########## - name: Login to Prod Azure - if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 with: creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} - name: Log into Prod container registry - if: ${{ needs.setup.outputs.has_secrets == 'true' }} run: az acr login -n bitwardenprod - name: Login to Azure - CI Subscription - if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 with: creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - name: Retrieve github PAT secrets - if: ${{ needs.setup.outputs.has_secrets == 'true' }} id: retrieve-secret-pat uses: bitwarden/gh-actions/get-keyvault-secrets@main with: @@ -279,7 +270,6 @@ jobs: run: echo "name=$_AZ_REGISTRY/${PROJECT_NAME}:${IMAGE_TAG}" >> $GITHUB_OUTPUT - name: Build Docker image - if: ${{ needs.setup.outputs.has_secrets == 'true' }} uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0 with: context: apps/web diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1b738bd7bcf..9dc72c7fdda 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,20 +1,12 @@ name: Lint on: - pull_request: - types: [opened, synchronize] + push: branches-ignore: - 'l10n_master' - 'cf-pages' paths-ignore: - '.github/workflows/**' - push: - branches: - - 'main' - - 'rc' - - 'hotfix-rc-*' - paths-ignore: - - '.github/workflows/**' workflow_dispatch: inputs: {} From 46e2e0233b9bbe9149e7f920a7e24fc72c145fbf Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Thu, 12 Dec 2024 18:27:16 +0100 Subject: [PATCH 10/10] Fix duplicate key in en/messages.json (#12375) This needs to be done, because Firefox compares the keys case-insensitive when a release is uploaded to the store. Co-authored-by: Daniel James Smith --- apps/browser/src/_locales/en/messages.json | 2 +- .../src/auth/popup/login-via-auth-request-v1.component.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 39bc6ed9b86..04858eecced 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -3173,7 +3173,7 @@ "viewAllLogInOptions": { "message": "View all log in options" }, - "viewAllLoginOptions": { + "viewAllLoginOptionsV1": { "message": "View all log in options" }, "notificationSentDevice": { diff --git a/apps/browser/src/auth/popup/login-via-auth-request-v1.component.html b/apps/browser/src/auth/popup/login-via-auth-request-v1.component.html index 2abff7bdb9c..e7fafbb252c 100644 --- a/apps/browser/src/auth/popup/login-via-auth-request-v1.component.html +++ b/apps/browser/src/auth/popup/login-via-auth-request-v1.component.html @@ -30,7 +30,7 @@
@@ -53,7 +53,7 @@