Browse Source

Commit for sync.

Cixo Develop 4 months ago
parent
commit
b7657d5b3a
43 changed files with 3005 additions and 198 deletions
  1. 1 1
      LICENSE
  2. 1189 0
      application/assets/cx-libtranslate.debug.js
  3. 3 0
      application/assets/cx-libtranslate.debug.js.map
  4. 0 0
      application/assets/cx-libtranslate.min.js
  5. 3 0
      application/assets/cx-libtranslate.min.js.map
  6. 72 0
      application/assets/languages/english.json
  7. 4 0
      application/assets/languages/index.json
  8. 72 0
      application/assets/languages/polish.json
  9. 3 0
      application/cx-libtranslate.debug.js.map
  10. 0 0
      application/cx-libtranslate.min.js
  11. 3 0
      application/cx-libtranslate.min.js.map
  12. 3 3
      application/scripts/confirm_action.js
  13. 12 0
      application/scripts/core.js
  14. 4 3
      application/scripts/delete_product_window.js
  15. 24 12
      application/scripts/import_products.js
  16. 8 8
      application/scripts/login_prompt.js
  17. 19 19
      application/scripts/product_adder.js
  18. 5 5
      application/scripts/product_all_rents.js
  19. 15 15
      application/scripts/product_editor.js
  20. 3 3
      application/scripts/product_give_back.js
  21. 2 2
      application/scripts/product_not_avairable.js
  22. 3 3
      application/scripts/product_rent.js
  23. 1 1
      application/scripts/product_response.js
  24. 0 20
      application/scripts/product_view.js
  25. 4 4
      application/scripts/rents_screen.js
  26. 5 3
      application/scripts/request.js
  27. 0 3
      application/scripts/reservations_loader.js
  28. 4 4
      application/scripts/searcher.js
  29. 2 1
      application/theme/core.sass
  30. 1 1
      application/theme/fonts.sass
  31. 12 0
      application/theme/language.sass
  32. 2 1
      application/views/core.html
  33. 71 0
      sample-phrasebook.json
  34. 1189 0
      static/assets/cx-libtranslate.debug.js
  35. 3 0
      static/assets/cx-libtranslate.debug.js.map
  36. 0 0
      static/assets/cx-libtranslate.min.js
  37. 3 0
      static/assets/cx-libtranslate.min.js.map
  38. 72 0
      static/assets/languages/english.json
  39. 4 0
      static/assets/languages/index.json
  40. 72 0
      static/assets/languages/polish.json
  41. 110 85
      static/bundle/app.js
  42. 0 0
      static/bundle/theme.css
  43. 2 1
      static/core.html

+ 1 - 1
LICENSE

@@ -1,5 +1,5 @@
 MIT License
-Copyright (c) <year> <copyright holders>
+Copyright (c) 2025 Les Amis Reunis Pubishing (Cixo Develop)
 
 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 

+ 1189 - 0
application/assets/cx-libtranslate.debug.js

