Browse Source

feat(admin-ui): Implement UI for entity duplication

Relates to #627
Michael Bromley 1 year ago
parent
commit
7aa0d16d4f
41 changed files with 673 additions and 104 deletions
  1. 50 45
      packages/admin-ui/i18n-coverage.json
  2. 6 0
      packages/admin-ui/src/lib/catalog/src/catalog.module.ts
  3. 28 0
      packages/admin-ui/src/lib/catalog/src/components/collection-list/collection-list-bulk-actions.ts
  4. 28 0
      packages/admin-ui/src/lib/catalog/src/components/facet-list/facet-list-bulk-actions.ts
  5. 1 1
      packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.html
  6. 29 1
      packages/admin-ui/src/lib/catalog/src/components/product-list/product-list-bulk-actions.ts
  7. 1 1
      packages/admin-ui/src/lib/catalog/src/components/product-variant-list/product-variant-list-bulk-actions.ts
  8. 64 27
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  9. 5 0
      packages/admin-ui/src/lib/core/src/common/introspection-result.ts
  10. 2 2
      packages/admin-ui/src/lib/core/src/common/utilities/configurable-operation-utils.ts
  11. 2 0
      packages/admin-ui/src/lib/core/src/public_api.ts
  12. 2 2
      packages/admin-ui/src/lib/core/src/shared/components/assign-to-channel-dialog/assign-to-channel-dialog.component.html
  13. 1 1
      packages/admin-ui/src/lib/core/src/shared/components/configurable-input/configurable-input.component.html
  14. 1 0
      packages/admin-ui/src/lib/core/src/shared/components/configurable-input/configurable-input.component.ts
  15. 41 0
      packages/admin-ui/src/lib/core/src/shared/components/duplicate-entity-dialog/duplicate-entity-dialog.component.html
  16. 0 0
      packages/admin-ui/src/lib/core/src/shared/components/duplicate-entity-dialog/duplicate-entity-dialog.component.scss
  17. 134 0
      packages/admin-ui/src/lib/core/src/shared/components/duplicate-entity-dialog/duplicate-entity-dialog.component.ts
  18. 39 0
      packages/admin-ui/src/lib/core/src/shared/components/duplicate-entity-dialog/duplicate-entity-dialog.graphql.ts
  19. 2 0
      packages/admin-ui/src/lib/core/src/shared/shared.module.ts
  20. 32 0
      packages/admin-ui/src/lib/marketing/src/components/promotion-list/promotion-list-bulk-actions.ts
  21. 2 0
      packages/admin-ui/src/lib/marketing/src/marketing.module.ts
  22. 10 1
      packages/admin-ui/src/lib/static/i18n-messages/ar.json
  23. 10 1
      packages/admin-ui/src/lib/static/i18n-messages/cs.json
  24. 10 1
      packages/admin-ui/src/lib/static/i18n-messages/de.json
  25. 10 2
      packages/admin-ui/src/lib/static/i18n-messages/en.json
  26. 10 1
      packages/admin-ui/src/lib/static/i18n-messages/es.json
  27. 10 1
      packages/admin-ui/src/lib/static/i18n-messages/fa.json
  28. 10 1
      packages/admin-ui/src/lib/static/i18n-messages/fr.json
  29. 10 1
      packages/admin-ui/src/lib/static/i18n-messages/he.json
  30. 10 1
      packages/admin-ui/src/lib/static/i18n-messages/hr.json
  31. 10 1
      packages/admin-ui/src/lib/static/i18n-messages/it.json
  32. 10 1
      packages/admin-ui/src/lib/static/i18n-messages/nb.json
  33. 13 4
      packages/admin-ui/src/lib/static/i18n-messages/ne.json
  34. 10 1
      packages/admin-ui/src/lib/static/i18n-messages/pl.json
  35. 10 1
      packages/admin-ui/src/lib/static/i18n-messages/pt_BR.json
  36. 10 1
      packages/admin-ui/src/lib/static/i18n-messages/pt_PT.json
  37. 10 1
      packages/admin-ui/src/lib/static/i18n-messages/ru.json
  38. 10 1
      packages/admin-ui/src/lib/static/i18n-messages/sv.json
  39. 10 1
      packages/admin-ui/src/lib/static/i18n-messages/uk.json
  40. 10 1
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json
  41. 10 1
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json

+ 50 - 45
packages/admin-ui/i18n-coverage.json

@@ -1,101 +1,106 @@
 {
-  "generatedOn": "2024-01-26T08:34:41.984Z",
-  "lastCommit": "2314ff6a392488179cd3e3c553a445bd0fd8202d",
+  "generatedOn": "2024-02-20T08:21:48.514Z",
+  "lastCommit": "752e740a338bdef4a8634260fa5a4c7379d448d5",
   "translationStatus": {
     "ar": {
-      "tokenCount": 769,
-      "translatedCount": 767,
+      "tokenCount": 778,
+      "translatedCount": 775,
       "percentage": 100
     },
     "cs": {
-      "tokenCount": 769,
-      "translatedCount": 579,
+      "tokenCount": 778,
+      "translatedCount": 587,
       "percentage": 75
     },
     "de": {
-      "tokenCount": 769,
-      "translatedCount": 767,
+      "tokenCount": 778,
+      "translatedCount": 775,
       "percentage": 100
     },
     "en": {
-      "tokenCount": 769,
-      "translatedCount": 768,
+      "tokenCount": 778,
+      "translatedCount": 777,
       "percentage": 100
     },
     "es": {
-      "tokenCount": 769,
-      "translatedCount": 767,
+      "tokenCount": 778,
+      "translatedCount": 775,
       "percentage": 100
     },
     "fa": {
-      "tokenCount": 769,
-      "translatedCount": 767,
+      "tokenCount": 778,
+      "translatedCount": 775,
       "percentage": 100
     },
     "fr": {
-      "tokenCount": 769,
-      "translatedCount": 765,
+      "tokenCount": 778,
+      "translatedCount": 773,
       "percentage": 99
     },
     "he": {
-      "tokenCount": 769,
-      "translatedCount": 767,
+      "tokenCount": 778,
+      "translatedCount": 775,
       "percentage": 100
     },
     "hr": {
-      "tokenCount": 769,
-      "translatedCount": 766,
-      "percentage": 100
+      "tokenCount": 778,
+      "translatedCount": 774,
+      "percentage": 99
     },
     "it": {
-      "tokenCount": 769,
-      "translatedCount": 767,
+      "tokenCount": 778,
+      "translatedCount": 775,
       "percentage": 100
     },
     "nb": {
-      "tokenCount": 769,
-      "translatedCount": 764,
+      "tokenCount": 778,
+      "translatedCount": 772,
       "percentage": 99
     },
     "ne": {
-      "tokenCount": 769,
-      "translatedCount": 756,
+      "tokenCount": 778,
+      "translatedCount": 764,
       "percentage": 98
     },
     "pl": {
-      "tokenCount": 769,
-      "translatedCount": 409,
-      "percentage": 53
+      "tokenCount": 778,
+      "translatedCount": 417,
+      "percentage": 54
     },
     "pt_BR": {
-      "tokenCount": 769,
-      "translatedCount": 766,
-      "percentage": 100
+      "tokenCount": 778,
+      "translatedCount": 774,
+      "percentage": 99
     },
     "pt_PT": {
-      "tokenCount": 769,
-      "translatedCount": 614,
+      "tokenCount": 778,
+      "translatedCount": 622,
       "percentage": 80
     },
     "ru": {
-      "tokenCount": 769,
-      "translatedCount": 767,
+      "tokenCount": 778,
+      "translatedCount": 775,
+      "percentage": 100
+    },
+    "sv": {
+      "tokenCount": 778,
+      "translatedCount": 776,
       "percentage": 100
     },
     "uk": {
-      "tokenCount": 769,
-      "translatedCount": 767,
+      "tokenCount": 778,
+      "translatedCount": 775,
       "percentage": 100
     },
     "zh_Hans": {
-      "tokenCount": 769,
-      "translatedCount": 549,
-      "percentage": 71
+      "tokenCount": 778,
+      "translatedCount": 557,
+      "percentage": 72
     },
     "zh_Hant": {
-      "tokenCount": 769,
-      "translatedCount": 396,
-      "percentage": 51
+      "tokenCount": 778,
+      "translatedCount": 404,
+      "percentage": 52
     }
   }
 }

+ 6 - 0
packages/admin-ui/src/lib/catalog/src/catalog.module.ts

@@ -28,6 +28,7 @@ import { CollectionBreadcrumbPipe } from './components/collection-list/collectio
 import {
     assignCollectionsToChannelBulkAction,
     deleteCollectionsBulkAction,
+    duplicateCollectionsBulkAction,
     moveCollectionsBulkAction,
     removeCollectionsFromChannelBulkAction,
 } from './components/collection-list/collection-list-bulk-actions';
@@ -42,6 +43,7 @@ import { FacetDetailComponent } from './components/facet-detail/facet-detail.com
 import {
     assignFacetsToChannelBulkAction,
     deleteFacetsBulkAction,
+    duplicateFacetsBulkAction,
     removeFacetsFromChannelBulkAction,
 } from './components/facet-list/facet-list-bulk-actions';
 import { FacetListComponent } from './components/facet-list/facet-list.component';
@@ -53,6 +55,7 @@ import {
     assignFacetValuesToProductsBulkAction,
     assignProductsToChannelBulkAction,
     deleteProductsBulkAction,
+    duplicateProductsBulkAction,
     removeProductsFromChannelBulkAction,
 } from './components/product-list/product-list-bulk-actions';
 import { ProductListComponent } from './components/product-list/product-list.component';
@@ -132,17 +135,20 @@ export class CatalogModule {
         bulkActionRegistryService.registerBulkAction(assignProductVariantsToChannelBulkAction);
         bulkActionRegistryService.registerBulkAction(removeProductsFromChannelBulkAction);
         bulkActionRegistryService.registerBulkAction(removeProductVariantsFromChannelBulkAction);
+        bulkActionRegistryService.registerBulkAction(duplicateProductsBulkAction);
         bulkActionRegistryService.registerBulkAction(deleteProductsBulkAction);
         bulkActionRegistryService.registerBulkAction(deleteProductVariantsBulkAction);
         bulkActionRegistryService.registerBulkAction(assignFacetValuesToProductVariantsBulkAction);
 
         bulkActionRegistryService.registerBulkAction(assignFacetsToChannelBulkAction);
         bulkActionRegistryService.registerBulkAction(removeFacetsFromChannelBulkAction);
+        bulkActionRegistryService.registerBulkAction(duplicateFacetsBulkAction);
         bulkActionRegistryService.registerBulkAction(deleteFacetsBulkAction);
 
         bulkActionRegistryService.registerBulkAction(moveCollectionsBulkAction);
         bulkActionRegistryService.registerBulkAction(assignCollectionsToChannelBulkAction);
         bulkActionRegistryService.registerBulkAction(removeCollectionsFromChannelBulkAction);
+        bulkActionRegistryService.registerBulkAction(duplicateCollectionsBulkAction);
         bulkActionRegistryService.registerBulkAction(deleteCollectionsBulkAction);
 
         pageService.registerPageTab({

+ 28 - 0
packages/admin-ui/src/lib/catalog/src/components/collection-list/collection-list-bulk-actions.ts

@@ -5,6 +5,7 @@ import {
     createBulkDeleteAction,
     createBulkRemoveFromChannelAction,
     DataService,
+    DuplicateEntityDialogComponent,
     GetCollectionListQuery,
     ItemOf,
     ModalService,
@@ -106,3 +107,30 @@ export const removeCollectionsFromChannelBulkAction = createBulkRemoveFromChanne
             })
             .pipe(map(res => res.removeCollectionsFromChannel)),
 });
+
+export const duplicateCollectionsBulkAction: BulkAction<
+    ItemOf<GetCollectionListQuery, 'collections'>,
+    CollectionListComponent
+> = {
+    location: 'collection-list',
+    label: _('common.duplicate'),
+    icon: 'copy',
+    onClick: ({ injector, selection, hostComponent, clearSelection }) => {
+        const modalService = injector.get(ModalService);
+        modalService
+            .fromComponent(DuplicateEntityDialogComponent<ItemOf<GetCollectionListQuery, 'collections'>>, {
+                locals: {
+                    entities: selection,
+                    entityName: 'Collection',
+                    title: _('catalog.duplicate-collections'),
+                    getEntityName: entity => entity.name,
+                },
+            })
+            .subscribe(result => {
+                if (result) {
+                    clearSelection();
+                    hostComponent.refresh();
+                }
+            });
+    },
+};

+ 28 - 0
packages/admin-ui/src/lib/catalog/src/components/facet-list/facet-list-bulk-actions.ts

@@ -6,6 +6,7 @@ import {
     createBulkRemoveFromChannelAction,
     currentChannelIsNotDefault,
     DataService,
+    DuplicateEntityDialogComponent,
     getChannelCodeFromUserStatus,
     GetFacetListQuery,
     ItemOf,
@@ -179,3 +180,30 @@ export const removeFacetsFromChannelBulkAction2: BulkAction<
             });
     },
 };
