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 { /**