Compare commits

...

14 Commits

Author SHA1 Message Date
Rainer Killinger
e934017a65 0.42.0 2021-02-16 14:04:40 +01:00
Rainer Killinger
88dbedd932 refactor: update dependenices 2021-02-15 10:57:25 +01:00
Rainer Killinger
8b1b3444fd refactor: align SCCreativeWork to schema.org spec 2021-02-15 10:37:43 +01:00
Rainer Killinger
d5f3d71a41 refactor: overhaul translator as mentioned in #118 2021-02-15 10:37:38 +01:00
wulkanat@gmail.com
cbcf9c9adb docs: update changelog 2021-02-08 14:59:21 +01:00
wulkanat@gmail.com
321e4f6f24 0.41.0 2021-02-08 14:59:14 +01:00
Wieland Schöbl
8510f11d7b fix: add date, numeric range filter to SCSearchFilter 2021-02-05 13:50:18 +01:00
wulkanat@gmail.com
68aa377fe2 docs: update changelog 2021-02-05 12:59:54 +01:00
wulkanat@gmail.com
29f2c77ecc 0.40.0 2021-02-05 12:59:47 +01:00
Wieland Schöbl
29bc00616e feat: add range filter, date sorting support 2021-02-02 15:07:49 +01:00
Rainer Killinger
02920af4a4 refactor: make SCDish additives translatable 2021-02-01 13:24:12 +00:00
Rainer Killinger
0c14b0f1a0 test: change SCSemester sample data acronym 2020-12-02 17:22:17 +01:00
Rainer Killinger
db0a239761 refactor: change SCSemester acronym pattern 2020-12-02 17:19:46 +01:00
Jovan Krunić
377f55bbbe docs: update changelog 2020-12-01 17:16:02 +01:00
17 changed files with 1097 additions and 957 deletions

5
.mailmap Normal file
View 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>

View File

