diff --git a/src/app/translation/thing-translator.parser.ts b/src/app/translation/thing-translator.parser.ts
new file mode 100644
index 00000000..d4cb4570
--- /dev/null
+++ b/src/app/translation/thing-translator.parser.ts
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2020-2021 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 {Injectable} from '@angular/core';
+import {isDefined} from '@ngx-translate/core/lib/util';
+
+// tslint:disable: member-ordering prefer-function-over-method completed-docs
+
+export abstract class TranslateParser {
+ /**
+ * Gets a value from an object by composed key
+ * parser.getValue({ key1: { keyA: 'valueI' }}, 'key1.keyA') ==> 'valueI'
+ */
+ abstract getValue(target: unknown, key: string): string;
+
+ /**
+ * TODO
+ */
+ abstract getValueFromKeyPath(instance: object, keypath: string): unknown;
+}
+
+@Injectable()
+export class TranslateDefaultParser extends TranslateParser {
+
+ /**
+ * TODO
+ *
+ */
+ getValue(target: unknown, key: string): string {
+ const keys = typeof key === 'string' ? key.split('.') : [key];
+ let aKey = '';
+ // tslint:disable-next-line: no-any
+ let newTarget = target as any;
+ do {
+ aKey += keys.shift();
+ if (isDefined(newTarget) && isDefined(newTarget[aKey]) && (typeof newTarget[aKey] === 'object' || keys.length === 0)) {
+ newTarget = newTarget[aKey];
+ aKey = '';
+ } else if (keys.length === 0) {
+ newTarget = undefined;
+ } else {
+ aKey += '.';
+ }
+ } while (keys.length > 0);
+
+ return newTarget;
+ }
+
+
+ getValueFromKeyPath(instance: object, keypath: string): unknown {
+ // keypath = aproperty[0].anotherproperty["arrayproperty"][42].finalproperty
+ let path = keypath.replace(/(?:\"|\')'"/gmi, '');
+ // path = aproperty[0].anotherproperty[.arrayproperty.][42].finalproperty
+ path = path.replace(/(?:\[|\])/gmi, '.');
+ // path = aproperty.0..anotherproperty..arrayproperty...42..finalproperty
+ path = path.replace(/\.{2,}/gmi, '.');
+ // path = aproperty.0.anotherproperty.arrayproperty.42.finalproperty
+
+ const keypathChain = keypath.split('.');
+
+ // tslint:disable-next-line: no-any
+ let property = instance as any;
+
+ for(const key of keypathChain) {
+ property = property[key] ?? undefined;
+ }
+
+ return property;
+ }
+}
diff --git a/src/app/translation/thing-translator.pipe.ts b/src/app/translation/thing-translator.pipe.ts
new file mode 100644
index 00000000..2a3f2e85
--- /dev/null
+++ b/src/app/translation/thing-translator.pipe.ts
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2020-2021 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 {Injectable, OnDestroy, Pipe, PipeTransform} from '@angular/core';
+import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
+import {isThing, SCThings} from '@openstapps/core';
+import {Subscription} from 'rxjs';
+import {ThingTranslatorService} from './thing-translator.service';
+
+// tslint:disable: member-ordering prefer-function-over-method completed-docs
+
+
+@Injectable()
+@Pipe({
+ name: 'translate',
+ pure: false, // required to update the value when the promise is resolved
+})
+export class ThingTranslatePipe implements PipeTransform, OnDestroy {
+ value = '';
+ lastKey?: string;
+ lastThing: SCThings;
+ onLangChange: Subscription;
+
+ constructor(private readonly translate: TranslateService,
+ // private readonly _ref: ChangeDetectorRef,
+ private readonly thingTranslate: ThingTranslatorService) {
+ }
+
+ updateValue(key: string, thing: SCThings): void {
+ this.value = String(this.thingTranslate.get(thing, key));
+ }
+
+ transform(query: string, thing: SCThings): string {
+ if (typeof query !== 'string' || query.length <= 0) {
+ return query;
+ }
+
+ if (!isThing(thing)){
+ throw new SyntaxError(`Wrong parameter in ThingTranslatePipe. Expected a valid SCThing, received: ${thing}`);
+ }
+
+ // store the query, in case it changes
+ this.lastKey = query;
+
+ // store the params, in case they change
+ this.lastThing = thing;
+
+ // set the value
+ this.updateValue(query, thing);
+
+ // if there is a subscription to onLangChange, clean it
+ this._dispose();
+
+ // subscribe to onLangChange event, in case the language changes
+ if (typeof this.onLangChange === 'undefined' || this.onLangChange.closed ) {
+ this.onLangChange = this.translate.onLangChange.subscribe((_event: LangChangeEvent) => {
+ if (typeof this.lastKey === 'string') {
+ this.lastKey = undefined; // we want to make sure it doesn't return the same value until it's been updated
+ this.updateValue(query, thing);
+ }
+ });
+ }
+
+ return this.value;
+ }
+
+ /**
+ * Clean any existing subscription to change events
+ */
+ private _dispose(): void {
+ if (this.onLangChange?.closed) {
+ this.onLangChange?.unsubscribe();
+ }
+ }
+
+ ngOnDestroy(): void {
+ this._dispose();
+ }
+}
diff --git a/src/app/translation/thing-translator.service.ts b/src/app/translation/thing-translator.service.ts
new file mode 100644
index 00000000..b5ceecf2
--- /dev/null
+++ b/src/app/translation/thing-translator.service.ts
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2020-2021 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 {Injectable, OnDestroy} from '@angular/core';
+import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
+import {isDefined} from '@ngx-translate/core/lib/util';
+import {SCLanguage, SCThings, SCThingTranslator, SCTranslations} from '@openstapps/core';
+import {Subscription} from 'rxjs';
+import {TranslateParser} from './thing-translator.parser';
+
+// export const DEFAULT_LANGUAGE = new InjectionToken('DEFAULT_LANGUAGE');
+
+// tslint:disable: member-ordering prefer-function-over-method newline-per-chained-call completed-docs
+
+@Injectable({
+ providedIn: 'root',
+})
+export class ThingTranslatorService implements OnDestroy {
+
+ onLangChange: Subscription;
+ translator: SCThingTranslator;
+
+ /**
+ *
+ * @param translateService Instance of Angular TranslateService
+ * @param parser An instance of the parser currently used
+ * @param language TODO
+ */
+ constructor(private readonly translateService: TranslateService,
+ public parser: TranslateParser,
+ language?: keyof SCTranslations) {
+
+ this.translator = new SCThingTranslator(language ?? this.translateService.currentLang as keyof SCTranslations);
+
+ /** set the default language from configuration */
+ this.onLangChange = this.translateService.onLangChange.subscribe((event: LangChangeEvent) => {
+ this.translator.language = event.lang as keyof SCTranslations;
+ });
+
+ }
+
+ /**
+ * Returns the parsed result of the translations
+ */
+ // tslint:disable-next-line: no-any
+ getParsedResult(target: object, key: string): any {
+ return this.parser.getValueFromKeyPath(target, key);
+ }
+
+ /**
+ * Gets the translated value of a key (or an array of keys)
+ * @returns the translated key, or an object of translated keys
+ */
+ public get(thing: SCThings, keyPath: string | string[]): string | number | boolean {
+ if (!isDefined(keyPath) || keyPath.length === 0) {
+ throw new Error(`Parameter "key" required`);
+ }
+ if (keyPath instanceof Array) {
+ return this.getParsedResult(thing, keyPath.join('.'));
+ }
+
+ return this.getParsedResult(thing, keyPath);
+ }
+
+ // tslint:disable-next-line: completed-docs
+ ngOnDestroy() {
+ if (!this.onLangChange.closed) {
+ this.onLangChange.unsubscribe();
+ }
+ }
+}