@@ -0,0 +1,1189 @@
+var cx_libtranslate = (() => {
+  var __getOwnPropNames = Object.getOwnPropertyNames;
+  var __commonJS = (cb, mod) => function __require() {
+    return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
+  };
+
+  // source/translation.js
+  var require_translation = __commonJS({
+    "source/translation.js"(exports) {
+      var translation = class {
+        /**
+         * @var {string}
+         * This is translated content.
+         */
+        #content;
+        /**
+         * @var {bool}
+         * This is true, when content is translated from dict, and false
+         * when could not being found.
+         */
+        #translated;
+        /**
+         * This create new translation. Translation store content of the 
+         * translation, make avairable to format translated phrase and also
+         * store that translation was found in the phrasebook.
+         * 
+         * @param {string} content - Content of the translation.
+         * @param {bool} translated - True when translation could be found.
+         */
+        constructor(content, translated = true) {
+          if (typeof content !== "string") {
+            throw new TypeError("Translated content must be string.");
+          }
+          if (typeof translated !== "boolean") {
+            throw new TypeError("Result of translation must be boolean.");
+          }
+          this.#content = content;
+          this.#translated = translated;
+          Object.freeze(this);
+        }
+        /**
+         * This convert transiation to string.
+         * 
+         * @returns {string} - Content of the translation.
+         */
+        toString() {
+          return this.#content;
+        }
+        /**
+         * @returns {string} - Content of the translation.
+         */
+        get text() {
+          return this.#content;
+        }
+        /**
+         * @returns {bool} - True when translation was found, false when not.
+         */
+        get valid() {
+          return this.translated;
+        }
+        /**
+         * This would format ready translation, with numbers, dats, and 
+         * other content, which could not being statically places into
+         * translation. To use it, place name of content object key into
+         * "#{}" in translation. 
+         * 
+         * @example ```
+         *  Translation: "I have more than #{how_many} apples!"
+         *  Object: { how_many: 10 }
+         *  Result: "I have more than 10 apples!"
+         * ```
+         * 
+         * @param {string} content 
+         * @returns {string}
+         */
+        format(content) {
+          if (typeof content !== "object") {
+            throw new TypeError("Content to format from must be object.");
+          }
+          if (!this.#translated) {
+            return this.#content;
+          }
+          return this.#parse_format(content);
+        }
+        /**
+         * This infill prepared translation with data from content 
+         * object.
+         * 
+         * @see format
+         * 
+         * @param {object} content - Content to load data from. 
+         * @returns {string} - Formater translation.
+         */
+        #parse_format(content) {
+          let parts = this.#content.split("#{");
+          let result = parts[0];
+          for (let count = 1; count < parts.length; ++count) {
+            const part = parts[count];
+            const splited = part.split("}");
+            if (splited.length === 1) {
+              return result + splited[0];
+            }
+            const name = splited.splice(0, 1)[0].trim();
+            const rest = splited.join("}");
+            if (!(name in content)) {
+              DEBUG: throw new RangeError(
+                'Could not find "' + name + '".'
+              );
+              result += rest;
+              continue;
+            }
+            result += content[name] + rest;
+          }
+          return result;
+        }
+      };
+      exports.translation = translation;
+    }
+  });
+
+  // source/phrasebook.js
+  var require_phrasebook = __commonJS({
+    "source/phrasebook.js"(exports) {
+      var translation = require_translation().translation;
+      var phrasebook = class _phrasebook {
+        /**
+         * @var {Map}
+         * This store phrases in flat notation.
+         */
+        #phrases;
+        /**
+         * @var {?object}
+         * This store object for nested object notation.
+         */
+        #objects;
+        /**
+         * This create new phrasebook from phrases map, and optional object
+         * for phrases in object notation.
+         * 
+         * @param {Map} phrases - This contain phrases in flat notation.
+         * @param {?object} objects - This contain phrases in object notation. 
+         */
+        constructor(phrases, objects = null) {
+          if (!(phrases instanceof Map)) {
+            throw new TypeError("Phrases must an map.");
+          }
+          if (objects !== null && typeof objects !== "object") {
+            throw new TypeError("Objects must be null or object.");
+          }
+          this.#phrases = phrases;
+          this.#objects = objects;
+        }
+        /**
+         * This translate given phrase. When phrase is in the nested object 
+         * notation, then try to find phrase in objects. When not, try to find
+         * phrase in the flat phrases. When could not find phrase, then return 
+         * not translated phrase. Content always is returned as translation
+         * object, which could be also formated wich numbers, dates and
+         * much more.
+         * 
+         * @param {string} phrase - Phrase to translate. 
+         * @returns {translation} - Translated phrase.
+         */
+        translate(phrase) {
+          if (typeof phrase !== "string") {
+            throw new TypeError("Phrase to translate must be an string.");
+          }
+          if (this.#is_nested(phrase)) {
+            return this.#translate_nested(phrase);
+          }
+          return this.#translate_flat(phrase);
+        }
+        /**
+         * This translate given phrase. When phrase is in the nested object 
+         * notation, then try to find phrase in objects. When not, try to find
+         * phrase in the flat phrases. When could not find phrase, then return 
+         * not translated phrase. Content always is returned as translation
+         * object, which could be also formated wich numbers, dates and
+         * much more.
+         * 
+         * @param {string} phrase - Phrase to translate. 
+         * @returns {translation} - Translated phrase.
+         */
+        get(phrase) {
+          return this.translate(phrase);
+        }
+        /**
+         * Check that phrase is nested or not.
+         * 
+         * @param {string} phrase - Phrase to check that is nested
+         * @returns {bool} - True when nested, false when not 
+         */
+        #is_nested(phrase) {
+          return phrase.indexOf(".") !== -1;
+        }
+        /**
+         * This translate object notated phrase.
+         * 
+         * @param {string} phrase - Phrase to translate.
+         * @returns {translation} - Translated phrase. 
+         */
+        #translate_nested(phrase) {
+          if (this.#objects === null) {
+            return this.#translate_flat(phrase);
+          }
+          const parts = phrase.trim().split(".");
+          let current = this.#objects;
+          for (const count in parts) {
+            const part = parts[count];
+            if (!(part in current)) {
+              return new translation(phrase, false);
+            }
+            current = current[part];
+          }
+          if (typeof current !== "string") {
+            return new translation(phrase, false);
+          }
+          return new translation(current, true);
+        }
+        /**
+         * This set phrasebook as default. That mean, it would add own translate
+         * function to global scope, and create _({string}) function in the global
+         * scope. It is useable to minify code with translations. It is not 
+         * avairable in the NodeJS, only in the browser.
+         * 
+         * @returns {phrasebook} - Object itself to chain loading.
+         */
+        set_as_default() {
+          const translate = (content) => {
+            return this.translate(content);
+          };
+          window._ = translate;
+          window.translate = translate;
+        }
+        /**
+         * This translate flat phrase.
+         * 
+         * @param {string} phrase - Phrase to translate. 
+         * @returns {translation} - Translated phrase.
+         */
+        #translate_flat(phrase) {
+          const prepared = _phrasebook.prepare(phrase);
+          const found = this.#phrases.has(prepared);
+          const translated = found ? this.#phrases.get(prepared) : phrase;
+          return new translation(translated, found);
+        }
+        /**
+         * This prepars phrase, that mean replece all spaces with "_", trim 
+         * and also replace all big letters with lowwer. 
+         * 
+         * @param {string} content - Phrase to preapre.
+         * @return {string} - Prepared phrase.
+         */
+        static prepare(content) {
+          if (typeof content !== "string") {
+            throw new TypeError("Content to prepare must be an string.");
+          }
+          return content.trim().replaceAll(" ", "_").replaceAll(".", "_").toLowerCase();
+        }
+      };
+      exports.phrasebook = phrasebook;
+    }
+  });
+
+  // source/loader.js
+  var require_loader = __commonJS({
+    "source/loader.js"(exports) {
+      var phrasebook = require_phrasebook().phrasebook;
+      var loader = class {
+        /**
+         * @var {string}
+         * This is location of the phrasebook on the server.
+         */
+        #path;
+        /**
+         * @var {bool}
+         * This is true, when must load local file, or false when fetch.
+         */
+        #local;
+        /**
+         * This create new loader of the phrasebook.
+         * 
+         * @param {string} path - Location of the phrasebook to fetch.
+         * @param {bool} local - False when must fetch from remote.
+         */
+        constructor(path, local = false) {
+          if (typeof path !== "string") {
+            throw new TypeError("Path of the file must be string.");
+          }
+          if (typeof local !== "boolean") {
+            throw new TypeError("Local must be bool variable.");
+          }
+          this.#path = path;
+          this.#local = local;
+        }
+        /**
+         * This load file from path given in the constructor, parse and return it.
+         * 
+         * @returns {phrasebook} - New phrasebook with content from JSON file.
+         */
+        async #load_remote() {
+          const request = await fetch(this.#path);
+          const response = await request.json();
+          return this.#parse(response);
+        }
+        /**
+         * This load file from path given in the constructor, parse and return it.
+         * 
+         * @returns {phrasebook} - New phrasebook with content from JSON file.
+         */
+        async #load_local() {
+          let fs = null;
+          if (fs === null) {
+            throw new Error("Could not use ndoe:fs in browser.");
+          }
+          const content = await fs.readFile(this.#path, { encoding: "utf8" });
+          const response = JSON.parse(content);
+          return this.#parse(response);
+        }
+        /**
+         * This load file from path given in the constructor, parse and return it.
+         * 
+         * @returns {phrasebook} - New phrasebook with content from JSON file.
+         */
+        load() {
+          if (this.#local) {
+            return this.#load_local();
+          }
+          return this.#load_remote();
+        }
+        /**
+         * This parse phrasebook. When phrasebook contain "phrases" or "objects" 
+         * keys, and also "objects" is not string, then parse it as nested file,
+         * in the other way parse it as flat.
+         * 
+         * @param {object} content - Fetched object with translations. 
+         * @returns {phrasebook} - Loaded phrasebook.
+         */
+        #parse(content) {
+          const has_objects = "objects" in content && typeof content["objects"] === "object";
+          const has_phrases = "phrases" in content && typeof content["phrases"] === "object";
+          const is_nested = has_objects || has_phrases;
+          if (is_nested) {
+            const phrases = has_phrases ? content["phrases"] : {};
+            const objects = has_objects ? content["objects"] : {};
+            return new phrasebook(
+              this.#parse_phrases(phrases),
+              objects
+            );
+          }
+          return new phrasebook(this.#parse_phrases(content));
+        }
+        /**
+         * This parse flat phrases object to map.
+         * 
+         * @param {object} content - Flat phrases object to pase.
+         * @returns {Map} - Phrases parsed as Map.
+         */
+        #parse_phrases(content) {
+          const phrases = /* @__PURE__ */ new Map();
+          Object.keys(content).forEach((phrase) => {
+            const name = phrasebook.prepare(phrase);
+            const translation = content[phrase];
+            phrases.set(name, translation);
+          });
+          return phrases;
+        }
+      };
+      exports.loader = loader;
+    }
+  });
+
+  // source/languages.js
+  var require_languages = __commonJS({
+    "source/languages.js"(exports) {
+      var loader = require_loader().loader;
+      var phrasebook = require_phrasebook().phrasebook;
+      var languages = class {
+        /**
+         * @var {string}  
+         * This represents path to directory where phrasebooks had been stored.
+         */
+        #path;
+        /**
+         * @var {Map}
+         * This store languages and its files on server. 
+         */
+        #libs;
+        /**
+         * @var {bool}
+         * This store that directory is in the local file system, or remote
+         * server. When true, resources would be loaded by node:fs. When 
+         * false, resources would be fetched.
+         */
+        #local;
+        /**
+         * This create new languages library. Next, languages could be added to
+         * the library by command, or by loading index file.
+         * 
+         * @throws {TypeError} - When parameters is not in correct format.
+         * 
+         * @param {string} path - Path to phrasebooks on the server or filesystem.
+         * @param {bool} local - True when phrasebooks dirs would be loaded by 
+         *                       node:fs module. False when would be fetch.
+         */
+        constructor(path, local = false) {
+          if (typeof path !== "string") {
+            throw new TypeError("Path to the phrasebooks must be string.");
+          }
+          if (typeof local !== "boolean") {
+            throw new TypeError("Local must be bool variable.");
+          }
+          this.#local = local;
+          this.#path = path;
+          this.#libs = /* @__PURE__ */ new Map();
+        }
+        /**
+         * This add new language to the library by name. Name must be in form
+         * like POSIX locale, like en_US, or pl_PL. That mean first two letter
+         * mest be ISO 639-1 and second two letters mst be in ISO 3166-1 alpha-2
+         * 2 letter country code format.
+         * 
+         * @see https://www.loc.gov/standards/iso639-2/php/code_list.php
+         * @see https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
+         * @see https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
+         * @see https://en.wikipedia.org/wiki/Locale_(computer_software)
+         * 
+         * @throws {TypeError} - When tpes of the parameters is not correct.
+         * 
+         * @param {string} name - Name of the language, like "en_US".
+         * @param {string} file - Name of the file in the directory.
+         * @return {languages} - Instnace of this class to chain.
+         */
+        add(name, file) {
+          if (typeof name !== "string") {
+            throw new TypeError("Name of the language must be sting.");
+          }
+          if (typeof file !== "string") {
+            throw new TypeError("File on in the directory must be string.");
+          }
+          if (this.#libs.has(name)) {
+            console.error('Language "' + name + '" already loaded.');
+            console.error("It could not being loaded twice.");
+            return this;
+          }
+          if (!this.#valid_locale(name)) {
+            console.error('Language name "' + name + '" invalid formated.');
+            console.error("It could not being loaded.");
+            return this;
+          }
+          this.#libs.set(name, file);
+          return this;
+        }
+        /**
+         * This load all phrasebook given in the index file. Index must be
+         * JSON file, which contain one object. That object properties must be
+         * languages names in the notation like in add function. Valus of that
+         * properties musts being strings which contains names of the phrasebook
+         * files in the path directory.
+         * 
+         * @example ``` { "pl_PL": "polish.json", "en_US": "english.json" } ```
+         * 
+         * @see add
+         * 
+         * @param {string} index - Index file in the phrasebook directory.
+         * @return {languages} - New languages instance with loaded index.
+         */
+        async load(index) {
+          if (typeof index !== "string") {
+            throw new TypeError("Name of index file is not string.");
+          }
+          const response = await this.#load_index(index);
+          this.#libs.clear();
+          Object.keys(response).forEach((name) => {
+            if (typeof name !== "string") {
+              console.error("Name of the language must be string.");
+              console.error("Check languages index.");
+              console.error("Skipping it.");
+              return;
+            }
+            if (typeof response[name] !== "string") {
+              console.error("Name of phrasebook file must be string.");
+              console.error("Check languages index.");
+              console.error("Skipping it.");
+              return;
+            }
+            this.add(name, response[name]);
+          });
+          return this;
+        }
+        /**
+         * This load index object. That check, and when content must be loaded
+         * from local filesystem, it use node:fs, or when it must be fetched from
+         * remote, then use fetch API.
+         * 
+         * @param {string} index - Name of the index file in library. 
+         * @returns {object} - Loaded index file content. 
+         */
+        async #load_index(index) {
+          const path = this.#full_path(index);
+          if (this.#local) {
+            let fs = null;
+            if (fs === null) {
+              throw new Error("Could not use ndoe:fs in browser.");
+            }
+            return JSON.parse(
+              await fs.readFile(path, { encoding: "utf-8" })
+            );
+          }
+          const request = await fetch(path);
+          return await request.json();
+        }
+        /**
+         * This check that language exists in languages library.
+         * 
+         * @param {string} name - Name of the language to check.
+         * @return {bool} - True when language exists, false when not
+         */
+        has(name) {
+          return this.#libs.has(name);
+        }
+        /**
+         * This return all avairable languages.
+         * 
+         * @return {Array} - List of all avairable languages.
+         */
+        get avairable() {
+          const alls = new Array();
+          this.#libs.keys().forEach((name) => {
+            alls.push(name);
+          });
+          return alls;
+        }
+        /**
+         * @returns {string} - Default 
+         */
+        get default() {
+          const avairable = this.avairable;
+          if (avairable.length === 0) {
+            throw new Error("Languages list is empty. Can not load default.");
+          }
+          return avairable[0];
+        }
+        /**
+         * This load phrasebook with give name.
+         * 
+         * @throws {TypeError} - Param type is not correct.
+         * @throws {RangeError} - Language not exists in libs.
+         * 
+         * @param {string} name - Name of the language to load. 
+         * @returns {phrasebook} - Phrasebook loaded from the file.
+         */
+        select(name) {
+          if (typeof name !== "string") {
+            throw new TypeError("Name of the language must be string.");
+          }
+          if (!this.has(name)) {
+            DEBUG: throw new RangeError(
+              'Not found language "' + name + '".'
+            );
+            return new phrasebook(/* @__PURE__ */ new Map());
+          }
+          const file = this.#libs.get(name);
+          const path = this.#full_path(file);
+          return new loader(path, this.#local).load();
+        }
+        /**
+         * This return full path to the file.
+         * 
+         * @param {string} name - Name of the file to get its path
+         * @return {string} - Full path of the file
+         */
+        #full_path(name) {
+          let glue = "/";
+          if (this.#path[this.#path.length - 1] === glue) {
+            glue = "";
+          }
+          return this.#path + glue + name;
+        }
+        /**
+         * This check that format is valid POSIX like locale.
+         * 
+         * @param {string} name - Name to check format of.
+         * @return {bool} - True when format is valid, false when not.
+         */
+        #valid_locale(name) {
+          const splited = name.split("_");
+          if (splited.length !== 2) {
+            return false;
+          }
+          const first = splited[0];
+          const second = splited[1];
+          if (first.toLowerCase() !== first || first.length !== 2) {
+            return false;
+          }
+          if (second.toUpperCase() !== second || second.length !== 2) {
+            return false;
+          }
+          return true;
+        }
+      };
+      exports.languages = languages;
+    }
+  });
+
+  // source/selector.js
+  var require_selector = __commonJS({
+    "source/selector.js"(exports) {
+      var languages = require_languages().languages;
+      var selector = class {
+        /**
+         * @var {languages}
+         * This store languages container.
+         */
+        #languages;
+        /**
+         * @var {?HTMLElement}
+         * This store selector HTML container, or null when not created.
+         */
+        #container;
+        /**
+         * @var {HTMLElement}
+         * This store languages HTML selector object.
+         */
+        #selector;
+        /**
+         * @var {Array}
+         * This is array of callbacks which must being called after change.
+         */
+        #callbacks;
+        /**
+         * This create new selector object, from languages container.
+         * 
+         * @param {languages} languages - Languages container to work on.
+         */
+        constructor(languages2) {
+          this.#container = null;
+          this.#languages = languages2;
+          this.#callbacks = new Array();
+          this.#selector = this.#create_selector();
+        }
+        /**
+         * This check that selector is currently inserted anywhere.
+         * 
+         * @returns {bool} - True when selector is inserted anywhere.
+         */
+        get is_inserted() {
+          return this.#container !== null;
+        }
+        /**
+         * This inserts selector into given HTML element, or directly into
+         * document body, when any element is not specified. It returns 
+         * itselt, to call more functions inline.
+         * 
+         * @param {?HTMLElement} where - Place to insert selector, or null.
+         * @returns {selector} - This object to chain loading.
+         */
+        insert(where = null) {
+          if (this.is_inserted) {
+            return this;
+          }
+          if (where === null) {
+            where = document.querySelector("body");
+          }
+          this.#container = this.#create_container();
+          where.appendChild(this.#container);
+          return this;
+        }
+        /**
+         * This run all functions which was registered as onChange callback.
+         */
+        #on_change() {
+          this.#callbacks.forEach((count) => {
+            count(this.current);
+          });
+        }
+        /**
+         * This function remove selector from HTML element, if it is already
+         * inserted anywhere.
+         * 
+         * @returns {selector} - This object to chain loading.
+         */
+        remove() {
+          if (!this.is_inserted) {
+            return this;
+          }
+          this.#container.remove();
+          this.#container = null;
+          return this;
+        }
+        /**
+         * This create new container with selector inside.
+         * 
+         * @returns {HTMLElement} - New container with selector.
+         */
+        #create_container() {
+          const container = document.createElement("div");
+          container.classList.add(this.class_name);
+          container.appendChild(this.#selector);
+          return container;
+        }
+        /**
+         * This add new callback to selector. All callbacks would be called
+         * after language change. Callback would get one parameter, which is
+         * name of the location.
+         * 
+         * @param {CallableFunction} callback - Function to call on change.
+         * @returns 
+         */
+        add_listener(callback) {
+          this.#callbacks.push(callback);
+          return this;
+        }
+        /**
+         * This return HTML class name of selector container.
+         * 
+         * @returns {string} - HTML class name of selector container.
+         */
+        get class_name() {
+          return "cx-libtranslate-language-selector";
+        }
+        /**
+         * This create HTML option element from language name.
+         * 
+         * @param {string} location - Name of single language. 
+         * @returns {HTMLElement} - New option element.
+         */
+        #create_option(location) {
+          const name = location.split("_").pop();
+          const option = document.createElement("option");
+          option.innerText = name;
+          option.value = location;
+          return option;
+        }
+        /**
+         * This return current selected language name.
+         * 
+         * @returns {string} - Current selected language.
+         */
+        get current() {
+          return this.#selector.value;
+        }
+        /**
+         * This set current selected language for the selector.
+         * 
+         * @param {string} name - Name of the language to set.
+         * @returns {selector} - This to chain loading.
+         */
+        set_selection(name) {
+          if (!this.#languages.has(name)) {
+            DEBUG: throw new Error(
+              'Selector has not "' + name + '" language in the container.'
+            );
+          }
+          this.#selector.value = name;
+          return this;
+        }
+        /**
+         * This reload languages list in the selector. It could be used
+         * after change in the languages container.
+         * 
+         * @returns {selector} - Itself to chain loading.
+         */
+        reload() {
+          while (this.#selector.lastChild) {
+            this.#selector.lastChild.remove();
+          }
+          this.#languages.avairable.forEach((count) => {
+            this.#selector.appendChild(this.#create_option(count));
+          });
+          return this;
+        }
+        /**
+         * This create new HTML selector object, witch all languages
+         * from container inserted as options.
+         * 
+         * @returns {HTMLElement} - New selector object.
+         */
+        #create_selector() {
+          const selector2 = document.createElement("select");
+          selector2.addEventListener("change", () => {
+            this.#on_change();
+          });
+          this.#languages.avairable.forEach((count) => {
+            selector2.appendChild(this.#create_option(count));
+          });
+          return selector2;
+        }
+      };
+      exports.selector = selector;
+    }
+  });
+
+  // source/autotranslate.js
+  var require_autotranslate = __commonJS({
+    "source/autotranslate.js"(exports) {
+      var { phrasebook } = require_phrasebook();
+      var autotranslate = 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(phrasebook2 = null) {
+          this.#observer = null;
+          this.#phrasebook = phrasebook2;
+        }
+        /**
+         * 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 (_ === void 0) {
+            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((count2) => {
+              this.#update_single(count2);
+            });
+          });
+        }
+        /**
+         * 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;
+    }
+  });
+
+  // source/preferences.js
+  var require_preferences = __commonJS({
+    "source/preferences.js"(exports) {
+      var languages = require_languages().languages;
+      var phrasebook = require_phrasebook().phrasebook;
+      var selector = require_selector().selector;
+      var autotranslate = require_autotranslate().autotranslate;
+      var preferences = class {
+        /**
+         * @var {languages}
+         * This store loaded languages object.
+         */
+        #languages;
+        /**
+         * @var {?selector}
+         * This store selector for preferences.
+         */
+        #selector;
+        /**
+         * @var {?autotranslate}
+         * This store autotranslator, or null.
+         */
+        #autotranslate;
+        /**
+         * This create new language preferences manager from loaded languages
+         * object.
+         *
+         * @throws {Error} - When trying to use it in NodeJS.
+         * 
+         * @param {languages} target - loaded languages object.
+         */
+        constructor(target) {
+          this.#selector = null;
+          this.#autotranslate = null;
+          this.#languages = target;
+        }
+        /**
+         * This return name of language key in localStorage.
+         * 
+         * @returns {string} - Name of key in localStorage.
+         */
+        get #setting_name() {
+          return "cx_libtranslate_lang";
+        }
+        /**
+         * This return current saved language name, or null if any language
+         * is not selected yet.
+         * 
+         * @returns {?string} - Saved language or null.
+         */
+        get #state() {
+          return localStorage.getItem(this.#setting_name);
+        }
+        /**
+         * This return selector, which could being used in the ui.
+         * 
+         * @returns {selector} - New UI selector of the languages.
+         */
+        get selector() {
+          if (this.#selector !== null) {
+            return this.#selector;
+          }
+          this.#selector = new selector(this.#languages).set_selection(this.current).add_listener((target) => {
+            this.update(target);
+          }).add_listener(async (target) => {
+            if (this.#autotranslate === null) {
+              return;
+            }
+            this.#reload_autotranslate();
+          });
+          return this.#selector;
+        }
+        async #reload_autotranslate() {
+          const connected = this.#autotranslate.is_connected;
+          if (connected) {
+            this.#autotranslate.disconnect();
+          }
+          this.#autotranslate = null;
+          const created = await this.get_autotranslate();
+          if (connected) {
+            created.connect();
+          }
+        }
+        /**
+         * This load phrasebook for selected language, create autotranslate
+         * for it, and returns it. Autotranslate is cached in this object.
+         * 
+         * @returns {autotranslate} - Autotranslate for phrasebook.
+         */
+        async get_autotranslate() {
+          if (this.#autotranslate !== null) {
+            return this.#autotranslate;
+          }
+          const phrasebook2 = await this.load_choosen_phrasebook();
+          this.#autotranslate = new autotranslate(phrasebook2);
+          this.#autotranslate.update();
+          return this.#autotranslate;
+        }
+        /**
+         * This save new selected language name to localStorage, or remove it 
+         * from there if null given.
+         * 
+         * @param {?string} content - Name of selected language or null.
+         */
+        set #state(content) {
+          if (content === null) {
+            localStorage.removeItem(this.#setting_name);
+            return;
+          }
+          localStorage.setItem(this.#setting_name, content);
+        }
+        /**
+         * This return current language from localStorage. When any language is
+         * not loaded yet, then return default language. It also check that value
+         * value from localStorage, and if it not avairable in languages storage,
+         * then also return default language.
+         * 
+         * @return {string} - Name of the current language.
+         */
+        get current() {
+          const saved = this.#state;
+          if (saved === null) {
+            return this.#languages.default;
+          }
+          if (this.#languages.has(saved)) {
+            return saved;
+          }
+          return this.#languages.default;
+        }
+        /**
+         * This load phrasebook for current selected language.
+         * 
+         * @returns {phrasebook} - Phrasebook for current language.
+         */
+        async load_choosen_phrasebook() {
+          return await this.#languages.select(this.current);
+        }
+        /**
+         * This return loaded languages container.
+         * 
+         * @returns {languages} - Languages container.
+         */
+        get languages() {
+          return this.#languages;
+        }
+        /**
+         * This set new language for user. It also check that language exists 
+         * in the language container. When not exists, throws error.
+         * 
+         * @throws {Error} - When language not exists in container.
+         * 
+         * @param {string} name - New language to select.
+         * @returns {preferences} - This object to chain.
+         */
+        update(name) {
+          DEBUG: if (!this.#languages.has(name)) {
+            let error = 'Can not set language "' + name + '" ';
+            error += "because not exists in languages container.";
+            throw new Error(error);
+          }
+          this.#state = name;
+          return this;
+        }
+      };
+      exports.preferences = preferences;
+    }
+  });
+
+  // source/core.js
+  var require_core = __commonJS({
+    "source/core.js"(exports, module) {
+      if (typeof module !== "undefined" && module.exports) {
+        module.exports.phrasebook = require_phrasebook().phrasebook;
+        module.exports.loader = require_loader().loader;
+        module.exports.languages = require_languages().languages;
+        module.exports.loader = require_loader().loader;
+        module.exports.preferences = require_preferences().preferences;
+        module.exports.selector = require_selector().selector;
+        module.exports.autotranslate = require_autotranslate().autotranslate;
+      } else {
+        window.cx_libtranslate = {
+          phrasebook: require_phrasebook().phrasebook,
+          loader: require_loader().loader,
+          languages: require_languages().languages,
+          translation: require_translation().translation,
+          preferences: require_preferences().preferences,
+          selector: require_selector().selector,
+          autotranslate: require_autotranslate().autotranslate
+        };
+      }
+    }
+  });
+  return require_core();
+})();
+//# sourceMappingURL=cx-libtranslate.debug.js.map

File diff suppressed because it is too large
+ 3 - 0
application/assets/cx-libtranslate.debug.js.map


File diff suppressed because it is too large
+ 0 - 0
application/assets/cx-libtranslate.min.js


File diff suppressed because it is too large
+ 3 - 0
application/assets/cx-libtranslate.min.js.map


+ 72 - 0
application/assets/languages/english.json

@@ -0,0 +1,72 @@
+{
+    "do-you-want-to-remove-it": "Are you sure you want to remove the item?",
+    "you-try-to-remove-__name__": "Are you sure you want to remove #{ name }?",
+    "product-give-back": "Product return",
+    "processing": "Processing...",
+    "give-back-successfull": "Return completed successfully!",
+    "name-category": "Name",
+    "author-category": "Author",
+    "not-found-anything": "Unfortunately, nothing was found. Try searching for something else...",
+    "browse-our-products": "Find something for yourself!",
+    "you-must-confirm-it": "Confirm action",
+    "clear": "Clear",
+    "send": "Submit",
+    "product-editor": "Edit product",
+    "name-prompt": "Name:",
+    "name-sample": "Puzzle",
+    "description-prompt": "Description:",
+    "description-sample": "This is a very interesting puzzle for children and adults!",
+    "author-prompt": "Author:",
+    "author-sample": "Puzzle Company S.A.",
+    "barcode-prompt": "EAN code:",
+    "barcode-sample": "123456789012",
+    "stock-count-prompt": "Quantity in stock:",
+    "stock-count-sample": "10",
+    "change-product-image": "Change image.",
+    "updating-product-data": "Updating product data...",
+    "processing-image": "Processing image...",
+    "uploaded-successfull": "Upload completed successfully!",
+    "email-prompt": "Email address:",
+    "email-sample": "[email protected]",
+    "phone-number-prompt": "Phone number:",
+    "phone-number-sample": "+48 111-222-333",
+    "import-products-json": "Import product database from JSON file",
+    "loading-file": "Loading file...",
+    "parsing-file-to-dataset": "Parsing file to memory...",
+    "skipping-import-product-__barcode__": "Skipped product #{ barcode }.",
+    "searching-for-product-__barcode__": "Searching for data for #{ barcode }...",
+    "creating-product-__barcode__": "Creating product #{ barcode }...",
+    "can-not-add-product-__barcode__": "Cannot add #{ barcode }.",
+    "skipping-product-__barcode": "Skipped product #{ barcode }.",
+    "created-product-__barcode__": "Product #{ barcode } created successfully.",
+    "all-items-imported": "All products imported successfully!",
+    "not-all-items-imported": "Warning! Not all products were imported successfully!",
+    "product-rent": "Rent product",
+    "new-rent-added": "Rental completed successfully",
+    "product-not-avairable": "Product not available",
+    "this-product-is-not-avairable-yet": "This product is currently unavailable.",
+    "all-rents": "List of all rentals",
+    "loading": "Loading...",
+    "user-must-be-logged-in": "User must be logged in!",
+    "fail-when-request-__url__": "Error when loading \"#{ url }\"!",
+    "bad-response-not-contain-result": "Invalid server response, does not contain status.",
+    "login-window": "Log in",
+    "logged-in": "Logged in successfully!",
+    "can-not-login-check-nick-and-password": "Cannot log in! Check username and password...",
+    "nick-prompt": "Username:",
+    "nick-sample": "kotelek",
+    "password-prompt": "Password:",
+    "password-sample": "Kocia1234",
+    "incomplete-request-with-good-status": "Incomplete response with valid status.",
+    "add-product": "Add product",
+    "fill-barcode-first": "Fill in the EAN code first!",
+    "searching-in-the-web": "Searching for product online...",
+    "autocomplete-ready-check-results": "Autocomplete ready. Check results...",
+    "autocomplete": "Autocomplete",
+    "image-prompt": "Image: ",
+    "load-any-image-first": "Load product image first!",
+    "creating-new-product": "Creating new product...",
+    "created-new-product-success": "Product created successfully!",
+    "close": "Close",
+    "search": "Search..."
+}

+ 4 - 0
application/assets/languages/index.json

@@ -0,0 +1,4 @@
+{
+    "pl_PL": "polish.json",
+    "en_EN": "english.json"
+}

+ 72 - 0
application/assets/languages/polish.json

@@ -0,0 +1,72 @@
+{
+    "do-you-want-to-remove-it": "Czy napewno chcesz usunąć element?",
+    "you-try-to-remove-__name__": "Czy napewno chcesz usunąć #{ name }?",
+    "product-give-back": "Zwrot produktu",
+    "processing": "Procesowanie...",
+    "give-back-successfull": "Zwrot zakończony powodzeniem!",
+    "name-category": "Nazwa",
+    "author-category": "Autor",
+    "not-found-anything": "Niestety, nic nie odnaleziono. Spróbuj wyszukać coś innego...",
+    "browse-our-products": "Znajdź coś dla siebie!",
+    "you-must-confirm-it": "Zatwierdź akcję",
+    "clear": "Wyczyść",
+    "send": "Zatwierdź",
+    "product-editor": "Edytuj produkt",
+    "name-prompt": "Nazwa:",
+    "name-sample": "Puzzle",
+    "description-prompt": "Opis:",
+    "description-sample": "To bardzo ciekawa układanka, dla dzieci i dorosłych!",
+    "author-prompt": "Autor:",
+    "author-sample": "Układankowo S.A.",
+    "barcode-prompt": "Kod EAN:",
+    "barcode-sample": "123456789012",
+    "stock-count-prompt": "Posiadana ilość:",
+    "stock-count-sample": "10",
+    "change-product-image": "Zmień obraz.",
+    "updating-product-data": "Aktualizowanie danych produktu...",
+    "processing-image": "Procesowanie zdjęcia...",
+    "uploaded-successfull": "Przesyłanie zakończone powodzeniem!",
+    "email-prompt": "Adres email:",
+    "email-sample": "[email protected]",
+    "phone-number-prompt": "Numer telefonu:",
+    "phone-number-sample": "+48 111-222-333",
+    "import-products-json": "Importuj bazę danych produktów z pliku JSON",
+    "loading-file": "Ładowanie pliku...",
+    "parsing-file-to-dataset": "Parsowanie pliku do pamięci...",
+    "skipping-import-product-__barcode__": "Pominięto produkt #{ barcode }.",
+    "searching-for-product-__barcode__": "Wyszukiwanie danych dla #{ barcode }...",
+    "creating-product-__barcode__": "Tworzenie produktu #{ barcode }...",
+    "can-not-add-product-__barcode__": "Nie można dodać #{ barcode }.",
+    "skipping-product-__barcode": "Pominięto produkt #{ barcode }.",
+    "created-product-__barcode__": "Tworzenie produktu #{ barcode } zakończone powodzeniem.",
+    "all-items-imported": "Wszystkie produkty zaimportowano pomyślnie!",
+    "not-all-items-imported": "Uwaga! Nie wszystkie produkty zaimportowano pomyślnie!",
+    "product-rent": "Wypożycz produkt",
+    "new-rent-added": "Wypożyczenie zakończone sukcesem",
+    "product-not-avairable": "Produkt nie dostępny",
+    "this-product-is-not-avairable-yet": "Ten produkt nie jest aktualnie dostępny.",
+    "all-rents": "Lista wszystkich najemców",
+    "loading": "Ładowanie...",
+    "user-must-be-logged-in": "Użytkownik musi być zalogowany!",
+    "fail-when-request-__url__": "Błąd podczas ładowania \"#{ url }\"!",
+    "bad-response-not-contain-result": "Błędna odpowiedź serwera, nie zawiera statusu.",
+    "login-window": "Zaloguj się",
+    "logged-in": "Zalogowano pomyślnie!",
+    "can-not-login-check-nick-and-password": "Nie można zalogować! Sprawdź nick i hasło...",
+    "nick-prompt": "Nick:",
+    "nick-sample": "kotelek",
+    "password-prompt": "Hasło:",
+    "password-sample": "Kocia1234",
+    "incomplete-request-with-good-status": "Niekompletna odpowiedź o prawidłowym statusie.",
+    "add-product": "Dodaj produkt",
+    "fill-barcode-first": "Najpierw wypełnij kod EAN!",
+    "searching-in-the-web": "Wyszukiwanie produktu w sieci...",
+    "autocomplete-ready-check-results": "Podpowiedzi gotowe. Sprawdź rezultaty...",
+    "autocomplete": "Uzupełnij automatycznie",
+    "image-prompt": "Obraz: ",
+    "load-any-image-first": "Najpierw załaduj obraz produktu!",
+    "creating-new-product": "Tworzenie nowego produktu...",
+    "created-new-product-success": "Produkt utworzono pomyślnie!",
+    "close": "Zamknij",
+    "search": "Wyszukaj..."
+}

File diff suppressed because it is too large
+ 3 - 0
application/cx-libtranslate.debug.js.map


File diff suppressed because it is too large
+ 0 - 0
application/cx-libtranslate.min.js


File diff suppressed because it is too large
+ 3 - 0
application/cx-libtranslate.min.js.map


+ 3 - 3
application/scripts/confirm_action.js

