diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json
index 59d565e4fb05..a9187194d901 100644
--- a/apps/browser/src/_locales/en/messages.json
+++ b/apps/browser/src/_locales/en/messages.json
@@ -1496,6 +1496,12 @@
"myFolders": {
"message": "My folders"
},
+ "addSharedFolder": {
+ "message": "Add shared folder"
+ },
+ "newSharedFolder": {
+ "message": "New shared folder"
+ },
"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."
},
diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts
index 40f3b9edb39c..0adaea6c5a80 100644
--- a/apps/desktop/src/app/services/services.module.ts
+++ b/apps/desktop/src/app/services/services.module.ts
@@ -149,6 +149,7 @@ import {
RoutedVaultFilterService,
RoutedVaultFilterBridgeService,
VAULT_FILTER_BASE_ROUTE,
+ Vfo1TerminologyService,
} from "@bitwarden/vault";
import { DesktopLoginComponentService } from "../../auth/login/desktop-login-component.service";
@@ -615,6 +616,7 @@ const safeProviders: SafeProvider[] = [
CollectionService,
AccountServiceAbstraction,
ConfigService,
+ Vfo1TerminologyService,
],
}),
safeProvider({
diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json
index 08d24f9c02d9..262cc2444f1b 100644
--- a/apps/desktop/src/locales/en/messages.json
+++ b/apps/desktop/src/locales/en/messages.json
@@ -116,6 +116,12 @@
"myFolders": {
"message": "My folders"
},
+ "addSharedFolder": {
+ "message": "Add shared folder"
+ },
+ "newSharedFolder": {
+ "message": "New shared folder"
+ },
"share": {
"message": "Share"
},
diff --git a/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.service.ts b/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.service.ts
index 39e272f11cb5..5d2068b214e8 100644
--- a/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.service.ts
+++ b/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.service.ts
@@ -12,7 +12,11 @@ import { StateProvider } from "@bitwarden/common/platform/state";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
-import { VaultFilterService as BaseVaultFilterService, CollectionFilter } from "@bitwarden/vault";
+import {
+ VaultFilterService as BaseVaultFilterService,
+ CollectionFilter,
+ Vfo1TerminologyService,
+} from "@bitwarden/vault";
@Injectable()
export class VaultFilterService extends BaseVaultFilterService implements OnDestroy {
@@ -35,6 +39,7 @@ export class VaultFilterService extends BaseVaultFilterService implements OnDest
collectionService: CollectionService,
accountService: AccountService,
configService: ConfigService,
+ vfo1Terminology: Vfo1TerminologyService,
) {
super(
organizationService,
@@ -46,6 +51,7 @@ export class VaultFilterService extends BaseVaultFilterService implements OnDest
collectionService,
accountService,
configService,
+ vfo1Terminology,
);
}
diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json
index 488c8a2b1a16..baa32420e0ad 100644
--- a/apps/web/src/locales/en/messages.json
+++ b/apps/web/src/locales/en/messages.json
@@ -1206,6 +1206,12 @@
"myFolders": {
"message": "My folders"
},
+ "addSharedFolder": {
+ "message": "Add shared folder"
+ },
+ "newSharedFolder": {
+ "message": "New shared folder"
+ },
"attachments": {
"message": "Attachments"
},
diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html
index 381d86cc25b4..b7cafe073bf0 100644
--- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html
+++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html
@@ -39,7 +39,7 @@
{{ "itemDetails" | i18n }}
@if (showCollectionsControl) {
- {{ "collections" | i18n }}
+ {{ "collections" | vfo1I18n: "sharedFolders" }}
{{ "itemDetails" | i18n }}
}
@if (!config.hideIndividualVaultFields) {
- {{ "folder" | i18n }}
+ {{ "folder" | vfo1I18n: "myFolder" }}
@for (folder of config.folders; track folder.id) {
diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts
index 6ca062321a74..84874718f373 100644
--- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts
+++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts
@@ -33,6 +33,7 @@ import {
TypographyModule,
} from "@bitwarden/components";
+import { Vfo1I18nPipe } from "../../../pipes/vfo1-i18n.pipe";
import {
CipherFormConfig,
OptionalInitialValues,
@@ -55,6 +56,7 @@ import { CipherFormContainer } from "../../cipher-form-container";
IconButtonModule,
JslibModule,
CommonModule,
+ Vfo1I18nPipe,
],
})
export class ItemDetailsSectionComponent implements OnInit {
diff --git a/libs/vault/src/cipher-view/item-details/item-details-v2.component.spec.ts b/libs/vault/src/cipher-view/item-details/item-details-v2.component.spec.ts
index 30362678fbb3..051600b18c45 100644
--- a/libs/vault/src/cipher-view/item-details/item-details-v2.component.spec.ts
+++ b/libs/vault/src/cipher-view/item-details/item-details-v2.component.spec.ts
@@ -9,6 +9,7 @@ import { DomainSettingsService } from "@bitwarden/common/autofill/services/domai
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
@@ -88,6 +89,27 @@ describe("ItemDetailsV2Component", () => {
expect(itemDetailsList[3].nativeElement.textContent.trim()).toContain(folder.name);
});
+ describe("getAriaLabel", () => {
+ it("separates the label and name with a space for organizations", () => {
+ const org = Object.assign(new Organization(), { name: "Organization 1" });
+ expect(component.getAriaLabel(org)).toBe("owner Organization 1");
+ });
+
+ it("separates the label and name with a space for collections", () => {
+ const col = new CollectionView({
+ id: "col1" as CollectionId,
+ organizationId: "org1" as OrganizationId,
+ name: "Collection 1",
+ });
+ expect(component.getAriaLabel(col)).toBe("collection Collection 1");
+ });
+
+ it("separates the label and name with a space for folders", () => {
+ const folderView = Object.assign(new FolderView(), { name: "Folder 1" });
+ expect(component.getAriaLabel(folderView)).toBe("folder Folder 1");
+ });
+ });
+
it("does not render owner when `hideOwner` is true", () => {
componentRef.setInput("hideOwner", true);
fixture.detectChanges();
diff --git a/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts b/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts
index 04c5137eacc4..8528d4ccba8e 100644
--- a/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts
+++ b/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts
@@ -1,7 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CommonModule } from "@angular/common";
-import { Component, computed, input, signal } from "@angular/core";
+import { Component, computed, inject, input, signal } from "@angular/core";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
import { toSignal } from "@angular/core/rxjs-interop";
import { fromEvent, map, startWith } from "rxjs";
@@ -23,6 +23,7 @@ import {
} from "@bitwarden/components";
import { OrgIconDirective } from "../../components/org-icon.directive";
+import { Vfo1TerminologyService } from "../../services/vfo1-terminology.service";
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@@ -47,6 +48,8 @@ export class ItemDetailsV2Component {
readonly collections = input();
readonly showAllDetails = signal(false);
+ private readonly terminology = inject(Vfo1TerminologyService);
+
readonly showOwnership = computed(() => {
return this.cipher().organizationId && this.organization() && !this.hideOwner();
});
@@ -95,11 +98,13 @@ export class ItemDetailsV2Component {
getAriaLabel(item: Organization | CollectionView | FolderView): string {
if (item instanceof Organization) {
- return this.i18nService.t("owner") + item.name;
+ return `${this.i18nService.t("owner")} ${item.name}`;
} else if (item instanceof CollectionView) {
- return this.i18nService.t("collection") + item.name;
+ const label = this.i18nService.t(this.terminology.enabled() ? "sharedFolder" : "collection");
+ return `${label} ${item.name}`;
} else if (item instanceof FolderView) {
- return this.i18nService.t("folder") + item.name;
+ const label = this.i18nService.t(this.terminology.enabled() ? "myFolder" : "folder");
+ return `${label} ${item.name}`;
}
return "";
}
@@ -117,9 +122,9 @@ export class ItemDetailsV2Component {
getItemTitle(item: Organization | CollectionView | FolderView): string {
if (item instanceof CollectionView) {
- return this.i18nService.t("collection");
+ return this.i18nService.t(this.terminology.enabled() ? "sharedFolder" : "collection");
} else if (item instanceof FolderView) {
- return this.i18nService.t("folder");
+ return this.i18nService.t(this.terminology.enabled() ? "myFolder" : "folder");
}
return "";
}
diff --git a/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.html b/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.html
index 8ace92850bbb..e1a11733987c 100644
--- a/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.html
+++ b/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.html
@@ -56,7 +56,7 @@
@if (canCreateCollection()) {
}
diff --git a/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.ts b/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.ts
index bf379d9bd9de..1c0b25c75471 100644
--- a/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.ts
+++ b/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.ts
@@ -1,5 +1,5 @@
import { CommonModule } from "@angular/common";
-import { Component, input, output } from "@angular/core";
+import { Component, inject, input, output } from "@angular/core";
import { toObservable, toSignal } from "@angular/core/rxjs-interop";
import { combineLatest, map, shareReplay } from "rxjs";
@@ -21,6 +21,9 @@ import {
} from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
+import { Vfo1I18nPipe } from "../../pipes/vfo1-i18n.pipe";
+import { Vfo1TerminologyService } from "../../services/vfo1-terminology.service";
+
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
@@ -33,6 +36,7 @@ import { I18nPipe } from "@bitwarden/ui-common";
PopoverModule,
I18nPipe,
JslibModule,
+ Vfo1I18nPipe,
TooltipDirective,
],
})
@@ -62,6 +66,8 @@ export class NewCipherMenuComponent {
cipherAdded = output();
onAddItemDialog = output();
+ private readonly terminology = inject(Vfo1TerminologyService);
+
private readonly btnTextAddCreateFeatureFlag = toSignal(
this.configService.getFeatureFlag$(FeatureFlag.PM32380_BtnTextAddCreate),
{ initialValue: false },
@@ -113,10 +119,11 @@ export class NewCipherMenuComponent {
// If only collections can be created, be specific
if (!canCreateCipher && !canCreateFolder && canCreateCollection) {
+ const sharedFolderTerminology = this.terminology.enabled();
if (btnTextAddCreateFeatureFlag) {
- return "addCollection";
+ return sharedFolderTerminology ? "addSharedFolder" : "addCollection";
} else {
- return "newCollection";
+ return sharedFolderTerminology ? "newSharedFolder" : "newCollection";
}
}
diff --git a/libs/vault/src/services/vault-filter.service.spec.ts b/libs/vault/src/services/vault-filter.service.spec.ts
index 015cb6592ed1..419dde29f8bb 100644
--- a/libs/vault/src/services/vault-filter.service.spec.ts
+++ b/libs/vault/src/services/vault-filter.service.spec.ts
@@ -31,6 +31,7 @@ import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { COLLAPSED_GROUPINGS } from "@bitwarden/common/vault/services/key-state/collapsed-groupings.state";
import { VaultFilterService } from "./vault-filter.service";
+import { Vfo1TerminologyService } from "./vfo1-terminology.service";
jest.mock("@bitwarden/angular/vault/vault-filter/services/vault-filter.service", () => ({
sortDefaultCollections: jest.fn((): CollectionView[] => []),
@@ -54,6 +55,7 @@ describe("vault filter service", () => {
let autoConfirmPolicy: ReplaySubject;
let stateProvider: FakeStateProvider;
let configService: MockProxy;
+ let vfo1Terminology: MockProxy;
const mockUserId = Utils.newGuid() as UserId;
let accountService: FakeAccountService;
@@ -70,6 +72,8 @@ describe("vault filter service", () => {
i18nService.collator = new Intl.Collator("en-US");
collectionService = mock();
configService = mock();
+ vfo1Terminology = mock();
+ vfo1Terminology.enabled.mockReturnValue(false);
organizations = new ReplaySubject(1);
folderViews = new ReplaySubject(1);
@@ -104,11 +108,28 @@ describe("vault filter service", () => {
collectionService,
accountService,
configService,
+ vfo1Terminology,
);
collapsedGroupingsState = stateProvider.singleUser.getFake(mockUserId, COLLAPSED_GROUPINGS);
organizations.next([]);
});
+ describe("shared-folder terminology filter heads", () => {
+ it("uses collection/folder head names when the flag is off", () => {
+ vfo1Terminology.enabled.mockReturnValue(false);
+
+ expect(vaultFilterService["getCollectionFilterHead"]().node.name).toBe("collections");
+ expect(vaultFilterService["getFolderFilterHead"]().node.name).toBe("folders");
+ });
+
+ it("uses shared-folder/my-folder head names when the flag is on", () => {
+ vfo1Terminology.enabled.mockReturnValue(true);
+
+ expect(vaultFilterService["getCollectionFilterHead"]().node.name).toBe("sharedFolders");
+ expect(vaultFilterService["getFolderFilterHead"]().node.name).toBe("myFolders");
+ });
+ });
+
describe("collapsed filter nodes", () => {
const nodes = new Set(["1", "2"]);
@@ -382,6 +403,7 @@ describe("vault filter service", () => {
collectionService,
accountService,
configService,
+ vfo1Terminology,
);
});
diff --git a/libs/vault/src/services/vault-filter.service.ts b/libs/vault/src/services/vault-filter.service.ts
index ffed37f5e361..845ef991bc59 100644
--- a/libs/vault/src/services/vault-filter.service.ts
+++ b/libs/vault/src/services/vault-filter.service.ts
@@ -53,6 +53,8 @@ import {
OrganizationFilter,
} from "..";
+import { Vfo1TerminologyService } from "./vfo1-terminology.service";
+
const NestingDelimiter = "/";
@Injectable()
@@ -211,6 +213,7 @@ export class VaultFilterService implements VaultFilterServiceAbstraction {
protected collectionService: CollectionService,
protected accountService: AccountService,
protected configService: ConfigService,
+ protected vfo1Terminology: Vfo1TerminologyService,
) {}
async getCollectionNodeFromTree(id: string) {
@@ -358,7 +361,8 @@ export class VaultFilterService implements VaultFilterServiceAbstraction {
protected getCollectionFilterHead(): TreeNode {
const head = CollectionView.vaultFilterHead() as CollectionFilter;
- return new TreeNode(head, null, "collections", "AllCollections");
+ const name = this.vfo1Terminology.enabled() ? "sharedFolders" : "collections";
+ return new TreeNode(head, null, name, "AllCollections");
}
protected async filterFolders(
@@ -401,7 +405,8 @@ export class VaultFilterService implements VaultFilterServiceAbstraction {
protected getFolderFilterHead(): TreeNode {
const head = new FolderView() as FolderFilter;
- return new TreeNode(head, null, "folders", "AllFolders");
+ const name = this.vfo1Terminology.enabled() ? "myFolders" : "folders";
+ return new TreeNode(head, null, name, "AllFolders");
}
protected buildCipherTypeTree(): Observable> {