/* * 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 clone = require('fast-clone'); import {Defined, TSOCType} from 'ts-optchain'; import {SCTranslations} from './general/i18n'; import {isThing} from './guards'; import {SCClasses} from './meta'; import {SCThing, SCThingType} from './things/abstract/thing'; // tslint:disable:no-any const standardCacheSize = 200; /** * SCThingTranslator class */ export class SCThingTranslator { /** * Property representing the translators target language */ private _language: keyof SCTranslations; /** * Property representing the translators base language * This means every translation is given for this language */ private readonly cache: LRUCache; /** * Property providing a mapping from a SCThingType to its known own meta class */ private readonly metaClasses: typeof SCClasses; /** * @example * // returns translator instance for german * new SCThingTranslator('de'); */ constructor(language: keyof SCTranslations, cacheCapacity: number = standardCacheSize) { this.cache = new LRUCache(cacheCapacity); this._language = language; this.metaClasses = SCClasses; } /** * Getter for language property */ get language(): keyof SCTranslations { 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) { if (language !== this._language) { this.cache.flush(); } this._language = language; } /** * Get field value translation recursively * * @param data The intermediate object / primitive returned by the Proxys get() method * @returns an TSOCType object allowing for access to translations or a translated value(s) */ // tslint:disable-next-line:prefer-function-over-method private deeptranslate(data?: T): TSOCType { const proxy = new Proxy( ((defaultValue?: Defined) => (data == null ? defaultValue : data)) as TSOCType, { get: (target, key) => { const obj: any = target(); return this.deeptranslate(obj[key]); }, }, ); 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 (typeof metaClass === 'undefined') { return undefined; } // Assigns every property in fieldTranslations to the known base language translation if (typeof metaClass.fieldTranslations.en !== 'undefined') { Object.keys(metaClass.fieldTranslations.en) .forEach((key) => { (fieldTranslations as any)[key] = metaClass.fieldTranslations.en[key]; }); } // Assigns every property in fieldTranslations to the known translation in given language if (typeof 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 Type of the thing * @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; } /** * Applies known field value translations of the given SCThings meta class to an instance * Translated values overwrite current values inplace (destructive) * * @param instance The thing / object that will be translated * @param language The language the thing / object is translated to * @returns The thing with translated meta field values */ private replaceAvailableMetaFieldValueTranslations(instance: any, language: keyof SCTranslations): any { const metaClass = this.getMetaClassInstance(instance.type); if (typeof metaClass === 'undefined') { return instance; } if (typeof metaClass.fieldValueTranslations[language] !== 'undefined') { Object.keys(metaClass.fieldValueTranslations[language]) .forEach((key) => { if (metaClass.fieldValueTranslations[language][key] instanceof Object && (instance as any)[key] instanceof Object) { // Assigns known translations of subproperties to property in given language (e.g. categories) Object.keys((instance as any)[key]) .forEach((subKey) => { (instance as any)[key][subKey] = metaClass.fieldValueTranslations[language][key][(instance as any)[key][subKey]]; }); } else if (metaClass.fieldValueTranslations[language][key] instanceof Object && typeof (instance as any)[key] === 'string') { // Assigns known translations of enum to property in given language (e.g. SCSettingInputType) (instance as any)[key] = metaClass.fieldValueTranslations[language][key][(instance as any)[key]]; } else { // Assigns property to known translation of fieldValueTranslations in given language (instance as any)[key] = metaClass.fieldValueTranslations[language][key]; } }); } return instance; } /** * 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 TSTSOCType * const dishAsBefore: SCDish = dishTranslatedAccess()!; * @param data Top level object that gets passed through the recursion * @returns an TSOCType object allowing for access to translations or a translated value(s) */ public translate(data: T): TSOCType { return new Proxy( ((defaultValue?: Defined) => (data == null ? defaultValue : data)) as TSOCType, { get: (target, key) => { const obj: any = target(); const objTranslatedFromCache = this.cache.get(data); if (typeof objTranslatedFromCache !== 'undefined') { return this.deeptranslate((objTranslatedFromCache as any)[key]); } const objTranslated = this.translateWholeThingDestructively(clone(obj)); this.cache.putObject(objTranslated); return this.deeptranslate(objTranslated[key]); }, }, ); } /** * Given a SCThingType this function returns an object with the same basic structure as the corresponding SCThing * All the values will be set to the known translations of the property/key name * @example * const translatedMetaDish = translator.translatedPropertyNames(SCThingType.CourseOfStudies); * @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 */ public translatedPropertyNames(type: SCThingType, language?: keyof SCTranslations): T | undefined { const targetLanguage = (typeof language !== 'undefined') ? language : this.language; return this.getAllMetaFieldTranslations(type, targetLanguage) as T; } /** * Given a SCThingType this function will translate it * * @param type The type that will be translated * @param language The language the type will be translated to * @returns Known translation of type parameter */ public translatedThingType(type: SCThingType, language?: keyof SCTranslations): string { const targetLanguage = (typeof language !== 'undefined') ? language : this.language; const metaClass = this.getMetaClassInstance(type); if (typeof metaClass.fieldValueTranslations[targetLanguage] !== 'undefined' && typeof metaClass.fieldValueTranslations[targetLanguage].type !== 'undefined') { return metaClass.fieldValueTranslations[targetLanguage].type as string ; } return type; } /** * 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 { 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; } } /** * LRUCache class * Small last recently used cache intended to get used by SCThingTranslator */ class LRUCache { /** * Map property that manages cached content */ private readonly entries: Map = new Map(); /** * Property representing cache maximum capacity */ private readonly maxEntries: number; /** * @example * // returns LRUCache instance with a maximum capacity of 500 * new LRUCache(500); */ constructor(maxEntries: number) { this.maxEntries = maxEntries; } /** * Flushes cache / removes all entries */ public flush() { this.entries.clear(); } /** * Get content from cache by key */ public get(somethingOrKey: string): T | undefined; /** * Get content from cache by another objects uid */ public get(something: U): T | undefined; /** * Get content from cache by key or by another objects uid * * @param somethingOrKey The key which maps to the cached content or an object for which content has been cached * @returns If available the content connected to the key or somethingOrKey.uid property */ public get(somethingOrKey: string | U): T | undefined { let key: string; if (typeof somethingOrKey === 'string') { key = somethingOrKey; } else if (isThing(somethingOrKey)) { key = somethingOrKey.uid; } else { throw new Error(`Passed argument ${somethingOrKey} cannot be key in LRUCache`); } const entry = this.entries.get(key); if (typeof entry !== 'undefined') { // LRU behavior this.entries.delete(key); this.entries.set(key, entry); } return entry; } /** * Place content in cache by key * * @param key The key for which content should be cached * @param content The content that should be cached */ public put(key: string, content: T) { if (this.entries.size >= this.maxEntries) { // LRU behavior const keyToDelete = this.entries.keys() .next().value; this.entries.delete(keyToDelete); } this.entries.set(key, content); } /** * Place content in cache by another objects uid * * @param something The object that should be cached under something.uid */ public putObject(something: U) { this.put(something.uid, (something as any) as T); } }