+
+export const duplicateFacetsBulkAction: BulkAction<
+    ItemOf<GetFacetListQuery, 'facets'>,
+    FacetListComponent
+> = {
+    location: 'facet-list',
+    label: _('common.duplicate'),
+    icon: 'copy',
+    onClick: ({ injector, selection, hostComponent, clearSelection }) => {
+        const modalService = injector.get(ModalService);
+        modalService
+            .fromComponent(DuplicateEntityDialogComponent<ItemOf<GetFacetListQuery, 'facets'>>, {
+                locals: {
+                    entities: selection,
+                    entityName: 'Facet',
+                    title: _('catalog.duplicate-facets'),
+                    getEntityName: entity => entity.name,
+                },
+            })
+            .subscribe(result => {
+                if (result) {
+                    clearSelection();
+                    hostComponent.refresh();
+                }
+            });
+    },
+};

+ 1 - 1
packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.html

@@ -69,7 +69,7 @@
                             </div>
                             <button class="button-small" (click)="assignToChannel()">
                                 <clr-icon shape="layers"></clr-icon>
-                                {{ 'catalog.assign-to-channel' | translate }}
+                                {{ 'common.assign-to-channel' | translate }}
                             </button>
                         </div>
                     </vdr-form-item>

+ 29 - 1
packages/admin-ui/src/lib/catalog/src/components/product-list/product-list-bulk-actions.ts

@@ -4,6 +4,7 @@ import {
     createBulkRemoveFromChannelAction,
     DataService,
     DeletionResult,
+    DuplicateEntityDialogComponent,
     GetProductListQuery,
     isMultiChannel,
     ItemOf,
@@ -80,7 +81,7 @@ export const assignProductsToChannelBulkAction: BulkAction<
     ProductListComponent
 > = {
     location: 'product-list',
-    label: _('catalog.assign-to-channel'),
+    label: _('common.assign-to-channel'),
     icon: 'layers',
     requiresPermission: userPermissions =>
         userPermissions.includes(Permission.UpdateCatalog) ||
@@ -155,3 +156,30 @@ export const assignFacetValuesToProductsBulkAction: BulkAction<
             });
     },
 };
+
+export const duplicateProductsBulkAction: BulkAction<
+    ItemOf<GetProductListQuery, 'products'>,
+    ProductListComponent
+> = {
+    location: 'product-list',
+    label: _('common.duplicate'),
+    icon: 'copy',
+    onClick: ({ injector, selection, hostComponent, clearSelection }) => {
+        const modalService = injector.get(ModalService);
+        modalService
+            .fromComponent(DuplicateEntityDialogComponent<ItemOf<GetProductListQuery, 'products'>>, {
+                locals: {
+                    entities: selection,
+                    entityName: 'Product',
+                    title: _('catalog.duplicate-products'),
+                    getEntityName: entity => entity.name,
+                },
+            })
+            .subscribe(result => {
+                if (result) {
+                    clearSelection();
+                    hostComponent.refresh();
+                }
+            });
+    },
+};

+ 1 - 1
packages/admin-ui/src/lib/catalog/src/components/product-variant-list/product-variant-list-bulk-actions.ts

@@ -24,7 +24,7 @@ export const assignProductVariantsToChannelBulkAction: BulkAction<
     ProductVariantListComponent
 > = {
     location: 'product-variant-list',
-    label: _('catalog.assign-to-channel'),
+    label: _('common.assign-to-channel'),
     icon: 'layers',
     requiresPermission: userPermissions =>
         userPermissions.includes(Permission.UpdateCatalog) ||

File diff suppressed because it is too large
+ 64 - 27
packages/admin-ui/src/lib/core/src/common/generated-types.ts


+ 5 - 0
packages/admin-ui/src/lib/core/src/common/introspection-result.ts

@@ -81,6 +81,10 @@
       "StringCustomFieldConfig",
       "TextCustomFieldConfig"
     ],
