Index: compile_rules.py
==================================================================
--- compile_rules.py
+++ compile_rules.py
@@ -1,13 +1,15 @@
 """
 Grammalecte: compile rules
 """
 
 import re
+import os
 import traceback
 import json
 import colorsys
+import time
 
 import compile_rules_js_convert as jsconv
 import compile_rules_graph as crg
 
 
@@ -417,11 +419,11 @@
     dOptPriority = {}
     for sLine in lOptionLines:
         sLine = sLine.strip()
         if sLine.startswith("OPTGROUP/"):
             m = re.match("OPTGROUP/([a-z0-9]+):(.+)$", sLine)
-            lStructOpt.append( (m.group(1), list(map(str.split, m.group(2).split(",")))) )
+            lStructOpt.append( [m.group(1), list(map(str.split, m.group(2).split(",")))] )
         elif sLine.startswith("OPTSOFTWARE:"):
             lOpt = [ [s, {}]  for s in sLine[12:].strip().split() ]  # don’t use tuples (s, {}), because unknown to JS
         elif sLine.startswith("OPT/"):
             m = re.match("OPT/([a-z0-9]+):(.+)$", sLine)
             for i, sOpt in enumerate(m.group(2).split()):
@@ -463,13 +465,22 @@
 def printBookmark (nLevel, sComment, nLine):
     "print bookmark within the rules file"
     print("  {:>6}:  {}".format(nLine, "  " * nLevel + sComment))
 
 
-def make (spLang, sLang, bJavaScript):
+def make (spLang, sLang, bUseCache=False):
     "compile rules, returns a dictionary of values"
     # for clarity purpose, don’t create any file here
+
+    if bUseCache and os.path.isfile("_build/data_cache.json"):
+        print("> don’t rebuild rules, use cache...")
+        sJSON = open("_build/data_cache.json", "r", encoding="utf-8").read()
+        dCacheVars = json.loads(sJSON)
+        print("  build made at: " + time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(dCacheVars.get("fBuildTime", 0))))
+        return dCacheVars
+
+    fBuildTime = time.time()
 
     print("> read rules file...")
     try:
         lRules = open(spLang + "/rules.grx", 'r', encoding="utf-8").readlines()
     except:
@@ -610,20 +621,25 @@
 
     displayStats(lParagraphRules, lSentenceRules)
 
     print("Unnamed rules: " + str(nRULEWITHOUTNAME))
 
-    dVars = {   "callables": sPyCallables,
-                "callablesJS": sJSCallables,
-                "gctests": sGCTests,
-                "gctestsJS": sGCTestsJS,
-                "paragraph_rules": mergeRulesByOption(lParagraphRules),
-                "sentence_rules": mergeRulesByOption(lSentenceRules),
-                "paragraph_rules_JS": jsconv.writeRulesToJSArray(mergeRulesByOption(lParagraphRulesJS)),
-                "sentence_rules_JS": jsconv.writeRulesToJSArray(mergeRulesByOption(lSentenceRulesJS)) }
+    dVars = {
+        "fBuildTime": fBuildTime,
+        "callables": sPyCallables,
+        "callablesJS": sJSCallables,
+        "gctests": sGCTests,
+        "gctestsJS": sGCTestsJS,
+        "paragraph_rules": mergeRulesByOption(lParagraphRules),
+        "sentence_rules": mergeRulesByOption(lSentenceRules),
+        "paragraph_rules_JS": jsconv.writeRulesToJSArray(mergeRulesByOption(lParagraphRulesJS)),
+        "sentence_rules_JS": jsconv.writeRulesToJSArray(mergeRulesByOption(lSentenceRulesJS))
+    }
     dVars.update(dOptions)
 
     # compile graph rules
-    dVars2 = crg.make(lGraphRule, dDEF, sLang, dOptPriority, bJavaScript)
+    dVars2 = crg.make(lGraphRule, dDEF, sLang, dOptPriority)
     dVars.update(dVars2)
 
+    with open("_build/data_cache.json", "w", encoding="utf-8") as hDst:
+        hDst.write(json.dumps(dVars, ensure_ascii=False))
     return dVars

Index: compile_rules_graph.py
==================================================================
--- compile_rules_graph.py
+++ compile_rules_graph.py
@@ -316,11 +316,11 @@
     else:
         print(" # Unknown action.", sActionId)
         return None
 
 
-def make (lRule, dDef, sLang, dOptPriority, bJavaScript):
+def make (lRule, dDef, sLang, dOptPriority):
     "compile rules, returns a dictionary of values"
     # for clarity purpose, don’t create any file here
 
     # removing comments, zeroing empty lines, creating definitions, storing tests, merging rule lines
     print("  parsing rules...")
@@ -452,10 +452,10 @@
 
     # Result
     return {
         "graph_callables": sPyCallables,
         "graph_callablesJS": sJSCallables,
-        "rules_graphs": dAllGraph,
+        "rules_graphs": str(dAllGraph),
         "rules_graphsJS": str(dAllGraph).replace("True", "true").replace("False", "false"),
-        "rules_actions": dACTIONS,
+        "rules_actions": str(dACTIONS),
         "rules_actionsJS": str(dACTIONS).replace("True", "true").replace("False", "false")
     }

Index: gc_core/js/lang_core/gc_engine.js
==================================================================
--- gc_core/js/lang_core/gc_engine.js
+++ gc_core/js/lang_core/gc_engine.js
@@ -1,17 +1,25 @@
 // Grammar checker engine
-/*jslint esversion: 6*/
-/*global console,require,exports*/
+
+/* jshint esversion:6, -W097 */
+/* jslint esversion:6 */
+/* global require, exports, console */
 
 "use strict";
 
 ${string}
 ${regex}
 ${map}
 
 
