Commit 90e3d223 authored by Rainer Killinger's avatar Rainer Killinger Committed by krlwlfrt
Browse files

feat: add SCThingTranslator class. move functionality accordingly

parent 797e5ca9
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -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",
+5 −4
Original line number Diff line number Diff line
@@ -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",
+1 −79
Original line number Diff line number Diff line
@@ -12,10 +12,8 @@
 * 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 {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<T extends SCThing>} 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<T extends SCThing>(language: keyof SCTranslations<T>,
                                                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<T>} 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<T extends SCThing>(language: keyof SCTranslations<T>,
                                                     field: SCThingsField,
                                                     thing: T): string {

    let translations: SCTranslations<SCThingTranslatableProperties>;

    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];
  }
}

/**

src/core/Translator.ts

0 → 100644
+303 −0
Original line number Diff line number Diff line
/*
 * 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 {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<SCThing>;

  /**
   * Property representing the translators target language
   */
  private language: keyof SCTranslations<SCThing>;

  /**
   * 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<SCThing>, baseLanguage?: keyof SCTranslations<SCThing>) {
    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<T> object allowing for access to translations or a translated value(s)
   */
  private deeptranslate<T, K extends SCThing>(firstObject: K, data?: T, keyPath?: string): OCType<T> {
    const proxy = new Proxy(
      ((defaultValue?: Defined<T>) => (data == null ? defaultValue : data)) as OCType<T>,
      {
        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<T extends SCThing>(thingType: SCThingType,
                                                         language: keyof SCTranslations<T>): 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<T extends SCThing>(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<T extends SCThing>(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<T>
   * const dishAsBefore: SCDish = dishTranslatedAccess()!;
   * @param data Top level object that gets passed through the recursion
   * @returns an OCType<T> object allowing for access to translations or a translated value(s)
   */
  public translate<T extends SCThing>(data?: T): OCType<T> {
    return new Proxy(
      ((defaultValue?: Defined<T>) => (data == null ? defaultValue : data)) as OCType<T>,
      {
        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<SCCourseOfStudies>(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<T extends SCThing>(thing: T,
                                                    language?: keyof SCTranslations<T>): T | undefined {
    const targetLanguage = (language) ? language : this.language;
    // return {...{}, ...this.getAllMetaFieldTranslations(thing.type, targetLanguage) as T};
    return this.getAllMetaFieldTranslations(thing.type, targetLanguage) as T;
  }
}
+10 −0
Original line number Diff line number Diff line
@@ -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<SCPlaceWithoutReferencesTranslatableProperties>;

}

export interface SCPlaceWithoutReferencesTranslatableProperties extends SCThingTranslatableProperties {
  address?: SCPostalAddress;
}
Loading