diff --git a/package-lock.json b/package-lock.json index 0bfde794..20ce1ae9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1145,6 +1145,11 @@ "strip-eof": "^1.0.0" } }, + "fast-clone": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/fast-clone/-/fast-clone-1.5.13.tgz", + "integrity": "sha512-0ez7coyFBQFjZtId+RJqJ+EQs61w9xARfqjqK0AD9vIUkSxWD4HvPt80+5evebZ1tTnv1GYKrPTipx7kOW5ipA==" + }, "fast-deep-equal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", @@ -2264,9 +2269,9 @@ } }, "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", "dev": true }, "minimist-options": { @@ -2286,6 +2291,14 @@ "dev": true, "requires": { "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } } }, "mocha": { @@ -3378,6 +3391,14 @@ "requires": { "os-tmpdir": "^1.0.0", "uuid": "^2.0.1" + }, + "dependencies": { + "uuid": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", + "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=", + "dev": true + } } }, "test-exclude": { @@ -3683,12 +3704,6 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true }, - "uuid": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", - "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=", - "dev": true - }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", diff --git a/package.json b/package.json index 65d6cbd3..e787fbc4 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "dependencies": { "@types/geojson": "1.0.6", "@types/json-patch": "0.0.30", + "fast-clone": "1.5.13", "json-patch": "0.7.0", "jsonschema": "1.2.4", "ts-optchain": "0.1.3" diff --git a/src/core/Translator.ts b/src/core/Translator.ts index fb71df99..5583868d 100644 --- a/src/core/Translator.ts +++ b/src/core/Translator.ts @@ -12,30 +12,33 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -import {SCClasses, SCThingsField} from './Classes'; +import {SCClasses} from './Classes'; import {SCThing, SCThingType} from './Thing'; +import {isThing} from './types/Guards'; import {SCTranslations} from './types/i18n'; +import clone = require('fast-clone'); import {Defined, OCType} from 'ts-optchain'; /** * SCThingTranslator class */ export class SCThingTranslator { - /** - * Property representing the translators base language. - * This means every translation is given for this language. - */ - private baseLanguage: keyof SCTranslations; /** * Property representing the translators target language */ - private language: keyof SCTranslations; + private _language: keyof SCTranslations; /** - * Property provinding a mapping from a SCThingType to its known own meta class. + * Property representing the translators base language + * This means every translation is given for this language + */ + private cache: LRUCache; + + /** + * Property providing a mapping from a SCThingType to its known own meta class */ private metaClasses: typeof SCClasses; @@ -45,42 +48,44 @@ export class SCThingTranslator { * // returns translator instance for german * new SCThingTranslator('de'); */ - constructor(language: keyof SCTranslations, baseLanguage?: keyof SCTranslations) { - this.baseLanguage = baseLanguage ? baseLanguage : 'en'; - this.language = language; + constructor(language: keyof SCTranslations, cacheCapacity: number = 200) { + this.cache = new LRUCache(cacheCapacity); + this._language = language; this.metaClasses = SCClasses; } + /** + * Getter for language property + */ + get language(): keyof SCTranslations { + return this._language; + } + + /** + * Setter for language property. Also flushes translation cache + * + * @param language The language the translator instance will use from now on + */ + set language(language: keyof SCTranslations) { + if (language !== this._language) { + this.cache.flush(); + } + this._language = language; + } + /** * Get field value translation recursively * - * @param firstObject Top level object that gets passed through the recursion * @param data The intermediate object / primitive returned by the Proxys get() method - * @param keyPath The keypath that (in the end) leads to the translatable property (when added to firstObject) * @returns an OCType object allowing for access to translations or a translated value(s) */ - private deeptranslate(firstObject: K, data?: T, keyPath?: string): OCType { + private deeptranslate(data?: T): OCType { const proxy = new Proxy( ((defaultValue?: Defined) => (data == null ? defaultValue : data)) as OCType, { get: (target, key) => { const obj: any = target(); - const extendedKeyPath = [keyPath, key.toString()].filter((e) => e != null).join('.'); - let possiblePrimitive = obj[key]; - // check if obj[key] is an array that contains primitive type (arrays in SCThings are not mixing types) - if (obj[key] instanceof Array && obj[key].length) { - possiblePrimitive = obj[key][0]; - } - if (typeof possiblePrimitive === 'string' || - typeof possiblePrimitive === 'number' || - typeof possiblePrimitive === 'boolean') { - // returns final translation for primitive data types - return this.deeptranslate(firstObject, - this.getFieldValueTranslation(firstObject, extendedKeyPath), - extendedKeyPath); - } - // recursion to get more calls to the Proxy handler 'get()' (key path not complete) - return this.deeptranslate(firstObject, obj[key], extendedKeyPath); + return this.deeptranslate(obj[key]); }, }, ); @@ -98,19 +103,19 @@ export class SCThingTranslator { language: keyof SCTranslations): object | undefined { const fieldTranslations = {}; const metaClass = this.getMetaClassInstance(thingType); - if (metaClass === undefined) { + if (typeof metaClass === 'undefined') { return undefined; } // Assigns every property in fieldTranslations to the known base language translation - if (metaClass.fieldTranslations[this.baseLanguage] !== undefined) { - Object.keys(metaClass.fieldTranslations[this.baseLanguage]).forEach((key) => { - (fieldTranslations as any)[key] = metaClass.fieldTranslations[this.baseLanguage][key]; + if (typeof metaClass.fieldTranslations.en !== 'undefined') { + Object.keys(metaClass.fieldTranslations.en).forEach((key) => { + (fieldTranslations as any)[key] = metaClass.fieldTranslations.en[key]; }); } // Assigns every property in fieldTranslations to the known translation in given language - if (metaClass.fieldTranslations[language] !== undefined) { + if (typeof metaClass.fieldTranslations[language] !== 'undefined') { Object.keys(metaClass.fieldTranslations[language]).forEach((key) => { (fieldTranslations as any)[key] = metaClass.fieldTranslations[language][key]; }); @@ -132,120 +137,34 @@ export class SCThingTranslator { } /** - * Returns property value at a certain (key) path of an object. - * @example - * // returns value of dish.offers[0].inPlace.categories[1] - * const dish: SCDish = {...}; - * this.valueFromPath(dish, 'offers[0].inPlace.categories[1]'); - * @param path Key path to evaluate - * @param obj Object to evaluate the key path upon - * @param separator Key path seperation element. Defaults to '.' - * @returns Property value at at key path + * Applies known field value translations of the given SCThings meta class to an instance + * Translated values overwrite current values inplace (destructive) + * + * @param language The language the thing is translated to + * @param thing The thing that will be translated + * @returns The thing with translated meta field values */ - private valueFromPath(path: string, obj: T, separator = '.') { - path = path.replace(/\[/g, '.'); - path = path.replace(/\]/g, '.'); - path = path.replace(/\.\./g, '.'); - path = path.replace(/\.$/, ''); - const properties = path.split(separator); - return properties.reduce((prev: any, curr: any) => prev && prev[curr], obj); - } - - /** - * Get field value translation - * @example - * // returns translation of the property (if available) in the language defined when creating the translator object - * const dish: SCDish = {...}; - * translator.translate(dish, 'offers[0].inPlace.categories[1]'); - * @param thing SCThing to get value translation for - * @param field Field to get value translation for (keypath allowed) - * @returns Translated value(s) or value(s) itself - */ - public getFieldValueTranslation(thing: T, - field: SCThingsField): string | string[] { - let translationPath = 'translations.' + this.language + '.' + field; - const regexTrimProperties = /.*(?:(\..*)(\[\d+\])|(\.[^\d]*$)|(\..*)(\.[\d]*$))/; - - const pathMatch = field.match(regexTrimProperties); - - // when translation is given in thing - let translation = this.valueFromPath(translationPath, thing); - if (translation) { - return translation; - } else if (pathMatch && pathMatch[1] && pathMatch[2] || pathMatch && pathMatch[4] && pathMatch[5]) { - // accessing iteratable of nested thing - const keyPath = (pathMatch[1] ? pathMatch[1] : pathMatch[4]) + (pathMatch[2] ? pathMatch[2] : pathMatch[5]); - const redactedField = field.replace(keyPath, ''); - - // when translation is given in nested thing - translationPath = `${redactedField}.translations.${this.language}${keyPath}`; - translation = this.valueFromPath(translationPath, thing); - if (translation) { - return translation; - } - - // when translation is given in nested meta thing via iterateable index - const nestedType = this.valueFromPath(field.replace(keyPath, '.type'), thing) as SCThingType; - translationPath = `fieldValueTranslations.${this.language}${keyPath}`; - translation = this.valueFromPath(translationPath.replace( - /\[(?=[^\[]*$).*|(?=[\d+]*$).*/, '[' + this.valueFromPath(field, thing) + ']'), - this.getMetaClassInstance(nestedType)); - if (translation) { - return translation; - } - - } else if (pathMatch && pathMatch[3]) { - // accessing meta or instance of nested thing primitive value depth > 0 - const keyPath = pathMatch[3]; - const redactedField = field.replace(pathMatch[3], ''); - - // when translation is given in nested thing - translationPath = `${redactedField}.translations.${this.language}${keyPath}`; - if (this.valueFromPath(translationPath, thing)) { - return this.valueFromPath(translationPath, thing); - } - - // when translation is given in nested meta thing - const nestedType = this.valueFromPath(field.replace(keyPath, '.type'), thing) as SCThingType; - translationPath = `fieldValueTranslations.${this.language}${keyPath}`; - translation = this.valueFromPath(translationPath, this.getMetaClassInstance(nestedType)); - if (translation instanceof Object) { // lookup translated keys in meta thing property - const translations: string[] = []; - this.valueFromPath(field, thing).forEach((key: string) => { - translationPath = `fieldValueTranslations.${this.language}${keyPath}.${key}`; - translations.push(this.valueFromPath(translationPath, this.getMetaClassInstance(nestedType))); - }); - return translations; - } - if (!translation) { // translation not given, return as is - return this.valueFromPath(field, thing) as string; - } - return translation; + private replaceAvailableMetaFieldValueTranslations(instance: any, + language: keyof SCTranslations): any { + const metaClass = this.getMetaClassInstance(instance.type); + if (typeof metaClass === 'undefined') { + return instance; } - // accessing meta thing primitive value depth = 0 - translationPath = `fieldValueTranslations.${this.language}.${field}`; - translation = this.valueFromPath(translationPath, this.getMetaClassInstance(thing.type)); - if (translation) { - if (translation instanceof Object) { // lookup translated keys in meta thing property - const translations: string[] = []; - this.valueFromPath(field, thing).forEach((key: string) => { - translationPath = `fieldValueTranslations.${this.language}.${field}.${key}`; - translations.push(this.valueFromPath(translationPath, this.getMetaClassInstance(thing.type))); - }); - return translations; - } - return translation; + if (typeof metaClass.fieldValueTranslations[language] !== 'undefined') { + Object.keys(metaClass.fieldValueTranslations[language]).forEach((key) => { + if (metaClass.fieldValueTranslations[language][key] instanceof Object) { + // Assigns known translations of subproperties to property in given language (e.g. categories) + Object.keys((instance as any)[key]).forEach((subKey) => { + (instance as any)[key][subKey] = + metaClass.fieldValueTranslations[language][key][(instance as any)[key][subKey]]; + }); + } else { + // Assigns property to known translation of fieldValueTranslations in given language + (instance as any)[key] = metaClass.fieldValueTranslations[language][key]; + } + }); } - - // accessing meta thing primitive via iteratable index value depth = 0 - translation = this.valueFromPath(translationPath.replace( - /\[(?=[^\[]*$).*|(?=[\d+]*$).*/, '[' + this.valueFromPath(field, thing) + ']'), - this.getMetaClassInstance(thing.type)); - if (translation) { - return translation; - } - // last resort: return as is - return this.valueFromPath(field, thing) as string; + return instance; } /** @@ -261,33 +180,27 @@ export class SCThingTranslator { * @param data Top level object that gets passed through the recursion * @returns an OCType object allowing for access to translations or a translated value(s) */ - public translate(data?: T): OCType { + public translate(data: T): OCType { return new Proxy( ((defaultValue?: Defined) => (data == null ? defaultValue : data)) as OCType, { get: (target, key) => { const obj: any = target(); - let translatable = obj[key]; - if (obj[key] instanceof Array && obj[key].length) { - translatable = obj[key][0]; - if (typeof obj[key][0] === 'object' && !obj[key][0].origin) { - translatable = obj[key][0][Object.keys(obj[key][0])[0]]; - } + const objTranslatedFromCache = this.cache.get(data); + if (typeof objTranslatedFromCache !== 'undefined') { + return this.deeptranslate((objTranslatedFromCache as any)[key]); } - if (typeof translatable === 'string') { - // retrieve final translation - return this.deeptranslate(data!, this.getFieldValueTranslation(data!, key.toString()), key.toString()); - } - // recursion to get more calls to the Proxy handler 'get()' (key path not complete) - return this.deeptranslate(data!, obj[key], key.toString()); + const objTranslated = this.translateWholeThingDestructively(clone(obj)); + this.cache.putObject(objTranslated); + return this.deeptranslate(objTranslated[key]); }, }, ); } /** - * Given a SCThingType this function returns an object with the same basic structure as the corresponding SCThing. - * All the values will be set to the known translations of the property/key name. + * Given a SCThingType this function returns an object with the same basic structure as the corresponding SCThing + * All the values will be set to the known translations of the property/key name * @example * const translatedMetaDish = translator.translatedPropertyNames(SCThingType.CourseOfStudies); * @param language The language the object is translated to @@ -300,4 +213,129 @@ export class SCThingTranslator { // return {...{}, ...this.getAllMetaFieldTranslations(thing.type, targetLanguage) as T}; return this.getAllMetaFieldTranslations(thing.type, targetLanguage) as T; } + + /** + * Recursively translates the given object in-place + * Translated values overwrite current values (destructive) + * + * @param language The language the thing is translated to + * @param thing The thing that will be translated + * @returns The thing translated + */ + public translateWholeThingDestructively(instance: any, + language?: keyof SCTranslations): any { + const targetLanguage = (language) ? language : this.language; + // Recursively call this function on all nested SCThings, arrays and objects + Object.keys(instance).forEach((key) => { + if ( + isThing((instance as any)[key]) || + instance[key] instanceof Array || + instance[key] instanceof Object) { + instance[key] = this.translateWholeThingDestructively(instance[key], targetLanguage); + } + }); + + // Spread variable translations given by the connector into thing + if (typeof instance.translations !== 'undefined') { + if (typeof instance.translations![targetLanguage] !== 'undefined') { + instance = {...instance, ...instance.translations![targetLanguage]} as typeof instance; + } + } + // Spread known translations from meta classes into (partly) translated thing + this.replaceAvailableMetaFieldValueTranslations(instance, targetLanguage); + return instance; + } + +} + +/** + * LRUCache class + * Small last recently used cache intended to get used by SCThingTranslator + */ +class LRUCache { + /** + * Map property that manages cached content + */ + private entries: Map = new Map(); + + /** + * Property representing cache maximum capacity + */ + private maxEntries: number; + + /** + * @constructor + * @example + * // returns LRUCache instance with a maximum capacity of 500 + * new LRUCache(500); + */ + constructor(maxEntries: number) { + this.maxEntries = maxEntries; + } + + /** + * Flushes cache / removes all entries + */ + public flush() { + this.entries.clear(); + } + + /** + * Get content from cache by key + */ + public get(somethingOrKey: string): T | undefined; + + /** + * Get content from cache by another objects uid + */ + public get(something: U): T | undefined; + + /** + * Get content from cache by key or by another objects uid + * + * @param somethingOrKey The key which maps to the cached content or an object for which content has been cached + * @returns If available the content connected to the key or somethingOrKey.uid property + */ + public get(somethingOrKey: string | U): T | undefined { + let key: string; + if (typeof somethingOrKey === 'string') { + key = somethingOrKey; + } else if (isThing(somethingOrKey)) { + key = somethingOrKey.uid; + } else { + throw new Error(`Passed argument ${somethingOrKey} cannot be key in LRUCache`); + } + + const entry = this.entries.get(key); + if (entry) { + // LRU behavior + this.entries.delete(key); + this.entries.set(key, entry); + } + return entry; + } + + /** + * Place content in cache by key + * + * @param key The key for which content should be cached + * @param content The content that should be cached + */ + public put(key: string, content: T) { + if (this.entries.size >= this.maxEntries) { + // LRU behavior + const keyToDelete = this.entries.keys().next().value; + this.entries.delete(keyToDelete); + } + this.entries.set(key, content); + } + + /** + * Place content in cache by another objects uid + * + * @param something The object that should be cached under something.uid + */ + public putObject(something: U) { + this.put(something.uid, (something as any) as T); + } } diff --git a/test/Translator.spec.ts b/test/Translator.spec.ts index 0f154483..00b7b8cf 100644 --- a/test/Translator.spec.ts +++ b/test/Translator.spec.ts @@ -13,11 +13,12 @@ * this program. If not, see . */ import {expect} from 'chai'; +import clone = require('fast-clone'); import {slow, suite, test, timeout} from 'mocha-typescript'; + import {SCThingOriginType, SCThingType} from '../src/core/Thing'; import {SCBuildingWithoutReferences} from '../src/core/things/Building'; import {SCDish, SCDishMeta} from '../src/core/things/Dish'; -import {SCPerson} from '../src/core/things/Person'; import {SCThingTranslator} from '../src/core/Translator'; const building: SCBuildingWithoutReferences = { @@ -96,29 +97,15 @@ const dish: SCDish = { uid: '540862f3-ea30-5b8f-8678-56b4dc217140', }; -const person: SCPerson = { - familyName: 'base-familyName-name', - givenName: 'base-givenName-name', - homeLocations: [building, building, building], - name : 'base-person-name', - origin: { - indexed: '1970', - name: 'ding', - type: SCThingOriginType.Remote, - }, - type: SCThingType.Person, - uid: '1234', -}; - -const translator = new SCThingTranslator('de', 'en'); +const translator = new SCThingTranslator('de'); // tslint:disable-next-line:no-eval const languageNonExistant = eval("'jp'"); // this will simulate a translator always utilizing the base language translations const translatorWithFallback = new SCThingTranslator(languageNonExistant); -// tslint:disable:member-ordering TranslationSpec +// tslint:disable:member-ordering TranslationSpecInplace @suite(timeout(10000), slow(5000)) -export class TranslationSpec { +export class TranslationSpecInplace { @test public directStringLiteralType() { @@ -233,135 +220,6 @@ export class TranslationSpec { } } -// tslint:disable:member-ordering TranslationSpecByString -@suite(timeout(10000), slow(5000)) -export class TranslationSpecByString { - - @test - public directStringLiteralType() { - expect(translator.getFieldValueTranslation(dish, 'type')).to.equal('Essen'); - } - - @test - public directStringProperty() { - expect(translator.getFieldValueTranslation(dish, 'name')).to.equal('de-dish-name'); - } - - @test - public directArrayOfString() { - expect(translator.getFieldValueTranslation(dish, 'characteristics')).to.deep - .equal([{name: 'de-characteristic0'}, {name: 'de-characteristic1'}]); - } - - @test - public directArrayOfStringSubscript() { - expect(translator.getFieldValueTranslation(dish, 'characteristics[1]')) - .to.deep.equal({name: 'de-characteristic1'}); - } - - @test - public directMetaArrayOfString() { - expect(translator.getFieldValueTranslation(dish, 'categories')).to.deep.equal(['Hauptgericht', 'Nachtisch']); - } - - @test - public directMetaArrayOfStringSubscript() { - expect(translator.getFieldValueTranslation(dish, 'categories[1]')).to.equal('Nachtisch'); - } - - @test - public nestedStringLiteralType() { - expect(translator.getFieldValueTranslation(dish, 'offers[0].inPlace.type')).to.equal('Gebäude'); - } - - @test - public nestedStringProperty() { - expect(translator.getFieldValueTranslation(dish, 'offers[0].inPlace.name')).to.equal('de-space-name'); - } - - @test - public nestedMetaArrayOfString() { - expect(translator.getFieldValueTranslation(dish, 'offers[0].inPlace.categories')) - .to.deep.equal(['Büro', 'Bildung']); - } - - @test - public nestedMetaArrayOfStringSubscript() { - expect(translator.getFieldValueTranslation(dish, 'offers[0].inPlace.categories[1]')).to.equal('Bildung'); - } - - @test - public nestedArrayOfStringSubscript() { - expect(translator.getFieldValueTranslation(dish, 'offers[0].inPlace.floors[1]')).to.equal('de-floor1'); - } - - @test - public directStringLiteralTypeFallback() { - expect(translatorWithFallback.getFieldValueTranslation(dish, 'type')).to.equal('dish'); - } - - @test - public directStringPropertyFallback() { - expect(translatorWithFallback.getFieldValueTranslation(dish, 'name')).to.equal('base-dish-name'); - } - - @test - public directArrayOfStringSubscriptFallback() { - expect(translatorWithFallback.getFieldValueTranslation(dish, 'characteristics[1]')) - .to.deep.equal({name: 'base-characteristic1'}); - } - - @test - public directMetaArrayOfStringFallback() { - expect(translatorWithFallback.getFieldValueTranslation(dish, 'categories')) - .to.deep.equal(['main dish', 'dessert']); - } - - @test - public directMetaArrayOfStringSubscriptFallback() { - expect(translatorWithFallback.getFieldValueTranslation(dish, 'categories[1]')).to.equal('dessert'); - } - - @test - public nestedStringLiteralTypeFallback() { - expect(translatorWithFallback.getFieldValueTranslation(dish, 'offers[0].inPlace.type')).to.equal('building'); - } - - @test - public nestedStringPropertyFallback() { - expect(translatorWithFallback.getFieldValueTranslation(dish, 'offers[0].inPlace.name')).to.equal('base-space-name'); - } - - @test - public nestedMetaArrayOfStringFallback() { - expect(translatorWithFallback.getFieldValueTranslation(dish, 'offers[0].inPlace.categories')) - .to.deep.equal(['office', 'education']); - } - - @test - public nestedMetaArrayOfStringSubscriptFallback() { - expect(translatorWithFallback.getFieldValueTranslation(dish, 'offers[0].inPlace.categories[1]')) - .to.equal('education'); - } - - @test - public nestedArrayOfStringSubscriptFallback() { - expect(translatorWithFallback.getFieldValueTranslation(dish, 'offers[0].inPlace.floors[1]')) - .to.equal('base-floor1'); - } - - @test - public nestedArrayOfStringSubscriptUncommonFallback() { - expect(translatorWithFallback.getFieldValueTranslation(dish, 'offers[0].inPlace.floors.1')).to.equal('base-floor1'); - } - - @test - public nestedNestedMetaArrayOfStringSubscriptUncommonFallback() { - expect(translatorWithFallback.getFieldValueTranslation(person, 'homeLocations.1.categories.1')) - .to.equal('education'); - } -} - // tslint:disable:member-ordering no-eval no-unused-expression TranslationSpec @suite(timeout(10000), slow(5000)) export class MetaTranslationSpec { @@ -376,7 +234,7 @@ export class MetaTranslationSpec { @test public thingWithoutMetaClass() { - const dishCopy = Object.assign({}, dish); + const dishCopy = clone(dish); const typeNonExistant = eval("(x) => x + 'typeNonExistant';"); // this will assign a non existant SCThingType to dishCopy dishCopy.type = typeNonExistant();