Index: gc_core/js/helpers.js
==================================================================
--- gc_core/js/helpers.js
+++ gc_core/js/helpers.js
@@ -5,94 +5,95 @@
 
 // In Firefox, there is no console.log in PromiseWorker, but there is worker.log.
 // In Thunderbird, you can’t access to console directly. So it’s required to pass a log function.
 let funcOutput = null;
 
-function setLogOutput (func) {
-    funcOutput = func;
-}
-
-function echo (obj) {
-    if (funcOutput !== null) {
-        funcOutput(obj);
-    } else {
-        console.log(obj);
-    }
-    return true;
-}
-
-function logerror (e, bStack=false) {
-    let sMsg = "\n" + e.fileName + "\n" + e.name + "\nline: " + e.lineNumber + "\n" + e.message;
-    if (bStack) {
-        sMsg += "\n--- Stack ---\n" + e.stack;
-    }
-    if (funcOutput !== null) {
-        funcOutput(sMsg);
-    } else {
-        console.error(sMsg);
-    }
-}
-
-function inspect (o) {
-    let sMsg = "__inspect__: " + typeof o;
-    for (let sParam in o) {
-        sMsg += "\n" + sParam + ": " + o.sParam;
-    }
-    sMsg += "\n" + JSON.stringify(o) + "\n__end__";
-    echo(sMsg);
-}
-
-
-// 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
-function loadFile (spf) {
-    try {
-        let xRequest;
-        if (typeof XMLHttpRequest !== "undefined") {
-            xRequest = new XMLHttpRequest();
-        }
-        else {
-            // JS bullshit again… necessary for Thunderbird
-            let { Cc, Ci } = require("chrome");
-            xRequest = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance();
-            xRequest.QueryInterface(Ci.nsIXMLHttpRequest);
-        }
-        xRequest.open('GET', spf, false); // 3rd arg is false for synchronous, sync is acceptable in workers
-        xRequest.send();
-        return xRequest.responseText;
-    }
-    catch (e) {
-        logerror(e);
-        return null
-    }
-}
-
-
-// conversions
-function objectToMap (obj) {
-    let m = new Map();
-    for (let param in obj) {
-        //console.log(param + " " + obj[param]);
-        m.set(param, obj[param]);
-    }
-    return m;
-}
-
-function mapToObject (m) {
-    let obj = {};
-    for (let [k, v] of m) {
-        obj[k] = v;
-    }
-    return obj;
-}
-
-
-if (typeof(exports) !== 'undefined') {
-    exports.setLogOutput = setLogOutput;
-    exports.echo = echo;
-    exports.logerror = logerror;
-    exports.inspect = inspect;
-    exports.loadFile = loadFile;
-    exports.objectToMap = objectToMap;
-    exports.mapToObject = mapToObject;
+var helpers = {
+
+    setLogOutput: function (func) {
+        funcOutput = func;
+    },
+
+    echo: function (obj) {
+        if (funcOutput !== null) {
+            funcOutput(obj);
+        } else {
+            console.log(obj);
+        }
+        return true;
+    },
+
+    logerror: function (e, bStack=false) {
+        let sMsg = "\n" + e.fileName + "\n" + e.name + "\nline: " + e.lineNumber + "\n" + e.message;
+        if (bStack) {
+            sMsg += "\n--- Stack ---\n" + e.stack;
+        }
+        if (funcOutput !== null) {
+            funcOutput(sMsg);
+        } else {
+            console.error(sMsg);
+        }
+    },
+
+    inspect: function (o) {
+        let sMsg = "__inspect__: " + typeof o;
+        for (let sParam in o) {
+            sMsg += "\n" + sParam + ": " + o.sParam;
+        }
+        sMsg += "\n" + JSON.stringify(o) + "\n__end__";
+        this.echo(sMsg);
+    },
+
+    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 {
+            let xRequest;
+            if (typeof XMLHttpRequest !== "undefined") {
+                xRequest = new XMLHttpRequest();
+            }
+            else {
+                // JS bullshit again… necessary for Thunderbird
+                let { Cc, Ci } = require("chrome");
+                xRequest = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance();
+                xRequest.QueryInterface(Ci.nsIXMLHttpRequest);
+            }
+            xRequest.open('GET', spf, false); // 3rd arg is false for synchronous, sync is acceptable in workers
+            xRequest.send();
+            return xRequest.responseText;
+        }
+        catch (e) {
+            this.logerror(e);
+            return null
+        }
+    },
+
+    // conversions
+    objectToMap: function (obj) {
+        let m = new Map();
+        for (let param in obj) {
+            //console.log(param + " " + obj[param]);
+            m.set(param, obj[param]);
+        }
+        return m;
+    },
+
+    mapToObject: function (m) {
+        let obj = {};
+        for (let [k, v] of m) {
+            obj[k] = v;
+        }
+        return obj;
+    }
+}
+
+
+if (typeof(exports) !== 'undefined') {
+    exports.setLogOutput = helpers.setLogOutput;
+    exports.echo = helpers.echo;
+    exports.logerror = helpers.logerror;
+    exports.inspect = helpers.inspect;
+    exports.loadFile = helpers.loadFile;
+    exports.objectToMap = helpers.objectToMap;
+    exports.mapToObject = helpers.mapToObject;
 }

Index: gc_core/js/ibdawg.js
==================================================================
--- gc_core/js/ibdawg.js
+++ gc_core/js/ibdawg.js
@@ -1,11 +1,14 @@
 //// IBDAWG
 
 "use strict";
 
-const st = require("resource://grammalecte/str_transform.js");
-const helpers = require("resource://grammalecte/helpers.js");
+
+if (typeof(exports) !== 'undefined') {
+    var str_transform = require("resource://grammalecte/str_transform.js");
+    var helpers = require("resource://grammalecte/helpers.js");
+}
 
 
 // String
 // Don’t remove. Necessary in TB.
 ${string}
