diff --git a/package-lock.json b/package-lock.json
index af7d3a50..75a356f4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2533,6 +2533,11 @@
}
}
},
+ "ts-optchain": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/ts-optchain/-/ts-optchain-0.1.2.tgz",
+ "integrity": "sha512-Xs1/xpXgTQhvgjP1qLIm5LWsgwAdpRnlfrHvMTyMPCNb4MP0WgYGCnK4xJBx0l4ZM+//IDubrmHkvp6BWfZfCg=="
+ },
"tslib": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
diff --git a/package.json b/package.json
index e3a1c30f..e0928bf4 100644
--- a/package.json
+++ b/package.json
@@ -38,12 +38,13 @@
"@types/geojson": "1.0.6",
"@types/json-patch": "0.0.30",
"json-patch": "0.7.0",
- "jsonschema": "1.2.4"
+ "jsonschema": "1.2.4",
+ "ts-optchain": "0.1.2"
},
"devDependencies": {
- "@openstapps/configuration": "0.6.0",
- "@openstapps/core-tools": "0.3.0",
- "@openstapps/logger": "0.0.5",
+ "@openstapps/configuration": "0.5.0",
+ "@openstapps/core-tools": "0.2.1",
+ "@openstapps/logger": "0.0.3",
"@types/chai": "4.1.7",
"@types/humanize-string": "1.0.0",
"@types/node": "11.9.4",
diff --git a/src/core/Thing.ts b/src/core/Thing.ts
index 4b393bee..16651e93 100644
--- a/src/core/Thing.ts
+++ b/src/core/Thing.ts
@@ -12,10 +12,8 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see .
*/
-import {SCThingsField} from './Classes';
import {SCOrganization} from './things/Organization';
import {SCPerson} from './things/Person';
-import {isThingWithTranslations} from './types/Guards';
import {SCTranslations} from './types/i18n';
import {SCISO8601Date} from './types/Time';
import {SCUuid} from './types/UUID';
@@ -219,85 +217,9 @@ export class SCThingMeta {
*/
static fieldValueTranslations: any = {
de: {
- type: {
- AcademicTerm: 'Studienabschnitt',
- Article: 'Artikel',
- Book: 'Buch',
- Catalog: 'Katalog',
- Date: 'Termin',
- Diff: 'Unterschied',
- Dish: 'Essen',
- Event: 'Veranstaltung',
- Favorite: 'Favorit',
- FloorPlan: 'Etagenplan',
- Message: 'Nachricht',
- Offer: 'Angebot',
- Organization: 'Organisation',
- Person: 'Person',
- Place: 'Ort',
- Setting: 'Einstellung',
- Thing: 'Ding',
- Ticket: 'Ticket',
- Tour: 'Tour',
- Video: 'Video',
- },
+ type: 'Ding',
},
};
-
- /**
- * Get field translation
- *
- * @param {keyof SCTranslations} language Language to get field translation for
- * @param {keyof T} field Field to get translation for
- * @returns {string} Translated field or field itself
- */
- static getFieldTranslation(language: keyof SCTranslations,
- field: SCThingsField): string {
- if (typeof this.fieldTranslations[language] !== 'undefined'
- && typeof this.fieldTranslations[language][field] !== 'undefined') {
- return this.fieldTranslations[language][field];
- }
-
- return field as string;
- }
-
- /**
- * Get field value translation
- *
- * @param {keyof SCTranslations} language Language to get value translation for
- * @param {string} field Field to get value translation for
- * @param {T} thing SCThing to get value translation for
- * @returns {string} Translated value or value itself
- */
- static getFieldValueTranslation(language: keyof SCTranslations,
- field: SCThingsField,
- thing: T): string {
-
- let translations: SCTranslations;
-
- if (isThingWithTranslations(thing)) {
- translations = thing.translations;
-
- const languageTranslations: SCThingTranslatableProperties | undefined = translations[language];
-
- if (typeof languageTranslations !== 'undefined') {
- if (typeof (languageTranslations as any)[field] !== 'undefined') {
- return (languageTranslations as any)[field];
- }
- }
- }
-
- // get translation from meta object
- if (typeof this.fieldValueTranslations[language] !== 'undefined'
- && typeof this.fieldValueTranslations[language][field] !== 'undefined'
- && typeof (thing as any)[field] !== 'undefined'
- && typeof this.fieldValueTranslations[language][field][(thing as any)[field]]) {
- return this.fieldValueTranslations[language][field][(thing as any)[field]];
- }
-
- // fallback to value itself
- return (thing as any)[field];
- }
}
/**
diff --git a/src/core/Translator.ts b/src/core/Translator.ts
new file mode 100644
index 00000000..fb71df99
--- /dev/null
+++ b/src/core/Translator.ts
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2019 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 .
+ */
+import {SCClasses, SCThingsField} from './Classes';
+import {SCThing, SCThingType} from './Thing';
+
+import {SCTranslations} from './types/i18n';
+
+import {Defined, OCType} from 'ts-optchain';
+
+/**
+ * SCThingTranslator class
+ */
+export class SCThingTranslator {
+ /**
+ * Property representing the translators base language.
+ * This means every translation is given for this language.
+ */
+ private baseLanguage: keyof SCTranslations;
+
+ /**
+ * Property representing the translators target language
+ */
+ private language: keyof SCTranslations;
+
+ /**
+ * Property provinding a mapping from a SCThingType to its known own meta class.
+ */
+ private metaClasses: typeof SCClasses;
+
+ /**
+ * @constructor
+ * @example
+ * // returns translator instance for german
+ * new SCThingTranslator('de');
+ */
+ constructor(language: keyof SCTranslations, baseLanguage?: keyof SCTranslations) {
+ this.baseLanguage = baseLanguage ? baseLanguage : 'en';
+ this.language = language;
+ this.metaClasses = SCClasses;
+ }
+
+ /**
+ * Get field value translation recursively
+ *
+ * @param firstObject Top level object that gets passed through the recursion
+ * @param data The intermediate object / primitive returned by the Proxys get() method
+ * @param keyPath The keypath that (in the end) leads to the translatable property (when added to firstObject)
+ * @returns an OCType object allowing for access to translations or a translated value(s)
+ */
+ private deeptranslate(firstObject: K, data?: T, keyPath?: string): OCType {
+ const proxy = new Proxy(
+ ((defaultValue?: Defined) => (data == null ? defaultValue : data)) as OCType,
+ {
+ get: (target, key) => {
+ const obj: any = target();
+ const extendedKeyPath = [keyPath, key.toString()].filter((e) => e != null).join('.');
+ let possiblePrimitive = obj[key];
+ // check if obj[key] is an array that contains primitive type (arrays in SCThings are not mixing types)
+ if (obj[key] instanceof Array && obj[key].length) {
+ possiblePrimitive = obj[key][0];
+ }
+ if (typeof possiblePrimitive === 'string' ||
+ typeof possiblePrimitive === 'number' ||
+ typeof possiblePrimitive === 'boolean') {
+ // returns final translation for primitive data types
+ return this.deeptranslate(firstObject,
+ this.getFieldValueTranslation(firstObject, extendedKeyPath),
+ extendedKeyPath);
+ }
+ // recursion to get more calls to the Proxy handler 'get()' (key path not complete)
+ return this.deeptranslate(firstObject, obj[key], extendedKeyPath);
+ },
+ },
+ );
+ return proxy;
+ }
+
+ /**
+ * Applies only known field translations of the given SCThings meta class to an instance
+ *
+ * @param thingType The type of thing that will be translated
+ * @param language The language the thing property values are translated to
+ * @returns The thing with all known meta values translated
+ */
+ private getAllMetaFieldTranslations(thingType: SCThingType,
+ language: keyof SCTranslations): object | undefined {
+ const fieldTranslations = {};
+ const metaClass = this.getMetaClassInstance(thingType);
+ if (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];
+ });
+ }
+
+ // Assigns every property in fieldTranslations to the known translation in given language
+ if (metaClass.fieldTranslations[language] !== undefined) {
+ Object.keys(metaClass.fieldTranslations[language]).forEach((key) => {
+ (fieldTranslations as any)[key] = metaClass.fieldTranslations[language][key];
+ });
+ }
+ return fieldTranslations;
+ }
+
+ /**
+ * Returns meta class needed for translations given a SCThingType
+ *
+ * @param thingType
+ * @returns An instance of the metaclass
+ */
+ private getMetaClassInstance(thingType: SCThingType): any {
+ if (thingType in this.metaClasses) {
+ return new (this.metaClasses as any)[thingType]();
+ }
+ return undefined;
+ }
+
+ /**
+ * 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
+ */
+ private valueFromPath(path: string, obj: T, separator = '.') {
+ path = path.replace(/\[/g, '.');
+ path = path.replace(/\]/g, '.');
+ path = path.replace(/\.\./g, '.');
+ path = path.replace(/\.$/, '');
+ const properties = path.split(separator);
+ return properties.reduce((prev: any, curr: any) => prev && prev[curr], obj);
+ }
+
+ /**
+ * Get field value translation
+ * @example
+ * // returns translation of the property (if available) in the language defined when creating the translator object
+ * const dish: SCDish = {...};
+ * translator.translate(dish, 'offers[0].inPlace.categories[1]');
+ * @param thing SCThing to get value translation for
+ * @param field Field to get value translation for (keypath allowed)
+ * @returns Translated value(s) or value(s) itself
+ */
+ public getFieldValueTranslation(thing: T,
+ field: SCThingsField): string | string[] {
+ let translationPath = 'translations.' + this.language + '.' + field;
+ const regexTrimProperties = /.*(?:(\..*)(\[\d+\])|(\.[^\d]*$)|(\..*)(\.[\d]*$))/;
+
+ const pathMatch = field.match(regexTrimProperties);
+
+ // when translation is given in thing
+ let translation = this.valueFromPath(translationPath, thing);
+ if (translation) {
+ return translation;
+ } else if (pathMatch && pathMatch[1] && pathMatch[2] || pathMatch && pathMatch[4] && pathMatch[5]) {
+ // accessing iteratable of nested thing
+ const keyPath = (pathMatch[1] ? pathMatch[1] : pathMatch[4]) + (pathMatch[2] ? pathMatch[2] : pathMatch[5]);
+ const redactedField = field.replace(keyPath, '');
+
+ // when translation is given in nested thing
+ translationPath = `${redactedField}.translations.${this.language}${keyPath}`;
+ translation = this.valueFromPath(translationPath, thing);
+ if (translation) {
+ return translation;
+ }
+
+ // when translation is given in nested meta thing via iterateable index
+ const nestedType = this.valueFromPath(field.replace(keyPath, '.type'), thing) as SCThingType;
+ translationPath = `fieldValueTranslations.${this.language}${keyPath}`;
+ translation = this.valueFromPath(translationPath.replace(
+ /\[(?=[^\[]*$).*|(?=[\d+]*$).*/, '[' + this.valueFromPath(field, thing) + ']'),
+ this.getMetaClassInstance(nestedType));
+ if (translation) {
+ return translation;
+ }
+
+ } else if (pathMatch && pathMatch[3]) {
+ // accessing meta or instance of nested thing primitive value depth > 0
+ const keyPath = pathMatch[3];
+ const redactedField = field.replace(pathMatch[3], '');
+
+ // when translation is given in nested thing
+ translationPath = `${redactedField}.translations.${this.language}${keyPath}`;
+ if (this.valueFromPath(translationPath, thing)) {
+ return this.valueFromPath(translationPath, thing);
+ }
+
+ // when translation is given in nested meta thing
+ const nestedType = this.valueFromPath(field.replace(keyPath, '.type'), thing) as SCThingType;
+ translationPath = `fieldValueTranslations.${this.language}${keyPath}`;
+ translation = this.valueFromPath(translationPath, this.getMetaClassInstance(nestedType));
+ if (translation instanceof Object) { // lookup translated keys in meta thing property
+ const translations: string[] = [];
+ this.valueFromPath(field, thing).forEach((key: string) => {
+ translationPath = `fieldValueTranslations.${this.language}${keyPath}.${key}`;
+ translations.push(this.valueFromPath(translationPath, this.getMetaClassInstance(nestedType)));
+ });
+ return translations;
+ }
+ if (!translation) { // translation not given, return as is
+ return this.valueFromPath(field, thing) as string;
+ }
+ return translation;
+ }
+ // 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;
+ }
+
+ // 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;
+ }
+
+ /**
+ * Get field value translation recursively
+ * @example
+ * const dish: SCDish = {...};
+ * translator.translate(dish).offers[0].inPlace.categories[1]());
+ * // or
+ * const dishTranslatedAccess = translator.translate(dish);
+ * dishTranslatedAccess.offers[0].inPlace.categories[1]();
+ * // undoing the OCType
+ * const dishAsBefore: SCDish = dishTranslatedAccess()!;
+ * @param data Top level object that gets passed through the recursion
+ * @returns an OCType object allowing for access to translations or a translated value(s)
+ */
+ public translate(data?: T): OCType {
+ return new Proxy(
+ ((defaultValue?: Defined) => (data == null ? defaultValue : data)) as OCType,
+ {
+ get: (target, key) => {
+ const obj: any = target();
+ let translatable = obj[key];
+ if (obj[key] instanceof Array && obj[key].length) {
+ translatable = obj[key][0];
+ if (typeof obj[key][0] === 'object' && !obj[key][0].origin) {
+ translatable = obj[key][0][Object.keys(obj[key][0])[0]];
+ }
+ }
+ 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());
+ },
+ },
+ );
+ }
+
+ /**
+ * Given a SCThingType this function returns an object with the same basic structure as the corresponding SCThing.
+ * All the values will be set to the known translations of the property/key name.
+ * @example
+ * const translatedMetaDish = translator.translatedPropertyNames(SCThingType.CourseOfStudies);
+ * @param language The language the object is translated to
+ * @param thingType
+ * @returns An object with the properties of the SCThingType where the values are the known property tranlations
+ */
+ public translatedPropertyNames(thing: T,
+ language?: keyof SCTranslations): T | undefined {
+ const targetLanguage = (language) ? language : this.language;
+ // return {...{}, ...this.getAllMetaFieldTranslations(thing.type, targetLanguage) as T};
+ return this.getAllMetaFieldTranslations(thing.type, targetLanguage) as T;
+ }
+}
diff --git a/src/core/base/Place.ts b/src/core/base/Place.ts
index db747759..d4b933ff 100644
--- a/src/core/base/Place.ts
+++ b/src/core/base/Place.ts
@@ -38,4 +38,14 @@ export interface SCPlaceWithoutReferences extends SCThing {
* @see http://wiki.openstreetmap.org/wiki/Key:opening_hours/specification
*/
openingHours?: string;
+
+ /**
+ * Translated fields of a place
+ */
+ translations?: SCTranslations;
+
+}
+
+export interface SCPlaceWithoutReferencesTranslatableProperties extends SCThingTranslatableProperties {
+ address?: SCPostalAddress;
}
diff --git a/src/core/things/Building.ts b/src/core/things/Building.ts
index 04732a24..a56a4faa 100644
--- a/src/core/things/Building.ts
+++ b/src/core/things/Building.ts
@@ -12,7 +12,7 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see .
*/
-import {SCPlaceWithoutReferences} from '../base/Place';
+import {SCPlaceWithoutReferences, SCPlaceWithoutReferencesTranslatableProperties} from '../base/Place';
import {
SCThingWithCategoriesSpecificValues,
SCThingWithCategoriesTranslatableProperties,
@@ -48,7 +48,7 @@ export interface SCBuildingWithoutReferences
/**
* Translated fields of a building
*/
- translations?: SCTranslations;
+ translations?: SCTranslations;
/**
* Type of the building
@@ -62,16 +62,33 @@ export interface SCBuildingWithoutReferences
* @validatable
*/
export interface SCBuilding extends SCBuildingWithoutReferences {
+ /**
+ * Translated fields of a building
+ */
+ translations?: SCTranslations;
+
/**
* Type of the building
*/
type: SCThingType.Building;
}
+export interface SCBuildingTranslatableProperties
+ extends SCPlaceWithoutReferencesTranslatableProperties, SCThingWithCategoriesTranslatableProperties {
+ floors?: string[];
+}
+
/**
* Meta information about a place
*/
export class SCBuildingMeta extends SCThingMeta {
+ static fieldTranslations = {
+ ...SCThingMeta.fieldTranslations,
+ de: {
+ floors: 'Etagen',
+ },
+ };
+
static fieldValueTranslations = {
...SCThingMeta.fieldValueTranslations,
de: {
@@ -85,6 +102,7 @@ export class SCBuildingMeta extends SCThingMeta {
'restroom': 'Toilette',
'student canteen': 'Mensa',
},
+ type: 'Gebäude',
},
};
}
diff --git a/src/core/things/Dish.ts b/src/core/things/Dish.ts
index 6b47fd46..faf29cd4 100644
--- a/src/core/things/Dish.ts
+++ b/src/core/things/Dish.ts
@@ -88,8 +88,12 @@ export interface SCDish extends SCDishWithoutReferences {
}
export interface SCDishTranslatableProperties
- extends SCThingWithCategoriesTranslatableProperties,
- SCThingThatCanBeOfferedTranslatableProperties {}
+ extends SCThingWithCategoriesTranslatableProperties, SCThingThatCanBeOfferedTranslatableProperties {
+ /**
+ * Characteristics of the dish
+ */
+ characteristics?: string[];
+}
/**
* Dish meta data
@@ -101,6 +105,21 @@ export class SCDishMeta extends SCThingMeta {
categories: 'Kategorien',
},
};
+
+ static fieldValueTranslations = {
+ ...SCThingMeta.fieldValueTranslations,
+ de: {
+ categories: {
+ appetizer: 'Vorspeise',
+ dessert: 'Nachtisch',
+ 'main dish': 'Hauptgericht',
+ salad: 'Salat',
+ 'side dish': 'Beilage',
+ soup: 'Suppe',
+ },
+ type: 'Essen',
+ },
+ };
}
/**
diff --git a/src/core/types/Guards.ts b/src/core/types/Guards.ts
index 103dfc4e..a397da5d 100644
--- a/src/core/types/Guards.ts
+++ b/src/core/types/Guards.ts
@@ -16,7 +16,16 @@ import {SCThingWithTranslations} from '../base/ThingWithTranslations';
import {SCBulkResponse} from '../protocol/routes/bulk/BulkResponse';
import {SCMultiSearchResponse} from '../protocol/routes/search/MultiSearchResponse';
import {SCSearchResponse} from '../protocol/routes/search/SearchResponse';
-import {SCThing} from '../Thing';
+import {SCThing, SCThingType} from '../Thing';
+
+/**
+ * Type guard to check if something is a SCThing
+ *
+ * @param {any} something Something to check
+ */
+export function isThing(something: any): something is SCThing {
+ return (something.type && something.type in SCThingType);
+}
/**
* Type guard to check if translations exist
diff --git a/src/core/types/i18n.ts b/src/core/types/i18n.ts
index 773ae5bb..665dd88b 100644
--- a/src/core/types/i18n.ts
+++ b/src/core/types/i18n.ts
@@ -668,7 +668,7 @@ export type SCNationality =
/**
* Translations for specific languages
*
- * @see https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes
+ * @see https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
*/
export interface SCTranslations {
/**