mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-20 16:42:56 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e934017a65 | ||
|
|
88dbedd932 | ||
|
|
8b1b3444fd | ||
|
|
d5f3d71a41 | ||
|
|
cbcf9c9adb |
5
.mailmap
Normal file
5
.mailmap
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
Rainer Killinger <mail-openstapps@killinger.co> Rainer Killinger <git@killinger.co>
|
||||||
|
Rainer Killinger <mail-openstapps@killinger.co> Rainer Killinger <killinge@hrz.uni-frankfurt.de>
|
||||||
|
Rainer Killinger <mail-openstapps@killinger.co> Rainer Killinger <killinger@hrz.uni-frankfurt.de>
|
||||||
|
Wieland Schöbl <wulkanat@gmail.com> wulkanat@gmail.com <wulkanat@gmail.com>
|
||||||
|
Wieland Schöbl <wulkanat@gmail.com> Wieland Schöbl <wieland.schoebl@campus.tu-berlin.de>
|
||||||
@@ -1,3 +1,12 @@
|
|||||||
|
# [0.41.0](https://gitlab.com/openstapps/core/compare/v0.40.0...v0.41.0) (2021-02-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add date, numeric range filter to SCSearchFilter ([8510f11](https://gitlab.com/openstapps/core/commit/8510f11d7b4c62a6b239a70f47fe07e8cc86ab63))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [0.40.0](https://gitlab.com/openstapps/core/compare/v0.39.0...v0.40.0) (2021-02-05)
|
# [0.40.0](https://gitlab.com/openstapps/core/compare/v0.39.0...v0.40.0) (2021-02-05)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
1494
package-lock.json
generated
1494
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
24
package.json
24
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@openstapps/core",
|
"name": "@openstapps/core",
|
||||||
"version": "0.41.0",
|
"version": "0.42.0",
|
||||||
"description": "StAppsCore - Generalized model of data",
|
"description": "StAppsCore - Generalized model of data",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"Model",
|
"Model",
|
||||||
@@ -32,22 +32,24 @@
|
|||||||
"contributors": [
|
"contributors": [
|
||||||
"Andreas Lehmann",
|
"Andreas Lehmann",
|
||||||
"Anselm Stordeur <anselmstordeur@gmail.com>",
|
"Anselm Stordeur <anselmstordeur@gmail.com>",
|
||||||
|
"Axel Nieder-Vahrenholz",
|
||||||
"Benjamin Jöckel",
|
"Benjamin Jöckel",
|
||||||
"Imran Hossain",
|
"Frank Nagel",
|
||||||
"Jovan Krunić <jovan.krunic@gmail.com>",
|
"Jovan Krunić <jovan.krunic@gmail.com>",
|
||||||
"Michel Jonathan Schmitz",
|
"Michel Jonathan Schmitz",
|
||||||
"Rainer Killinger <mail-openstapps@killinger.co>",
|
"Rainer Killinger <mail-openstapps@killinger.co>",
|
||||||
|
"Roman Klopsch",
|
||||||
"Sebastian Lange",
|
"Sebastian Lange",
|
||||||
"Wieland Schöbl",
|
"Wieland Schöbl"
|
||||||
"Roman Klopsch"
|
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@openstapps/core-tools": "0.16.0",
|
"@openstapps/core-tools": "0.17.0",
|
||||||
"@types/geojson": "1.0.6",
|
"@types/geojson": "1.0.6",
|
||||||
"@types/json-patch": "0.0.30",
|
"@types/json-patch": "0.0.30",
|
||||||
"@types/json-schema": "7.0.6",
|
"@types/json-schema": "7.0.7",
|
||||||
"@types/node": "10.17.44",
|
"@types/node": "10.17.44",
|
||||||
"fast-clone": "1.5.13",
|
"fast-clone": "1.5.13",
|
||||||
|
"fast-deep-equal": "3.1.3",
|
||||||
"http-status-codes": "2.1.4",
|
"http-status-codes": "2.1.4",
|
||||||
"json-patch": "0.7.0",
|
"json-patch": "0.7.0",
|
||||||
"json-schema": "0.2.5",
|
"json-schema": "0.2.5",
|
||||||
@@ -57,16 +59,16 @@
|
|||||||
"@openstapps/configuration": "0.25.0",
|
"@openstapps/configuration": "0.25.0",
|
||||||
"@openstapps/logger": "0.5.0",
|
"@openstapps/logger": "0.5.0",
|
||||||
"@testdeck/mocha": "0.1.2",
|
"@testdeck/mocha": "0.1.2",
|
||||||
"@types/chai": "4.2.14",
|
"@types/chai": "4.2.15",
|
||||||
"@types/rimraf": "3.0.0",
|
"@types/rimraf": "3.0.0",
|
||||||
"chai": "4.2.0",
|
"chai": "4.3.0",
|
||||||
"conditional-type-checks": "1.0.5",
|
"conditional-type-checks": "1.0.5",
|
||||||
"conventional-changelog-cli": "2.1.0",
|
"conventional-changelog-cli": "2.1.1",
|
||||||
"mocha": "8.2.0",
|
"mocha": "8.3.0",
|
||||||
"nyc": "15.1.0",
|
"nyc": "15.1.0",
|
||||||
"rimraf": "3.0.2",
|
"rimraf": "3.0.2",
|
||||||
"source-map-support": "0.5.19",
|
"source-map-support": "0.5.19",
|
||||||
"ts-node": "9.0.0",
|
"ts-node": "9.1.1",
|
||||||
"tslint": "6.1.3",
|
"tslint": "6.1.3",
|
||||||
"typedoc": "0.18.0",
|
"typedoc": "0.18.0",
|
||||||
"typescript": "3.8.3"
|
"typescript": "3.8.3"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License along with
|
* You should have received a copy of the GNU General Public License along with
|
||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import {SCLanguage, SCMetaTranslations, SCTranslations} from '../../general/i18n';
|
import {SCLanguageCode, SCMetaTranslations, SCTranslations} from '../../general/i18n';
|
||||||
import {SCISO8601Date} from '../../general/time';
|
import {SCISO8601Date} from '../../general/time';
|
||||||
import {SCOrganizationWithoutReferences} from '../organization';
|
import {SCOrganizationWithoutReferences} from '../organization';
|
||||||
import {SCPersonWithoutReferences} from '../person';
|
import {SCPersonWithoutReferences} from '../person';
|
||||||
@@ -30,6 +30,12 @@ import {
|
|||||||
*/
|
*/
|
||||||
export interface SCCreativeWorkWithoutReferences
|
export interface SCCreativeWorkWithoutReferences
|
||||||
extends SCThingWithoutReferences, SCThingThatCanBeOfferedWithoutReferences {
|
extends SCThingWithoutReferences, SCThingThatCanBeOfferedWithoutReferences {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Languages this creative work is available in
|
||||||
|
*/
|
||||||
|
availableLanguages?: SCLanguageCode[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Date the creative work was published
|
* Date the creative work was published
|
||||||
*
|
*
|
||||||
@@ -38,9 +44,11 @@ export interface SCCreativeWorkWithoutReferences
|
|||||||
datePublished?: SCISO8601Date;
|
datePublished?: SCISO8601Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of languages this creative work is written/recorded/... in
|
* Languages this creative work is written/recorded/... in
|
||||||
|
*
|
||||||
|
* @filterable
|
||||||
*/
|
*/
|
||||||
inLanguages?: SCLanguage[];
|
inLanguage?: SCLanguageCode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keywords of the creative work
|
* Keywords of the creative work
|
||||||
@@ -103,9 +111,10 @@ export class SCCreativeWorkMeta
|
|||||||
de: {
|
de: {
|
||||||
...SCThingMeta.getInstance<SCThingMeta>().fieldTranslations.de,
|
...SCThingMeta.getInstance<SCThingMeta>().fieldTranslations.de,
|
||||||
...SCThingThatCanBeOfferedMeta.getInstance().fieldTranslations.de,
|
...SCThingThatCanBeOfferedMeta.getInstance().fieldTranslations.de,
|
||||||
authors: 'Authoren',
|
authors: 'Autoren',
|
||||||
|
availableLanguages: 'verfügbare Übersetzungen',
|
||||||
datePublished: 'Veröffentlichungsdatum',
|
datePublished: 'Veröffentlichungsdatum',
|
||||||
inLanguages: 'verfügbare Übersetzungen',
|
inLanguage: 'Inhaltssprache',
|
||||||
keywords: 'Schlagwörter',
|
keywords: 'Schlagwörter',
|
||||||
publishers: 'Verleger',
|
publishers: 'Verleger',
|
||||||
},
|
},
|
||||||
@@ -113,8 +122,9 @@ export class SCCreativeWorkMeta
|
|||||||
...SCThingMeta.getInstance<SCThingMeta>().fieldTranslations.en,
|
...SCThingMeta.getInstance<SCThingMeta>().fieldTranslations.en,
|
||||||
...SCThingThatCanBeOfferedMeta.getInstance().fieldTranslations.en,
|
...SCThingThatCanBeOfferedMeta.getInstance().fieldTranslations.en,
|
||||||
authors: 'authors',
|
authors: 'authors',
|
||||||
|
availableLanguages: 'available languages',
|
||||||
datePublished: 'release date',
|
datePublished: 'release date',
|
||||||
inLanguages: 'available Languages',
|
inLanguage: 'content language',
|
||||||
keywords: 'keywords',
|
keywords: 'keywords',
|
||||||
publishers: 'publishers',
|
publishers: 'publishers',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,8 +13,9 @@
|
|||||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import clone = require('fast-clone');
|
import clone = require('fast-clone');
|
||||||
|
import equal = require('fast-deep-equal/es6');
|
||||||
import {Defined, TSOCType} from 'ts-optchain';
|
import {Defined, TSOCType} from 'ts-optchain';
|
||||||
import {SCTranslations} from './general/i18n';
|
import {SCLanguageCode} from './general/i18n';
|
||||||
import {isThing} from './guards';
|
import {isThing} from './guards';
|
||||||
import {SCClasses} from './meta';
|
import {SCClasses} from './meta';
|
||||||
import {SCThing, SCThingType} from './things/abstract/thing';
|
import {SCThing, SCThingType} from './things/abstract/thing';
|
||||||
@@ -27,13 +28,32 @@ const standardCacheSize = 200;
|
|||||||
*/
|
*/
|
||||||
export class SCThingTranslator {
|
export class SCThingTranslator {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Getter for language property
|
||||||
|
*/
|
||||||
|
get language(): SCLanguageCode {
|
||||||
|
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: SCLanguageCode) {
|
||||||
|
if (language !== this._language) {
|
||||||
|
this.cache.flush();
|
||||||
|
}
|
||||||
|
this._language = language;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Property representing the translators target language
|
* Property representing the translators target language
|
||||||
*/
|
*/
|
||||||
private _language: keyof SCTranslations<SCThing>;
|
private _language: SCLanguageCode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Property representing the translators base language
|
* LRU cache containing already translated SCThings
|
||||||
* This means every translation is given for this language
|
|
||||||
*/
|
*/
|
||||||
private readonly cache: LRUCache<SCThing>;
|
private readonly cache: LRUCache<SCThing>;
|
||||||
|
|
||||||
@@ -42,36 +62,23 @@ export class SCThingTranslator {
|
|||||||
*/
|
*/
|
||||||
private readonly metaClasses: typeof SCClasses;
|
private readonly metaClasses: typeof SCClasses;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LRU cache containing SCThings translations have been provided for
|
||||||
|
*/
|
||||||
|
private readonly sourceCache: LRUCache<SCThing>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @example
|
* @example
|
||||||
* // returns translator instance for german
|
* // returns translator instance for german
|
||||||
* new SCThingTranslator('de');
|
* new SCThingTranslator('de');
|
||||||
*/
|
*/
|
||||||
constructor(language: keyof SCTranslations<SCThing>, cacheCapacity: number = standardCacheSize) {
|
constructor(language: SCLanguageCode, cacheCapacity: number = standardCacheSize) {
|
||||||
this.cache = new LRUCache(cacheCapacity);
|
this.cache = new LRUCache(cacheCapacity);
|
||||||
|
this.sourceCache = new LRUCache(cacheCapacity);
|
||||||
this._language = language;
|
this._language = language;
|
||||||
this.metaClasses = SCClasses;
|
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
|
* Get field value translation recursively
|
||||||
*
|
*
|
||||||
@@ -101,8 +108,8 @@ export class SCThingTranslator {
|
|||||||
* @param language The language the thing property values are translated to
|
* @param language The language the thing property values are translated to
|
||||||
* @returns The thing with all known meta values translated
|
* @returns The thing with all known meta values translated
|
||||||
*/
|
*/
|
||||||
private getAllMetaFieldTranslations<T extends SCThing>(thingType: SCThingType,
|
private getAllMetaFieldTranslations(thingType: SCThingType,
|
||||||
language: keyof SCTranslations<T>): object | undefined {
|
language: SCLanguageCode): object | undefined {
|
||||||
const fieldTranslations = {};
|
const fieldTranslations = {};
|
||||||
const metaClass = this.getMetaClassInstance(thingType);
|
const metaClass = this.getMetaClassInstance(thingType);
|
||||||
if (typeof metaClass === 'undefined') {
|
if (typeof metaClass === 'undefined') {
|
||||||
@@ -151,7 +158,7 @@ export class SCThingTranslator {
|
|||||||
* @returns The thing with translated meta field values
|
* @returns The thing with translated meta field values
|
||||||
*/
|
*/
|
||||||
private replaceAvailableMetaFieldValueTranslations(instance: any,
|
private replaceAvailableMetaFieldValueTranslations(instance: any,
|
||||||
language: keyof SCTranslations<any>): any {
|
language: SCLanguageCode): any {
|
||||||
const metaClass = this.getMetaClassInstance(instance.type);
|
const metaClass = this.getMetaClassInstance(instance.type);
|
||||||
if (typeof metaClass === 'undefined') {
|
if (typeof metaClass === 'undefined') {
|
||||||
return instance;
|
return instance;
|
||||||
@@ -181,6 +188,60 @@ export class SCThingTranslator {
|
|||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively translates the given object in-place
|
||||||
|
* Translated values overwrite current values (destructive)
|
||||||
|
*
|
||||||
|
* @param instance The thing / object that will be translated
|
||||||
|
* @returns The thing translated
|
||||||
|
*/
|
||||||
|
private translateThingInPlaceDestructively<T>(instance: T): T {
|
||||||
|
const targetLanguage = this.language;
|
||||||
|
|
||||||
|
let nextInstance = instance as any;
|
||||||
|
// Recursively call this function on all nested SCThings, arrays and objects
|
||||||
|
Object.keys(nextInstance)
|
||||||
|
.forEach((key) => {
|
||||||
|
if (
|
||||||
|
isThing(nextInstance[key]) ||
|
||||||
|
nextInstance[key] instanceof Array ||
|
||||||
|
nextInstance[key] instanceof Object) {
|
||||||
|
nextInstance[key] = this.translateThingInPlaceDestructively(nextInstance[key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Spread variable translations given by the connector into thing
|
||||||
|
if (typeof nextInstance.translations?.[targetLanguage] !== 'undefined') {
|
||||||
|
nextInstance = {...nextInstance, ...nextInstance.translations![targetLanguage]} as T;
|
||||||
|
}
|
||||||
|
// Spread known translations from meta classes into (partly) translated thing
|
||||||
|
this.replaceAvailableMetaFieldValueTranslations(nextInstance, targetLanguage);
|
||||||
|
|
||||||
|
return nextInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively translates the given object in-place
|
||||||
|
* Translated values overwrite current values (destructive)
|
||||||
|
*
|
||||||
|
* @param instance The thing / object that will be translated
|
||||||
|
* @returns The thing translated
|
||||||
|
*/
|
||||||
|
public translate<T extends SCThing>(thing: T): T {
|
||||||
|
if (equal(this.sourceCache.get(thing), thing)) {
|
||||||
|
const cachedInstance = this.cache.get(thing);
|
||||||
|
if (typeof cachedInstance !== 'undefined') {
|
||||||
|
return cachedInstance as T;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const translatedInstance = this.translateThingInPlaceDestructively(clone(thing));
|
||||||
|
delete translatedInstance.translations;
|
||||||
|
this.cache.putObject(translatedInstance);
|
||||||
|
this.sourceCache.putObject(thing);
|
||||||
|
|
||||||
|
return translatedInstance as T;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get field value translation recursively
|
* Get field value translation recursively
|
||||||
* @example
|
* @example
|
||||||
@@ -191,21 +252,24 @@ export class SCThingTranslator {
|
|||||||
* dishTranslatedAccess.offers[0].inPlace.categories[1]();
|
* dishTranslatedAccess.offers[0].inPlace.categories[1]();
|
||||||
* // undoing the TSOCType<T>
|
* // undoing the TSOCType<T>
|
||||||
* const dishAsBefore: SCDish = dishTranslatedAccess()!;
|
* const dishAsBefore: SCDish = dishTranslatedAccess()!;
|
||||||
* @param data Top level object that gets passed through the recursion
|
* @param thing Top level object that gets passed through the recursion
|
||||||
* @returns an TSOCType<T> object allowing for access to translations or a translated value(s)
|
* @returns an TSOCType<T> object allowing for access to translations or a translated value(s)
|
||||||
*/
|
*/
|
||||||
public translate<T extends SCThing>(data: T): TSOCType<T> {
|
public translatedAccess<T extends SCThing>(thing: T): TSOCType<T> {
|
||||||
return new Proxy(
|
return new Proxy(
|
||||||
((defaultValue?: Defined<T>) => (data == null ? defaultValue : data)) as TSOCType<T>,
|
((defaultValue?: Defined<T>) => (thing == null ? defaultValue : thing)) as TSOCType<T>,
|
||||||
{
|
{
|
||||||
get: (target, key) => {
|
get: (target, key) => {
|
||||||
const obj: any = target();
|
const obj: any = target();
|
||||||
const objTranslatedFromCache = this.cache.get(data);
|
if (equal(this.sourceCache.get(thing), thing)) {
|
||||||
if (typeof objTranslatedFromCache !== 'undefined') {
|
const objTranslatedFromCache = this.cache.get(thing);
|
||||||
return this.deeptranslate((objTranslatedFromCache as any)[key]);
|
if (typeof objTranslatedFromCache !== 'undefined') {
|
||||||
|
return this.deeptranslate((objTranslatedFromCache as any)[key]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const objTranslated = this.translateWholeThingDestructively(clone(obj));
|
const objTranslated = this.translateThingInPlaceDestructively(clone(obj));
|
||||||
this.cache.putObject(objTranslated);
|
this.cache.putObject(objTranslated);
|
||||||
|
this.sourceCache.putObject(thing);
|
||||||
|
|
||||||
return this.deeptranslate(objTranslated[key]);
|
return this.deeptranslate(objTranslated[key]);
|
||||||
},
|
},
|
||||||
@@ -219,69 +283,31 @@ export class SCThingTranslator {
|
|||||||
* @example
|
* @example
|
||||||
* const translatedMetaDish = translator.translatedPropertyNames<SCCourseOfStudies>(SCThingType.CourseOfStudies);
|
* const translatedMetaDish = translator.translatedPropertyNames<SCCourseOfStudies>(SCThingType.CourseOfStudies);
|
||||||
* @param type The type whose property names will be translated
|
* @param type The type whose property names will be translated
|
||||||
* @param language The language all property names will be translated to
|
|
||||||
* @returns An object with the properties of the SCThingType where the values are the known property tranlations
|
* @returns An object with the properties of the SCThingType where the values are the known property tranlations
|
||||||
*/
|
*/
|
||||||
public translatedPropertyNames<T extends SCThing>(type: SCThingType,
|
public translatedPropertyNames<T extends SCThing>(type: SCThingType): T | undefined {
|
||||||
language?: keyof SCTranslations<T>): T | undefined {
|
return this.getAllMetaFieldTranslations(type, this.language) as T;
|
||||||
return this.getAllMetaFieldTranslations(type, language ?? this.language) as T;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a SCThingType and a corresponding property name it returns the known property value translation
|
* Given a SCThingType and a corresponding property name it returns the known property value translation
|
||||||
* Access pattern to the meta object containing the translation can be thought of as type.field[key] with key being optional
|
* Access pattern to the meta object containing the translation can be thought of as type.field[key] with key being optional
|
||||||
* @example
|
* @example
|
||||||
* const translatedMetaDish = translator.translatedPropertyNames(SCThingType.Dish, 'categories', 'main dish');
|
* const singleValueTranslation = translator.translatedPropertyValue(SCThingType.Dish, 'categories', 'main dish');
|
||||||
* @param type The type for whose property values a translation is required
|
* @param type The type for whose property values a translation is required
|
||||||
* @param field The property for which a translation is required
|
* @param field The property for which a translation is required
|
||||||
* @param key If specified tries to access the field with this key
|
* @param key If specified tries to access the field with this key
|
||||||
* @param language The language all property names will be translated to
|
|
||||||
* @returns Known translation for the property
|
* @returns Known translation for the property
|
||||||
*/
|
*/
|
||||||
public translatedPropertyValue<T extends unknown>(type: SCThingType,
|
public translatedPropertyValue(type: SCThingType,
|
||||||
field: string,
|
field: string,
|
||||||
key?: string,
|
key?: string): string | undefined {
|
||||||
language?: keyof SCTranslations<T>): string | undefined {
|
const fieldValueTranslations = this.getMetaClassInstance(type).fieldValueTranslations[this.language] ??
|
||||||
const fieldTranslation = this.getMetaClassInstance(type).fieldValueTranslations[language ?? this.language]?.[field];
|
this.getMetaClassInstance(type).fieldValueTranslations.en;
|
||||||
|
const fieldTranslation = fieldValueTranslations?.[field];
|
||||||
|
|
||||||
return fieldTranslation?.[key ?? ''] ?? key ?? fieldTranslation;
|
return fieldTranslation?.[key ?? ''] ?? key ?? fieldTranslation;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively translates the given object in-place
|
|
||||||
* Translated values overwrite current values (destructive)
|
|
||||||
*
|
|
||||||
* @param instance The thing / object that will be translated
|
|
||||||
* @param language The language the thing / object is translated to
|
|
||||||
* @returns The thing translated
|
|
||||||
*/
|
|
||||||
public translateWholeThingDestructively(instance: any,
|
|
||||||
language?: keyof SCTranslations<any>): any {
|
|
||||||
const targetLanguage = (typeof language !== 'undefined') ? language : this.language;
|
|
||||||
let nextInstance = instance;
|
|
||||||
// Recursively call this function on all nested SCThings, arrays and objects
|
|
||||||
Object.keys(nextInstance)
|
|
||||||
.forEach((key) => {
|
|
||||||
if (
|
|
||||||
isThing((nextInstance as any)[key]) ||
|
|
||||||
nextInstance[key] instanceof Array ||
|
|
||||||
nextInstance[key] instanceof Object) {
|
|
||||||
nextInstance[key] = this.translateWholeThingDestructively(nextInstance[key], targetLanguage);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Spread variable translations given by the connector into thing
|
|
||||||
if (typeof nextInstance.translations !== 'undefined') {
|
|
||||||
if (typeof nextInstance.translations![targetLanguage] !== 'undefined') {
|
|
||||||
nextInstance = {...nextInstance, ...nextInstance.translations![targetLanguage]} as typeof instance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Spread known translations from meta classes into (partly) translated thing
|
|
||||||
this.replaceAvailableMetaFieldValueTranslations(nextInstance, targetLanguage);
|
|
||||||
|
|
||||||
return nextInstance;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -21,12 +21,7 @@
|
|||||||
"name": "Symposion Publishing"
|
"name": "Symposion Publishing"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"inLanguages": [
|
"inLanguage": "de",
|
||||||
{
|
|
||||||
"name": "german",
|
|
||||||
"code": "de"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"bookEdition": "2., überarb. u. erw. Aufl.",
|
"bookEdition": "2., überarb. u. erw. Aufl.",
|
||||||
"isbn": "3936608776",
|
"isbn": "3936608776",
|
||||||
"numberOfPages": 537,
|
"numberOfPages": 537,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
import {expect} from 'chai';
|
import {expect} from 'chai';
|
||||||
import clone = require('fast-clone');
|
import clone = require('fast-clone');
|
||||||
import {slow, suite, test, timeout} from '@testdeck/mocha';
|
import {slow, suite, test, timeout} from '@testdeck/mocha';
|
||||||
import {SCThingOriginType, SCThingType} from '../src/things/abstract/thing';
|
import {SCThingOriginType, SCThingType, SCThingRemoteOrigin} from '../src/things/abstract/thing';
|
||||||
import {SCBuildingWithoutReferences} from '../src/things/building';
|
import {SCBuildingWithoutReferences} from '../src/things/building';
|
||||||
import {SCDish, SCDishMeta} from '../src/things/dish';
|
import {SCDish, SCDishMeta} from '../src/things/dish';
|
||||||
import {SCSetting, SCSettingInputType} from '../src/things/setting';
|
import {SCSetting, SCSettingInputType} from '../src/things/setting';
|
||||||
@@ -105,142 +105,182 @@ const setting: SCSetting = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const translator = new SCThingTranslator('de');
|
const translator = new SCThingTranslator('de');
|
||||||
// tslint:disable-next-line:no-eval
|
const translatorEN = new SCThingTranslator('en');
|
||||||
const languageNonExistant = eval("'jp'");
|
|
||||||
// this will simulate a translator always utilizing the base language translations
|
// this will simulate a translator always utilizing the base language translations
|
||||||
const translatorWithFallback = new SCThingTranslator(languageNonExistant);
|
const translatorWithFallback = new SCThingTranslator('tt');
|
||||||
|
|
||||||
|
const translatedThingDE = translator.translate(dish);
|
||||||
|
const translatedThingFallback = translatorWithFallback.translate(dish);
|
||||||
|
|
||||||
// tslint:disable:max-line-length member-ordering newline-per-chained-call prefer-function-over-method completed-docs TranslationSpecInplace
|
// tslint:disable:max-line-length member-ordering newline-per-chained-call prefer-function-over-method completed-docs TranslationSpecInplace
|
||||||
@suite(timeout(10000), slow(5000))
|
@suite(timeout(10000), slow(5000))
|
||||||
export class TranslationSpecInplace {
|
export class TranslationSpecInplace {
|
||||||
@test
|
@test
|
||||||
public directEnumSingleValue () {
|
public directEnumSingleValue() {
|
||||||
expect(translator.translate(setting).inputType()).to.equal('einfache Auswahl');
|
expect(translator.translatedAccess(setting).inputType()).to.equal('einfache Auswahl');
|
||||||
}
|
}
|
||||||
|
|
||||||
@test
|
@test
|
||||||
public directStringLiteralType() {
|
public directStringLiteralType() {
|
||||||
expect(translator.translate(dish).type()).to.equal('Essen');
|
expect(translator.translatedAccess(dish).type()).to.equal('Essen');
|
||||||
|
expect(translatedThingDE.type).to.equal('Essen');
|
||||||
}
|
}
|
||||||
|
|
||||||
@test
|
@test
|
||||||
public directStringProperty() {
|
public directStringProperty() {
|
||||||
expect(translator.translate(dish).name()).to.equal('de-dish-name');
|
expect(translator.translatedAccess(dish).name()).to.equal('de-dish-name');
|
||||||
|
expect(translatedThingDE.name).to.equal('de-dish-name');
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@test
|
@test
|
||||||
public directArrayOfString() {
|
public directArrayOfString() {
|
||||||
expect(translator.translate(dish).characteristics()).to.deep
|
expect(translator.translatedAccess(dish).characteristics()).to.deep
|
||||||
.equal([{name: 'de-characteristic0'}, {name: 'de-characteristic1'}]);
|
.equal([{name: 'de-characteristic0'}, {name: 'de-characteristic1'}]);
|
||||||
|
expect(translatedThingDE.characteristics).to.deep
|
||||||
|
.equal([{name: 'de-characteristic0'}, {name: 'de-characteristic1'}]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@test
|
@test
|
||||||
public directArrayOfStringSubscript() {
|
public directArrayOfStringSubscript() {
|
||||||
expect(translator.translate(dish).characteristics[1]()).to.deep.equal({name: 'de-characteristic1'});
|
expect(translator.translatedAccess(dish).characteristics[1]()).to.deep.equal({name: 'de-characteristic1'});
|
||||||
|
expect(translatedThingDE.characteristics![1]).to.deep.equal({name: 'de-characteristic1'});
|
||||||
}
|
}
|
||||||
|
|
||||||
@test
|
@test
|
||||||
public directMetaArrayOfString() {
|
public directMetaArrayOfString() {
|
||||||
expect(translator.translate(dish).categories()).to.deep.equal(['Hauptgericht', 'Nachtisch']);
|
expect(translator.translatedAccess(dish).categories()).to.deep.equal(['Hauptgericht', 'Nachtisch']);
|
||||||
|
expect(translatedThingDE.categories).to.deep.equal(['Hauptgericht', 'Nachtisch']);
|
||||||
}
|
}
|
||||||
|
|
||||||
@test
|
@test
|
||||||
public directMetaArrayOfStringSubscript() {
|
public directMetaArrayOfStringSubscript() {
|
||||||
expect(translator.translate(dish).categories[1]()).to.equal('Nachtisch');
|
expect(translator.translatedAccess(dish).categories[1]()).to.equal('Nachtisch');
|
||||||
|
expect(translatedThingDE.categories[1]).to.equal('Nachtisch');
|
||||||
}
|
}
|
||||||
|
|
||||||
@test
|
@test
|
||||||
public nestedStringLiteralType() {
|
public nestedStringLiteralType() {
|
||||||
expect(translator.translate(dish).offers[0].inPlace.type()).to.equal('Gebäude');
|
expect(translator.translatedAccess(dish).offers[0].inPlace.type()).to.equal('Gebäude');
|
||||||
|
expect(translatedThingDE.offers![0].inPlace!.type).to.equal('Gebäude');
|
||||||
}
|
}
|
||||||
|
|
||||||
@test
|
@test
|
||||||
public nestedStringProperty() {
|
public nestedStringProperty() {
|
||||||
expect(translator.translate(dish).offers[0].inPlace.name()).to.equal('de-space-name');
|
expect(translator.translatedAccess(dish).offers[0].inPlace.name()).to.equal('de-space-name');
|
||||||
|
expect(translatedThingDE.offers![0].inPlace!.name).to.equal('de-space-name');
|
||||||
}
|
}
|
||||||
|
|
||||||
@test
|
@test
|
||||||
public nestedMetaArrayOfString() {
|
public nestedMetaArrayOfString() {
|
||||||
expect(translator.translate(dish).offers[0].inPlace.categories()).to.deep.equal(['Büro', 'Bildung']);
|
expect(translator.translatedAccess(dish).offers[0].inPlace.categories()).to.deep.equal(['Büro', 'Bildung']);
|
||||||
|
expect(translatedThingDE.offers![0].inPlace!.categories).to.deep.equal(['Büro', 'Bildung']);
|
||||||
}
|
}
|
||||||
|
|
||||||
@test
|
@test
|
||||||
public nestedMetaArrayOfStringSubscript() {
|
public nestedMetaArrayOfStringSubscript() {
|
||||||
expect(translator.translate(dish).offers[0].inPlace.categories[1]()).to.equal('Bildung');
|
expect(translator.translatedAccess(dish).offers[0].inPlace.categories[1]()).to.equal('Bildung');
|
||||||
|
expect(translatedThingDE.offers![0].inPlace!.categories[1]).to.equal('Bildung');
|
||||||
}
|
}
|
||||||
|
|
||||||
@test
|
@test
|
||||||
public directStringLiteralTypeFallback() {
|
public directStringLiteralTypeFallback() {
|
||||||
expect(translatorWithFallback.translate(dish).type()).to.equal('dish');
|
expect(translatorWithFallback.translatedAccess(dish).type()).to.equal('dish');
|
||||||
|
expect(translatedThingFallback.type).to.equal('dish');
|
||||||
}
|
}
|
||||||
|
|
||||||
@test
|
@test
|
||||||
public directStringPropertyFallback() {
|
public directStringPropertyFallback() {
|
||||||
expect(translatorWithFallback.translate(dish).name()).to.equal('base-dish-name');
|
expect(translatorWithFallback.translatedAccess(dish).name()).to.equal('base-dish-name');
|
||||||
|
expect(translatedThingFallback.name).to.equal('base-dish-name');
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@test
|
@test
|
||||||
public directArrayOfStringSubscriptFallback() {
|
public directArrayOfStringSubscriptFallback() {
|
||||||
expect(translatorWithFallback.translate(dish).characteristics[1]())
|
expect(translatorWithFallback.translatedAccess(dish).characteristics[1]())
|
||||||
|
.to.deep.equal({name: 'base-characteristic1'});
|
||||||
|
expect(translatedThingFallback.characteristics![1])
|
||||||
.to.deep.equal({name: 'base-characteristic1'});
|
.to.deep.equal({name: 'base-characteristic1'});
|
||||||
}
|
}
|
||||||
|
|
||||||
@test
|
@test
|
||||||
public directMetaArrayOfStringFallback() {
|
public directMetaArrayOfStringFallback() {
|
||||||
expect(translatorWithFallback.translate(dish).categories()).to.deep.equal(['main dish', 'dessert']);
|
expect(translatorWithFallback.translatedAccess(dish).categories()).to.deep.equal(['main dish', 'dessert']);
|
||||||
|
expect(translatedThingFallback.categories).to.deep.equal(['main dish', 'dessert']);
|
||||||
}
|
}
|
||||||
|
|
||||||
@test
|
@test
|
||||||
public directMetaArrayOfStringSubscriptFallback() {
|
public directMetaArrayOfStringSubscriptFallback() {
|
||||||
expect(translatorWithFallback.translate(dish).categories[1]()).to.equal('dessert');
|
expect(translatorWithFallback.translatedAccess(dish).categories[1]()).to.equal('dessert');
|
||||||
|
expect(translatedThingFallback.categories[1]).to.equal('dessert');
|
||||||
}
|
}
|
||||||
|
|
||||||
@test
|
@test
|
||||||
public nestedStringLiteralTypeFallback() {
|
public nestedStringLiteralTypeFallback() {
|
||||||
expect(translatorWithFallback.translate(dish).offers[0].inPlace.type()).to.equal('building');
|
expect(translatorWithFallback.translatedAccess(dish).offers[0].inPlace.type()).to.equal('building');
|
||||||
|
expect(translatedThingFallback.offers![0].inPlace!.type).to.equal('building');
|
||||||
}
|
}
|
||||||
|
|
||||||
@test
|
@test
|
||||||
public nestedStringPropertyFallback() {
|
public nestedStringPropertyFallback() {
|
||||||
expect(translatorWithFallback.translate(dish).offers[0].inPlace.name()).to.equal('base-space-name');
|
expect(translatorWithFallback.translatedAccess(dish).offers[0].inPlace.name()).to.equal('base-space-name');
|
||||||
|
expect(translatedThingFallback.offers![0].inPlace!.name).to.equal('base-space-name');
|
||||||
}
|
}
|
||||||
|
|
||||||
@test
|
@test
|
||||||
public nestedMetaArrayOfStringFallback() {
|
public nestedMetaArrayOfStringFallback() {
|
||||||
expect(translatorWithFallback.translate(dish).offers[0].inPlace.categories())
|
expect(translatorWithFallback.translatedAccess(dish).offers[0].inPlace.categories())
|
||||||
|
.to.deep.equal(['office', 'education']);
|
||||||
|
expect(translatedThingFallback.offers![0].inPlace!.categories)
|
||||||
.to.deep.equal(['office', 'education']);
|
.to.deep.equal(['office', 'education']);
|
||||||
}
|
}
|
||||||
|
|
||||||
@test
|
@test
|
||||||
public nestedMetaArrayOfStringSubscriptFallback() {
|
public nestedMetaArrayOfStringSubscriptFallback() {
|
||||||
expect(translatorWithFallback.translate(dish).offers[0].inPlace.categories[1]()).to.equal('education');
|
expect(translatorWithFallback.translatedAccess(dish).offers[0].inPlace.categories[1]()).to.equal('education');
|
||||||
|
expect(translatedThingFallback.offers![0].inPlace!.categories[1]).to.equal('education');
|
||||||
}
|
}
|
||||||
|
|
||||||
@test
|
@test
|
||||||
public directStringLiteralTypeUndefined() {
|
public directStringLiteralTypeUndefined() {
|
||||||
// tslint:disable-next-line:no-eval
|
// tslint:disable-next-line:no-eval
|
||||||
const undefinedThing = eval('(x) => undefined;');
|
const undefinedThing = eval('(x) => undefined;');
|
||||||
expect(translator.translate(undefinedThing())('defaultValue')).to.equal('defaultValue');
|
expect(translator.translatedAccess(undefinedThing())('defaultValue')).to.equal('defaultValue');
|
||||||
expect(translator.translate(dish).name('defaultValue')).to.not.equal('defaultValue');
|
expect(translator.translatedAccess(dish).name('defaultValue')).to.not.equal('defaultValue');
|
||||||
}
|
}
|
||||||
|
|
||||||
@test
|
@test
|
||||||
public nestedMetaArrayOfStringSubscriptUndefined() {
|
public nestedMetaArrayOfStringSubscriptUndefined() {
|
||||||
// tslint:disable-next-line: no-eval
|
// tslint:disable-next-line: no-eval
|
||||||
const workingTranslation = eval('translator.translate(dish).offers[0].inPlace.categories[1](\'printer\');');
|
const workingTranslation = eval('translator.translatedAccess(dish).offers[0].inPlace.categories[1](\'printer\');');
|
||||||
// tslint:disable-next-line: no-eval
|
// tslint:disable-next-line: no-eval
|
||||||
const defaultValueTranslation = eval('translator.translate(dish).offers[0].inPlace.categories[1234](\'printer\');');
|
const defaultValueTranslation = eval('translator.translatedAccess(dish).offers[0].inPlace.categories[1234](\'printer\');');
|
||||||
|
|
||||||
expect(defaultValueTranslation).to.equal('printer');
|
expect(defaultValueTranslation).to.equal('printer');
|
||||||
expect(workingTranslation).to.not.equal('printer');
|
expect(workingTranslation).to.not.equal('printer');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@test
|
||||||
|
public reaccessWithChangedSourceOmitsLRUCache() {
|
||||||
|
const translatorDE = new SCThingTranslator('de');
|
||||||
|
const dishCopy = clone(dish);
|
||||||
|
const translatedDish = translatorDE.translatedAccess(dish);
|
||||||
|
const distructivelyTranslatedDish = translatorDE.translate(dish);
|
||||||
|
|
||||||
|
(dishCopy.origin as SCThingRemoteOrigin).name = 'tranlator.spec';
|
||||||
|
expect(translatorDE.translatedAccess(dishCopy)).not.to.deep.equal(translatedDish);
|
||||||
|
expect(translatorDE.translate(dishCopy)).not.to.equal(distructivelyTranslatedDish);
|
||||||
|
}
|
||||||
|
|
||||||
@test
|
@test
|
||||||
public changingTranslatorLanguageFlushesItsLRUCache() {
|
public changingTranslatorLanguageFlushesItsLRUCache() {
|
||||||
const translatorDE = new SCThingTranslator('de');
|
const translatorDE = new SCThingTranslator('de');
|
||||||
expect(translatorDE.translate(dish).name()).to.equal('de-dish-name');
|
expect(translatorDE.translatedAccess(dish).name()).to.equal('de-dish-name');
|
||||||
|
expect(translatorDE.translate(dish).name).to.equal('de-dish-name');
|
||||||
translatorDE.language = 'en';
|
translatorDE.language = 'en';
|
||||||
expect(translatorDE.translate(dish).name()).to.equal('base-dish-name');
|
expect(translatorDE.translatedAccess(dish).name()).to.equal('base-dish-name');
|
||||||
|
expect(translatorDE.translate(dish).name).to.equal('base-dish-name');
|
||||||
}
|
}
|
||||||
|
|
||||||
@test
|
@test
|
||||||
@@ -250,7 +290,7 @@ export class TranslationSpecInplace {
|
|||||||
for (let i = 0; i < 201; i++) {
|
for (let i = 0; i < 201; i++) {
|
||||||
const anotherDish = Object.assign({}, dish);
|
const anotherDish = Object.assign({}, dish);
|
||||||
anotherDish.uid = String(i);
|
anotherDish.uid = String(i);
|
||||||
expect(translatorDE.translate(anotherDish).name()).to.equal('de-dish-name');
|
expect(translatorDE.translatedAccess(anotherDish).name()).to.equal('de-dish-name');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -262,7 +302,7 @@ export class MetaTranslationSpec {
|
|||||||
@test
|
@test
|
||||||
public consistencyWithMetaClass() {
|
public consistencyWithMetaClass() {
|
||||||
const dishMetaTranslationsDE = translator.translatedPropertyNames(dish.type);
|
const dishMetaTranslationsDE = translator.translatedPropertyNames(dish.type);
|
||||||
const dishMetaTranslationsEN = translator.translatedPropertyNames(dish.type, 'en');
|
const dishMetaTranslationsEN = translatorEN.translatedPropertyNames(dish.type);
|
||||||
expect(dishMetaTranslationsEN).to.not.deep.equal(dishMetaTranslationsDE);
|
expect(dishMetaTranslationsEN).to.not.deep.equal(dishMetaTranslationsDE);
|
||||||
expect(dishMetaTranslationsDE).to.deep.equal(SCDishMeta.getInstance().fieldTranslations.de);
|
expect(dishMetaTranslationsDE).to.deep.equal(SCDishMeta.getInstance().fieldTranslations.de);
|
||||||
expect(dishMetaTranslationsEN).to.deep.equal(SCDishMeta.getInstance().fieldTranslations.en);
|
expect(dishMetaTranslationsEN).to.deep.equal(SCDishMeta.getInstance().fieldTranslations.en);
|
||||||
@@ -271,17 +311,17 @@ export class MetaTranslationSpec {
|
|||||||
@test
|
@test
|
||||||
public retrieveTranslatedPropertyValueType() {
|
public retrieveTranslatedPropertyValueType() {
|
||||||
const dishTypeDE = translator.translatedPropertyValue(dish.type, 'type');
|
const dishTypeDE = translator.translatedPropertyValue(dish.type, 'type');
|
||||||
const dishTypeEN = translator.translatedPropertyValue(dish.type, 'type', undefined, 'en');
|
const dishTypeEN = translatorEN.translatedPropertyValue(dish.type, 'type', undefined);
|
||||||
const dishTypeBASE = translatorWithFallback.translatedPropertyValue(dish.type, 'type');
|
const dishTypeBASE = translatorWithFallback.translatedPropertyValue(dish.type, 'type');
|
||||||
expect(dishTypeDE).to.deep.equal(SCDishMeta.getInstance().fieldValueTranslations.de.type);
|
expect(dishTypeDE).to.deep.equal(SCDishMeta.getInstance().fieldValueTranslations.de.type);
|
||||||
expect(dishTypeEN).to.deep.equal(SCDishMeta.getInstance().fieldValueTranslations.en.type);
|
expect(dishTypeEN).to.deep.equal(SCDishMeta.getInstance().fieldValueTranslations.en.type);
|
||||||
expect(dishTypeBASE).to.be.undefined;
|
expect(dishTypeBASE).to.deep.equal(SCDishMeta.getInstance().fieldValueTranslations.en.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
@test
|
@test
|
||||||
public retrieveTranslatedPropertyValueNested() {
|
public retrieveTranslatedPropertyValueNested() {
|
||||||
const dishTypeDE = translator.translatedPropertyValue<SCDish>(dish.type, 'categories', 'main dish');
|
const dishTypeDE = translator.translatedPropertyValue(dish.type, 'categories', 'main dish');
|
||||||
const dishTypeEN = translator.translatedPropertyValue<SCDish>(dish.type, 'categories', 'main dish', 'en');
|
const dishTypeEN = translatorEN.translatedPropertyValue(dish.type, 'categories', 'main dish');
|
||||||
const dishTypeBASE = translatorWithFallback.translatedPropertyValue(dish.type, 'categories', 'main dish');
|
const dishTypeBASE = translatorWithFallback.translatedPropertyValue(dish.type, 'categories', 'main dish');
|
||||||
expect(dishTypeDE).to.deep.equal(SCDishMeta.getInstance<SCDishMeta>().fieldValueTranslations.de.categories['main dish']);
|
expect(dishTypeDE).to.deep.equal(SCDishMeta.getInstance<SCDishMeta>().fieldValueTranslations.de.categories['main dish']);
|
||||||
expect(dishTypeEN).to.deep.equal(dish.categories[0]);
|
expect(dishTypeEN).to.deep.equal(dish.categories[0]);
|
||||||
|
|||||||
Reference in New Issue
Block a user