Explorar o código

Merge branch 'master' into minor

Michael Bromley %!s(int64=3) %!d(string=hai) anos
pai
achega
4900e1eab9
Modificáronse 32 ficheiros con 364 adicións e 148 borrados
  1. 38 1
      README.md
  2. 2 2
      docs/content/plugins/extending-the-admin-ui/_index.md
  3. 2 2
      docs/content/plugins/extending-the-admin-ui/admin-ui-theming-branding/_index.md
  4. 1 1
      docs/content/plugins/extending-the-admin-ui/custom-form-inputs/_index.md
  5. 1 1
      docs/content/plugins/extending-the-admin-ui/using-angular/_index.md
  6. 5 1
      docs/content/plugins/plugin-examples/defining-db-entity.md
  7. 70 0
      docs/content/plugins/plugin-examples/extending-graphql-api.md
  8. 5 3
      packages/core/src/api/common/custom-field-relation-resolver.service.ts
  9. 19 4
      packages/core/src/common/configurable-operation.ts
  10. 1 1
      packages/core/src/config/promotion/actions/order-fixed-discount-action.ts
  11. 3 2
      packages/core/src/service/helpers/entity-hydrator/entity-hydrator.service.ts
  12. 3 3
      packages/core/src/service/helpers/locale-string-hydrator/locale-string-hydrator.ts
  13. 4 3
      packages/core/src/service/helpers/order-modifier/order-modifier.ts
  14. 1 1
      packages/core/src/service/helpers/order-state-machine/order-state-machine.ts
  15. 24 0
      packages/core/src/service/helpers/translator/translator.service.ts
  16. 41 19
      packages/core/src/service/helpers/utils/translate-entity.spec.ts
  17. 23 15
      packages/core/src/service/helpers/utils/translate-entity.ts
  18. 2 0
      packages/core/src/service/service.module.ts
  19. 13 15
      packages/core/src/service/services/collection.service.ts
  20. 6 5
      packages/core/src/service/services/country.service.ts
  21. 5 4
      packages/core/src/service/services/customer.service.ts
  22. 18 9
      packages/core/src/service/services/facet-value.service.ts
  23. 32 19
      packages/core/src/service/services/facet.service.ts
  24. 3 2
      packages/core/src/service/services/order-testing.service.ts
  25. 5 4
      packages/core/src/service/services/order.service.ts
  26. 5 4
      packages/core/src/service/services/product-option-group.service.ts
  27. 4 3
      packages/core/src/service/services/product-option.service.ts
  28. 9 8
      packages/core/src/service/services/product-variant.service.ts
  29. 7 6
      packages/core/src/service/services/product.service.ts
  30. 5 4
      packages/core/src/service/services/shipping-method.service.ts
  31. 5 4
      packages/core/src/service/services/zone.service.ts
  32. 2 2
      packages/dev-server/dev-config.ts

+ 38 - 1
README.md

@@ -82,12 +82,49 @@ DB=<mysql|postgres|sqlite> yarn dev-server:start
 ```
 If you do not specify the `DB` argument, it will default to "mysql".
 
-### 6. Launch the admin ui
+### Testing admin ui changes locally
+
+If you are making changes to the admin ui, you need to start the admin ui independent from the dev-server:
 
 1. `cd packages/admin-ui`
 2. `yarn start`
 3. Go to http://localhost:4200 and log in with "superadmin", "superadmin"
 
+This will auto restart when you make changes to the admin ui. You don't need this step when you just use the admin ui just
+to test backend changes.
+
+### Testing your changes locally
+This example shows how to test changes to the `payments-plugin` package locally, but it will also work for other packages.
+
+1. Open 2 terminal windows:
+
+- Terminal 1 for watching and compiling the changes of the package you are developing
+- Terminal 2 for running the dev-server
+
+```shell
+# Terminal 1
+cd packages/payments-plugin
+yarn watch
+```
+:warning: If you are developing changes for the `core`package, you also need to watch the `common` package:
+```shell
+# Terminal 1
+# Root of the project
+yarn watch:core-common
+```
+
+2. After the changes in your package are compiled you have to stop and restart the dev-server:
+
+```shell
+# Terminal 2
+cd packages/dev-server
+DB=sqlite yarn start
+```
+
+3. The dev-server will now have your local changes from the changed package.
+
+:information_source: Lerna links to the `dist` folder of the packages, so you **don't** need to rerun 'yarn bootstrap'
+
 ### Code generation
 
 [graphql-code-generator](https://github.com/dotansimha/graphql-code-generator) is used to automatically create TypeScript interfaces for all GraphQL server operations and admin ui queries. These generated interfaces are used in both the admin ui and the server.

+ 2 - 2
docs/content/plugins/extending-the-admin-ui/_index.md

@@ -42,7 +42,7 @@ plugins: [
   AdminUiPlugin.init({
     port: 3002,
     app: compileUiExtensions({
-      outputPath: path.join(__dirname, 'admin-ui'),
+      outputPath: path.join(__dirname, '../admin-ui'),
       extensions: [{
         // ...
       }],
@@ -62,7 +62,7 @@ import { compileUiExtensions } from '@vendure/ui-devkit/compiler';
 import * as path from 'path';
 
 compileUiExtensions({
-    outputPath: path.join(__dirname, 'admin-ui'),
+    outputPath: path.join(__dirname, '../admin-ui'),
     extensions: [/* ... */],
 }).compile?.().then(() => {
     process.exit(0);

+ 2 - 2
docs/content/plugins/extending-the-admin-ui/admin-ui-theming-branding/_index.md

@@ -47,7 +47,7 @@ You can replace the Vendure logos and favicon with your own brand logo:
       plugins: [
         AdminUiPlugin.init({
           app: compileUiExtensions({
-            outputPath: path.join(__dirname, 'admin-ui'),
+            outputPath: path.join(__dirname, '../admin-ui'),
             extensions: [
               setBranding({
                 // The small logo appears in the top left of the screen  
@@ -91,7 +91,7 @@ Much of the visual styling of the Admin UI can be customized by providing your o
       plugins: [
         AdminUiPlugin.init({
           app: compileUiExtensions({
-            outputPath: path.join(__dirname, 'admin-ui'),
+            outputPath: path.join(__dirname, '../admin-ui'),
             extensions: [{
               globalStyles: path.join(__dirname, 'my-theme.scss')
             }],

+ 1 - 1
docs/content/plugins/extending-the-admin-ui/custom-form-inputs/_index.md

@@ -66,7 +66,7 @@ The `SharedExtensionModule` is then passed to the `compileUiExtensions()` functi
 AdminUiPlugin.init({
   port: 5001,
   app: compileUiExtensions({
-    outputPath: path.join(__dirname, 'admin-ui'),
+    outputPath: path.join(__dirname, '../admin-ui'),
     extensions: [{
       extensionPath: path.join(__dirname, 'ui-extensions'),
       ngModules: [{

+ 1 - 1
docs/content/plugins/extending-the-admin-ui/using-angular/_index.md

@@ -88,7 +88,7 @@ export const config: VendureConfig = {
     AdminUiPlugin.init({
       port: 5001,
       app: compileUiExtensions({
-        outputPath: path.join(__dirname, 'admin-ui'),
+        outputPath: path.join(__dirname, '../admin-ui'),
         extensions: [{
           extensionPath: path.join(__dirname, 'ui-extensions'),
           ngModules: [{

+ 5 - 1
docs/content/plugins/plugin-examples/defining-db-entity.md

@@ -5,7 +5,7 @@ showtoc: true
 
 # Defining a new database entity
 
-This example shows how new TypeORM database entities can be defined by plugins.
+This example shows how new [TypeORM database entities](https://typeorm.io/entities) can be defined by plugins.
 
 ```TypeScript
 // product-review.entity.ts