@@ -39,15 +42,15 @@
 
         this.dChar = helpers.objectToMap(this.dChar);
         //this.byDic = new Uint8Array(this.byDic);  // not quicker, even slower
 
         if (this.cStemming == "S") {
-            this.funcStemming = st.getStemFromSuffixCode;
+            this.funcStemming = str_transform.getStemFromSuffixCode;
         } else if (this.cStemming == "A") {
-            this.funcStemming = st.getStemFromAffixCode;
+            this.funcStemming = str_transform.getStemFromAffixCode;
         } else {
-            this.funcStemming = st.noStemming;
+            this.funcStemming = str_transform.noStemming;
         }
 
         // Configuring DAWG functions according to nVersion
         switch (this.nVersion) {
             case 1:

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
@@ -2,10 +2,21 @@
 
 ${string}
 ${regex}
 ${map}
 
+
+if (typeof(exports) !== 'undefined') {
+    var ibdawg = require("resource://grammalecte/ibdawg.js");
+    var helpers = require("resource://grammalecte/helpers.js");
+    var gc_options = require("resource://grammalecte/${lang}/gc_options.js");
+    var gc_rules = require("resource://grammalecte/${lang}/gc_rules.js");
+    var cregex = require("resource://grammalecte/${lang}/cregex.js");
+    var text = require("resource://grammalecte/text.js");
+    var echo = require("resource://grammalecte/helpers.js").echo;
+}
+
 
 function capitalizeArray (aArray) {
     // can’t map on user defined function??
     let aNew = [];
     for (let i = 0; i < aArray.length; i = i + 1) {
@@ -12,339 +23,337 @@
         aNew[i] = aArray[i].gl_toCapitalize();
     }
     return aNew;
 }
 
-const ibdawg = require("resource://grammalecte/ibdawg.js");
-const helpers = require("resource://grammalecte/helpers.js");
-const gc_options = require("resource://grammalecte/${lang}/gc_options.js");
-const cr = require("resource://grammalecte/${lang}/cregex.js");
-const text = require("resource://grammalecte/text.js");
-const echo = require("resource://grammalecte/helpers.js").echo;
-
-const lang = "${lang}";
-const locales = ${loc};
-const pkg = "${implname}";
-const name = "${name}";
-const version = "${version}";
-const author = "${author}";
-
-// commons regexes
-const _zEndOfSentence = new RegExp ('([.?!:;…][ .?!… »”")]*|.$)', "g");
-const _zBeginOfParagraph = new RegExp ("^[-  –—.,;?!…]*", "ig");
-const _zEndOfParagraph = new RegExp ("[-  .,;?!…–—]*$", "ig");
-
-// grammar rules and dictionary
-//const _rules = require("./gc_rules.js");
-let _sContext = "";                                 // what software is running
-const _rules = require("resource://grammalecte/${lang}/gc_rules.js");
+
+// data
+let _sAppContext = "";                                  // what software is running
 let _dOptions = null;
 let _aIgnoredRules = new Set();
 let _oDict = null;
-let _dAnalyses = new Map();                         // cache for data from dictionary
-
-
-///// Parsing
-
-function parse (sText, sCountry="${country_default}", bDebug=false, bContext=false) {
-    // analyses the paragraph sText and returns list of errors
-    let dErrors;
-    let errs;
-    let sAlt = sText;
-    let dDA = new Map();        // Disamnbiguator
-    let dPriority = new Map();  // Key = position; value = priority
-    let sNew = "";
-
-    // parse paragraph
-    try {
-        [sNew, dErrors] = _proofread(sText, sAlt, 0, true, dDA, dPriority, sCountry, bDebug, bContext);
-        if (sNew) {
-            sText = sNew;
-        }
-    }
-    catch (e) {
-        helpers.logerror(e);
-    }
-
-    // cleanup
-    if (sText.includes(" ")) {
-        sText = sText.replace(/ /g, ' '); // nbsp
-    }
-    if (sText.includes(" ")) {
-        sText = sText.replace(/ /g, ' '); // snbsp
-    }
-    if (sText.includes("'")) {
-        sText = sText.replace(/'/g, "’");
-    }
-    if (sText.includes("‑")) {
-        sText = sText.replace(/‑/g, "-"); // nobreakdash
-    }
-
-    // parse sentence
-    for (let [iStart, iEnd] of _getSentenceBoundaries(sText)) {
-        if (4 < (iEnd - iStart) < 2000) {
-            dDA.clear();
-            //echo(sText.slice(iStart, iEnd));
-            try {
-                [_, errs] = _proofread(sText.slice(iStart, iEnd), sAlt.slice(iStart, iEnd), iStart, false, dDA, dPriority, sCountry, bDebug, bContext);
-                dErrors.gl_update(errs);
-            }
-            catch (e) {
-                helpers.logerror(e);
-            }
-        }
-    }
-    return Array.from(dErrors.values());
-}
-
-function* _getSentenceBoundaries (sText) {
-    let mBeginOfSentence = _zBeginOfParagraph.exec(sText)
-    let iStart = _zBeginOfParagraph.lastIndex;
-    let m;
-    while ((m = _zEndOfSentence.exec(sText)) !== null) {
-        yield [iStart, _zEndOfSentence.lastIndex];
-        iStart = _zEndOfSentence.lastIndex;
-    }
-}
-
-function _proofread (s, sx, nOffset, bParagraph, dDA, dPriority, sCountry, bDebug, bContext) {
-    let dErrs = new Map();
-    let bChange = false;
-    let bIdRule = option('idrule');
-    let m;
-    let bCondMemo;
-    let nErrorStart;
-
-    for (let [sOption, lRuleGroup] of _getRules(bParagraph)) {
-        if (!sOption || option(sOption)) {
-            for (let [zRegex, bUppercase, sLineId, sRuleId, nPriority, lActions, lGroups, lNegLookBefore] of lRuleGroup) {
-                if (!_aIgnoredRules.has(sRuleId)) {
-                    while ((m = zRegex.gl_exec2(s, lGroups, lNegLookBefore)) !== null) {
-                        bCondMemo = null;
-                        /*if (bDebug) {
-                            echo(">>>> Rule # " + sLineId + " - Text: " + s + " opt: "+ sOption);
-                        }*/
-                        for (let [sFuncCond, cActionType, sWhat, ...eAct] of lActions) {
-                        // action in lActions: [ condition, action type, replacement/suggestion/action[, iGroup[, message, URL]] ]
-                            try {
-                                //echo(oEvalFunc[sFuncCond]);
-                                bCondMemo = (!sFuncCond || oEvalFunc[sFuncCond](s, sx, m, dDA, sCountry, bCondMemo))
-                                if (bCondMemo) {
-                                    switch (cActionType) {
-                                        case "-":
-                                            // grammar error
-                                            //echo("-> error detected in " + sLineId + "\nzRegex: " + zRegex.source);
-                                            nErrorStart = nOffset + m.start[eAct[0]];
-                                            if (!dErrs.has(nErrorStart) || nPriority > dPriority.get(nErrorStart)) {
-                                                dErrs.set(nErrorStart, _createError(s, sx, sWhat, nOffset, m, eAct[0], sLineId, sRuleId, bUppercase, eAct[1], eAct[2], bIdRule, sOption, bContext));
-                                                dPriority.set(nErrorStart, nPriority);
-                                            }
-                                            break;
-                                        case "~":
-                                            // text processor
-                                            //echo("-> text processor by " + sLineId + "\nzRegex: " + zRegex.source);
-                                            s = _rewrite(s, sWhat, eAct[0], m, bUppercase);
-                                            bChange = true;
-                                            if (bDebug) {
-                                                echo("~ " + s + "  -- " + m[eAct[0]] + "  # " + sLineId);
-                                            }
-                                            break;
-                                        case "=":
-                                            // disambiguation
-                                            //echo("-> disambiguation by " + sLineId + "\nzRegex: " + zRegex.source);
-                                            oEvalFunc[sWhat](s, m, dDA);
-                                            if (bDebug) {
-                                                echo("= " + m[0] + "  # " + sLineId + "\nDA: " + dDA.gl_toString());
-                                            }
-                                            break;
-                                        case ">":
-                                            // we do nothing, this test is just a condition to apply all following actions
-                                            break;
-                                        default:
-                                            echo("# error: unknown action at " + sLineId);
-                                    }
-                                } else {
-                                    if (cActionType == ">") {
-                                        break;
-                                    }
-                                }
-                            }
-                            catch (e) {
-                                echo(s);
-                                echo("# line id: " + sLineId + "\n# rule id: " + sRuleId);
-                                helpers.logerror(e);
+let _dAnalyses = new Map();                             // cache for data from dictionary
+
+
+const gc_engine = {
+
+    //// Informations
+
+    lang: "${lang}",
+    locales: ${loc},
+    pkg: "${implname}",
+    name: "${name}",
+    version: "${version}",
+    author: "${author}",
+
+    //// Parsing
+
+    parse: function (sText, sCountry="${country_default}", bDebug=false, bContext=false) {
+        // analyses the paragraph sText and returns list of errors
+        let dErrors;
+        let errs;
+        let sAlt = sText;
+        let dDA = new Map();        // Disamnbiguator
+        let dPriority = new Map();  // Key = position; value = priority
+        let sNew = "";
+
+        // parse paragraph
+        try {
+            [sNew, dErrors] = this._proofread(sText, sAlt, 0, true, dDA, dPriority, sCountry, bDebug, bContext);
+            if (sNew) {
+                sText = sNew;
+            }
+        }
+        catch (e) {
+            helpers.logerror(e);
+        }
+
+        // cleanup
+        if (sText.includes(" ")) {
+            sText = sText.replace(/ /g, ' '); // nbsp
+        }
+        if (sText.includes(" ")) {
+            sText = sText.replace(/ /g, ' '); // snbsp
+        }
+        if (sText.includes("'")) {
+            sText = sText.replace(/'/g, "’");
+        }
+        if (sText.includes("‑")) {
+            sText = sText.replace(/‑/g, "-"); // nobreakdash
+        }
+
+        // parse sentence
+        for (let [iStart, iEnd] of this._getSentenceBoundaries(sText)) {
+            if (4 < (iEnd - iStart) < 2000) {
+                dDA.clear();
+                //echo(sText.slice(iStart, iEnd));
+                try {
+                    [_, errs] = this._proofread(sText.slice(iStart, iEnd), sAlt.slice(iStart, iEnd), iStart, false, dDA, dPriority, sCountry, bDebug, bContext);
+                    dErrors.gl_update(errs);
+                }
+                catch (e) {
+                    helpers.logerror(e);
+                }
+            }
+        }
+        return Array.from(dErrors.values());
+    },
+
+    _zEndOfSentence: new RegExp ('([.?!:;…][ .?!… »”")]*|.$)', "g"),
+    _zBeginOfParagraph: new RegExp ("^[-  –—.,;?!…]*", "ig"),
+    _zEndOfParagraph: new RegExp ("[-  .,;?!…–—]*$", "ig"),
+
+    _getSentenceBoundaries: function* (sText) {
+        let mBeginOfSentence = this._zBeginOfParagraph.exec(sText)
+        let iStart = this._zBeginOfParagraph.lastIndex;
+        let m;
+        while ((m = this._zEndOfSentence.exec(sText)) !== null) {
+            yield [iStart, this._zEndOfSentence.lastIndex];
+            iStart = this._zEndOfSentence.lastIndex;
+        }
+    },
+
+    _proofread: function (s, sx, nOffset, bParagraph, dDA, dPriority, sCountry, bDebug, bContext) {
+        let dErrs = new Map();
+        let bChange = false;
+        let bIdRule = option('idrule');
+        let m;
+        let bCondMemo;
+        let nErrorStart;
+
+        for (let [sOption, lRuleGroup] of this._getRules(bParagraph)) {
+            if (!sOption || option(sOption)) {
+                for (let [zRegex, bUppercase, sLineId, sRuleId, nPriority, lActions, lGroups, lNegLookBefore] of lRuleGroup) {
+                    if (!_aIgnoredRules.has(sRuleId)) {
+                        while ((m = zRegex.gl_exec2(s, lGroups, lNegLookBefore)) !== null) {
+                            bCondMemo = null;
+                            /*if (bDebug) {
+                                echo(">>>> Rule # " + sLineId + " - Text: " + s + " opt: "+ sOption);
+                            }*/
+                            for (let [sFuncCond, cActionType, sWhat, ...eAct] of lActions) {
+                            // action in lActions: [ condition, action type, replacement/suggestion/action[, iGroup[, message, URL]] ]
+                                try {
+                                    //echo(oEvalFunc[sFuncCond]);
+                                    bCondMemo = (!sFuncCond || oEvalFunc[sFuncCond](s, sx, m, dDA, sCountry, bCondMemo))
+                                    if (bCondMemo) {
+                                        switch (cActionType) {
+                                            case "-":
+                                                // grammar error
+                                                //echo("-> error detected in " + sLineId + "\nzRegex: " + zRegex.source);
+                                                nErrorStart = nOffset + m.start[eAct[0]];
+                                                if (!dErrs.has(nErrorStart) || nPriority > dPriority.get(nErrorStart)) {
+                                                    dErrs.set(nErrorStart, this._createError(s, sx, sWhat, nOffset, m, eAct[0], sLineId, sRuleId, bUppercase, eAct[1], eAct[2], bIdRule, sOption, bContext));
+                                                    dPriority.set(nErrorStart, nPriority);
+                                                }
+                                                break;
+                                            case "~":
+                                                // text processor
+                                                //echo("-> text processor by " + sLineId + "\nzRegex: " + zRegex.source);
+                                                s = this._rewrite(s, sWhat, eAct[0], m, bUppercase);
+                                                bChange = true;
+                                                if (bDebug) {
+                                                    echo("~ " + s + "  -- " + m[eAct[0]] + "  # " + sLineId);
+                                                }
+                                                break;
+                                            case "=":
+                                                // disambiguation
+                                                //echo("-> disambiguation by " + sLineId + "\nzRegex: " + zRegex.source);
+                                                oEvalFunc[sWhat](s, m, dDA);
+                                                if (bDebug) {
+                                                    echo("= " + m[0] + "  # " + sLineId + "\nDA: " + dDA.gl_toString());
+                                                }
+                                                break;
+                                            case ">":
+                                                // we do nothing, this test is just a condition to apply all following actions
+                                                break;
+                                            default:
+                                                echo("# error: unknown action at " + sLineId);
+                                        }
+                                    } else {
+                                        if (cActionType == ">") {
+                                            break;
+                                        }
+                                    }
+                                }
+                                catch (e) {
+                                    echo(s);
+                                    echo("# line id: " + sLineId + "\n# rule id: " + sRuleId);
+                                    helpers.logerror(e);
+                                }
                             }
                         }
                     }
                 }
             }
         }
-    }
-    if (bChange) {
-        return [s, dErrs];
-    }
-    return [false, dErrs];
-}
-
-function _createError (s, sx, sRepl, nOffset, m, iGroup, sLineId, sRuleId, bUppercase, sMsg, sURL, bIdRule, sOption, bContext) {
-    let oErr = {};
-    oErr["nStart"] = nOffset + m.start[iGroup];
-    oErr["nEnd"] = nOffset + m.end[iGroup];
-    oErr["sLineId"] = sLineId;
-    oErr["sRuleId"] = sRuleId;
-    oErr["sType"] = (sOption) ? sOption : "notype";
-    // suggestions
-    if (sRepl[0] === "=") {
-        let sugg = oEvalFunc[sRepl.slice(1)](s, m);
-        if (sugg) {
-            if (bUppercase && m[iGroup].slice(0,1).gl_isUpperCase()) {
-                oErr["aSuggestions"] = capitalizeArray(sugg.split("|"));
-            } else {
-                oErr["aSuggestions"] = sugg.split("|");
-            }
-        } else {
-            oErr["aSuggestions"] = [];
-        }
-    } else if (sRepl == "_") {
-        oErr["aSuggestions"] = [];
-    } else {
-        if (bUppercase && m[iGroup].slice(0,1).gl_isUpperCase()) {
-            oErr["aSuggestions"] = capitalizeArray(sRepl.gl_expand(m).split("|"));
-        } else {
-            oErr["aSuggestions"] = sRepl.gl_expand(m).split("|");
-        }
-    }
-    // Message
-    if (sMsg[0] === "=") {
-        sMessage = oEvalFunc[sMsg.slice(1)](s, m)
-    } else {
-        sMessage = sMsg.gl_expand(m);
-    }
-    if (bIdRule) {
-        sMessage += " ##" + sLineId + " #" + sRuleId;
-    }
-    oErr["sMessage"] = sMessage;
-    // URL
-    oErr["URL"] = sURL || "";
-    // Context
-    if (bContext) {
-        oErr["sUnderlined"] = sx.slice(m.start[iGroup], m.end[iGroup]);
-        oErr["sBefore"] = sx.slice(Math.max(0, m.start[iGroup]-80), m.start[iGroup]);
-        oErr["sAfter"] = sx.slice(m.end[iGroup], m.end[iGroup]+80);
-    }
-    return oErr;
-}
-
-function _rewrite (s, sRepl, iGroup, m, bUppercase) {
-    // text processor: write sRepl in s at iGroup position"
-    let ln = m.end[iGroup] - m.start[iGroup];
-    let sNew = "";
-    if (sRepl === "*") {
-        sNew = " ".repeat(ln);
-    } else if (sRepl === ">" || sRepl === "_" || sRepl === "~") {
-        sNew = sRepl + " ".repeat(ln-1);
-    } else if (sRepl === "@") {
-        sNew = "@".repeat(ln);
-    } else if (sRepl.slice(0,1) === "=") {
-        sNew = oEvalFunc[sRepl.slice(1)](s, m);
-        sNew = sNew + " ".repeat(ln-sNew.length);
-        if (bUppercase && m[iGroup].slice(0,1).gl_isUpperCase()) {
-            sNew = sNew.gl_toCapitalize();
-        }
-    } else {
-        sNew = sRepl.gl_expand(m);
-        sNew = sNew + " ".repeat(ln-sNew.length);
-    }
-    //echo("\n"+s+"\nstart: "+m.start[iGroup]+" end:"+m.end[iGroup])
-    return s.slice(0, m.start[iGroup]) + sNew + s.slice(m.end[iGroup]);
-}
-
-function ignoreRule (sRuleId) {
-    _aIgnoredRules.add(sRuleId);
-}
-
-function resetIgnoreRules () {
-    _aIgnoredRules.clear();
-}
-
-function reactivateRule (sRuleId) {
-    _aIgnoredRules.delete(sRuleId);
-}
-
-function listRules (sFilter=null) {
-    // generator: returns tuple (sOption, sLineId, sRuleId)
-    try {
-        for ([sOption, lRuleGroup] of _getRules(true)) {
-            for ([_, _, sLineId, sRuleId, _, _] of lRuleGroup) {
-                if (!sFilter || sRuleId.test(sFilter)) {
-                    yield [sOption, sLineId, sRuleId];
-                }
-            }
-        }
-        for ([sOption, lRuleGroup] of _getRules(false)) {
-            for ([_, _, sLineId, sRuleId, _, _] of lRuleGroup) {
-                if (!sFilter || sRuleId.test(sFilter)) {
-                    yield [sOption, sLineId, sRuleId];
-                }
-            }
-        }
-    }
-    catch (e) {
-        helpers.logerror(e);
-    }
-}
-
-
-//////// init
-
-function load (sContext="JavaScript") {
-    try {
-        _oDict = new ibdawg.IBDAWG("${dic_name}.json");
-        _sContext = sContext;
-        _dOptions = gc_options.getOptions(sContext).gl_shallowCopy();     // duplication necessary, to be able to reset to default
-    }
-    catch (e) {
-        helpers.logerror(e);
-    }
-}
-
-function setOption (sOpt, bVal) {
-    if (_dOptions.has(sOpt)) {
-        _dOptions.set(sOpt, bVal);
-    }
-}
-
-function setOptions (dOpt) {
-    _dOptions.gl_updateOnlyExistingKeys(dOpt);
-}
-
-function getOptions () {
-    return _dOptions;
-}
-
-function getDefaultOptions () {
-    return gc_options.getOptions(_sContext).gl_shallowCopy();
-}
-
-function resetOptions () {
-    _dOptions = gc_options.getOptions(_sContext).gl_shallowCopy();
-}
-
-function getDictionary () {
-    return _oDict;
-}
-
-function _getRules (bParagraph) {
-    if (!bParagraph) {
-        return _rules.lSentenceRules;
-    }
-    return _rules.lParagraphRules;
-}
-
-
-
-//////// common functions
+        if (bChange) {
+            return [s, dErrs];
+        }
+        return [false, dErrs];
+    },
+
+    _createError: function (s, sx, sRepl, nOffset, m, iGroup, sLineId, sRuleId, bUppercase, sMsg, sURL, bIdRule, sOption, bContext) {
+        let oErr = {};
+        oErr["nStart"] = nOffset + m.start[iGroup];
+        oErr["nEnd"] = nOffset + m.end[iGroup];
+        oErr["sLineId"] = sLineId;
+        oErr["sRuleId"] = sRuleId;
+        oErr["sType"] = (sOption) ? sOption : "notype";
+        // suggestions
+        if (sRepl[0] === "=") {
+            let sugg = oEvalFunc[sRepl.slice(1)](s, m);
+            if (sugg) {
+                if (bUppercase && m[iGroup].slice(0,1).gl_isUpperCase()) {
+                    oErr["aSuggestions"] = capitalizeArray(sugg.split("|"));
+                } else {
+                    oErr["aSuggestions"] = sugg.split("|");
+                }
+            } else {
+                oErr["aSuggestions"] = [];
+            }
+        } else if (sRepl == "_") {
+            oErr["aSuggestions"] = [];
+        } else {
+            if (bUppercase && m[iGroup].slice(0,1).gl_isUpperCase()) {
+                oErr["aSuggestions"] = capitalizeArray(sRepl.gl_expand(m).split("|"));
+            } else {
+                oErr["aSuggestions"] = sRepl.gl_expand(m).split("|");
+            }
+        }
+        // Message
+        if (sMsg[0] === "=") {
+            sMessage = oEvalFunc[sMsg.slice(1)](s, m)
+        } else {
+            sMessage = sMsg.gl_expand(m);
+        }
+        if (bIdRule) {
+            sMessage += " ##" + sLineId + " #" + sRuleId;
+        }
+        oErr["sMessage"] = sMessage;
+        // URL
+        oErr["URL"] = sURL || "";
+        // Context
+        if (bContext) {
+            oErr["sUnderlined"] = sx.slice(m.start[iGroup], m.end[iGroup]);
+            oErr["sBefore"] = sx.slice(Math.max(0, m.start[iGroup]-80), m.start[iGroup]);
+            oErr["sAfter"] = sx.slice(m.end[iGroup], m.end[iGroup]+80);
+        }
+        return oErr;
+    },
+
+    _rewrite: function (s, sRepl, iGroup, m, bUppercase) {
+        // text processor: write sRepl in s at iGroup position"
+        let ln = m.end[iGroup] - m.start[iGroup];
+        let sNew = "";
+        if (sRepl === "*") {
+            sNew = " ".repeat(ln);
+        } else if (sRepl === ">" || sRepl === "_" || sRepl === "~") {
+            sNew = sRepl + " ".repeat(ln-1);
+        } else if (sRepl === "@") {
+            sNew = "@".repeat(ln);
+        } else if (sRepl.slice(0,1) === "=") {
+            sNew = oEvalFunc[sRepl.slice(1)](s, m);
+            sNew = sNew + " ".repeat(ln-sNew.length);
+            if (bUppercase && m[iGroup].slice(0,1).gl_isUpperCase()) {
+                sNew = sNew.gl_toCapitalize();
+            }
+        } else {
+            sNew = sRepl.gl_expand(m);
+            sNew = sNew + " ".repeat(ln-sNew.length);
+        }
+        //echo("\n"+s+"\nstart: "+m.start[iGroup]+" end:"+m.end[iGroup])
+        return s.slice(0, m.start[iGroup]) + sNew + s.slice(m.end[iGroup]);
+    },
+
+    // Actions on rules
+
+    ignoreRule: function (sRuleId) {
+        _aIgnoredRules.add(sRuleId);
+    },
+
+    resetIgnoreRules: function () {
+        _aIgnoredRules.clear();
+    },
+
+    reactivateRule: function (sRuleId) {
+        _aIgnoredRules.delete(sRuleId);
+    },
+
+    listRules: function (sFilter=null) {
+        // generator: returns tuple (sOption, sLineId, sRuleId)
+        try {
+            for ([sOption, lRuleGroup] of _getRules(true)) {
+                for ([_, _, sLineId, sRuleId, _, _] of lRuleGroup) {
+                    if (!sFilter || sRuleId.test(sFilter)) {
+                        yield [sOption, sLineId, sRuleId];
+                    }
+                }
+            }
+            for ([sOption, lRuleGroup] of _getRules(false)) {
+                for ([_, _, sLineId, sRuleId, _, _] of lRuleGroup) {
+                    if (!sFilter || sRuleId.test(sFilter)) {
+                        yield [sOption, sLineId, sRuleId];
+                    }
+                }
+            }
+        }
+        catch (e) {
+            helpers.logerror(e);
+        }
+    },
+
+    _getRules: function (bParagraph) {
+        if (!bParagraph) {
+            return gc_rules.lSentenceRules;
+        }
+        return gc_rules.lParagraphRules;
+    },
+
+    //// Initialization
+
+    load: function (sContext="JavaScript") {
+        try {
+            _oDict = new ibdawg.IBDAWG("${dic_name}.json");
+            _sAppContext = sContext;
+            _dOptions = gc_options.getOptions(sContext).gl_shallowCopy();     // duplication necessary, to be able to reset to default
+        }
+        catch (e) {
+            helpers.logerror(e);
+        }
+    },
+
+    getDictionary: function () {
+        return _oDict;
+    },
+
+    //// Options
+
+    setOption: function (sOpt, bVal) {
+        if (_dOptions.has(sOpt)) {
+            _dOptions.set(sOpt, bVal);
+        }
+    },
+
+    setOptions: function (dOpt) {
+        _dOptions.gl_updateOnlyExistingKeys(dOpt);
+    },
+
+    getOptions: function () {
+        return _dOptions;
+    },
+
+    getDefaultOptions: function () {
+        return gc_options.getOptions(_sAppContext).gl_shallowCopy();
+    },
+
+    resetOptions: function () {
+        _dOptions = gc_options.getOptions(_sAppContext).gl_shallowCopy();
+    }
+}
+
+
+//////// Common functions
 
 function option (sOpt) {
     // return true if option sOpt is active
     return _dOptions.get(sOpt);
 }
@@ -594,10 +603,11 @@
 
 function define (dDA, nPos, lMorph) {
     dDA.set(nPos, lMorph);
     return true;
 }
+
 
 //////// GRAMMAR CHECKER PLUGINS
 
 ${pluginsJS}
 
@@ -605,20 +615,32 @@
 ${callablesJS}
 
 
 
 if (typeof(exports) !== 'undefined') {
-    exports.load = load;
-    exports.parse = parse;
-    exports.lang = lang;
-    exports.version = version;
-    exports.getDictionary = getDictionary;
-    exports.setOption = setOption;
-    exports.setOptions = setOptions;
-    exports.getOptions = getOptions;
-    exports.getDefaultOptions = getDefaultOptions;
-    exports.resetOptions = resetOptions;
-    exports.ignoreRule = ignoreRule;
-    exports.reactivateRule = reactivateRule;
-    exports.resetIgnoreRules = resetIgnoreRules;
-    exports.listRules = listRules;
+    exports.lang = gc_engine.lang;
+    exports.locales = gc_engine.locales;
+    exports.pkg = gc_engine.pkg;
+    exports.name = gc_engine.name;
+    exports.version = gc_engine.version;
+    exports.author = gc_engine.author;
+    exports.parse = gc_engine.parse;
+    exports._zEndOfSentence = gc_engine._zEndOfSentence;
+    exports._zBeginOfParagraph = gc_engine._zBeginOfParagraph;
+    exports._zEndOfParagraph = gc_engine._zEndOfParagraph;
+    exports._getSentenceBoundaries = gc_engine._getSentenceBoundaries;
+    exports._proofread = gc_engine._proofread;
+    exports._createError = gc_engine._createError;
+    exports._rewrite = gc_engine._rewrite;
+    exports.ignoreRule = gc_engine.ignoreRule;
+    exports.resetIgnoreRules = gc_engine.resetIgnoreRules;
+    exports.reactivateRule = gc_engine.reactivateRule;
+    exports.listRules = gc_engine.listRules;
+    exports._getRules = gc_engine._getRules;
+    exports.load = gc_engine.load;
+    exports.getDictionary = gc_engine.getDictionary;
+    exports.setOption = gc_engine.setOption;
+    exports.setOptions = gc_engine.setOptions;
+    exports.getOptions = gc_engine.getOptions;
+    exports.getDefaultOptions = gc_engine.getDefaultOptions;
+    exports.resetOptions = gc_engine.resetOptions;
 }

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,27 +1,31 @@
 // Options for Grammalecte
 
 ${map}
 
-function getOptions (sContext="JavaScript") {
-    if (dOpt.hasOwnProperty(sContext)) {
-        return dOpt[sContext];
-    }
-    return dOpt["JavaScript"];
-}
-
-const lStructOpt = ${lStructOpt};
-
-const dOpt = {
-    "JavaScript": new Map (${dOptJavaScript}),
-    "Firefox": new Map (${dOptFirefox}),
-    "Thunderbird": new Map (${dOptThunderbird}),
-}
-
-const dOptLabel = ${dOptLabel};
+
+const gc_options = {
+    getOptions: function (sContext="JavaScript") {
+        if (this.dOpt.hasOwnProperty(sContext)) {
+            return this.dOpt[sContext];
+        }
+        return this.dOpt["JavaScript"];
+    },
+
+    lStructOpt: ${lStructOpt},
+
+    dOpt: {
+        "JavaScript": new Map (${dOptJavaScript}),
+        "Firefox": new Map (${dOptFirefox}),
+        "Thunderbird": new Map (${dOptThunderbird}),
+    },
+
+    dOptLabel: ${dOptLabel}
+}
 
 
 if (typeof(exports) !== 'undefined') {
-	exports.getOptions = getOptions;
-	exports.lStructOpt = lStructOpt;
-	exports.dOptLabel = dOptLabel;
+	exports.getOptions = gc_options.getOptions;
+	exports.lStructOpt = gc_options.lStructOpt;
+    exports.dOpt = gc_options.dOpt;
+	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
@@ -2,14 +2,16 @@
 "use strict";
 
 ${string}
 ${regex}
 
-const lParagraphRules = ${paragraph_rules_JS};
+const gc_rules = {
+    lParagraphRules: ${paragraph_rules_JS},
 
-const lSentenceRules = ${sentence_rules_JS};
+    lSentenceRules: ${sentence_rules_JS}
+}
 
 
 if (typeof(exports) !== 'undefined') {
-	exports.lParagraphRules = lParagraphRules;
-	exports.lSentenceRules = lSentenceRules;
+    exports.lParagraphRules = gc_rules.lParagraphRules;
+    exports.lSentenceRules = gc_rules.lSentenceRules;
 }

Index: gc_core/js/str_transform.js
==================================================================
--- gc_core/js/str_transform.js
+++ gc_core/js/str_transform.js
@@ -1,60 +1,32 @@
 //// STRING TRANSFORMATION
 
-var dSimilarChars = new Map ([
-    ["a", "aàâáä"],
-    ["à", "aàâáä"],
-    ["â", "aàâáä"],
-    ["á", "aàâáä"],
-    ["ä", "aàâáä"],
-    ["c", "cç"],
-    ["ç", "cç"],
-    ["e", "eéêèë"],
-    ["é", "eéêèë"],
-    ["ê", "eéêèë"],
-    ["è", "eéêèë"],
-    ["ë", "eéêèë"],
-    ["i", "iîïíì"],
-    ["î", "iîïíì"],
-    ["ï", "iîïíì"],
-    ["í", "iîïíì"],
-    ["ì", "iîïíì"],
-    ["o", "oôóòö"],
-    ["ô", "oôóòö"],
-    ["ó", "oôóòö"],
-    ["ò", "oôóòö"],
-    ["ö", "oôóòö"],
-    ["u", "uûùüú"],
-    ["û", "uûùüú"],
-    ["ù", "uûùüú"],
-    ["ü", "uûùüú"],
-    ["ú", "uûùüú"]
-]);
-
 // Note: 48 is the ASCII code for "0"
 
-// Suffix only
-function getStemFromSuffixCode (sFlex, sSfxCode) {
-    if (sSfxCode == "0") {
-        return sFlex;
-    }
-    return sSfxCode[0] == '0' ? sFlex + sSfxCode.slice(1) : sFlex.slice(0, -(sSfxCode.charCodeAt(0)-48)) + sSfxCode.slice(1);
-}
-
-// Prefix and suffix
-function getStemFromAffixCode (sFlex, sAffCode) {
-    if (sAffCode == "0") {
-        return sFlex;
-    }
-    if (!sAffCode.includes("/")) {
-        return "# error #";
-    }
-    var [sPfxCode, sSfxCode] = sAffCode.split('/');
-    sFlex = sPfxCode.slice(1) + sFlex.slice(sPfxCode.charCodeAt(0)-48);
-    return sSfxCode[0] == '0' ? sFlex + sSfxCode.slice(1) : sFlex.slice(0, -(sSfxCode.charCodeAt(0)-48)) + sSfxCode.slice(1);
+const str_transform = {
+    getStemFromSuffixCode: function (sFlex, sSfxCode) {
+        // Suffix only
+        if (sSfxCode == "0") {
+            return sFlex;
+        }
+        return sSfxCode[0] == '0' ? sFlex + sSfxCode.slice(1) : sFlex.slice(0, -(sSfxCode.charCodeAt(0)-48)) + sSfxCode.slice(1);
+    },
+    
+    getStemFromAffixCode: function (sFlex, sAffCode) {
+        // Prefix and suffix
+        if (sAffCode == "0") {
+            return sFlex;
+        }
+        if (!sAffCode.includes("/")) {
+            return "# error #";
+        }
+        let [sPfxCode, sSfxCode] = sAffCode.split('/');
+        sFlex = sPfxCode.slice(1) + sFlex.slice(sPfxCode.charCodeAt(0)-48);
+        return sSfxCode[0] == '0' ? sFlex + sSfxCode.slice(1) : sFlex.slice(0, -(sSfxCode.charCodeAt(0)-48)) + sSfxCode.slice(1);
+    }
 }
 
 
 if (typeof(exports) !== 'undefined') {
-    exports.getStemFromSuffixCode = getStemFromSuffixCode;
-    exports.getStemFromAffixCode = getStemFromAffixCode;
+    exports.getStemFromSuffixCode = str_transform.getStemFromSuffixCode;
+    exports.getStemFromAffixCode = str_transform.getStemFromAffixCode;
 }

Index: gc_core/js/tests.js
==================================================================
--- gc_core/js/tests.js
+++ gc_core/js/tests.js
@@ -1,11 +1,13 @@
 // JavaScript
 
 "use strict";
 
 
-const helpers = require("resource://grammalecte/helpers.js");
+if (typeof(exports) !== 'undefined') {
+    var helpers = require("resource://grammalecte/helpers.js");
+}
 
 
 class TestGrammarChecking {
 
     constructor (gce) {

Index: gc_core/js/text.js
==================================================================
--- gc_core/js/text.js
+++ gc_core/js/text.js
@@ -1,64 +1,69 @@
 // JavaScript
 
 "use strict";
 
-const helpers = require("resource://grammalecte/helpers.js");
-
-
-function* getParagraph (sText) {
-    // generator: returns paragraphs of text
-    let iStart = 0;
-    let iEnd = 0;
-    sText = sText.replace("\r", "");
-    while ((iEnd = sText.indexOf("\n", iStart)) !== -1) {
-        yield sText.slice(iStart, iEnd);
-        iStart = iEnd + 1;
-    }
-    yield sText.slice(iStart);
-}
-
-function* wrap (sText, nWidth=80) {
-    // generator: returns text line by line
-    while (sText) {
-        if (sText.length >= nWidth) {
-            let nEnd = sText.lastIndexOf(" ", nWidth) + 1;
-            if (nEnd > 0) {
-                yield sText.slice(0, nEnd);
-                sText = sText.slice(nEnd);
-            } else {
-                yield sText.slice(0, nWidth);
-                sText = sText.slice(nWidth);
-            }
-        } else {
-            break;
-        }
-    }
-    yield sText;
-}
-
-function getReadableError (oErr) {
-    // Returns an error oErr as a readable error
-    try {
-        let sResult = "\n* " + oErr['nStart'] + ":" + oErr['nEnd'] 
-                    + "  # " + oErr['sLineId'] + "  # " + oErr['sRuleId'] + ":\n";
-        sResult += "  " + oErr["sMessage"];
-        if (oErr["aSuggestions"].length > 0) {
-            sResult += "\n  > Suggestions : " + oErr["aSuggestions"].join(" | ");
-        }
-        if (oErr["URL"] !== "") {
-            sResult += "\n  > URL: " + oErr["URL"];
-        }
-        return sResult;
-    }
-    catch (e) {
-        helpers.logerror(e);
-        return "\n# Error. Data: " + oErr.toString();
+
+if (typeof(exports) !== 'undefined') {
+    var helpers = require("resource://grammalecte/helpers.js");
+}
+
+
+const text = {
+    getParagraph: function* (sText) {
+        // generator: returns paragraphs of text
+        let iStart = 0;
+        let iEnd = 0;
+        sText = sText.replace("\r", "");
+        while ((iEnd = sText.indexOf("\n", iStart)) !== -1) {
+            yield sText.slice(iStart, iEnd);
+            iStart = iEnd + 1;
+        }
+        yield sText.slice(iStart);
+    },
+
+    wrap: function* (sText, nWidth=80) {
+        // generator: returns text line by line
+        while (sText) {
+            if (sText.length >= nWidth) {
+                let nEnd = sText.lastIndexOf(" ", nWidth) + 1;
+                if (nEnd > 0) {
+                    yield sText.slice(0, nEnd);
+                    sText = sText.slice(nEnd);
+                } else {
+                    yield sText.slice(0, nWidth);
+                    sText = sText.slice(nWidth);
+                }
+            } else {
+                break;
+            }
+        }
+        yield sText;
+    },
+
+    getReadableError: function (oErr) {
+        // Returns an error oErr as a readable error
+        try {
+            let sResult = "\n* " + oErr['nStart'] + ":" + oErr['nEnd'] 
+                        + "  # " + oErr['sLineId'] + "  # " + oErr['sRuleId'] + ":\n";
+            sResult += "  " + oErr["sMessage"];
+            if (oErr["aSuggestions"].length > 0) {
+                sResult += "\n  > Suggestions : " + oErr["aSuggestions"].join(" | ");
+            }
+            if (oErr["URL"] !== "") {
+                sResult += "\n  > URL: " + oErr["URL"];
+            }
+            return sResult;
+        }
+        catch (e) {
+            helpers.logerror(e);
+            return "\n# Error. Data: " + oErr.toString();
+        }
     }
 }
 
 
 if (typeof(exports) !== 'undefined') {
-    exports.getParagraph = getParagraph;
-    exports.wrap = wrap;
-    exports.getReadableError = getReadableError;
+    exports.getParagraph = text.getParagraph;
+    exports.wrap = text.wrap;
+    exports.getReadableError = text.getReadableError;
 }

Index: gc_core/js/tokenizer.js
==================================================================
--- gc_core/js/tokenizer.js
+++ gc_core/js/tokenizer.js
@@ -1,13 +1,17 @@
 // JavaScript
 // Very simple tokenizer
 
 "use strict";
 
-const helpers = require("resource://grammalecte/helpers.js");
+
+if (typeof(exports) !== 'undefined') {
+    var helpers = require("resource://grammalecte/helpers.js");
+}
+
 
-const aPatterns = {
+const aTkzPatterns = {
     // All regexps must start with ^.
     "default":
         [
             [/^[   \t]+/, 'SPACE'],
             [/^[,.;:!?…«»“”‘’"(){}\[\]/·–—]+/, 'SEPARATOR'],
@@ -40,14 +44,14 @@
 
 class Tokenizer {
 
     constructor (sLang) {
         this.sLang = sLang;
-        if (!aPatterns.hasOwnProperty(sLang)) {
+        if (!aTkzPatterns.hasOwnProperty(sLang)) {
             this.sLang = "default";
         }
-        this.aRules = aPatterns[this.sLang];
+        this.aRules = aTkzPatterns[this.sLang];
     };
 
     * genTokens (sText) {
         let m;
         let i = 0;

Index: gc_lang/fr/modules-js/conj.js
==================================================================
--- gc_lang/fr/modules-js/conj.js
+++ gc_lang/fr/modules-js/conj.js
@@ -6,183 +6,173 @@
 ${map}
 
 
 let helpers = null; // module not loaded in Firefox content script
 
-let _oData = {};
-let _lVtyp = null;
-let _lTags = null;
-let _dPatternConj = {};
-let _dVerb = {};
-
-
-if (typeof(exports) !== 'undefined') {
-    // used within Grammalecte library
-    helpers = require("resource://grammalecte/helpers.js");
-    _oData = JSON.parse(helpers.loadFile("resource://grammalecte/fr/conj_data.json"));
-    _lVtyp = _oData.lVtyp;
-    _lTags = _oData.lTags;
-    _dPatternConj = _oData.dPatternConj;
-    _dVerb = _oData.dVerb;
-} else {
-    // 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 (sData) {
-        _oData = JSON.parse(sData);
-        _lVtyp = _oData.lVtyp;
-        _lTags = _oData.lTags;
-        _dPatternConj = _oData.dPatternConj;
-        _dVerb = _oData.dVerb;
-    });
-}
-
-
-const _zStartVoy = new RegExp("^[aeéiouœê]");
-const _zNeedTeuph = new RegExp("[tdc]$");
-
-const _dProSuj = new Map ([ [":1s", "je"], [":1ś", "je"], [":2s", "tu"], [":3s", "il"], [":1p", "nous"], [":2p", "vous"], [":3p", "ils"] ]);
-const _dProObj = new Map ([ [":1s", "me "], [":1ś", "me "], [":2s", "te "], [":3s", "se "], [":1p", "nous "], [":2p", "vous "], [":3p", "se "] ]);
-const _dProObjEl = new Map ([ [":1s", "m’"], [":1ś", "m’"], [":2s", "t’"], [":3s", "s’"], [":1p", "nous "], [":2p", "vous "], [":3p", "s’"] ]);
-const _dImpePro = new Map ([ [":2s", "-toi"], [":1p", "-nous"], [":2p", "-vous"] ]);
-const _dImpeProNeg = new Map ([ [":2s", "ne te "], [":1p", "ne nous "], [":2p", "ne vous "] ]);
-const _dImpeProEn = new Map ([ [":2s", "-t’en"], [":1p", "-nous-en"], [":2p", "-vous-en"] ]);
-const _dImpeProNegEn = new Map ([ [":2s", "ne t’en "], [":1p", "ne nous en "], [":2p", "ne vous en "] ]);
-
-const _dGroup = new Map ([ ["0", "auxiliaire"], ["1", "1ᵉʳ groupe"], ["2", "2ᵉ groupe"], ["3", "3ᵉ groupe"] ]);
-
-const _dTenseIdx = new Map ([ [":PQ", 0], [":Ip", 1], [":Iq", 2], [":Is", 3], [":If", 4], [":K", 5], [":Sp", 6], [":Sq", 7], [":E", 8] ]);
-
-
-function isVerb (sVerb) {
-    return _dVerb.hasOwnProperty(sVerb);
-}
-
-function getConj (sVerb, sTense, sWho) {
-    // returns conjugation (can be an empty string)
-    if (!_dVerb.hasOwnProperty(sVerb)) {
-        return null;
-    }
-    if (!_dPatternConj[sTense][_lTags[_dVerb[sVerb][1]][_dTenseIdx.get(sTense)]].hasOwnProperty(sWho)) {
-        return "";
-    }
-    return _modifyStringWithSuffixCode(sVerb, _dPatternConj[sTense][_lTags[_dVerb[sVerb][1]][_dTenseIdx.get(sTense)]][sWho]);
-}
-
-function hasConj (sVerb, sTense, sWho) {
-    // returns false if no conjugation (also if empty) else true
-    if (!_dVerb.hasOwnProperty(sVerb)) {
-        return false;
-    }
-    if (_dPatternConj[sTense][_lTags[_dVerb[sVerb][1]][_dTenseIdx.get(sTense)]].hasOwnProperty(sWho)
-            && _dPatternConj[sTense][_lTags[_dVerb[sVerb][1]][_dTenseIdx.get(sTense)]][sWho]) {
-        return true;
-    }
-    return false;
-}
-
-function getVtyp (sVerb) {
-    // returns raw informations about sVerb
-    if (!_dVerb.hasOwnProperty(sVerb)) {
-        return null;
-    }
-    return _lVtyp[_dVerb[sVerb][0]];
-}
-
-function getSimil (sWord, sMorph, sFilter=null) {
-    if (!sMorph.includes(":V")) {
-        return new Set();
-    }
-    let sInfi = sMorph.slice(1, sMorph.indexOf(" "));
-    let tTags = _getTags(sInfi);
-    let aSugg = new Set();
-    if (sMorph.includes(":Q") || sMorph.includes(":Y")) {
-        // we suggest conjugated forms
-        if (sMorph.includes(":V1")) {
-            aSugg.add(sInfi);
-            aSugg.add(_getConjWithTags(sInfi, tTags, ":Ip", ":3s"));
-            aSugg.add(_getConjWithTags(sInfi, tTags, ":Ip", ":2p"));
-            aSugg.add(_getConjWithTags(sInfi, tTags, ":Iq", ":1s"));
-            aSugg.add(_getConjWithTags(sInfi, tTags, ":Iq", ":3s"));
-            aSugg.add(_getConjWithTags(sInfi, tTags, ":Iq", ":3p"));
-        } else if (sMorph.includes(":V2")) {
-            aSugg.add(_getConjWithTags(sInfi, tTags, ":Ip", ":1s"));
-            aSugg.add(_getConjWithTags(sInfi, tTags, ":Ip", ":3s"));
-        } else if (sMorph.includes(":V3")) {
-            aSugg.add(_getConjWithTags(sInfi, tTags, ":Ip", ":1s"));
-            aSugg.add(_getConjWithTags(sInfi, tTags, ":Ip", ":3s"));
-            aSugg.add(_getConjWithTags(sInfi, tTags, ":Is", ":1s"));
-            aSugg.add(_getConjWithTags(sInfi, tTags, ":Is", ":3s"));
-        } else if (isMorph.includes(":V0a")) {
-            aSugg.add("eus");
-            aSugg.add("eut");
-        } else {
-            aSugg.add("étais");
-            aSugg.add("était");
-        }
-        aSugg.delete("");
-    } else {
-        // we suggest past participles
-        aSugg.add(_getConjWithTags(sInfi, tTags, ":PQ", ":Q1"));
-        aSugg.add(_getConjWithTags(sInfi, tTags, ":PQ", ":Q2"));
-        aSugg.add(_getConjWithTags(sInfi, tTags, ":PQ", ":Q3"));
-        aSugg.add(_getConjWithTags(sInfi, tTags, ":PQ", ":Q4"));
-        aSugg.delete("");
-        // if there is only one past participle (epi inv), unreliable.
-        if (aSugg.size === 1) {
-            aSugg.clear();
-        }
-        if (sMorph.includes(":V1")) {
-            aSugg.add(sInfi);
-        }
-    }
-    return aSugg;
-}
-
-
-function _getTags (sVerb) {
-    // returns tuple of tags (usable with functions _getConjWithTags and _hasConjWithTags)
-    if (!_dVerb.hasOwnProperty(sVerb)) {
-        return null;
-    }
-    return _lTags[_dVerb[sVerb][1]];
-}
-
-function _getConjWithTags (sVerb, tTags, sTense, sWho) {
-    // returns conjugation (can be an empty string)
-    if (!_dPatternConj[sTense][tTags[_dTenseIdx.get(sTense)]].hasOwnProperty(sWho)) {
-        return "";
-    }
-    return _modifyStringWithSuffixCode(sVerb, _dPatternConj[sTense][tTags[_dTenseIdx.get(sTense)]][sWho]);
-}
-
-function _hasConjWithTags (tTags, sTense, sWho) {
-    // returns false if no conjugation (also if empty) else true
-    if (_dPatternConj[sTense][tTags[_dTenseIdx.get(sTense)]].hasOwnProperty(sWho)
-            && _dPatternConj[sTense][tTags[_dTenseIdx.get(sTense)]][sWho]) {
-        return true;
-    }
-    return false;
-}
-
-function _modifyStringWithSuffixCode (sWord, sSfx) {
-    // returns sWord modified by sSfx
-    if (sSfx === "") {
-        return "";
-    }
-    if (sSfx === "0") {
-        return sWord;
-    }
-    try {
-        if (sSfx[0] !== '0') {
-            return sWord.slice(0, -(sSfx.charCodeAt(0)-48)) + sSfx.slice(1); // 48 is the ASCII code for "0"
-        } else {
-            return sWord + sSfx.slice(1);
-        }
-    }
-    catch (e) {
-        console.log(e);
-        return "## erreur, code : " + sSfx + " ##";
+const conj = {
+    _lVtyp: null,
+    _lTags: null,
+    _dPatternConj: {},
+    _dVerb: {},
+
+    init: function (sJSONData) {
+        try {
+            let _oData = JSON.parse(sJSONData);
+            this._lVtyp = _oData.lVtyp;
+            this._lTags = _oData.lTags;
+            this._dPatternConj = _oData.dPatternConj;
+            this._dVerb = _oData.dVerb;
+        }
+        catch (e) {
+            console.error(e);
+        }
+    },
+
+    _zStartVoy: new RegExp("^[aeéiouœê]"),
+    _zNeedTeuph: new RegExp("[tdc]$"),
+
+    _dProSuj: new Map ([ [":1s", "je"], [":1ś", "je"], [":2s", "tu"], [":3s", "il"], [":1p", "nous"], [":2p", "vous"], [":3p", "ils"] ]),
+    _dProObj: new Map ([ [":1s", "me "], [":1ś", "me "], [":2s", "te "], [":3s", "se "], [":1p", "nous "], [":2p", "vous "], [":3p", "se "] ]),
+    _dProObjEl: new Map ([ [":1s", "m’"], [":1ś", "m’"], [":2s", "t’"], [":3s", "s’"], [":1p", "nous "], [":2p", "vous "], [":3p", "s’"] ]),
+    _dImpePro: new Map ([ [":2s", "-toi"], [":1p", "-nous"], [":2p", "-vous"] ]),
+    _dImpeProNeg: new Map ([ [":2s", "ne te "], [":1p", "ne nous "], [":2p", "ne vous "] ]),
+    _dImpeProEn: new Map ([ [":2s", "-t’en"], [":1p", "-nous-en"], [":2p", "-vous-en"] ]),
+    _dImpeProNegEn: new Map ([ [":2s", "ne t’en "], [":1p", "ne nous en "], [":2p", "ne vous en "] ]),
+
+    _dGroup: new Map ([ ["0", "auxiliaire"], ["1", "1ᵉʳ groupe"], ["2", "2ᵉ groupe"], ["3", "3ᵉ groupe"] ]),
+
+    _dTenseIdx: new Map ([ [":PQ", 0], [":Ip", 1], [":Iq", 2], [":Is", 3], [":If", 4], [":K", 5], [":Sp", 6], [":Sq", 7], [":E", 8] ]),
+
+    isVerb: function (sVerb) {
+        return this._dVerb.hasOwnProperty(sVerb);
+    },
+
+    getConj: function (sVerb, sTense, sWho) {
+        // returns conjugation (can be an empty string)
+        if (!this._dVerb.hasOwnProperty(sVerb)) {
+            return null;
+        }
+        if (!this._dPatternConj[sTense][this._lTags[this._dVerb[sVerb][1]][this._dTenseIdx.get(sTense)]].hasOwnProperty(sWho)) {
+            return "";
+        }
+        return this._modifyStringWithSuffixCode(sVerb, this._dPatternConj[sTense][this._lTags[this._dVerb[sVerb][1]][this._dTenseIdx.get(sTense)]][sWho]);
+    },
+
+    hasConj: function (sVerb, sTense, sWho) {
+        // returns false if no conjugation (also if empty) else true
+        if (!this._dVerb.hasOwnProperty(sVerb)) {
+            return false;
+        }
+        if (this._dPatternConj[sTense][this._lTags[this._dVerb[sVerb][1]][this._dTenseIdx.get(sTense)]].hasOwnProperty(sWho)
+                && this._dPatternConj[sTense][this._lTags[this._dVerb[sVerb][1]][this._dTenseIdx.get(sTense)]][sWho]) {
+            return true;
+        }
+        return false;
+    },
+
+    getVtyp: function (sVerb) {
+        // returns raw informations about sVerb
+        if (!this._dVerb.hasOwnProperty(sVerb)) {
+            return null;
+        }
+        return this._lVtyp[this._dVerb[sVerb][0]];
+    },
+
+    getSimil: function (sWord, sMorph, sFilter=null) {
+        if (!sMorph.includes(":V")) {
+            return new Set();
+        }
+        let sInfi = sMorph.slice(1, sMorph.indexOf(" "));
+        let tTags = this._getTags(sInfi);
+        let aSugg = new Set();
+        if (sMorph.includes(":Q") || sMorph.includes(":Y")) {
+            // we suggest conjugated forms
+            if (sMorph.includes(":V1")) {
+                aSugg.add(sInfi);
+                aSugg.add(this._getConjWithTags(sInfi, tTags, ":Ip", ":3s"));
+                aSugg.add(this._getConjWithTags(sInfi, tTags, ":Ip", ":2p"));
+                aSugg.add(this._getConjWithTags(sInfi, tTags, ":Iq", ":1s"));
+                aSugg.add(this._getConjWithTags(sInfi, tTags, ":Iq", ":3s"));
+                aSugg.add(this._getConjWithTags(sInfi, tTags, ":Iq", ":3p"));
+            } else if (sMorph.includes(":V2")) {
+                aSugg.add(this._getConjWithTags(sInfi, tTags, ":Ip", ":1s"));
+                aSugg.add(this._getConjWithTags(sInfi, tTags, ":Ip", ":3s"));
+            } else if (sMorph.includes(":V3")) {
+                aSugg.add(this._getConjWithTags(sInfi, tTags, ":Ip", ":1s"));
+                aSugg.add(this._getConjWithTags(sInfi, tTags, ":Ip", ":3s"));
+                aSugg.add(this._getConjWithTags(sInfi, tTags, ":Is", ":1s"));
+                aSugg.add(this._getConjWithTags(sInfi, tTags, ":Is", ":3s"));
+            } else if (isMorph.includes(":V0a")) {
+                aSugg.add("eus");
+                aSugg.add("eut");
+            } else {
+                aSugg.add("étais");
+                aSugg.add("était");
+            }
+            aSugg.delete("");
+        } else {
+            // we suggest past participles
+            aSugg.add(this._getConjWithTags(sInfi, tTags, ":PQ", ":Q1"));
+            aSugg.add(this._getConjWithTags(sInfi, tTags, ":PQ", ":Q2"));
+            aSugg.add(this._getConjWithTags(sInfi, tTags, ":PQ", ":Q3"));
+            aSugg.add(this._getConjWithTags(sInfi, tTags, ":PQ", ":Q4"));
+            aSugg.delete("");
+            // if there is only one past participle (epi inv), unreliable.
+            if (aSugg.size === 1) {
+                aSugg.clear();
+            }
+            if (sMorph.includes(":V1")) {
+                aSugg.add(sInfi);
+            }
+        }
+        return aSugg;
+    },
+
+    _getTags: function (sVerb) {
+        // returns tuple of tags (usable with functions _getConjWithTags and _hasConjWithTags)
+        if (!this._dVerb.hasOwnProperty(sVerb)) {
+            return null;
+        }
+        return this._lTags[this._dVerb[sVerb][1]];
+    },
+
+    _getConjWithTags: function (sVerb, tTags, sTense, sWho) {
+        // returns conjugation (can be an empty string)
+        if (!this._dPatternConj[sTense][tTags[this._dTenseIdx.get(sTense)]].hasOwnProperty(sWho)) {
+            return "";
+        }
+        return this._modifyStringWithSuffixCode(sVerb, this._dPatternConj[sTense][tTags[this._dTenseIdx.get(sTense)]][sWho]);
+    },
+
+    _hasConjWithTags: function (tTags, sTense, sWho) {
+        // returns false if no conjugation (also if empty) else true
+        if (this._dPatternConj[sTense][tTags[this._dTenseIdx.get(sTense)]].hasOwnProperty(sWho)
+                && this._dPatternConj[sTense][tTags[this._dTenseIdx.get(sTense)]][sWho]) {
+            return true;
+        }
+        return false;
+    },
+
+    _modifyStringWithSuffixCode: function (sWord, sSfx) {
+        // returns sWord modified by sSfx
+        if (sSfx === "") {
+            return "";
+        }
+        if (sSfx === "0") {
+            return sWord;
+        }
+        try {
+            if (sSfx[0] !== '0') {
+                return sWord.slice(0, -(sSfx.charCodeAt(0)-48)) + sSfx.slice(1); // 48 is the ASCII code for "0"
+            } else {
+                return sWord + sSfx.slice(1);
+            }
+        }
+        catch (e) {
+            console.log(e);
+            return "## erreur, code : " + sSfx + " ##";
+        }
     }
 }
 
 
 class Verb {
@@ -191,107 +181,107 @@
         if (typeof sVerb !== "string" || sVerb === "") {
             throw new TypeError ("The value should be a non-empty string");
         }
         this.sVerb = sVerb;
         this.sVerbAux = "";
-        this._sRawInfo = getVtyp(this.sVerb);
+        this._sRawInfo = conj.getVtyp(this.sVerb);
         this.sInfo = this._readableInfo(this._sRawInfo);
-        this._tTags = _getTags(sVerb);
-        this._tTagsAux = _getTags(this.sVerbAux);
+        this._tTags = conj._getTags(sVerb);
+        this._tTagsAux = conj._getTags(this.sVerbAux);
         this.bProWithEn = (this._sRawInfo[5] === "e");
         this.dConj = new Map ([
             [":Y", new Map ([
                 ["label", "Infinitif"],
                 [":Y", sVerb]
             ])],
             [":PQ", new Map ([
                 ["label", "Participes passés et présent"],
-                [":Q1", _getConjWithTags(sVerb, this._tTags, ":PQ", ":Q1")],
-                [":Q2", _getConjWithTags(sVerb, this._tTags, ":PQ", ":Q2")],
-                [":Q3", _getConjWithTags(sVerb, this._tTags, ":PQ", ":Q3")],
-                [":Q4", _getConjWithTags(sVerb, this._tTags, ":PQ", ":Q4")],
-                [":P", _getConjWithTags(sVerb, this._tTags, ":PQ", ":P")]
+                [":Q1", conj._getConjWithTags(sVerb, this._tTags, ":PQ", ":Q1")],
+                [":Q2", conj._getConjWithTags(sVerb, this._tTags, ":PQ", ":Q2")],
+                [":Q3", conj._getConjWithTags(sVerb, this._tTags, ":PQ", ":Q3")],
+                [":Q4", conj._getConjWithTags(sVerb, this._tTags, ":PQ", ":Q4")],
+                [":P", conj._getConjWithTags(sVerb, this._tTags, ":PQ", ":P")]
             ])],
             [":Ip", new Map ([
                 ["label", "Présent"],
-                [":1s", _getConjWithTags(sVerb, this._tTags, ":Ip", ":1s")],
-                [":1ś", _getConjWithTags(sVerb, this._tTags, ":Ip", ":1ś")],
-                [":2s", _getConjWithTags(sVerb, this._tTags, ":Ip", ":2s")],
-                [":3s", _getConjWithTags(sVerb, this._tTags, ":Ip", ":3s")],
-                [":1p", _getConjWithTags(sVerb, this._tTags, ":Ip", ":1p")],
-                [":2p", _getConjWithTags(sVerb, this._tTags, ":Ip", ":2p")],
-                [":3p", _getConjWithTags(sVerb, this._tTags, ":Ip", ":3p")]
+                [":1s", conj._getConjWithTags(sVerb, this._tTags, ":Ip", ":1s")],
+                [":1ś", conj._getConjWithTags(sVerb, this._tTags, ":Ip", ":1ś")],
+                [":2s", conj._getConjWithTags(sVerb, this._tTags, ":Ip", ":2s")],
+                [":3s", conj._getConjWithTags(sVerb, this._tTags, ":Ip", ":3s")],
+                [":1p", conj._getConjWithTags(sVerb, this._tTags, ":Ip", ":1p")],
+                [":2p", conj._getConjWithTags(sVerb, this._tTags, ":Ip", ":2p")],
+                [":3p", conj._getConjWithTags(sVerb, this._tTags, ":Ip", ":3p")]
             ])],
             [":Iq", new Map ([
                 ["label", "Imparfait"],
-                [":1s", _getConjWithTags(sVerb, this._tTags, ":Iq", ":1s")],
-                [":2s", _getConjWithTags(sVerb, this._tTags, ":Iq", ":2s")],
-                [":3s", _getConjWithTags(sVerb, this._tTags, ":Iq", ":3s")],
-                [":1p", _getConjWithTags(sVerb, this._tTags, ":Iq", ":1p")],
-                [":2p", _getConjWithTags(sVerb, this._tTags, ":Iq", ":2p")],
-                [":3p", _getConjWithTags(sVerb, this._tTags, ":Iq", ":3p")]
+                [":1s", conj._getConjWithTags(sVerb, this._tTags, ":Iq", ":1s")],
+                [":2s", conj._getConjWithTags(sVerb, this._tTags, ":Iq", ":2s")],
+                [":3s", conj._getConjWithTags(sVerb, this._tTags, ":Iq", ":3s")],
+                [":1p", conj._getConjWithTags(sVerb, this._tTags, ":Iq", ":1p")],
+                [":2p", conj._getConjWithTags(sVerb, this._tTags, ":Iq", ":2p")],
+                [":3p", conj._getConjWithTags(sVerb, this._tTags, ":Iq", ":3p")]
             ])],
             [":Is", new Map ([
                 ["label", "Passé simple"],
-                [":1s", _getConjWithTags(sVerb, this._tTags, ":Is", ":1s")],
-                [":2s", _getConjWithTags(sVerb, this._tTags, ":Is", ":2s")],
-                [":3s", _getConjWithTags(sVerb, this._tTags, ":Is", ":3s")],
-                [":1p", _getConjWithTags(sVerb, this._tTags, ":Is", ":1p")],
-                [":2p", _getConjWithTags(sVerb, this._tTags, ":Is", ":2p")],
-                [":3p", _getConjWithTags(sVerb, this._tTags, ":Is", ":3p")]
+                [":1s", conj._getConjWithTags(sVerb, this._tTags, ":Is", ":1s")],
+                [":2s", conj._getConjWithTags(sVerb, this._tTags, ":Is", ":2s")],
+                [":3s", conj._getConjWithTags(sVerb, this._tTags, ":Is", ":3s")],
+                [":1p", conj._getConjWithTags(sVerb, this._tTags, ":Is", ":1p")],
+                [":2p", conj._getConjWithTags(sVerb, this._tTags, ":Is", ":2p")],
+                [":3p", conj._getConjWithTags(sVerb, this._tTags, ":Is", ":3p")]
             ])],
             [":If", new Map ([
                 ["label", "Futur"],
-                [":1s", _getConjWithTags(sVerb, this._tTags, ":If", ":1s")],
-                [":2s", _getConjWithTags(sVerb, this._tTags, ":If", ":2s")],
-                [":3s", _getConjWithTags(sVerb, this._tTags, ":If", ":3s")],
-                [":1p", _getConjWithTags(sVerb, this._tTags, ":If", ":1p")],
-                [":2p", _getConjWithTags(sVerb, this._tTags, ":If", ":2p")],
-                [":3p", _getConjWithTags(sVerb, this._tTags, ":If", ":3p")]
+                [":1s", conj._getConjWithTags(sVerb, this._tTags, ":If", ":1s")],
+                [":2s", conj._getConjWithTags(sVerb, this._tTags, ":If", ":2s")],
+                [":3s", conj._getConjWithTags(sVerb, this._tTags, ":If", ":3s")],
+                [":1p", conj._getConjWithTags(sVerb, this._tTags, ":If", ":1p")],
+                [":2p", conj._getConjWithTags(sVerb, this._tTags, ":If", ":2p")],
+                [":3p", conj._getConjWithTags(sVerb, this._tTags, ":If", ":3p")]
             ])],
             [":Sp", new Map ([
                 ["label", "Présent subjonctif"],
-                [":1s", _getConjWithTags(sVerb, this._tTags, ":Sp", ":1s")],
-                [":1ś", _getConjWithTags(sVerb, this._tTags, ":Sp", ":1ś")],
-                [":2s", _getConjWithTags(sVerb, this._tTags, ":Sp", ":2s")],
-                [":3s", _getConjWithTags(sVerb, this._tTags, ":Sp", ":3s")],
-                [":1p", _getConjWithTags(sVerb, this._tTags, ":Sp", ":1p")],
-                [":2p", _getConjWithTags(sVerb, this._tTags, ":Sp", ":2p")],
-                [":3p", _getConjWithTags(sVerb, this._tTags, ":Sp", ":3p")]
+                [":1s", conj._getConjWithTags(sVerb, this._tTags, ":Sp", ":1s")],
+                [":1ś", conj._getConjWithTags(sVerb, this._tTags, ":Sp", ":1ś")],
+                [":2s", conj._getConjWithTags(sVerb, this._tTags, ":Sp", ":2s")],
+                [":3s", conj._getConjWithTags(sVerb, this._tTags, ":Sp", ":3s")],
+                [":1p", conj._getConjWithTags(sVerb, this._tTags, ":Sp", ":1p")],
+                [":2p", conj._getConjWithTags(sVerb, this._tTags, ":Sp", ":2p")],
+                [":3p", conj._getConjWithTags(sVerb, this._tTags, ":Sp", ":3p")]
             ])],
             [":Sq", new Map ([
                 ["label", "Imparfait subjonctif"],
-                [":1s", _getConjWithTags(sVerb, this._tTags, ":Sq", ":1s")],
-                [":1ś", _getConjWithTags(sVerb, this._tTags, ":Sq", ":1ś")],
-                [":2s", _getConjWithTags(sVerb, this._tTags, ":Sq", ":2s")],
-                [":3s", _getConjWithTags(sVerb, this._tTags, ":Sq", ":3s")],
-                [":1p", _getConjWithTags(sVerb, this._tTags, ":Sq", ":1p")],
-                [":2p", _getConjWithTags(sVerb, this._tTags, ":Sq", ":2p")],
-                [":3p", _getConjWithTags(sVerb, this._tTags, ":Sq", ":3p")]
+                [":1s", conj._getConjWithTags(sVerb, this._tTags, ":Sq", ":1s")],
+                [":1ś", conj._getConjWithTags(sVerb, this._tTags, ":Sq", ":1ś")],
+                [":2s", conj._getConjWithTags(sVerb, this._tTags, ":Sq", ":2s")],
+                [":3s", conj._getConjWithTags(sVerb, this._tTags, ":Sq", ":3s")],
+                [":1p", conj._getConjWithTags(sVerb, this._tTags, ":Sq", ":1p")],
+                [":2p", conj._getConjWithTags(sVerb, this._tTags, ":Sq", ":2p")],
+                [":3p", conj._getConjWithTags(sVerb, this._tTags, ":Sq", ":3p")]
             ])],
             [":K", new Map ([
                 ["label", "Conditionnel"],
-                [":1s", _getConjWithTags(sVerb, this._tTags, ":K", ":1s")],
-                [":2s", _getConjWithTags(sVerb, this._tTags, ":K", ":2s")],
-                [":3s", _getConjWithTags(sVerb, this._tTags, ":K", ":3s")],
-                [":1p", _getConjWithTags(sVerb, this._tTags, ":K", ":1p")],
-                [":2p", _getConjWithTags(sVerb, this._tTags, ":K", ":2p")],
-                [":3p", _getConjWithTags(sVerb, this._tTags, ":K", ":3p")]
+                [":1s", conj._getConjWithTags(sVerb, this._tTags, ":K", ":1s")],
+                [":2s", conj._getConjWithTags(sVerb, this._tTags, ":K", ":2s")],
+                [":3s", conj._getConjWithTags(sVerb, this._tTags, ":K", ":3s")],
+                [":1p", conj._getConjWithTags(sVerb, this._tTags, ":K", ":1p")],
+                [":2p", conj._getConjWithTags(sVerb, this._tTags, ":K", ":2p")],
+                [":3p", conj._getConjWithTags(sVerb, this._tTags, ":K", ":3p")]
             ])],
             [":E", new Map ([
                 ["label", "Impératif"],
-                [":2s", _getConjWithTags(sVerb, this._tTags, ":E", ":2s")],
-                [":1p", _getConjWithTags(sVerb, this._tTags, ":E", ":1p")],
-                [":2p", _getConjWithTags(sVerb, this._tTags, ":E", ":2p")]
+                [":2s", conj._getConjWithTags(sVerb, this._tTags, ":E", ":2s")],
+                [":1p", conj._getConjWithTags(sVerb, this._tTags, ":E", ":1p")],
+                [":2p", conj._getConjWithTags(sVerb, this._tTags, ":E", ":2p")]
             ])]
         ]);
     };
 
     _readableInfo () {
         // returns readable infos
         this.sVerbAux = (this._sRawInfo.slice(7,8) == "e") ? "être" : "avoir";
-        let sGroup = _dGroup.get(this._sRawInfo[0]);
+        let sGroup = conj._dGroup.get(this._sRawInfo[0]);
         let sInfo = "";
         if (this._sRawInfo.slice(3,4) == "t") {
             sInfo = "transitif";
         } else if (this._sRawInfo.slice(4,5) == "n") {
             sInfo = "transitif indirect";
@@ -323,11 +313,11 @@
         }
         if (bPro) {
             if (this.bProWithEn) {
                 sInfi = "s’en " + sInfi;
             } else {
-                sInfi = (_zStartVoy.test(sInfi)) ? "s’" + sInfi : "se " + sInfi;
+                sInfi = (conj._zStartVoy.test(sInfi)) ? "s’" + sInfi : "se " + sInfi;
             }
         }
         if (bNeg) {
             sInfi = "ne pas " + sInfi;
         }
@@ -348,18 +338,18 @@
         if (!this.dConj.get(":PQ").get(":P")) {
             return "";
         }
         let sPartPre;
         if (bTpsCo) {
-            sPartPre = (!bPro) ? _getConjWithTags(this.sVerbAux, this._tTagsAux, ":PQ", ":P") : getConj("être", ":PQ", ":P");
+            sPartPre = (!bPro) ? conj._getConjWithTags(this.sVerbAux, this._tTagsAux, ":PQ", ":P") : getConj("être", ":PQ", ":P");
         } else {
             sPartPre = this.dConj.get(":PQ").get(":P");
         }
         if (sPartPre === "") {
             return "";
         }
-        let bEli = _zStartVoy.test(sPartPre);
+        let bEli = conj._zStartVoy.test(sPartPre);
         if (bPro) {
             if (this.bProWithEn) {
                 sPartPre = "s’en " + sPartPre;
             } else {
                 sPartPre = (bEli) ? "s’" + sPartPre : "se " + sPartPre;
@@ -384,30 +374,30 @@
         let sConj;
         if (!bTpsCo && bInt && sWho == ":1s" && this.dConj.get(sTemps).gl_get(":1ś", false)) {
             sWho = ":1ś";
         }
         if (bTpsCo) {
-            sConj = (!bPro) ? _getConjWithTags(this.sVerbAux, this._tTagsAux, sTemps, sWho) : getConj("être", sTemps, sWho);
+            sConj = (!bPro) ? conj._getConjWithTags(this.sVerbAux, this._tTagsAux, sTemps, sWho) : getConj("être", sTemps, sWho);
         } else {
             sConj = this.dConj.get(sTemps).get(sWho);
         }
         if (sConj === "") {
             return "";
         }
-        let bEli = _zStartVoy.test(sConj);
+        let bEli = conj._zStartVoy.test(sConj);
         if (bPro) {
             if (!this.bProWithEn) {
-                sConj = (bEli) ? _dProObjEl.get(sWho) + sConj : _dProObj.get(sWho) + sConj;
+                sConj = (bEli) ? conj._dProObjEl.get(sWho) + sConj : conj._dProObj.get(sWho) + sConj;
             } else {
-                sConj = _dProObjEl.get(sWho) + "en " + sConj;
+                sConj = conj._dProObjEl.get(sWho) + "en " + sConj;
             }
         }
         if (bNeg) {
             sConj = (bEli && !bPro) ? "n’" + sConj : "ne " + sConj;
         }
         if (bInt) {
-            if (sWho == ":3s" && !_zNeedTeuph.test(sConj)) {
+            if (sWho == ":3s" && !conj._zNeedTeuph.test(sConj)) {
                 sConj += "-t";
             }
             sConj += "-" + this._getPronom(sWho, bFem);
         } else {
             if (sWho == ":1s" && bEli && !bNeg && !bPro) {
@@ -436,39 +426,39 @@
                 return "elle";
             }
         } else if (sWho == ":3p" && bFem) {
             return "elles";
         }
-        return _dProSuj.get(sWho);
+        return conj._dProSuj.get(sWho);
     };
 
     imperatif (sWho, bPro, bNeg, bTpsCo, bFem) {
         if (!this.dConj.get(":E").get(sWho)) {
             return "";
         }
         let sImpe;
         if (bTpsCo) {
-            sImpe = (!bPro) ? _getConjWithTags(this.sVerbAux, this._tTagsAux, ":E", sWho) : getConj("être", ":E", sWho);
+            sImpe = (!bPro) ? conj._getConjWithTags(this.sVerbAux, this._tTagsAux, ":E", sWho) : getConj("être", ":E", sWho);
         } else {
             sImpe = this.dConj.get(":E").get(sWho);
         }
         if (sImpe === "") {
             return "";
         }
-        let bEli = _zStartVoy.test(sImpe);
+        let bEli = conj._zStartVoy.test(sImpe);
         if (bNeg) {
             if (bPro) {
                 if (!this.bProWithEn) {
-                    sImpe = (bEli && sWho == ":2s") ? "ne t’" + sImpe + " pas" : _dImpeProNeg.get(sWho) + sImpe + " pas";
+                    sImpe = (bEli && sWho == ":2s") ? "ne t’" + sImpe + " pas" : conj._dImpeProNeg.get(sWho) + sImpe + " pas";
                 } else {
-                    sImpe = _dImpeProNegEn.get(sWho) + sImpe + " pas";
+                    sImpe = conj._dImpeProNegEn.get(sWho) + sImpe + " pas";
                 }
             } else {
                 sImpe = (bEli) ? "n’" + sImpe + " pas" : "ne " + sImpe + " pas";
             }
         } else if (bPro) {
-            sImpe = (this.bProWithEn) ? sImpe + _dImpeProEn.get(sWho) : sImpe + _dImpePro.get(sWho);
+            sImpe = (this.bProWithEn) ? sImpe + conj._dImpeProEn.get(sWho) : sImpe + conj._dImpePro.get(sWho);
         }
         if (bTpsCo) {
             return sImpe + " " + this._seekPpas(bPro, bFem, sWho.endsWith("p") || this._sRawInfo[5] == "r");
         }
         return sImpe;
@@ -488,17 +478,45 @@
     }
 }
 
 
 if (typeof(exports) !== 'undefined') {
-    // Used for Grammalecte library.
-    // In content scripts, these variable are directly reachable
+    // used within Grammalecte library
+    let helpers = require("resource://grammalecte/helpers.js");
+    conj.init(helpers.loadFile("resource://grammalecte/fr/conj_data.json"));
+} else {
+    // 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);
+    });
+}
+
+
+if (typeof(exports) !== 'undefined') {
+    exports._lVtyp = conj._lVtyp;
+    exports._lTags = conj._lTags;
+    exports._dPatternConj = conj._dPatternConj;
+    exports._dVerb = conj._dVerb;
+    exports.init = conj.init;
+    exports._zStartVoy = conj._zStartVoy;
+    exports._zNeedTeuph = conj._zNeedTeuph;
+    exports._dProSuj = conj._dProSuj;
+    exports._dProObj = conj._dProObj;
+    exports._dProObjEl = conj._dProObjEl;
+    exports._dImpePro = conj._dImpePro;
+    exports._dImpeProNeg = conj._dImpeProNeg;
+    exports._dImpeProEn = conj._dImpeProEn;
+    exports._dImpeProNegEn = conj._dImpeProNegEn;
+    exports._dGroup = conj._dGroup;
+    exports._dTenseIdx = conj._dTenseIdx;
+    exports.isVerb = conj.isVerb;
+    exports.getConj = conj.getConj;
+    exports.hasConj = conj.hasConj;
+    exports.getVtyp = conj.getVtyp;
+    exports.getSimil = conj.getSimil;
+    exports._getTags = conj._getTags;
+    exports._getConjWithTags = conj._getConjWithTags;
+    exports._hasConjWithTags = conj._hasConjWithTags;
+    exports._modifyStringWithSuffixCode = conj._modifyStringWithSuffixCode;
     exports.Verb = Verb;
-    exports.isVerb = isVerb;
-    exports.getConj = getConj;
-    exports.hasConj = hasConj;
-    exports.getVtyp = getVtyp;
-    exports.getSimil = getSimil;
-    exports._getTags = _getTags;
-    exports._hasConjWithTags = _hasConjWithTags;
-    exports._getConjWithTags = _getConjWithTags;
 }

Index: gc_lang/fr/modules-js/cregex.js
==================================================================
--- gc_lang/fr/modules-js/cregex.js
+++ gc_lang/fr/modules-js/cregex.js
@@ -1,301 +1,348 @@
 //// Grammalecte - Compiled regular expressions
 
 
-///// Lemme
-const zLemma = new RegExp("^>([a-zà-öø-ÿ0-9Ā-ʯ][a-zà-öø-ÿ0-9Ā-ʯ-]+)");
-
-///// Masculin / féminin / singulier / pluriel
-const zGender = new RegExp(":[mfe]");
-const zNumber = new RegExp(":[spi]");
-
-///// Nom et adjectif
-const zNA = new RegExp(":[NA]");
-
-//// nombre
-const zNAs = new RegExp(":[NA].*:s");
-const zNAp = new RegExp(":[NA].*:p");
-const zNAi = new RegExp(":[NA].*:i");
-const zNAsi = new RegExp(":[NA].*:[si]");
-const zNApi = new RegExp(":[NA].*:[pi]");
-
-//// genre
-const zNAm = new RegExp(":[NA].*:m");
-const zNAf = new RegExp(":[NA].*:f");
-const zNAe = new RegExp(":[NA].*:e");
-const zNAme = new RegExp(":[NA].*:[me]");
-const zNAfe = new RegExp(":[NA].*:[fe]");
-
-//// nombre et genre
-// singuilier
-const zNAms = new RegExp(":[NA].*:m.*:s");
-const zNAfs = new RegExp(":[NA].*:f.*:s");
-const zNAes = new RegExp(":[NA].*:e.*:s");
-const zNAmes = new RegExp(":[NA].*:[me].*:s");
-const zNAfes = new RegExp(":[NA].*:[fe].*:s");
-
-// singulier et invariable
-const zNAmsi = new RegExp(":[NA].*:m.*:[si]");
-const zNAfsi = new RegExp(":[NA].*:f.*:[si]");
-const zNAesi = new RegExp(":[NA].*:e.*:[si]");
-const zNAmesi = new RegExp(":[NA].*:[me].*:[si]");
-const zNAfesi = new RegExp(":[NA].*:[fe].*:[si]");
-
-// pluriel
-const zNAmp = new RegExp(":[NA].*:m.*:p");
-const zNAfp = new RegExp(":[NA].*:f.*:p");
-const zNAep = new RegExp(":[NA].*:e.*:p");
-const zNAmep = new RegExp(":[NA].*:[me].*:p");
-const zNAfep = new RegExp(":[NA].*:[me].*:p");
-
-// pluriel et invariable
-const zNAmpi = new RegExp(":[NA].*:m.*:[pi]");
-const zNAfpi = new RegExp(":[NA].*:f.*:[pi]");
-const zNAepi = new RegExp(":[NA].*:e.*:[pi]");
-const zNAmepi = new RegExp(":[NA].*:[me].*:[pi]");
-const zNAfepi = new RegExp(":[NA].*:[fe].*:[pi]");
-
-//// Divers
-const zAD = new RegExp(":[AB]");
-
-///// Verbe
-const zVconj = new RegExp(":[123][sp]");
-const zVconj123 = new RegExp(":V[123].*:[123][sp]");
-
-///// Nom | Adjectif | Verbe
-const zNVconj = new RegExp(":(?:N|[123][sp])");
-const zNAVconj = new RegExp(":(?:N|A|[123][sp])");
-
-///// Spécifique
-const zNnotA = new RegExp(":N(?!:A)");
-const zPNnotA = new RegExp(":(?:N(?!:A)|Q)");
-
-///// Noms propres
-const zNP = new RegExp(":(?:M[12P]|T)");
-const zNPm = new RegExp(":(?:M[12P]|T):m");
-const zNPf = new RegExp(":(?:M[12P]|T):f");
-const zNPe = new RegExp(":(?:M[12P]|T):e");
-
-
-///// FONCTIONS
-
-function getLemmaOfMorph (sMorph) {
-    return zLemma.exec(sMorph)[1];
-}
-
-function checkAgreement (l1, l2) {
-    // check number agreement
-    if (!mbInv(l1) && !mbInv(l2)) {
-        if (mbSg(l1) && !mbSg(l2)) {
-            return false;
-        }
-        if (mbPl(l1) && !mbPl(l2)) {
-            return false;
-        }
-    }
-    // check gender agreement
-    if (mbEpi(l1) || mbEpi(l2)) {
-        return true;
-    }
-    if (mbMas(l1) && !mbMas(l2)) {
-        return false;
-    }
-    if (mbFem(l1) && !mbFem(l2)) {
-        return false;
-    }
-    return true;
-}
-
-function checkConjVerb (lMorph, sReqConj) {
-    return lMorph.some(s  =>  s.includes(sReqConj));
-}
-
-function getGender (lMorph) {
-    // returns gender of word (':m', ':f', ':e' or empty string).
-    let sGender = "";
-    for (let sMorph of lMorph) {
-        let m = zGender.exec(sMorph);
-        if (m) {
-            if (!sGender) {
-                sGender = m[0];
-            } else if (sGender != m[0]) {
-                return ":e";
-            }
-        }
-    }
-    return sGender;
-}
-
-function getNumber (lMorph) {
-    // returns number of word (':s', ':p', ':i' or empty string).
-    let sNumber = "";
-    for (let sMorph of lMorph) {
-        let m = zNumber.exec(sWord);
-        if (m) {
-            if (!sNumber) {
-                sNumber = m[0];
-            } else if (sNumber != m[0]) {
-                return ":i";
-            }
-        }
-    }
-    return sNumber;
-}
-
-// NOTE :  isWhat (lMorph)    returns true   if lMorph contains nothing else than What
-//         mbWhat (lMorph)    returns true   if lMorph contains What at least once
-
-//// isXXX = it’s certain
-
-function isNom (lMorph) {
-    return lMorph.every(s  =>  s.includes(":N"));
-}
-
-function isNomNotAdj (lMorph) {
-    return lMorph.every(s  =>  zNnotA.test(s));
-}
-
-function isAdj (lMorph) {
-    return lMorph.every(s  =>  s.includes(":A"));
-}
-
-function isNomAdj (lMorph) {
-    return lMorph.every(s  =>  zNA.test(s));
-}
-
-function isNomVconj (lMorph) {
-    return lMorph.every(s  =>  zNVconj.test(s));
-}
-
-function isInv (lMorph) {
-    return lMorph.every(s  =>  s.includes(":i"));
-}
-function isSg (lMorph) {
-    return lMorph.every(s  =>  s.includes(":s"));
-}
-function isPl (lMorph) {
-    return lMorph.every(s  =>  s.includes(":p"));
-}
-function isEpi (lMorph) {
-    return lMorph.every(s  =>  s.includes(":e"));
-}
-function isMas (lMorph) {
-    return lMorph.every(s  =>  s.includes(":m"));
-}
-function isFem (lMorph) {
-    return lMorph.every(s  =>  s.includes(":f"));
-}
-
-
-//// mbXXX = MAYBE XXX
-
-function mbNom (lMorph) {
-    return lMorph.some(s  =>  s.includes(":N"));
-}
-
-function mbAdj (lMorph) {
-    return lMorph.some(s  =>  s.includes(":A"));
-}
-
-function mbAdjNb (lMorph) {
-    return lMorph.some(s  =>  zAD.test(s));
-}
-
-function mbNomAdj (lMorph) {
-    return lMorph.some(s  =>  zNA.test(s));
-}
-
-function mbNomNotAdj (lMorph) {
-    let b = false;
-    for (let s of lMorph) {
-        if (s.includes(":A")) {
-            return false;
-        }
-        if (s.includes(":N")) {
-            b = true;
-        }
-    }
-    return b;
-}
-
-function mbPpasNomNotAdj (lMorph) {
-    return lMorph.some(s  =>  zPNnotA.test(s));
-}
-
-function mbVconj (lMorph) {
-    return lMorph.some(s  =>  zVconj.test(s));
-}
-
-function mbVconj123 (lMorph) {
-    return lMorph.some(s  =>  zVconj123.test(s));
-}
-
-function mbMG (lMorph) {
-    return lMorph.some(s  =>  s.includes(":G"));
-}
-
-function mbInv (lMorph) {
-    return lMorph.some(s  =>  s.includes(":i"));
-}
-function mbSg (lMorph) {
-    return lMorph.some(s  =>  s.includes(":s"));
-}
-function mbPl (lMorph) {
-    return lMorph.some(s  =>  s.includes(":p"));
-}
-function mbEpi (lMorph) {
-    return lMorph.some(s  =>  s.includes(":e"));
-}
-function mbMas (lMorph) {
-    return lMorph.some(s  =>  s.includes(":m"));
-}
-function mbFem (lMorph) {
-    return lMorph.some(s  =>  s.includes(":f"));
-}
-
-function mbNpr (lMorph) {
-    return lMorph.some(s  =>  zNP.test(s));
-}
-
-function mbNprMasNotFem (lMorph) {
-    if (lMorph.some(s  =>  zNPf.test(s))) {
-        return false;
-    }
-    return lMorph.some(s  =>  zNPm.test(s));
+var cregex = {
+    ///// Lemme
+    _zLemma: new RegExp(">([a-zà-öø-ÿ0-9Ā-ʯ][a-zà-öø-ÿ0-9Ā-ʯ-]+)"),
+
+    ///// Masculin / féminin / singulier / pluriel
+    _zGender: new RegExp(":[mfe]"),
+    _zNumber: new RegExp(":[spi]"),
+
+    ///// Nom et adjectif
+    _zNA: new RegExp(":[NA]"),
+
+    //// nombre
+    _zNAs: new RegExp(":[NA].*:s"),
+    _zNAp: new RegExp(":[NA].*:p"),
+    _zNAi: new RegExp(":[NA].*:i"),
+    _zNAsi: new RegExp(":[NA].*:[si]"),
+    _zNApi: new RegExp(":[NA].*:[pi]"),
+
+    //// genre
+    _zNAm: new RegExp(":[NA].*:m"),
+    _zNAf: new RegExp(":[NA].*:f"),
+    _zNAe: new RegExp(":[NA].*:e"),
+    _zNAme: new RegExp(":[NA].*:[me]"),
+    _zNAfe: new RegExp(":[NA].*:[fe]"),
+
+    //// nombre et genre
+    // singuilier
+    _zNAms: new RegExp(":[NA].*:m.*:s"),
+    _zNAfs: new RegExp(":[NA].*:f.*:s"),
+    _zNAes: new RegExp(":[NA].*:e.*:s"),
+    _zNAmes: new RegExp(":[NA].*:[me].*:s"),
+    _zNAfes: new RegExp(":[NA].*:[fe].*:s"),
+
+    // singulier et invariable
+    _zNAmsi: new RegExp(":[NA].*:m.*:[si]"),
+    _zNAfsi: new RegExp(":[NA].*:f.*:[si]"),
+    _zNAesi: new RegExp(":[NA].*:e.*:[si]"),
+    _zNAmesi: new RegExp(":[NA].*:[me].*:[si]"),
+    _zNAfesi: new RegExp(":[NA].*:[fe].*:[si]"),
+
+    // pluriel
+    _zNAmp: new RegExp(":[NA].*:m.*:p"),
+    _zNAfp: new RegExp(":[NA].*:f.*:p"),
+    _zNAep: new RegExp(":[NA].*:e.*:p"),
+    _zNAmep: new RegExp(":[NA].*:[me].*:p"),
+    _zNAfep: new RegExp(":[NA].*:[me].*:p"),
+
+    // pluriel et invariable
+    _zNAmpi: new RegExp(":[NA].*:m.*:[pi]"),
+    _zNAfpi: new RegExp(":[NA].*:f.*:[pi]"),
+    _zNAepi: new RegExp(":[NA].*:e.*:[pi]"),
+    _zNAmepi: new RegExp(":[NA].*:[me].*:[pi]"),
+    _zNAfepi: new RegExp(":[NA].*:[fe].*:[pi]"),
+
+    //// Divers
+    _zAD: new RegExp(":[AB]"),
+
+    ///// Verbe
+    _zVconj: new RegExp(":[123][sp]"),
+    _zVconj123: new RegExp(":V[123].*:[123][sp]"),
+
+    ///// Nom | Adjectif | Verbe
+    _zNVconj: new RegExp(":(?:N|[123][sp])"),
+    _zNAVconj: new RegExp(":(?:N|A|[123][sp])"),
+
+    ///// Spécifique
+    _zNnotA: new RegExp(":N(?!:A)"),
+    _zPNnotA: new RegExp(":(?:N(?!:A)|Q)"),
+
+    ///// Noms propres
+    _zNP: new RegExp(":(?:M[12P]|T)"),
+    _zNPm: new RegExp(":(?:M[12P]|T):m"),
+    _zNPf: new RegExp(":(?:M[12P]|T):f"),
+    _zNPe: new RegExp(":(?:M[12P]|T):e"),
+
+
+    ///// FONCTIONS
+
+    getLemmaOfMorph: function (sMorph) {
+        return this._zLemma.exec(sMorph)[1];
+    },
+
+    checkAgreement: function (l1, l2) {
+        // check number agreement
+        if (!this.mbInv(l1) && !this.mbInv(l2)) {
+            if (this.mbSg(l1) && !this.mbSg(l2)) {
+                return false;
+            }
+            if (this.mbPl(l1) && !this.mbPl(l2)) {
+                return false;
+            }
+        }
+        // check gender agreement
+        if (this.mbEpi(l1) || this.mbEpi(l2)) {
+            return true;
+        }
+        if (this.mbMas(l1) && !this.mbMas(l2)) {
+            return false;
+        }
+        if (this.mbFem(l1) && !this.mbFem(l2)) {
+            return false;
+        }
+        return true;
+    },
+
+    checkConjVerb: function (lMorph, sReqConj) {
+        return lMorph.some(s  =>  s.includes(sReqConj));
+    },
+
+    getGender: function (lMorph) {
+        // returns gender of word (':m', ':f', ':e' or empty string).
+        let sGender = "";
+        for (let sMorph of lMorph) {
+            let m = this._zGender.exec(sMorph);
+            if (m) {
+                if (!sGender) {
+                    sGender = m[0];
+                } else if (sGender != m[0]) {
+                    return ":e";
+                }
+            }
+        }
+        return sGender;
+    },
+
+    getNumber: function (lMorph) {
+        // returns number of word (':s', ':p', ':i' or empty string).
+        let sNumber = "";
+        for (let sMorph of lMorph) {
+            let m = this._zNumber.exec(sWord);
+            if (m) {
+                if (!sNumber) {
+                    sNumber = m[0];
+                } else if (sNumber != m[0]) {
+                    return ":i";
+                }
+            }
+        }
+        return sNumber;
+    },
+
+    // NOTE :  isWhat (lMorph)    returns true   if lMorph contains nothing else than What
+    //         mbWhat (lMorph)    returns true   if lMorph contains What at least once
+
+    //// isXXX = it’s certain
+
+    isNom: function (lMorph) {
+        return lMorph.every(s  =>  s.includes(":N"));
+    },
+
+    isNomNotAdj: function (lMorph) {
+        return lMorph.every(s  =>  this._zNnotA.test(s));
+    },
+
+    isAdj: function (lMorph) {
+        return lMorph.every(s  =>  s.includes(":A"));
+    },
+
+    isNomAdj: function (lMorph) {
+        return lMorph.every(s  =>  this._zNA.test(s));
+    },
+
+    isNomVconj: function (lMorph) {
+        return lMorph.every(s  =>  this._zNVconj.test(s));
+    },
+
+    isInv: function (lMorph) {
+        return lMorph.every(s  =>  s.includes(":i"));
+    },
+    isSg: function (lMorph) {
+        return lMorph.every(s  =>  s.includes(":s"));
+    },
+    isPl: function (lMorph) {
+        return lMorph.every(s  =>  s.includes(":p"));
+    },
+    isEpi: function (lMorph) {
+        return lMorph.every(s  =>  s.includes(":e"));
+    },
+    isMas: function (lMorph) {
+        return lMorph.every(s  =>  s.includes(":m"));
+    },
+    isFem: function (lMorph) {
+        return lMorph.every(s  =>  s.includes(":f"));
+    },
+
+
+    //// mbXXX = MAYBE XXX
+
+    mbNom: function (lMorph) {
+        return lMorph.some(s  =>  s.includes(":N"));
+    },
+
+    mbAdj: function (lMorph) {
+        return lMorph.some(s  =>  s.includes(":A"));
+    },
+
+    mbAdjNb: function (lMorph) {
+        return lMorph.some(s  =>  this._zAD.test(s));
+    },
+
+    mbNomAdj: function (lMorph) {
+        return lMorph.some(s  =>  this._zNA.test(s));
+    },
+
+    mbNomNotAdj: function (lMorph) {
+        let b = false;
+        for (let s of lMorph) {
+            if (s.includes(":A")) {
+                return false;
+            }
+            if (s.includes(":N")) {
+                b = true;
+            }
+        }
+        return b;
+    },
+
+    mbPpasNomNotAdj: function (lMorph) {
+        return lMorph.some(s  =>  this._zPNnotA.test(s));
+    },
+
+    mbVconj: function (lMorph) {
+        return lMorph.some(s  =>  this._zVconj.test(s));
+    },
+
+    mbVconj123: function (lMorph) {
+        return lMorph.some(s  =>  this._zVconj123.test(s));
+    },
+
+    mbMG: function (lMorph) {
+        return lMorph.some(s  =>  s.includes(":G"));
+    },
+
+    mbInv: function (lMorph) {
+        return lMorph.some(s  =>  s.includes(":i"));
+    },
+    mbSg: function (lMorph) {
+        return lMorph.some(s  =>  s.includes(":s"));
+    },
+    mbPl: function (lMorph) {
+        return lMorph.some(s  =>  s.includes(":p"));
+    },
+    mbEpi: function (lMorph) {
+        return lMorph.some(s  =>  s.includes(":e"));
+    },
+    mbMas: function (lMorph) {
+        return lMorph.some(s  =>  s.includes(":m"));
+    },
+    mbFem: function (lMorph) {
+        return lMorph.some(s  =>  s.includes(":f"));
+    },
+
+    mbNpr: function (lMorph) {
+        return lMorph.some(s  =>  this._zNP.test(s));
+    },
+
+    mbNprMasNotFem: function (lMorph) {
+        if (lMorph.some(s  =>  this._zNPf.test(s))) {
+            return false;
+        }
+        return lMorph.some(s  =>  this._zNPm.test(s));
+    }
 }
 
 
 if (typeof(exports) !== 'undefined') {
-    exports.getLemmaOfMorph = getLemmaOfMorph;
-    exports.checkAgreement = checkAgreement;
-    exports.checkConjVerb = checkConjVerb;
-    exports.getGender = getGender;
-    exports.getNumber = getNumber;
-    exports.isNom = isNom;
-    exports.isNomNotAdj = isNomNotAdj;
-    exports.isAdj = isAdj;
-    exports.isNomAdj = isNomAdj;
-    exports.isNomVconj = isNomVconj;
-    exports.isInv = isInv;
-    exports.isSg = isSg;
-    exports.isPl = isPl;
-    exports.isEpi = isEpi;
-    exports.isMas = isMas;
-    exports.isFem = isFem;
-    exports.mbNom = mbNom;
-    exports.mbAdj = mbAdj;
-    exports.mbAdjNb = mbAdjNb;
-    exports.mbNomAdj = mbNomAdj;
-    exports.mbNomNotAdj = mbNomNotAdj;
-    exports.mbPpasNomNotAdj = mbPpasNomNotAdj;
-    exports.mbVconj = mbVconj;
-    exports.mbVconj123 = mbVconj123;
-    exports.mbMG = mbMG;
-    exports.mbInv = mbInv;
-    exports.mbSg = mbSg;
-    exports.mbPl = mbPl;
-    exports.mbEpi = mbEpi;
-    exports.mbMas = mbMas;
-    exports.mbFem = mbFem;
-    exports.mbNpr = mbNpr;
-    exports.mbNprMasNotFem = mbNprMasNotFem;
+    exports._zLemma = cregex._zLemma;
+    exports._zGender = cregex._zGender;
+    exports._zNumber = cregex._zNumber;
+    exports._zNA = cregex._zNA;
+    exports._zNAs = cregex._zNAs;
+    exports._zNAp = cregex._zNAp;
+    exports._zNAi = cregex._zNAi;
+    exports._zNAsi = cregex._zNAsi;
+    exports._zNApi = cregex._zNApi;
+    exports._zNAm = cregex._zNAm;
+    exports._zNAf = cregex._zNAf;
+    exports._zNAe = cregex._zNAe;
+    exports._zNAme = cregex._zNAme;
+    exports._zNAfe = cregex._zNAfe;
+    exports._zNAms = cregex._zNAms;
+    exports._zNAfs = cregex._zNAfs;
+    exports._zNAes = cregex._zNAes;
+    exports._zNAmes = cregex._zNAmes;
+    exports._zNAfes = cregex._zNAfes;
+    exports._zNAmsi = cregex._zNAmsi;
+    exports._zNAfsi = cregex._zNAfsi;
+    exports._zNAesi = cregex._zNAesi;
+    exports._zNAmesi = cregex._zNAmesi;
+    exports._zNAfesi = cregex._zNAfesi;
+    exports._zNAmp = cregex._zNAmp;
+    exports._zNAfp = cregex._zNAfp;
+    exports._zNAep = cregex._zNAep;
+    exports._zNAmep = cregex._zNAmep;
+    exports._zNAfep = cregex._zNAfep;
+    exports._zNAmpi = cregex._zNAmpi;
+    exports._zNAfpi = cregex._zNAfpi;
+    exports._zNAepi = cregex._zNAepi;
+    exports._zNAmepi = cregex._zNAmepi;
+    exports._zNAfepi = cregex._zNAfepi;
+    exports._zAD = cregex._zAD;
+    exports._zVconj = cregex._zVconj;
+    exports._zVconj123 = cregex._zVconj123;
+    exports._zNVconj = cregex._zNVconj;
+    exports._zNAVconj = cregex._zNAVconj;
+    exports._zNnotA = cregex._zNnotA;
+    exports._zPNnotA = cregex._zPNnotA;
+    exports._zNP = cregex._zNP;
+    exports._zNPm = cregex._zNPm;
+    exports._zNPf = cregex._zNPf;
+    exports._zNPe = cregex._zNPe;
+    exports.getLemmaOfMorph = cregex.getLemmaOfMorph;
+    exports.checkAgreement = cregex.checkAgreement;
+    exports.checkConjVerb = cregex.checkConjVerb;
+    exports.getGender = cregex.getGender;
+    exports.getNumber = cregex.getNumber;
+    exports.isNom = cregex.isNom;
+    exports.isNomNotAdj = cregex.isNomNotAdj;
+    exports.isAdj = cregex.isAdj;
+    exports.isNomAdj = cregex.isNomAdj;
+    exports.isNomVconj = cregex.isNomVconj;
+    exports.isInv = cregex.isInv;
+    exports.isSg = cregex.isSg;
+    exports.isPl = cregex.isPl;
+    exports.isEpi = cregex.isEpi;
+    exports.isMas = cregex.isMas;
+    exports.isFem = cregex.isFem;
+    exports.mbNom = cregex.mbNom;
+    exports.mbAdj = cregex.mbAdj;
+    exports.mbAdjNb = cregex.mbAdjNb;
+    exports.mbNomAdj = cregex.mbNomAdj;
+    exports.mbNomNotAdj = cregex.mbNomNotAdj;
+    exports.mbPpasNomNotAdj = cregex.mbPpasNomNotAdj;
+    exports.mbVconj = cregex.mbVconj;
+    exports.mbVconj123 = cregex.mbVconj123;
+    exports.mbMG = cregex.mbMG;
+    exports.mbInv = cregex.mbInv;
+    exports.mbSg = cregex.mbSg;
+    exports.mbPl = cregex.mbPl;
+    exports.mbEpi = cregex.mbEpi;
+    exports.mbMas = cregex.mbMas;
+    exports.mbFem = cregex.mbFem;
+    exports.mbNpr = cregex.mbNpr;
+    exports.mbNprMasNotFem = cregex.mbNprMasNotFem;
 }

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
@@ -20,11 +20,11 @@
     if (s2 == "eux") {
         return "ils";
     }
     if (s2 == "elle" || s2 == "elles") {
         // We don’t check if word exists in _dAnalyses, for it is assumed it has been done before
-        if (cr.mbNprMasNotFem(_dAnalyses.gl_get(s1, ""))) {
+        if (cregex.mbNprMasNotFem(_dAnalyses.gl_get(s1, ""))) {
             return "ils";
         }
         // si épicène, indéterminable, mais OSEF, le féminin l’emporte
         return "elles";
     }
@@ -32,22 +32,22 @@
 }
 
 function apposition (sWord1, sWord2) {
     // returns true if nom + nom (no agreement required)
     // We don’t check if word exists in _dAnalyses, for it is assumed it has been done before
-    return cr.mbNomNotAdj(_dAnalyses.gl_get(sWord2, "")) && cr.mbPpasNomNotAdj(_dAnalyses.gl_get(sWord1, ""));
+    return cregex.mbNomNotAdj(_dAnalyses.gl_get(sWord2, "")) && cregex.mbPpasNomNotAdj(_dAnalyses.gl_get(sWord1, ""));
 }
 
 function isAmbiguousNAV (sWord) {
     // words which are nom|adj and verb are ambiguous (except être and avoir)
     if (!_dAnalyses.has(sWord) && !_storeMorphFromFSA(sWord)) {
         return false;
     }
-    if (!cr.mbNomAdj(_dAnalyses.gl_get(sWord, "")) || sWord == "est") {
+    if (!cregex.mbNomAdj(_dAnalyses.gl_get(sWord, "")) || sWord == "est") {
         return false;
     }
-    if (cr.mbVconj(_dAnalyses.gl_get(sWord, "")) && !cr.mbMG(_dAnalyses.gl_get(sWord, ""))) {
+    if (cregex.mbVconj(_dAnalyses.gl_get(sWord, "")) && !cregex.mbMG(_dAnalyses.gl_get(sWord, ""))) {
         return true;
     }
     return false;
 }
 
@@ -56,19 +56,19 @@
     // We don’t check if word exists in _dAnalyses, for it is assumed it has been done before
     let a2 = _dAnalyses.gl_get(sWord2, null);
     if (!a2 || a2.length === 0) {
         return false;
     }
-    if (cr.checkConjVerb(a2, sReqMorphConj)) {
+    if (cregex.checkConjVerb(a2, sReqMorphConj)) {
         // verb word2 is ok
         return false;
     }
     let a1 = _dAnalyses.gl_get(sWord1, null);
     if (!a1 || a1.length === 0) {
         return false;
     }
-    if (cr.checkAgreement(a1, a2) && (cr.mbAdj(a2) || cr.mbAdj(a1))) {
+    if (cregex.checkAgreement(a1, a2) && (cregex.mbAdj(a2) || cregex.mbAdj(a1))) {
         return false;
     }
     return true;
 }
 
@@ -77,26 +77,26 @@
     // We don’t check if word exists in _dAnalyses, for it is assumed it has been done before
     let a2 = _dAnalyses.gl_get(sWord2, null)
     if (!a2 || a2.length === 0) {
         return false;
     }
-    if (cr.checkConjVerb(a2, sReqMorphConj)) {
+    if (cregex.checkConjVerb(a2, sReqMorphConj)) {
         // verb word2 is ok
         return false;
     }
     let a1 = _dAnalyses.gl_get(sWord1, null);
     if (!a1 || a1.length === 0) {
         return false;
     }
-    if (cr.checkAgreement(a1, a2) && (cr.mbAdj(a2) || cr.mbAdjNb(a1))) {
+    if (cregex.checkAgreement(a1, a2) && (cregex.mbAdj(a2) || cregex.mbAdjNb(a1))) {
         return false;
     }
     // now, we know there no agreement, and conjugation is also wrong
-    if (cr.isNomAdj(a1)) {
+    if (cregex.isNomAdj(a1)) {
         return true;
     }
-    //if cr.isNomAdjVerb(a1): # considered true
+    //if cregex.isNomAdjVerb(a1): # considered true
     if (bLastHopeCond) {
         return true;
     }
     return false;
 }
@@ -109,11 +109,11 @@
     }
     let a1 = _dAnalyses.gl_get(sWord1, null);
     if (!a1 || a1.length === 0) {
         return true;
     }
-    return cr.checkAgreement(a1, a2);
+    return cregex.checkAgreement(a1, a2);
 }
 
 function mbUnit (s) {
     if (/[µ\/⁰¹²³⁴⁵⁶⁷⁸⁹Ωℓ·]/.test(s)) {
         return true;

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,12 @@
 //// GRAMMAR CHECKING ENGINE PLUGIN: Suggestion mechanisms
 
-const conj = require("resource://grammalecte/fr/conj.js");
-const mfsp = require("resource://grammalecte/fr/mfsp.js");
-const phonet = require("resource://grammalecte/fr/phonet.js");
+if (typeof(exports) !== '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");
+}
 
 
 //// verbs
 
 function suggVerb (sFlex, sWho, funcSugg2=null) {
@@ -195,11 +197,11 @@
     // returns plural forms assuming sFlex is singular
     if (sWordToAgree) {
         if (!_dAnalyses.has(sWordToAgree) && !_storeMorphFromFSA(sWordToAgree)) {
             return "";
         }
-        let sGender = cr.getGender(_dAnalyses.gl_get(sWordToAgree, []));
+        let sGender = cregex.getGender(_dAnalyses.gl_get(sWordToAgree, []));
         if (sGender == ":m") {
             return suggMasPlur(sFlex);
         } else if (sGender == ":f") {
             return suggFemPlur(sFlex);
         }
@@ -261,18 +263,18 @@
         if (!sMorph.includes(":V")) {
             // not a verb
             if (sMorph.includes(":m") || sMorph.includes(":e")) {
                 aSugg.add(suggSing(sFlex));
             } else {
-                let sStem = cr.getLemmaOfMorph(sMorph);
+                let sStem = cregex.getLemmaOfMorph(sMorph);
                 if (mfsp.isFemForm(sStem)) {
                     mfsp.getMasForm(sStem, false).forEach(function(x) { aSugg.add(x); });
                 }
             }
         } else {
             // a verb
-            let sVerb = cr.getLemmaOfMorph(sMorph);
+            let sVerb = cregex.getLemmaOfMorph(sMorph);
             if (conj.hasConj(sVerb, ":PQ", ":Q1") && conj.hasConj(sVerb, ":PQ", ":Q3")) {
                 // We also check if the verb has a feminine form.
                 // If not, we consider it’s better to not suggest the masculine one, as it can be considered invariable.
                 aSugg.add(conj.getConj(sVerb, ":PQ", ":Q1"));
             }
@@ -297,18 +299,18 @@
         if (!sMorph.includes(":V")) {
             // not a verb
             if (sMorph.includes(":m") || sMorph.includes(":e")) {
                 aSugg.add(suggPlur(sFlex));
             } else {
-                let sStem = cr.getLemmaOfMorph(sMorph);
+                let sStem = cregex.getLemmaOfMorph(sMorph);
                 if (mfsp.isFemForm(sStem)) {
                     mfsp.getMasForm(sStem, true).forEach(function(x) { aSugg.add(x); });
                 }
             }
         } else {
             // a verb
-            let sVerb = cr.getLemmaOfMorph(sMorph);
+            let sVerb = cregex.getLemmaOfMorph(sMorph);
             if (conj.hasConj(sVerb, ":PQ", ":Q2")) {
                 aSugg.add(conj.getConj(sVerb, ":PQ", ":Q2"));
             } else if (conj.hasConj(sVerb, ":PQ", ":Q1")) {
                 let sSugg = conj.getConj(sVerb, ":PQ", ":Q1");
                 // it is necessary to filter these flexions, like “succédé” or “agi” that are not masculine plural
@@ -338,18 +340,18 @@
         if (!sMorph.includes(":V")) {
             // not a verb
             if (sMorph.includes(":f") || sMorph.includes(":e")) {
                 aSugg.add(suggSing(sFlex));
             } else {
-                let sStem = cr.getLemmaOfMorph(sMorph);
+                let sStem = cregex.getLemmaOfMorph(sMorph);
                 if (mfsp.isFemForm(sStem)) {
                     aSugg.add(sStem);
                 }
             }
         } else {
             // a verb
-            let sVerb = cr.getLemmaOfMorph(sMorph);
+            let sVerb = cregex.getLemmaOfMorph(sMorph);
             if (conj.hasConj(sVerb, ":PQ", ":Q3")) {
                 aSugg.add(conj.getConj(sVerb, ":PQ", ":Q3"));
             }
         }
     }
@@ -372,18 +374,18 @@
         if (!sMorph.includes(":V")) {
             // not a verb
             if (sMorph.includes(":f") || sMorph.includes(":e")) {
                 aSugg.add(suggPlur(sFlex));
             } else {
-                let sStem = cr.getLemmaOfMorph(sMorph);
+                let sStem = cregex.getLemmaOfMorph(sMorph);
                 if (mfsp.isFemForm(sStem)) {
                     aSugg.add(sStem+"s");
                 }
             }
         } else {
             // a verb
-            let sVerb = cr.getLemmaOfMorph(sMorph);
+            let sVerb = cregex.getLemmaOfMorph(sMorph);
             if (conj.hasConj(sVerb, ":PQ", ":Q4")) {
                 aSugg.add(conj.getConj(sVerb, ":PQ", ":Q4"));
             }
         }
     }

Index: gc_lang/fr/modules-js/lexicographe.js
==================================================================
--- gc_lang/fr/modules-js/lexicographe.js
+++ gc_lang/fr/modules-js/lexicographe.js
@@ -5,13 +5,13 @@
 
 ${string}
 ${map}
 
 
-const helpers = require("resource://grammalecte/helpers.js");
-const tkz = require("resource://grammalecte/tokenizer.js");
-
+if (typeof(exports) !== 'undefined') {
+    const helpers = require("resource://grammalecte/helpers.js");
+}
 
 const _dTAGS = new Map ([
     [':G', "[mot grammatical]"],
     [':N', " nom,"],
     [':A', " adjectif,"],

Index: gc_lang/fr/modules-js/mfsp.js
==================================================================
--- gc_lang/fr/modules-js/mfsp.js
+++ gc_lang/fr/modules-js/mfsp.js
@@ -1,87 +1,93 @@
 // Grammalecte
 
 "use strict";
 
-const helpers = require("resource://grammalecte/helpers.js");
-const echo = helpers.echo;
+
+if (typeof(exports) !== 'undefined') {
+    var helpers = require("resource://grammalecte/helpers.js");
+}
+
 
-const oData = JSON.parse(helpers.loadFile("resource://grammalecte/fr/mfsp_data.json"));
+const _oMfspData = JSON.parse(helpers.loadFile("resource://grammalecte/fr/mfsp_data.json"));
 
 // list of affix codes
-const _lTagMiscPlur = oData.lTagMiscPlur;
-const _lTagMasForm = oData.lTagMasForm;
+const _lTagMiscPlur = _oMfspData.lTagMiscPlur;
+const _lTagMasForm = _oMfspData.lTagMasForm;
 
 // dictionary of words with uncommon plurals (-x, -ux, english, latin and italian plurals) and tags to generate them
-const _dMiscPlur = helpers.objectToMap(oData.dMiscPlur);
+const _dMiscPlur = helpers.objectToMap(_oMfspData.dMiscPlur);
 
 // dictionary of feminine forms and tags to generate masculine forms (singular and plural)
-const _dMasForm = helpers.objectToMap(oData.dMasForm);
-
-
-
-function isFemForm (sWord) {
-    // returns True if sWord exists in _dMasForm
-    return _dMasForm.has(sWord);
-}
-
-function getMasForm (sWord, bPlur) {
-    // returns masculine form with feminine form
-    if (_dMasForm.has(sWord)) {
-        return [ for (sTag of _whatSuffixCodes(sWord, bPlur))  _modifyStringWithSuffixCode(sWord, sTag) ];
-    }
-    return [];
-}
-
-function hasMiscPlural (sWord) {
-    // returns True if sWord exists in dMiscPlur
-    return _dMiscPlur.has(sWord);
-}
-
-function getMiscPlural (sWord) {
-    // returns plural form with singular form
-    if (_dMiscPlur.has(sWord)) {
-        return [ for (sTag of _lTagMiscPlur[_dMiscPlur.get(sWord)].split("|"))  _modifyStringWithSuffixCode(sWord, sTag) ];
-    }
-    return [];
-}
-
-function _whatSuffixCodes (sWord, bPlur) {
-    // necessary only for dMasFW
-    let sSfx = _lTagMasForm[_dMasForm.get(sWord)];
-    if (sSfx.includes("/")) {
-        if (bPlur) {
-            return sSfx.slice(sSfx.indexOf("/")+1).split("|");
-        }
-        return sSfx.slice(0, sSfx.indexOf("/")).split("|");
-    }
-    return sSfx.split("|");
-}
-
-function _modifyStringWithSuffixCode (sWord, sSfx) {
-    // returns sWord modified by sSfx
-    if (!sWord) {
-        return "";
-    }
-    if (sSfx === "0") {
-        return sWord;
-    }
-    try {
-        if (sSfx[0] !== '0') {
-            return sWord.slice(0, -(sSfx.charCodeAt(0)-48)) + sSfx.slice(1); // 48 is the ASCII code for "0"
-        } else {
-            return sWord + sSfx.slice(1);
-        }
-    }
-    catch (e) {
-        console.log(e);
-        return "## erreur, code : " + sSfx + " ##";
+const _dMasForm = helpers.objectToMap(_oMfspData.dMasForm);
+
+
+const mfsp = {
+    isFemForm: function (sWord) {
+        // returns True if sWord exists in _dMasForm
+        return _dMasForm.has(sWord);
+    },
+
+    getMasForm: function (sWord, bPlur) {
+        // returns masculine form with feminine form
+        if (_dMasForm.has(sWord)) {
+            return [ for (sTag of this._whatSuffixCode(sWord, bPlur))  this._modifyStringWithSuffixCode(sWord, sTag) ];
+        }
+        return [];
+    },
+
+    hasMiscPlural: function (sWord) {
+        // returns True if sWord exists in dMiscPlur
+        return _dMiscPlur.has(sWord);
+    },
+
+    getMiscPlural: function (sWord) {
+        // returns plural form with singular form
+        if (_dMiscPlur.has(sWord)) {
+            return [ for (sTag of _lTagMiscPlur[_dMiscPlur.get(sWord)].split("|"))  this._modifyStringWithSuffixCode(sWord, sTag) ];
+        }
+        return [];
+    },
+
+    _whatSuffixCode: function (sWord, bPlur) {
+        // necessary only for dMasFW
+        let sSfx = _lTagMasForm[_dMasForm.get(sWord)];
+        if (sSfx.includes("/")) {
+            if (bPlur) {
+                return sSfx.slice(sSfx.indexOf("/")+1).split("|");
+            }
+            return sSfx.slice(0, sSfx.indexOf("/")).split("|");
+        }
+        return sSfx.split("|");
+    },
+
+    _modifyStringWithSuffixCode: function (sWord, sSfx) {
+        // returns sWord modified by sSfx
+        if (!sWord) {
+            return "";
+        }
+        if (sSfx === "0") {
+            return sWord;
+        }
+        try {
+            if (sSfx[0] !== '0') {
+                return sWord.slice(0, -(sSfx.charCodeAt(0)-48)) + sSfx.slice(1); // 48 is the ASCII code for "0"
+            } else {
+                return sWord + sSfx.slice(1);
+            }
+        }
+        catch (e) {
+            console.log(e);
+            return "## erreur, code : " + sSfx + " ##";
+        }
     }
 }
 
 
 if (typeof(exports) !== 'undefined') {
-    exports.isFemForm = isFemForm;
-    exports.getMasForm = getMasForm;
-    exports.hasMiscPlural = hasMiscPlural;
-    exports.getMiscPlural = getMiscPlural;
+    exports.isFemForm = mfsp.isFemForm;
+    exports.getMasForm = mfsp.getMasForm;
+    exports.hasMiscPlural = mfsp.hasMiscPlural;
+    exports.getMiscPlural = mfsp.getMiscPlural;
+    exports._whatSuffixCode = mfsp._whatSuffixCode;
+    exports._modifyStringWithSuffixCode = mfsp._modifyStringWithSuffixCode;
 }

Index: gc_lang/fr/modules-js/phonet.js
==================================================================
--- gc_lang/fr/modules-js/phonet.js
+++ gc_lang/fr/modules-js/phonet.js
@@ -1,75 +1,77 @@
 // Grammalecte - Suggestion phonétique
 
-const helpers = require("resource://grammalecte/helpers.js");
-const echo = helpers.echo;
-
-const oData = JSON.parse(helpers.loadFile("resource://grammalecte/fr/phonet_data.json"));
-
-const _dWord = helpers.objectToMap(oData.dWord);
-const _lSet = oData.lSet;
-const _dMorph = helpers.objectToMap(oData.dMorph);
-
-
-
-function hasSimil (sWord, sPattern=null) {
-    // return True if there is list of words phonetically similar to sWord
-    if (!sWord) {
-        return false;
-    }
-    if (_dWord.has(sWord)) {
-        if (sPattern) {
-            return getSimil(sWord).some(sSimil => _dMorph.gl_get(sSimil, []).some(sMorph => sMorph.search(sPattern) >= 0));
-        }
-        return true;
-    }
-    if (sWord.slice(0,1).gl_isUpperCase()) {
-        sWord = sWord.toLowerCase();
-        if (_dWord.has(sWord)) {
-            if (sPattern) {
-                return getSimil(sWord).some(sSimil => _dMorph.gl_get(sSimil, []).some(sMorph => sMorph.search(sPattern) >= 0));
-            }
-            return true;
-        }
-    }
-    return false;
-}
-
-function getSimil (sWord) {
-    // return list of words phonetically similar to sWord
-    if (!sWord) {
-        return [];
-    }
-    if (_dWord.has(sWord)) {
-        return _lSet[_dWord.get(sWord)];
-    }
-    if (sWord.slice(0,1).gl_isUpperCase()) {
-        sWord = sWord.toLowerCase();
-        if (_dWord.has(sWord)) {
-            return _lSet[_dWord.get(sWord)];
-        }
-    }
-    return [];
-}
-
-function selectSimil (sWord, sPattern) {
-    // return list of words phonetically similar to sWord and whom POS is matching sPattern
-    if (!sPattern) {
-        return new Set(getSimil(sWord));
-    }
-    let aSelect = new Set();
-    for (let sSimil of getSimil(sWord)) {
-        for (let sMorph of _dMorph.gl_get(sSimil, [])) {
-            if (sMorph.search(sPattern) >= 0) {
-                aSelect.add(sSimil);
-            }
-        }
-    }
-    return aSelect;
+if (typeof(exports) !== 'undefined') {
+    var helpers = require("resource://grammalecte/helpers.js");
+}
+
+
+const _oPhonetData = JSON.parse(helpers.loadFile("resource://grammalecte/fr/phonet_data.json"));
+const _dPhonetWord = helpers.objectToMap(_oPhonetData.dWord);
+const _lPhonetSet = _oPhonetData.lSet;
+const _dPhonetMorph = helpers.objectToMap(_oPhonetData.dMorph);
+
+
+var phonet = {
+    hasSimil: function (sWord, sPattern=null) {
+        // return True if there is list of words phonetically similar to sWord
+        if (!sWord) {
+            return false;
+        }
+        if (_dPhonetWord.has(sWord)) {
+            if (sPattern) {
+                return this.getSimil(sWord).some(sSimil => _dPhonetMorph.gl_get(sSimil, []).some(sMorph => sMorph.search(sPattern) >= 0));
+            }
+            return true;
+        }
+        if (sWord.slice(0,1).gl_isUpperCase()) {
+            sWord = sWord.toLowerCase();
+            if (_dPhonetWord.has(sWord)) {
+                if (sPattern) {
+                    return this.getSimil(sWord).some(sSimil => _dPhonetMorph.gl_get(sSimil, []).some(sMorph => sMorph.search(sPattern) >= 0));
+                }
+                return true;
+            }
+        }
+        return false;
+    },
+
+    getSimil: function (sWord) {
+        // return list of words phonetically similar to sWord
+        if (!sWord) {
+            return [];
+        }
+        if (_dPhonetWord.has(sWord)) {
+            return _lPhonetSet[_dPhonetWord.get(sWord)];
+        }
+        if (sWord.slice(0,1).gl_isUpperCase()) {
+            sWord = sWord.toLowerCase();
+            if (_dPhonetWord.has(sWord)) {
+                return _lPhonetSet[_dPhonetWord.get(sWord)];
+            }
+        }
+        return [];
+    },
+
+    selectSimil: function (sWord, sPattern) {
+        // return list of words phonetically similar to sWord and whom POS is matching sPattern
+        if (!sPattern) {
+            return new Set(this.getSimil(sWord));
+        }
+        let aSelect = new Set();
+        for (let sSimil of this.getSimil(sWord)) {
+            for (let sMorph of _dPhonetMorph.gl_get(sSimil, [])) {
+                if (sMorph.search(sPattern) >= 0) {
+                    aSelect.add(sSimil);
+                }
+            }
+        }
+        return aSelect;
+    }
 }
 
 
 if (typeof(exports) !== 'undefined') {
-    exports.hasSimil = hasSimil;
-    exports.getSimil = getSimil;
-    exports.selectSimil = selectSimil;
+    exports.hasSimil = phonet.hasSimil;
+    exports.getSimil = phonet.getSimil;
+    exports.selectSimil = phonet.selectSimil;
 }

Index: gc_lang/fr/modules-js/textformatter.js
==================================================================
--- gc_lang/fr/modules-js/textformatter.js
+++ gc_lang/fr/modules-js/textformatter.js
@@ -196,11 +196,11 @@
     "ma_1letter_uppercase":       [ [/[  ]([LDJNMTSCÇ]) (?=[aàeéêiîoôuyhAÀEÉÊIÎOÔUYH])/g, "$1’"],
                                     [/^([LDJNMTSCÇ]) (?=[aàeéêiîoôuyhAÀEÉÊIÎOÔUYH])/g, "$1’"] ]
 };
 
 
-const dDefaultOptions = new Map ([
+const dTFDefaultOptions = new Map ([
     ["ts_units", true],
     ["start_of_paragraph", true],
     ["end_of_paragraph", true],
     ["between_words", true],
     ["before_punctuation", true],
@@ -249,11 +249,11 @@
     ["ma_word", true],
     ["ma_1letter_lowercase", false],
     ["ma_1letter_uppercase", false]
 ]);
 
-const dOptions = dDefaultOptions.gl_shallowCopy();
+const dTFOptions = dTFDefaultOptions.gl_shallowCopy();
 
 
 class TextFormatter {
 
     constructor () {
@@ -260,13 +260,13 @@
         this.sLang = "fr";
     };
 
     formatText (sText, dOpt=null) {
         if (dOpt !== null) {
-            dOptions.gl_updateOnlyExistingKeys(dOpt);
+            dTFOptions.gl_updateOnlyExistingKeys(dOpt);
         }
-        for (let [sOptName, bVal] of dOptions) {
+        for (let [sOptName, bVal] of dTFOptions) {
             if (bVal && oReplTable.has(sOptName)) {
                 for (let [zRgx, sRep] of oReplTable[sOptName]) {
                     sText = sText.replace(zRgx, sRep);
                 }
             }
@@ -273,14 +273,14 @@
         }
         return sText;
     };
 
     getDefaultOptions () {
-        return dDefaultOptions;
+        return dTFDefaultOptions;
     }
 }
 
 
 if (typeof(exports) !== 'undefined') {
     exports.TextFormatter = TextFormatter;
     exports.oReplTable = oReplTable;
 }

Index: gc_lang/fr/tb/content/overlay.js
==================================================================
--- gc_lang/fr/tb/content/overlay.js
+++ gc_lang/fr/tb/content/overlay.js
@@ -910,10 +910,11 @@
 }, false);
 
 window.addEventListener("load", function (xEvent) {
     oDictIgniter.init();
     oGrammarChecker.loadGC();
+    //oGrammarChecker.fullTests();
 }, false);
 
 window.addEventListener("compose-window-init", function (xEvent) {
     oGrammarChecker.loadUI();
     oGrammarChecker.closePanel();

Index: gc_lang/fr/webext/gce_worker.js
==================================================================
--- gc_lang/fr/webext/gce_worker.js
+++ gc_lang/fr/webext/gce_worker.js
@@ -1,31 +1,30 @@
-
-
 /*
-try {
-    console.log("BEFORE");
-    //var myhelpers = require('./grammalecte/helpers.js');
-    require(['./grammalecte/helpers.js'], function (foo) {
-        console.log("LOADING");
-        echo("MODULE LOADED2");
-    });
-    console.log("AFTER");
-}
-catch (e) {
-    console.log("\n" + e.fileName + "\n" + e.name + "\nline: " + e.lineNumber + "\n" + e.message);
-    console.error(e);
-}*/
-
-echo("VA TE FAIRE FOUTRE");
-
-
-
-let gce = null; // module: grammar checker engine
-let text = null;
-let tkz = null; // module: tokenizer
-let lxg = null; // module: lexicographer
-let helpers = null;
+    WARNING.
+
+    JavaScript still sucks.
+    No module available in WebExtension at the moment! :(
+    No require, no import/export.
+
+    In order to use the same base of code with XUL-addon for Thunderbird and SDK-addon for Firefox,
+    all modules have been “objectified”. And while they are still imported via “require”
+    in the previous extensions, they are loaded as background scripts in WebExtension sharing
+    the same memory space (it seems)…
+
+    When JavaScript become a modern language, “deobjectify” the modules…
+
+    ATM, import/export are not available by default:
+    — Chrome 60 – behind the Experimental Web Platform flag in chrome:flags.
+    — Firefox 54 – behind the dom.moduleScripts.enabled setting in about:config.
+    — Edge 15 – behind the Experimental JavaScript Features setting in about:flags.
+
+    https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import
+    https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export
+*/
+
+helpers.echo("START");
+
 
 let oTokenizer = null;
 let oDict = null;
 let oLxg = null;
 

Index: gc_lang/fr/webext/manifest.json
==================================================================
--- gc_lang/fr/webext/manifest.json
+++ gc_lang/fr/webext/manifest.json
@@ -28,11 +28,16 @@
     "default_popup": "panel/main.html",
     "default_title": "Grammalecte [fr]",
     "browser_style": false
   },
   "background": {
-    "scripts": ["require.js", "grammalecte/helpers.js", "gce_worker.js"]
+    "scripts": [
+      "grammalecte/helpers.js",
+      "grammalecte/text.js",
+      "grammalecte/tokenizer.js",
+      "gce_worker.js"
+    ]
   },
   "web_accessible_resources": [
     "beasts/frog.jpg",
     "beasts/turtle.jpg",
     "beasts/snake.jpg"

Index: gc_lang/fr/xpi/data/conj_panel.js
==================================================================
--- gc_lang/fr/xpi/data/conj_panel.js
+++ gc_lang/fr/xpi/data/conj_panel.js
@@ -69,11 +69,11 @@
             if (sVerb.endsWith("?")) {
                 document.getElementById('oint').checked = true;
                 sVerb = sVerb.slice(0,-1).trim();
             }
 
-            if (!isVerb(sVerb)) {
+            if (!conj.isVerb(sVerb)) {
                 document.getElementById('verb').style = "color: #BB4411;";
             } else {
                 self.port.emit("show");
                 document.getElementById('verb_title').textContent = sVerb;
                 document.getElementById('verb').style = "color: #999999;";