refactor: simpify translator class functions

This commit is contained in:
Rainer Killinger
2019-03-27 13:01:04 +01:00
committed by Jovan Krunić
parent 27417e80e1
commit cf83692e71
4 changed files with 230 additions and 318 deletions

33
package-lock.json generated
View File

@@ -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",

View File

@@ -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"

View File

@@ -12,30 +12,33 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
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<SCThing>;
/**
* Property representing the translators target language
*/
private language: keyof SCTranslations<SCThing>;
private _language: keyof SCTranslations<SCThing>;
/**
* 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<SCThing>;
/**
* 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<SCThing>, baseLanguage?: keyof SCTranslations<SCThing>) {
this.baseLanguage = baseLanguage ? baseLanguage : 'en';
this.language = language;
constructor(language: keyof SCTranslations<SCThing>, cacheCapacity: number = 200) {
this.cache = new LRUCache(cacheCapacity);
this._language = language;
this.metaClasses = SCClasses;
}
/**
* Getter for language property
*/
get language(): keyof SCTranslations<SCThing> {
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<SCThing>) {
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<T> object allowing for access to translations or a translated value(s)
*/
private deeptranslate<T, K extends SCThing>(firstObject: K, data?: T, keyPath?: string): OCType<T> {
private deeptranslate<T>(data?: T): OCType<T> {
const proxy = new Proxy(
((defaultValue?: Defined<T>) => (data == null ? defaultValue : data)) as OCType<T>,
{
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<T>): 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<T extends SCThing>(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<T extends SCThing>(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>): 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<T> object allowing for access to translations or a translated value(s)
*/
public translate<T extends SCThing>(data?: T): OCType<T> {
public translate<T extends SCThing>(data: T): OCType<T> {
return new Proxy(
((defaultValue?: Defined<T>) => (data == null ? defaultValue : data)) as OCType<T>,
{
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<SCCourseOfStudies>(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>): 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<T> {
/**
* Map property that manages cached content
*/
private entries: Map<string, T> = new Map<string, T>();
/**
* 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<U extends SCThing>(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<U extends SCThing>(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<U extends SCThing>(something: U) {
this.put(something.uid, (something as any) as T);
}
}

View File

@@ -13,11 +13,12 @@
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
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();