@@ -1,3 +1,30 @@
# [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)
### Features
* add range filter, date sorting support ([29bc006](https://gitlab.com/openstapps/core/commit/29bc00616e87a8d346d8c304fab2e3818921c75e))
# [0.39.0](https://gitlab.com/openstapps/core/compare/v0.38.1...v0.39.0) (2020-12-01)
### Features
* extend property value translation retrival ([a246bde](https://gitlab.com/openstapps/core/commit/a246bdea84e0ca390be6ab38723d637626db87d2))
## [0.38.1](https://gitlab.com/openstapps/core/compare/v0.38.0...v0.38.1) (2020-11-02) ## [0.38.1](https://gitlab.com/openstapps/core/compare/v0.38.0...v0.38.1) (2020-11-02)

View File

@@ -42,8 +42,9 @@ External dependencies can not be covered by the annotations. Documentation about
| `@indexable` | marks the type as indexable if the core schema is used to put data into a database/key-value store | | | `@indexable` | marks the type as indexable if the core schema is used to put data into a database/key-value store | |
| `@integer` | number field is interpreted as integer | | | `@integer` | number field is interpreted as integer | |
| `@keyword` | string field is interpreted as keyword | | | `@keyword` | string field is interpreted as keyword | |
| `@sortable` | field is sortable if the core schema is used to put data into a database/key-value store | sort method to be used: `ducet`, `price`, `distance` | | `@sortable` | field is sortable if the core schema is used to put data into a database/key-value store. Fields are always sortable through generic sort, even without annotation. | sort method to be used: `ducet`, `price`, `distance` |
| `@text` | string field is interpreted as text | | | `@text` | string field is interpreted as text | |
| `@date` | string field is interpreted as a date field | |
| `@validatable` | marks the type as validatable if the core schema is used to put data into a database/key-value store | | | `@validatable` | marks the type as validatable if the core schema is used to put data into a database/key-value store | |
| `@filterable` | non-object/nested field is filterable if the core schema is used to put data into a database/key-value store | | | `@filterable` | non-object/nested field is filterable if the core schema is used to put data into a database/key-value store | |
| `@inheritTags` | inherit all tags from another field | `[SCThingType]::[field]` | | `@inheritTags` | inherit all tags from another field | `[SCThingType]::[field]` |

1494
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "@openstapps/core", "name": "@openstapps/core",
"version": "0.39.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"

View File

@@ -18,6 +18,8 @@
* *
* @pattern ^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$ * @pattern ^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$
* @see https://gist.github.com/philipashlock/8830168 * @see https://gist.github.com/philipashlock/8830168
*
* @date
*/ */
export type SCISO8601Date = string; export type SCISO8601Date = string;
/* tslint:enable */ /* tslint:enable */

View File

@@ -19,6 +19,7 @@ import {SCMap} from '../../general/map';
import {SCSearchAvailabilityFilter} from './filters/availability'; import {SCSearchAvailabilityFilter} from './filters/availability';
import {SCSearchBooleanFilter} from './filters/boolean'; import {SCSearchBooleanFilter} from './filters/boolean';
import {SCSearchDistanceFilter} from './filters/distance'; import {SCSearchDistanceFilter} from './filters/distance';
import {SCSearchDateRangeFilter, SCSearchNumericRangeFilter} from './filters/range';
import {SCSearchValueFilter} from './filters/value'; import {SCSearchValueFilter} from './filters/value';
/** /**
@@ -28,7 +29,9 @@ export type SCSearchFilterType =
'availability' 'availability'
| 'boolean' | 'boolean'
| 'distance' | 'distance'
| 'value'; | 'value'
| 'date range'
| 'numeric range';
/** /**
* Structure of a filter instruction * Structure of a filter instruction
@@ -57,4 +60,6 @@ export type SCSearchFilter =
SCSearchAvailabilityFilter SCSearchAvailabilityFilter
| SCSearchBooleanFilter | SCSearchBooleanFilter
| SCSearchDistanceFilter | SCSearchDistanceFilter
| SCSearchValueFilter; | SCSearchValueFilter
| SCSearchNumericRangeFilter
| SCSearchDateRangeFilter;

View File

@@ -0,0 +1,112 @@
/*
* Copyright (C) 2020 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* 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 {SCThingsField} from '../../../meta';
import {SCSearchAbstractFilter, SCSearchAbstractFilterArguments} from '../filter';
/**
* A date range filter
*
* Filter for documents with a date field that satisfies the given constraints
*
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-range-query.html#_date_format_in_range_queries
*/
export interface SCSearchDateRangeFilter extends SCSearchAbstractFilter<SCDateRangeFilterArguments> {
/**
* @see SCSearchAbstractFilter.type
*/
type: 'date range';
}
/**
* A distance filter
*
* Filter for documents with a numeric field that satisfies the given constraints
*
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-range-query.html#query-dsl-range-query
*/
export interface SCSearchNumericRangeFilter extends SCSearchAbstractFilter<SCNumericRangeFilterArguments> {
/**
* @see SCSearchAbstractFilter.type
*/
type: 'numeric range';
}
/**
* Additional arguments for date range filters
*
* Filter uses a plain string to allow for date math expressions
* @see https://www.elastic.co/guide/en/elasticsearch/client/net-api/current/date-math-expressions.html
*/
export interface SCDateRangeFilterArguments extends SCRangeFilterArguments<string> {
/**
* Optional date format specifier
*
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-range-query.html#_date_format_in_range_queries
*/
format?: string;
/**
* Optional timezone specifier
*
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-range-query.html#_time_zone_in_range_queries
*/
timeZone?: string;
}
/**
* Additional arguments for numeric range filters
*/
export type SCNumericRangeFilterArguments = SCRangeFilterArguments<number>;
/**
* Additional arguments for range filters
*
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-range-query.html#query-dsl-range-query
*/
export interface SCRangeFilterArguments<T> extends SCSearchAbstractFilterArguments {
/**
* Bounds of the range
*/
bounds: Bounds<T>;
/**
* Field where the filter will be applied
*/
field: SCThingsField;
}
export interface Bounds<T> {
/**
* The lower bound
*/
lowerBound?: Bound<T>;
/**
* The upper bound
*/
upperBound?: Bound<T>;
}
export interface Bound<T> {
/**
* Limit of the bound
*/
limit: T;
/**
* Bound mode
*/
mode: 'inclusive' | 'exclusive';
}

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2019 StApps * Copyright (C) 2019, 2020 StApps
* This program is free software: you can redistribute it and/or modify it * This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free * under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3. * Software Foundation, version 3.
@@ -16,6 +16,7 @@ import {SCMap} from '../../general/map';
import {SCThingsField} from '../../meta'; import {SCThingsField} from '../../meta';
import {SCDistanceSort} from './sorts/distance'; import {SCDistanceSort} from './sorts/distance';
import {SCDucetSort} from './sorts/ducet'; import {SCDucetSort} from './sorts/ducet';
import {SCGenericSort} from './sorts/generic';
import {SCPriceSort} from './sorts/price'; import {SCPriceSort} from './sorts/price';
/** /**
@@ -51,9 +52,9 @@ export interface SCSearchAbstractSortArguments extends SCMap<unknown> {
/** /**
* Type of a sort instruction * Type of a sort instruction
*/ */
export type SCSearchSortType = 'distance' | 'price' | 'ducet'; export type SCSearchSortType = 'distance' | 'price' | 'ducet' | 'generic';
/** /**
* A sort instruction * A sort instruction
*/ */
export type SCSearchSort = SCDistanceSort | SCPriceSort | SCDucetSort; export type SCSearchSort = SCDistanceSort | SCPriceSort | SCDucetSort | SCGenericSort;

View File

@@ -0,0 +1,25 @@
/*
* Copyright (C) 2020 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* 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 {SCSearchAbstractSort, SCSearchAbstractSortArguments} from '../sort';
/**
* Sort instruction for generic sort such as date
*/
export interface SCGenericSort extends SCSearchAbstractSort<SCSearchAbstractSortArguments> {
/**
* @see SCSearchAbstractSort.type
*/
type: 'generic';
}

View File

@@ -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',
}, },

View File

@@ -91,6 +91,13 @@ export interface SCDish
export interface SCDishTranslatableProperties export interface SCDishTranslatableProperties
extends SCThingWithCategoriesTranslatableProperties, SCThingThatCanBeOfferedTranslatableProperties { extends SCThingWithCategoriesTranslatableProperties, SCThingThatCanBeOfferedTranslatableProperties {
/**
* Additives of the dish
*
* @filterable
* @keyword
*/
additives?: string[];
/** /**
* Characteristics of the dish * Characteristics of the dish
*/ */

View File

@@ -29,7 +29,7 @@ export interface SCSemesterWithoutReferences
* The short name of the semester, using the given pattern. * The short name of the semester, using the given pattern.
* *
* @filterable * @filterable
* @pattern ^(WS|SS) [0-9]{4}(/[0-9]{2})?$ * @pattern ^(WS|SS|WiSe|SoSe) [0-9]{4}(/[0-9]{2})?$
* @keyword * @keyword
*/ */
acronym: string; acronym: string;

View File

@@ -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;
}
} }
/** /**

View File

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

View File

@@ -4,9 +4,9 @@
"uid": "7cebbc3e-0a21-5371-ab0d-f7ba12f53dbd", "uid": "7cebbc3e-0a21-5371-ab0d-f7ba12f53dbd",
"type": "semester", "type": "semester",
"name": "Sommersemester 2018", "name": "Sommersemester 2018",
"acronym": "SS 2018", "acronym": "SoSe 2018",
"alternateNames": [ "alternateNames": [
"SoSe 2018" "Sommer 2018"
], ],
"startDate": "2018-04-01", "startDate": "2018-04-01",
"endDate": "2018-09-30", "endDate": "2018-09-30",

View File

@@ -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]);