@@ -43,3 +43,7 @@ import { ProductReview } from './product-review.entity';
 })
 export class ReviewsPlugin {}
 ```
+
+## Corresponding GraphQL type
+
+Once you have defined a new DB entity, it is likely that you want to expose it in your GraphQL API. Here's how to [define a new type in your GraphQL API]({{< relref "extending-graphql-api" >}}#defining-a-new-type).

+ 70 - 0
docs/content/plugins/plugin-examples/extending-graphql-api.md

@@ -5,6 +5,39 @@ showtoc: true
 
 # Extending the GraphQL API
 
+Extension to the GraphQL API consists of two parts:
+
+1. **Schema extensions**. These define new types, fields, queries and mutations.
+2. **Resolvers**. These provide the logic that backs up the schema extensions.
+
+The Shop API and Admin APIs can be extended independently:
+
+```TypeScript {hl_lines=["16-22"]}
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
+import gql from 'graphql-tag';
+import { TopSellersResolver } from './top-products.resolver';
+
+const schemaExtension = gql`
+  extend type Query {
+    topProducts: [Product!]!
+  }
+`
+
+@VendurePlugin({
+  imports: [PluginCommonModule],
+  // We pass our schema extension and any related resolvers
+  // to our plugin metadata  
+  shopApiExtensions: {
+    schema: schemaExtension,
+    resolvers: [TopProductsResolver],
+  },
+  // Likewise, if you want to extend the Admin API,
+  // you would use `adminApiExtensions` in exactly the
+  // same way.  
+})
+export class TopProductsPlugin {}
+```
+
 There are a number of ways the GraphQL APIs can be modified by a plugin.
 
 ## Adding a new Query or Mutation
@@ -76,6 +109,43 @@ extend type Mutation {
 }
 ```
 
+## Defining a new type
+
+If you have [defined a new database entity]({{< relref "defining-db-entity" >}}), it is likely that you'll want to expose this entity in your GraphQL API. To do so, you'll need to define a corresponding GraphQL type:
+
+```TypeScript
+import gql from 'graphql-tag';
+import { PluginCommonModule, VendurePlugin } from '@vendure/core';
+import { ReviewsResolver } from './reviews.resolver';
+import { ProductReview } from './product-review.entity';
+
+@VendurePlugin({
+  imports: [PluginCommonModule],
+  shopApiExtensions: {
+    schema: gql`
+      # This is where we define the GraphQL type
+      # which corresponds to the Review entity
+      type ProductReview implements Node {
+        id: ID!
+        createdAt: DateTime!
+        updatedAt: DateTime!
+        text: String!
+        rating: Float!
+      }
+      
+      extend type Query {
+        # Now we can use this ProductReview type in queries
+        # and mutations.
+        reviewsForProduct(productId: ID!): [ProductReview!]!
+      }
+    `,
+    resolvers: [ReviewsResolver]
+  },
+  entities: [ProductReview],  
+})
+export class ReviewsPlugin {}
+```
+
 ## Add fields to existing types
 
 Let's say you want to add a new field, "availability" to the ProductVariant type, to allow the storefront to display some indication of whether a variant is available to purchase. First you define a resolver function:

+ 5 - 3
packages/core/src/api/common/custom-field-relation-resolver.service.ts

@@ -9,7 +9,7 @@ import { TransactionalConnection } from '../../connection/transactional-connecti
 import { VendureEntity } from '../../entity/base/base.entity';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
 import { ProductPriceApplicator } from '../../service/helpers/product-price-applicator/product-price-applicator';
-import { translateDeep } from '../../service/helpers/utils/translate-entity';
+import { TranslatorService } from '../../service/helpers/translator/translator.service';
 
 import { RequestContext } from './request-context';
 
@@ -26,7 +26,9 @@ export class CustomFieldRelationResolverService {
         private connection: TransactionalConnection,
         private configService: ConfigService,
         private productPriceApplicator: ProductPriceApplicator,
+        private translator: TranslatorService,
     ) {}