-if (typeof(require) !== 'undefined') {
+if(typeof(process) !== 'undefined') {
+    var gc_options = require("./gc_options.js");
+    var gc_rules = require("./gc_rules.js");
+    var gc_rules_graph = require("./gc_rules_graph.js");
+    var cregex = require("./cregex.js");
+    var text = require("../text.js");
+} else if (typeof(require) !== 'undefined') {
     var gc_options = require("resource://grammalecte/${lang}/gc_options.js");
     var gc_rules = require("resource://grammalecte/${lang}/gc_rules.js");
     var gc_rules_graph = require("resource://grammalecte/${lang}/gc_rules_graph.js");
     var cregex = require("resource://grammalecte/${lang}/cregex.js");
     var text = require("resource://grammalecte/text.js");
@@ -51,11 +59,14 @@
 
     //// Initialization
 
     load: function (sContext="JavaScript", sColorType="aRGB", sPath="") {
         try {
-            if (typeof(require) !== 'undefined') {
+            if(typeof(process) !== 'undefined') {
+                var spellchecker = require("../graphspell/spellchecker.js");
+                _oSpellChecker = new spellchecker.SpellChecker("${lang}", "", "${dic_main_filename_js}", "${dic_extended_filename_js}", "${dic_community_filename_js}", "${dic_personal_filename_js}");
+            } else if (typeof(require) !== 'undefined') {
                 var spellchecker = require("resource://grammalecte/graphspell/spellchecker.js");
                 _oSpellChecker = new spellchecker.SpellChecker("${lang}", "", "${dic_main_filename_js}", "${dic_extended_filename_js}", "${dic_community_filename_js}", "${dic_personal_filename_js}");
             } else {
                 _oSpellChecker = new SpellChecker("${lang}", sPath, "${dic_main_filename_js}", "${dic_extended_filename_js}", "${dic_community_filename_js}", "${dic_personal_filename_js}");
             }
@@ -179,11 +190,11 @@
         this.dError = new Map();
         this.dErrorPriority = new Map();  // Key = position; value = priority
     }
 
     asString () {
-        let s = "===== TEXT =====\n"
+        let s = "===== TEXT =====\n";
         s += "sentence: " + this.sSentence0 + "\n";
         s += "now:      " + this.sSentence  + "\n";
         for (let dToken of this.lToken) {
             s += `#${dToken["i"]}\t${dToken["nStart"]}:${dToken["nEnd"]}\t${dToken["sValue"]}\t${dToken["sType"]}`;
             if (dToken.hasOwnProperty("lMorph")) {

Index: gc_core/js/lang_core/gc_options.js
==================================================================
--- gc_core/js/lang_core/gc_options.js
+++ gc_core/js/lang_core/gc_options.js
@@ -1,8 +1,10 @@
 // Options for Grammalecte
-/*jslint esversion: 6*/
-/*global exports*/
+
+/* jshint esversion:6 */
+/* jslint esversion:6 */
+/* global exports */
 
 ${map}
 
 
 var gc_options = {
@@ -23,11 +25,11 @@
             }
             return dColor;
         }
         catch (e) {
             console.error(e);
-            return {}
+            return {};
         }
     },
 
     lStructOpt: ${lStructOpt},
 
@@ -40,17 +42,17 @@
     dColorType: ${dColorType},
 
     dOptColor: ${dOptColor},
 
     dOptLabel: ${dOptLabel}
-}
+};
 
 
 if (typeof(exports) !== 'undefined') {
-	exports.getOptions = gc_options.getOptions;
+    exports.getOptions = gc_options.getOptions;
     exports.getOptionsColors = gc_options.getOptionsColors;
-	exports.lStructOpt = gc_options.lStructOpt;
+    exports.lStructOpt = gc_options.lStructOpt;
     exports.dOpt = gc_options.dOpt;
     exports.dColorType = gc_options.dColorType;
     exports.dOptColor = gc_options.dOptColor;
-	exports.dOptLabel = gc_options.dOptLabel;
+    exports.dOptLabel = gc_options.dOptLabel;
 }

Index: gc_core/js/lang_core/gc_rules.js
==================================================================
--- gc_core/js/lang_core/gc_rules.js
+++ gc_core/js/lang_core/gc_rules.js
@@ -1,7 +1,9 @@
 // Grammar checker rules
-/*jslint esversion: 6*/
+
+/* jshint esversion:6, -W097 */
+/* jslint esversion:6 */
 /*global exports*/
 
 "use strict";
 
 ${string}
@@ -9,12 +11,12 @@
 
 var gc_rules = {
     lParagraphRules: ${paragraph_rules_JS},
 
     lSentenceRules: ${sentence_rules_JS}
-}
+};
 
 
 if (typeof(exports) !== 'undefined') {
     exports.lParagraphRules = gc_rules.lParagraphRules;
     exports.lSentenceRules = gc_rules.lSentenceRules;
 }

Index: gc_core/js/lang_core/gc_rules_graph.js
==================================================================
--- gc_core/js/lang_core/gc_rules_graph.js
+++ gc_core/js/lang_core/gc_rules_graph.js
@@ -1,8 +1,10 @@
 // Grammar checker graph rules
-/*jslint esversion: 6*/
-/*global exports*/
+
+/* jshint esversion:6, -W097 */
+/* jslint esversion:6 */
+/* global exports */
 
 "use strict";
 
 ${string}
 
@@ -9,12 +11,12 @@
 
 var gc_rules_graph = {
     dAllGraph: ${rules_graphsJS},
 
     dRule: ${rules_actionsJS}
-}
+};
 
 
 if (typeof(exports) !== 'undefined') {
     exports.dAllGraph = gc_rules_graph.dAllGraph;
     exports.dRule = gc_rules_graph.dRule;
 }

Index: gc_core/js/tests.js
==================================================================
--- gc_core/js/tests.js
+++ gc_core/js/tests.js
@@ -1,13 +1,17 @@
 // JavaScript
-/*jslint esversion: 6*/
-/*global console,require,exports*/
+
+/* jshint esversion:6, -W097 */
+/* jslint esversion:6 */
+/* global require, exports, console */
 
 "use strict";
 
 
-if (typeof(require) !== 'undefined') {
+if(typeof(process) !== 'undefined') {
+    var helpers = require("./graphspell/helpers.js");
+} else if (typeof(require) !== 'undefined') {
     var helpers = require("resource://grammalecte/graphspell/helpers.js");
 }
 
 
 class TestGrammarChecking {
@@ -18,11 +22,16 @@
         this._aRuleTested = new Set();
     }
 
     * testParse (bDebug=false) {
         const t0 = Date.now();
-        let sURL = (this.spfTests !== "") ? this.spfTests : "resource://grammalecte/"+this.gce.lang+"/tests_data.json";
+        let sURL;
+        if(typeof(process) !== 'undefined') {
+            sURL = (this.spfTests !== "") ? this.spfTests : "./"+this.gce.lang+"/tests_data.json";
+        } else {
+            sURL = (this.spfTests !== "") ? this.spfTests : "resource://grammalecte/"+this.gce.lang+"/tests_data.json";
+        }
         const aData = JSON.parse(helpers.loadFile(sURL)).aData;
         let nInvalid = 0;
         let nTotal = 0;
         let sErrorText;
         let sSugg;

Index: gc_core/js/text.js
==================================================================
--- gc_core/js/text.js
+++ gc_core/js/text.js
@@ -1,8 +1,10 @@
 // JavaScript
-/*jslint esversion: 6*/
-/*global require,exports*/
+
+/* jshint esversion:6, -W097 */
+/* jslint esversion:6 */
+/* global require, exports, console */
 
 "use strict";
 
 
 var text = {

Index: gc_lang/fr/build.py
==================================================================
--- gc_lang/fr/build.py
+++ gc_lang/fr/build.py
@@ -10,10 +10,11 @@
 
 def build (sLang, dVars, spLangPack):
     "complementary build launched from make.py"
     createWebExtension(sLang, dVars)
     createThunderbirdExtension(sLang, dVars, spLangPack)
+    createNodeJSPackage(sLang)
 
 
 def createWebExtension (sLang, dVars):
     "create Web-extension"
     print("Building WebExtension")
@@ -85,5 +86,11 @@
         if not os.path.isdir("grammalecte-js/graphspell/_dictionaries/"+sf):
             hZip.write("grammalecte-js/graphspell/_dictionaries/"+sf, sAddPath+"grammalecte-js/graphspell/_dictionaries/"+sf)
     for sf in os.listdir(spLangPack):
         if not os.path.isdir(spLangPack+"/"+sf):
             hZip.write(spLangPack+"/"+sf, sAddPath+spLangPack+"/"+sf)
+
+
+def createNodeJSPackage (sLang):
+    helpers.createCleanFolder("_build/nodejs/"+sLang)
+    dir_util.copy_tree("gc_lang/"+sLang+"/nodejs/", "_build/nodejs/"+sLang)
+    dir_util.copy_tree("grammalecte-js", "_build/nodejs/"+sLang+"/core/grammalecte")

Index: gc_lang/fr/modules-js/conj.js
==================================================================
--- gc_lang/fr/modules-js/conj.js
+++ gc_lang/fr/modules-js/conj.js
@@ -1,16 +1,20 @@
 // Grammalecte - Conjugueur
 // License: GPL 3
-/*jslint esversion: 6*/
-/*global console,require,exports,self,browser*/
+
+/* jshint esversion:6, -W097 */
+/* jslint esversion:6 */
+/* global require, exports, console, self, browser, chrome, __dirname */
 
 "use strict";
 
 ${map}
 
 
-if (typeof(require) !== 'undefined') {
+if(typeof(process) !== 'undefined') {
+    var helpers = require("../graphspell/helpers.js");
+} else if (typeof(require) !== 'undefined') {
     var helpers = require("resource://grammalecte/graphspell/helpers.js");
 }
 
 var conj = {
     _lVtyp: [],
@@ -494,25 +498,28 @@
     }
 }
 
 
 // Initialization
-if (!conj.bInit && typeof(browser) !== 'undefined') {
+if(!conj.bInit && typeof(process) !== 'undefined') {
+    // Work with nodejs
+    conj.init(helpers.loadFile(__dirname+"/conj_data.json"));
+} else if (!conj.bInit && typeof(browser) !== 'undefined') {
     // WebExtension Standard (but not in Worker)
     conj.init(helpers.loadFile(browser.extension.getURL("grammalecte/fr/conj_data.json")));
 } else if (!conj.bInit && typeof(chrome) !== 'undefined') {
     // WebExtension Chrome (but not in Worker)
     conj.init(helpers.loadFile(chrome.extension.getURL("grammalecte/fr/conj_data.json")));
 } else if (!conj.bInit && typeof(require) !== 'undefined') {
     // Add-on SDK and Thunderbird
     conj.init(helpers.loadFile("resource://grammalecte/fr/conj_data.json"));
-} else if (!conj.bInit && typeof(self) !== 'undefined' && typeof(self.port) !== 'undefined' && typeof(self.port.on) !== "undefined") {
+} else if (!conj.bInit && typeof(self) !== 'undefined' && typeof(self.port) !== 'undefined' && typeof(self.port.on) !== 'undefined') {
     // used within Firefox content script (conjugation panel).
     // can’t load JSON from here, so we do it in ui.js and send it here.
     self.port.on("provideConjData", function (sJSONData) {
         conj.init(sJSONData);
-    });    
+    });
 } else if (conj.bInit){
     console.log("Module conj déjà initialisé");
 } else {
     //console.log("Module conj non initialisé");
 }

Index: gc_lang/fr/modules-js/conj_generator.js
==================================================================
--- gc_lang/fr/modules-js/conj_generator.js
+++ gc_lang/fr/modules-js/conj_generator.js
@@ -1,11 +1,13 @@
 //  JavaScript
 
-/*
-    Conjugation generator
-    beta stage, unfinished, the root for a new way to generate flexions…
-*/
+//    Conjugation generator
+//    beta stage, unfinished, the root for a new way to generate flexions…
+
+
+/* jshint esversion:6, -W097 */
+/* jslint esversion:6 */
 
 "use strict";
 
 
 var conj_generator = {
@@ -137,11 +139,11 @@
             [2,     "ît",           ":Sq:3s/*",         false],
             [2,     "is",           ":E:2s/*",          false],
             [2,     "issons",       ":E:1p/*",          false],
             [2,     "issez",        ":E:2p/*",          false]
         ],
-        
+
         // premier groupe (bien plus irrégulier que prétendu)
         "V1": {
             // a
             // verbes en -er, -ger, -yer, -cer
             "er": [

Index: gc_lang/fr/modules-js/cregex.js
==================================================================
--- gc_lang/fr/modules-js/cregex.js
+++ gc_lang/fr/modules-js/cregex.js
@@ -1,7 +1,9 @@
-//// Grammalecte - Compiled regular expressions
-/*jslint esversion: 6*/
+// Grammalecte - Compiled regular expressions
+
+/* jshint esversion:6 */
+/* jslint esversion:6 */
 
 
 var cregex = {
     ///// Lemme
     _zLemma: new RegExp(">([a-zà-öø-ÿ0-9Ā-ʯ][a-zà-öø-ÿ0-9Ā-ʯ-]+)"),

Index: gc_lang/fr/modules-js/gce_analyseur.js
==================================================================
--- gc_lang/fr/modules-js/gce_analyseur.js
+++ gc_lang/fr/modules-js/gce_analyseur.js
@@ -1,7 +1,9 @@
-//// GRAMMAR CHECKING ENGINE PLUGIN: Parsing functions for French language
-/*jslint esversion: 6*/
+// GRAMMAR CHECKING ENGINE PLUGIN: Parsing functions for French language
+
+/* jshint esversion:6 */
+/* jslint esversion:6 */
 
 function g_morphVC (dToken, sPattern, sNegPattern="") {
     let nEnd = dToken["sValue"].lastIndexOf("-");
     if (dToken["sValue"].includes("-t-")) {
         nEnd = nEnd - 2;
@@ -128,11 +130,11 @@
     }
     return false;
 }
 
 
-//// Exceptions
+// Exceptions
 
 const aREGULARPLURAL = new Set(["abricot", "amarante", "aubergine", "acajou", "anthracite", "brique", "caca", "café",
                                 "carotte", "cerise", "chataigne", "corail", "citron", "crème", "grave", "groseille",
                                 "jonquille", "marron", "olive", "pervenche", "prune", "sable"]);
 const aSHOULDBEVERB = new Set(["aller", "manger"]);

Index: gc_lang/fr/modules-js/gce_date_verif.js
==================================================================
--- gc_lang/fr/modules-js/gce_date_verif.js
+++ gc_lang/fr/modules-js/gce_date_verif.js
@@ -1,11 +1,12 @@
-//// GRAMMAR CHECKING ENGINE PLUGIN
-/*jslint esversion: 6*/
+// GRAMMAR CHECKING ENGINE PLUGIN
 
 // Check date validity
-
 // WARNING: when creating a Date, month must be between 0 and 11
+
+/* jshint esversion:6 */
+/* jslint esversion:6 */
 
 
 const _lDay = ["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"];
 const _dMonth = new Map ([
     ["janvier", 1], ["février", 2], ["mars", 3], ["avril", 4], ["mai", 5], ["juin", 6], ["juillet", 7],

Index: gc_lang/fr/modules-js/gce_suggestions.js
==================================================================
--- gc_lang/fr/modules-js/gce_suggestions.js
+++ gc_lang/fr/modules-js/gce_suggestions.js
@@ -1,10 +1,16 @@
-//// GRAMMAR CHECKING ENGINE PLUGIN: Suggestion mechanisms
-/*jslint esversion: 6*/
-/*global require*/
+// GRAMMAR CHECKING ENGINE PLUGIN: Suggestion mechanisms
+
+/* jshint esversion:6 */
+/* jslint esversion:6 */
+/* global require */
 
-if (typeof(require) !== 'undefined') {
+if(typeof(process) !== 'undefined') {
+    var conj = require("./conj.js");
+    var mfsp = require("./mfsp.js");
+    var phonet = require("./phonet.js");
+} else if (typeof(require) !== 'undefined') {
     var conj = require("resource://grammalecte/fr/conj.js");
     var mfsp = require("resource://grammalecte/fr/mfsp.js");
     var phonet = require("resource://grammalecte/fr/phonet.js");
 }
 

Index: gc_lang/fr/modules-js/lexicographe.js
==================================================================
--- gc_lang/fr/modules-js/lexicographe.js
+++ gc_lang/fr/modules-js/lexicographe.js
@@ -1,9 +1,11 @@
 // Grammalecte - Lexicographe
 // License: MPL 2
-/*jslint esversion: 6*/
-/*global require,exports*/
+
+/* jshint esversion:6, -W097 */
+/* jslint esversion:6 */
+/* global require, exports, console */
 
 "use strict";
 
 ${string}
 ${map}

Index: gc_lang/fr/modules-js/mfsp.js
==================================================================
--- gc_lang/fr/modules-js/mfsp.js
+++ gc_lang/fr/modules-js/mfsp.js
@@ -1,13 +1,17 @@
 // Grammalecte
-/*jslint esversion: 6*/
-/*global console,require,exports,browser*/
+
+/* jshint esversion:6, -W097 */
+/* jslint esversion:6 */
+/* global require, exports, console, browser,__dirname */
 
 "use strict";
 
 
-if (typeof(require) !== 'undefined') {
+if(typeof(process) !== 'undefined') {
+    var helpers = require("../graphspell/helpers.js");
+} else if (typeof(require) !== 'undefined') {
     var helpers = require("resource://grammalecte/graphspell/helpers.js");
 }
 
 
 var mfsp = {
@@ -102,11 +106,14 @@
     }
 };
 
 
 // Initialization
-if (!mfsp.bInit && typeof(browser) !== 'undefined') {
+if(!mfsp.bInit && typeof(process) !== 'undefined') {
+    //Nodejs
+    mfsp.init(helpers.loadFile(__dirname+"/mfsp_data.json"));
+} else if (!mfsp.bInit && typeof(browser) !== 'undefined') {
     // WebExtension
     mfsp.init(helpers.loadFile(browser.extension.getURL("grammalecte/fr/mfsp_data.json")));
 } else if (!mfsp.bInit && typeof(require) !== 'undefined') {
     // Add-on SDK and Thunderbird
     mfsp.init(helpers.loadFile("resource://grammalecte/fr/mfsp_data.json"));

Index: gc_lang/fr/modules-js/phonet.js
==================================================================
--- gc_lang/fr/modules-js/phonet.js
+++ gc_lang/fr/modules-js/phonet.js
@@ -1,9 +1,14 @@
 // Grammalecte - Suggestion phonétique
-/*jslint esversion: 6*/
+
+/* jshint esversion:6 */
+/* jslint esversion:6 */
+/* global __dirname */
 
-if (typeof(require) !== 'undefined') {
+if(typeof(process) !== 'undefined') {
+    var helpers = require("../graphspell/helpers.js");
+} else if (typeof(require)  !== 'undefined') {
     var helpers = require("resource://grammalecte/graphspell/helpers.js");
 }
 
 
 var phonet = {
@@ -82,11 +87,14 @@
     }
 };
 
 
 // Initialization
-if (!phonet.bInit && typeof(browser) !== 'undefined') {
+if (!phonet.bInit && typeof(process) !== 'undefined') {
+    //Nodejs
+    phonet.init(helpers.loadFile(__dirname+"/phonet_data.json"));
+} else if (!phonet.bInit && typeof(browser) !== 'undefined') {
     // WebExtension
     phonet.init(helpers.loadFile(browser.extension.getURL("grammalecte/fr/phonet_data.json")));
 } else if (!phonet.bInit && typeof(require) !== 'undefined') {
     // Add-on SDK and Thunderbird
     phonet.init(helpers.loadFile("resource://grammalecte/fr/phonet_data.json"));

Index: gc_lang/fr/modules-js/textformatter.js
==================================================================
--- gc_lang/fr/modules-js/textformatter.js
+++ gc_lang/fr/modules-js/textformatter.js
@@ -1,12 +1,14 @@
 // Grammalecte - text formatter
-/*jslint esversion: 6*/
-/*global exports*/
+
+/* jshint esversion:6, -W097 */
+/* jslint esversion:6 */
+/* global exports, console */
 
 "use strict";
 
-${map}
+//!${map}
 
 
 // Latin letters: http://unicode-table.com/fr/
 // 0-9
 // A-Z
@@ -83,11 +85,11 @@
     "ts_apostrophe":              [ [/\b([ldnjmtscç])['´‘′`](?=[a-zA-Zà-ö0-9À-Öø-ÿØ-ßĀ-ʯ])/ig, "$1’"],
                                     [/\b(qu|jusqu|lorsqu|puisqu|quoiqu|quelqu|presqu|entr|aujourd|prud)['´‘′`]/ig, "$1’"] ],
     "ts_ellipsis":                [ [/\.\.\./g, "…"],
                                     [/…\.\./g, "……"],
                                     [/…\.(?!\.)/g, "…"] ],
-    "ts_n_dash_middle":           [ [/ [-—] /g, " – "], 
+    "ts_n_dash_middle":           [ [/ [-—] /g, " – "],
                                     [/ [-—],/g, " –,"] ],
     "ts_m_dash_middle":           [ [/ [-–] /g, " — "],
                                     [/ [-–],/g, " —,"] ],
     "ts_n_dash_start":            [ [/^[-—][  ]/gm, "– "],
                                     [/^– /gm, "– "],
@@ -256,38 +258,145 @@
     ["ma_word", true],
     ["ma_1letter_lowercase", false],
     ["ma_1letter_uppercase", false]
 ]);
 
-const dTFOptions = dTFDefaultOptions.gl_shallowCopy();
-
 
 class TextFormatter {
 
-    constructor () {
+    constructor (bDebug=false) {
         this.sLang = "fr";
+        this.bDebug = bDebug;
+        //don't change this in external ;)
+        this.dOptions = dTFDefaultOptions.gl_shallowCopy();
     }
 
     formatText (sText, dOpt=null) {
         if (dOpt !== null) {
-            dTFOptions.gl_updateOnlyExistingKeys(dOpt);
+            this.dOptions.gl_updateOnlyExistingKeys(dOpt);
+        }
+        for (let [sOptName, bVal] of this.dOptions) {
+            //console.log(oReplTable);
+            if (bVal && oReplTable[sOptName]) {
+                for (let [zRgx, sRep] of oReplTable[sOptName]) {
+                    sText = sText.replace(zRgx, sRep);
+                }
+            }
+        }
+        return sText;
+    }
+
+    formatTextCount (sText, dOpt=null) {
+        let nCount = 0;
+        if (dOpt !== null) {
+            this.dOptions.gl_updateOnlyExistingKeys(dOpt);
         }
-        for (let [sOptName, bVal] of dTFOptions) {
-            if (bVal && oReplTable.has(sOptName)) {
+        for (let [sOptName, bVal] of this.dOptions) {
+            if (bVal && oReplTable[sOptName]) {
                 for (let [zRgx, sRep] of oReplTable[sOptName]) {
+                    nCount += (sText.match(zRgx) || []).length;
                     sText = sText.replace(zRgx, sRep);
                 }
             }
         }
+        return [sText, nCount];
+    }
+
+    formatTextRule (sText, sRuleName) {
+        if (oReplTable[sRuleName]) {
+            for (let [zRgx, sRep] of oReplTable[sRuleName]) {
+                sText = sText.replace(zRgx, sRep);
+            }
+        } else if (this.bDebug){
+            console.log("# Error. TF: there is no option “" + sRuleName+ "”.");
+        }
+        return sText;
+    }
+
+    formatTextRuleCount (sText, sRuleName) {
+        let nCount = 0;
+        if (oReplTable[sRuleName]) {
+            for (let [zRgx, sRep] of oReplTable[sRuleName]) {
+                nCount += (sText.match(zRgx) || []).length;
+                sText = sText.replace(zRgx, sRep);
+            }
+        } else if (this.bDebug){
+            console.log("# Error. TF: there is no option “" + sRuleName+ "”.");
+        }
+        return [sText, nCount];
+    }
+
+    removeHyphenAtEndOfParagraphs (sText) {
+        sText = sText.replace(/-[  ]*\n/gm, "");
         return sText;
     }
+
+    removeHyphenAtEndOfParagraphsCount (sText) {
+        let nCount = (sText.match(/-[  ]*\n/gm) || []).length;
+        sText = sText.replace(/-[  ]*\n/gm, "");
+        return [sText, nCount];
+    }
+
+    mergeContiguousParagraphs (sText) {
+        sText = sText.replace(/^[  ]+$/gm, ""); // clear empty paragraphs
+        let s = "";
+        for (let sParagraph of this.getParagraph(sText)) {
+            if (sParagraph === "") {
+                s += "\n";
+            } else {
+                s += sParagraph + " ";
+            }
+        }
+        s = s.replace(/  +/gm, " ").replace(/ $/gm, "");
+        return s;
+    }
+
+    mergeContiguousParagraphsCount (sText) {
+        let nCount = 0;
+        sText = sText.replace(/^[  ]+$/gm, ""); // clear empty paragraphs
+        let s = "";
+        for (let sParagraph of this.getParagraph(sText)) {
+            if (sParagraph === "") {
+                s += "\n";
+            } else {
+                s += sParagraph + " ";
+                nCount += 1;
+            }
+        }
+        s = s.replace(/  +/gm, " ").replace(/ $/gm, "");
+        return [s, nCount];
+    }
+
+    * getParagraph (sText, sSep="\n") {
+        // generator: returns paragraphs of text
+        let iStart = 0;
+        let iEnd = 0;
+        while ((iEnd = sText.indexOf(sSep, iStart)) !== -1) {
+            yield sText.slice(iStart, iEnd);
+            iStart = iEnd + 1;
+        }
+        yield sText.slice(iStart);
+    }
 
     getDefaultOptions () {
-        return dTFDefaultOptions;
+        //we return a copy to make sure they are no modification in external
+        return dTFDefaultOptions.gl_shallowCopy();
+    }
+
+    getOptions () {
+        //we return a copy to make sure they are no modification in external
+        return this.dOptions.gl_shallowCopy();
+    }
+
+    setOptions (dOpt=null) {
+        if (dOpt !== null) {
+            this.dOptions.gl_updateOnlyExistingKeys(dOpt);
+        } else if (this.bDebug){
+            console.log("# Error. TF: no option to change.");
+        }
     }
 }
 
 
 if (typeof(exports) !== 'undefined') {
     exports.TextFormatter = TextFormatter;
-    exports.oReplTable = oReplTable;
 }

ADDED   gc_lang/fr/nodejs/cli/bin/gramma-cli.bat
Index: gc_lang/fr/nodejs/cli/bin/gramma-cli.bat
==================================================================
--- /dev/null
+++ gc_lang/fr/nodejs/cli/bin/gramma-cli.bat
@@ -0,0 +1,1 @@
+@node gramma-cli.js %*

ADDED   gc_lang/fr/nodejs/cli/bin/gramma-cli.js
Index: gc_lang/fr/nodejs/cli/bin/gramma-cli.js
==================================================================
--- /dev/null
+++ gc_lang/fr/nodejs/cli/bin/gramma-cli.js
@@ -0,0 +1,632 @@
+#! /usr/bin/env node
+// -*- js -*-
+
+// Gramma-Cli
+// Grammalect client pour node
+
+/* jshint esversion:6, -W097 */
+/* jslint esversion:6 */
+/* global require, console */
+
+/*
+Doc :
+https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment
+https://stackoverflow.com/questions/41058569/what-is-the-difference-between-const-and-const-in-javascript
+*/
+
+const argCmd = require("../lib/minimist.js")(process.argv.slice(2));
+const { performance } = require("perf_hooks");
+
+//Initialisation des messages
+const msgStart = "\x1b[31mBienvenue sur Grammalecte pour NodeJS!!!\x1b[0m\n";
+const msgPrompt = "\x1b[36mGrammaJS\x1b[33m>\x1b[0m ";
+const msgSuite = "\x1b[33m…\x1b[0m ";
+const msgEnd = "\x1b[31m\x1b[5m\x1b[5mBye bye!\x1b[0m";
+
+var repPreference = {
+    json: false,
+    perf: false
+};
+
+var sBufferConsole = "";
+var sCmdToExec = "";
+var sText = "";
+
+var cmdAction = {
+    help: {
+        short: "",
+        arg: "",
+        description: "Affiche les informations que vous lisez ;)",
+        execute: ""
+    },
+    perf: {
+        short: "",
+        arg: "on/off",
+        description: "Permet d’afficher le temps d’exécution des commandes.",
+        execute: ""
+    },
+    json: {
+        short: "",
+        arg: "on/off",
+        description: "Réponse en format format json.",
+        execute: ""
+    },
+    exit: {
+        short: "",
+        arg: "",
+        description: "Client interactif: Permet de le quitter.",
+        execute: ""
+    },
+    text: {
+        short: "",
+        arg: "texte",
+        description: "Client / Server: Définir un texte pour plusieurs actions.",
+        execute: ""
+    },
+    format: {
+        short: "",
+        arg: "texte",
+        description: "Permet de mettre en forme le texte.",
+        execute: "formatText"
+    },
+    check: {
+        short: "",
+        arg: "texte",
+        description: "Vérifie la grammaire et l’orthographe d'un texte.",
+        execute: "verifParagraph"
+    },
+    lexique: {
+        short: "",
+        arg: "texte",
+        description: "Affiche le lexique du texte.",
+        execute: "lexique"
+    },
+    spell: {
+        short: "",
+        arg: "mot",
+        description: "Vérifie l’existence d’un mot.",
+        execute: "spell"
+    },
+    suggest: {
+        short: "",
+        arg: "mot",
+        description: "Suggestion des graphies proches d’un mot.",
+        execute: "suggest"
+    },
+    morph: {
+        short: "",
+        arg: "mot",
+        description: "Affiche les informations pour un mot.",
+        execute: "morph"
+    },
+    lemma: {
+        short: "",
+        arg: "mot",
+        description: "Donne le lemme d’un mot.",
+        execute: "lemma"
+    },
+    gceoption: {
+        short: "",
+        arg: "+/-name",
+        description: "Définit les options à utiliser par le correcteur grammatical.",
+        execute: ""
+    },
+    tfoption: {
+        short: "",
+        arg: "+/-name",
+        description: "Définit les options à utiliser par le formateur de texte.",
+        execute: ""
+    }
+};
+
+var cmdOne = ["json", "perf", "help", "exit"];
+var cmdMulti = ["text", "format", "check", "lexique", "spell", "suggest", "morph", "lemma"];
+
+var cmdAll = [...cmdOne, ...cmdMulti];
+
+function getArgVal(aArg, lArgOk) {
+    for (let eArgOk of lArgOk) {
+        if (typeof aArg[eArgOk] !== "undefined") {
+            return aArg[eArgOk];
+        }
+    }
+    return false;
+}
+
+function getArg(aArg, lArgOk) {
+    for (let eArgOk of lArgOk) {
+        if (typeof aArg[eArgOk] !== "undefined") {
+            return true;
+        }
+    }
+    return false;
+}
+
+function toBool(aStr) {
+    return aStr === "true" || aStr === "on";
+}
+
+function isBool(aStr) {
+    if (typeof aStr === "boolean" || typeof aStr === "undefined") {
+        return true;
+    }
+    aStr = aStr.toLowerCase();
+    return aStr === "true" || aStr === "on" || aStr === "false" || aStr === "off" || aStr === "";
+}
+
+function toTitle(aStr) {
+    return aStr.charAt(0).toUpperCase() + aStr.slice(1);
+}
+
+function repToText(oRep) {
+    //console.log(oRep);
+    let repText = "";
+    for (const action of ["json", "perf", "gceoption", "tfoption"]) {
+        if (action in oRep) {
+            repText += toTitle(action) + " " + oRep[action];
+        }
+    }
+
+    for (const action of ["morph", "lemma"]) {
+        if (action in oRep) {
+            for (const toAff of oRep[action]) {
+                if (toAff.text == "NoText") {
+                    repText += "\n" + toTitle(action) + ": Pas de texte à vérifier.";
+                } else {
+                    if (toAff.reponse.length == 0) {
+                        repText += "\nAuncun " + toTitle(action) + " existant pour: «" + toAff.text + "»";
+                    } else {
+                        let ascii = "├";
+                        let numRep = 0;
+                        repText += "\n" + toTitle(action) + " possible de: «" + toAff.text + "»";
+                        for (let reponse of toAff.reponse) {
+                            numRep++;
+                            if (numRep == toAff.reponse.length) {
+                                ascii = "└";
+                            }
+                            repText += "\n " + ascii + " " + reponse;
+                        }
+                    }
+                    repText += affPerf(toAff.time);
+                }
+            }
+        }
+    }
+
+    if ("spell" in oRep) {
+        for (const toAff of oRep.spell) {
+            if (toAff.text == "NoText") {
+                repText += "\nSpell: Pas de texte à vérifier.";
+            } else {
+                repText += "\nLe mot «" + toAff.text + "» " + (toAff.reponse ? "existe" : "innexistant");
+                repText += affPerf(toAff.time);
+            }
+        }
+    }
+
+    if ("suggest" in oRep) {
+        for (const toAff of oRep.suggest) {
+            if (toAff.text == "NoText") {
+                repText += "\nSuggest : Pas de texte à vérifier.";
+            } else {
+                //let numgroup = 0;
+                if (toAff.reponse.length == 0) {
+                    repText += "\nAucune suggestion possible pour: «" + toAff.text + "»";
+                } else {
+                    repText += "\nSuggestion possible de: «" + toAff.text + "»";
+                    let ascii = "├";
+                    let numRep = 0;
+                    for (let reponse of toAff.reponse) {
+                        numRep++;
+                        if (numRep == toAff.reponse.length) {
+                            ascii = "└";
+                        }
+                        repText += "\n " + ascii + " " + reponse;
+                    }
+                }
+                repText += affPerf(toAff.time);
+            }
+        }
+    }
+
+    if ("format" in oRep) {
+        for (const toAff of oRep.format) {
+            if (toAff.text == "NoText") {
+                repText += "\nPas de texte à formatter.";
+            } else {
+                repText += "\nMise en forme:\n" + toAff.reponse;
+                repText += affPerf(toAff.time);
+            }
+        }
+    }
+
+    if ("lexique" in oRep) {
+        for (const toAff of oRep.lexique) {
+            if (toAff.text == "NoText") {
+                repText += "\nLexique: Pas de texte à vérifier.";
+            } else {
+                repText += "\nLexique:";
+
+                let ascii1, ascii1a, numRep1, ascii2, numRep2, replength;
+
+                ascii1 = "├";
+                ascii1a = "│";
+                numRep1 = 0;
+
+                replength = toAff.reponse.length;
+                for (let reponse of toAff.reponse) {
+                    numRep1++;
+                    if (numRep1 == replength) {
+                        ascii1 = "└";
+                        ascii1a = " ";
+                    }
+                    repText += "\n  " + ascii1 + " " + reponse.sValue;
+                    let ascii = "├";
+                    let numRep = 0;
+                    for (let label of reponse.aLabel) {
+                        numRep++;
+                        if (numRep == reponse.aLabel.length) {
+                            ascii = "└";
+                        }
+                        repText += "\n  " + ascii1a + " " + ascii + " " + label.trim();
+                    }
+                }
+                repText += affPerf(toAff.time);
+            }
+        }
+    }
+
+    if ("check" in oRep) {
+        for (const toAff of oRep.check) {
+            if (toAff.text == "NoText") {
+                repText += "\nCheck: Pas de texte à vérifier.";
+            } else {
+                let ascii1, ascii1a, numRep1, ascii2, numRep2, replength;
+
+                ascii1 = "├";
+                ascii1a = "│";
+                numRep1 = 0;
+                replength = Object.keys(toAff.reponse.lGrammarErrors).length;
+                if (replength == 0) {
+                    repText += "\nPas de faute de grammaire";
+                } else {
+                    repText += "\nFaute(s) de grammaire";
+                    for (let gramma of toAff.reponse.lGrammarErrors) {
+                        numRep1++;
+                        if (numRep1 == replength) {
+                            ascii1 = "└";
+                            ascii1a = " ";
+                        }
+                        repText += "\n " + ascii1 + " " + gramma.nStart + "->" + gramma.nEnd + " " + gramma.sMessage;
+                        ascii2 = "├";
+                        numRep2 = 0;
+                        for (let suggestion of gramma.aSuggestions) {
+                            numRep2++;
+                            if (numRep2 == gramma.aSuggestions.length) {
+                                ascii2 = "└";
+                            }
+                            repText += "\n " + ascii1a + "  " + ascii2 + ' "' + suggestion + '"';
+                        }
+                    }
+                }
+
+                ascii1 = "├";
+                ascii1a = "│";
+                numRep1 = 0;
+                replength = Object.keys(toAff.reponse.lSpellingErrors).length;
+                if (replength == 0) {
+                    repText += "\nPas de faute d'orthographe";
+                } else {
+                    repText += "\nFaute(s) d'orthographe";
+                    for (let ortho of toAff.reponse.lSpellingErrors) {
+                        numRep1++;
+                        if (numRep1 == replength) {
+                            ascii1 = "└";
+                            ascii1a = " ";
+                        }
+                        repText += "\n " + ascii1 + " " + ortho.nStart + "->" + ortho.nEnd + " " + ortho.sValue;
+                        ascii2 = "├";
+                        numRep2 = 0;
+                        for (let suggestion of ortho.aSuggestions) {
+                            numRep2++;
+                            if (numRep2 == ortho.aSuggestions.length) {
+                                ascii2 = "└";
+                            }
+                            repText += "\n " + ascii1a + "  " + ascii2 + ' "' + suggestion + '"';
+                        }
+                    }
+                }
+                repText += affPerf(toAff.time);
+            }
+        }
+    }
+
+    if ("help" in oRep) {
+        let colorNum = 31;
+        for (const action of oRep.help) {
+            //Uniquement pour le fun on met de la couleur ;)
+            if (action.indexOf("===") > -1) {
+                console.log("\x1b[" + colorNum + "m" + action + "\x1b[0m");
+                colorNum = colorNum + 2;
+            } else {
+                console.log(action);
+            }
+        }
+    }
+
+    return repText.trim("\n");
+}
+
+function affPerf(aTime) {
+    if (aTime == "NA") {
+        return "";
+    }
+    return "\nExécuté en: " + aTime + " ms";
+}
+
+function actionGramma(repPreference, action, aAction) {
+    let tStart, tEnd;
+    let tmpRep = {
+        text: "",
+        reponse: "",
+        time: "NA"
+    };
+
+    if (!isBool(aAction) && aAction !== "") {
+        tmpRep.text = aAction;
+        sText = aAction;
+    } else if (!isBool(sText)) {
+        //Utilisation du dernier texte connu
+        tmpRep.text = sText;
+    } else {
+        tmpRep.text = "NoText";
+    }
+
+    if (repPreference.perf) {
+        tStart = performance.now();
+    }
+
+    tmpRep.reponse = oGrammarChecker[cmdAction[action].execute](tmpRep.text);
+
+    if (repPreference.perf) {
+        tEnd = performance.now();
+        tmpRep["time"] = (Math.round((tEnd - tStart) * 1000) / 1000).toString();
+    }
+
+    return tmpRep;
+}
+
+function actionToExec(aArg) {
+    let repAction = {};
+
+    if (!isBool(aArg.text)) {
+        sText = aArg.text;
+    }
+
+    for (const action of ["json", "perf"]) {
+        if (getArg(aArg, [action])) {
+            repPreference[action] = getArgVal(aArg, [action]);
+            repAction[action] = repPreference[action] ? "ON" : "OFF";
+        }
+    }
+
+    for (const action of ["gceoption", "tfoption"]) {
+        if (getArg(aArg, [action])) {
+            let sFonction = action == "gceoption" ? "GceOption" : "TfOption";
+            let sOpt = sText.split(" ");
+            if (sOpt[0] == "reset") {
+                oGrammarChecker["reset" + sFonction + "s"]();
+                repAction[action] = "reset";
+            } else {
+                for (const optAction of sOpt) {
+                    let bOptVal = optAction[0] == "+" ? true : false;
+                    let sOptName = optAction.slice(1, optAction.length);
+                    oGrammarChecker["set" + sFonction](sOptName, bOptVal);
+                    repAction[action] = sText;
+                }
+            }
+        }
+    }
+
+    for (const action in aArg) {
+        if (cmdAction[action] && cmdAction[action].execute !== "") {
+            //console.log(aArg, aArg[action], !isBool(aArg[action]), !isBool(repAction.text));
+            if (!repAction[action]) {
+                repAction[action] = [];
+            }
+
+            if (typeof aArg[action] === "object") {
+                for (const valAction of aArg[action]) {
+                    tmpRep = actionGramma(repPreference, action, valAction);
+                    repAction[action].push(tmpRep);
+                }
+            } else {
+                tmpRep = actionGramma(repPreference, action, aArg[action]);
+                repAction[action].push(tmpRep);
+            }
+        }
+    }
+
+    if (getArg(aArg, ["help"])) {
+        repAction["help"] = [];
+
+        repAction["help"].push("================================== Aide: ==================================");
+        repAction["help"].push("");
+        repAction["help"].push("Il y a trois modes de fonctionnement: client / client intératif / serveur.");
+
+        repAction["help"].push(" * le client intéractif: «gramma-cli -i».");
+        repAction["help"].push(' * pour le client exemple: «gramma-cli --command "mot/texte"».');
+        repAction["help"].push(" * le serveur se lance avec la commande «gramma-cli --server --port 8085».");
+
+        repAction["help"].push("");
+        repAction["help"].push("========================= Les commandes/arguments: ========================");
+        repAction["help"].push("");
+        for (const action in cmdAction) {
+            repAction["help"].push(action.padEnd(10, " ") + ": " + cmdAction[action].arg.padEnd(8, " ") + ": " + cmdAction[action].description);
+        }
+        repAction["help"].push("");
+        repAction["help"].push("================================== Note: ==================================");
+        repAction["help"].push("");
+        repAction["help"].push("En mode client: les arguments sont de la forme «--argument» !");
+        repAction["help"].push("En mode client intéractif: pour les commandes concernant un texte, vous");
+        repAction["help"].push("  pouvez taper la commande puis Entrée (pour saisir le texte) pour ");
+        repAction["help"].push('  terminer la saisie du texte et exécuter la commande taper /"commande"');
+    }
+
+    if (repPreference.json) {
+        return JSON.stringify(repAction);
+    } else {
+        return repToText(repAction);
+    }
+}
+
+function argToExec(aCommand, aText, rl, resetCmd = true) {
+    let execAct = {};
+    aCommand = aCommand.toLowerCase();
+
+    if (!isBool(aText)) {
+        execAct["text"] = aText;
+        execAct[aCommand] = true;
+    } else {
+        execAct[aCommand] = toBool(aText);
+    }
+
+    console.log(actionToExec(execAct));
+    //sBufferConsole = "";
+    if (resetCmd) {
+        sCmdToExec = "";
+    }
+
+    if (typeof rl !== "undefined") {
+        rl.setPrompt(msgPrompt);
+    }
+}
+
+function completer(line) {
+    var hits = cmdAll.filter(function(c) {
+        if (c.indexOf(line) == 0) {
+            return c;
+        }
+    });
+    return [hits && hits.length ? hits : cmdAll, line];
+}
+
+if (process.argv.length <= 2) {
+    console.log(actionToExec({ help: true }));
+} else {
+    //var GrammarChecker = require("./api.js");
+    //console.log(module.paths);
+    var GrammarChecker = require("grammalecte");
+    var oGrammarChecker = new GrammarChecker.GrammarChecker(["Grammalecte", "Graphspell", "TextFormatter", "Lexicographer", "Tokenizer"], "fr");
+
+    if (argCmd.server) {
+        var http = require("http");
+        var url = require("url");
+        var querystring = require("querystring");
+
+        var collectRequestData = function(aRequest, aResponse, callback) {
+            let sBody = "";
+            aRequest.on("data", chunk => {
+                sBody += chunk.toString();
+            });
+            aRequest.on("end", () => {
+                let oParams = querystring.parse(sBody);
+                //console.log(oParams /*, page*/);
+                callback(querystring.parse(sBody), aResponse);
+            });
+        };
+
+        var reponseRequest = function(aParms, aResponse) {
+            aResponse.setHeader("access-control-allow-origin", "*");
+            aResponse.writeHead(200, { "Content-Type": "application/json" });
+            aParms["json"] = true; //Forcage de la réponse en json
+            aResponse.write(actionToExec(aParms));
+            aResponse.end();
+        };
+
+        var server = http.createServer(function(aRequest, aResponse) {
+            var sPage = url.parse(aRequest.url).pathname;
+            if (sPage !== "/") {
+                //favicon.ico
+                aResponse.writeHead(404, { "Content-Type": "text/plain" });
+                aResponse.write("Error 404");
+                aResponse.end();
+            } else {
+                if (aRequest.method === "POST") {
+                    collectRequestData(aRequest, aResponse, reponseRequest);
+                } else {
+                    let oParams = querystring.parse(url.parse(aRequest.url).query);
+                    reponseRequest(oParams, aResponse);
+                }
+            }
+        });
+        server.listen(argCmd.port || 2212);
+        console.log("Server started on http://127.0.0.1:" + (argCmd.port || 2212) + "/");
+    } else if (getArg(argCmd, ["i", "interactive"])) {
+        process.stdin.setEncoding("utf8");
+
+        const readline = require("readline");
+        const rl = readline.createInterface({
+            crlfDelay: Infinity,
+            input: process.stdin,
+            output: process.stdout,
+            completer: completer,
+            prompt: msgPrompt
+        });
+
+        //console.log( process.stdin.isTTY );
+        console.log(msgStart);
+        rl.prompt();
+        rl.on("line", sBuffer => {
+            //process.stdout.write
+            if (sBuffer == "exit") {
+                console.log(msgEnd);
+                process.exit(0);
+            }
+
+            let lg = sBuffer.toLowerCase().trim();
+            let bSpace = lg.indexOf(" ") > -1;
+            if (!bSpace) {
+                if (cmdOne.indexOf(lg) > -1) {
+                    argToExec(lg, sBuffer, rl, true);
+                } else if (cmdAll.indexOf(lg) > -1) {
+                    sBufferConsole = "";
+                    sCmdToExec = lg;
+                    //Prompt simple pour distinguer que c"est une suite d"une commande
+                    rl.setPrompt(msgSuite);
+                } else if (lg.slice(1) == sCmdToExec) {
+                    argToExec(sCmdToExec, sBufferConsole, rl, true);
+                } else if (cmdAll.indexOf(lg.slice(0, lg.length - 1)) > -1) {
+                    argToExec(lg.slice(0, lg.length - 1), sBufferConsole, rl, true);
+                } else if (lg == "") {
+                    sBufferConsole += "\n";
+                }
+            } else if (sCmdToExec == "") {
+                let regRep = /(.*?) (.*)/gm.exec(sBuffer);
+                //console.log(regRep.length,sBuffer);
+                if (regRep && regRep.length == 3) {
+                    argToExec(regRep[1], regRep[2]);
+                }
+            } else {
+                sBufferConsole += sBuffer + "\n";
+            }
+
+            rl.prompt();
+        }).on("close", () => {
+            console.log(msgEnd);
+            process.exit(0);
+        });
+    } else {
+        if (
+            typeof argCmd.text !== "object" &&
+            typeof argCmd.json !== "object" &&
+            typeof argCmd.perf !== "object" &&
+            typeof argCmd.gceoption !== "object" &&
+            typeof argCmd.tfoption !== "object"
+        ) {
+            console.log(actionToExec(argCmd));
+        } else {
+            console.log("Votre demmande est confuse.");
+        }
+    }
+}

ADDED   gc_lang/fr/nodejs/cli/data/script.gramma
Index: gc_lang/fr/nodejs/cli/data/script.gramma
==================================================================
--- /dev/null
+++ gc_lang/fr/nodejs/cli/data/script.gramma
@@ -0,0 +1,17 @@
+json false
+perf true
+spell salut
+suggest salut
+morph salut
+lemma salut
+gceoption -typo
+check
+salut comment,il vas???bienss...et tu!  "salut commentss il vas???"
+/check
+gceoption +typo
+check/
+#lexique/
+tfoption +nnbsp_before_punctuation -ts_ellipsis
+format/
+tfoption reset
+format/

ADDED   gc_lang/fr/nodejs/cli/lib/minimist.js
Index: gc_lang/fr/nodejs/cli/lib/minimist.js
==================================================================
--- /dev/null
+++ gc_lang/fr/nodejs/cli/lib/minimist.js
@@ -0,0 +1,236 @@
+module.exports = function (args, opts) {
+    if (!opts) opts = {};
+    
+    var flags = { bools : {}, strings : {}, unknownFn: null };
+
+    if (typeof opts['unknown'] === 'function') {
+        flags.unknownFn = opts['unknown'];
+    }
+
+    if (typeof opts['boolean'] === 'boolean' && opts['boolean']) {
+      flags.allBools = true;
+    } else {
+      [].concat(opts['boolean']).filter(Boolean).forEach(function (key) {
+          flags.bools[key] = true;
+      });
+    }
+    
+    var aliases = {};
+    Object.keys(opts.alias || {}).forEach(function (key) {
+        aliases[key] = [].concat(opts.alias[key]);
+        aliases[key].forEach(function (x) {
+            aliases[x] = [key].concat(aliases[key].filter(function (y) {
+                return x !== y;
+            }));
+        });
+    });
+
+    [].concat(opts.string).filter(Boolean).forEach(function (key) {
+        flags.strings[key] = true;
+        if (aliases[key]) {
+            flags.strings[aliases[key]] = true;
+        }
+     });
+
+    var defaults = opts['default'] || {};
+    
+    var argv = { _ : [] };
+    Object.keys(flags.bools).forEach(function (key) {
+        setArg(key, defaults[key] === undefined ? false : defaults[key]);
+    });
+    
+    var notFlags = [];
+
+    if (args.indexOf('--') !== -1) {
+        notFlags = args.slice(args.indexOf('--')+1);
+        args = args.slice(0, args.indexOf('--'));
+    }
+
+    function argDefined(key, arg) {
+        return (flags.allBools && /^--[^=]+$/.test(arg)) ||
+            flags.strings[key] || flags.bools[key] || aliases[key];
+    }
+
+    function setArg (key, val, arg) {
+        if (arg && flags.unknownFn && !argDefined(key, arg)) {
+            if (flags.unknownFn(arg) === false) return;
+        }
+
+        var value = !flags.strings[key] && isNumber(val)
+            ? Number(val) : val
+        ;
+        setKey(argv, key.split('.'), value);
+        
+        (aliases[key] || []).forEach(function (x) {
+            setKey(argv, x.split('.'), value);
+        });
+    }
+
+    function setKey (obj, keys, value) {
+        var o = obj;
+        keys.slice(0,-1).forEach(function (key) {
+            if (o[key] === undefined) o[key] = {};
+            o = o[key];
+        });
+
+        var key = keys[keys.length - 1];
+        if (o[key] === undefined || flags.bools[key] || typeof o[key] === 'boolean') {
+            o[key] = value;
+        }
+        else if (Array.isArray(o[key])) {
+            o[key].push(value);
+        }
+        else {
+            o[key] = [ o[key], value ];
+        }
+    }
+    
+    function aliasIsBoolean(key) {
+      return aliases[key].some(function (x) {
+          return flags.bools[x];
+      });
+    }
+
+    for (var i = 0; i < args.length; i++) {
+        var arg = args[i];
+        
+        if (/^--.+=/.test(arg)) {
+            // Using [\s\S] instead of . because js doesn't support the
+            // 'dotall' regex modifier. See:
+            // http://stackoverflow.com/a/1068308/13216
+            var m = arg.match(/^--([^=]+)=([\s\S]*)$/);
+            var key = m[1];
+            var value = m[2];
+            if (flags.bools[key]) {
+                value = value !== 'false';
+            }
+            setArg(key, value, arg);
+        }
+        else if (/^--no-.+/.test(arg)) {
+            var key = arg.match(/^--no-(.+)/)[1];
+            setArg(key, false, arg);
+        }
+        else if (/^--.+/.test(arg)) {
+            var key = arg.match(/^--(.+)/)[1];
+            var next = args[i + 1];
+            if (next !== undefined && !/^-/.test(next)
+            && !flags.bools[key]
+            && !flags.allBools
+            && (aliases[key] ? !aliasIsBoolean(key) : true)) {
+                setArg(key, next, arg);
+                i++;
+            }
+            else if (/^(true|false)$/.test(next)) {
+                setArg(key, next === 'true', arg);
+                i++;
+            }
+            else {
+                setArg(key, flags.strings[key] ? '' : true, arg);
+            }
+        }
+        else if (/^-[^-]+/.test(arg)) {
+            var letters = arg.slice(1,-1).split('');
+            
+            var broken = false;
+            for (var j = 0; j < letters.length; j++) {
+                var next = arg.slice(j+2);
+                
+                if (next === '-') {
+                    setArg(letters[j], next, arg)
+                    continue;
+                }
+                
+                if (/[A-Za-z]/.test(letters[j]) && /=/.test(next)) {
+                    setArg(letters[j], next.split('=')[1], arg);
+                    broken = true;
+                    break;
+                }
+                
+                if (/[A-Za-z]/.test(letters[j])
+                && /-?\d+(\.\d*)?(e-?\d+)?$/.test(next)) {
+                    setArg(letters[j], next, arg);
+                    broken = true;
+                    break;
+                }
+                
+                if (letters[j+1] && letters[j+1].match(/\W/)) {
+                    setArg(letters[j], arg.slice(j+2), arg);
+                    broken = true;
+                    break;
+                }
+                else {
+                    setArg(letters[j], flags.strings[letters[j]] ? '' : true, arg);
+                }
+            }
+            
+            var key = arg.slice(-1)[0];
+            if (!broken && key !== '-') {
+                if (args[i+1] && !/^(-|--)[^-]/.test(args[i+1])
+                && !flags.bools[key]
+                && (aliases[key] ? !aliasIsBoolean(key) : true)) {
+                    setArg(key, args[i+1], arg);
+                    i++;
+                }
+                else if (args[i+1] && /true|false/.test(args[i+1])) {
+                    setArg(key, args[i+1] === 'true', arg);
+                    i++;
+                }
+                else {
+                    setArg(key, flags.strings[key] ? '' : true, arg);
+                }
+            }
+        }
+        else {
+            if (!flags.unknownFn || flags.unknownFn(arg) !== false) {
+                argv._.push(
+                    flags.strings['_'] || !isNumber(arg) ? arg : Number(arg)
+                );
+            }
+            if (opts.stopEarly) {
+                argv._.push.apply(argv._, args.slice(i + 1));
+                break;
+            }
+        }
+    }
+    
+    Object.keys(defaults).forEach(function (key) {
+        if (!hasKey(argv, key.split('.'))) {
+            setKey(argv, key.split('.'), defaults[key]);
+            
+            (aliases[key] || []).forEach(function (x) {
+                setKey(argv, x.split('.'), defaults[key]);
+            });
+        }
+    });
+    
+    if (opts['--']) {
+        argv['--'] = new Array();
+        notFlags.forEach(function(key) {
+            argv['--'].push(key);
+        });
+    }
+    else {
+        notFlags.forEach(function(key) {
+            argv._.push(key);
+        });
+    }
+
+    return argv;
+};
+
+function hasKey (obj, keys) {
+    var o = obj;
+    keys.slice(0,-1).forEach(function (key) {
+        o = (o[key] || {});
+    });
+
+    var key = keys[keys.length - 1];
+    return key in o;
+}
+
+function isNumber (x) {
+    if (typeof x === 'number') return true;
+    if (/^0x[0-9a-f]+$/i.test(x)) return true;
+    return /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/.test(x);
+}
+

ADDED   gc_lang/fr/nodejs/cli/package.json
Index: gc_lang/fr/nodejs/cli/package.json
==================================================================
--- /dev/null
+++ gc_lang/fr/nodejs/cli/package.json
@@ -0,0 +1,32 @@
+{
+  "name": "grammalecte-cli",
+  "version": "1.0.0",
+  "description": "Grammalecte command line interface",
+  "keywords": [
+    "cli",
+    "french",
+    "grammar",
+    "proofreader"
+  ],
+  "author": "Sébastien GRAVIER, Olivier R.",
+  "license": "GPL-3.0-or-later",
+  "homepage": "https://www.dicollecte.org/",
+  "bin": {
+    "gramma-cli": "bin/gramma-cli.js"
+  },
+  "engines": {
+    "node": ">=9.0.0"
+  },
+  "scripts": {
+    "test": "node bin/gramma-cli.js"
+  },
+  "dependencies": {
+    "grammalecte": "~1.0.0"
+  },
+  "files": [
+    "bin",
+    "lib",
+    "data",
+    "readme.md"
+  ]
+}

ADDED   gc_lang/fr/nodejs/cli/readme.md
Index: gc_lang/fr/nodejs/cli/readme.md
==================================================================
--- /dev/null
+++ gc_lang/fr/nodejs/cli/readme.md
@@ -0,0 +1,132 @@
+# Client/Serveur de Grammalecte pour NodeJS
+
+## Informations
+
+Il y a trois modes de fonctionnement : client / client interactif / serveur.
+
+* Client interactif: `gramma-cli -i`.
+* Client: `gramma-cli --command \"mot/texte\"`.
+* Serveur: lancé avec la commande `gramma-cli --server --port NumPort`.
+
+## Installation
+
+> npm install grammalecte-cli -g
+
+## Commandes
+
+| Commande  | Argument | Description                                                   |
+| --------- | -------- | ------------------------------------------------------------- |
+| help      |          | Affiche les informations que vous lisez ;)                    |
+| perf      | on/off   | Permet d’afficher le temps d’exécution des commandes.         |
+| json      | on/off   | Réponse en format json.                                       |
+| exit      |          | Client interactif : permet de le quitter.                     |
+| text      | texte    | Client / Server: Définir un texte pour plusieurs actions.     |
+| format    | texte    | Permet de mettre en forme le texte.                           |
+| check     | texte    | Vérifie la grammaire et l’orthographe d'un texte.             |
+| lexique   | texte    | Affiche le lexique du texte.                                  |
+| spell     | mot      | Vérifie l’existence d'un mot.                                 |
+| suggest   | mot      | Suggestion des orthographes possible d’un mot.                |
+| morph     | mot      | Affiche les informations pour un mot.                         |
+| lemma     | mot      | Donne le lemme d’un mot.                                      |
+| gceoption | +/-name  | Définit les options à utiliser par le correcteur grammatical. |
+| tfoption  | +/-name  | Définit les options à utiliser par le formateur de texte.     |
+
+## Client interactif
+
+Le mode interactif est un mode question/réponse. Pour le lancer vous devez saisir `gramma-cli -i`.
+
+Exemple pour les vérifications portant sur un mot:
+
+```
+CMD> gramma-cli -i
+Bienvenu sur Grammalecte pour NodeJS!!!
+GrammaJS> suggest salit
+Suggestion possible de: salit
+ ├ salit
+ ├ salît
+ ├ salie
+ ├ salis
+ ├ salir
+ ├ salin
+ ├ sali
+ ├ salait
+ ├ salut
+ └ salât
+GrammaJS> exit
+```
+
+Exemple pour les vérifications portant sur un texte:
+
+```
+CMD> gramma-cli -i
+Bienvenue sur Grammalecte pour NodeJS!!!
+GrammaJS> format
+> salut,les copains!!!
+> vous allez bien ?
+> /format
+Mise en forme:
+salut, les copains!!!
+vous allez bien ?
+GrammaJS> exit
+```
+
+## Client
+
+Exemple simple:
+
+```
+CMD> gramma-cli --spell saluti
+Le mot saluti innexistant
+
+CMD>
+```
+
+Exemple faisant plusieurs actions:
+
+```
+CMD> gramma-cli --lemma --morph --suggest --text salut
+Morph possible de: salut
+ └ >salut/:N:m:s/*
+Lemme possible de: salut
+ └ salut
+Suggestion possible de: salut
+ ├ salut
+ ├ salit
+ ├ salue
+ ├ salua
+ ├ saluai
+ ├ saluts
+ ├ salué
+ ├ saluât
+ ├ salât
+ └ salît
+
+CMD>
+```
+
+Note :
+
+Il ne peut pas y avoir plusieurs fois les arguments text, json, perf, gceoption et tfoption.
+
+Il est cependant possible de vérifier par exemple plusieurs morph en faisant: `gramma-cli --morph mot1 --morph mot2`
+
+## Serveur
+
+Le serveur supporte les requêtes POST et GET...
+
+Par défaut le port d’écoute est le 2212, pour le changer utilisez l’argument `--port` lors du lancement.
+
+## Les fichiers
+
+* bin/gramma-cli.bat  : Fait juste un appel `node gramma-cli.js argument(s)`
+* bin/gramma-cli.js   : Le code principal pour la console
+* data/script.gramma  : Exemple de script pour faire des vérifications automatiques
+  * (sous widows) `type script.gramma | gramma-cli -i`
+  * (sous linux)  `cat script.gramma  | gramma-cli -i`
+* lib/minimist.js     : Une librairie pour simplifier la gestion des arguments
+* package.json        : Fichier d’information pour npm
+* readme.md           : Le fichier que vous lisez (ou pas) actuellement ;)
+
+## Utilisation d'une librairie (incluse)
+
+* [Minimist](https://github.com/substack/minimist) => Simplify parser argument

ADDED   gc_lang/fr/nodejs/core/api.js
Index: gc_lang/fr/nodejs/core/api.js
==================================================================
--- /dev/null
+++ gc_lang/fr/nodejs/core/api.js
@@ -0,0 +1,283 @@
+/*
+    ! Grammalecte, grammar checker !
+    API pour faciliter l'utilisation de Grammalecte.
+*/
+
+/* jshint esversion:6, -W097 */
+/* jslint esversion:6 */
+/* global require, exports, console */
+
+"use strict";
+
+class GrammarChecker {
+
+    constructor(aInit, sLangCode = "fr", sContext = "Javascript") {
+        this.sLangCode = sLangCode;
+        this.sContext = sContext;
+
+        //Importation des fichiers nécessaire
+        this.sPathRoot = __dirname + "/grammalecte";
+        this._helpers = require(this.sPathRoot + "/graphspell/helpers.js");
+
+        this.isInit = {
+            Grammalecte: false,
+            Graphspell: false,
+            Tokenizer: false,
+            TextFormatter: false,
+            Lexicographer: false
+        };
+
+        if (aInit){
+            this.load(aInit);
+        }
+    }
+
+    //Auto-chargement avec dépendence
+    load(aInit = ["Grammalecte", "Graphspell", "TextFormatter", "Lexicographer", "Tokenizer"]){
+        //aInit permet de charger que certain composant
+        // => évite de charger toutes données si par exemple on a besoin que du lexigraphe
+        // => sorte de gestionnaire de dépendence (peut être amélioré)
+        this.isInit = {};
+        if ( aInit.indexOf("Grammalecte") !== false ){
+            //console.log('init Grammalecte');
+            this._oGce = require(this.sPathRoot + "/fr/gc_engine.js");
+            this._oGce.load(this.sContext);
+            this.isInit.Grammalecte = true;
+            this.oSpellChecker = this._oGce.getSpellChecker();
+            this.isInit.Graphspell = true;
+            this.oTokenizer = this.oSpellChecker.getTokenizer();
+            this.isInit.Tokenizer = true;
+        }
+
+        if ( !this.isInit.Graphspell && (aInit.indexOf("Graphspell") !== false || aInit.indexOf("Lexicographer") !== false)){
+            //console.log('init Graphspell');
+            this._SpellChecker = require(this.sPathRoot + "/graphspell/spellchecker.js");
+            this.oSpellChecker = new this._SpellChecker.SpellChecker(this.sLangCode, this.sPathRoot + "/graphspell/_dictionaries");
+            this.isInit.Graphspell = true;
+            this.oTokenizer = this.oSpellChecker.getTokenizer();
+            this.isInit.Tokenizer = true;
+        }
+
+        if ( !this.isInit.Tokenizer && aInit.indexOf("Tokenizer") !== false ){
+            //console.log('init Tokenizer');
+            this._Tokenizer = require(this.sPathRoot + "/graphspell/tokenizer.js");
+            this.oTokenizer = new this._Tokenizer.Tokenizer(this.sLangCode);
+            this.isInit.Tokenizer = true;
+        }
+
+        if ( aInit.indexOf("TextFormatter") !== false ){
+            //console.log('init TextFormatter');
+            this._oText = require(this.sPathRoot + "/fr/textformatter.js");
+            this.oTextFormatter = new this._oText.TextFormatter();
+            this.isInit.TextFormatter = true;
+        }
+
+        if ( aInit.indexOf("Lexicographer") !== false ){
+            //console.log('init Lexicographer');
+            this._oLex = require(this.sPathRoot + "/fr/lexicographe.js");
+            this.oLexicographer = new this._oLex.Lexicographe(
+                this.oSpellChecker,
+                this.oTokenizer,
+                this._helpers.loadFile(this.sPathRoot + "/fr/locutions_data.json")
+            );
+            this.isInit.Lexicographer = true;
+        }
+    }
+
+    //Fonctions concernant: Grammalecte
+    getGrammalecte(){
+        if (!this.isInit.Grammalecte) {
+            this.load(["Grammalecte"]);
+        }
+        return this._oGce;
+    }
+
+    gramma(sText){
+        if (!this.isInit.Grammalecte) {
+            this.load(["Grammalecte"]);
+        }
+        return Array.from(this._oGce.parse(sText, this.sLangCode));
+    }
+
+    getGceOptions () {
+        if (!this.isInit.Grammalecte) {
+            this.load(["Grammalecte"]);
+        }
+        return this._helpers.mapToObject(this._oGce.getOptions());
+    }
+
+    getGceDefaultOptions () {
+        if (!this.isInit.Grammalecte) {
+            this.load(["Grammalecte"]);
+        }
+        return this._helpers.mapToObject(this._oGce.getDefaultOptions());
+    }
+
+    setGceOptions (dOptions) {
+        if (!this.isInit.Grammalecte) {
+            this.load(["Grammalecte"]);
+        }
+        if (!(dOptions instanceof Map)) {
+            dOptions = this._helpers.objectToMap(dOptions);
+        }
+        this._oGce.setOptions(dOptions);
+        return this._helpers.mapToObject(this._oGce.getOptions());
+    }
+
+    setGceOption (sOptName, bValue) {
+        if (!this.isInit.Grammalecte) {
+            this.load(["Grammalecte"]);
+        }
+        if (sOptName) {
+            this._oGce.setOption(sOptName, bValue);
+            return this._helpers.mapToObject(this._oGce.getOptions());
+        }
+        return false;
+    }
+
+    resetGceOptions () {
+        if (!this.isInit.Grammalecte) {
+            this.load(["Grammalecte"]);
+        }
+        this._oGce.resetOptions();
+        return this._helpers.mapToObject(this._oGce.getOptions());
+    }
+
+    //Fonctions concernant: Graphspell
+    getGraphspell(){
+        if (!this.isInit.Graphspell) {
+            this.load(["Graphspell"]);
+        }
+        return this.oSpellChecker;
+    }
+
+    spellParagraph(sText, bSuggest = true){
+        if (!this.isInit.Graphspell) {
+            this.load(["Graphspell"]);
+        }
+        if (bSuggest){
+            let lError = this.oSpellChecker.parseParagraph(sText);
+            for (let token of lError) {
+                token.aSuggestions = this.suggest(token.sValue);
+            }
+            return lError;
+        } else {
+            return this.oSpellChecker.parseParagraph(sText);
+        }
+    }
+
+    spell(sWord){
+        if (!this.isInit.Graphspell) {
+            this.load(["Graphspell"]);
+        }
+        return this.oSpellChecker.isValid(sWord);
+    }
+
+    suggest(sWord, nbLimit = 10, bMerge = true){
+        if (!this.isInit.Graphspell) {
+            this.load(["Graphspell"]);
+        }
+        let lSuggest = this.oSpellChecker.suggest(sWord, nbLimit);
+        if (bMerge){
+            let lSuggestRep = [];
+            for (let lSuggestTmp of lSuggest) {
+                for (let word of lSuggestTmp) {
+                    lSuggestRep.push(word);
+                }
+            }
+            return lSuggestRep;
+        } else {
+            return Array.from(lSuggest);
+        }
+
+    }
+
+    lemma(sWord){
+        if (!this.isInit.Graphspell) {
+            this.load(["Graphspell"]);
+        }
+        return this.oSpellChecker.getLemma(sWord);
+    }
+
+    morph(sWord){
+        if (!this.isInit.Graphspell) {
+            this.load(["Graphspell"]);
+        }
+        return this.oSpellChecker.getMorph(sWord);
+    }
+
+    //Fonctions concernant: Lexicographer
+    getLexicographer(){
+        if (!this.isInit.Lexicographer) {
+            this.load(["Lexicographer"]);
+        }
+        return this.oLexicographer;
+    }
+
+    lexique(sText){
+        if (!this.isInit.Lexicographer) {
+            this.load(["Lexicographer"]);
+        }
+        return this.oLexicographer.getListOfTokensReduc(sText);
+    }
+
+    //Fonctions concernant: TextFormatter
+    getTextFormatter(){
+        if (!this.isInit.TextFormatter) {
+            this.load(["TextFormatter"]);
+        }
+        return this.oTextFormatter;
+    }
+
+    formatText(sText){
+        if (!this.isInit.TextFormatter) {
+            this.load(["TextFormatter"]);
+        }
+        return this.oTextFormatter.formatText(sText);
+    }
+
+    setTfOptions(dOptions) {
+        if (!this.isInit.TextFormatter) {
+            this.load(["TextFormatter"]);
+        }
+        this.oTextFormatter.setOptions(dOptions);
+        return this._helpers.mapToObject(this.oTextFormatter.getOptions());
+    }
+
+    setTfOption(sOptName, bValue) {
+        if (!this.isInit.TextFormatter) {
+            this.load(["TextFormatter"]);
+        }
+        if (sOptName) {
+            let optionsTF = this.oTextFormatter.getOptions();
+            optionsTF.set(sOptName, bValue);
+            return this._helpers.mapToObject(this.oTextFormatter.getOptions());
+        }
+        return false;
+    }
+
+    resetTfOptions() {
+        if (!this.isInit.TextFormatter) {
+            this.load(["TextFormatter"]);
+        }
+        let optionsTF = this.oTextFormatter.getDefaultOptions();
+        this.oTextFormatter.setOptions(optionsTF);
+        return this._helpers.mapToObject(this.oTextFormatter.getOptions());
+    }
+
+    //fonctions concernant plussieurs parties
+    verifParagraph(sText, bSuggest = true){
+        if (!this.isInit.Grammalecte || !this.isInit.Graphspell) {
+            this.load(["Grammalecte"]);
+        }
+        return {
+            lGrammarErrors: Array.from(this._oGce.parse(sText, this.sLangCode)),
+            lSpellingErrors: this.spellParagraph(sText, bSuggest)
+        };
+    }
+
+}
+
+if (typeof exports !== "undefined") {
+    exports.GrammarChecker = GrammarChecker;
+}

ADDED   gc_lang/fr/nodejs/core/package.json
Index: gc_lang/fr/nodejs/core/package.json
==================================================================
--- /dev/null
+++ gc_lang/fr/nodejs/core/package.json
@@ -0,0 +1,20 @@
+{
+  "name": "grammalecte",
+  "version": "1.0.0",
+  "description": "Grammalecte is a grammar proofreader",
+  "keywords": [
+    "french",
+    "grammar",
+    "proofreader"
+  ],
+  "author": "Olivier R.",
+  "license": "GPL-3.0-or-later",
+  "homepage": "https://www.dicollecte.org/",
+  "main": "api.js",
+  "engines": {
+    "node": ">=9.0.0"
+  },
+  "scripts": {
+    "test": "echo \"Error: no test specified\""
+  }
+}

ADDED   gc_lang/fr/nodejs/core/readme.md
Index: gc_lang/fr/nodejs/core/readme.md
==================================================================
--- /dev/null
+++ gc_lang/fr/nodejs/core/readme.md
@@ -0,0 +1,19 @@
+# Grammalecte pour NodeJS
+
+## Informations
+
+Grammalecte est un correcteur grammatical open source dédié à la langue française.
+Site Internet : https://www.dicollecte.org/
+
+## Installation
+
+Dans votre répertoire de développement :
+
+> npm install grammalecte
+
+## Les fichiers
+
+* grammalecte/*       : Tout le contenu de Grammalecte pour JavaScript
+* api.js              : Une API pour simplifier l’utilisation de Grammalecte
+* package.json        : Fichier d’information pour npm
+* readme.md           : Le fichier que vous lisez (ou pas) actuellement ;)

ADDED   gc_lang/fr/nodejs/note.md
Index: gc_lang/fr/nodejs/note.md
==================================================================
--- /dev/null
+++ gc_lang/fr/nodejs/note.md
@@ -0,0 +1,23 @@
+# Note pour le dévellepement pour NodeJS
+
+## Commande pour définir l’utilisation d’un paquetage local
+
+```
+cd core
+npm link
+cd ..
+cd cli
+npm link grammalecte
+npm install --global
+cd ..
+```
+
+## Commande désinstaller le paquetage local et son utilisation
+
+```
+npm rm grammalecte --global
+cd cli
+npm unlink grammalecte
+npm rm grammalecte-cli --global
+cd ..
+```

Index: gc_lang/fr/webext/content_scripts/panel_tf.js
==================================================================
--- gc_lang/fr/webext/content_scripts/panel_tf.js
+++ gc_lang/fr/webext/content_scripts/panel_tf.js
@@ -9,10 +9,16 @@
     constructor (...args) {
         super(...args);
         this.xTFNode = this._createTextFormatter();
         this.xPanelContent.appendChild(this.xTFNode);
         this.xTextArea = null;
+
+        this.TextFormatter = new TextFormatter();
+        this.formatText = this.TextFormatter.formatTextRuleCount;
+        this.removeHyphenAtEndOfParagraphs = this.TextFormatter.removeHyphenAtEndOfParagraphsCount;
+        this.mergeContiguousParagraphs = this.TextFormatter.mergeContiguousParagraphsCount;
+        this.getParagraph = this.TextFormatter.getParagraph;
     }
 
     _createTextFormatter () {
         let xTFNode = document.createElement("div");
         try {
@@ -157,11 +163,11 @@
         xLine.appendChild(this._createOption("o_ordinals_no_exponant", true, "Ordinaux (15e, XXIe…)"));
         xLine.appendChild(this._createOption("o_ordinals_exponant", true, "e → ᵉ"));
         xLine.appendChild(oGrammalecte.createNode("div", {id: "res_"+"o_ordinals_no_exponant", className: "grammalecte_tf_result", textContent: "·"}));
         return xLine;
     }
-    
+
     /*
         Actions
     */
     start (xNode) {
         if (xNode !== null && xNode.tagName == "TEXTAREA") {
@@ -258,11 +264,11 @@
             //window.setCursor("wait"); // change pointer
             this.resetProgressBar();
             let sText = this.xTextArea.value.normalize("NFC");
             document.getElementById('grammalecte_tf_progressbar').max = 7;
             let n1 = 0, n2 = 0, n3 = 0, n4 = 0, n5 = 0, n6 = 0, n7 = 0;
-            
+
             // Restructuration
             if (this.isSelected("o_group_struct")) {
                 if (this.isSelected("o_remove_hyphens_at_end_of_paragraphs")) {
                     [sText, n1] = this.removeHyphenAtEndOfParagraphs(sText);
                     document.getElementById('res_o_remove_hyphens_at_end_of_paragraphs').textContent = n1;
@@ -508,61 +514,10 @@
         catch (e) {
             showError(e);
         }
     }
 
-    formatText (sText, sOptName) {
-        let nCount = 0;
-        try {
-            if (!oReplTable.hasOwnProperty(sOptName)) {
-                console.log("# Error. TF: there is no option “" + sOptName+ "”.");
-                return [sText, nCount];
-            }
-            for (let [zRgx, sRep] of oReplTable[sOptName]) {
-                nCount += (sText.match(zRgx) || []).length;
-                sText = sText.replace(zRgx, sRep);
-            }
-        }
-        catch (e) {
-            showError(e);
-        }
-        return [sText, nCount];
-    }
-
-    removeHyphenAtEndOfParagraphs (sText) {
-        let nCount = (sText.match(/-[  ]*\n/gm) || []).length;
-        sText = sText.replace(/-[  ]*\n/gm, "");
-        return [sText, nCount];
-    }
-
-    mergeContiguousParagraphs (sText) {
-        let nCount = 0;
-        sText = sText.replace(/^[  ]+$/gm, ""); // clear empty paragraphs
-        let s = "";
-        for (let sParagraph of this.getParagraph(sText)) {
-            if (sParagraph === "") {
-                s += "\n";
-            } else {
-                s += sParagraph + " ";
-                nCount += 1;
-            }
-        }
-        s = s.replace(/  +/gm, " ").replace(/ $/gm, "");
-        return [s, nCount];
-    }
-
-    * getParagraph (sText) {
-        // generator: returns paragraphs of text
-        let iStart = 0;
-        let iEnd = 0;
-        while ((iEnd = sText.indexOf("\n", iStart)) !== -1) {
-            yield sText.slice(iStart, iEnd);
-            iStart = iEnd + 1;
-        }
-        yield sText.slice(iStart);
-    }
-
     getTimeRes (n) {
         // returns duration in seconds as string
         if (n < 10) {
             return n.toFixed(3).toString() + " s";
         }

Index: graphspell-js/char_player.js
==================================================================
--- graphspell-js/char_player.js
+++ graphspell-js/char_player.js
@@ -1,8 +1,11 @@
 // list of similar chars
 // useful for suggestion mechanism
 
+/* jshint esversion:6 */
+/* jslint esversion:6 */
+
 ${map}
 
 
 var char_player = {
 
@@ -23,11 +26,11 @@
         ['â', 'a'],  ['è', 'e'],  ['ï', 'i'],  ['ö', 'o'],  ['ù', 'u'],  ['ŷ', 'i'],
         ['ä', 'a'],  ['ê', 'e'],  ['í', 'i'],  ['ó', 'o'],  ['ü', 'u'],  ['ý', 'i'],
         ['á', 'a'],  ['ë', 'e'],  ['ì', 'i'],  ['ò', 'o'],  ['ú', 'u'],  ['ỳ', 'i'],
         ['ā', 'a'],  ['ē', 'e'],  ['ī', 'i'],  ['ō', 'o'],  ['ū', 'u'],  ['ȳ', 'i'],
         ['ç', 'c'],  ['ñ', 'n'],  ['k', 'q'],  ['w', 'v'],
-        ['œ', 'oe'], ['æ', 'ae'], 
+        ['œ', 'oe'], ['æ', 'ae'],
         ['ſ', 's'],  ['ffi', 'ffi'],  ['ffl', 'ffl'],  ['ff', 'ff'],  ['ſt', 'ft'],  ['fi', 'fi'],  ['fl', 'fl'],  ['st', 'st']
     ]),
 
     simplifyWord: function (sWord) {
         // word simplication before calculating distance between words
@@ -102,11 +105,11 @@
         ["f", "fF"],
         ["F", "Ff"],
 
         ["g", "gGjJĵĴ"],
         ["G", "GgJjĴĵ"],
-        
+
         ["h", "hH"],
         ["H", "Hh"],
 
         ["i", "iIîÎïÏyYíÍìÌīĪÿŸ"],
         ["I", "IiÎîÏïYyÍíÌìĪīŸÿ"],
@@ -380,11 +383,11 @@
     // Other functions
     filterSugg: function (aSugg) {
         return aSugg.filter((sSugg) => { return !sSugg.endsWith("è") && !sSugg.endsWith("È"); });
     }
 
-}
+};
 
 
 if (typeof(exports) !== 'undefined') {
     exports._xTransCharsForSpelling = char_player._xTransCharsForSpelling;
     exports.spellingNormalization = char_player.spellingNormalization;

Index: graphspell-js/dawg.js
==================================================================
--- graphspell-js/dawg.js
+++ graphspell-js/dawg.js
@@ -6,17 +6,21 @@
 // License: MPL 2
 //
 // This tool encodes lexicon into an indexable binary dictionary
 // Input files MUST be encoded in UTF-8.
 
+/* jshint esversion:6, -W097 */
+/* jslint esversion:6 */
+/* global require, exports, console, helpers */
+
 "use strict";
 
-
-if (typeof(require) !== 'undefined') {
+if(typeof(process) !== 'undefined') {
+    var str_transform = require("./str_transform.js");
+} else if (typeof(require) !== 'undefined') {
     var str_transform = require("resource://grammalecte/graphspell/str_transform.js");
 }
-
 
 ${map}
 
 
 class DAWG {
@@ -98,11 +102,11 @@
             let lTemp = [];
             for (let c of sFlex) {
                 lTemp.push(dChar.get(c));
             }
             lTemp.push(iAff+nChar);
-            lTemp.push(iTag+nChar+nAff)
+            lTemp.push(iTag+nChar+nAff);
             lWord.push(lTemp);
         }
         lEntry.length = 0; // clear the array
 
         // Dictionary of arc values occurrency, to sort arcs of each node
@@ -428,11 +432,11 @@
     },
 
     reset: function () {
         this.nNextId = 0;
     }
-}
+};
 
 
 class DawgNode {
 
     constructor () {
@@ -542,11 +546,11 @@
         let sHexVal = nVal.toString(16); // conversion to hexadecimal string
         //console.log(`value: ${nVal} in ${nByte} bytes`);
         if (sHexVal.length < (nByte*2)) {
             return "0".repeat((nByte*2) - sHexVal.length) + sHexVal;
         } else if (sHexVal.length == (nByte*2)) {
-            return sHexVal
+            return sHexVal;
         } else {
             throw "Conversion to byte string: value bigger than allowed.";
         }
     }
 }

Index: graphspell-js/helpers.js
==================================================================
--- graphspell-js/helpers.js
+++ graphspell-js/helpers.js
@@ -1,9 +1,10 @@
-
 // HELPERS
-/*jslint esversion: 6*/
-/*global console,require,exports,XMLHttpRequest*/
+
+/* jshint esversion:6, -W097 */
+/* jslint esversion:6 */
+/* global require, exports, console, XMLHttpRequest */
 
 "use strict";
 
 
 var helpers = {
@@ -20,17 +21,23 @@
     loadFile: function (spf) {
         // load ressources in workers (suggested by Mozilla extensions reviewers)
         // for more options have a look here: https://gist.github.com/Noitidart/ec1e6b9a593ec7e3efed
         // if not in workers, use sdk/data.load() instead
         try {
-            console.log("loadFile: " + spf);
-            let xRequest;
-            xRequest = new XMLHttpRequest();
-            xRequest.open('GET', spf, false); // 3rd arg is false for synchronous, sync is acceptable in workers
-            xRequest.overrideMimeType('text/json');
-            xRequest.send();
-            return xRequest.responseText;
+            if(typeof(process) !== 'undefined') {
+                //console.log('loadFile(disque): ' + spf);
+                let fs = require("fs");
+                return fs.readFileSync(spf, "utf8");
+            } else {
+                console.log("loadFile: " + spf);
+                let xRequest;
+                xRequest = new XMLHttpRequest();
+                xRequest.open('GET', spf, false); // 3rd arg is false for synchronous, sync is acceptable in workers
+                xRequest.overrideMimeType('text/json');
+                xRequest.send();
+                return xRequest.responseText;
+            }
         }
         catch (e) {
             console.error(e);
             return null;
         }

Index: graphspell-js/ibdawg.js
==================================================================
--- graphspell-js/ibdawg.js
+++ graphspell-js/ibdawg.js
@@ -1,18 +1,22 @@
-//// IBDAWG
-/*jslint esversion: 6*/
-/*global console,require,exports*/
+// IBDAWG
+
+/* jshint esversion:6, -W097 */
+/* jslint esversion:6 */
+/* global require, exports, console, __dirname */
 
 "use strict";
 
-
-if (typeof(require) !== 'undefined') {
+if(typeof(process) !== 'undefined') {
+    var str_transform = require("./str_transform.js");
+    var helpers = require("./helpers.js");
+    var char_player = require("./char_player.js");
+} else if (typeof(require) !== 'undefined') {
     var str_transform = require("resource://grammalecte/graphspell/str_transform.js");
     var helpers = require("resource://grammalecte/graphspell/helpers.js");
     var char_player = require("resource://grammalecte/graphspell/char_player.js");
 }
-
 
 // Don’t remove <string>. Necessary in TB.
 ${string}
 ${map}
 ${set}
@@ -95,11 +99,16 @@
     constructor (param1, sPath="") {
         // param1 can be a filename or a object with all the necessary data.
         try {
             let oData = null;
             if (typeof(param1) == "string") {
-                let sURL = (sPath !== "") ? sPath + "/" + param1 : "resource://grammalecte/graphspell/_dictionaries/"+param1;
+                let sURL;
+                if(typeof(process) !== 'undefined') {
+                    sURL = (sPath !== "") ? sPath + "/" + param1 : __dirname + "/_dictionaries/"+param1;
+                } else {
+                    sURL = (sPath !== "") ? sPath + "/" + param1 : "resource://grammalecte/graphspell/_dictionaries/"+param1;
+                }
                 oData = JSON.parse(helpers.loadFile(sURL));
             } else {
                 oData = param1;
             }
             Object.assign(this, oData);
@@ -221,11 +230,11 @@
         return oJSON;
     }
 
     isValidToken (sToken) {
         // checks if sToken is valid (if there is hyphens in sToken, sToken is split, each part is checked)
-        sToken = char_player.spellingNormalization(sToken)
+        sToken = char_player.spellingNormalization(sToken);
         if (this.isValid(sToken)) {
             return true;
         }
         if (sToken.includes("-")) {
             if (sToken.gl_count("-") > 4) {
@@ -298,11 +307,11 @@
         return Boolean(this._convBytesToInteger(this.byDic.slice(iAddr, iAddr+this.nBytesArc)) & this._finalNodeMask);
     }
 
     getMorph (sWord) {
         // retrieves morphologies list, different casing allowed
-        sWord = char_player.spellingNormalization(sWord)
+        sWord = char_player.spellingNormalization(sWord);
         let l = this.morph(sWord);
         if (sWord[0].gl_isUpperCase()) {
             l.push(...this.morph(sWord.toLowerCase()));
             if (sWord.gl_isUpperCase() && sWord.length > 1) {
                 l.push(...this.morph(sWord.gl_toCapitalize()));
@@ -311,12 +320,12 @@
         return l;
     }
 
     suggest (sWord, nSuggLimit=10) {
         // returns a array of suggestions for <sWord>
-        //const t0 = Date.now();
-        sWord = char_player.spellingNormalization(sWord)
+        //console.time("Suggestions for " + sWord);
+        sWord = char_player.spellingNormalization(sWord);
         let sPfx = "";
         let sSfx = "";
         [sPfx, sWord, sSfx] = char_player.cut(sWord);
         let nMaxSwitch = Math.max(Math.floor(sWord.length / 3), 1);
         let nMaxDel = Math.floor(sWord.length / 5);
@@ -325,14 +334,13 @@
         let oSuggResult = new SuggResult(sWord);
         this._suggest(oSuggResult, sWord, nMaxSwitch, nMaxDel, nMaxHardRepl, nMaxJump);
         let aSugg = oSuggResult.getSuggestions(nSuggLimit);
         if (sSfx || sPfx) {
             // we add what we removed
-            return aSugg.map( (sSugg) => { return sPfx + sSugg + sSfx } );
+            return aSugg.map( (sSugg) => { return sPfx + sSugg + sSfx; } );
         }
-        //const t1 = Date.now();
-        //console.log("Suggestions for " + sWord + " in " + ((t1-t0)/1000).toString() + " s");
+        //console.timeEnd("Suggestions for " + sWord);
         return aSugg;
     }
 
     _suggest (oSuggResult, sRemain, nMaxSwitch=0, nMaxDel=0, nMaxHardRepl=0, nMaxJump=0, nDist=0, nDeep=0, iAddr=0, sNewWord="", bAvoidLoop=false) {
         // returns a set of suggestions
@@ -589,11 +597,11 @@
             }
         }
     }
 
     * _getArcs1 (iAddr) {
-        "generator: return all arcs at <iAddr> as tuples of (nVal, iAddr)"
+        // generator: return all arcs at <iAddr> as tuples of (nVal, iAddr)
         while (true) {
             let iEndArcAddr = iAddr+this.nBytesArc;
             let nRawArc = this._convBytesToInteger(this.byDic.slice(iAddr, iEndArcAddr));
             yield [nRawArc & this._arcMask, this._convBytesToInteger(this.byDic.slice(iEndArcAddr, iEndArcAddr+this.nBytesNodeAddress))];
             if (nRawArc & this._lastArcMask) {

Index: graphspell-js/spellchecker.js
==================================================================
--- graphspell-js/spellchecker.js
+++ graphspell-js/spellchecker.js
@@ -6,19 +6,23 @@
 // - the main dictionary, bundled with the package
 // - the extended dictionary
 // - the community dictionary, added by an organization
 // - the personal dictionary, created by the user for its own convenience
 
+/* jshint esversion:6, -W097 */
+/* jslint esversion:6 */
+/* global require, exports, console, IBDAWG, Tokenizer */
 
 "use strict";
 
-
-if (typeof(require) !== 'undefined') {
+if(typeof(process) !== 'undefined') {
+    var ibdawg = require("./ibdawg.js");
+    var tokenizer = require("./tokenizer.js");
+} else if (typeof(require) !== 'undefined') {
     var ibdawg = require("resource://grammalecte/graphspell/ibdawg.js");
     var tokenizer = require("resource://grammalecte/graphspell/tokenizer.js");
 }
-
 
 ${map}
 
 
 const dDefaultDictionaries = new Map([
@@ -64,11 +68,11 @@
         catch (e) {
             let sfDictionary = (typeof(dictionary) == "string") ? dictionary : dictionary.sLangName + "/" + dictionary.sFileName;
             if (bNecessary) {
                 throw "Error: <" + sfDictionary + "> not loaded. " + e.message;
             }
-            console.log("Error: <" + sfDictionary + "> not loaded.")
+            console.log("Error: <" + sfDictionary + "> not loaded.");
             console.log(e.message);
             return null;
         }
     }
 
@@ -195,11 +199,11 @@
             return true;
         }
         if (this.bExtendedDic && this.oExtendedDic.isValid(sWord)) {
             return true;
         }
-        if (this.bCommunityDic && this.oCommunityDic.isValid(sToken)) {
+        if (this.bCommunityDic && this.oCommunityDic.isValid(sWord)) {
             return true;
         }
         if (this.bPersonalDic && this.oPersonalDic.isValid(sWord)) {
             return true;
         }
@@ -212,11 +216,11 @@
             return true;
         }
         if (this.bExtendedDic && this.oExtendedDic.lookup(sWord)) {
             return true;
         }
-        if (this.bCommunityDic && this.oCommunityDic.lookup(sToken)) {
+        if (this.bCommunityDic && this.oCommunityDic.lookup(sWord)) {
             return true;
         }
         if (this.bPersonalDic && this.oPersonalDic.lookup(sWord)) {
             return true;
         }
@@ -271,11 +275,11 @@
         }
     }
 
     * select (sFlexPattern="", sTagsPattern="") {
         // generator: returns all entries which flexion fits <sFlexPattern> and morphology fits <sTagsPattern>
-        yield* this.oMainDic.select(sFlexPattern, sTagsPattern)
+        yield* this.oMainDic.select(sFlexPattern, sTagsPattern);
         if (this.bExtendedDic) {
             yield* this.oExtendedDic.select(sFlexPattern, sTagsPattern);
         }
         if (this.bCommunityDic) {
             yield* this.oCommunityDic.select(sFlexPattern, sTagsPattern);

Index: graphspell-js/str_transform.js
==================================================================
--- graphspell-js/str_transform.js
+++ graphspell-js/str_transform.js
@@ -1,7 +1,10 @@
-//// STRING TRANSFORMATION
-/*jslint esversion: 6*/
+// STRING TRANSFORMATION
+
+/* jshint esversion:6, -W097 */
+/* jslint esversion:6 */
+/* global exports, console */
 
 "use strict";
 
 
 // Note: 48 is the ASCII code for "0"

Index: graphspell-js/tokenizer.js
==================================================================
--- graphspell-js/tokenizer.js
+++ graphspell-js/tokenizer.js
@@ -1,9 +1,11 @@
 // JavaScript
 // Very simple tokenizer
-/*jslint esversion: 6*/
-/*global require,exports*/
+
+/* jshint esversion:6, -W097 */
+/* jslint esversion:6 */
+/*global require, exports, console*/
 
 "use strict";
 
 
 const aTkzPatterns = {
@@ -70,11 +72,11 @@
             for (let [zRegex, sType] of this.aRules) {
                 if (sType !== "SPACE"  ||  bWithSpaces) {
                     try {
                         if ((m = zRegex.exec(sText)) !== null) {
                             iToken += 1;
-                            yield { "i": iToken, "sType": sType, "sValue": m[0], "nStart": iNext, "nEnd": iNext + m[0].length }
+                            yield { "i": iToken, "sType": sType, "sValue": m[0], "nStart": iNext, "nEnd": iNext + m[0].length };
                             iCut = m[0].length;
                             break;
                         }
                     }
                     catch (e) {

Index: make.py
==================================================================
--- make.py
+++ make.py
@@ -184,11 +184,11 @@
     for sf in os.listdir(spLangPack):
         if not os.path.isdir(spLangPack+"/"+sf):
             hZip.write(spLangPack+"/"+sf, sAddPath+spLangPack+"/"+sf)
 
 
-def create (sLang, xConfig, bInstallOXT, bJavaScript):
+def create (sLang, xConfig, bInstallOXT, bJavaScript, bUseCache):
     "make Grammalecte for project <sLang>"
     oNow = datetime.datetime.now()
     print("============== MAKE GRAMMALECTE [{0}] at {1.hour:>2} h {1.minute:>2} min {1.second:>2} s ==============".format(sLang, oNow))
 
     #### READ CONFIGURATION
@@ -198,11 +198,11 @@
     dVars = xConfig._sections['args']
     dVars['locales'] = dVars["locales"].replace("_", "-")
     dVars['loc'] = str(dict([ [s, [s[0:2], s[3:5], ""]] for s in dVars["locales"].split(" ") ]))
 
     ## COMPILE RULES
-    dResult = compile_rules.make(spLang, dVars['lang'], bJavaScript)
+    dResult = compile_rules.make(spLang, dVars['lang'], bUseCache)
     dVars.update(dResult)
 
     ## READ GRAMMAR CHECKER PLUGINS
     print("PYTHON:")
     print("+ Plugins: ", end="")
@@ -373,10 +373,11 @@
 def main ():
     "build Grammalecte with requested options"
     print("Python: " + sys.version)
     xParser = argparse.ArgumentParser()
     xParser.add_argument("lang", type=str, nargs='+', help="lang project to generate (name of folder in /lang)")
+    xParser.add_argument("-uc", "--use_cache", help="use data cache instead of rebuilding rules", action="store_true")
     xParser.add_argument("-b", "--build_data", help="launch build_data.py (part 1 and 2)", action="store_true")
     xParser.add_argument("-bb", "--build_data_before", help="launch build_data.py (only part 1: before dictionary building)", action="store_true")
     xParser.add_argument("-ba", "--build_data_after", help="launch build_data.py (only part 2: before dictionary building)", action="store_true")
     xParser.add_argument("-d", "--dict", help="generate FSA dictionary", action="store_true")
     xParser.add_argument("-t", "--tests", help="run unit tests", action="store_true")
@@ -440,11 +441,11 @@
 
             # copy dictionaries from Graphspell
             copyGraphspellDictionaries(dVars, xArgs.javascript, xArgs.add_extended_dictionary, xArgs.add_community_dictionary, xArgs.add_personal_dictionary)
 
             # make
-            sVersion = create(sLang, xConfig, xArgs.install, xArgs.javascript, )
+            sVersion = create(sLang, xConfig, xArgs.install, xArgs.javascript, xArgs.use_cache)
 
             # tests
             if xArgs.tests or xArgs.perf or xArgs.perf_memo:
                 print("> Running tests")
                 try: