|
|
@@ -0,0 +1,230 @@
|
|
|
+const { phrasebook } = require("./phrasebook");
|
|
|
+
|
|
|
+/**
|
|
|
+ * This class is responsible for automatic translate site content of the
|
|
|
+ * HTML elements. It could automatic translate content for all elements
|
|
|
+ * which are in the "translate" class, and has "phrase" attribute. This
|
|
|
+ * attrobite store phrase to translate, and insert into element innerText
|
|
|
+ * or placeholder (for inputs). It could also observate HTML Node, and
|
|
|
+ * automatic update transltions when any item had been changed.
|
|
|
+ */
|
|
|
+class autotranslate {
|
|
|
+ /**
|
|
|
+ * @var {?phrasebook}
|
|
|
+ * This store phrasebook to get translates from, or store null, to get
|
|
|
+ * translate content from global translate function.
|
|
|
+ */
|
|
|
+ #phrasebook;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @var {?MutationObserver}
|
|
|
+ * This store observer object, when it is connecter and waiting for
|
|
|
+ * changaes, or store null, when observer currently not working.
|
|
|
+ */
|
|
|
+ #observer;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * This create new autotranslator. It require phrasebook, to loads
|
|
|
+ * translations for phrases from. When null had been given, the it
|
|
|
+ * use global translate function.
|
|
|
+ *
|
|
|
+ * @throws {Error} - When trying to use it in the NodeJS.
|
|
|
+ *
|
|
|
+ * @param {?phrasebook} phrasebook
|
|
|
+ */
|
|
|
+ constructor(phrasebook = null) {
|
|
|
+ NODE: throw new Error("It is not avairable in the NodeJS.");
|
|
|
+ this.#observer = null;
|
|
|
+ this.#phrasebook = phrasebook;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * It return class name for elements, which would be translated by
|
|
|
+ * autotranslator.
|
|
|
+ *
|
|
|
+ * @returns {string} - Class name for autotranslating elements.
|
|
|
+ */
|
|
|
+ static get_class_name() {
|
|
|
+ return "translate";
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * This return selector for choose elements which must be autotranslated.
|
|
|
+ *
|
|
|
+ * @returns {string} - Selector of the elements to translate.
|
|
|
+ */
|
|
|
+ get #class_selector() {
|
|
|
+ return "." + autotranslate.get_class_name();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * This return name of attribute which store phrase to translate.
|
|
|
+ *
|
|
|
+ * @returns {string} - Name of attribute which store phrase.
|
|
|
+ */
|
|
|
+ static get_attribute_name() {
|
|
|
+ return "phrase";
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * This check that autotranslator is connected and waiting to changes.
|
|
|
+ *
|
|
|
+ * @returns {bool} - True when observer is connected, fakse when not.
|
|
|
+ */
|
|
|
+ get is_connected() {
|
|
|
+ return this.#observer !== null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * This search elements which could be translated in the element given
|
|
|
+ * in the parameter. When null given, then it search elements in the
|
|
|
+ * all document.
|
|
|
+ *
|
|
|
+ * @param {?HTMLElement} where - Item to load items from or null.
|
|
|
+ * @returns {Array} - Array of elements to translate.
|
|
|
+ */
|
|
|
+ #get_all_items(where = null) {
|
|
|
+ if (where === null) {
|
|
|
+ where = document;
|
|
|
+ }
|
|
|
+
|
|
|
+ return Array.from(
|
|
|
+ where.querySelectorAll(this.#class_selector)
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * It translate given phrase, baseed on loaded phrasebook, or when not
|
|
|
+ * loaded any, then use global translate function. When it also not
|
|
|
+ * exists, then throws error in debug mode, or return not translated
|
|
|
+ * phrase on production.
|
|
|
+ *
|
|
|
+ * @throws {Error} - When any option to translate not exists.
|
|
|
+ *
|
|
|
+ * @param {string} content - Phrase to translate.
|
|
|
+ * @returns {string} - Translated content.
|
|
|
+ */
|
|
|
+ #translate(content) {
|
|
|
+ if (this.#phrasebook !== null) {
|
|
|
+ return this.#phrasebook.translate(content);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (_ === undefined) {
|
|
|
+ DEBUG: throw new Error("All translate options are unavairable.");
|
|
|
+ return content;
|
|
|
+ }
|
|
|
+
|
|
|
+ return _(content);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * This add mutable observer to the body. It wait for DOM modifications,
|
|
|
+ * and when any new node had been adder, or any phrase attribute had
|
|
|
+ * been changed, then it trying to translate it.
|
|
|
+ *
|
|
|
+ * @returns {autotranslate} - This object to chain load.
|
|
|
+ */
|
|
|
+ connect() {
|
|
|
+ if (this.is_connected) {
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ const body = document.querySelector("body");
|
|
|
+ const callback = (targets) => { this.#process(targets); };
|
|
|
+ const options = {
|
|
|
+ childList: true,
|
|
|
+ attributes: true,
|
|
|
+ characterData: false,
|
|
|
+ subtree: true,
|
|
|
+ attributeFilter: [ autotranslate.get_attribute_name() ],
|
|
|
+ attributeOldValue: false,
|
|
|
+ characterDataOldValue: false
|
|
|
+ };
|
|
|
+
|
|
|
+ this.#observer = new MutationObserver(callback);
|
|
|
+ this.#observer.observe(body, options);
|
|
|
+
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * This prcoess all given in the array mutable records.
|
|
|
+ *
|
|
|
+ * @param {Array} targets - Array with mutable records.
|
|
|
+ */
|
|
|
+ #process(targets) {
|
|
|
+ targets.forEach(count => {
|
|
|
+ if (count.type === "attributes") {
|
|
|
+ this.#update_single(count.target);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.#get_all_items(count.target).forEach(count => {
|
|
|
+ this.#update_single(count);
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * This disconnect observer, and remove it.
|
|
|
+ *
|
|
|
+ * @returns {autotranslate} - This object to chain loading.
|
|
|
+ */
|
|
|
+ disconnect() {
|
|
|
+ if (!this.is_connected) {
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.#observer.disconnect();
|
|
|
+ this.#observer = null;
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * This update single element, based on phrase attribute. When element
|
|
|
+ * is standard HTMLElement, then it place translated content into
|
|
|
+ * innerText, but when element is input, like HTMLInputElement or
|
|
|
+ * HTMLTextAreaElement, then it place result into placeholder. When
|
|
|
+ * input is button, or submit, then it put content into value.
|
|
|
+ *
|
|
|
+ * @param {HTMLElement} target - Element to translate
|
|
|
+ */
|
|
|
+ #update_single(target) {
|
|
|
+ const attrobute_name = autotranslate.get_attribute_name();
|
|
|
+ const phrase = target.getAttribute(attrobute_name);
|
|
|
+ const result = this.#translate(phrase);
|
|
|
+
|
|
|
+ if (target instanceof HTMLInputElement) {
|
|
|
+ if (target.type === "button" || target.type === "submit") {
|
|
|
+ target.value = result;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ target.placeholder = result;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (target instanceof HTMLTextAreaElement) {
|
|
|
+ target.placeholder = result;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ target.innerText = result
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * This update translation of all elements in the document. It is useable
|
|
|
+ * when new autotranslator is created.
|
|
|
+ *
|
|
|
+ * @returns {autotranslate} - Instance of object to chain loading.
|
|
|
+ */
|
|
|
+ update() {
|
|
|
+ this.#get_all_items().forEach(count => {
|
|
|
+ this.#update_single(count);
|
|
|
+ });
|
|
|
+
|
|
|
+ return this;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+exports.autotranslate = autotranslate;
|