/* eslint-disable @typescript-eslint/no-explicit-any */ /* * Copyright (C) 2019-2022 Open 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 from 'fast-deep-equal/es6/index.js'; import clone from 'rfdc'; import {SCLanguageCode} from './general/i18n.js'; import {isThing} from './guards.js'; import {SCClasses} from './meta.js'; import {SCThing, SCThingType} from './things/abstract/thing.js'; // eslint disable @typescript-eslint/no-explicit-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; // Initalize all meta classes once if (typeof (this.metaClasses as any)[Object.keys(this.metaClasses)[0]] === 'function') { for (const metaClass of Object.keys(this.metaClasses)) { (this.metaClasses as any)[metaClass] = new (SCClasses as any)[metaClass](); } } } /** * Get field value translation recursively * @param data The intermediate object / primitive returned by the Proxys get() method * @returns a T object allowing for access to translations or a translated value(s) */ private deeptranslate(data: T): T { return typeof data === 'object' ? (new Proxy(data as never, { get: (target, key) => this.deeptranslate(target[key as never]), }) as unknown as T) : data; } /** * 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 (metaClass === undefined) { return undefined; } // Assigns every property in fieldTranslations to the known base language translation if (metaClass.fieldTranslations.en !== undefined) { for (const key of Object.keys(metaClass.fieldTranslations.en)) { (fieldTranslations as any)[key] = metaClass.fieldTranslations.en[key]; } } // Assigns every property in fieldTranslations to the known translation in given language if (metaClass.fieldTranslations[language] !== undefined) { for (const key of Object.keys(metaClass.fieldTranslations[language])) { (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 this.metaClasses[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 (metaClass === undefined) { return instance; } if (metaClass.fieldValueTranslations[language] !== undefined) { for (const key of Object.keys(metaClass.fieldValueTranslations[language])) { 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) for (const subKey of Object.keys((instance as any)[key])) { (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 for (const key of Object.keys(nextInstance)) { if ( isThing(nextInstance[key]) || Array.isArray(nextInstance[key]) || nextInstance[key] instanceof Object ) { nextInstance[key] = this.translateThingInPlaceDestructively(nextInstance[key]); } } // Spread variable translations given by the connector into thing if (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 thing 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 (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): T { return new Proxy(thing, { get: (target, key) => { const object: any = target; if (equal(this.sourceCache.get(thing), thing)) { const objectTranslatedFromCache = this.cache.get(thing); if (objectTranslatedFromCache !== undefined) { return this.deeptranslate((objectTranslatedFromCache as any)[key]); } } const objectTranslated = this.translateThingInPlaceDestructively(clone()(object)); this.cache.putObject(objectTranslated); this.sourceCache.putObject(thing); return this.deeptranslate(objectTranslated[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 (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); } }