소스 검색

Continue working on project.

Cixo Develop 5 달 전
부모
커밋
449a6e70b9
12개의 변경된 파일606개의 추가작업 그리고 46개의 파일을 삭제
  1. 0 1
      dist/cx-libtranslate.min.js
  2. 0 1
      dist/cx-libtranslate.min.js.map
  3. 2 2
      package.json
  4. 12 2
      source/core.js
  5. 218 0
      source/languages.js
  6. 191 0
      source/loader.js
  7. 115 28
      source/phrasebook.js
  8. 50 0
      source/translation.js
  9. 0 12
      test/01-phrasebook.js
  10. 12 0
      test/backend.js
  11. 6 0
      test/flat.json
  12. 0 0
      test/frontend.html

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 1
dist/cx-libtranslate.min.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 1
dist/cx-libtranslate.min.js.map


+ 2 - 2
package.json

@@ -4,8 +4,8 @@
   "description": "This is simple library for translating JS app.",
   "main": "source/core.js",
   "scripts": {
-    "test-phrasebook": "node test/01-phrasebook.js",
-    "build": "esbuild source/core.js --bundle --outfile=dist/cx-libtranslate.min.js --minify --sourcemap --global-name=cx_libtranslate"
+    "test": "node test/backend.js",
+    "build": "esbuild source/core.js --drop-labels=NODE --bundle --outfile=dist/cx-libtranslate.min.js --minify --sourcemap --global-name=cx_libtranslate"
   },
   "repository": {
     "type": "git",

+ 12 - 2
source/core.js

@@ -1,7 +1,17 @@
 if (typeof(module) !== "undefined" && module.exports) {
+    /* Load for NodeJS */
     module.exports.phrasebook = require("./phrasebook.js").phrasebook;
+    module.exports.loader = require("./loader.js").loader;
+    module.exports.languages = require("./languages.js").languages;
+    module.exports.loader = require("./loader.js").loader;
 } else {
+    /* Load for web browser */
     window.cx_libtranslate = {
-        phrasebook: require("./phrasebook.js").phrasebook
-    }
+        phrasebook: require("./phrasebook.js").phrasebook,
+        loader: require("./loader.js").loader,
+        languages: require("./languages.js").languages,
+        translation: require("./translation.js").translation
+    };
+
+    window.languages = window.cx_libtranslate.languages;
 }

+ 218 - 0
source/languages.js

@@ -0,0 +1,218 @@
+const loader = require("./loader.js").loader;
+const phrasebook = require("./phrasebook.js").phrasebook;
+
+/**
+ * This class represents languages library. This store its location on the 
+ * server, and create loaders for them. It could also load langiage 
+ * library from json file.
+ */
+class languages {
+    /**
+     * @var {string}  
+     * This represents path to directory where phrasebooks had been stored.
+     */
+    #path;
+
+    /**
+     * @var {Map}
+     * This store languages and its files on server. 
+     */
+    #libs;
+
+    /**
+     * 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.
+     */
+    constructor(path) {
+        if (typeof(path) !== "string") {
+            throw new TypeError("Path to the phrasebooks must be string.");
+        }
+
+        this.#path = path;
+        this.#libs = 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 request = await fetch(this.#full_path(index));
+        const response = await request.json();
+
+        const result = new languages(this.#path);
+
+        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.");
+            }
+
+            result.add(name, response[name]);
+        });
+
+        return result;
+    }
+
+    /**
+     * 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;
+    }
+
+    /**
+     * 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)) {
+            throw new RangeError("Not found language \"" + name + "\".");
+        }
+        
+        const file = this.#libs.get(name);
+        const path = this.#full_path(file);
+
+        return new loader(path).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;

+ 191 - 0
source/loader.js

@@ -0,0 +1,191 @@
+const phrasebook = require("./phrasebook.js").phrasebook;
+
+/**
+ * This class fetch and prepare phrasebook from JSON file. That JSON file 
+ * could be simple flat JSON, which contain string phrases as keys, and 
+ * its translatedequivalents as values. Also avalirable is format, where
+ * phrases not being used, but JSON contain nested objects. End value must
+ * being translated string, but then notation like "a.b.c" could being used.
+ * 
+ * @example simple flat phrasebook ```JSON
+ * JSON:
+ * {
+ *      "phrase a": "Translated phrase a",
+ *      "phrase b": "Translated phrase b"
+ * }
+ * 
+ * Results:
+ *  "phrase a" -> "Translated phrase a",
+ *  "phrase b" -> "Translated phrase b"
+ * ```
+ * 
+ * @example simple nested phrasebook ```JSON
+ * JSON:
+ * {
+ *      "phrases": {
+ *          "phrase a": "Translated phrase a"
+ *      },
+ *      "objects": {
+ *          "a": {
+ *              "b": {
+ *                  "c": "Second object notation"
+ *              }
+ *          }  
+ *      }
+ * }
+ * 
+ * Results:
+ *  "phrase a" -> "Translated phrase a",
+ *  "a.b.c" -> "Second object notation" 
+ * ```      
+ */
+class loader {
+    /**
+     * @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;
+        NODE: fs =require('node:fs/promises');
+
+        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 keys = Object.keys(content);
+
+        if ("objects" in keys && typeof(keys["objects"]) === "object") {
+            return this.#parse_nested(content);
+        }
+
+        return this.#parse_standard(content);
+    }
+
+    /**
+     * This function parse nested phrasebook.
+     * 
+     * @param {object} content - Object fetched from server.
+     * @returns {phrasebook} - Parsed nested phrasebook.
+     */
+    #parse_nested(content) {
+        const keys = Object.keys(content);
+        let phrases = {};
+        let objects = {};
+
+        if ("phrases" in keys) {
+            phrases = content["phrases"];
+        }
+
+        if ("objects" in keys) {
+            objects = content["objects"];
+        }
+
+        return new phrasebook(
+            this.#parse_phrases(phrases), 
+            Object.freeze(objects)
+        );
+    }
+
+    /**
+     * 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 = new Map();
+
+        Object.keys(content).forEach(phrase => {
+            const name = phrasebook.prepare(phrase);
+            const translation = content[phrase];
+
+            phrases.set(name, translation);
+        });
+
+        return phrases;
+    }
+
+    /**
+     * This parse phrasebook file as simple flat phrasebook.
+     * 
+     * @param {object} content - Fetched object which contain flat phrases.
+     * @returns {phrasebook} - Parsed flat phrasebook.
+     */
+    #parse_standard(content) {
+        return new phrasebook(this.#parse_phrases(content));
+    }
+}
+
+exports.loader = loader;