@@ -12,7 +12,7 @@ export class confirm_action {
     }
 
     get _title() {
-        return "You must confirm it.";
+        return _("you-must-confirm-it");
     }
 
     _action() {
@@ -88,7 +88,7 @@ export class confirm_action {
         const cancel_button = document.createElement("button");
         cancel_button.classList.add("cancel");
         cancel_button.classList.add("material-icons");
-        cancel_button.innerText = "clear";
+        cancel_button.innerText = _("clear");
         buttons.appendChild(cancel_button);
  
         cancel_button.addEventListener("click", () => {
@@ -99,7 +99,7 @@ export class confirm_action {
             const confirm_button = document.createElement("button");
             confirm_button.classList.add("confirm");
             confirm_button.classList.add("material-icons");
-            confirm_button.innerText = "send";
+            confirm_button.innerText = _("send");
             buttons.appendChild(confirm_button);
             
             confirm_button.addEventListener("click", () => {

+ 12 - 0
application/scripts/core.js

@@ -9,6 +9,18 @@ import { scroll_up } from "./scroll_up.js";
 import { color_theme } from "./color_theme.js";
 
 document.addEventListener("DOMContentLoaded", async () => {
+    const languages = new cx_libtranslate.languages("app/assets/languages");
+    await languages.load("index.json");
+
+    const preferences = new cx_libtranslate.preferences(languages);
+    preferences.selector.insert().add_listener(() => { location.reload(); });
+
+    const phrasebook = await preferences.load_choosen_phrasebook();
+    phrasebook.set_as_default();
+
+    const autotranslate = await preferences.get_autotranslate();
+    autotranslate.connect();
+
     const top_bar_spacing = new height_equaler(
         document.querySelector(".top-bar"),
         document.querySelector(".top-bar-spacing")

+ 4 - 3
application/scripts/delete_product_window.js

@@ -11,12 +11,13 @@ export class delete_product_window extends confirm_action {
     }
 
     get _title() {
-        return "Do you want remove it?";
+        return _("do-you-want-to-remove-it");
     }
 
     get _description() {
-        let content = "You try to remove " + this.#target.name + ". ";
-        content += "You can not restore it, when confirm.";
+        let content = _("you-try-to-remove-__name__").format({
+            name: this.#target.name
+        });
 
         return content;
     }

+ 24 - 12
application/scripts/import_products.js

@@ -11,7 +11,7 @@ export class import_products extends formscreen {
     #content;
 
     get _name() {
-        return "Import products JSON";
+        return _("import-products-json");
     }
 
     _build_form() {
@@ -24,7 +24,7 @@ export class import_products extends formscreen {
 
     async #load_file() {
         if (this.#file.files.length === 0) {
-            throw new Error("Select JSON products database first.");
+            throw new Error("select-products-json-database-first");
         }
 
         const file = this.#file.files.item(0);
@@ -35,15 +35,17 @@ export class import_products extends formscreen {
 
     async _process() {
         try {
-            this._info = "Loading file...";
+            this._info = _("loading-file");
             this.#content = await this.#load_file();
-            this._info = "Parsing file to dataset...";
+            this._info = _("parsing-file-to-dataset");
             
             const result = new import_log();
 
             const dataset = new database(this.#content)
             .on_skip((fail) => {
-                this._info = "Skipping " + fail.product.barcode + "...";
+                this._info = _("skipping-import-product-__barcode__").format({
+                    barcode: fail.product.barcode
+                });
                 result.skip(fail);
             })
             .process()
@@ -51,21 +53,31 @@ export class import_products extends formscreen {
 
             const loop = new import_loop(dataset)
             .on_autocomplete((target) => {
-                this._info = "Searching for " + target.barcode + "...";
+                this._info = _("searching-for-product-__barcode__").format({
+                    barcode: target.barcode
+                });
             })
             .on_create((target) => {
-                this._info = "Creating " + target.barcode + "...";
+                this._info = _("creating-product-__barcode__").format({
+                    barcode: target.barcode
+                });
             })
             .on_single_fail((target) => {
-                this._info = "Can not add " + target.product.barcode + "...";
+                this._info = _("can-not-add-product-__barcode__").format({
+                    barcode: target.product.barcode
+                })
                 result.fail(target);
             })
             .on_skip((target) => {
-                this._info = "Skipping " + target.product.barcode + "...";
+                this._info = _("skipping-product-__barcode").format({
+                    barcode: target.product.barcode
+                })
                 result.skip(target);
             })
             .on_single_success((target) => {
-                this._info = "Created " + target.barcode + " success.";
+                this._info = _("created-product-__barcode__").format({
+                    barcode: target.barcode
+                })
             })
             .finally(() => {
                 searcher.reload();
@@ -78,13 +90,13 @@ export class import_products extends formscreen {
                 .process();
                 
                 if (result.length === 0) {
-                    this._success = "All items imported.";
+                    this._success = _("all-items-imported");
 
                     setTimeout(() => {
                         this.hide();
                     });
                 } else {
-                    this._success = "Not all items imported...";
+                    this._success = _("not-all-items-imported");
                 }
             })
             .process();

+ 8 - 8
application/scripts/login_prompt.js

@@ -14,14 +14,14 @@ export class login_prompt extends formscreen {
     }
 
     get _name() {
-        return "Login";
+        return _("login-window");
     }
 
     async _process() {
         try {
-            this._info = "Processing...";
+            this._info = _("processing");
             await this.#login();
-            this._success = "Logged in!";
+            this._success = _("logged-in");
 
             setTimeout(() => {
                 location.reload();
@@ -42,20 +42,20 @@ export class login_prompt extends formscreen {
             return;
         } 
         
-        throw new Error("Can not login. Check nick and password.");
+        throw new Error(_("can-not-login-check-nick-and-password"));
     }
 
     _build_form() {
         this.#nick = this._create_input(
             "nick",
-            "Nick:",
-            "Sample..."
+            _("nick-prompt"),
+            _("nick-sample")
         );
 
         this.#password = this._create_input(
             "password",
-            "Password:",
-            "ABCDEFGH",
+            _("password-prompt"),
+            _("password-sample"),
             (input) => { input.type = "password"; }
         );
     }

+ 19 - 19
application/scripts/product_adder.js

@@ -19,18 +19,18 @@ export class product_adder extends formscreen {
     #image_preview;
 
     get _name() {
-        return "Add product";
+        return _("add-product");
     }
 
     async #autocomplete() {
         const barcode = this.#barcode();
 
         if (barcode.length === 0) {
-            this._info = "Fill barcode first.";
+            this._info = _("fill-barcode-first");
             return;
         }
         
-        this._info = "Searching in the web..."
+        this._info = _("searching-in-the-web");
 
         try {
             const request = new autocomplete_request(barcode);
@@ -51,7 +51,7 @@ export class product_adder extends formscreen {
             
             this.#update_image_preview();
             
-            this._info = "Ready. Check results.";
+            this._info = _("autocomplete-ready-check-results");
         } catch (error) {
             this._error = new String(error);
         }
@@ -79,7 +79,7 @@ export class product_adder extends formscreen {
 
         const text = document.createElement("span");
         text.classList.add("text");
-        text.innerText = "Autocomplete";
+        text.innerText = _("autocomplete");
         button.appendChild(text);
 
         return button;
@@ -91,39 +91,39 @@ export class product_adder extends formscreen {
 
         this.#name = this._create_input(
             "name", 
-            "Name:", 
-            "Sample..."
+            _("name-prompt"), 
+            _("name-sample")
         );
 
         this.#description = this._create_input(
             "description",
-            "Description:",
-            "This is sample product..."
+            _("description-prompt"),
+            _("description-sample")
         );
 
         this.#author = this._create_input(
             "author",
-            "Author:",
-            "Jack Black"
+            _("author-prompt"),
+            _("author-sample")
         );
 
         this.#barcode = this._create_input(
             "barcode",
-            "Barcode (EAN):",
-            "123456789012...",
+            _("barcode-prompt"),
+            _("barcode-sample"),
             (input) => { input.type = "number"; }
         );
 
         this.#stock_count = this._create_input(
             "stock_count",
-            "Stock count:",
-            "10...",
+            _("stock-count-prompt"),
+            _("stock-count-sample"),
             (input) => { input.type = "number"; }
         );
 
         this._create_input(
             "image",
-            "Product image:",
+            _("image-prompt"),
             "",
             (input) => {
                 this.#image = input;
@@ -175,7 +175,7 @@ export class product_adder extends formscreen {
 
     get #ready_image() {
         if (this.#loaded_image === null) {
-            throw new Error("Loady any image first.");
+            throw new Error(_("load-any-image-first"));
         }
 
         return this.#loaded_image;
@@ -199,9 +199,9 @@ export class product_adder extends formscreen {
 
     async _process() {
         try {
-            this._info = "Uploading...";
+            this._info = _("creating-new-product");
             await this.#submit();
-            this._success = "Created success!";
+            this._success = _("created-new-product-success");
         
             searcher.reload();
 

+ 5 - 5
application/scripts/product_all_rents.js

@@ -13,7 +13,7 @@ export class product_all_rents extends formscreen {
     }
 
     get _name() {
-        return "All rents";
+        return _("all-rents");
     }
 
     get _has_submit() {
@@ -64,13 +64,13 @@ export class product_all_rents extends formscreen {
 
         button.addEventListener("click", async () => {
             try {
-                this._info = "Processing...";
+                this._info = _("processing");
 
                 const request = new product_give_back_request(target);
                 const response = await request.connect();
 
                 if (!response.result) {
-                    throw new Error(response.cause);
+                    throw new Error(_(response.cause));
                 }
 
                 this._refresh();
@@ -89,7 +89,7 @@ export class product_all_rents extends formscreen {
 
     async _build_form() {
         try {
-            this._info = "Loading...";
+            this._info = _("loading");
 
             const request = new product_reservations_request(this.#target);
             const response = await request.connect();
@@ -116,7 +116,7 @@ export class product_all_rents extends formscreen {
             this._append_child(list);
             
             if (empty) {
-                this._success = "Not found any reservations.";
+                this._success = "not-found-any-reservations";
             } else {
                 this._clear_results();
             }

+ 15 - 15
application/scripts/product_editor.js

@@ -26,7 +26,7 @@ export class product_editor extends formscreen {
     }
 
     get _name() {
-        return "Product editor";
+        return _("product-editor");
     }
 
     _build_form() {
@@ -35,8 +35,8 @@ export class product_editor extends formscreen {
 
         this.#name = this._create_input(
             "name", 
-            "Name:", 
-            "Sample...",
+            _("name-prompt"),
+            _("name-sample"),
             (input) => {
                 input.value = this.#target.name;
             }
@@ -44,8 +44,8 @@ export class product_editor extends formscreen {
 
         this.#description = this._create_input(
             "description",
-            "Description:",
-            "This is sample product...",
+            _("description-prompt"),
+            _("description-sample"),
             (input) => {
                 input.value = this.#target.description;
             }
@@ -53,8 +53,8 @@ export class product_editor extends formscreen {
 
         this.#author = this._create_input(
             "author",
-            "Author:",
-            "Jack Black",
+            _("author-prompt"),
+            _("author-sample"),
             (input) => {
                 input.value = this.#target.author;
             }
@@ -62,8 +62,8 @@ export class product_editor extends formscreen {
 
         this.#barcode = this._create_input(
             "barcode",
-            "Barcode (EAN):",
-            "123456789012...",
+            _("barcode-prompt"),
+            _("barcode-sample"),
             (input) => { 
                 input.type = "number"; 
                 input.value = this.#target.barcode
@@ -72,8 +72,8 @@ export class product_editor extends formscreen {
 
         this.#stock_count = this._create_input(
             "stock_count",
-            "Stock count:",
-            "10...",
+            _("stock-count-prompt"),
+            _("stock-count-sample"),
             (input) => { 
                 input.type = "number"; 
                 input.value = this.#target.stock_count
@@ -82,7 +82,7 @@ export class product_editor extends formscreen {
 
         this._create_input(
             "image",
-            "Change product image:",
+            _("change-product-image"),
             "",
             (input) => {
                 this.#image = input;
@@ -175,11 +175,11 @@ export class product_editor extends formscreen {
 
     async _process() {
         try {
-            this._info = "Uploading...";
+            this._info = _("updating-product-data");
             await this.#submit();
-            this._info = "Processing image...";
+            this._info = _("processing-image");
             await this.#image_submit();
-            this._success = "Updated success!";
+            this._success = _("uploaded-successfull");
 
             searcher.reload();
 

+ 3 - 3
application/scripts/product_give_back.js

@@ -6,12 +6,12 @@ import { searcher } from "./searcher.js";
 
 export class product_give_back extends rents_screen {
     get _name() {
-        return "Product give back";
+        return _("product-give-back");
     }
 
     async _process() {
         try {
-            this._info = "Processing...";
+            this._info = _("processing");
             
             const target = new reservation_factory()
             .email(this._email)
@@ -26,7 +26,7 @@ export class product_give_back extends rents_screen {
                 throw new Error(response.cause);
             }
 
-            this._success = "Success!";
+            this._success = _("give-back-successfull");
             searcher.reload();
 
             setTimeout(() => {

+ 2 - 2
application/scripts/product_not_avairable.js

@@ -2,11 +2,11 @@ import { confirm_action } from "./confirm_action";
 
 export class product_not_avairable extends confirm_action {
     get _title() {
-        return "Error";
+        return _("product-not-avairable");
     }    
 
     get _description() {
-        return "This product is not avairable. Anybody can not rent it.";
+        return _("this-product-is-not-avairable-yet");
     }
 
     get _info() {

+ 3 - 3
application/scripts/product_rent.js

@@ -6,12 +6,12 @@ import { searcher } from "./searcher.js";
 
 export class product_rent extends rents_screen {
     get _name() {
-        return "Product rent";
+        return _("product-rent");
     }
 
     async _process() {
         try {
-            this._info = "Processing...";
+            this._info = _("processing");
 
             const target = new reservation_factory()
             .email(this._email)
@@ -26,7 +26,7 @@ export class product_rent extends rents_screen {
                 throw new Error(response.cause);
             }
 
-            this._success = "New rent added.";
+            this._success = _("new-rent-added");
             searcher.reload();
 
             setTimeout(() => {

+ 1 - 1
application/scripts/product_response.js

@@ -11,7 +11,7 @@ export class product_response extends bool_response {
         
         if (this.result) {
             if (!("product" in target)) {
-                throw new Error("Incomplete response with good status.");
+                throw new Error(_("incomplete-request-with-good-status"));
             }
 
             this.#product = new product(target.product);

+ 0 - 20
application/scripts/product_view.js

@@ -1,20 +0,0 @@
-import { product_base } from "./product_base.js";
-
-export product_view export product_base {
-    image_url;
-    thumbnail_url;
-
-    constructor(target) {
-        super(target);
-        
-        this.image_url = this._extract(target, "image");
-        this.thumbnail_url = this._extract(target, "thumbnail");
-    }
-
-    dump() {
-        return Object.assign(super.dump(), {
-            "image": this.image_url,
-            "thumbnail": this.thumbnail_url
-        });
-    }
-}

+ 4 - 4
application/scripts/rents_screen.js

@@ -26,8 +26,8 @@ export class rents_screen extends formscreen {
     _build_form() {
         this.#email = this._create_input(
             "email",
-            "E-mail:",
-            "[email protected]",
+            _("email-prompt"),
+            _("email-sample"),
             (input) => { 
                 input.type = "email"; 
             }
@@ -35,8 +35,8 @@ export class rents_screen extends formscreen {
 
         this.#phone = this._create_input(
             "phone",
-            "Phone number:",
-            "+1 123-456-789",
+            _("phone-number-prompt"),
+            _("phone-number-sample"),
             (input) => {
                 input.type = "tel";
                

+ 5 - 3
application/scripts/request.js

@@ -26,7 +26,7 @@ export class request {
             return manager.apikey;
         }
 
-        throw new Error("User must be logged in.");
+        throw new Error(_("user-must-be-logged-in"));
     }
 
     get method() {
@@ -73,13 +73,15 @@ export class request {
         const request = await fetch(this.url, this.settings);
 
         if (!request.ok) {
-            throw new Error("Fail when requested: \"" + this.url + "\".");
+            throw new Error(_("fail-when-request-__url__").format({
+                url: this.url
+            }));
         }
 
         const response = await request.json();
 
         if (!("result" in response)) {
-            throw new Error("Bad response, not contain result.");
+            throw new Error(_("bad-response-not-contain-result"));
         }
 
         return new this._response(response);

+ 0 - 3
application/scripts/reservations_loader.js

@@ -1,3 +0,0 @@
-export class reservations_loader {
-    static 
-}   

+ 4 - 4
application/scripts/searcher.js

@@ -44,8 +44,8 @@ export class searcher {
 
     get categories() {
         return {
-            "name": "Name",
-            "author": "Author"
+            "name": _("name-category"),
+            "author": _("author-category")
         }
     }
 
@@ -96,9 +96,9 @@ export class searcher {
 
     #insert(list) {
         if (list.length === 0) {
-            this.#result_title = "Not found anything.";
+            this.#result_title = _("not-found-anything");
         } else {
-            this.#result_title = "Browse our products!";
+            this.#result_title = _("browse-our-products");
         }
 
         this.#manager

+ 2 - 1
application/theme/core.sass

@@ -6,4 +6,5 @@
 @import fullscreen
 @import scroll
 @import confirm_window
-@import reservations
+@import reservations
+@import language

+ 1 - 1
application/theme/fonts.sass

@@ -1,4 +1,4 @@
-body, input, button 
+body, input, button, select, option 
     font-size: 16px
     font-family: "Raleway", sans-serif
     font-style: normal

+ 12 - 0
application/theme/language.sass

@@ -0,0 +1,12 @@
+.cx-libtranslate-language-selector 
+    position: fixed
+    bottom: 20px
+    right: 20px
+
+    select
+        background-color: $secondary
+        color: $primary
+        border: none
+        border-radius: 10px
+        padding: 10px
+        border: 2px solid $primary

+ 2 - 1
application/views/core.html

@@ -15,6 +15,7 @@
         <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
 
         <script src="app/bundle/app.js" type="text/javascript"></script>
+        <script src="app/assets/cx-libtranslate.debug.js" type="text/javascript"></script>
         <link rel="stylesheet" type="text/css" href="app/bundle/theme.css">
     </head>
 
@@ -24,7 +25,7 @@
                 <div class="top-bar">
                     <div class="left">
                         <form class="search">
-                            <input type="text" name="content" placeholder="Search" class="search-content">
+                            <input type="text" name="content" phrase="search" class="search-content translate">
                             <select name="search-by"></select>
                             <button type="submit" name="submit" class="search-submit material-icons">search</button>
                         </form>

+ 71 - 0
sample-phrasebook.json

@@ -0,0 +1,71 @@
+{
+    "do-you-want-to-remove-it": "",
+    "you-try-to-remove-__name__": "",
+    "product-give-back": "",
+    "processing": "",
+    "give-back-successfull": "",
+    "name-category": "",
+    "author-category": "",
+    "not-found-anything": "",
+    "browse-our-products": "",
+    "you-must-confirm-it": "",
+    "clear": "",
+    "send": "",
+    "product-editor": "",
+    "name-prompt": "",
+    "name-sample": "",
+    "description-prompt": "",
+    "description-sample": "",
+    "author-prompt": "",
+    "author-sample": "",
+    "barcode-prompt": "",
+    "barcode-sample": "",
+    "stock-count-prompt": "",
+    "stock-count-sample": "",
+    "change-product-image": "",
+    "updating-product-data": "",
+    "processing-image": "",
+    "uploaded-successfull": "",
+    "email-prompt": "",
+    "email-sample": "",
+    "phone-number-prompt": "",
+    "phone-number-sample": "",
+    "import-products-json": "",
+    "loading-file": "",
+    "parsing-file-to-dataset": "",
+    "skipping-import-product-__barcode__": "",
+    "searching-for-product-__barcode__": "",
+    "creating-product-__barcode__": "",
+    "can-not-add-product-__barcode__": "",
+    "skipping-product-__barcode": "",
+    "created-product-__barcode__": "",
+    "all-items-imported": "",
+    "not-all-items-imported": "",
+    "product-rent": "",
+    "new-rent-added": "",
+    "product-not-avairable": "",
+    "this-product-is-not-avairable-yet": "",
+    "all-rents": "",
+    "loading": "",
+    "user-must-be-logged-in": "",
+    "fail-when-request-__url__": "",
+    "bad-response-not-contain-result": "",
+    "login-window": "",
+    "logged-in": "",
+    "can-not-login-check-nick-and-password": "",
+    "nick-prompt": "",
+    "nick-sample": "",
+    "password-prompt": "",
+    "password-sample": "",
+    "incomplete-request-with-good-status": "",
+    "add-product": "",
+    "fill-barcode-first": "",
+    "searching-in-the-web": "",
+    "autocomplete-ready-check-results": "",
+    "autocomplete": "",
+    "image-prompt": "",
+    "load-any-image-first": "",
+    "creating-new-product": "",
+    "created-new-product-success": "",
+    "close": ""
+}

+ 1189 - 0
static/assets/cx-libtranslate.debug.js

@@ -0,0 +1,1189 @@
+var cx_libtranslate = (() => {
+  var __getOwnPropNames = Object.getOwnPropertyNames;
+  var __commonJS = (cb, mod) => function __require() {
+    return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
+  };
+
+  // source/translation.js
+  var require_translation = __commonJS({
+    "source/translation.js"(exports) {
+      var translation = class {
+        /**
+         * @var {string}
+         * This is translated content.
+         */
+        #content;
+        /**
+         * @var {bool}
+         * This is true, when content is translated from dict, and false
+         * when could not being found.
+         */
+        #translated;
+        /**
+         * This create new translation. Translation store content of the 
+         * translation, make avairable to format translated phrase and also
+         * store that translation was found in the phrasebook.
+         * 
+         * @param {string} content - Content of the translation.
+         * @param {bool} translated - True when translation could be found.
+         */
+        constructor(content, translated = true) {
+          if (typeof content !== "string") {
+            throw new TypeError("Translated content must be string.");
+          }
+          if (typeof translated !== "boolean") {
+            throw new TypeError("Result of translation must be boolean.");
+          }
+          this.#content = content;
+          this.#translated = translated;
+          Object.freeze(this);
+        }
+        /**
+         * This convert transiation to string.
+         * 
+         * @returns {string} - Content of the translation.
+         */
+        toString() {
+          return this.#content;
+        }
+        /**
+         * @returns {string} - Content of the translation.
+         */
+        get text() {
+          return this.#content;
+        }
+        /**
+         * @returns {bool} - True when translation was found, false when not.
+         */
+        get valid() {
+          return this.translated;
+        }
+        /**
+         * This would format ready translation, with numbers, dats, and 
+         * other content, which could not being statically places into
+         * translation. To use it, place name of content object key into
+         * "#{}" in translation. 
+         * 
+         * @example ```
+         *  Translation: "I have more than #{how_many} apples!"
+         *  Object: { how_many: 10 }
+         *  Result: "I have more than 10 apples!"
+         * ```
+         * 
+         * @param {string} content 
+         * @returns {string}
+         */
+        format(content) {
+          if (typeof content !== "object") {
+            throw new TypeError("Content to format from must be object.");
+          }
+          if (!this.#translated) {
+            return this.#content;
+          }
+          return this.#parse_format(content);
+        }
+        /**
+         * This infill prepared translation with data from content 
+         * object.
+         * 
+         * @see format
+         * 
+         * @param {object} content - Content to load data from. 
+         * @returns {string} - Formater translation.
+         */
+        #parse_format(content) {
+          let parts = this.#content.split("#{");
+          let result = parts[0];
+          for (let count = 1; count < parts.length; ++count) {
+            const part = parts[count];
+            const splited = part.split("}");
+            if (splited.length === 1) {
+              return result + splited[0];
+            }
+            const name = splited.splice(0, 1)[0].trim();
+            const rest = splited.join("}");
+            if (!(name in content)) {
+              DEBUG: throw new RangeError(
+                'Could not find "' + name + '".'
+              );
+              result += rest;
+              continue;
+            }
+            result += content[name] + rest;
+          }
+          return result;
+        }
+      };
+      exports.translation = translation;
+    }
+  });
+
+  // source/phrasebook.js
+  var require_phrasebook = __commonJS({
+    "source/phrasebook.js"(exports) {
+      var translation = require_translation().translation;
+      var phrasebook = class _phrasebook {
+        /**
+         * @var {Map}
+         * This store phrases in flat notation.
+         */
+        #phrases;
+        /**
+         * @var {?object}
+         * This store object for nested object notation.
+         */
+        #objects;
+        /**
+         * This create new phrasebook from phrases map, and optional object
+         * for phrases in object notation.
+         * 
+         * @param {Map} phrases - This contain phrases in flat notation.
+         * @param {?object} objects - This contain phrases in object notation. 
+         */
+        constructor(phrases, objects = null) {
+          if (!(phrases instanceof Map)) {
+            throw new TypeError("Phrases must an map.");
+          }
+          if (objects !== null && typeof objects !== "object") {
+            throw new TypeError("Objects must be null or object.");
+          }
+          this.#phrases = phrases;
+          this.#objects = objects;
+        }
+        /**
+         * This translate given phrase. When phrase is in the nested object 
+         * notation, then try to find phrase in objects. When not, try to find
+         * phrase in the flat phrases. When could not find phrase, then return 
+         * not translated phrase. Content always is returned as translation
+         * object, which could be also formated wich numbers, dates and
+         * much more.
+         * 
+         * @param {string} phrase - Phrase to translate. 
+         * @returns {translation} - Translated phrase.
+         */
+        translate(phrase) {
+          if (typeof phrase !== "string") {
+            throw new TypeError("Phrase to translate must be an string.");
+          }
+          if (this.#is_nested(phrase)) {
+            return this.#translate_nested(phrase);
+          }
+          return this.#translate_flat(phrase);
+        }
+        /**
+         * This translate given phrase. When phrase is in the nested object 
+         * notation, then try to find phrase in objects. When not, try to find
+         * phrase in the flat phrases. When could not find phrase, then return 
+         * not translated phrase. Content always is returned as translation
+         * object, which could be also formated wich numbers, dates and
+         * much more.
+         * 
+         * @param {string} phrase - Phrase to translate. 
+         * @returns {translation} - Translated phrase.
+         */
+        get(phrase) {
+          return this.translate(phrase);
+        }
+        /**
+         * Check that phrase is nested or not.
+         * 
+         * @param {string} phrase - Phrase to check that is nested
+         * @returns {bool} - True when nested, false when not 
+         */
+        #is_nested(phrase) {
+          return phrase.indexOf(".") !== -1;
+        }
+        /**
+         * This translate object notated phrase.
+         * 
+         * @param {string} phrase - Phrase to translate.
+         * @returns {translation} - Translated phrase. 
+         */
+        #translate_nested(phrase) {
+          if (this.#objects === null) {
+            return this.#translate_flat(phrase);
+          }
+          const parts = phrase.trim().split(".");
+          let current = this.#objects;
+          for (const count in parts) {
+            const part = parts[count];
+            if (!(part in current)) {
+              return new translation(phrase, false);
+            }
+            current = current[part];
+          }
+          if (typeof current !== "string") {
+            return new translation(phrase, false);
+          }
+          return new translation(current, true);
+        }
+        /**
+         * This set phrasebook as default. That mean, it would add own translate
+         * function to global scope, and create _({string}) function in the global
+         * scope. It is useable to minify code with translations. It is not 
+         * avairable in the NodeJS, only in the browser.
+         * 
+         * @returns {phrasebook} - Object itself to chain loading.
+         */
+        set_as_default() {
+          const translate = (content) => {
+            return this.translate(content);
+          };
+          window._ = translate;
+          window.translate = translate;
+        }
+        /**
+         * This translate flat phrase.
+         * 
+         * @param {string} phrase - Phrase to translate. 
+         * @returns {translation} - Translated phrase.
+         */
+        #translate_flat(phrase) {
+          const prepared = _phrasebook.prepare(phrase);
+          const found = this.#phrases.has(prepared);
+          const translated = found ? this.#phrases.get(prepared) : phrase;
+          return new translation(translated, found);
+        }
+        /**
+         * This prepars phrase, that mean replece all spaces with "_", trim 
+         * and also replace all big letters with lowwer. 
+         * 
+         * @param {string} content - Phrase to preapre.
+         * @return {string} - Prepared phrase.
+         */
+        static prepare(content) {
+          if (typeof content !== "string") {
+            throw new TypeError("Content to prepare must be an string.");
+          }
+          return content.trim().replaceAll(" ", "_").replaceAll(".", "_").toLowerCase();
+        }
+      };
+      exports.phrasebook = phrasebook;
+    }
+  });
+
+  // source/loader.js
+  var require_loader = __commonJS({
+    "source/loader.js"(exports) {
+      var phrasebook = require_phrasebook().phrasebook;
+      var loader = class {
+        /**
+         * @var {string}
+         * This is location of the phrasebook on the server.
+         */
+        #path;
+        /**
+         * @var {bool}
+         * This is true, when must load local file, or false when fetch.
+         */
+        #local;
+        /**
+         * This create new loader of the phrasebook.
+         * 
+         * @param {string} path - Location of the phrasebook to fetch.
+         * @param {bool} local - False when must fetch from remote.
+         */
+        constructor(path, local = false) {
+          if (typeof path !== "string") {
+            throw new TypeError("Path of the file must be string.");
+          }
+          if (typeof local !== "boolean") {
+            throw new TypeError("Local must be bool variable.");
+          }
+          this.#path = path;
+          this.#local = local;
+        }
+        /**
+         * This load file from path given in the constructor, parse and return it.
+         * 
+         * @returns {phrasebook} - New phrasebook with content from JSON file.
+         */
+        async #load_remote() {
+          const request = await fetch(this.#path);
+          const response = await request.json();
+          return this.#parse(response);
+        }
+        /**
+         * This load file from path given in the constructor, parse and return it.
+         * 
+         * @returns {phrasebook} - New phrasebook with content from JSON file.
+         */
+        async #load_local() {
+          let fs = null;
+          if (fs === null) {
+            throw new Error("Could not use ndoe:fs in browser.");
+          }
+          const content = await fs.readFile(this.#path, { encoding: "utf8" });
+          const response = JSON.parse(content);
+          return this.#parse(response);
+        }
+        /**
+         * This load file from path given in the constructor, parse and return it.
+         * 
+         * @returns {phrasebook} - New phrasebook with content from JSON file.
+         */
+        load() {
+          if (this.#local) {
+            return this.#load_local();
+          }
+          return this.#load_remote();
+        }
+        /**
+         * This parse phrasebook. When phrasebook contain "phrases" or "objects" 
+         * keys, and also "objects" is not string, then parse it as nested file,
+         * in the other way parse it as flat.
+         * 
+         * @param {object} content - Fetched object with translations. 
+         * @returns {phrasebook} - Loaded phrasebook.
+         */
+        #parse(content) {
+          const has_objects = "objects" in content && typeof content["objects"] === "object";
+          const has_phrases = "phrases" in content && typeof content["phrases"] === "object";
+          const is_nested = has_objects || has_phrases;
+          if (is_nested) {
+            const phrases = has_phrases ? content["phrases"] : {};
+            const objects = has_objects ? content["objects"] : {};
+            return new phrasebook(
+              this.#parse_phrases(phrases),
+              objects
+            );
+          }
+          return new phrasebook(this.#parse_phrases(content));
+        }
+        /**
+         * This parse flat phrases object to map.
+         * 
+         * @param {object} content - Flat phrases object to pase.
+         * @returns {Map} - Phrases parsed as Map.
+         */
+        #parse_phrases(content) {
+          const phrases = /* @__PURE__ */ new Map();
+          Object.keys(content).forEach((phrase) => {
+            const name = phrasebook.prepare(phrase);
+            const translation = content[phrase];
+            phrases.set(name, translation);
+          });
+          return phrases;
+        }
+      };
+      exports.loader = loader;
+    }
+  });
+
+  // source/languages.js
+  var require_languages = __commonJS({
+    "source/languages.js"(exports) {
+      var loader = require_loader().loader;
+      var phrasebook = require_phrasebook().phrasebook;
+      var languages = class {
+        /**
+         * @var {string}  
+         * This represents path to directory where phrasebooks had been stored.
+         */
+        #path;
+        /**
+         * @var {Map}
+         * This store languages and its files on server. 
+         */
+        #libs;
+        /**
+         * @var {bool}
+         * This store that directory is in the local file system, or remote
+         * server. When true, resources would be loaded by node:fs. When 
+         * false, resources would be fetched.
+         */
+        #local;
+        /**
+         * This create new languages library. Next, languages could be added to
+         * the library by command, or by loading index file.
+         * 
+         * @throws {TypeError} - When parameters is not in correct format.
+         * 
+         * @param {string} path - Path to phrasebooks on the server or filesystem.
+         * @param {bool} local - True when phrasebooks dirs would be loaded by 
+         *                       node:fs module. False when would be fetch.
+         */
+        constructor(path, local = false) {
+          if (typeof path !== "string") {
+            throw new TypeError("Path to the phrasebooks must be string.");
+          }
+          if (typeof local !== "boolean") {
+            throw new TypeError("Local must be bool variable.");
+          }
+          this.#local = local;
+          this.#path = path;
+          this.#libs = /* @__PURE__ */ new Map();
+        }
+        /**
+         * This add new language to the library by name. Name must be in form
+         * like POSIX locale, like en_US, or pl_PL. That mean first two letter
+         * mest be ISO 639-1 and second two letters mst be in ISO 3166-1 alpha-2
+         * 2 letter country code format.
+         * 
+         * @see https://www.loc.gov/standards/iso639-2/php/code_list.php
+         * @see https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes
+         * @see https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
+         * @see https://en.wikipedia.org/wiki/Locale_(computer_software)
+         * 
+         * @throws {TypeError} - When tpes of the parameters is not correct.
+         * 
+         * @param {string} name - Name of the language, like "en_US".
+         * @param {string} file - Name of the file in the directory.
+         * @return {languages} - Instnace of this class to chain.
+         */
+        add(name, file) {
+          if (typeof name !== "string") {
+            throw new TypeError("Name of the language must be sting.");
+          }
+          if (typeof file !== "string") {
+            throw new TypeError("File on in the directory must be string.");
+          }
+          if (this.#libs.has(name)) {
+            console.error('Language "' + name + '" already loaded.');
+            console.error("It could not being loaded twice.");
+            return this;
+          }
+          if (!this.#valid_locale(name)) {
+            console.error('Language name "' + name + '" invalid formated.');
+            console.error("It could not being loaded.");
+            return this;
+          }
+          this.#libs.set(name, file);
+          return this;
+        }
+        /**
+         * This load all phrasebook given in the index file. Index must be
+         * JSON file, which contain one object. That object properties must be
+         * languages names in the notation like in add function. Valus of that
+         * properties musts being strings which contains names of the phrasebook
+         * files in the path directory.
+         * 
+         * @example ``` { "pl_PL": "polish.json", "en_US": "english.json" } ```
+         * 
+         * @see add
+         * 
+         * @param {string} index - Index file in the phrasebook directory.
+         * @return {languages} - New languages instance with loaded index.
+         */
+        async load(index) {
+          if (typeof index !== "string") {
+            throw new TypeError("Name of index file is not string.");
+          }
+          const response = await this.#load_index(index);
+          this.#libs.clear();
+          Object.keys(response).forEach((name) => {
+            if (typeof name !== "string") {
+              console.error("Name of the language must be string.");
+              console.error("Check languages index.");
+              console.error("Skipping it.");
+              return;
+            }
+            if (typeof response[name] !== "string") {
+              console.error("Name of phrasebook file must be string.");
+              console.error("Check languages index.");
+              console.error("Skipping it.");
+              return;
+            }
+            this.add(name, response[name]);
+          });
+          return this;
+        }
+        /**
+         * This load index object. That check, and when content must be loaded
+         * from local filesystem, it use node:fs, or when it must be fetched from
+         * remote, then use fetch API.
+         * 
+         * @param {string} index - Name of the index file in library. 
+         * @returns {object} - Loaded index file content. 
+         */
+        async #load_index(index) {
+          const path = this.#full_path(index);
+          if (this.#local) {
+            let fs = null;
+            if (fs === null) {
+              throw new Error("Could not use ndoe:fs in browser.");
+            }
+            return JSON.parse(
+              await fs.readFile(path, { encoding: "utf-8" })
+            );
+          }
+          const request = await fetch(path);
+          return await request.json();
+        }
+        /**
+         * This check that language exists in languages library.
+         * 
+         * @param {string} name - Name of the language to check.
+         * @return {bool} - True when language exists, false when not
+         */
+        has(name) {
+          return this.#libs.has(name);
+        }
+        /**
+         * This return all avairable languages.
+         * 
+         * @return {Array} - List of all avairable languages.
+         */
+        get avairable() {
+          const alls = new Array();
+          this.#libs.keys().forEach((name) => {
+            alls.push(name);
+          });
+          return alls;
+        }
+        /**
+         * @returns {string} - Default 
+         */
+        get default() {
+          const avairable = this.avairable;
+          if (avairable.length === 0) {
+            throw new Error("Languages list is empty. Can not load default.");
+          }
+          return avairable[0];
+        }
+        /**
+         * This load phrasebook with give name.
+         * 
+         * @throws {TypeError} - Param type is not correct.
+         * @throws {RangeError} - Language not exists in libs.
+         * 
+         * @param {string} name - Name of the language to load. 
+         * @returns {phrasebook} - Phrasebook loaded from the file.
+         */
+        select(name) {
+          if (typeof name !== "string") {
+            throw new TypeError("Name of the language must be string.");
+          }
+          if (!this.has(name)) {
+            DEBUG: throw new RangeError(
+              'Not found language "' + name + '".'
+            );
+            return new phrasebook(/* @__PURE__ */ new Map());
+          }
+          const file = this.#libs.get(name);
+          const path = this.#full_path(file);
+          return new loader(path, this.#local).load();
+        }
+        /**
+         * This return full path to the file.
+         * 
+         * @param {string} name - Name of the file to get its path
+         * @return {string} - Full path of the file
+         */
+        #full_path(name) {
+          let glue = "/";
+          if (this.#path[this.#path.length - 1] === glue) {
+            glue = "";
+          }
+          return this.#path + glue + name;
+        }
+        /**
+         * This check that format is valid POSIX like locale.
+         * 
+         * @param {string} name - Name to check format of.
+         * @return {bool} - True when format is valid, false when not.
+         */
+        #valid_locale(name) {
+          const splited = name.split("_");
+          if (splited.length !== 2) {
+            return false;
+          }
+          const first = splited[0];
+          const second = splited[1];
+          if (first.toLowerCase() !== first || first.length !== 2) {
+            return false;
+          }
+          if (second.toUpperCase() !== second || second.length !== 2) {
+            return false;
+          }
+          return true;
+        }
+      };
+      exports.languages = languages;
+    }
+  });
+
+  // source/selector.js
+  var require_selector = __commonJS({
+    "source/selector.js"(exports) {
+      var languages = require_languages().languages;
+      var selector = class {
+        /**
+         * @var {languages}
+         * This store languages container.
+         */
+        #languages;
+        /**
+         * @var {?HTMLElement}
+         * This store selector HTML container, or null when not created.
+         */
+        #container;
+        /**
+         * @var {HTMLElement}
+         * This store languages HTML selector object.
+         */
+        #selector;
+        /**
+         * @var {Array}
+         * This is array of callbacks which must being called after change.
+         */
+        #callbacks;
+        /**
+         * This create new selector object, from languages container.
+         * 
+         * @param {languages} languages - Languages container to work on.
+         */
+        constructor(languages2) {
+          this.#container = null;
+          this.#languages = languages2;
+          this.#callbacks = new Array();
+          this.#selector = this.#create_selector();
+        }
+        /**
+         * This check that selector is currently inserted anywhere.
+         * 
+         * @returns {bool} - True when selector is inserted anywhere.
+         */
+        get is_inserted() {
+          return this.#container !== null;
+        }
+        /**
+         * This inserts selector into given HTML element, or directly into
+         * document body, when any element is not specified. It returns 
+         * itselt, to call more functions inline.
+         * 
+         * @param {?HTMLElement} where - Place to insert selector, or null.
+         * @returns {selector} - This object to chain loading.
+         */
+        insert(where = null) {
+          if (this.is_inserted) {
+            return this;
+          }
+          if (where === null) {
+            where = document.querySelector("body");
+          }
+          this.#container = this.#create_container();
+          where.appendChild(this.#container);
+          return this;
+        }
+        /**
+         * This run all functions which was registered as onChange callback.
+         */
+        #on_change() {
+          this.#callbacks.forEach((count) => {
+            count(this.current);
+          });
+        }
+        /**
+         * This function remove selector from HTML element, if it is already
+         * inserted anywhere.
+         * 
+         * @returns {selector} - This object to chain loading.
+         */
+        remove() {
+          if (!this.is_inserted) {
+            return this;
+          }
+          this.#container.remove();
+          this.#container = null;
+          return this;
+        }
+        /**
+         * This create new container with selector inside.
+         * 
+         * @returns {HTMLElement} - New container with selector.
+         */
+        #create_container() {
+          const container = document.createElement("div");
+          container.classList.add(this.class_name);
+          container.appendChild(this.#selector);
+          return container;
+        }
+        /**
+         * This add new callback to selector. All callbacks would be called
+         * after language change. Callback would get one parameter, which is
+         * name of the location.
+         * 
+         * @param {CallableFunction} callback - Function to call on change.
+         * @returns 
+         */
+        add_listener(callback) {
+          this.#callbacks.push(callback);
+          return this;
+        }
+        /**
+         * This return HTML class name of selector container.
+         * 
+         * @returns {string} - HTML class name of selector container.
+         */
+        get class_name() {
+          return "cx-libtranslate-language-selector";
+        }
+        /**
+         * This create HTML option element from language name.
+         * 
+         * @param {string} location - Name of single language. 
+         * @returns {HTMLElement} - New option element.
+         */
+        #create_option(location) {
+          const name = location.split("_").pop();
+          const option = document.createElement("option");
+          option.innerText = name;
+          option.value = location;
+          return option;
+        }
+        /**
+         * This return current selected language name.
+         * 
+         * @returns {string} - Current selected language.
+         */
+        get current() {
+          return this.#selector.value;
+        }
+        /**
+         * This set current selected language for the selector.
+         * 
+         * @param {string} name - Name of the language to set.
+         * @returns {selector} - This to chain loading.
+         */
+        set_selection(name) {
+          if (!this.#languages.has(name)) {
+            DEBUG: throw new Error(
+              'Selector has not "' + name + '" language in the container.'
+            );
+          }
+          this.#selector.value = name;
+          return this;
+        }
+        /**
+         * This reload languages list in the selector. It could be used
+         * after change in the languages container.
+         * 
+         * @returns {selector} - Itself to chain loading.
+         */
+        reload() {
+          while (this.#selector.lastChild) {
+            this.#selector.lastChild.remove();
+          }
+          this.#languages.avairable.forEach((count) => {
+            this.#selector.appendChild(this.#create_option(count));
+          });
+          return this;
+        }
+        /**
+         * This create new HTML selector object, witch all languages
+         * from container inserted as options.
+         * 
+         * @returns {HTMLElement} - New selector object.
+         */
+        #create_selector() {
+          const selector2 = document.createElement("select");
+          selector2.addEventListener("change", () => {
+            this.#on_change();
+          });
+          this.#languages.avairable.forEach((count) => {
+            selector2.appendChild(this.#create_option(count));
+          });
+          return selector2;
+        }
+      };
+      exports.selector = selector;
+    }
+  });
+
+  // source/autotranslate.js
+  var require_autotranslate = __commonJS({
+    "source/autotranslate.js"(exports) {
+      var { phrasebook } = require_phrasebook();
+      var autotranslate = 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(phrasebook2 = null) {
+          this.#observer = null;
+          this.#phrasebook = phrasebook2;
+        }
+        /**
+         * 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 (_ === void 0) {
+            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((count2) => {
+              this.#update_single(count2);
+            });
+          });
+        }
+        /**
+         * 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;
+    }
+  });
+
+  // source/preferences.js
+  var require_preferences = __commonJS({
+    "source/preferences.js"(exports) {
+      var languages = require_languages().languages;
+      var phrasebook = require_phrasebook().phrasebook;
+      var selector = require_selector().selector;
+      var autotranslate = require_autotranslate().autotranslate;
+      var preferences = class {
+        /**
+         * @var {languages}
+         * This store loaded languages object.
+         */
+        #languages;
+        /**
+         * @var {?selector}
+         * This store selector for preferences.
+         */
+        #selector;
+        /**
+         * @var {?autotranslate}
+         * This store autotranslator, or null.
+         */
+        #autotranslate;
+        /**
+         * This create new language preferences manager from loaded languages
+         * object.
+         *
+         * @throws {Error} - When trying to use it in NodeJS.
+         * 
+         * @param {languages} target - loaded languages object.
+         */
+        constructor(target) {
+          this.#selector = null;
+          this.#autotranslate = null;
+          this.#languages = target;
+        }
+        /**
+         * This return name of language key in localStorage.
+         * 
+         * @returns {string} - Name of key in localStorage.
+         */
+        get #setting_name() {
+          return "cx_libtranslate_lang";
+        }
+        /**
+         * This return current saved language name, or null if any language
+         * is not selected yet.
+         * 
+         * @returns {?string} - Saved language or null.
+         */
+        get #state() {
+          return localStorage.getItem(this.#setting_name);
+        }
+        /**
+         * This return selector, which could being used in the ui.
+         * 
+         * @returns {selector} - New UI selector of the languages.
+         */
+        get selector() {
+          if (this.#selector !== null) {
+            return this.#selector;
+          }
+          this.#selector = new selector(this.#languages).set_selection(this.current).add_listener((target) => {
+            this.update(target);
+          }).add_listener(async (target) => {
+            if (this.#autotranslate === null) {
+              return;
+            }
+            this.#reload_autotranslate();
+          });
+          return this.#selector;
+        }
+        async #reload_autotranslate() {
+          const connected = this.#autotranslate.is_connected;
+          if (connected) {
+            this.#autotranslate.disconnect();
+          }
+          this.#autotranslate = null;
+          const created = await this.get_autotranslate();
+          if (connected) {
+            created.connect();
+          }
+        }
+        /**
+         * This load phrasebook for selected language, create autotranslate
+         * for it, and returns it. Autotranslate is cached in this object.
+         * 
+         * @returns {autotranslate} - Autotranslate for phrasebook.
+         */
+        async get_autotranslate() {
+          if (this.#autotranslate !== null) {
+            return this.#autotranslate;
+          }
+          const phrasebook2 = await this.load_choosen_phrasebook();
+          this.#autotranslate = new autotranslate(phrasebook2);
+          this.#autotranslate.update();
+          return this.#autotranslate;
+        }
+        /**
+         * This save new selected language name to localStorage, or remove it 
+         * from there if null given.
+         * 
+         * @param {?string} content - Name of selected language or null.
+         */
+        set #state(content) {
+          if (content === null) {
+            localStorage.removeItem(this.#setting_name);
+            return;
+          }
+          localStorage.setItem(this.#setting_name, content);
+        }
+        /**
+         * This return current language from localStorage. When any language is
+         * not loaded yet, then return default language. It also check that value
+         * value from localStorage, and if it not avairable in languages storage,
+         * then also return default language.
+         * 
+         * @return {string} - Name of the current language.
+         */
+        get current() {
+          const saved = this.#state;
+          if (saved === null) {
+            return this.#languages.default;
+          }
+          if (this.#languages.has(saved)) {
+            return saved;
+          }
+          return this.#languages.default;
+        }
+        /**
+         * This load phrasebook for current selected language.
+         * 
+         * @returns {phrasebook} - Phrasebook for current language.
+         */
+        async load_choosen_phrasebook() {
+          return await this.#languages.select(this.current);
+        }
+        /**
+         * This return loaded languages container.
+         * 
+         * @returns {languages} - Languages container.
+         */
+        get languages() {
+          return this.#languages;
+        }
+        /**
+         * This set new language for user. It also check that language exists 
+         * in the language container. When not exists, throws error.
+         * 
+         * @throws {Error} - When language not exists in container.
+         * 
+         * @param {string} name - New language to select.
+         * @returns {preferences} - This object to chain.
+         */
+        update(name) {
+          DEBUG: if (!this.#languages.has(name)) {
+            let error = 'Can not set language "' + name + '" ';
+            error += "because not exists in languages container.";
+            throw new Error(error);
+          }
+          this.#state = name;
+          return this;
+        }
+      };
+      exports.preferences = preferences;
+    }
+  });
+
+  // source/core.js
+  var require_core = __commonJS({
+    "source/core.js"(exports, module) {
+      if (typeof module !== "undefined" && module.exports) {
+        module.exports.phrasebook = require_phrasebook().phrasebook;
+        module.exports.loader = require_loader().loader;
+        module.exports.languages = require_languages().languages;
+        module.exports.loader = require_loader().loader;
+        module.exports.preferences = require_preferences().preferences;
+        module.exports.selector = require_selector().selector;
+        module.exports.autotranslate = require_autotranslate().autotranslate;
+      } else {
+        window.cx_libtranslate = {
+          phrasebook: require_phrasebook().phrasebook,
+          loader: require_loader().loader,
+          languages: require_languages().languages,
+          translation: require_translation().translation,
+          preferences: require_preferences().preferences,
+          selector: require_selector().selector,
+          autotranslate: require_autotranslate().autotranslate
+        };
+      }
+    }
+  });
+  return require_core();
+})();
+//# sourceMappingURL=cx-libtranslate.debug.js.map

File diff suppressed because it is too large
+ 3 - 0
static/assets/cx-libtranslate.debug.js.map


File diff suppressed because it is too large
+ 0 - 0
static/assets/cx-libtranslate.min.js


File diff suppressed because it is too large
+ 3 - 0
static/assets/cx-libtranslate.min.js.map


+ 72 - 0
static/assets/languages/english.json

@@ -0,0 +1,72 @@
+{
+    "do-you-want-to-remove-it": "Are you sure you want to remove the item?",
+    "you-try-to-remove-__name__": "Are you sure you want to remove #{ name }?",
+    "product-give-back": "Product return",
+    "processing": "Processing...",
+    "give-back-successfull": "Return completed successfully!",
+    "name-category": "Name",
+    "author-category": "Author",
+    "not-found-anything": "Unfortunately, nothing was found. Try searching for something else...",
+    "browse-our-products": "Find something for yourself!",
+    "you-must-confirm-it": "Confirm action",
+    "clear": "Clear",
+    "send": "Submit",
+    "product-editor": "Edit product",
+    "name-prompt": "Name:",
+    "name-sample": "Puzzle",
+    "description-prompt": "Description:",
+    "description-sample": "This is a very interesting puzzle for children and adults!",
+    "author-prompt": "Author:",
+    "author-sample": "Puzzle Company S.A.",
+    "barcode-prompt": "EAN code:",
+    "barcode-sample": "123456789012",
+    "stock-count-prompt": "Quantity in stock:",
+    "stock-count-sample": "10",
+    "change-product-image": "Change image.",
+    "updating-product-data": "Updating product data...",
+    "processing-image": "Processing image...",
+    "uploaded-successfull": "Upload completed successfully!",
+    "email-prompt": "Email address:",
+    "email-sample": "[email protected]",
+    "phone-number-prompt": "Phone number:",
+    "phone-number-sample": "+48 111-222-333",
+    "import-products-json": "Import product database from JSON file",
+    "loading-file": "Loading file...",
+    "parsing-file-to-dataset": "Parsing file to memory...",
+    "skipping-import-product-__barcode__": "Skipped product #{ barcode }.",
+    "searching-for-product-__barcode__": "Searching for data for #{ barcode }...",
+    "creating-product-__barcode__": "Creating product #{ barcode }...",
+    "can-not-add-product-__barcode__": "Cannot add #{ barcode }.",
+    "skipping-product-__barcode": "Skipped product #{ barcode }.",
+    "created-product-__barcode__": "Product #{ barcode } created successfully.",
+    "all-items-imported": "All products imported successfully!",
+    "not-all-items-imported": "Warning! Not all products were imported successfully!",
+    "product-rent": "Rent product",
+    "new-rent-added": "Rental completed successfully",
+    "product-not-avairable": "Product not available",
+    "this-product-is-not-avairable-yet": "This product is currently unavailable.",
+    "all-rents": "List of all rentals",
+    "loading": "Loading...",
+    "user-must-be-logged-in": "User must be logged in!",
+    "fail-when-request-__url__": "Error when loading \"#{ url }\"!",
+    "bad-response-not-contain-result": "Invalid server response, does not contain status.",
+    "login-window": "Log in",
+    "logged-in": "Logged in successfully!",
+    "can-not-login-check-nick-and-password": "Cannot log in! Check username and password...",
+    "nick-prompt": "Username:",
+    "nick-sample": "kotelek",
+    "password-prompt": "Password:",
+    "password-sample": "Kocia1234",
+    "incomplete-request-with-good-status": "Incomplete response with valid status.",
+    "add-product": "Add product",
+    "fill-barcode-first": "Fill in the EAN code first!",
+    "searching-in-the-web": "Searching for product online...",
+    "autocomplete-ready-check-results": "Autocomplete ready. Check results...",
+    "autocomplete": "Autocomplete",
+    "image-prompt": "Image: ",
+    "load-any-image-first": "Load product image first!",
+    "creating-new-product": "Creating new product...",
+    "created-new-product-success": "Product created successfully!",
+    "close": "Close",
+    "search": "Search..."
+}

+ 4 - 0
static/assets/languages/index.json

@@ -0,0 +1,4 @@
+{
+    "pl_PL": "polish.json",
+    "en_EN": "english.json"
+}

+ 72 - 0
static/assets/languages/polish.json

@@ -0,0 +1,72 @@
+{
+    "do-you-want-to-remove-it": "Czy napewno chcesz usunąć element?",
+    "you-try-to-remove-__name__": "Czy napewno chcesz usunąć #{ name }?",
+    "product-give-back": "Zwrot produktu",
+    "processing": "Procesowanie...",
+    "give-back-successfull": "Zwrot zakończony powodzeniem!",
+    "name-category": "Nazwa",
+    "author-category": "Autor",
+    "not-found-anything": "Niestety, nic nie odnaleziono. Spróbuj wyszukać coś innego...",
+    "browse-our-products": "Znajdź coś dla siebie!",
+    "you-must-confirm-it": "Zatwierdź akcję",
+    "clear": "Wyczyść",
+    "send": "Zatwierdź",
+    "product-editor": "Edytuj produkt",
+    "name-prompt": "Nazwa:",
+    "name-sample": "Puzzle",
+    "description-prompt": "Opis:",
+    "description-sample": "To bardzo ciekawa układanka, dla dzieci i dorosłych!",
+    "author-prompt": "Autor:",
+    "author-sample": "Układankowo S.A.",
+    "barcode-prompt": "Kod EAN:",
+    "barcode-sample": "123456789012",
+    "stock-count-prompt": "Posiadana ilość:",
+    "stock-count-sample": "10",
+    "change-product-image": "Zmień obraz.",
+    "updating-product-data": "Aktualizowanie danych produktu...",
+    "processing-image": "Procesowanie zdjęcia...",
+    "uploaded-successfull": "Przesyłanie zakończone powodzeniem!",
+    "email-prompt": "Adres email:",
+    "email-sample": "[email protected]",
+    "phone-number-prompt": "Numer telefonu:",
+    "phone-number-sample": "+48 111-222-333",
+    "import-products-json": "Importuj bazę danych produktów z pliku JSON",
+    "loading-file": "Ładowanie pliku...",
+    "parsing-file-to-dataset": "Parsowanie pliku do pamięci...",
+    "skipping-import-product-__barcode__": "Pominięto produkt #{ barcode }.",
+    "searching-for-product-__barcode__": "Wyszukiwanie danych dla #{ barcode }...",
+    "creating-product-__barcode__": "Tworzenie produktu #{ barcode }...",
+    "can-not-add-product-__barcode__": "Nie można dodać #{ barcode }.",
+    "skipping-product-__barcode": "Pominięto produkt #{ barcode }.",
+    "created-product-__barcode__": "Tworzenie produktu #{ barcode } zakończone powodzeniem.",
+    "all-items-imported": "Wszystkie produkty zaimportowano pomyślnie!",
+    "not-all-items-imported": "Uwaga! Nie wszystkie produkty zaimportowano pomyślnie!",
+    "product-rent": "Wypożycz produkt",
+    "new-rent-added": "Wypożyczenie zakończone sukcesem",
+    "product-not-avairable": "Produkt nie dostępny",
+    "this-product-is-not-avairable-yet": "Ten produkt nie jest aktualnie dostępny.",
+    "all-rents": "Lista wszystkich najemców",
+    "loading": "Ładowanie...",
+    "user-must-be-logged-in": "Użytkownik musi być zalogowany!",
+    "fail-when-request-__url__": "Błąd podczas ładowania \"#{ url }\"!",
+    "bad-response-not-contain-result": "Błędna odpowiedź serwera, nie zawiera statusu.",
+    "login-window": "Zaloguj się",
+    "logged-in": "Zalogowano pomyślnie!",
+    "can-not-login-check-nick-and-password": "Nie można zalogować! Sprawdź nick i hasło...",
+    "nick-prompt": "Nick:",
+    "nick-sample": "kotelek",
+    "password-prompt": "Hasło:",
+    "password-sample": "Kocia1234",
+    "incomplete-request-with-good-status": "Niekompletna odpowiedź o prawidłowym statusie.",
+    "add-product": "Dodaj produkt",
+    "fill-barcode-first": "Najpierw wypełnij kod EAN!",
+    "searching-in-the-web": "Wyszukiwanie produktu w sieci...",
+    "autocomplete-ready-check-results": "Podpowiedzi gotowe. Sprawdź rezultaty...",
+    "autocomplete": "Uzupełnij automatycznie",
+    "image-prompt": "Obraz: ",
+    "load-any-image-first": "Najpierw załaduj obraz produktu!",
+    "creating-new-product": "Tworzenie nowego produktu...",
+    "created-new-product-success": "Produkt utworzono pomyślnie!",
+    "close": "Zamknij",
+    "search": "Wyszukaj..."
+}

+ 110 - 85
static/bundle/app.js

@@ -337,7 +337,7 @@
       throw new TypeError("It must be overwriten.");
     }
     get _title() {
-      return "You must confirm it.";
+      return _("you-must-confirm-it");
     }
     _action() {
       throw new TypeError("It must be overwriten.");
@@ -396,7 +396,7 @@
       const cancel_button = document.createElement("button");
       cancel_button.classList.add("cancel");
       cancel_button.classList.add("material-icons");
-      cancel_button.innerText = "clear";
+      cancel_button.innerText = _("clear");
       buttons.appendChild(cancel_button);
       cancel_button.addEventListener("click", () => {
         this.hide();
@@ -405,7 +405,7 @@
         const confirm_button = document.createElement("button");
         confirm_button.classList.add("confirm");
         confirm_button.classList.add("material-icons");
-        confirm_button.innerText = "send";
+        confirm_button.innerText = _("send");
         buttons.appendChild(confirm_button);
         confirm_button.addEventListener("click", () => {
           if (this.#action === false) {
@@ -440,7 +440,7 @@
       if (manager.logged_in) {
         return manager.apikey;
       }
-      throw new Error("User must be logged in.");
+      throw new Error(_("user-must-be-logged-in"));
     }
     get method() {
       throw new TypeError("It must be overwrite.");
@@ -476,11 +476,13 @@
     async connect() {
       const request2 = await fetch(this.url, this.settings);
       if (!request2.ok) {
-        throw new Error('Fail when requested: "' + this.url + '".');
+        throw new Error(_("fail-when-request-__url__").format({
+          url: this.url
+        }));
       }
       const response = await request2.json();
       if (!("result" in response)) {
-        throw new Error("Bad response, not contain result.");
+        throw new Error(_("bad-response-not-contain-result"));
       }
       return new this._response(response);
     }
@@ -612,8 +614,8 @@
     }
     get categories() {
       return {
-        "name": "Name",
-        "author": "Author"
+        "name": _("name-category"),
+        "author": _("author-category")
       };
     }
     #selector_complete() {
@@ -653,9 +655,9 @@
     }
     #insert(list) {
       if (list.length === 0) {
-        this.#result_title = "Not found anything.";
+        this.#result_title = _("not-found-anything");
       } else {
-        this.#result_title = "Browse our products!";
+        this.#result_title = _("browse-our-products");
       }
       this.#manager.clean().add_list(list).update();
     }
@@ -672,11 +674,12 @@
       this.#target = target;
     }
     get _title() {
-      return "Do you want remove it?";
+      return _("do-you-want-to-remove-it");
     }
     get _description() {
-      let content = "You try to remove " + this.#target.name + ". ";
-      content += "You can not restore it, when confirm.";
+      let content = _("you-try-to-remove-__name__").format({
+        name: this.#target.name
+      });
       return content;
     }
     async _action() {
@@ -945,39 +948,39 @@
       return this.#target;
     }
     get _name() {
-      return "Product editor";
+      return _("product-editor");
     }
     _build_form() {
       this.#loaded_image = null;
       this.#loaded_image_type = null;
       this.#name = this._create_input(
         "name",
-        "Name:",
-        "Sample...",
+        _("name-prompt"),
+        _("name-sample"),
         (input) => {
           input.value = this.#target.name;
         }
       );
       this.#description = this._create_input(
         "description",
-        "Description:",
-        "This is sample product...",
+        _("description-prompt"),
+        _("description-sample"),
         (input) => {
           input.value = this.#target.description;
         }
       );
       this.#author = this._create_input(
         "author",
-        "Author:",
-        "Jack Black",
+        _("author-prompt"),
+        _("author-sample"),
         (input) => {
           input.value = this.#target.author;
         }
       );
       this.#barcode = this._create_input(
         "barcode",
-        "Barcode (EAN):",
-        "123456789012...",
+        _("barcode-prompt"),
+        _("barcode-sample"),
         (input) => {
           input.type = "number";
           input.value = this.#target.barcode;
@@ -985,8 +988,8 @@
       );
       this.#stock_count = this._create_input(
         "stock_count",
-        "Stock count:",
-        "10...",
+        _("stock-count-prompt"),
+        _("stock-count-sample"),
         (input) => {
           input.type = "number";
           input.value = this.#target.stock_count;
@@ -994,7 +997,7 @@
       );
       this._create_input(
         "image",
-        "Change product image:",
+        _("change-product-image"),
         "",
         (input) => {
           this.#image = input;
@@ -1064,11 +1067,11 @@
     }
     async _process() {
       try {
-        this._info = "Uploading...";
+        this._info = _("updating-product-data");
         await this.#submit();
-        this._info = "Processing image...";
+        this._info = _("processing-image");
         await this.#image_submit();
-        this._success = "Updated success!";
+        this._success = _("uploaded-successfull");
         searcher.reload();
         setTimeout(() => {
           this.hide();
@@ -1100,16 +1103,16 @@
     _build_form() {
       this.#email = this._create_input(
         "email",
-        "E-mail:",
-        "[email protected]",
+        _("email-prompt"),
+        _("email-sample"),
         (input) => {
           input.type = "email";
         }
       );
       this.#phone = this._create_input(
         "phone",
-        "Phone number:",
-        "+1 123-456-789",
+        _("phone-number-prompt"),
+        _("phone-number-sample"),
         (input) => {
           input.type = "tel";
           const add_prefix = () => {
@@ -1228,18 +1231,18 @@
   // application/scripts/product_rent.js
   var product_rent = class extends rents_screen {
     get _name() {
-      return "Product rent";
+      return _("product-rent");
     }
     async _process() {
       try {
-        this._info = "Processing...";
+        this._info = _("processing");
         const target = new reservation_factory().email(this._email).phone_number(this._phone).product(this._target).result();
         const request2 = new product_rent_request(target);
         const response = await request2.connect();
         if (!response.result) {
           throw new Error(response.cause);
         }
-        this._success = "New rent added.";
+        this._success = _("new-rent-added");
         searcher.reload();
         setTimeout(() => {
           this.hide();
@@ -1276,18 +1279,18 @@
   // application/scripts/product_give_back.js
   var product_give_back = class extends rents_screen {
     get _name() {
-      return "Product give back";
+      return _("product-give-back");
     }
     async _process() {
       try {
-        this._info = "Processing...";
+        this._info = _("processing");
         const target = new reservation_factory().email(this._email).phone_number(this._phone).product(this._target).result();
         const request2 = new product_give_back_request(target);
         const response = await request2.connect();
         if (!response.result) {
           throw new Error(response.cause);
         }
-        this._success = "Success!";
+        this._success = _("give-back-successfull");
         searcher.reload();
         setTimeout(() => {
           this.hide();
@@ -1350,7 +1353,7 @@
       this.#target = target;
     }
     get _name() {
-      return "All rents";
+      return _("all-rents");
     }
     get _has_submit() {
       return false;
@@ -1390,11 +1393,11 @@
       button.innerText = "save_alt";
       button.addEventListener("click", async () => {
         try {
-          this._info = "Processing...";
+          this._info = _("processing");
           const request2 = new product_give_back_request(target);
           const response = await request2.connect();
           if (!response.result) {
-            throw new Error(response.cause);
+            throw new Error(_(response.cause));
           }
           this._refresh();
           searcher.reload();
@@ -1409,7 +1412,7 @@
     }
     async _build_form() {
       try {
-        this._info = "Loading...";
+        this._info = _("loading");
         const request2 = new product_reservations_request(this.#target);
         const response = await request2.connect();
         const list = document.createElement("div");
@@ -1427,7 +1430,7 @@
         });
         this._append_child(list);
         if (empty) {
-          this._success = "Not found any reservations.";
+          this._success = "not-found-any-reservations";
         } else {
           this._clear_results();
         }
@@ -1440,10 +1443,10 @@
   // application/scripts/product_not_avairable.js
   var product_not_avairable = class extends confirm_action {
     get _title() {
-      return "Error";
+      return _("product-not-avairable");
     }
     get _description() {
-      return "This product is not avairable. Anybody can not rent it.";
+      return _("this-product-is-not-avairable-yet");
     }
     get _info() {
       return true;
@@ -1626,13 +1629,13 @@
       });
     }
     get _name() {
-      return "Login";
+      return _("login-window");
     }
     async _process() {
       try {
-        this._info = "Processing...";
+        this._info = _("processing");
         await this.#login();
-        this._success = "Logged in!";
+        this._success = _("logged-in");
         setTimeout(() => {
           location.reload();
         }, 250);
@@ -1649,18 +1652,18 @@
       if (result) {
         return;
       }
-      throw new Error("Can not login. Check nick and password.");
+      throw new Error(_("can-not-login-check-nick-and-password"));
     }
     _build_form() {
       this.#nick = this._create_input(
         "nick",
-        "Nick:",
-        "Sample..."
+        _("nick-prompt"),
+        _("nick-sample")
       );
       this.#password = this._create_input(
         "password",
-        "Password:",
-        "ABCDEFGH",
+        _("password-prompt"),
+        _("password-sample"),
         (input) => {
           input.type = "password";
         }
@@ -1747,15 +1750,15 @@
     #loaded_image;
     #image_preview;
     get _name() {
-      return "Add product";
+      return _("add-product");
     }
     async #autocomplete() {
       const barcode = this.#barcode();
       if (barcode.length === 0) {
-        this._info = "Fill barcode first.";
+        this._info = _("fill-barcode-first");
         return;
       }
-      this._info = "Searching in the web...";
+      this._info = _("searching-in-the-web");
       try {
         const request2 = new autocomplete_request(barcode);
         const response = await request2.connect();
@@ -1770,7 +1773,7 @@
         this.#loaded_image = product2.image;
         this.#loaded_image_type = product2.image_type;
         this.#update_image_preview();
-        this._info = "Ready. Check results.";
+        this._info = _("autocomplete-ready-check-results");
       } catch (error) {
         this._error = new String(error);
       }
@@ -1789,7 +1792,7 @@
       button.appendChild(icon);
       const text = document.createElement("span");
       text.classList.add("text");
-      text.innerText = "Autocomplete";
+      text.innerText = _("autocomplete");
       button.appendChild(text);
       return button;
     }
@@ -1798,38 +1801,38 @@
       this.#loaded_image_type = null;
       this.#name = this._create_input(
         "name",
-        "Name:",
-        "Sample..."
+        _("name-prompt"),
+        _("name-sample")
       );
       this.#description = this._create_input(
         "description",
-        "Description:",
-        "This is sample product..."
+        _("description-prompt"),
+        _("description-sample")
       );
       this.#author = this._create_input(
         "author",
-        "Author:",
-        "Jack Black"
+        _("author-prompt"),
+        _("author-sample")
       );
       this.#barcode = this._create_input(
         "barcode",
-        "Barcode (EAN):",
-        "123456789012...",
+        _("barcode-prompt"),
+        _("barcode-sample"),
         (input) => {
           input.type = "number";
         }
       );
       this.#stock_count = this._create_input(
         "stock_count",
-        "Stock count:",
-        "10...",
+        _("stock-count-prompt"),
+        _("stock-count-sample"),
         (input) => {
           input.type = "number";
         }
       );
       this._create_input(
         "image",
-        "Product image:",
+        _("image-prompt"),
         "",
         (input) => {
           this.#image = input;
@@ -1871,7 +1874,7 @@
     }
     get #ready_image() {
       if (this.#loaded_image === null) {
-        throw new Error("Loady any image first.");
+        throw new Error(_("load-any-image-first"));
       }
       return this.#loaded_image;
     }
@@ -1890,9 +1893,9 @@
     }
     async _process() {
       try {
-        this._info = "Uploading...";
+        this._info = _("creating-new-product");
         await this.#submit();
-        this._success = "Created success!";
+        this._success = _("created-new-product-success");
         searcher.reload();
         setTimeout(() => {
           this.hide();
@@ -1996,7 +1999,7 @@
       this.#product = null;
       if (this.result) {
         if (!("product" in target)) {
-          throw new Error("Incomplete response with good status.");
+          throw new Error(_("incomplete-request-with-good-status"));
         }
         this.#product = new product(target.product);
       }
@@ -2236,7 +2239,7 @@
     #file;
     #content;
     get _name() {
-      return "Import products JSON";
+      return _("import-products-json");
     }
     _build_form() {
       this._create_input("file", "Database:", "", (input) => {
@@ -2247,7 +2250,7 @@
     }
     async #load_file() {
       if (this.#file.files.length === 0) {
-        throw new Error("Select JSON products database first.");
+        throw new Error("select-products-json-database-first");
       }
       const file = this.#file.files.item(0);
       const text = await file.text();
@@ -2255,36 +2258,48 @@
     }
     async _process() {
       try {
-        this._info = "Loading file...";
+        this._info = _("loading-file");
         this.#content = await this.#load_file();
-        this._info = "Parsing file to dataset...";
+        this._info = _("parsing-file-to-dataset");
         const result = new import_log();
         const dataset = new database(this.#content).on_skip((fail) => {
-          this._info = "Skipping " + fail.product.barcode + "...";
+          this._info = _("skipping-import-product-__barcode__").format({
+            barcode: fail.product.barcode
+          });
           result.skip(fail);
         }).process().results();
         const loop = new import_loop(dataset).on_autocomplete((target) => {
-          this._info = "Searching for " + target.barcode + "...";
+          this._info = _("searching-for-product-__barcode__").format({
+            barcode: target.barcode
+          });
         }).on_create((target) => {
-          this._info = "Creating " + target.barcode + "...";
+          this._info = _("creating-product-__barcode__").format({
+            barcode: target.barcode
+          });
         }).on_single_fail((target) => {
-          this._info = "Can not add " + target.product.barcode + "...";
+          this._info = _("can-not-add-product-__barcode__").format({
+            barcode: target.product.barcode
+          });
           result.fail(target);
         }).on_skip((target) => {
-          this._info = "Skipping " + target.product.barcode + "...";
+          this._info = _("skipping-product-__barcode").format({
+            barcode: target.product.barcode
+          });
           result.skip(target);
         }).on_single_success((target) => {
-          this._info = "Created " + target.barcode + " success.";
+          this._info = _("created-product-__barcode__").format({
+            barcode: target.barcode
+          });
         }).finally(() => {
           searcher.reload();
           const log = new downloader().content(result.content()).type("text/plain").encode("utf-8").name("import-json.log").process();
           if (result.length === 0) {
-            this._success = "All items imported.";
+            this._success = _("all-items-imported");
             setTimeout(() => {
               this.hide();
             });
           } else {
-            this._success = "Not all items imported...";
+            this._success = _("not-all-items-imported");
           }
         }).process();
       } catch (error) {
@@ -2457,6 +2472,16 @@
 
   // application/scripts/core.js
   document.addEventListener("DOMContentLoaded", async () => {
+    const languages = new cx_libtranslate.languages("app/assets/languages");
+    await languages.load("index.json");
+    const preferences = new cx_libtranslate.preferences(languages);
+    preferences.selector.insert().add_listener(() => {
+      location.reload();
+    });
+    const phrasebook = await preferences.load_choosen_phrasebook();
+    phrasebook.set_as_default();
+    const autotranslate = await preferences.get_autotranslate();
+    autotranslate.connect();
     const top_bar_spacing = new height_equaler(
       document.querySelector(".top-bar"),
       document.querySelector(".top-bar-spacing")

File diff suppressed because it is too large
+ 0 - 0
static/bundle/theme.css


+ 2 - 1
static/core.html

@@ -15,6 +15,7 @@
         <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
 
         <script src="app/bundle/app.js" type="text/javascript"></script>
+        <script src="app/assets/cx-libtranslate.debug.js" type="text/javascript"></script>
         <link rel="stylesheet" type="text/css" href="app/bundle/theme.css">
     </head>
 
@@ -24,7 +25,7 @@
                 <div class="top-bar">
                     <div class="left">
                         <form class="search">
-                            <input type="text" name="content" placeholder="Search" class="search-content">
+                            <input type="text" name="content" phrase="search" class="search-content translate">
                             <select name="search-by"></select>
                             <button type="submit" name="submit" class="search-submit material-icons">search</button>
                         </form>

Some files were not shown because too many files changed in this diff