+    "DuplicateEntityResult": [
+      "DuplicateEntityError",
+      "DuplicateEntitySuccess"
+    ],
     "ErrorResult": [
       "AlreadyRefundedError",
       "CancelActiveOrderError",
@@ -90,6 +94,7 @@
       "CouponCodeInvalidError",
       "CouponCodeLimitError",
       "CreateFulfillmentError",
+      "DuplicateEntityError",
       "EmailAddressConflictError",
       "EmptyOrderLineSelectionError",
       "FacetInUseError",

+ 2 - 2
packages/admin-ui/src/lib/core/src/common/utilities/configurable-operation-utils.ts

@@ -36,7 +36,7 @@ export function encodeConfigArgValue(value: any): string {
  * Creates an empty ConfigurableOperation object based on the definition.
  */
 export function configurableDefinitionToInstance(
-    def: ConfigurableOperationDefinition,
+    def: Omit<ConfigurableOperationDefinition, '__typename'>,
 ): ConfigurableOperation {
     return {
         ...def,
@@ -68,7 +68,7 @@ export function configurableDefinitionToInstance(
  * ```
  */
 export function toConfigurableOperationInput(
-    operation: ConfigurableOperation,
+    operation: Omit<ConfigurableOperation, '__typename'>,
     formValueOperations: { args: Record<string, string> | Array<{ name: string; value: string }> },
 ): ConfigurableOperationInput {
     const argsArray = Array.isArray(formValueOperations.args) ? formValueOperations.args : undefined;

+ 2 - 0
packages/admin-ui/src/lib/core/src/public_api.ts

@@ -176,6 +176,8 @@ export * from './shared/components/dropdown/dropdown-item.directive';
 export * from './shared/components/dropdown/dropdown-menu.component';
 export * from './shared/components/dropdown/dropdown-trigger.directive';
 export * from './shared/components/dropdown/dropdown.component';
+export * from './shared/components/duplicate-entity-dialog/duplicate-entity-dialog.component';
+export * from './shared/components/duplicate-entity-dialog/duplicate-entity-dialog.graphql';
 export * from './shared/components/edit-note-dialog/edit-note-dialog.component';
 export * from './shared/components/empty-placeholder/empty-placeholder.component';
 export * from './shared/components/entity-info/entity-info.component';

+ 2 - 2
packages/admin-ui/src/lib/core/src/shared/components/assign-to-channel-dialog/assign-to-channel-dialog.component.html

@@ -36,10 +36,10 @@
         class="btn btn-primary"
     >
         <ng-template [ngIf]="selectedChannels.length > 0" [ngIfElse]="noSelection">
-            {{ 'catalog.assign-to-channels' | translate : { count: selectedChannels.length } }}
+            {{ 'common.assign-to-channels' | translate : { count: selectedChannels.length } }}
         </ng-template>
         <ng-template #noSelection>
-            {{ 'catalog.no-channel-selected' | translate : { count: selectedChannels.length } }}
+            {{ 'common.no-channel-selected' | translate : { count: selectedChannels.length } }}
         </ng-template>
     </button>
 </ng-template>

+ 1 - 1
packages/admin-ui/src/lib/core/src/shared/components/configurable-input/configurable-input.component.html

@@ -1,5 +1,5 @@
 <div class="card" *ngIf="operation">
-    <div class="card-block">{{ interpolateDescription() }}</div>
+    <div class="card-block" *ngIf="hideDescription !== true">{{ interpolateDescription() }}</div>
     <div class="card-block" *ngIf="operation.args?.length">
         <form [formGroup]="form" *ngIf="operation" class="operation-inputs">
             <div *ngFor="let arg of operation.args; trackBy: trackByName" class="arg-row">

+ 1 - 0
packages/admin-ui/src/lib/core/src/shared/components/configurable-input/configurable-input.component.ts

@@ -61,6 +61,7 @@ export class ConfigurableInputComponent
     @Input() readonly = false;
     @Input() removable = true;
     @Input() position = 0;
+    @Input() hideDescription = false;
     @Output() remove = new EventEmitter<ConfigurableOperation>();
     argValues: { [name: string]: any } = {};
     onChange: (val: any) => void;

+ 41 - 0
packages/admin-ui/src/lib/core/src/shared/components/duplicate-entity-dialog/duplicate-entity-dialog.component.html

@@ -0,0 +1,41 @@
+<ng-template vdrDialogTitle>
+    {{ title || 'common.duplicate-entity' | translate }}
+</ng-template>
+<div class="">
+    <ul>
+        <li *ngFor="let entity of entities">
+            {{ getEntityName(entity) }}
+        </li>
+    </ul>
+    <ng-container *ngIf="entityDuplicators$ | async as duplicators">
+        <select
+            name=""
+            id=""
+            *ngIf="1 < duplicators.length"
+            class="mt-4"
+            [ngModel]="selectedDuplicator"
+            (ngModelChange)="setSelectedDuplicator($event)"
+        >
+            <option *ngFor="let duplicator of duplicators" [ngValue]="duplicator">
+                {{ duplicator.description }} ({{ duplicator.code }})
+            </option>
+        </select>
+    </ng-container>
+    <ng-container *ngIf="selectedDuplicator as duplicator">
+        <vdr-configurable-input
+            *ngIf="duplicator.args.length > 0"
+            [hideDescription]="true"
+            [operation]="duplicatorInstance"
+            [operationDefinition]="duplicator"
+            [removable]="false"
+            [formControl]="formGroup"
+        ></vdr-configurable-input>
+    </ng-container>
+</div>
+
+<ng-template vdrDialogButtons>
+    <button type="button" class="btn" (click)="cancel()">{{ 'common.cancel' | translate }}</button>
+    <button type="submit" (click)="duplicate()" [disabled]="!selectedDuplicator" class="btn btn-primary">
+        {{ 'common.duplicate' | translate }}
+    </button>
+</ng-template>

+ 0 - 0
packages/admin-ui/src/lib/core/src/shared/components/duplicate-entity-dialog/duplicate-entity-dialog.component.scss


+ 134 - 0
packages/admin-ui/src/lib/core/src/shared/components/duplicate-entity-dialog/duplicate-entity-dialog.component.ts

@@ -0,0 +1,134 @@
+import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
+import { FormControl } from '@angular/forms';
+import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
+import { assertNever } from '@vendure/common/lib/shared-utils';
+
+import { lastValueFrom, Observable } from 'rxjs';
+import { tap } from 'rxjs/operators';
+import {
+    ConfigurableOperation,
+    DuplicateEntityDocument,
+    GetEntityDuplicatorsDocument,
+    GetEntityDuplicatorsQuery,
+} from '../../../common/generated-types';
+import {
+    configurableDefinitionToInstance,
+    toConfigurableOperationInput,
+} from '../../../common/utilities/configurable-operation-utils';
+import { DataService } from '../../../data/providers/data.service';
+import { Dialog } from '../../../providers/modal/modal.types';
+import { NotificationService } from '../../../providers/notification/notification.service';
+
+type EntityDuplicatorDef = GetEntityDuplicatorsQuery['entityDuplicators'][0];
+
+@Component({
+    selector: 'vdr-duplicate-entity-dialog',
+    templateUrl: './duplicate-entity-dialog.component.html',
+    styleUrls: ['./duplicate-entity-dialog.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class DuplicateEntityDialogComponent<T extends { id: string }> implements OnInit, Dialog<boolean> {
+    resolveWith: (result?: boolean | undefined) => void;
+    protected entityDuplicators$: Observable<EntityDuplicatorDef[]>;
+    protected selectedDuplicator: EntityDuplicatorDef | undefined;
+    protected duplicatorInstance: ConfigurableOperation;
+    protected formGroup = new FormControl<ConfigurableOperation>({
+        code: '',
+        args: [],
+    });
+
+    title?: string;
+    entities: T[];
+    entityName: string;
+    getEntityName: (entity: T) => string;
+
+    constructor(
+        private dataService: DataService,
+        private notificationService: NotificationService,
+        private changeDetectorRef: ChangeDetectorRef,
+    ) {}
+
+    ngOnInit() {
+        this.entityDuplicators$ = this.dataService
+            .query(GetEntityDuplicatorsDocument)
+            .mapSingle(data => data.entityDuplicators.filter(d => d.forEntities.includes(this.entityName)))
+            .pipe(
+                tap(duplicators => {
+                    if (0 < duplicators.length) {
+                        this.setSelectedDuplicator(duplicators[0]);
+                    }
+                }),
+            );
+    }
+
+    setSelectedDuplicator(duplicator: EntityDuplicatorDef) {
+        this.selectedDuplicator = duplicator;
+        this.duplicatorInstance = configurableDefinitionToInstance(this.selectedDuplicator);
+        this.formGroup.patchValue(this.duplicatorInstance);
+        this.changeDetectorRef.markForCheck();
+    }
+
+    async duplicate() {
+        const selectedDuplicator = this.selectedDuplicator;
+        const formValue = this.formGroup.value;
+        if (!selectedDuplicator || !formValue) {
+            return;
+        }
+        const duplicatorInput = toConfigurableOperationInput(this.duplicatorInstance, formValue);
+
+        const succeeded: string[] = [];
+        const failed: Array<{ name: string; message: string }> = [];
+        for (const entity of this.entities) {
+            const { duplicateEntity } = await lastValueFrom(
+                this.dataService.mutate(DuplicateEntityDocument, {
+                    input: {
+                        entityId: entity.id,
+                        entityName: this.entityName,
+                        duplicatorInput,
+                    },
+                }),
+            );
+            switch (duplicateEntity.__typename) {
+                case 'DuplicateEntitySuccess':
+                    succeeded.push(this.getEntityName(entity));
+                    break;
+                case 'DuplicateEntityError':
+                    failed.push({
+                        name: this.getEntityName(entity),
+                        message: duplicateEntity.duplicationError,
+                    });
+                    break;
+                case undefined:
+                    break;
+                default:
+                    assertNever(duplicateEntity);
+            }
+        }
+        if (0 < succeeded.length) {
+            this.notificationService.success(_('common.notify-duplicate-success'), {
+                count: succeeded.length,
+                names: succeeded.join(', '),
+            });
+        }
+        if (0 < failed.length) {
+            const failedCount = failed.length;
+            const maxNotices = 5;
+            const excess = failedCount - maxNotices;
+            for (let i = 0; i < Math.min(failedCount, maxNotices); i++) {
+                const failedItem = failed[i];
+                this.notificationService.error(_('common.notify-duplicate-error'), {
+                    name: failedItem.name,
+                    error: failedItem.message,
+                });
+            }
+            if (excess > 0) {
+                this.notificationService.error(_('common.notify-duplicate-error-excess'), { count: excess });
+            }
+        }
+        this.resolveWith(true);
+    }
+
+    cancel() {
+        this.resolveWith();
+    }
+}

+ 39 - 0
packages/admin-ui/src/lib/core/src/shared/components/duplicate-entity-dialog/duplicate-entity-dialog.graphql.ts

@@ -0,0 +1,39 @@
+import { gql } from 'apollo-angular';
+
+export const GET_ENTITY_DUPLICATORS = gql`
+    query GetEntityDuplicators {
+        entityDuplicators {
+            code
+            description
+            forEntities
+            requiresPermission
+            args {
+                name
+                type
+                required
+                defaultValue
+                list
+                ui
+                label
+                description
+            }
+        }
+    }
+`;
+
+export const DUPLICATE_ENTITY = gql`
+    mutation DuplicateEntity($input: DuplicateEntityInput!) {
+        duplicateEntity(input: $input) {
+            ... on DuplicateEntitySuccess {
+                newEntityId
+            }
+            ... on ErrorResult {
+                errorCode
+                message
+            }
+            ... on DuplicateEntityError {
+                duplicationError
+            }
+        }
+    }
+`;

+ 2 - 0
packages/admin-ui/src/lib/core/src/shared/shared.module.ts

@@ -174,6 +174,7 @@ import { DataTableFilterPresetsComponent } from './components/data-table-filter-
 import { AddFilterPresetButtonComponent } from './components/data-table-filter-presets/add-filter-preset-button.component';
 import { RenameFilterPresetDialogComponent } from './components/data-table-filter-presets/rename-filter-preset-dialog.component';
 import { ActionBarDropdownMenuComponent } from './components/action-bar-dropdown-menu/action-bar-dropdown-menu.component';
+import { DuplicateEntityDialogComponent } from './components/duplicate-entity-dialog/duplicate-entity-dialog.component';
 
 const IMPORTS = [
     ClarityModule,
@@ -320,6 +321,7 @@ const DECLARATIONS = [
     DataTableFilterPresetsComponent,
     AddFilterPresetButtonComponent,
     RenameFilterPresetDialogComponent,
+    DuplicateEntityDialogComponent,
 ];
 
 const DYNAMIC_FORM_INPUTS = [

+ 32 - 0
packages/admin-ui/src/lib/marketing/src/components/promotion-list/promotion-list-bulk-actions.ts

@@ -1,15 +1,20 @@
+import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 import {
     AssignPromotionsToChannelDocument,
+    BulkAction,
     createBulkAssignToChannelAction,
     createBulkDeleteAction,
     createBulkRemoveFromChannelAction,
+    DuplicateEntityDialogComponent,
     GetPromotionListQuery,
     ItemOf,
+    ModalService,
     Permission,
     RemovePromotionsFromChannelDocument,
 } from '@vendure/admin-ui/core';
 import { gql } from 'apollo-angular';
 import { map } from 'rxjs/operators';
+import { PromotionListComponent } from './promotion-list.component';
 
 const ASSIGN_PROMOTIONS_TO_CHANNEL = gql`
     mutation AssignPromotionsToChannel($input: AssignPromotionsToChannelInput!) {
@@ -75,3 +80,30 @@ export const removePromotionsFromChannelBulkAction = createBulkRemoveFromChannel
             })
             .pipe(map(res => res.removePromotionsFromChannel)),
 });
+
+export const duplicatePromotionsBulkAction: BulkAction<
+    ItemOf<GetPromotionListQuery, 'promotions'>,
+    PromotionListComponent
+> = {
+    location: 'promotion-list',
+    label: _('common.duplicate'),
+    icon: 'copy',
+    onClick: ({ injector, selection, hostComponent, clearSelection }) => {
+        const modalService = injector.get(ModalService);
+        modalService
+            .fromComponent(DuplicateEntityDialogComponent<ItemOf<GetPromotionListQuery, 'promotions'>>, {
+                locals: {
+                    entities: selection,
+                    entityName: 'Promotion',
+                    title: _('marketing.duplicate-promotions'),
+                    getEntityName: entity => entity.name,
+                },
+            })
+            .subscribe(result => {
+                if (result) {
+                    clearSelection();
+                    hostComponent.refresh();
+                }
+            });
+    },
+};

+ 2 - 0
packages/admin-ui/src/lib/marketing/src/marketing.module.ts

@@ -14,6 +14,7 @@ import { PromotionDetailComponent } from './components/promotion-detail/promotio
 import {
     assignPromotionsToChannelBulkAction,
     deletePromotionsBulkAction,
+    duplicatePromotionsBulkAction,
     removePromotionsFromChannelBulkAction,
 } from './components/promotion-list/promotion-list-bulk-actions';
 import { PromotionListComponent } from './components/promotion-list/promotion-list.component';
@@ -40,6 +41,7 @@ export class MarketingModule {
         }
         bulkActionRegistryService.registerBulkAction(assignPromotionsToChannelBulkAction);
         bulkActionRegistryService.registerBulkAction(removePromotionsFromChannelBulkAction);
+        bulkActionRegistryService.registerBulkAction(duplicatePromotionsBulkAction);
         bulkActionRegistryService.registerBulkAction(deletePromotionsBulkAction);
 
         pageService.registerPageTab({

+ 10 - 1
packages/admin-ui/src/lib/static/i18n-messages/ar.json

@@ -66,7 +66,6 @@
     "assets": "نماذج",
     "assign-product-to-channel-success": "تم تعيينه بنجاح {count , الجمع , واحد {1 منتج} {{count} products}} إلى {channel}",
     "assign-products-to-channel": "تعيين المنتجات للقناة",
-    "assign-to-channel": "تعيين للقناة",
     "assign-to-named-channel": "تعيين إلى {ChannelCode}",
     "assign-variant-to-channel-success": "تم تعيين {count , الجمع , واحد {1 متغير المنتج} {{count} متغيرات المنتج}} إلى {channel}",
     "assign-variants-to-channel": "تعيين متغيرات المنتج للقناة",
@@ -99,6 +98,9 @@
     "default-currency": "العملة الافتراضية",
     "do-not-inherit-filters": "لا ترث مرشحات",
     "drop-files-to-upload": "إسقاط الملفات للتحميل",
+    "duplicate-collections": "تكرار المجموعات",
+    "duplicate-facets": "تكرار السمات",
+    "duplicate-products": "تكرار المنتجات",
     "edit-facet-values": "تحرير قيم الفئة",
     "edit-options": "تحرير الخيارات",
     "facet": "Facet",
@@ -197,6 +199,7 @@
     "add-note": "اضف ملاحظة",
     "apply": "يتقدم",
     "assign-to-channel": "تعيين للقناة",
+    "assign-to-channels": "تعيين إلى {count, plural, one {قناة} other {القنوات}}",
     "available-currencies": "العملات المتاحة",
     "available-languages": "اللغات المتوفرة",
     "boolean-and": "و",
@@ -234,6 +237,7 @@
     "details": "تفاصيل",
     "disabled": "عاجز",
     "discard-changes": "تجاهل التغييرات",
+    "duplicate": "تكرار",
     "edit": "تحرير",
     "edit-field": "تحرير الحقل",
     "edit-note": "تحرير مذكرة",
@@ -268,6 +272,7 @@
     "name": "اسم",
     "no-alerts": "لا تنبيهات",
     "no-bulk-actions-available": "لا توجد إجراءات كبيرة متوفرة",
+    "no-channel-selected": "",
     "no-results": "لا توجد نتائج",
     "not-applicable": "غير قابل للتطبيق",
     "not-set": "غير مضبوط",
@@ -279,6 +284,9 @@
     "notify-delete-error-with-count": "لا يمكن حذف {count , الجمع , واحد {1 عنصر} {{count} العناصر}}",
     "notify-delete-success": "تم حذف { entity }",
     "notify-delete-success-with-count": "تم حذفه بنجاح {count , الجمع , واحد {1 item} {{count} head}}",
+    "notify-duplicate-error": "تعذر تكرار { name } بسبب خطأ: { error }",
+    "notify-duplicate-error-excess": "تعذر تكرار { count } {count, plural, one {عنصر} other {عناصر}} إضافي{count, plural, one {} other {ن}} بسبب أخطاء",
+    "notify-duplicate-success": "تم تكرار {count, plural, one {عنصر واحد} other {{count} عناصر}} بنجاح: { names }",
     "notify-remove-from-channel-success-with-count": "تم إزالته بنجاح {count} عناصر من القناة",
     "notify-save-changes-error": "حدث خطأ , لم يستطع حفظ التغييرات",
     "notify-saved-changes": "التغييرات المحفوظة",
@@ -489,6 +497,7 @@
     "conditions": "شروط",
     "coupon-code": "رمز الكوبون",
     "create-new-promotion": "إنشاء عرض ترويجي جديد",
+    "duplicate-promotions": "تكرار الترويجات",
     "ends-at": "ينتهي عند",
     "per-customer-limit": "حد كل عميل",
     "per-customer-limit-tooltip": "الحد الأقصى لعدد مرات استخدام هذا العرض من قبل عميل واحد",

+ 10 - 1
packages/admin-ui/src/lib/static/i18n-messages/cs.json

@@ -66,7 +66,6 @@
     "assets": "",
     "assign-product-to-channel-success": "Produkt byl úspěšně přiřazen do \"{ channel }\"",
     "assign-products-to-channel": "Přiřadit produkty do kanálu",
-    "assign-to-channel": "Přiřadit do kanálu",
     "assign-to-named-channel": "Přiřadit do { channelCode }",
     "assign-variant-to-channel-success": "Varianta byla úspěšně přiřazena do kanálu \"{ channel }\"",
     "assign-variants-to-channel": "Přiřadit varianty do kanálu",
@@ -99,6 +98,9 @@
     "default-currency": "",
     "do-not-inherit-filters": "",
     "drop-files-to-upload": "Přetáhněte soubory k nahrávání",
+    "duplicate-collections": "Duplikovat kolekce",
+    "duplicate-facets": "Duplikovat prvky",
+    "duplicate-products": "Duplikovat produkty",
     "edit-facet-values": "",
     "edit-options": "",
     "facet": "",
@@ -197,6 +199,7 @@
     "add-note": "Přidat poznámku",
     "apply": "",
     "assign-to-channel": "",
+    "assign-to-channels": "Přiřadit k {count, plural, one {kanálu} other {kanálům}}",
     "available-currencies": "",
     "available-languages": "Dostupné jazyky",
     "boolean-and": "",
@@ -234,6 +237,7 @@
     "details": "Detaily",
     "disabled": "Vypnuto",
     "discard-changes": "Zrušit změny",
+    "duplicate": "Duplikovat",
     "edit": "Upravit",
     "edit-field": "Upravit pole",
     "edit-note": "Upravit poznámku",
@@ -268,6 +272,7 @@
     "name": "jméno",
     "no-alerts": "",
     "no-bulk-actions-available": "",
+    "no-channel-selected": "",
     "no-results": "Žádné výsledky",
     "not-applicable": "",
     "not-set": "Nenastaveno",
@@ -279,6 +284,9 @@
     "notify-delete-error-with-count": "",
     "notify-delete-success": "Smazáno: { entity }",
     "notify-delete-success-with-count": "",
+    "notify-duplicate-error": "Nelze duplikovat { name } kvůli chybě: { error }",
+    "notify-duplicate-error-excess": "Nelze duplikovat dalších { count } {count, plural, one {položka} other {položek}} kvůli chybám",
+    "notify-duplicate-success": "Bylo úspěšně zduplikováno {count, plural, one {1 položka} other {{count} položek}}: { names }",
     "notify-remove-from-channel-success-with-count": "",
     "notify-save-changes-error": "Vyskytla se chyba, nebylo možné uložit změny",
     "notify-saved-changes": "Změny uloženy",
@@ -489,6 +497,7 @@
     "conditions": "Podmínky",
     "coupon-code": "Kód kupónu",
     "create-new-promotion": "Nová propagace",
+    "duplicate-promotions": "Duplikovat akce",
     "ends-at": "Končí",
     "per-customer-limit": "Limit za zákazníka",
     "per-customer-limit-tooltip": "Maximální počet použití tohoto promočního kódu jedním zákazníkem",

+ 10 - 1
packages/admin-ui/src/lib/static/i18n-messages/de.json

@@ -66,7 +66,6 @@
     "assets": "Assets",
     "assign-product-to-channel-success": "Produkt erfolgreich an \"{ channel }\" zugewiesen",
     "assign-products-to-channel": "Produkte dem Kanal zuweisen",
-    "assign-to-channel": "Zuweisung an Kanal",
     "assign-to-named-channel": "Zuweisen an { channelCode }",
     "assign-variant-to-channel-success": "Produktvariante erfolgreich dem Kanal zugewiesen",
     "assign-variants-to-channel": "Produktvarianten Kanälen zuweisen",
@@ -99,6 +98,9 @@
     "default-currency": "Standard Währung",
     "do-not-inherit-filters": "Erbe keine Filter",
     "drop-files-to-upload": "Dateien zum Hochladen ablegen",
+    "duplicate-collections": "Sammlungen duplizieren",
+    "duplicate-facets": "Facetten duplizieren",
+    "duplicate-products": "Produkte duplizieren",
     "edit-facet-values": "Facettenwerte bearbeiten",
     "edit-options": "Optionen bearbeiten",
     "facet": "Facette",
@@ -197,6 +199,7 @@
     "add-note": "Notiz hinzufügen",
     "apply": "Übernehmen",
     "assign-to-channel": "Kanal zuweisen",
+    "assign-to-channels": "Zuweisen zu {count, plural, one {Kanal} other {Kanälen}}",
     "available-currencies": "Verfügbare Währungen",
     "available-languages": "Verfügbare Sprachen",
     "boolean-and": "und",
@@ -234,6 +237,7 @@
     "details": "Details",
     "disabled": "Deaktiviert",
     "discard-changes": "Änderungen verwerfen",
+    "duplicate": "Duplizieren",
     "edit": "Bearbeiten",
     "edit-field": "Feld bearbeiten",
     "edit-note": "Notiz bearbeiten",
@@ -268,6 +272,7 @@
     "name": "Name",
     "no-alerts": "Keine Alarme",
     "no-bulk-actions-available": "Keine Sammelaktionen verfügbar",
+    "no-channel-selected": "",
     "no-results": "Keine Ergebnisse",
     "not-applicable": "Nicht zutreffend",
     "not-set": "Nicht festgelegt",
@@ -279,6 +284,9 @@
     "notify-delete-error-with-count": "Konnte {count, plural, one {1 Element} other {{count} Elemente}} nicht löschen",
     "notify-delete-success": "{ entity } gelöscht",
     "notify-delete-success-with-count": "{count, plural, one {1 Element} other {{count} Elemente}} erfolgreich gelöscht",
+    "notify-duplicate-error": "Konnte { name } nicht duplizieren wegen eines Fehlers: { error }",
+    "notify-duplicate-error-excess": "Zusätzliche { count } {count, plural, one {Element} other {Elemente}} konnten aufgrund von Fehlern nicht dupliziert werden",
+    "notify-duplicate-success": "Erfolgreich {count, plural, one {1 Element} other {{count} Elemente}} dupliziert: { names }",
     "notify-remove-from-channel-success-with-count": "{ count } Elemente erfolgreich aus Kanal entfernt",
     "notify-save-changes-error": "Ein Fehler ist aufgetreten, die Änderungen konnten nicht gespeichert werden",
     "notify-saved-changes": "Änderungen gespeichert",
@@ -489,6 +497,7 @@
     "conditions": "Bedingungen",
     "coupon-code": "Gutscheincode",
     "create-new-promotion": "Neue Werbeaktion erstellen",
+    "duplicate-promotions": "Aktionen duplizieren",
     "ends-at": "Endet am",
     "per-customer-limit": "Begrenzung pro Kunde",
     "per-customer-limit-tooltip": "Maximale Anzahl der Verwendungen dieser Aktion pro einzelnen Kunden",

+ 10 - 2
packages/admin-ui/src/lib/static/i18n-messages/en.json

@@ -66,8 +66,6 @@
     "assets": "Assets",
     "assign-product-to-channel-success": "Successfully assigned {count, plural, one {1 product} other {{count} products}} to { channel }",
     "assign-products-to-channel": "Assign products to channel",
-    "assign-to-channel": "Assign to channel",
-    "assign-to-channels": "Assign to {count, plural, one {channel} other {channels}}",
     "assign-to-named-channel": "Assign to { channelCode }",
     "assign-variant-to-channel-success": "Successfully assigned {count, plural, one {1 product variant} other {{count} product variants}} to { channel }",
     "assign-variants-to-channel": "Assign product variants to channel",
@@ -100,6 +98,9 @@
     "default-currency": "Default currency",
     "do-not-inherit-filters": "Do not inherit filters",
     "drop-files-to-upload": "Drop files to upload",
+    "duplicate-collections": "Duplicate collections",
+    "duplicate-facets": "Duplicate facets",
+    "duplicate-products": "Duplicate products",
     "edit-facet-values": "Edit facet values",
     "edit-options": "Edit options",
     "facet": "Facet",
@@ -198,6 +199,7 @@
     "add-note": "Add note",
     "apply": "Apply",
     "assign-to-channel": "Assign to channel",
+    "assign-to-channels": "Assign to {count, plural, one {channel} other {channels}}",
     "available-currencies": "Available currencies",
     "available-languages": "Available languages",
     "boolean-and": "and",
@@ -235,6 +237,7 @@
     "details": "Details",
     "disabled": "Disabled",
     "discard-changes": "Discard changes",
+    "duplicate": "Duplicate",
     "edit": "Edit",
     "edit-field": "Edit field",
     "edit-note": "Edit note",
@@ -269,6 +272,7 @@
     "name": "Name",
     "no-alerts": "No alerts",
     "no-bulk-actions-available": "No bulk actions available",
+    "no-channel-selected": "",
     "no-results": "No results",
     "not-applicable": "Not applicable",
     "not-set": "Not set",
@@ -280,6 +284,9 @@
     "notify-delete-error-with-count": "Could not delete {count, plural, one {1 item} other {{count} items}}",
     "notify-delete-success": "Deleted { entity }",
     "notify-delete-success-with-count": "Successfully deleted {count, plural, one {1 item} other {{count} items}}",
+    "notify-duplicate-error": "Could not duplicate { name } due to an error: { error }",
+    "notify-duplicate-error-excess": "An additional { count } {count, plural, one {item} other {items}} could not be duplicated due to errors",
+    "notify-duplicate-success": "Successfully duplicated {count, plural, one {1 item} other {{count} items}}: { names }",
     "notify-remove-from-channel-success-with-count": "Successfully removed { count } items from channel",
     "notify-save-changes-error": "An error occurred, could not save changes",
     "notify-saved-changes": "Saved changes",
@@ -490,6 +497,7 @@
     "conditions": "Conditions",
     "coupon-code": "Coupon code",
     "create-new-promotion": "Create new promotion",
+    "duplicate-promotions": "Duplicate promotions",
     "ends-at": "Ends at",
     "per-customer-limit": "Per-customer limit",
     "per-customer-limit-tooltip": "Maximum number of times this promotion can be used by a single customer",

+ 10 - 1
packages/admin-ui/src/lib/static/i18n-messages/es.json

@@ -66,7 +66,6 @@
     "assets": "Recursos",
     "assign-product-to-channel-success": "Producto asignado a \"{ channel }\" con éxito",
     "assign-products-to-channel": "Asignar productos a canal de ventas",
-    "assign-to-channel": "Asignar a canal de ventas",
     "assign-to-named-channel": "Asignar a { channelCode }",
     "assign-variant-to-channel-success": "Variante de producto asignada a \"{ channel }\" con éxito",
     "assign-variants-to-channel": "Asignar variantes de producto a canal de ventas",
@@ -99,6 +98,9 @@
     "default-currency": "Moneda por defecto",
     "do-not-inherit-filters": "No heredar filtros",
     "drop-files-to-upload": "Arrastra recursos para subirlos",
+    "duplicate-collections": "Duplicar colecciones",
+    "duplicate-facets": "Duplicar facetas",
+    "duplicate-products": "Duplicar productos",
     "edit-facet-values": "Edita valores de la faceta",
     "edit-options": "Editar opciones",
     "facet": "Faceta",
@@ -197,6 +199,7 @@
     "add-note": "Añadir nota",
     "apply": "Aplicar",
     "assign-to-channel": "Asignar a canal",
+    "assign-to-channels": "Asignar a {count, plural, one {canal} other {canales}}",
     "available-currencies": "Monedas disponibles",
     "available-languages": "Idiomas disponibles",
     "boolean-and": "y",
@@ -234,6 +237,7 @@
     "details": "Detalles",
     "disabled": "Deshabilitado",
     "discard-changes": "Descartar cambios",
+    "duplicate": "Duplicar",
     "edit": "Editar",
     "edit-field": "Editar campo",
     "edit-note": "Editar nota",
@@ -268,6 +272,7 @@
     "name": "Nombre",
     "no-alerts": "No hay alertas",
     "no-bulk-actions-available": "No hay acciones agrupadas disponibles",
+    "no-channel-selected": "",
     "no-results": "Sin resultados",
     "not-applicable": "No aplicable",
     "not-set": "Sin fijar",
@@ -279,6 +284,9 @@
     "notify-delete-error-with-count": "No se ha podido borrar {count, plural, one {1 item} other {{count} items}}",
     "notify-delete-success": "Eliminado { entity }",
     "notify-delete-success-with-count": "Borrados con éxito {count, plural, one {1 item} other {{count} items}}",
+    "notify-duplicate-error": "No se pudo duplicar { name } debido a un error: { error }",
+    "notify-duplicate-error-excess": "No se pudieron duplicar { count } {count, plural, one {elemento} other {elementos}} adicionales debido a errores",
+    "notify-duplicate-success": "Duplicado correctamente {count, plural, one {1 elemento} other {{count} elementos}}: { names }",
     "notify-remove-from-channel-success-with-count": "Eliminados con éxito { count } items del canal",
     "notify-save-changes-error": "Ha ocurrido un problema, imposible de guardar cambios",
     "notify-saved-changes": "Cambios guardados",
@@ -489,6 +497,7 @@
     "conditions": "Condiciones",
     "coupon-code": "Código de descuento",
     "create-new-promotion": "Crear nueva promoción",
+    "duplicate-promotions": "Duplicar promociones",
     "ends-at": "Finaliza el",
     "per-customer-limit": "Límite por cliente",
     "per-customer-limit-tooltip": "Número máximo de veces que esta promoción puede ser utilizada por un solo cliente",

+ 10 - 1
packages/admin-ui/src/lib/static/i18n-messages/fa.json

@@ -66,7 +66,6 @@
     "assets": "دارایی ها",
     "assign-product-to-channel-success": "{count} محصول به { channel } تخصیص داده شد",
     "assign-products-to-channel": "تخصیص محصولات به کانال",
-    "assign-to-channel": "تخصیص به کانال",
     "assign-to-named-channel": "تخصیص به { channelCode }",
     "assign-variant-to-channel-success": "{count} نوع محصول به { channel } تخصیص داده شد",
     "assign-variants-to-channel": "تخصیص نوع محصول به کانال",
@@ -99,6 +98,9 @@
     "default-currency": "حذف واحد پولی",
     "do-not-inherit-filters": "فیلترها به ارث برده نشود",
     "drop-files-to-upload": "فایل ها را رها کنید تا آپلود شوند",
+    "duplicate-collections": "تکثیر مجموعه‌ها",
+    "duplicate-facets": "تکثیر ابعاد",
+    "duplicate-products": "تکثیر محصولات",
     "edit-facet-values": "ویرایش مقادیر ویژگی ها",
     "edit-options": "ویرایش قابلیت ها",
     "facet": "ویژگی",
@@ -197,6 +199,7 @@
     "add-note": "افزودن یادداشت",
     "apply": "اعمال",
     "assign-to-channel": "تخصیص به کانال",
+    "assign-to-channels": "تخصیص به {count, plural, one {کانال} other {کانال‌ها}}",
     "available-currencies": "واحد های پولی موجود",
     "available-languages": "زبان های موجود",
     "boolean-and": "و",
@@ -234,6 +237,7 @@
     "details": "جزئیات",
     "disabled": "غیرفعال است",
     "discard-changes": "لغو تغییرات",
+    "duplicate": "تکثیر",
     "edit": "ویرایش",
     "edit-field": "ویرایش فیلد",
     "edit-note": "ویرایش یادداشت",
@@ -268,6 +272,7 @@
     "name": "نام",
     "no-alerts": "هیچ پیغامی وجود ندارد",
     "no-bulk-actions-available": "هیچ اقدام انبوهی در دسترس نیست.",
+    "no-channel-selected": "",
     "no-results": "بدون نتیجه",
     "not-applicable": "قابل اجرا نیست",
     "not-set": "تنظیم نشده است.",
@@ -279,6 +284,9 @@
     "notify-delete-error-with-count": "تعداد {count} آیتم حذف نشد",
     "notify-delete-success": "{ entity } حذف شد.",
     "notify-delete-success-with-count": "تعداد {count} آیتم با موفقیت حذف شد.",
+    "notify-duplicate-error": "نمی‌توان { name } را به دلیل خطا تکثیر کرد: { error }",
+    "notify-duplicate-error-excess": "نمی‌توان { count } {count, plural, one {مورد} other {مورد}} اضافی به دلیل خطا تکثیر کرد",
+    "notify-duplicate-success": "تکثیر {count, plural, one {1 مورد} other {{count} مورد}} با موفقیت انجام شد: { names }",
     "notify-remove-from-channel-success-with-count": "{ count } آیتم با موفقیت از کانال حذف شد",
     "notify-save-changes-error": "خطایی روی داد، تغییرات ذخیره نشد.",
     "notify-saved-changes": "تغییرات ذخیره شد",
@@ -489,6 +497,7 @@
     "conditions": "شرایط",
     "coupon-code": "کد تخفیف",
     "create-new-promotion": "ایجاد تبلیغ جدید",
+    "duplicate-promotions": "تکثیر تبلیغات",
     "ends-at": "تاریخ پایان",
     "per-customer-limit": "محدودیت به ازای مشتری",
     "per-customer-limit-tooltip": "حداکثر تعداد دفعاتی که یک مشتری می‌تواند از این تخفیف استفاده کند",

+ 10 - 1
packages/admin-ui/src/lib/static/i18n-messages/fr.json

@@ -66,7 +66,6 @@
     "assets": "Ressources",
     "assign-product-to-channel-success": "produit attribué au canal \"{ channel }\"",
     "assign-products-to-channel": "Attribuer les produits au canal",
-    "assign-to-channel": "Attribuer au canal",
     "assign-to-named-channel": "Attribuer à { channelCode }",
     "assign-variant-to-channel-success": "Variation produit attribuée au canal \"{ channel }\"",
     "assign-variants-to-channel": "Attribuer une variation du produit au canal",
@@ -99,6 +98,9 @@
     "default-currency": "Devise par défaut",
     "do-not-inherit-filters": "Ne pas hériter des filtres",
     "drop-files-to-upload": "Déposer des fichiers pour téléverser",
+    "duplicate-collections": "Dupliquer les collections",
+    "duplicate-facets": "Dupliquer les facettes",
+    "duplicate-products": "Dupliquer les produits",
     "edit-facet-values": "Modifier les valeurs de composant",
     "edit-options": "Modifier les options",
     "facet": "Valeur de composant",
@@ -197,6 +199,7 @@
     "add-note": "Ajouter une note",
     "apply": "Appliquer",
     "assign-to-channel": "Assigner à un canal",
+    "assign-to-channels": "Assigner à {count, plural, one {canal} other {canaux}}",
     "available-currencies": "Devises disponibles",
     "available-languages": "Langues disponibles",
     "boolean-and": "et",
@@ -234,6 +237,7 @@
     "details": "Détails",
     "disabled": "Désactivé",
     "discard-changes": "Annuler les changements",
+    "duplicate": "Dupliquer",
     "edit": "Editer",
     "edit-field": "Editer champ",
     "edit-note": "Editer note",
@@ -268,6 +272,7 @@
     "name": "Nom",
     "no-alerts": "Pas d'alerte",
     "no-bulk-actions-available": "Aucune action groupée disponible",
+    "no-channel-selected": "",
     "no-results": "Aucun resultat",
     "not-applicable": "",
     "not-set": "Non défini",
@@ -279,6 +284,9 @@
     "notify-delete-error-with-count": "Impossible de supprimer {count, plural, one {1 item} other {{count} items}}",
     "notify-delete-success": "{ entity } supprimé",
     "notify-delete-success-with-count": "{count, plural, one {1 item} other {{count} items}} ont bien été supprimés",
+    "notify-duplicate-error": "Impossible de dupliquer { name } en raison d'une erreur : { error }",
+    "notify-duplicate-error-excess": "{ count } {count, plural, one {élément} other {éléments}} supplémentaire{count, plural, one {} other {s}} n'ont pas pu être dupliqué{count, plural, one {} other {s}} en raison d'erreurs",
+    "notify-duplicate-success": "{count, plural, one {1 élément} other {{count} éléments}} dupliqués avec succès : { names }",
     "notify-remove-from-channel-success-with-count": "{count, plural, one {1 item} other {{count} items}} ont bien été retirés du canal",
     "notify-save-changes-error": "Une erreur est survenue, Changements non enregistrés",
     "notify-saved-changes": "Changements enregistrés",
@@ -489,6 +497,7 @@
     "conditions": "Conditions",
     "coupon-code": "Code promo",
     "create-new-promotion": "Creer nouvelle promotion",
+    "duplicate-promotions": "Dupliquer les promotions",
     "ends-at": "Termine au",
     "per-customer-limit": "Limite par client",
     "per-customer-limit-tooltip": "Nombre maximum de fois où cette promotion peut être utilisée par un seul client",

+ 10 - 1
packages/admin-ui/src/lib/static/i18n-messages/he.json

@@ -66,7 +66,6 @@
     "assets": "נכסים",
     "assign-product-to-channel-success": "הקצאה מוצלחת של {count, plural, one {מוצר 1} other {{count} מוצרים}} ל- { channel }",
     "assign-products-to-channel": "הקצה מוצרים לערוץ",
-    "assign-to-channel": "הקצה לערוץ",
     "assign-to-named-channel": "הקצה ל-{ channelCode }",
     "assign-variant-to-channel-success": "הקצאה מוצלחת של {count, plural, one {1 וריאנט מוצר} other {{count} וריאנטים של מוצר}} ל-{ channel }",
     "assign-variants-to-channel": "הקצה וריאנטים של מוצר לערוץ",
@@ -99,6 +98,9 @@
     "default-currency": "מטבע ברירת מחדל",
     "do-not-inherit-filters": "אל תרש את המסננים",
     "drop-files-to-upload": "שחרר קבצים להעלאה",
+    "duplicate-collections": "שכפול אוספים",
+    "duplicate-facets": "שכפול מאפיינים",
+    "duplicate-products": "שכפול מוצרים",
     "edit-facet-values": "ערוך ערכי גבעול",
     "edit-options": "ערוך אפשרויות",
     "facet": "גבעול",
@@ -197,6 +199,7 @@
     "add-note": "הוסף הערה",
     "apply": "החל",
     "assign-to-channel": "הקצה לערוץ",
+    "assign-to-channels": "הקצאה ל{count, plural, one {ערוץ} other {ערוצים}}",
     "available-currencies": "מטבעות זמינים",
     "available-languages": "שפות זמינות",
     "boolean-and": "וגם",
@@ -234,6 +237,7 @@
     "details": "פרטים",
     "disabled": "מושבת",
     "discard-changes": "בטל שינויים",
+    "duplicate": "שכפול",
     "edit": "ערוך",
     "edit-field": "ערוך שדה",
     "edit-note": "ערוך הערה",
@@ -268,6 +272,7 @@
     "name": "שם",
     "no-alerts": "אין התראות",
     "no-bulk-actions-available": "אין פעולות גורפות זמינות",
+    "no-channel-selected": "",
     "no-results": "אין תוצאות",
     "not-applicable": "לא רלוונטי",
     "not-set": "לא מוגדר",
@@ -279,6 +284,9 @@
     "notify-delete-error-with-count": "לא הצלחנו למחוק {count, plural, one {פריט 1} other {{count} פריטים}}",
     "notify-delete-success": "מחקנו { entity }",
     "notify-delete-success-with-count": "מחקנו בהצלחה {count, plural, one {פריט 1} other {{count} פריטים}}",
+    "notify-duplicate-error": "לא ניתן לשכפל { name } עקב שגיאה: { error }",
+    "notify-duplicate-error-excess": "{ count } {count, plural, one {פריט} other {פריטים}} נוספים לא ניתן לשכפל עקב שגיאות",
+    "notify-duplicate-success": "{count, plural, one {פריט אחד} other {{count} פריטים}} שוכפלו בהצלחה: { names }",
     "notify-remove-from-channel-success-with-count": "הוסרו בהצלחה { count } פריטים מהערוץ",
     "notify-save-changes-error": "אירעה שגיאה, לא הצלחנו לשמור את השינויים",
     "notify-saved-changes": "שינויים נשמרו",
@@ -489,6 +497,7 @@
     "conditions": "תנאים",
     "coupon-code": "קוד קופון",
     "create-new-promotion": "צור מבצע חדש",
+    "duplicate-promotions": "שכפול מבצעים",
     "ends-at": "מסתיים בתאריך",
     "per-customer-limit": "הגבלה ללקוח",
     "per-customer-limit-tooltip": "מספר הפעמים המרבי שבו ניתן להשתמש במבצע זה על ידי לקוח בודד",

+ 10 - 1
packages/admin-ui/src/lib/static/i18n-messages/hr.json

@@ -66,7 +66,6 @@
     "assets": "Sredstva",
     "assign-product-to-channel-success": "Uspješno dodijeljeno {count, plural, one {1 proizvod} other {{count} proizvoda}} kanalu {channel}",
     "assign-products-to-channel": "Dodijeli proizvode kanalu",
-    "assign-to-channel": "Dodijeli kanalu",
     "assign-to-named-channel": "Dodijeli kanalu {channelCode}",
     "assign-variant-to-channel-success": "Uspješno dodijeljeno {count, plural, one {1 varijanta proizvoda} other {{count} varijanti proizvoda}} kanalu {channel}",
     "assign-variants-to-channel": "Dodijeli varijante proizvoda kanalu",
@@ -99,6 +98,9 @@
     "default-currency": "Zadana valuta",
     "do-not-inherit-filters": "Ne nasljeđuj filtre",
     "drop-files-to-upload": "Ispustite datoteke za prijenos",
+    "duplicate-collections": "Dupliciranje zbirki",
+    "duplicate-facets": "Dupliciranje aspekata",
+    "duplicate-products": "Dupliciranje proizvoda",
     "edit-facet-values": "Uredi vrijednosti aspekta",
     "edit-options": "Uredi opcije",
     "facet": "Aspekt",
@@ -197,6 +199,7 @@
     "add-note": "Dodaj bilješku",
     "apply": "Primijeni",
     "assign-to-channel": "Dodijeli kanalu",
+    "assign-to-channels": "Dodijeli {count, plural, one {kanalu} other {kanalima}}",
     "available-currencies": "Dostupne valute",
     "available-languages": "Dostupni jezici",
     "boolean-and": "i",
@@ -234,6 +237,7 @@
     "details": "Detalji",
     "disabled": "Onemogućeno",
     "discard-changes": "Odbaci promjene",
+    "duplicate": "Duplicirati",
     "edit": "Uredi",
     "edit-field": "Uredi polje",
     "edit-note": "Uredi bilješku",
@@ -268,6 +272,7 @@
     "name": "Ime",
     "no-alerts": "Nema upozorenja",
     "no-bulk-actions-available": "Nema dostupnih grupnih radnji",
+    "no-channel-selected": "",
     "no-results": "Nema rezultata",
     "not-applicable": "Nije primjenjivo",
     "not-set": "Nije postavljeno",
@@ -279,6 +284,9 @@
     "notify-delete-error-with-count": "Nije moguće izbrisati {count, plural, one {1 stavku} other {{count} stavki}}",
     "notify-delete-success": "Izbrisano { entity }",
     "notify-delete-success-with-count": "Uspješno izbrisano {count, plural, one {1 stavka} other {{count} stavki}}",
+    "notify-duplicate-error": "Nije moguće duplicirati { name } zbog pogreške: { error }",
+    "notify-duplicate-error-excess": "Dodatnih { count } {count, plural, one {stavka} other {stavki}} nije moguće duplicirati zbog pogrešaka",
+    "notify-duplicate-success": "Uspješno duplicirano {count, plural, one {1 stavka} other {{count} stavki}}: { names }",
     "notify-remove-from-channel-success-with-count": "Uspješno uklonjeno { count } stavki iz kanala",
     "notify-save-changes-error": "Došlo je do pogreške, nije moguće spremiti promjene",
     "notify-saved-changes": "Spremljene promjene",
@@ -489,6 +497,7 @@
     "conditions": "Uvjeti",
     "coupon-code": "Kod kupona",
     "create-new-promotion": "Stvori novu promociju",
+    "duplicate-promotions": "Dupliciranje promocija",
     "ends-at": "Završava u",
     "per-customer-limit": "Limit po kupcu",
     "per-customer-limit-tooltip": "Maksimalni broj puta koje ovu promociju može koristiti jedan pojedinačni kupac",

+ 10 - 1
packages/admin-ui/src/lib/static/i18n-messages/it.json

@@ -66,7 +66,6 @@
     "assets": "Media",
     "assign-product-to-channel-success": "Prodotto assegnato correttamente a \"{ channel }\"",
     "assign-products-to-channel": "Assegna prodotto al canale",
-    "assign-to-channel": "Assegna a un canale",
     "assign-to-named-channel": "Assegna al canale { channelCode }",
     "assign-variant-to-channel-success": "Variante assegnata con successo a \"{ channel }\"",
     "assign-variants-to-channel": "Assegna varianti al canale",
@@ -99,6 +98,9 @@
     "default-currency": "Valuta predefinita",
     "do-not-inherit-filters": "Non ereditare i filtri",
     "drop-files-to-upload": "Trascina file da caricare",
+    "duplicate-collections": "Duplica collezioni",
+    "duplicate-facets": "Duplica caratteristiche",
+    "duplicate-products": "Duplica prodotti",
     "edit-facet-values": "Modifica il valore degli attributi",
     "edit-options": "Modifica opzioni",
     "facet": "Attributo",
@@ -197,6 +199,7 @@
     "add-note": "Aggiungi nota",
     "apply": "Applica",
     "assign-to-channel": "Assegna a un canale",
+    "assign-to-channels": "Assegna a {count, plural, one {canale} other {canali}}",
     "available-currencies": "Valute disponibili",
     "available-languages": "Lingue disponibili",
     "boolean-and": "e",
@@ -234,6 +237,7 @@
     "details": "Dettagli",
     "disabled": "Disabilitato",
     "discard-changes": "Annulla modifiche",
+    "duplicate": "Duplica",
     "edit": "Modifica",
     "edit-field": "Modifica campo",
     "edit-note": "Modifica nota",
@@ -268,6 +272,7 @@
     "name": "Nome",
     "no-alerts": "Nessun avviso",
     "no-bulk-actions-available": "Nessuna azione di massa disponibile",
+    "no-channel-selected": "",
     "no-results": "Nessun risultato",
     "not-applicable": "Non applicabile",
     "not-set": "Non impostato",
@@ -279,6 +284,9 @@
     "notify-delete-error-with-count": "Impossibile eliminare {count, plural, one {1 elemento} other {{count} elementi}}",
     "notify-delete-success": "Cancellato { entity }",
     "notify-delete-success-with-count": "Eliminati con successo {count, plural, one {1 elemento} other {{count} elementi}}",
+    "notify-duplicate-error": "Impossibile duplicare { name } a causa di un errore: { error }",
+    "notify-duplicate-error-excess": "Non è stato possibile duplicare ulteriori { count } {count, plural, one {elemento} other {elementi}} a causa di errori",
+    "notify-duplicate-success": "Duplicati con successo {count, plural, one {1 elemento} other {{count} elementi}}: { names }",
     "notify-remove-from-channel-success-with-count": "Rimossi con successo { count } elementi dal canale",
     "notify-save-changes-error": "Si è verificato un errore, impossibile salvare le modifiche",
     "notify-saved-changes": "Modifiche salvate",
@@ -489,6 +497,7 @@
     "conditions": "Condizioni",
     "coupon-code": "Codice coupon",
     "create-new-promotion": "Crea nuova promozione",
+    "duplicate-promotions": "Duplica promozioni",
     "ends-at": "Finisce a",
     "per-customer-limit": "Limiti per cliente",
     "per-customer-limit-tooltip": "Numero massimo di volte in cui questa promozione può essere utilizzata da un singolo cliente",

+ 10 - 1
packages/admin-ui/src/lib/static/i18n-messages/nb.json

@@ -66,7 +66,6 @@
     "assets": "Ressurser",
     "assign-product-to-channel-success": "{count, plural, one {1 produkt} other {{count} Produkter}} lagt til { channel }",
     "assign-products-to-channel": "Legg produkter til kanal",
-    "assign-to-channel": "Legg til kanal",
     "assign-to-named-channel": "Legg til { channelCode }",
     "assign-variant-to-channel-success": "{count, plural, one {1 produktvariant} other {{count} Produktvarianter}} er nå lagt til { channel }",
     "assign-variants-to-channel": "Legg til produktvarianter til kanal",
@@ -99,6 +98,9 @@
     "default-currency": "Standard valuta",
     "do-not-inherit-filters": "Ikke arv filtre",
     "drop-files-to-upload": "Slipp filer for opplasting",
+    "duplicate-collections": "Dupliserte samlinger",
+    "duplicate-facets": "Dupliserte fasetter",
+    "duplicate-products": "Dupliserte produkter",
     "edit-facet-values": "Rediger fasettverdier",
     "edit-options": "Rediger alternativer",
     "facet": "Fasett",
@@ -197,6 +199,7 @@
     "add-note": "Legg til notat",
     "apply": "Bruk",
     "assign-to-channel": "Tilordne til kanal",
+    "assign-to-channels": "Tilordne til {count, plural, one {kanal} other {kanaler}}",
     "available-currencies": "Tilgjengelige valutaer",
     "available-languages": "Tilgjengelige språk",
     "boolean-and": "og",
@@ -234,6 +237,7 @@
     "details": "Detaljer",
     "disabled": "Deaktivert",
     "discard-changes": "Forkast endringer",
+    "duplicate": "Duplisere",
     "edit": "Rediger",
     "edit-field": "Rediger felt",
     "edit-note": "Rediger notat",
@@ -268,6 +272,7 @@
     "name": "Navn",
     "no-alerts": "Ingen varsler",
     "no-bulk-actions-available": "Ingen massehandlinger tilgjengelige",
+    "no-channel-selected": "",
     "no-results": "Ingen resultater",
     "not-applicable": "Ikke relevant",
     "not-set": "Ikke satt",
@@ -279,6 +284,9 @@
     "notify-delete-error-with-count": "Kunne ikke slette {count, plural, one {1 element} other {{count} elementer}}",
     "notify-delete-success": "Slettet { entity }",
     "notify-delete-success-with-count": "Vellykket slettet {count, plural, one {1 element} other {{count} elementer}}",
+    "notify-duplicate-error": "Kunne ikke duplisere { name } på grunn av en feil: { error }",
+    "notify-duplicate-error-excess": "Kunne ikke duplisere ytterligere { count } {count, plural, one {element} other {elementer}} på grunn av feil",
+    "notify-duplicate-success": "Vellykket duplisert {count, plural, one {1 element} other {{count} elementer}}: { names }",
     "notify-remove-from-channel-success-with-count": "Fjernet vellykket { count } elementer fra kanal",
     "notify-save-changes-error": "En feil oppstod, kunne ikke lagre endringer",
     "notify-saved-changes": "Lagrede endringer",
@@ -489,6 +497,7 @@
     "conditions": "Betingelser",
     "coupon-code": "Kupongkode",
     "create-new-promotion": "Opprett ny kampanje",
+    "duplicate-promotions": "Dupliserte kampanjer",
     "ends-at": "Slutter",
     "per-customer-limit": "Grense per kunde",
     "per-customer-limit-tooltip": "Maksimalt antall ganger denne kampanjen kan brukes av én kunde",

+ 13 - 4
packages/admin-ui/src/lib/static/i18n-messages/ne.json

@@ -4,7 +4,7 @@
   },
   "asset": {
     "add-asset": "फाइल थप्नुहोस्",
-    "add-asset-with-count": "थप्नुहोस् {count, plural, =0 {फाइल} एउटा {१ फाइल} अन्य {{count} फाइल}}",
+    "add-asset-with-count": "थप्नुहोस् {count, plural, =0 {फाइल} one {१ फाइल} other {{count} फाइल}}",
     "assets-selected-count": "{ count } फाइल चयन गरियो",
     "dimensions": "आयामहरू",
     "focal-point": "बिन्दु",
@@ -66,7 +66,6 @@
     "assets": "फाइलहरू",
     "assign-product-to-channel-success": "{count, plural, one {1 समान} other {{count} समानहरू}} लाई { channel } मा सफलतापूर्वक लगाइएको",
     "assign-products-to-channel": "समानहरूलाई च्यानलमा लगाउनुहोस्",
-    "assign-to-channel": "च्यानलमा लगाउनुहोस्",
     "assign-to-named-channel": "{ channelCode } मा लगाउनुहोस्",
     "assign-variant-to-channel-success": "{count, plural, one {1 समान विविधता} other {{count} समान विविधताहरू}} लाई { channel } मा सफलतापूर्वक लगाइएको",
     "assign-variants-to-channel": "समान विविधताहरूलाई च्यानलमा लगाउनुहोस्",
@@ -99,6 +98,9 @@
     "default-currency": "पूर्वनिर्धारित मुद्रा",
     "do-not-inherit-filters": "फिल्टरहरू समावेश गर्न नगर्नुहोस्",
     "drop-files-to-upload": "अपलोड गर्नका लागि फाइलहरू टाँच्नुहोस्",
+    "duplicate-collections": "डुप्लिकेट संग्रहहरू",
+    "duplicate-facets": "डुप्लिकेट विशेषताहरू",
+    "duplicate-products": "डुप्लिकेट उत्पादनहरू",
     "edit-facet-values": "मूल्य मूल्यहरू सम्पादन गर्नुहोस्",
     "edit-options": "विकल्प सम्पादन गर्नुहोस्",
     "facet": "मूल्य",
@@ -197,6 +199,7 @@
     "add-note": "नोट थप्नुहोस्",
     "apply": "लागू गर्नुहोस्",
     "assign-to-channel": "च्यानलमा खस्यौँ गर्नुहोस्",
+    "assign-to-channels": "{count, plural, one {च्यानलमा} other {च्यानलहरूमा}} सौंप्नुहोस्",
     "available-currencies": "उपलब्ध मुद्राहरू",
     "available-languages": "उपलब्ध भाषाहरू",
     "boolean-and": "र",
@@ -234,6 +237,7 @@
     "details": "विवरणहरू",
     "disabled": "निष्कृय",
     "discard-changes": "परिवर्तन फाल्नुहोस्",
+    "duplicate": "डुप्लिकेट",
     "edit": "सम्पादन गर्नुहोस्",
     "edit-field": "क्षेत्र सम्पादन गर्नुहोस्",
     "edit-note": "नोट सम्पादन गर्नुहोस्",
@@ -249,7 +253,7 @@
     "guest": "अतिथि",
     "id": "गुणस्तर",
     "image": "प्रतिमा",
-    "items-per-page-option": "{ पृष्ठमा गर्ने } प्रति पृष्ठ",
+    "items-per-page-option": "{ count } प्रति पृष्ठ",
     "items-selected-count": "{ परिमाण } {count, plural, one {समान} other {समानहरू}} चयन गरिएको",
     "keep-editing": "सम्पादन जारी राख्नुहोस्",
     "language": "भाषा",
@@ -268,6 +272,7 @@
     "name": "नाम",
     "no-alerts": "कुनै सूचना छैन",
     "no-bulk-actions-available": "कुनै थप्दा कार्यक्रम उपलब्ध छैन",
+    "no-channel-selected": "",
     "no-results": "कुनै परिणाम छैन",
     "not-applicable": "लागू छैन",
     "not-set": "सेट गरिएको छैन",
@@ -279,6 +284,9 @@
     "notify-delete-error-with-count": "{count, plural, one {1 समान} other {{count} समानहरू}} मेटाउन असमर्थ",
     "notify-delete-success": "{ entity } मेटाइयो",
     "notify-delete-success-with-count": "{count, plural, one {1 समान} other {{count} समानहरू}} सफलतापूर्वक मेटाइयो",
+    "notify-duplicate-error": "त्रुटि कारण { name } को प्रतिलिपि बनाउन सकिएन: { error }",
+    "notify-duplicate-error-excess": "त्रुटिहरू कारण अतिरिक्त { count } {count, plural, one {वस्तु} other {वस्तुहरू}} नबनाइन्",
+    "notify-duplicate-success": "{count, plural, one {1 वस्तु} other {{count} वस्तुहरू}} सफलतापूर्वक प्रतिलिपि बनाइयो: { names }",
     "notify-remove-from-channel-success-with-count": "{ count } च्यानलबाट सफलतापूर्वक हटाइयो",
     "notify-save-changes-error": "त्रुटि देखा पर्‍यो, परिवर्तनहरू संरक्षण गर्न असमर्थ",
     "notify-saved-changes": "परिवर्तनहरू संरक्षित गरियो",
@@ -342,7 +350,7 @@
   },
   "customer": {
     "add-customer-to-group": "ग्रुपमा ग्राहक थप्नुहोस्",
-    "add-customer-to-groups-with-count": "{count, plural, one {1 ग्रुप} other {{count} ग्रुपहरूमा} ग्राहक थप्नुहोस्}",
+    "add-customer-to-groups-with-count": "{count, plural, one {1 ग्रुप} other {{count} ग्रुपहरूमा}} ग्राहक थप्नुहोस्",
     "add-customers-to-group": "ग्रुपमा ग्राहकहरू थप्नुहोस्",
     "add-customers-to-group-success": "थपिएको छ {customerCount, plural, one {1 ग्राहक} other {{customerCount} ग्राहकहरू}} \"{ groupName }\" मा",
     "add-customers-to-group-with-count": "थप्नुहोस् {count, plural, one {1 ग्राहक} other {{count} ग्राहकहरू}}",
@@ -489,6 +497,7 @@
     "conditions": "सर्तहरू",
     "coupon-code": "कुपन कोड",
     "create-new-promotion": "नयाँ प्रमोशन बनाउनुहोस्",
+    "duplicate-promotions": "डुप्लिकेट प्रमोशनहरू",
     "ends-at": "यहाँ समाप्त हुन्छ",
     "per-customer-limit": "ग्राहक प्रति सीमा",
     "per-customer-limit-tooltip": "एकमात्र ग्राहकको द्वारा यस प्रमोशनको प्रयोग गर्न मिल्ने बाटोको अधिकतम संख्या",

+ 10 - 1
packages/admin-ui/src/lib/static/i18n-messages/pl.json

@@ -66,7 +66,6 @@
     "assets": "",
     "assign-product-to-channel-success": "Pomyślnie przypisano produkt do \"{ channel }\"",
     "assign-products-to-channel": "Przypisz produkt do kanału",
-    "assign-to-channel": "Przypisz do kanału",
     "assign-to-named-channel": "Przypisz do { channelCode }",
     "assign-variant-to-channel-success": "",
     "assign-variants-to-channel": "",
@@ -99,6 +98,9 @@
     "default-currency": "",
     "do-not-inherit-filters": "",
     "drop-files-to-upload": "Upuść pliki do uploadu",
+    "duplicate-collections": "Zduplikowane kolekcje",
+    "duplicate-facets": "Zduplikowane cechy",
+    "duplicate-products": "Zduplikowane produkty",
     "edit-facet-values": "",
     "edit-options": "",
     "facet": "",
@@ -197,6 +199,7 @@
     "add-note": "",
     "apply": "",
     "assign-to-channel": "",
+    "assign-to-channels": "Przypisz do {count, plural, one {kanału} other {kanałów}}",
     "available-currencies": "",
     "available-languages": "Dostępne języki",
     "boolean-and": "",
@@ -234,6 +237,7 @@
     "details": "",
     "disabled": "Wyłączony",
     "discard-changes": "Odrzuć zmiany",
+    "duplicate": "Zduplikować",
     "edit": "Edytuj",
     "edit-field": "Edytuj pole",
     "edit-note": "",
@@ -268,6 +272,7 @@
     "name": "Nazwa",
     "no-alerts": "",
     "no-bulk-actions-available": "",
+    "no-channel-selected": "",
     "no-results": "Brak wyników",
     "not-applicable": "",
     "not-set": "Nie ustawione",
@@ -279,6 +284,9 @@
     "notify-delete-error-with-count": "",
     "notify-delete-success": "Usunięto { entity }",
     "notify-delete-success-with-count": "",
+    "notify-duplicate-error": "Nie można zduplikować { name } z powodu błędu: { error }",
+    "notify-duplicate-error-excess": "Nie można zduplikować dodatkowych { count } {count, plural, one {elementu} other {elementów}} z powodu błędów",
+    "notify-duplicate-success": "Pomyślnie zduplikowano {count, plural, one {1 element} other {{count} elementy}}: { names }",
     "notify-remove-from-channel-success-with-count": "",
     "notify-save-changes-error": "Wystąpił błąd, nie można zapisać zmian",
     "notify-saved-changes": "Zapisano zmiany",
@@ -489,6 +497,7 @@
     "conditions": "Warunki",
     "coupon-code": "Kod rabatowy",
     "create-new-promotion": "Utwórz nową promocje",
+    "duplicate-promotions": "Zduplikować promocje",
     "ends-at": "Kończy się",
     "per-customer-limit": "Limit klientów",
     "per-customer-limit-tooltip": "Maksymalna liczba razy, kiedy ta promocja może być użyta przez pojedynczego klienta",

+ 10 - 1
packages/admin-ui/src/lib/static/i18n-messages/pt_BR.json

@@ -66,7 +66,6 @@
     "assets": "Imagens",
     "assign-product-to-channel-success": "Produto atribuído com sucesso a \"{ channel }\"",
     "assign-products-to-channel": "Atribuir produtos ao canal",
-    "assign-to-channel": "Atribuir ao canal",
     "assign-to-named-channel": "Atribuir a { channelCode }",
     "assign-variant-to-channel-success": "Variações atribuídas ao canal com sucesso",
     "assign-variants-to-channel": "Atribuir variação ao canal",
@@ -99,6 +98,9 @@
     "default-currency": "Moeda padrão",
     "do-not-inherit-filters": "Não herdar filtros",
     "drop-files-to-upload": "Soltar arquivos para envio",
+    "duplicate-collections": "Duplicar coleções",
+    "duplicate-facets": "Duplicar facetas",
+    "duplicate-products": "Duplicar produtos",
     "edit-facet-values": "Editar valores",
     "edit-options": "Editar opções",
     "facet": "Etiqueta",
@@ -197,6 +199,7 @@
     "add-note": "Adicionar nota",
     "apply": "Aplicar",
     "assign-to-channel": "Atribuir a um canal",
+    "assign-to-channels": "Atribuir a {count, plural, one {canal} other {canais}}",
     "available-currencies": "Moedas disponíveis",
     "available-languages": "Idiomas disponíveis",
     "boolean-and": "e",
@@ -234,6 +237,7 @@
     "details": "Detalhes",
     "disabled": "Desabilitado",
     "discard-changes": "Descartar modificações",
+    "duplicate": "Duplicar",
     "edit": "Editar",
     "edit-field": "Editar campo",
     "edit-note": "Editar nota",
@@ -268,6 +272,7 @@
     "name": "Nome",
     "no-alerts": "Sem alertas",
     "no-bulk-actions-available": "Nenhuma ação em massa disponível",
+    "no-channel-selected": "",
     "no-results": "Sem resultados",
     "not-applicable": "Não aplicável",
     "not-set": "Não configurado",
@@ -279,6 +284,9 @@
     "notify-delete-error-with-count": "Nāo foi possível remover {count, plural, one {1 item} other {{count} itens}}",
     "notify-delete-success": "Excluído { entity }",
     "notify-delete-success-with-count": "Removido {count, plural, one {1 item} other {{count} items}} com sucesso",
+    "notify-duplicate-error": "Não foi possível duplicar { name } devido a um erro: { error }",
+    "notify-duplicate-error-excess": "Não foi possível duplicar { count } {count, plural, one {item} other {itens}} adicionais devido a erros",
+    "notify-duplicate-success": "Duplicação realizada com sucesso {count, plural, one {1 item} other {{count} itens}}: { names }",
     "notify-remove-from-channel-success-with-count": "Removido { count } itens do canal com sucesso",
     "notify-save-changes-error": "Ocorreu um erro, não foi possível salvar as alterações",
     "notify-saved-changes": "Alterações salvas",
@@ -489,6 +497,7 @@
     "conditions": "Condições",
     "coupon-code": "Código do cupom",
     "create-new-promotion": "Criar nova promoção",
+    "duplicate-promotions": "Duplicar promoções",
     "ends-at": "Termina em",
     "per-customer-limit": "Limite por cliente",
     "per-customer-limit-tooltip": "Número máximo de vezes que esta promoção pode ser usada por um único cliente",

+ 10 - 1
packages/admin-ui/src/lib/static/i18n-messages/pt_PT.json

@@ -66,7 +66,6 @@
     "assets": "",
     "assign-product-to-channel-success": "Produto atribuído com sucesso a \"{ channel }\"",
     "assign-products-to-channel": "Atribuir produtos ao canal",
-    "assign-to-channel": "Atribuir ao canal",
     "assign-to-named-channel": "Atribuir a { channelCode }",
     "assign-variant-to-channel-success": "Variantes atribuídas ao canal com sucesso",
     "assign-variants-to-channel": "Atribuir variante ao canal",
@@ -99,6 +98,9 @@
     "default-currency": "",
     "do-not-inherit-filters": "",
     "drop-files-to-upload": "Colocar ficheiros para enviar",
+    "duplicate-collections": "Duplicar coleções",
+    "duplicate-facets": "Duplicar facetas",
+    "duplicate-products": "Duplicar produtos",
     "edit-facet-values": "",
     "edit-options": "",
     "facet": "",
@@ -197,6 +199,7 @@
     "add-note": "Adicionar nota",
     "apply": "",
     "assign-to-channel": "",
+    "assign-to-channels": "Atribuir a {count, plural, one {canal} other {canais}}",
     "available-currencies": "",
     "available-languages": "Idiomas disponíveis",
     "boolean-and": "",
@@ -234,6 +237,7 @@
     "details": "Detalhes",
     "disabled": "Inactivo",
     "discard-changes": "Descartar modificações",
+    "duplicate": "Duplicar",
     "edit": "Editar",
     "edit-field": "Editar campo",
     "edit-note": "Editar nota",
@@ -268,6 +272,7 @@
     "name": "Nome",
     "no-alerts": "",
     "no-bulk-actions-available": "",
+    "no-channel-selected": "",
     "no-results": "Nenhum resultado encontrado",
     "not-applicable": "",
     "not-set": "Não configurado",
@@ -279,6 +284,9 @@
     "notify-delete-error-with-count": "",
     "notify-delete-success": "{ entity } excluído(a)",
     "notify-delete-success-with-count": "",
+    "notify-duplicate-error": "Não foi possível duplicar { name } devido a um erro: { error }",
+    "notify-duplicate-error-excess": "Não foi possível duplicar { count } {count, plural, one {item} other {itens}} adicionais devido a erros",
+    "notify-duplicate-success": "Duplicação realizada com sucesso {count, plural, one {1 item} other {{count} itens}}: { names }",
     "notify-remove-from-channel-success-with-count": "",
     "notify-save-changes-error": "Ocorreu um erro. Não foi possível guardar as alterações",
     "notify-saved-changes": "Alterações guardadas",
@@ -489,6 +497,7 @@
     "conditions": "Condições",
     "coupon-code": "Código promocional",
     "create-new-promotion": "Criar nova promoção",
+    "duplicate-promotions": "Duplicar promoções",
     "ends-at": "Válido até",
     "per-customer-limit": "Limite por cliente",
     "per-customer-limit-tooltip": "Número máximo de vezes que esta promoção pode ser utilizada por um único cliente",

+ 10 - 1
packages/admin-ui/src/lib/static/i18n-messages/ru.json

@@ -66,7 +66,6 @@
     "assets": "Медиафайлы",
     "assign-product-to-channel-success": "Товар успешно добавлен в канал \"{ channel }\"",
     "assign-products-to-channel": "Добавить товары в канал",
-    "assign-to-channel": "Добавить в канал",
     "assign-to-named-channel": "Добавить для { channelCode }",
     "assign-variant-to-channel-success": "Вариант товара успешно добавлен в канал \"{ channel }\"",
     "assign-variants-to-channel": "Добавить варианты товара в канал",
@@ -99,6 +98,9 @@
     "default-currency": "Валюта по умолчанию",
     "do-not-inherit-filters": "Не наследовать фильтры",
     "drop-files-to-upload": "Перетащите файлы для загрузки",
+    "duplicate-collections": "Дублировать коллекции",
+    "duplicate-facets": "Дублировать фасеты",
+    "duplicate-products": "Дублировать продукты",
     "edit-facet-values": "Редактировать значения тега",
     "edit-options": "Редактировать опции",
     "facet": "Тег",
@@ -197,6 +199,7 @@
     "add-note": "Добавить заметку",
     "apply": "Применить",
     "assign-to-channel": "Назначить к каналу",
+    "assign-to-channels": "Назначить на {count, plural, one {канал} other {каналы}}",
     "available-currencies": "Доступные валюты",
     "available-languages": "Доступные языки",
     "boolean-and": "И",
@@ -234,6 +237,7 @@
     "details": "Детали",
     "disabled": "Выключен",
     "discard-changes": "Отменить изменения",
+    "duplicate": "Дублировать",
     "edit": "Редактировать",
     "edit-field": "Редактировать поле",
     "edit-note": "Редактировать заметку",
@@ -268,6 +272,7 @@
     "name": "Имя",
     "no-alerts": "Нет уведомлений",
     "no-bulk-actions-available": "Нет доступных массовых действий",
+    "no-channel-selected": "",
     "no-results": "Нет результатов",
     "not-applicable": "Непригодный",
     "not-set": "Не задано",
@@ -279,6 +284,9 @@
     "notify-delete-error-with-count": "Невзозможно удалить {count, plural, one {1 позицию} other {{count} позиций}}",
     "notify-delete-success": "Удалено { entity }",
     "notify-delete-success-with-count": "Успешно удалено {count, plural, one {1 позиция} other {{count} позиций}}",
+    "notify-duplicate-error": "Не удалось продублировать { name } из-за ошибки: { error }",
+    "notify-duplicate-error-excess": "Дополнительные { count } {count, plural, one {элемент} other {элементы}} не могут быть продублированы из-за ошибок",
+    "notify-duplicate-success": "Успешно продублировано {count, plural, one {1 элемент} other {{count} элементов}}: { names }",
     "notify-remove-from-channel-success-with-count": "Успешно удалено { count } позиций из канала",
     "notify-save-changes-error": "Произошла ошибка, не удалось сохранить изменения",
     "notify-saved-changes": "Сохранены изменения",
@@ -489,6 +497,7 @@
     "conditions": "Условия",
     "coupon-code": "Код купона",
     "create-new-promotion": "Создать новую акцию",
+    "duplicate-promotions": "Дублировать акции",
     "ends-at": "Заканчивается в",
     "per-customer-limit": "Лимит на клиента",
     "per-customer-limit-tooltip": "Максимальное количество раз, когда этот промокод может быть использован одним клиентом",

+ 10 - 1
packages/admin-ui/src/lib/static/i18n-messages/sv.json

@@ -66,7 +66,6 @@
     "assets": "Tillgångar",
     "assign-product-to-channel-success": "Lyckades tilldela {count, plural, one {1 produkt} other {{count} produkter}} till { channel }",
     "assign-products-to-channel": "Tilldela produkter till kanal",
-    "assign-to-channel": "Tilldela till kanal",
     "assign-to-named-channel": "Tilldela till { channelCode }",
     "assign-variant-to-channel-success": "Lyckades tilldela {count, plural, one {1 produktvariant} other {{count} produktvarianter}} till { channel }",
     "assign-variants-to-channel": "Tilldela produktvarianter till kanal",
@@ -99,6 +98,9 @@
     "default-currency": "Standardvaluta",
     "do-not-inherit-filters": "Ärv inte filter",
     "drop-files-to-upload": "Släpp filer för att ladda upp",
+    "duplicate-collections": "Duplicera samlingar",
+    "duplicate-facets": "Duplicera fasetter",
+    "duplicate-products": "Duplicera produkter",
     "edit-facet-values": "Redigera etikettvärden",
     "edit-options": "Redigera alternativ",
     "facet": "Etikett",
@@ -197,6 +199,7 @@
     "add-note": "Lägg till anteckning",
     "apply": "Använd",
     "assign-to-channel": "Tilldela till kanal",
+    "assign-to-channels": "Tilldela till {count, plural, one {kanal} other {kanaler}}",
     "available-currencies": "Tillgängliga valutor",
     "available-languages": "Tillgängliga språk",
     "boolean-and": "och",
@@ -234,6 +237,7 @@
     "details": "Detaljer",
     "disabled": "Inaktiverad",
     "discard-changes": "Släng ändringar",
+    "duplicate": "Duplicera",
     "edit": "Redigera",
     "edit-field": "Redigera fält",
     "edit-note": "Redigera anteckning",
@@ -268,6 +272,7 @@
     "name": "Namn",
     "no-alerts": "Inga aviseringar",
     "no-bulk-actions-available": "Inga massåtgärder tillgängliga",
+    "no-channel-selected": "",
     "no-results": "Inga resultat",
     "not-applicable": "Ej tillämpligt",
     "not-set": "Ej inställt",
@@ -279,6 +284,9 @@
     "notify-delete-error-with-count": "Kunde inte radera {count, plural, one {1 objekt} other {{count} objekt}}",
     "notify-delete-success": "Raderade { entity }",
     "notify-delete-success-with-count": "Lyckades radera {count, plural, one {1 objekt} other {{count} objekt}}",
+    "notify-duplicate-error": "Kunde inte duplicera { name } på grund av ett fel: { error }",
+    "notify-duplicate-error-excess": "Ytterligare { count } {count, plural, one {objekt} other {objekt}} kunde inte dupliceras på grund av fel",
+    "notify-duplicate-success": "Duplicering av {count, plural, one {1 objekt} other {{count} objekt}} lyckades: { names }",
     "notify-remove-from-channel-success-with-count": "Tog bort { count } objekt från kanalen",
     "notify-save-changes-error": "Ett fel uppstod, kunde inte spara ändringar",
     "notify-saved-changes": "Sparade ändringar",
@@ -489,6 +497,7 @@
     "conditions": "Villkor",
     "coupon-code": "Kupongkod",
     "create-new-promotion": "Skapa ny kampanj",
+    "duplicate-promotions": "Duplicera kampanjer",
     "ends-at": "Avslutas den",
     "per-customer-limit": "Begränsning per kund",
     "per-customer-limit-tooltip": "Maximalt antal gånger den här kampanjen kan användas av en enskild kund",

+ 10 - 1
packages/admin-ui/src/lib/static/i18n-messages/uk.json

@@ -66,7 +66,6 @@
     "assets": "Медіа-об'єкти",
     "assign-product-to-channel-success": "Товар успішно доданий в канал \"{ channel }\"",
     "assign-products-to-channel": "Додати товари в канал",
-    "assign-to-channel": "Додати в канал",
     "assign-to-named-channel": "Додати для { channelCode }",
     "assign-variant-to-channel-success": "Варіант товару успішно доданий в канал \"{ channel }\"",
     "assign-variants-to-channel": "Додати варіанти товару в канал",
@@ -99,6 +98,9 @@
     "default-currency": "Основна валюта",
     "do-not-inherit-filters": "Не наслідувати фільтри",
     "drop-files-to-upload": "Перетягніть файли для завантаження",
+    "duplicate-collections": "Дублювати колекції",
+    "duplicate-facets": "Дублювати аспекти",
+    "duplicate-products": "Дублювати товари",
     "edit-facet-values": "Редагувати значення тега",
     "edit-options": "Редагувати опції",
     "facet": "Тег",
@@ -197,6 +199,7 @@
     "add-note": "Додати замітку",
     "apply": "Застосувати",
     "assign-to-channel": "Призначити каналу",
+    "assign-to-channels": "Призначити на {count, plural, one {канал} other {канали}}",
     "available-currencies": "Доступні валюти",
     "available-languages": "Доступні мови",
     "boolean-and": "і",
@@ -234,6 +237,7 @@
     "details": "Подробиці",
     "disabled": "Виключений",
     "discard-changes": "Скасувати зміни",
+    "duplicate": "Дублювати",
     "edit": "Редагувати",
     "edit-field": "Редагувати поле",
     "edit-note": "Редагувати замітку",
@@ -268,6 +272,7 @@
     "name": "Ім'я",
     "no-alerts": "Немає сповіщень",
     "no-bulk-actions-available": "Масові дії недоступні",
+    "no-channel-selected": "",
     "no-results": "Немає результатів",
     "not-applicable": "Не застосовно",
     "not-set": "Не задано",
@@ -279,6 +284,9 @@
     "notify-delete-error-with-count": "Не вдалося видалити {count, plural, one {1 елемент} few {{count} елементи} other {{count} елементів}}",
     "notify-delete-success": "Видалено { entity }",
     "notify-delete-success-with-count": "Успішно видалено {count, plural, one {1 елемент} few {{count} елементи}  other {{count} елементів}}",
+    "notify-duplicate-error": "Не вдалося дублювати { name } через помилку: { error }",
+    "notify-duplicate-error-excess": "Додаткові { count } {count, plural, one {елемент} other {елементи}} не можуть бути дубльовані через помилки",
+    "notify-duplicate-success": "Успішно дубльовано {count, plural, one {1 елемент} other {{count} елементів}}: { names }",
     "notify-remove-from-channel-success-with-count": "Успішно видалено { count } елементів з каналу",
     "notify-save-changes-error": "Сталася помилка, не вдалося зберегти зміни",
     "notify-saved-changes": "Збережені зміни",
@@ -489,6 +497,7 @@
     "conditions": "Умови",
     "coupon-code": "Код купона",
     "create-new-promotion": "Створити нову акцію",
+    "duplicate-promotions": "Дублювати акції",
     "ends-at": "Закінчується в",
     "per-customer-limit": "Ліміт на клієнта",
     "per-customer-limit-tooltip": "Максимальна кількість разів, коли цей промокод може бути використаний одним клієнтом",

+ 10 - 1
packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json

@@ -66,7 +66,6 @@
     "assets": "",
     "assign-product-to-channel-success": "成功将产品添加至销售渠道\"{ channel }\"",
     "assign-products-to-channel": "分配产品到销售渠道",
-    "assign-to-channel": "分配至销售渠道",
     "assign-to-named-channel": "分配到{ channelCode }",
     "assign-variant-to-channel-success": "成功添加商品到 \"{ channel }\"",
     "assign-variants-to-channel": "添加到销售渠道",
@@ -99,6 +98,9 @@
     "default-currency": "",
     "do-not-inherit-filters": "",
     "drop-files-to-upload": "拖拽文件上传",
+    "duplicate-collections": "复制集合",
+    "duplicate-facets": "复制外观",
+    "duplicate-products": "复制产品",
     "edit-facet-values": "",
     "edit-options": "",
     "facet": "",
@@ -197,6 +199,7 @@
     "add-note": "添加注释",
     "apply": "",
     "assign-to-channel": "",
+    "assign-to-channels": "分配到{count, plural, one {渠道} other {渠道}}",
     "available-currencies": "",
     "available-languages": "可用语言",
     "boolean-and": "",
@@ -234,6 +237,7 @@
     "details": "详情",
     "disabled": "禁用",
     "discard-changes": "放弃修改",
+    "duplicate": "复制",
     "edit": "编辑",
     "edit-field": "编辑域",
     "edit-note": "编辑笔记",
@@ -268,6 +272,7 @@
     "name": "名称",
     "no-alerts": "",
     "no-bulk-actions-available": "",
+    "no-channel-selected": "",
     "no-results": "没找到任何结果",
     "not-applicable": "",
     "not-set": "未设置",
@@ -279,6 +284,9 @@
     "notify-delete-error-with-count": "",
     "notify-delete-success": "{ entity }已删除",
     "notify-delete-success-with-count": "",
+    "notify-duplicate-error": "由于错误无法复制 { name } :{ error }",
+    "notify-duplicate-error-excess": "由于错误,无法复制额外的 { count } {count, plural, one {项目} other {项目}}",
+    "notify-duplicate-success": "成功复制 {count, plural, one {1 项目} other {{count} 项目}} :{ names }",
     "notify-remove-from-channel-success-with-count": "",
     "notify-save-changes-error": "保存失败",
     "notify-saved-changes": "修改已保存",
@@ -489,6 +497,7 @@
     "conditions": "使用限制",
     "coupon-code": "优惠码",
     "create-new-promotion": "添加促销产品",
+    "duplicate-promotions": "复制促销活动",
     "ends-at": "有效起始时间",
     "per-customer-limit": "每人限领数",
     "per-customer-limit-tooltip": "单个客户可以使用此优惠的最大次数",

+ 10 - 1
packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json

@@ -66,7 +66,6 @@
     "assets": "",
     "assign-product-to-channel-success": "成功將產品新增至渠道\"{ channel }\"",
     "assign-products-to-channel": "分配產品到渠道",
-    "assign-to-channel": "分配至渠道",
     "assign-to-named-channel": "分配到{ channelCode }",
     "assign-variant-to-channel-success": "",
     "assign-variants-to-channel": "",
@@ -99,6 +98,9 @@
     "default-currency": "",
     "do-not-inherit-filters": "",
     "drop-files-to-upload": "拖拽文件上傳",
+    "duplicate-collections": "複製集合",
+    "duplicate-facets": "複製外觀",
+    "duplicate-products": "複製產品",
     "edit-facet-values": "",
     "edit-options": "",
     "facet": "",
@@ -197,6 +199,7 @@
     "add-note": "",
     "apply": "",
     "assign-to-channel": "",
+    "assign-to-channels": "分配到{count, plural, one {渠道} other {渠道}}",
     "available-currencies": "",
     "available-languages": "可用語言",
     "boolean-and": "",
@@ -234,6 +237,7 @@
     "details": "",
     "disabled": "禁用",
     "discard-changes": "放弃修改",
+    "duplicate": "複製",
     "edit": "編辑",
     "edit-field": "編辑域",
     "edit-note": "",
@@ -268,6 +272,7 @@
     "name": "名稱",
     "no-alerts": "",
     "no-bulk-actions-available": "",
+    "no-channel-selected": "",
     "no-results": "没找到任何結果",
     "not-applicable": "",
     "not-set": "未設定",
@@ -279,6 +284,9 @@
     "notify-delete-error-with-count": "",
     "notify-delete-success": "{ entity }已移除",
     "notify-delete-success-with-count": "",
+    "notify-duplicate-error": "由於錯誤,無法複製 { name } :{ error }",
+    "notify-duplicate-error-excess": "由於錯誤,無法複製額外的 { count } {count, plural, one {項目} other {項目}}",
+    "notify-duplicate-success": "成功複製 {count, plural, one {1 項目} other {{count} 項目}} :{ names }",
     "notify-remove-from-channel-success-with-count": "",
     "notify-save-changes-error": "保存失敗",
     "notify-saved-changes": "修改已保存",
@@ -489,6 +497,7 @@
     "conditions": "使用限制",
     "coupon-code": "優惠碼",
     "create-new-promotion": "新增促销產品",
+    "duplicate-promotions": "複製促銷活動",
     "ends-at": "結束時間",
     "per-customer-limit": "領取上限",
     "per-customer-limit-tooltip": "單一客戶可以使用此優惠的最大次數",

Some files were not shown because too many files changed in this diff