+ 115 - 28
source/phrasebook.js

@@ -1,56 +1,143 @@
+const translation = require("./translation.js").translation;
+
 /**
  * This class repesents phrasebook, which is something like dictionary,
  * but not for words, for all phrases in app. It give functions which 
  * could find and translates phrases.
  */
 class phrasebook {
+    
+    /**
+     * @var {Map}
+     * This store phrases in flat notation.
+     */
+    #phrases;
 
-    #collection;
+    /**
+     * @var {?object}
+     * This store object for nested object notation.
+     */
+    #objects;
 
     /**
-     * This create new phrasebook, which given collection of phrqses.
-     *
-     * @param {Map} collection - Collection of phrases to translate from.
+     * 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(collection) {
-        if (!(collection instanceof Map)) {
-            throw new Error("Phrasebook must be indialized with map.");
+    constructor(phrases, objects = null) {
+        if (!(phrases instanceof Map)) {
+            throw new TypeError("Phrases must an map.");
         }
 
-        this.#collection = collection;
+        if (objects !== null && typeof(objects) !== "object") {
+            throw new TypeError("Objects must be null or object.");
+        }
+    
+        this.#phrases = phrases;
+        this.#objects = objects;
     }
 
     /**
-     * This function translate phrase to lang which coresponds to the 
-     * phrasebook. It try to find given phrase in the phrasebook, and 
-     * when find it, return that. In the other way, when phrase could
-     * not being fount, write warning to the debug console, and return
-     * phrase in given not translated form. When phrase in phrasebook
-     * is not string, then also return not translated phrase and 
-     * report it into developer console.
+     * 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.
-     * @return {string} Translated phrase.
+     * @param {string} phrase - Phrase to translate. 
+     * @returns {translation} - Translated phrase.
+     */
+    translate(phrase) {
+        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) {
-        if (typeof(phrase) !== "string") {
-            throw new Error("Phrase to translate must be string.");
+        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);
         }
 
-        if (this.#collection.has(phrase)) {
-            const translated = this.#collection.get(phrase);
-            
-            if (typeof(translated) !== "string") {
-                console.error("Result for \"" + phrase + "\" is not string.");
-                return phrase;
+        const parts = phrase.trim().split(".");
+        let current = this.#objects;
+
+        for (const part in parts) {
+            if (!(part in current)) {
+                return new translation(phrase, false);
             }
 
-            return translated;
         }
 
-        console.warn("Could not find \"" + phrase + "\" in phrasebook.");
+        if (typeof(current) !== "string") {
+            return new translation(phrase, false);
+        }
+
+        return new translation(current, true);
+    }
 
-        return phrase;
+    /**
+     * 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) {
+        return content
+        .trim()
+        .replaceAll(" ", "_")
+        .replaceAll(".", "_")
+        .toLowerCase();
     }
 }
 

+ 50 - 0
source/translation.js

@@ -0,0 +1,50 @@
+class translation {
+    #content;
+    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;
+    }
+
+    /**
+     * 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;
+    }
+}
+
+exports.translation = translation;

+ 0 - 12
test/01-phrasebook.js

@@ -1,12 +0,0 @@
-const dict = new Map();
-dict.set("simple-phrase.", "Prosta fraza.");
-dict.set("other", "inne");
-dict.set("not-str", 10);
-
-const libtranslate = require("../source/core.js");
-const phrase = new libtranslate.phrasebook(dict);
-
-console.log(phrase.get("other"));
-console.log(phrase.get("simple-phrase."));
-console.log(phrase.get("not-str"));
-console.log(phrase.get("Not set"));

+ 12 - 0
test/backend.js

@@ -0,0 +1,12 @@
+const test_all = async () => {
+    const libtranslate = require("../source/core.js");
+    
+    const flat = await new libtranslate.
+    loader("test/flat.json", true).
+    load();
+
+    console.log(flat.get("simple").text);
+    console.log(flat.get("this is simple phrase.").text);
+}
+
+test_all();

+ 6 - 0
test/flat.json

@@ -0,0 +1,6 @@
+{
+    "simple": "prosta",
+    "this is simple phrase.": "To jest prosta fraza.",
+    "other phrase": "Inna fraza.",
+    "And much more...": "I o wiele więcej..."
+}

+ 0 - 0
test/02-web.html → test/frontend.html


이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.