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