Эх сурвалжийг харах

Final ready to first release.

Cixo Develop 5 сар өмнө
parent
commit
487beda8fa

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
dist/cx-libtranslate.min.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 3
dist/cx-libtranslate.min.js.map


+ 4 - 1
package.json

@@ -5,7 +5,10 @@
   "main": "source/core.js",
   "scripts": {
     "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"
+    "build": "npm run build-min && npm run build-full && npm run build-debug",
+    "build-min": "esbuild source/core.js --drop-labels=NODE,DEBUG --bundle --outfile=dist/cx-libtranslate.min.js --minify --sourcemap --global-name=cx_libtranslate",
+    "build-full": "esbuild source/core.js --drop-labels=NODE,DEBUG --bundle --outfile=dist/cx-libtranslate.full.js --sourcemap --global-name=cx_libtranslate",
+    "build-debug": "esbuild source/core.js --drop-labels=NODE --bundle --outfile=dist/cx-libtranslate.debug.js --sourcemap --global-name=cx_libtranslate"
   },
   "repository": {
     "type": "git",

+ 54 - 8
source/languages.js

@@ -19,6 +19,14 @@ class languages {
      */
     #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.
@@ -26,12 +34,19 @@ class languages {
      * @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) {
+    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 = new Map();
     }
@@ -97,11 +112,9 @@ class languages {
             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);
-
+        const response = await this.#load_index(index);
+        const result = new languages(this.#path, this.#local);
+        
         Object.keys(response).forEach(name => {
             if (typeof(name) !== "string") {
                 console.error("Name of the language must be string.");
@@ -114,6 +127,7 @@ class languages {
                 console.error("Name of phrasebook file must be string.");
                 console.error("Check languages index.");
                 console.error("Skipping it.");
+                return;
             }
 
             result.add(name, response[name]);
@@ -122,6 +136,34 @@ class languages {
         return result;
     }
 
+    /**
+     * 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;
+            NODE: fs = require("node:fs/promises"); 
+           
+            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.
      * 
@@ -162,13 +204,17 @@ class languages {
         }
 
         if (!this.has(name)) {
-            throw new RangeError("Not found language \"" + name + "\".");
+            DEBUG: throw new RangeError(
+                "Not found language \"" + name + "\"."
+            );
+
+            return new phrasebook(new Map());
         }
         
         const file = this.#libs.get(name);
         const path = this.#full_path(file);
 
-        return new loader(path).load();
+        return new loader(path, this.#local).load();
     }
 
     /**

+ 18 - 37
source/loader.js

@@ -90,7 +90,7 @@ class loader {
      */
     async #load_local() {
         let fs = null;
-        NODE: fs =require('node:fs/promises');
+        NODE: fs = require("node:fs/promises");
 
         if (fs === null) {
             throw new Error("Could not use ndoe:fs in browser.");
@@ -124,38 +124,29 @@ class loader {
      * @returns {phrasebook} - Loaded phrasebook.
      */
     #parse(content) {
-        const keys = Object.keys(content);
+        const has_objects = (
+            "objects" in content &&
+            typeof(content["objects"]) === "object"
+        );
 
-        if ("objects" in keys && typeof(keys["objects"]) === "object") {
-            return this.#parse_nested(content);
-        }
+        const has_phrases = (
+            "phrases" in content &&
+            typeof(content["phrases"]) === "object"
+        );
 
-        return this.#parse_standard(content);
-    }
+        const is_nested = has_objects || has_phrases;
 
-    /**
-     * 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 (is_nested) {
+            const phrases = has_phrases ? content["phrases"] : {};
+            const objects = has_objects ? content["objects"] : {};
 
-        if ("objects" in keys) {
-            objects = content["objects"];
+            return new phrasebook(
+                this.#parse_phrases(phrases),
+                objects
+            );
         }
 
-        return new phrasebook(
-            this.#parse_phrases(phrases), 
-            Object.freeze(objects)
-        );
+        return new phrasebook(this.#parse_phrases(content));
     }
 
     /**
@@ -176,16 +167,6 @@ class loader {
 
         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;

+ 12 - 1
source/phrasebook.js

@@ -51,6 +51,10 @@ class phrasebook {
      * @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);
         }
@@ -97,11 +101,14 @@ class phrasebook {
         const parts = phrase.trim().split(".");
         let current = this.#objects;
 
-        for (const part in parts) {
+        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") {
@@ -133,6 +140,10 @@ class phrasebook {
      * @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(" ", "_")

+ 80 - 2
source/translation.js

@@ -1,6 +1,16 @@
 class translation {
+    /**
+     * @var {string}
+     * This is translated content.
+     */
     #content;
-    translated;
+    
+    /**
+     * @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 
@@ -20,7 +30,9 @@ class translation {
         }
 
         this.#content = content;
-        this.translated = translated;
+        this.#translated = translated;
+
+        Object.freeze(this);
     }
 
     /**
@@ -45,6 +57,72 @@ class translation {
     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;

+ 49 - 6
test/backend.js

@@ -1,12 +1,55 @@
+let test = 1;
+
+const check = (value, expect) => {
+    console.log("Running test " + test + "...");
+
+    test = test + 1;
+
+    if (value.toString() === expect.toString()) {
+        console.log("Pass.");
+        console.log("");
+        return;
+    }
+
+    console.log("FAIL!!!");
+    console.log("Expected: \"" + expect.toString() + "\".");
+    console.log("Result: \"" + value.toString() + "\".");
+    console.log("");
+}
+
 const test_all = async () => {
+    console.log("Loading and testing loader + phrasebook.");
+
     const libtranslate = require("../source/core.js");
     
-    const flat = await new libtranslate.
-    loader("test/flat.json", true).
-    load();
+    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);
-}
+    check(flat.get("test a"), "Test A");
+    check(flat.get("simple"), "prosta");
+
+    console.log("Loading and testing languages library.");
+
+    const langs = await new libtranslate
+    .languages("test/", true)
+    .load("index.json");
+
+    const loaded = await langs.select("pl_PL");
+
+    check(loaded.get("test a"), "Test A");
+
+    console.log("Testing objects notation.");
+
+    const obj = await langs.select("en_US");
+
+    check(obj.get("test.result"), "yes");
+    check(obj.get("test a"), "Test A");
+    
+    console.log("Testing format.");
+
+    check(obj.get("test.format").format({ many: 10 }), "More than 10");
+
+};
 
 test_all();

+ 2 - 1
test/flat.json

@@ -2,5 +2,6 @@
     "simple": "prosta",
     "this is simple phrase.": "To jest prosta fraza.",
     "other phrase": "Inna fraza.",
-    "And much more...": "I o wiele więcej..."
+    "And much more...": "I o wiele więcej...",
+    "test a": "Test A"
 }

+ 4 - 0
test/index.json

@@ -0,0 +1,4 @@
+{
+    "pl_PL": "flat.json",
+    "en_US": "objects.json"
+}

+ 11 - 0
test/objects.json

@@ -0,0 +1,11 @@
+{
+    "objects": {
+        "test": {
+            "result": "yes",
+            "format": "More than #{ many }"
+        }
+    },
+    "phrases": {
+        "test a": "Test A"
+    }
+}

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно