Parcourir la source

translateDeep can handle more cases

Michael Bromley il y a 7 ans
Parent
commit
545e290ffd

+ 1 - 1
modules/core/locale/locale-types.ts

@@ -16,7 +16,7 @@ export type NonTranslateableKeys<T> = { [K in keyof T]: T[K] extends LocaleStrin
  */
  */
 export type Translatable<T> =
 export type Translatable<T> =
     // Translatable must include all non-translatable keys of the interface
     // Translatable must include all non-translatable keys of the interface
-    { [K in NonTranslateableKeys<T>]: T[K] extends Array<any> ? Array<Translatable<T[K][number]>> : T[K] } &
+    { [K in NonTranslateableKeys<T>]: T[K] extends Array<any> ? Array<Translatable<T[K][number]>> : T[K] | Translatable<T[K]> } &
     // Translatable must not include any translatable keys (these are instead handled by the Translation)
     // Translatable must not include any translatable keys (these are instead handled by the Translation)
     { [K in TranslatableKeys<T>]?: never } &
     { [K in TranslatableKeys<T>]?: never } &
     // Translatable must include a reference to all translations of the translatable keys
     // Translatable must include a reference to all translations of the translatable keys

+ 58 - 4
modules/core/locale/translate-entity.spec.ts

@@ -1,11 +1,12 @@
-import { Entity } from 'typeorm';
 import { ProductOptionTranslationEntity } from '../entity/product-option/product-option-translation.entity';
 import { ProductOptionTranslationEntity } from '../entity/product-option/product-option-translation.entity';
 import { ProductOptionEntity } from '../entity/product-option/product-option.entity';
 import { ProductOptionEntity } from '../entity/product-option/product-option.entity';
+import { ProductOption } from '../entity/product-option/product-option.interface';
 import { ProductVariantTranslationEntity } from '../entity/product-variant/product-variant-translation.entity';
 import { ProductVariantTranslationEntity } from '../entity/product-variant/product-variant-translation.entity';
 import { ProductVariantEntity } from '../entity/product-variant/product-variant.entity';
 import { ProductVariantEntity } from '../entity/product-variant/product-variant.entity';
+import { ProductVariant } from '../entity/product-variant/product-variant.interface';
 import { ProductTranslationEntity } from '../entity/product/product-translation.entity';
 import { ProductTranslationEntity } from '../entity/product/product-translation.entity';
 import { ProductEntity } from '../entity/product/product.entity';
 import { ProductEntity } from '../entity/product/product.entity';
-import { LocaleString, Translatable, Translation } from './locale-types';
+import { Translatable, Translation } from './locale-types';
 import { translateDeep, translateEntity } from './translate-entity';
 import { translateDeep, translateEntity } from './translate-entity';
 
 
 const LANGUAGE_CODE = 'en';
 const LANGUAGE_CODE = 'en';
@@ -60,6 +61,30 @@ describe('translateEntity()', () => {
 });
 });
 
 
 describe('translateDeep()', () => {
 describe('translateDeep()', () => {
+    interface TestProduct {
+        singleTestVariant: TestVariant;
+        singleRealVariant: ProductVariant;
+    }
+
+    class TestProductEntity implements Translatable<TestProduct> {
+        id: number;
+        singleTestVariant: TestVariantEntity;
+        singleRealVariant: ProductVariantEntity;
+        translations: Translation<TestProduct>[];
+    }
+
+    interface TestVariant {
+        singleOption: ProductOption;
+    }
+
+    class TestVariantEntity implements Translatable<TestVariant> {
+        id: number;
+        singleOption: ProductOptionEntity;
+        translations: Translation<TestVariant>[];
+    }
+
+    let testProduct: TestProductEntity;
+    let testVariant: TestVariantEntity;
     let product: ProductEntity;
     let product: ProductEntity;
     let productTranslation: ProductTranslationEntity;
     let productTranslation: ProductTranslationEntity;
     let productVariant: ProductVariantEntity;
     let productVariant: ProductVariantEntity;
@@ -96,6 +121,13 @@ describe('translateDeep()', () => {
         product.id = 1;
         product.id = 1;
         product.translations = [productTranslation];
         product.translations = [productTranslation];
         product.variants = [productVariant];
         product.variants = [productVariant];
+
+        testVariant = new TestVariantEntity();
+        testVariant.singleOption = productOption;
+
+        testProduct = new TestProductEntity();
+        testProduct.singleTestVariant = testVariant;
+        testProduct.singleRealVariant = productVariant;
     });
     });
 
 
     it('should translate the root entity', () => {
     it('should translate the root entity', () => {
@@ -104,14 +136,36 @@ describe('translateDeep()', () => {
         expect(result).toHaveProperty('name', PRODUCT_NAME);
         expect(result).toHaveProperty('name', PRODUCT_NAME);
     });
     });
 
 
-    it('should translate a first-level nested entity', () => {
+    it('should not throw if root entity has no translations', () => {
+        expect(() => translateDeep(testProduct)).not.toThrow();
+    });
+
+    it('should translate a first-level nested non-array entity', () => {
+        const result = translateDeep(testProduct, ['singleRealVariant']);
+
+        expect(result.singleRealVariant).toHaveProperty('name', VARIANT_NAME);
+    });
+
+    it('should translate a first-level nested entity array', () => {
         const result = translateDeep(product, ['variants']);
         const result = translateDeep(product, ['variants']);
 
 
         expect(result).toHaveProperty('name', PRODUCT_NAME);
         expect(result).toHaveProperty('name', PRODUCT_NAME);
         expect(result.variants[0]).toHaveProperty('name', VARIANT_NAME);
         expect(result.variants[0]).toHaveProperty('name', VARIANT_NAME);
     });
     });
 
 
-    it('should translate a second-level nested entity', () => {
+    it('should translate a second-level nested non-array entity', () => {
+        const result = translateDeep(testProduct, [['singleTestVariant', 'singleOption']]);
+
+        expect(result.singleTestVariant.singleOption).toHaveProperty('name', OPTION_NAME);
+    });
+
+    it('should translate a second-level nested entity array (first-level is not array)', () => {
+        const result = translateDeep(testProduct, [['singleRealVariant', 'options']]);
+
+        expect(result.singleRealVariant.options[0]).toHaveProperty('name', OPTION_NAME);
+    });
+
+    it('should translate a second-level nested entity array', () => {
         const result = translateDeep(product, ['variants', ['variants', 'options']]);
         const result = translateDeep(product, ['variants', ['variants', 'options']]);
 
 
         expect(result).toHaveProperty('name', PRODUCT_NAME);
         expect(result).toHaveProperty('name', PRODUCT_NAME);

+ 63 - 38
modules/core/locale/translate-entity.ts

@@ -1,5 +1,30 @@
 import { Translatable, TranslatedEntity } from './locale-types';
 import { Translatable, TranslatedEntity } from './locale-types';
 
 
+// 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 Array<string> ? never :
+    T[K] extends Array<number> ? never :
+    T[K] extends Array<boolean> ? never :
+    K extends 'translations' ? never : K
+}[keyof T];
+
+export type UnwrappedArray<T extends Array<any>> = T[number];
+
+// prettier-ignore
+export type NestedTranslatableRelations<T> = {
+    [K in TranslatableRelationsKeys<T>]: T[K] extends Array<any> ?
+        [K, TranslatableRelationsKeys<UnwrappedArray<T[K]>>]:
+        [K, TranslatableRelationsKeys<T[K]>]
+};
+
+export type NestedTranslatableRelationKeys<T> = NestedTranslatableRelations<T>[keyof NestedTranslatableRelations<T>];
+
+export type DeepTranslatableRelations<T> = Array<TranslatableRelationsKeys<T> | NestedTranslatableRelationKeys<T>>;
+
 /**
 /**
  * Converts a Translatable entity into the public-facing entity by unwrapping
  * Converts a Translatable entity into the public-facing entity by unwrapping
  * the translated strings from the first of the Translation entities.
  * the translated strings from the first of the Translation entities.
@@ -21,55 +46,55 @@ export function translateEntity<T>(translatable: Translatable<T>): TranslatedEnt
     return translated;
     return translated;
 }
 }
 
 
-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 Array<string>
-                        ? never
-                        : T[K] extends Array<number>
-                            ? never
-                            : T[K] extends Array<boolean> ? never : K extends 'translations' ? never : K
-}[keyof T];
-
-export type UnwrappedArray<T extends Array<any>> = T[number];
-
-export type NestedTranslatableRelations<T> = {
-    [K in TranslatableRelationsKeys<T>]: T[K] extends Array<any>
-        ? [K, TranslatableRelationsKeys<UnwrappedArray<T[K]>>]
-        : TranslatableRelationsKeys<T[K]>
-};
-
-export type NestedTranslatableRelationKeys<T> = NestedTranslatableRelations<T>[keyof NestedTranslatableRelations<T>];
-
-export type DeepTranslatableRelations<T> = Array<TranslatableRelationsKeys<T> | NestedTranslatableRelationKeys<T>>;
-
+/**
+ * Translates an entity and its deeply-nested translatable properties. Supports up to 2 levels of nesting.
+ */
 export function translateDeep<T>(
 export function translateDeep<T>(
     translatable: Translatable<T>,
     translatable: Translatable<T>,
     translatableRelations: DeepTranslatableRelations<T> = [],
     translatableRelations: DeepTranslatableRelations<T> = [],
 ): TranslatedEntity<T> {
 ): TranslatedEntity<T> {
-    const translatedEntity = translateEntity(translatable);
+    let translatedEntity: TranslatedEntity<T>;
+    try {
+        translatedEntity = translateEntity(translatable);
+    } catch (e) {
+        translatedEntity = translatable as any;
+    }
 
 
     for (const path of translatableRelations) {
     for (const path of translatableRelations) {
+        let object: any;
+        let property: string;
+        let value: any;
+
         if (Array.isArray(path) && path.length === 2) {
         if (Array.isArray(path) && path.length === 2) {
-            const [path0, path1] = path;
-            const value = translatable[path0].forEach((nested1, index) => {
-                translatedEntity[path0][index][path1] = nested1[path1].map(nested2 => translateEntity(nested2));
-            });
-        } else {
-            const value = (translatable as any)[path];
-            if (Array.isArray(value)) {
-                (translatedEntity as any)[path] = value.map(val => translateEntity(val));
+            const [path0, path1] = path as any;
+            const valueLevel0 = translatable[path0];
+
+            if (Array.isArray(valueLevel0)) {
+                valueLevel0.forEach((nested1, index) => {
+                    object = translatedEntity[path0][index];
+                    property = path1;
+                    value = translateLeaf(object, property);
+                });
             } else {
             } else {
-                (translatedEntity as any)[path] = translateEntity(value);
+                object = translatedEntity[path0];
+                property = path1;
+                value = translateLeaf(object, property);
             }
             }
+        } else {
+            object = translatedEntity;
+            property = path as any;
+            value = translateLeaf(object, property);
         }
         }
+        object[property] = value;
     }
     }
 
 
     return translatedEntity as any;
     return translatedEntity as any;
 }
 }
+
+function translateLeaf(object: any, property: string): any {
+    if (Array.isArray(object[property])) {
+        return object[property].map(nested2 => translateEntity(nested2));
+    } else {
+        return translateEntity(object[property]);
+    }
+}