+
     /**
      * @description
      * Used to dynamically resolve related entities in custom fields. Based on the field
@@ -62,9 +64,9 @@ export class CustomFieldRelationResolverService {
         }
 
         const translated: any = Array.isArray(result)
-            ? result.map(r => (this.isTranslatable(r) ? translateDeep(r, ctx.languageCode) : r))
+            ? result.map(r => (this.isTranslatable(r) ? this.translator.translate(r, ctx) : r))
             : this.isTranslatable(result)
-            ? translateDeep(result, ctx.languageCode)
+            ? this.translator.translate(result, ctx)
             : result;
 
         return translated;

+ 19 - 4
packages/core/src/common/configurable-operation.ts

@@ -363,7 +363,7 @@ export class ConfigurableOperationDef<T extends ConfigArgs = ConfigArgs> {
     toGraphQlType(ctx: RequestContext): ConfigurableOperationDefinition {
         return {
             code: this.code,
-            description: localizeString(this.description, ctx.languageCode),
+            description: localizeString(this.description, ctx.languageCode, ctx.channel.defaultLanguageCode),
             args: Object.entries(this.args).map(
                 ([name, arg]) =>
                     ({
@@ -373,8 +373,16 @@ export class ConfigurableOperationDef<T extends ConfigArgs = ConfigArgs> {
                         required: arg.required ?? true,
                         defaultValue: arg.defaultValue,
                         ui: arg.ui,
-                        label: arg.label && localizeString(arg.label, ctx.languageCode),
-                        description: arg.description && localizeString(arg.description, ctx.languageCode),
+                        label:
+                            arg.label &&
+                            localizeString(arg.label, ctx.languageCode, ctx.channel.defaultLanguageCode),
+                        description:
+                            arg.description &&
+                            localizeString(
+                                arg.description,
+                                ctx.languageCode,
+                                ctx.channel.defaultLanguageCode,
+                            ),
                     } as Required<ConfigArgDefinition>),
             ),
         };
@@ -405,8 +413,15 @@ export class ConfigurableOperationDef<T extends ConfigArgs = ConfigArgs> {
     }
 }
 
-function localizeString(stringArray: LocalizedStringArray, languageCode: LanguageCode): string {
+function localizeString(
+    stringArray: LocalizedStringArray,
+    languageCode: LanguageCode,
+    channelLanguageCode: LanguageCode,
+): string {
     let match = stringArray.find(x => x.languageCode === languageCode);
+    if (!match) {
+        match = stringArray.find(x => x.languageCode === channelLanguageCode);
+    }
     if (!match) {
         match = stringArray.find(x => x.languageCode === DEFAULT_LANGUAGE_CODE);
     }

+ 1 - 1
packages/core/src/config/promotion/actions/order-fixed-discount-action.ts

@@ -13,7 +13,7 @@ export const orderFixedDiscount = new PromotionOrderAction({
         },
     },
     execute(ctx, order, args) {
-        return -args.discount;
+        return -Math.min(args.discount, order.total);
     },
     description: [{ languageCode: LanguageCode.en, value: 'Discount order by fixed amount' }],
 });

+ 3 - 2
packages/core/src/service/helpers/entity-hydrator/entity-hydrator.service.ts

@@ -9,7 +9,7 @@ import { TransactionalConnection } from '../../../connection/transactional-conne
 import { VendureEntity } from '../../../entity/base/base.entity';
 import { ProductVariant } from '../../../entity/product-variant/product-variant.entity';
 import { ProductPriceApplicator } from '../product-price-applicator/product-price-applicator';
-import { translateDeep } from '../utils/translate-entity';
+import { TranslatorService } from '../translator/translator.service';
 
 import { HydrateOptions } from './entity-hydrator-types';
 
@@ -55,6 +55,7 @@ export class EntityHydrator {
     constructor(
         private connection: TransactionalConnection,
         private productPriceApplicator: ProductPriceApplicator,
+        private translator: TranslatorService,
     ) {}
 
     /**
@@ -135,7 +136,7 @@ export class EntityHydrator {
 
                 this.assignSettableProperties(
                     target,
-                    translateDeep(target as any, ctx.languageCode, translateDeepRelations as any),
+                    this.translator.translate(target as any, ctx, translateDeepRelations as any),
                 );
             }
         }

+ 3 - 3
packages/core/src/service/helpers/locale-string-hydrator/locale-string-hydrator.ts

@@ -6,8 +6,7 @@ import { RequestContextCacheService } from '../../../cache/request-context-cache
 import { Translatable, TranslatableKeys, Translated } from '../../../common/types/locale-types';
 import { TransactionalConnection } from '../../../connection/transactional-connection';
 import { VendureEntity } from '../../../entity/base/base.entity';
-import { ProductVariant } from '../../../entity/product-variant/product-variant.entity';
-import { translateDeep } from '../utils/translate-entity';
+import { TranslatorService } from '../translator/translator.service';
 
 /**
  * This helper class is to be used in GraphQL entity resolvers, to resolve fields which depend on being
@@ -18,6 +17,7 @@ export class LocaleStringHydrator {
     constructor(
         private connection: TransactionalConnection,
         private requestCache: RequestContextCacheService,
+        private translator: TranslatorService,
     ) {}
 
     async hydrateLocaleStringField<T extends VendureEntity & Translatable & { languageCode?: LanguageCode }>(
@@ -60,7 +60,7 @@ export class LocaleStringHydrator {
             });
         }
         if (entity.translations.length) {
-            const translated = translateDeep(entity, ctx.languageCode);
+            const translated = this.translator.translate(entity, ctx);
             for (const localeStringProp of Object.keys(entity.translations[0])) {
                 if (
                     localeStringProp === 'base' ||

+ 4 - 3
packages/core/src/service/helpers/order-modifier/order-modifier.ts

@@ -50,8 +50,8 @@ import { StockMovementService } from '../../services/stock-movement.service';
 import { CustomFieldRelationService } from '../custom-field-relation/custom-field-relation.service';
 import { EntityHydrator } from '../entity-hydrator/entity-hydrator.service';
 import { OrderCalculator } from '../order-calculator/order-calculator';
+import { TranslatorService } from '../translator/translator.service';
 import { patchEntity } from '../utils/patch-entity';
-import { translateDeep } from '../utils/translate-entity';
 
 /**
  * @description
@@ -81,6 +81,7 @@ export class OrderModifier {
         private eventBus: EventBus,
         private entityHydrator: EntityHydrator,
         private historyService: HistoryService,
+        private translator: TranslatorService,
     ) {}
 
     /**
@@ -158,13 +159,13 @@ export class OrderModifier {
                 'productVariant.taxCategory',
             ],
         });
-        lineWithRelations.productVariant = translateDeep(
+        lineWithRelations.productVariant = this.translator.translate(
             await this.productVariantService.applyChannelPriceAndTax(
                 lineWithRelations.productVariant,
                 ctx,
                 order,
             ),
-            ctx.languageCode,
+            ctx,
         );
         order.lines.push(lineWithRelations);
         await this.connection.getRepository(ctx, Order).save(order, { reload: false });

+ 1 - 1
packages/core/src/service/helpers/order-state-machine/order-state-machine.ts

@@ -108,7 +108,7 @@ export class OrderStateMachine {
                 return `message.cannot-transition-from-arranging-additional-payment`;
             }
         }
-        if (fromState === 'AddingItems' && toState !== 'Cancelled') {
+        if (fromState === 'AddingItems' && toState !== 'Cancelled' && data.order.lines.length > 0) {
             const variantIds = unique(data.order.lines.map(l => l.productVariant.id));
             const qb = this.connection
                 .getRepository(data.ctx, ProductVariant)

+ 24 - 0
packages/core/src/service/helpers/translator/translator.service.ts

@@ -0,0 +1,24 @@
+import { Injectable } from '@nestjs/common';
+
+import { RequestContext } from '../../../api/common/request-context';
+import { Translatable } from '../../../common/types/locale-types';
+import { ConfigService } from '../../../config';
+import { VendureEntity } from '../../../entity';
+import { DeepTranslatableRelations, translateDeep } from '../utils/translate-entity';
+
+@Injectable()
+export class TranslatorService {
+    constructor(private configService: ConfigService) {}
+
+    translate<T extends Translatable & VendureEntity>(
+        translatable: T,
+        ctx: RequestContext,
+        translatableRelations: DeepTranslatableRelations<T> = [],
+    ) {
+        return translateDeep(
+            translatable,
+            [ctx.languageCode, ctx.channel.defaultLanguageCode, this.configService.defaultLanguageCode],
+            translatableRelations,
+        );
+    }
+}

+ 41 - 19
packages/core/src/service/helpers/utils/translate-entity.spec.ts

@@ -54,25 +54,25 @@ describe('translateEntity()', () => {
     });
 
     it('should unwrap the matching translation', () => {
-        const result = translateEntity(product, LanguageCode.en);
+        const result = translateEntity(product, [LanguageCode.en, LanguageCode.en]);
 
         expect(result).toHaveProperty('name', PRODUCT_NAME_EN);
     });
 
     it('should not overwrite translatable id with translation id', () => {
-        const result = translateEntity(product, LanguageCode.en);
+        const result = translateEntity(product, [LanguageCode.en, LanguageCode.en]);
 
         expect(result).toHaveProperty('id', '1');
     });
 
     it('should note transfer the base from the selected translation', () => {
-        const result = translateEntity(product, LanguageCode.en);
+        const result = translateEntity(product, [LanguageCode.en, LanguageCode.en]);
 
         expect(result).not.toHaveProperty('base');
     });
 
     it('should transfer the languageCode from the selected translation', () => {
-        const result = translateEntity(product, LanguageCode.en);
+        const result = translateEntity(product, [LanguageCode.en, LanguageCode.en]);
 
         expect(result).toHaveProperty('languageCode', 'en');
     });
@@ -83,7 +83,7 @@ describe('translateEntity()', () => {
                 aBooleanField: true,
             };
             product.customFields = customFields;
-            const result = translateEntity(product, LanguageCode.en);
+            const result = translateEntity(product, [LanguageCode.en, LanguageCode.en]);
 
             expect(result.customFields).toEqual(customFields);
         });
@@ -94,7 +94,7 @@ describe('translateEntity()', () => {
                 aLocaleString2: 'translated2',
             };
             product.translations[0].customFields = translatedCustomFields;
-            const result = translateEntity(product, LanguageCode.en);
+            const result = translateEntity(product, [LanguageCode.en, LanguageCode.en]);
 
             expect(result.customFields).toEqual(translatedCustomFields);
         });
@@ -110,7 +110,7 @@ describe('translateEntity()', () => {
             };
             product.customFields = productCustomFields;
             product.translations[0].customFields = translatedCustomFields;
-            const result = translateEntity(product, LanguageCode.en);
+            const result = translateEntity(product, [LanguageCode.en, LanguageCode.en]);
 
             expect(result.customFields).toEqual({ ...productCustomFields, ...translatedCustomFields });
         });
@@ -119,13 +119,17 @@ describe('translateEntity()', () => {
     it('throw if there are no translations available', () => {
         product.translations = [];
 
-        expect(() => translateEntity(product, LanguageCode.en)).toThrow(
+        expect(() => translateEntity(product, [LanguageCode.en, LanguageCode.en])).toThrow(
             'error.entity-has-no-translation-in-language',
         );
     });
 
     it('falls back to default language', () => {
-        expect(translateEntity(product, LanguageCode.zu).name).toEqual(PRODUCT_NAME_EN);
+        expect(translateEntity(product, [LanguageCode.zu, LanguageCode.en]).name).toEqual(PRODUCT_NAME_EN);
+    });
+
+    it('falls back to default language', () => {
+        expect(translateEntity(product, [LanguageCode.zu, LanguageCode.de]).name).toEqual(PRODUCT_NAME_DE);
     });
 });
 
@@ -206,54 +210,72 @@ describe('translateDeep()', () => {
     });
 
     it('should translate the root entity', () => {
-        const result = translateDeep(product, LanguageCode.en);
+        const result = translateDeep(product, [LanguageCode.en, LanguageCode.en]);
 
         expect(result).toHaveProperty('name', PRODUCT_NAME_EN);
     });
 
     it('should not throw if root entity has no translations', () => {
-        expect(() => translateDeep(testProduct, LanguageCode.en)).not.toThrow();
+        expect(() => translateDeep(testProduct, [LanguageCode.en, LanguageCode.en])).not.toThrow();
     });
 
     it('should not throw if first-level nested entity is not defined', () => {
         testProduct.singleRealVariant = undefined as any;
-        expect(() => translateDeep(testProduct, LanguageCode.en, ['singleRealVariant'])).not.toThrow();
+        expect(() =>
+            translateDeep(testProduct, [LanguageCode.en, LanguageCode.en], ['singleRealVariant']),
+        ).not.toThrow();
     });
 
     it('should not throw if second-level nested entity is not defined', () => {
         testProduct.singleRealVariant.options = undefined as any;
         expect(() =>
-            translateDeep(testProduct, LanguageCode.en, [['singleRealVariant', 'options']]),
+            translateDeep(
+                testProduct,
+                [LanguageCode.en, LanguageCode.en],
+                [['singleRealVariant', 'options']],
+            ),
         ).not.toThrow();
     });
 
     it('should translate a first-level nested non-array entity', () => {
-        const result = translateDeep(testProduct, LanguageCode.en, ['singleRealVariant']);
+        const result = translateDeep(testProduct, [LanguageCode.en, LanguageCode.en], ['singleRealVariant']);
 
         expect(result.singleRealVariant).toHaveProperty('name', VARIANT_NAME_EN);
     });
 
     it('should translate a first-level nested entity array', () => {
-        const result = translateDeep(product, LanguageCode.en, ['variants']);
+        const result = translateDeep(product, [LanguageCode.en, LanguageCode.en], ['variants']);
 
         expect(result).toHaveProperty('name', PRODUCT_NAME_EN);
         expect(result.variants[0]).toHaveProperty('name', VARIANT_NAME_EN);
     });
 
     it('should translate a second-level nested non-array entity', () => {
-        const result = translateDeep(testProduct, LanguageCode.en, [['singleTestVariant', 'singleOption']]);
+        const result = translateDeep(
+            testProduct,
+            [LanguageCode.en, LanguageCode.en],
+            [['singleTestVariant', 'singleOption']],
+        );
 
         expect(result.singleTestVariant.singleOption).toHaveProperty('name', OPTION_NAME_EN);
     });
 
     it('should translate a second-level nested entity array (first-level is not array)', () => {
-        const result = translateDeep(testProduct, LanguageCode.en, [['singleRealVariant', 'options']]);
+        const result = translateDeep(
+            testProduct,
+            [LanguageCode.en, LanguageCode.en],
+            [['singleRealVariant', 'options']],
+        );
 
         expect(result.singleRealVariant.options[0]).toHaveProperty('name', OPTION_NAME_EN);
     });
 
     it('should translate a second-level nested entity array', () => {
-        const result = translateDeep(product, LanguageCode.en, ['variants', ['variants', 'options']]);
+        const result = translateDeep(
+            product,
+            [LanguageCode.en, LanguageCode.en],
+            ['variants', ['variants', 'options']],
+        );
 
         expect(result).toHaveProperty('name', PRODUCT_NAME_EN);
         expect(result.variants[0]).toHaveProperty('name', VARIANT_NAME_EN);
@@ -306,7 +328,7 @@ describe('translateTree()', () => {
     });
 
     it('translates all entities in the tree', () => {
-        const result = translateTree(cat1, LanguageCode.en, []);
+        const result = translateTree(cat1, [LanguageCode.en, LanguageCode.en], []);
 
         expect(result.languageCode).toBe(LanguageCode.en);
         expect(result.name).toBe('cat1 en');

+ 23 - 15
packages/core/src/service/helpers/utils/translate-entity.ts

@@ -9,20 +9,20 @@ import { VendureEntity } from '../../../entity/base/base.entity';
 // prettier-ignore
 export type TranslatableRelationsKeys<T> = {
     [K in keyof T]: T[K] extends string ? never :
-    T[K] extends number ? never :
-    T[K] extends boolean ? never :
-    T[K] extends undefined ? never :
-    T[K] extends string[] ? never :
-    T[K] extends number[] ? never :
-    T[K] extends boolean[] ? never :
-    K extends 'translations' ? never :
-    K extends 'customFields' ? never : K
+        T[K] extends number ? never :
+            T[K] extends boolean ? never :
+                T[K] extends undefined ? never :
+                    T[K] extends string[] ? never :
+                        T[K] extends number[] ? never :
+                            T[K] extends boolean[] ? never :
+                                K extends 'translations' ? never :
+                                    K extends 'customFields' ? never : K
 }[keyof T];
 
 // prettier-ignore
 export type NestedTranslatableRelations<T> = {
     [K in TranslatableRelationsKeys<T>]: T[K] extends any[] ?
-        [K, TranslatableRelationsKeys<UnwrappedArray<T[K]>>]:
+        [K, TranslatableRelationsKeys<UnwrappedArray<T[K]>>] :
         [K, TranslatableRelationsKeys<T[K]>]
 };
 
@@ -38,11 +38,19 @@ export type DeepTranslatableRelations<T> = Array<TranslatableRelationsKeys<T> |
  */
 export function translateEntity<T extends Translatable & VendureEntity>(
     translatable: T,
-    languageCode: LanguageCode,
+    languageCode: LanguageCode | [LanguageCode, ...LanguageCode[]],
 ): Translated<T> {
     let translation: Translation<VendureEntity> | undefined;
     if (translatable.translations) {
-        translation = translatable.translations.find(t => t.languageCode === languageCode);
+        if (Array.isArray(languageCode)) {
+            for (const lc of languageCode) {
+                translation = translatable.translations.find(t => t.languageCode === lc);
+                if (translation) break;
+            }
+        } else {
+            translation = translatable.translations.find(t => t.languageCode === languageCode);
+        }
+
         if (!translation && languageCode !== DEFAULT_LANGUAGE_CODE) {
             translation = translatable.translations.find(t => t.languageCode === DEFAULT_LANGUAGE_CODE);
         }
@@ -56,7 +64,7 @@ export function translateEntity<T extends Translatable & VendureEntity>(
     if (!translation) {
         throw new InternalServerError(`error.entity-has-no-translation-in-language`, {
             entityName: translatable.constructor.name,
-            languageCode,
+            languageCode: Array.isArray(languageCode) ? languageCode.join() : languageCode,
         });
     }
 
@@ -83,7 +91,7 @@ export function translateEntity<T extends Translatable & VendureEntity>(
  */
 export function translateDeep<T extends Translatable & VendureEntity>(
     translatable: T,
-    languageCode: LanguageCode,
+    languageCode: LanguageCode | [LanguageCode, ...LanguageCode[]],
     translatableRelations: DeepTranslatableRelations<T> = [],
 ): Translated<T> {
     let translatedEntity: Translated<T>;
@@ -132,7 +140,7 @@ export function translateDeep<T extends Translatable & VendureEntity>(
 function translateLeaf(
     object: { [key: string]: any } | undefined,
     property: string,
-    languageCode: LanguageCode,
+    languageCode: LanguageCode | [LanguageCode, ...LanguageCode[]],
 ): any {
     if (object && object[property]) {
         if (Array.isArray(object[property])) {
@@ -150,7 +158,7 @@ export type TreeNode = { children: TreeNode[] } & Translatable & VendureEntity;
  */
 export function translateTree<T extends TreeNode>(
     node: T,
-    languageCode: LanguageCode,
+    languageCode: LanguageCode | [LanguageCode, ...LanguageCode[]],
     translatableRelations: DeepTranslatableRelations<T> = [],
 ): Translated<T> {
     const output = translateDeep(node, languageCode, translatableRelations);

+ 2 - 0
packages/core/src/service/service.module.ts

@@ -26,6 +26,7 @@ import { RequestContextService } from './helpers/request-context/request-context
 import { ShippingCalculator } from './helpers/shipping-calculator/shipping-calculator';
 import { SlugValidator } from './helpers/slug-validator/slug-validator';
 import { TranslatableSaver } from './helpers/translatable-saver/translatable-saver';
+import { TranslatorService } from './helpers/translator/translator.service';
 import { VerificationTokenGenerator } from './helpers/verification-token-generator/verification-token-generator';
 import { InitializerService } from './initializer.service';
 import { AdministratorService } from './services/administrator.service';
@@ -118,6 +119,7 @@ const helpers = [
     ProductPriceApplicator,
     EntityHydrator,
     RequestContextService,
+    TranslatorService,
 ];
 
 /**

+ 13 - 15
packages/core/src/service/services/collection.service.ts

@@ -24,7 +24,6 @@ import { assertFound, idsAreEqual } from '../../common/utils';
 import { ConfigService } from '../../config/config.service';
 import { Logger } from '../../config/logger/vendure-logger';
 import { TransactionalConnection } from '../../connection/transactional-connection';
-import { Asset, FacetValue } from '../../entity';
 import { CollectionTranslation } from '../../entity/collection/collection-translation.entity';
 import { Collection } from '../../entity/collection/collection.entity';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
@@ -40,8 +39,8 @@ import { CustomFieldRelationService } from '../helpers/custom-field-relation/cus
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { SlugValidator } from '../helpers/slug-validator/slug-validator';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
+import { TranslatorService } from '../helpers/translator/translator.service';
 import { moveToIndex } from '../helpers/utils/move-to-index';
-import { translateDeep } from '../helpers/utils/translate-entity';
 
 import { AssetService } from './asset.service';
 import { ChannelService } from './channel.service';
@@ -77,6 +76,7 @@ export class CollectionService implements OnModuleInit {
         private slugValidator: SlugValidator,
         private configArgService: ConfigArgService,
         private customFieldRelationService: CustomFieldRelationService,
+        private translator: TranslatorService,
     ) {}
 
     /**
@@ -89,8 +89,7 @@ export class CollectionService implements OnModuleInit {
         merge(productEvents$, variantEvents$)
             .pipe(debounceTime(50))
             .subscribe(async event => {
-                const collections = await this.connection
-                    .rawConnection
+                const collections = await this.connection.rawConnection
                     .getRepository(Collection)
                     .createQueryBuilder('collection')
                     .select('collection.id', 'id')
@@ -150,7 +149,7 @@ export class CollectionService implements OnModuleInit {
             .getManyAndCount()
             .then(async ([collections, totalItems]) => {
                 const items = collections.map(collection =>
-                    translateDeep(collection, ctx.languageCode, ['parent']),
+                    this.translator.translate(collection, ctx, ['parent']),
                 );
                 return {
                     items,
@@ -177,7 +176,7 @@ export class CollectionService implements OnModuleInit {
         if (!collection) {
             return;
         }
-        return translateDeep(collection, ctx.languageCode, ['parent']);
+        return this.translator.translate(collection, ctx, ['parent']);
     }
 
     async findByIds(
@@ -190,7 +189,7 @@ export class CollectionService implements OnModuleInit {
             loadEagerRelations: true,
         });
         return collections.then(values =>
-            values.map(collection => translateDeep(collection, ctx.languageCode, ['parent'])),
+            values.map(collection => this.translator.translate(collection, ctx, ['parent'])),
         );
     }
 
@@ -242,7 +241,7 @@ export class CollectionService implements OnModuleInit {
             )
             .getOne();
 
-        return parent && translateDeep(parent, ctx.languageCode);
+        return parent && this.translator.translate(parent, ctx);
     }
 
     /**
@@ -294,7 +293,7 @@ export class CollectionService implements OnModuleInit {
         }
         const result = await qb.getMany();
 
-        return result.map(collection => translateDeep(collection, ctx.languageCode));
+        return result.map(collection => this.translator.translate(collection, ctx));
     }
 
     /**
@@ -321,7 +320,7 @@ export class CollectionService implements OnModuleInit {
         };
 
         const descendants = await getChildren(rootId);
-        return descendants.map(c => translateDeep(c, ctx.languageCode));
+        return descendants.map(c => this.translator.translate(c, ctx));
     }
 
     /**
@@ -360,7 +359,7 @@ export class CollectionService implements OnModuleInit {
                 ancestors.forEach(a => {
                     const category = categories.find(c => c.id === a.id);
                     if (category) {
-                        resultCategories.push(ctx ? translateDeep(category, ctx.languageCode) : category);
+                        resultCategories.push(ctx ? this.translator.translate(category, ctx) : category);
                     }
                 });
                 return resultCategories;
@@ -562,8 +561,7 @@ export class CollectionService implements OnModuleInit {
         ]);
         const postIds = collection.productVariants.map(v => v.id);
         try {
-            await this.connection
-                .rawConnection
+            await this.connection.rawConnection
                 .getRepository(Collection)
                 // Only update the exact changed properties, to avoid VERY hard-to-debug
                 // non-deterministic race conditions e.g. when the "position" is changed
@@ -677,7 +675,7 @@ export class CollectionService implements OnModuleInit {
             .getOne();
 
         if (existingRoot) {
-            this.rootCollection = translateDeep(existingRoot, ctx.languageCode);
+            this.rootCollection = this.translator.translate(existingRoot, ctx);
             return this.rootCollection;
         }
 
@@ -702,7 +700,7 @@ export class CollectionService implements OnModuleInit {
                 filters: [],
             }),
         );
-        this.rootCollection = translateDeep(newRoot, ctx.languageCode);
+        this.rootCollection = this.translator.translate(newRoot, ctx);
         return this.rootCollection;
     }
 }

+ 6 - 5
packages/core/src/service/services/country.service.ts

@@ -21,7 +21,7 @@ import { EventBus } from '../../event-bus';
 import { CountryEvent } from '../../event-bus/events/country-event';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
-import { translateDeep } from '../helpers/utils/translate-entity';
+import { TranslatorService } from '../helpers/translator/translator.service';
 
 /**
  * @description
@@ -36,6 +36,7 @@ export class CountryService {
         private listQueryBuilder: ListQueryBuilder,
         private translatableSaver: TranslatableSaver,
         private eventBus: EventBus,
+        private translator: TranslatorService,
     ) {}
 
     findAll(
@@ -47,7 +48,7 @@ export class CountryService {
             .build(Country, options, { ctx, relations })
             .getManyAndCount()
             .then(([countries, totalItems]) => {
-                const items = countries.map(country => translateDeep(country, ctx.languageCode));
+                const items = countries.map(country => this.translator.translate(country, ctx));
                 return {
                     items,
                     totalItems,
@@ -63,7 +64,7 @@ export class CountryService {
         return this.connection
             .getRepository(ctx, Country)
             .findOne(countryId, { relations })
-            .then(country => country && translateDeep(country, ctx.languageCode));
+            .then(country => country && this.translator.translate(country, ctx));
     }
 
     /**
@@ -74,7 +75,7 @@ export class CountryService {
         return this.connection
             .getRepository(ctx, Country)
             .find({ where: { enabled: true } })
-            .then(items => items.map(country => translateDeep(country, ctx.languageCode)));
+            .then(items => items.map(country => this.translator.translate(country, ctx)));
     }
 
     /**
@@ -90,7 +91,7 @@ export class CountryService {
         if (!country) {
             throw new UserInputError('error.country-code-not-valid', { countryCode });
         }
-        return translateDeep(country, ctx.languageCode);
+        return this.translator.translate(country, ctx);
     }
 
     async create(ctx: RequestContext, input: CreateCountryInput): Promise<Translated<Country>> {

+ 5 - 4
packages/core/src/service/services/customer.service.ts

@@ -58,9 +58,9 @@ import { PasswordResetEvent } from '../../event-bus/events/password-reset-event'
 import { PasswordResetVerifiedEvent } from '../../event-bus/events/password-reset-verified-event';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
+import { TranslatorService } from '../helpers/translator/translator.service';
 import { addressToLine } from '../helpers/utils/address-to-line';
 import { patchEntity } from '../helpers/utils/patch-entity';
-import { translateDeep } from '../helpers/utils/translate-entity';
 
 import { ChannelService } from './channel.service';
 import { CountryService } from './country.service';
@@ -85,6 +85,7 @@ export class CustomerService {
         private historyService: HistoryService,
         private channelService: ChannelService,
         private customFieldRelationService: CustomFieldRelationService,
+        private translator: TranslatorService,
     ) {}
 
     findAll(
@@ -155,7 +156,7 @@ export class CustomerService {
             .getMany()
             .then(addresses => {
                 addresses.forEach(address => {
-                    address.country = translateDeep(address.country, ctx.languageCode);
+                    address.country = this.translator.translate(address.country, ctx);
                 });
                 return addresses;
             });
@@ -724,7 +725,7 @@ export class CustomerService {
         if (input.countryCode && input.countryCode !== address.country.code) {
             address.country = await this.countryService.findOneByCode(ctx, input.countryCode);
         } else {
-            address.country = translateDeep(address.country, ctx.languageCode);
+            address.country = this.translator.translate(address.country, ctx);
         }
         let updatedAddress = patchEntity(address, input);
         updatedAddress = await this.connection.getRepository(ctx, Address).save(updatedAddress);
@@ -758,7 +759,7 @@ export class CustomerService {
         if (!customer) {
             throw new EntityNotFoundError('Address', id);
         }
-        address.country = translateDeep(address.country, ctx.languageCode);
+        address.country = this.translator.translate(address.country, ctx);
         await this.reassignDefaultsForDeletedAddress(ctx, address);
         await this.historyService.createHistoryEntryForCustomer({
             customerId: address.customer.id,

+ 18 - 9
packages/core/src/service/services/facet-value.service.ts

@@ -22,6 +22,7 @@ import { EventBus } from '../../event-bus';
 import { FacetValueEvent } from '../../event-bus/events/facet-value-event';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
+import { TranslatorService } from '../helpers/translator/translator.service';
 import { translateDeep } from '../helpers/utils/translate-entity';
 
 import { ChannelService } from './channel.service';
@@ -41,6 +42,7 @@ export class FacetValueService {
         private customFieldRelationService: CustomFieldRelationService,
         private channelService: ChannelService,
         private eventBus: EventBus,
+        private translator: TranslatorService,
     ) {}
 
     /**
@@ -48,16 +50,23 @@ export class FacetValueService {
      */
     findAll(lang: LanguageCode): Promise<Array<Translated<FacetValue>>>;
     findAll(ctx: RequestContext, lang: LanguageCode): Promise<Array<Translated<FacetValue>>>;
-    findAll(ctxOrLang: RequestContext | LanguageCode, lang?: LanguageCode): Promise<Array<Translated<FacetValue>>> {
-        const [repository, languageCode] = ctxOrLang instanceof RequestContext 
-            ? [this.connection.getRepository(ctxOrLang, FacetValue), lang!]
-            : [this.connection.rawConnection.getRepository(FacetValue), ctxOrLang];
-
+    findAll(
+        ctxOrLang: RequestContext | LanguageCode,
+        lang?: LanguageCode,
+    ): Promise<Array<Translated<FacetValue>>> {
+        const [repository, languageCode] =
+            ctxOrLang instanceof RequestContext
+                ? // tslint:disable-next-line:no-non-null-assertion
+                  [this.connection.getRepository(ctxOrLang, FacetValue), lang!]
+                : [this.connection.rawConnection.getRepository(FacetValue), ctxOrLang];
+        // ToDo Implement usage of channelLanguageCode
         return repository
             .find({
                 relations: ['facet'],
             })
-            .then(facetValues => facetValues.map(facetValue => translateDeep(facetValue, languageCode, ['facet'])));
+            .then(facetValues =>
+                facetValues.map(facetValue => translateDeep(facetValue, languageCode, ['facet'])),
+            );
     }
 
     findOne(ctx: RequestContext, id: ID): Promise<Translated<FacetValue> | undefined> {
@@ -66,7 +75,7 @@ export class FacetValueService {
             .findOne(id, {
                 relations: ['facet'],
             })
-            .then(facetValue => facetValue && translateDeep(facetValue, ctx.languageCode, ['facet']));
+            .then(facetValue => facetValue && this.translator.translate(facetValue, ctx, ['facet']));
     }
 
     findByIds(ctx: RequestContext, ids: ID[]): Promise<Array<Translated<FacetValue>>> {
@@ -74,7 +83,7 @@ export class FacetValueService {
             relations: ['facet'],
         });
         return facetValues.then(values =>
-            values.map(facetValue => translateDeep(facetValue, ctx.languageCode, ['facet'])),
+            values.map(facetValue => this.translator.translate(facetValue, ctx, ['facet'])),
         );
     }
 
@@ -90,7 +99,7 @@ export class FacetValueService {
                     facet: { id },
                 },
             })
-            .then(values => values.map(facetValue => translateDeep(facetValue, ctx.languageCode)));
+            .then(values => values.map(facetValue => this.translator.translate(facetValue, ctx)));
     }
 
     async create(

+ 32 - 19
packages/core/src/service/services/facet.service.ts

@@ -22,6 +22,7 @@ import { FacetEvent } from '../../event-bus/events/facet-event';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
+import { TranslatorService } from '../helpers/translator/translator.service';
 import { translateDeep } from '../helpers/utils/translate-entity';
 
 import { ChannelService } from './channel.service';
@@ -44,6 +45,7 @@ export class FacetService {
         private channelService: ChannelService,
         private customFieldRelationService: CustomFieldRelationService,
         private eventBus: EventBus,
+        private translator: TranslatorService,
     ) {}
 
     findAll(
@@ -60,7 +62,7 @@ export class FacetService {
             .getManyAndCount()
             .then(([facets, totalItems]) => {
                 const items = facets.map(facet =>
-                    translateDeep(facet, ctx.languageCode, ['values', ['values', 'facet']]),
+                    this.translator.translate(facet, ctx, ['values', ['values', 'facet']]),
                 );
                 return {
                     items,
@@ -78,32 +80,43 @@ export class FacetService {
             .findOneInChannel(ctx, Facet, facetId, ctx.channelId, {
                 relations: relations ?? ['values', 'values.facet', 'channels'],
             })
-            .then(facet => facet && translateDeep(facet, ctx.languageCode, ['values', ['values', 'facet']]));
+            .then(facet => facet && this.translator.translate(facet, ctx, ['values', ['values', 'facet']]));
     }
 
     /**
      * @deprecated Use {@link FacetService.findByCode findByCode(ctx, facetCode, lang)} instead
      */
     findByCode(facetCode: string, lang: LanguageCode): Promise<Translated<Facet> | undefined>;
-    findByCode(ctx: RequestContext, facetCode: string, lang: LanguageCode): Promise<Translated<Facet> | undefined>;
     findByCode(
-        ctxOrFacetCode: RequestContext | string, 
-        facetCodeOrLang: string | LanguageCode, 
-        lang?: LanguageCode
+        ctx: RequestContext,
+        facetCode: string,
+        lang: LanguageCode,
+    ): Promise<Translated<Facet> | undefined>;
+    findByCode(
+        ctxOrFacetCode: RequestContext | string,
+        facetCodeOrLang: string | LanguageCode,
+        lang?: LanguageCode,
     ): Promise<Translated<Facet> | undefined> {
         const relations = ['values', 'values.facet'];
-        const [repository, facetCode, languageCode] = ctxOrFacetCode instanceof RequestContext 
-            ? [this.connection.getRepository(ctxOrFacetCode, Facet), facetCodeOrLang, lang!]
-            : [this.connection.rawConnection.getRepository(Facet), ctxOrFacetCode, facetCodeOrLang as LanguageCode];
-
-
-        return repository.findOne({
-            where: {
-                code: facetCode,
-            },
-            relations,
-        })
-        .then(facet => facet && translateDeep(facet, languageCode, ['values', ['values', 'facet']]));
+        const [repository, facetCode, languageCode] =
+            ctxOrFacetCode instanceof RequestContext
+                ? // tslint:disable-next-line:no-non-null-assertion
+                  [this.connection.getRepository(ctxOrFacetCode, Facet), facetCodeOrLang, lang!]
+                : [
+                      this.connection.rawConnection.getRepository(Facet),
+                      ctxOrFacetCode,
+                      facetCodeOrLang as LanguageCode,
+                  ];
+
+        // ToDo Implement usage of channelLanguageCode
+        return repository
+            .findOne({
+                where: {
+                    code: facetCode,
+                },
+                relations,
+            })
+            .then(facet => facet && translateDeep(facet, languageCode, ['values', ['values', 'facet']]));
     }
 
     /**
@@ -119,7 +132,7 @@ export class FacetService {
             .where('facetValue.id = :id', { id })
             .getOne();
         if (facet) {
-            return translateDeep(facet, ctx.languageCode);
+            return this.translator.translate(facet, ctx);
         }
     }
 

+ 3 - 2
packages/core/src/service/services/order-testing.service.ts

@@ -23,7 +23,7 @@ import { ConfigArgService } from '../helpers/config-arg/config-arg.service';
 import { OrderCalculator } from '../helpers/order-calculator/order-calculator';
 import { ProductPriceApplicator } from '../helpers/product-price-applicator/product-price-applicator';
 import { ShippingCalculator } from '../helpers/shipping-calculator/shipping-calculator';
-import { translateDeep } from '../helpers/utils/translate-entity';
+import { TranslatorService } from '../helpers/translator/translator.service';
 
 /**
  * @description
@@ -41,6 +41,7 @@ export class OrderTestingService {
         private configArgService: ConfigArgService,
         private configService: ConfigService,
         private productPriceApplicator: ProductPriceApplicator,
+        private translator: TranslatorService,
     ) {}
 
     /**
@@ -87,7 +88,7 @@ export class OrderTestingService {
         const eligibleMethods = await this.shippingCalculator.getEligibleShippingMethods(ctx, mockOrder);
         return eligibleMethods
             .map(result => {
-                translateDeep(result.method, ctx.languageCode);
+                this.translator.translate(result.method, ctx);
                 return result;
             })
             .map(result => {

+ 5 - 4
packages/core/src/service/services/order.service.ts

@@ -112,6 +112,7 @@ import { PaymentState } from '../helpers/payment-state-machine/payment-state';
 import { PaymentStateMachine } from '../helpers/payment-state-machine/payment-state-machine';
 import { RefundStateMachine } from '../helpers/refund-state-machine/refund-state-machine';
 import { ShippingCalculator } from '../helpers/shipping-calculator/shipping-calculator';
+import { TranslatorService } from '../helpers/translator/translator.service';
 import {
     orderItemsAreAllCancelled,
     orderItemsAreDelivered,
@@ -120,7 +121,6 @@ import {
     totalCoveredByPayments,
 } from '../helpers/utils/order-utils';
 import { patchEntity } from '../helpers/utils/patch-entity';
-import { translateDeep } from '../helpers/utils/translate-entity';
 
 import { ChannelService } from './channel.service';
 import { CountryService } from './country.service';
@@ -165,6 +165,7 @@ export class OrderService {
         private orderModifier: OrderModifier,
         private customFieldRelationService: CustomFieldRelationService,
         private requestCache: RequestContextCacheService,
+        private translator: TranslatorService,
     ) {}
 
     /**
@@ -258,13 +259,13 @@ export class OrderService {
         if (order) {
             if (effectiveRelations.includes('lines.productVariant')) {
                 for (const line of order.lines) {
-                    line.productVariant = translateDeep(
+                    line.productVariant = this.translator.translate(
                         await this.productVariantService.applyChannelPriceAndTax(
                             line.productVariant,
                             ctx,
                             order,
                         ),
-                        ctx.languageCode,
+                        ctx,
                     );
                 }
             }
@@ -1228,7 +1229,7 @@ export class OrderService {
                 line.productVariant,
             );
             if (fulfillableStockLevel < lineInput.quantity) {
-                const productVariant = translateDeep(line.productVariant, ctx.languageCode);
+                const productVariant = this.translator.translate(line.productVariant, ctx);
                 return new InsufficientStockOnHandError(
                     productVariant.id as string,
                     productVariant.name,

+ 5 - 4
packages/core/src/service/services/product-option-group.service.ts

@@ -20,7 +20,7 @@ import { EventBus } from '../../event-bus';
 import { ProductOptionGroupEvent } from '../../event-bus/events/product-option-group-event';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
-import { translateDeep } from '../helpers/utils/translate-entity';
+import { TranslatorService } from '../helpers/translator/translator.service';
 
 import { ProductOptionService } from './product-option.service';
 
@@ -38,6 +38,7 @@ export class ProductOptionGroupService {
         private customFieldRelationService: CustomFieldRelationService,
         private productOptionService: ProductOptionService,
         private eventBus: EventBus,
+        private translator: TranslatorService,
     ) {}
 
     findAll(
@@ -56,7 +57,7 @@ export class ProductOptionGroupService {
         return this.connection
             .getRepository(ctx, ProductOptionGroup)
             .find(findOptions)
-            .then(groups => groups.map(group => translateDeep(group, ctx.languageCode, ['options'])));
+            .then(groups => groups.map(group => this.translator.translate(group, ctx, ['options'])));
     }
 
     findOne(
@@ -69,7 +70,7 @@ export class ProductOptionGroupService {
             .findOne(id, {
                 relations: relations ?? ['options'],
             })
-            .then(group => group && translateDeep(group, ctx.languageCode, ['options']));
+            .then(group => group && this.translator.translate(group, ctx, ['options']));
     }
 
     getOptionGroupsByProductId(ctx: RequestContext, id: ID): Promise<Array<Translated<ProductOptionGroup>>> {
@@ -84,7 +85,7 @@ export class ProductOptionGroupService {
                     id: 'ASC',
                 },
             })
-            .then(groups => groups.map(group => translateDeep(group, ctx.languageCode, ['options'])));
+            .then(groups => groups.map(group => this.translator.translate(group, ctx, ['options'])));
     }
 
     async create(

+ 4 - 3
packages/core/src/service/services/product-option.service.ts

@@ -21,7 +21,7 @@ import { EventBus } from '../../event-bus';
 import { ProductOptionEvent } from '../../event-bus/events/product-option-event';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
-import { translateDeep } from '../helpers/utils/translate-entity';
+import { TranslatorService } from '../helpers/translator/translator.service';
 
 /**
  * @description
@@ -36,6 +36,7 @@ export class ProductOptionService {
         private translatableSaver: TranslatableSaver,
         private customFieldRelationService: CustomFieldRelationService,
         private eventBus: EventBus,
+        private translator: TranslatorService,
     ) {}
 
     findAll(ctx: RequestContext): Promise<Array<Translated<ProductOption>>> {
@@ -44,7 +45,7 @@ export class ProductOptionService {
             .find({
                 relations: ['group'],
             })
-            .then(options => options.map(option => translateDeep(option, ctx.languageCode)));
+            .then(options => options.map(option => this.translator.translate(option, ctx)));
     }
 
     findOne(ctx: RequestContext, id: ID): Promise<Translated<ProductOption> | undefined> {
@@ -53,7 +54,7 @@ export class ProductOptionService {
             .findOne(id, {
                 relations: ['group'],
             })
-            .then(option => option && translateDeep(option, ctx.languageCode));
+            .then(option => option && this.translator.translate(option, ctx));
     }
 
     async create(

+ 9 - 8
packages/core/src/service/services/product-variant.service.ts

@@ -41,8 +41,8 @@ import { CustomFieldRelationService } from '../helpers/custom-field-relation/cus
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { ProductPriceApplicator } from '../helpers/product-price-applicator/product-price-applicator';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
+import { TranslatorService } from '../helpers/translator/translator.service';
 import { samplesEach } from '../helpers/utils/samples-each';
-import { translateDeep } from '../helpers/utils/translate-entity';
 
 import { AssetService } from './asset.service';
 import { ChannelService } from './channel.service';
@@ -76,6 +76,7 @@ export class ProductVariantService {
         private customFieldRelationService: CustomFieldRelationService,
         private requestCache: RequestContextCacheService,
         private productPriceApplicator: ProductPriceApplicator,
+        private translator: TranslatorService,
     ) {}
 
     async findAll(
@@ -115,7 +116,7 @@ export class ProductVariantService {
             })
             .then(async result => {
                 if (result) {
-                    return translateDeep(await this.applyChannelPriceAndTax(result, ctx), ctx.languageCode, [
+                    return this.translator.translate(await this.applyChannelPriceAndTax(result, ctx), ctx, [
                         'product',
                     ]);
                 }
@@ -235,7 +236,7 @@ export class ProductVariantService {
             relations: ['productVariant', 'productVariant.taxCategory'],
             includeSoftDeleted: true,
         });
-        return translateDeep(await this.applyChannelPriceAndTax(productVariant, ctx), ctx.languageCode);
+        return this.translator.translate(await this.applyChannelPriceAndTax(productVariant, ctx), ctx);
     }
 
     /**
@@ -247,7 +248,7 @@ export class ProductVariantService {
             .findOneInChannel(ctx, ProductVariant, variantId, ctx.channelId, {
                 relations: ['options'],
             })
-            .then(variant => (!variant ? [] : variant.options.map(o => translateDeep(o, ctx.languageCode))));
+            .then(variant => (!variant ? [] : variant.options.map(o => this.translator.translate(o, ctx))));
     }
 
     getFacetValuesForVariant(ctx: RequestContext, variantId: ID): Promise<Array<Translated<FacetValue>>> {
@@ -256,7 +257,7 @@ export class ProductVariantService {
                 relations: ['facetValues', 'facetValues.facet', 'facetValues.channels'],
             })
             .then(variant =>
-                !variant ? [] : variant.facetValues.map(o => translateDeep(o, ctx.languageCode, ['facet'])),
+                !variant ? [] : variant.facetValues.map(o => this.translator.translate(o, ctx, ['facet'])),
             );
     }
 
@@ -270,7 +271,7 @@ export class ProductVariantService {
         const product = await this.connection.getEntityOrThrow(ctx, Product, variant.productId, {
             includeSoftDeleted: true,
         });
-        return translateDeep(product, ctx.languageCode);
+        return this.translator.translate(product, ctx);
     }
 
     /**
@@ -607,7 +608,7 @@ export class ProductVariantService {
         return await Promise.all(
             variants.map(async variant => {
                 const variantWithPrices = await this.applyChannelPriceAndTax(variant, ctx);
-                return translateDeep(variantWithPrices, ctx.languageCode, [
+                return this.translator.translate(variantWithPrices, ctx, [
                     'options',
                     'facetValues',
                     ['facetValues', 'facet'],
@@ -770,7 +771,7 @@ export class ProductVariantService {
                 const variantOptionIds = this.sortJoin(variant.options, ',', 'id');
                 if (variantOptionIds === inputOptionIds) {
                     throw new UserInputError('error.product-variant-options-combination-already-exists', {
-                        variantName: translateDeep(variant, ctx.languageCode).name,
+                        variantName: this.translator.translate(variant, ctx).name,
                     });
                 }
             });

+ 7 - 6
packages/core/src/service/services/product.service.ts

@@ -34,7 +34,7 @@ import { CustomFieldRelationService } from '../helpers/custom-field-relation/cus
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { SlugValidator } from '../helpers/slug-validator/slug-validator';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
-import { translateDeep } from '../helpers/utils/translate-entity';
+import { TranslatorService } from '../helpers/translator/translator.service';
 
 import { AssetService } from './asset.service';
 import { ChannelService } from './channel.service';
@@ -69,6 +69,7 @@ export class ProductService {
         private eventBus: EventBus,
         private slugValidator: SlugValidator,
         private customFieldRelationService: CustomFieldRelationService,
+        private translator: TranslatorService,
         private productOptionGroupService: ProductOptionGroupService,
     ) {}
 
@@ -87,7 +88,7 @@ export class ProductService {
             .getManyAndCount()
             .then(async ([products, totalItems]) => {
                 const items = products.map(product =>
-                    translateDeep(product, ctx.languageCode, ['facetValues', ['facetValues', 'facet']]),
+                    this.translator.translate(product, ctx, ['facetValues', ['facetValues', 'facet']]),
                 );
                 return {
                     items,
@@ -116,7 +117,7 @@ export class ProductService {
         if (!product) {
             return;
         }
-        return translateDeep(product, ctx.languageCode, ['facetValues', ['facetValues', 'facet']]);
+        return this.translator.translate(product, ctx, ['facetValues', ['facetValues', 'facet']]);
     }
 
     async findByIds(
@@ -138,7 +139,7 @@ export class ProductService {
             .getMany()
             .then(products =>
                 products.map(product =>
-                    translateDeep(product, ctx.languageCode, ['facetValues', ['facetValues', 'facet']]),
+                    this.translator.translate(product, ctx, ['facetValues', ['facetValues', 'facet']]),
                 ),
             );
     }
@@ -162,7 +163,7 @@ export class ProductService {
                 relations: ['facetValues', 'facetValues.facet', 'facetValues.channels'],
             })
             .then(variant =>
-                !variant ? [] : variant.facetValues.map(o => translateDeep(o, ctx.languageCode, ['facet'])),
+                !variant ? [] : variant.facetValues.map(o => this.translator.translate(o, ctx, ['facet'])),
             );
     }
 
@@ -201,7 +202,7 @@ export class ProductService {
             .getOne()
             .then(product =>
                 product
-                    ? translateDeep(product, ctx.languageCode, ['facetValues', ['facetValues', 'facet']])
+                    ? this.translator.translate(product, ctx, ['facetValues', ['facetValues', 'facet']])
                     : undefined,
             );
     }

+ 5 - 4
packages/core/src/service/services/shipping-method.service.ts

@@ -26,7 +26,7 @@ import { ConfigArgService } from '../helpers/config-arg/config-arg.service';
 import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
-import { translateDeep } from '../helpers/utils/translate-entity';
+import { TranslatorService } from '../helpers/translator/translator.service';
 
 import { ChannelService } from './channel.service';
 
@@ -47,6 +47,7 @@ export class ShippingMethodService {
         private translatableSaver: TranslatableSaver,
         private customFieldRelationService: CustomFieldRelationService,
         private eventBus: EventBus,
+        private translator: TranslatorService,
     ) {}
 
     /** @internal */
@@ -73,7 +74,7 @@ export class ShippingMethodService {
             })
             .getManyAndCount()
             .then(([items, totalItems]) => ({
-                items: items.map(i => translateDeep(i, ctx.languageCode)),
+                items: items.map(i => this.translator.translate(i, ctx)),
                 totalItems,
             }));
     }
@@ -94,7 +95,7 @@ export class ShippingMethodService {
                 ...(includeDeleted === false ? { where: { deletedAt: null } } : {}),
             },
         );
-        return shippingMethod && translateDeep(shippingMethod, ctx.languageCode);
+        return shippingMethod && this.translator.translate(shippingMethod, ctx);
     }
 
     async create(ctx: RequestContext, input: CreateShippingMethodInput): Promise<ShippingMethod> {
@@ -205,7 +206,7 @@ export class ShippingMethodService {
         });
         return shippingMethods
             .filter(sm => sm.channels.find(c => idsAreEqual(c.id, ctx.channelId)))
-            .map(m => translateDeep(m, ctx.languageCode));
+            .map(m => this.translator.translate(m, ctx));
     }
 
     /**

+ 5 - 4
packages/core/src/service/services/zone.service.ts

@@ -21,8 +21,8 @@ import { Zone } from '../../entity/zone/zone.entity';
 import { EventBus } from '../../event-bus';
 import { ZoneEvent } from '../../event-bus/events/zone-event';
 import { ZoneMembersEvent } from '../../event-bus/events/zone-members-event';
+import { TranslatorService } from '../helpers/translator/translator.service';
 import { patchEntity } from '../helpers/utils/patch-entity';
-import { translateDeep } from '../helpers/utils/translate-entity';
 
 /**
  * @description
@@ -40,6 +40,7 @@ export class ZoneService {
         private connection: TransactionalConnection,
         private configService: ConfigService,
         private eventBus: EventBus,
+        private translator: TranslatorService,
     ) {}
 
     /** @internal */
@@ -67,10 +68,10 @@ export class ZoneService {
     }
 
     async findAll(ctx: RequestContext): Promise<Zone[]> {
-        return this.zones.memoize([ctx.languageCode], [ctx], (zones, languageCode) => {
+        return this.zones.memoize([], [ctx], zones => {
             return zones.map((zone, i) => {
                 const cloneZone = { ...zone };
-                cloneZone.members = zone.members.map(country => translateDeep(country, languageCode));
+                cloneZone.members = zone.members.map(country => this.translator.translate(country, ctx));
                 return cloneZone;
             });
         });
@@ -84,7 +85,7 @@ export class ZoneService {
             })
             .then(zone => {
                 if (zone) {
-                    zone.members = zone.members.map(country => translateDeep(country, ctx.languageCode));
+                    zone.members = zone.members.map(country => this.translator.translate(country, ctx));
                     return zone;
                 }
             });

+ 2 - 2
packages/dev-server/dev-config.ts

@@ -66,8 +66,8 @@ export const devConfig: VendureConfig = {
             assetUploadDir: path.join(__dirname, 'assets'),
         }),
         DefaultSearchPlugin.init({ bufferUpdates: true, indexStockStatus: false }),
-        BullMQJobQueuePlugin.init({}),
-        // DefaultJobQueuePlugin.init({}),
+        // BullMQJobQueuePlugin.init({}),
+        DefaultJobQueuePlugin.init({}),
         // JobQueueTestPlugin.init({ queueCount: 10 }),
         // ElasticsearchPlugin.init({
         //     host: 'http://localhost',