mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-03 12:02:53 +00:00
405 lines
14 KiB
TypeScript
405 lines
14 KiB
TypeScript
/*
|
|
* 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 <https://www.gnu.org/licenses/>.
|
|
*/
|
|
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<SCThing>;
|
|
|
|
/**
|
|
* 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<SCThing>;
|
|
|
|
/**
|
|
* @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<T> object allowing for access to translations or a translated value(s)
|
|
*/
|
|
// tslint:disable-next-line:prefer-function-over-method
|
|
private deeptranslate<T>(data?: T): TSOCType<T> {
|
|
const proxy = new Proxy(
|
|
((defaultValue?: Defined<T>) => (data == null ? defaultValue : data)) as TSOCType<T>,
|
|
{
|
|
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<T>(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<T extends SCThing>(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<T>
|
|
* const dishAsBefore: SCDish = dishTranslatedAccess()!;
|
|
* @param thing Top level object that gets passed through the recursion
|
|
* @returns an TSOCType<T> object allowing for access to translations or a translated value(s)
|
|
*/
|
|
public translatedAccess<T extends SCThing>(thing: T): TSOCType<T> {
|
|
return new Proxy(
|
|
((defaultValue?: Defined<T>) => (thing == null ? defaultValue : thing)) as TSOCType<T>,
|
|
{
|
|
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<SCCourseOfStudy>(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<T extends SCThing>(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<T> {
|
|
/**
|
|
* Map property that manages cached content
|
|
*/
|
|
private readonly entries: Map<string, T> = new Map<string, T>();
|
|
|
|
/**
|
|
* 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<U extends SCThing>(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<U extends SCThing>(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<U extends SCThing>(something: U) {
|
|
this.put(something.uid, (something as any) as T);
|
|
}
|
|
}
|