/* * 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 equal = require('fast-deep-equal/es6'); import clone = require('rfdc'); import {Defined, TSOCType} from 'ts-optchain'; import {SCLanguageCode} 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 { /** * 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 */ private _language: SCLanguageCode; /** * LRU cache containing already translated SCThings */ private readonly cache: LRUCache; /** * Property providing a mapping from a SCThingType to its known own meta class */ private readonly metaClasses: typeof SCClasses; /** * LRU cache containing SCThings translations have been provided for */ private readonly sourceCache: LRUCache; /** * @example * // returns translator instance for german * new SCThingTranslator('de'); */ constructor(language: SCLanguageCode, cacheCapacity: number = standardCacheSize) { this.cache = new LRUCache(cacheCapacity); this.sourceCache = new LRUCache(cacheCapacity); this._language = language; this.metaClasses = SCClasses; } /** * 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: SCLanguageCode): 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: SCLanguageCode): 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; } /** * 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(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(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 * @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 TSOCType * const dishAsBefore: SCDish = dishTranslatedAccess()!; * @param thing Top level object that gets passed through the recursion * @returns an TSOCType object allowing for access to translations or a translated value(s) */ public translatedAccess(thing: T): TSOCType { return new Proxy( ((defaultValue?: Defined) => (thing == null ? defaultValue : thing)) as TSOCType, { get: (target, key) => { const obj: any = target(); if (equal(this.sourceCache.get(thing), thing)) { const objTranslatedFromCache = this.cache.get(thing); if (typeof objTranslatedFromCache !== 'undefined') { return this.deeptranslate((objTranslatedFromCache as any)[key]); } } const objTranslated = this.translateThingInPlaceDestructively(clone()(obj)); this.cache.putObject(objTranslated); this.sourceCache.putObject(thing); 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.CourseOfStudy); * @param type The type whose property names will be translated * @returns An object with the properties of the SCThingType where the values are the known property tranlations */ public translatedPropertyNames(type: SCThingType): T | undefined { return this.getAllMetaFieldTranslations(type, this.language) as T; } /** * 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 * @example * const singleValueTranslation = translator.translatedPropertyValue(SCThingType.Dish, 'categories', 'main dish'); * @param type The type for whose property values 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 * @returns Known translation for the property */ public translatedPropertyValue(type: SCThingType, field: string, key?: string): string | undefined { const fieldValueTranslations = this.getMetaClassInstance(type).fieldValueTranslations[this.language] ?? this.getMetaClassInstance(type).fieldValueTranslations.en; const fieldTranslation = fieldValueTranslations?.[field]; return fieldTranslation?.[key ?? ''] ?? key ?? fieldTranslation; } } /** * 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); } }