From fbbee53d4ed86dcc40fe8173fbe938008c9786ce Mon Sep 17 00:00:00 2001
From: Volker Schukai <volker.schukai@schukai.com>
Date: Thu, 2 Feb 2023 12:27:49 +0100
Subject: [PATCH] feat: document translations

---
 application/source/data/transformer.mjs     | 43 +++++++++++++
 application/source/i18n/provider.mjs        | 68 +++++++++++++++++++-
 application/source/i18n/providers/embed.mjs | 70 +++++++++++++++++----
 application/source/i18n/translations.mjs    | 52 ++++++++++++++-
 4 files changed, 218 insertions(+), 15 deletions(-)

diff --git a/application/source/data/transformer.mjs b/application/source/data/transformer.mjs
index 7170fcd01..9d87dbab6 100644
--- a/application/source/data/transformer.mjs
+++ b/application/source/data/transformer.mjs
@@ -165,6 +165,9 @@ function transform(value) {
     let key;
     let defaultValue;
 
+    let element;
+    let attribute;
+
     switch (this.command) {
         case "static":
             return this.args.join(":");
@@ -189,9 +192,11 @@ function transform(value) {
             validateInteger(n);
             return n;
 
+        case "to-json":
         case "tojson":
             return JSON.stringify(value);
 
+        case "from-json":
         case "fromjson":
             return JSON.parse(value);
 
@@ -456,6 +461,44 @@ function transform(value) {
 
             throw new Error("type not supported");
 
+        // case "element-by-id":
+        //     return getGlobal("document").getElementById(convertToString(value));
+        //
+        // case "element-query":
+        // case "element-query-selector":
+        //     return getGlobal("document").querySelector(convertToString(value));
+        //
+        // case "element-value":
+        //     return getGlobal("document").getElementById(convertToString(value))?.value;
+        //    
+        // case "element-text":
+        //     return getGlobal("document").getElementById(convertToString(value))?.innerText;
+        //    
+        // case "element-html":
+        //     return getGlobal("document").getElementById(convertToString(value))?.innerHTML;
+        //    
+        // case "element-attribute":
+        //     let element = getGlobal("document").getElementById(convertToString(value));
+        //     let attribute = args.shift();
+        //     if (attribute === undefined) {
+        //         throw new Error("missing attribute parameter");
+        //     }
+        //     return element?.getAttribute(attribute);
+
+        case "translation":
+            element = getGlobal("document").getElementById(convertToString(value));
+            if (element === undefined) {
+                throw new Error("missing element parameter");
+            }
+            
+            
+            attribute = args.shift();
+            if (attribute === undefined) {
+                throw new Error("missing attribute parameter");
+            }
+            return element?.getAttribute(attribute);
+
+
         default:
             throw new Error(`unknown command ${this.command}`);
     }
diff --git a/application/source/i18n/provider.mjs b/application/source/i18n/provider.mjs
index 007511fb0..6dbd7a520 100644
--- a/application/source/i18n/provider.mjs
+++ b/application/source/i18n/provider.mjs
@@ -11,7 +11,16 @@ import {BaseWithOptions} from "../types/basewithoptions.mjs";
 import {Locale} from "./locale.mjs";
 import {Translations} from "./translations.mjs";
 
-export { Provider };
+export {Provider, translationsLinkSymbol};
+
+/**
+ * @memberOf Monster.I18n
+ * @type {symbol}
+ * @license AGPLv3
+ * @since 3.9.0
+ * @private
+ */
+const translationsLinkSymbol = Symbol.for("@schukai/monster/i18n/translations@@link");
 
 /**
  * A provider makes a translation object available.
@@ -28,6 +37,11 @@ class Provider extends BaseWithOptions {
      * @return {Promise}
      */
     getTranslations(locale) {
+
+        if (locale === undefined) {
+            locale = getLocaleOfDocument();
+        }
+
         return new Promise((resolve, reject) => {
             try {
                 resolve(new Translations(locale));
@@ -36,4 +50,56 @@ class Provider extends BaseWithOptions {
             }
         });
     }
+
+    /**
+     * @param {Locale|string} locale
+     * @param {HTMLElement} element
+     * @return {Provider}
+     */
+    assignToElement(locale, element) {
+
+        if (locale === undefined) {
+            locale = getLocaleOfDocument();
+        }
+
+        if (!(locale instanceof Locale)) {
+            throw new Error("Locale is not an instance of Locale");
+        }
+
+        if (!(element instanceof HTMLElement)) {
+            element = document.querySelector("body");
+        }
+
+        if (!(element instanceof HTMLElement)) {
+            throw new Error("Element is not an HTMLElement");
+        }
+
+        return this.getTranslations(locale).then((obj) => {
+
+            let translations = null;
+            if (hasObjectLink(element, translationsLinkSymbol)) {
+                const objects = getLinkedObjects(element, translationsLinkSymbol);
+                for (const o of objects) {
+                    if (o instanceof Translations) {
+                        translations = o;
+                        break;
+                    }
+                }
+                
+                if (!(translations instanceof Translations)) {
+                    throw new Error("Object is not an instance of Translations");
+                }
+                
+                translations.assignTranslations(obj);
+                
+            } else {
+                addToObjectLink(element, translationsLinkSymbol, obj);
+            }
+
+
+            return obj;
+        });
+
+    }
+
 }
diff --git a/application/source/i18n/providers/embed.mjs b/application/source/i18n/providers/embed.mjs
index 6fb5a4757..0f2e5a1c0 100644
--- a/application/source/i18n/providers/embed.mjs
+++ b/application/source/i18n/providers/embed.mjs
@@ -42,22 +42,27 @@ class Embed extends Provider {
      * new Embed('translations')
      * ```
      *
-     * @param {string} id
+     * @param {HTMLElement|string} elementOrId
      * @param {Object} options
      */
-    constructor(id, options) {
+    constructor(elementOrId, options) {
         super(options);
 
         if (options === undefined) {
             options = {};
         }
 
-        validateString(id);
-
-        /**
-         * @property {string}
-         */
-        this.textId = id;
+        if (elementOrId instanceof HTMLElement) {
+            /**
+             * @property {HTMLElement|string}
+             */
+            this.translateElement = elementOrId;
+        } else {
+            /**
+             * @property {HTMLElement|string}
+             */
+            this.translateElement = getDocument().getElementById(validateString(elementOrId));
+        }
 
         /**
          * @private
@@ -86,16 +91,25 @@ class Embed extends Provider {
         }
 
         return new Promise((resolve, reject) => {
-            let text = getGlobalObject("document").getElementById(this.textId);
 
-            if (text === null) {
+            if (this.translateElement === null) {
                 reject(new Error("Text not found"));
                 return;
             }
 
+            if (!(this.translateElement instanceof HTMLScriptElement)) {
+                reject(new Error("Element is not a script tag"));
+                return;
+            }
+
+            if (this.translateElement.type !== "application/json") {
+                reject(new Error("Element is not a script tag with type application/json"));
+                return;
+            }
+
             let translations = null;
             try {
-                translations = JSON.parse(text.innerHTML);
+                translations = JSON.parse(this.translateElement.innerHTML);
             } catch (e) {
                 reject(e);
                 return;
@@ -112,4 +126,38 @@ class Embed extends Provider {
             resolve(t);
         });
     }
+
+
+    /**
+     * Initializes the translations for the current document.
+     *
+     * `script[data-monster-role=translations]` is searched for and the translations are assigned to the element.
+     *
+     * @param element
+     * @returns {Promise<unknown[]>}
+     */
+    static assignTranslationsToElement(element) {
+        const d = getDocument()
+
+        if (!(element instanceof HTMLElement)) {
+            element = d.querySelector("body");
+        }
+
+        const list = d.querySelectorAll("script[data-monster-role=translations]");
+        if (list === null) {
+            return;
+        }
+
+        const promises = [];
+        
+        let result
+        
+        list.forEach((translationElement) => {
+            const p = new Embed(translationElement);
+            promises.push(p.assignToElement(undefined, element));
+        });
+
+        return Promise.all(promises);
+    }
+    
 }
diff --git a/application/source/i18n/translations.mjs b/application/source/i18n/translations.mjs
index ef1addda5..2df47635c 100644
--- a/application/source/i18n/translations.mjs
+++ b/application/source/i18n/translations.mjs
@@ -34,11 +34,12 @@ class Translations extends Base {
     constructor(locale) {
         super();
 
-        if (isString(locale)) {
-            locale = parseLocale(locale);
+        if (locale instanceof Locale) {
+            this.locale = locale;
+        } else {
+            this.locale = parseLocale(validateString(locale));
         }
 
-        this.locale = validateInstance(locale, Locale);
         this.storage = new Map();
     }
 
@@ -169,6 +170,13 @@ class Translations extends Base {
      */
     assignTranslations(translations) {
         validateObject(translations);
+        
+        if (translations instanceof Translations) {
+            translations.storage.forEach((v, k) => {
+                this.setText(k, v);
+            });
+            return this;
+        }
 
         for (const [k, v] of Object.entries(translations)) {
             this.setText(k, v);
@@ -177,3 +185,41 @@ class Translations extends Base {
         return this;
     }
 }
+
+/**
+ * Returns the translations for the current document.
+ *
+ * @param element
+ * @returns {*}
+ * @throws {Error} Element is not an HTMLElement
+ * @throws {Error} Missing translations
+ */
+function getDocumentTranslations(element) {
+
+    const d = getDocument()
+
+    if (!(element instanceof HTMLElement)) {
+        element = d.querySelector("body");
+    }
+
+    if (!(element instanceof HTMLElement)) {
+        throw new Error("Element is not an HTMLElement");
+    }
+
+    if (!hasObjectLink(element, translationsLinkSymbol)) {
+        throw new Error("Missing translations");
+    }
+    
+    let obj = getLinkedObjects(element, translationsLinkSymbol);
+    
+    for (const t of obj) {
+        if (t instanceof Translations) {
+            return t;
+        }
+    }
+
+    throw new Error("Missing translations");
+
+}
+
+
-- 
GitLab