Index: gc_core/js/helpers.js
==================================================================
--- gc_core/js/helpers.js
+++ gc_core/js/helpers.js
@@ -5,94 +5,94 @@
 
 // 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;
-}
+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 = setLogOutput;
-    exports.echo = echo;
-    exports.logerror = logerror;
-    exports.inspect = inspect;
-    exports.loadFile = loadFile;
-    exports.objectToMap = objectToMap;
-    exports.mapToObject = mapToObject;
+    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(require) !== '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}
@@ -13,16 +16,15 @@
 
 
 class IBDAWG {
     // INDEXABLE BINARY DIRECT ACYCLIC WORD GRAPH
 
-    constructor (sDicName) {
+    constructor (sDicName, sPath="") {
         try {
-            const dict = JSON.parse(helpers.loadFile("resource://grammalecte/_dictionaries/"+sDicName));
+            let sURL = (sPath !== "") ? sPath + "/" + sDicName : "resource://grammalecte/_dictionaries/"+sDicName;
+            const dict = JSON.parse(helpers.loadFile(sURL));
             Object.assign(this, dict);
-            //const dict = require("resource://grammalecte/"+sLang+"/dictionary.js");
-            //Object.assign(this, dict.dictionary);
         }
         catch (e) {
             throw Error("# Error. File not found or not loadable.\n" + e.message + "\n");
         }
         /*
@@ -39,15 +41,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:
@@ -72,18 +74,18 @@
                 throw ValueError("# Error: unknown code: " + this.nVersion);
         }
         //console.log(this.getInfo());
         this.bOptNumSigle = true;
         this.bOptNumAtLast = false;
-    };
+    }
 
     getInfo () {
         return  `  Language: ${this.sLang}      Version: ${this.nVersion}      Stemming: ${this.cStemming}FX\n` +
                 `  Arcs values:  ${this.nArcVal} = ${this.nChar} characters,  ${this.nAff} affixes,  ${this.nTag} tags\n` +
                 `  Dictionary: ${this.nEntries} entries,    ${this.nNode} nodes,   ${this.nArc} arcs\n` +
                 `  Address size: ${this.nBytesNodeAddress} bytes,  Arc size: ${this.nBytesArc} bytes\n`;
-    };
+    }
 
     isValidToken (sToken) {
         // checks if sToken is valid (if there is hyphens in sToken, sToken is split, each part is checked)
         if (this.isValid(sToken)) {
             return true;
@@ -93,11 +95,11 @@
                 return true;
             }
             return sToken.split("-").every(sWord  =>  this.isValid(sWord)); 
         }
         return false;
-    };
+    }
 
     isValid (sWord) {
         // checks if sWord is valid (different casing tested if the first letter is a capital)
         if (!sWord) {
             return null;
@@ -123,11 +125,11 @@
             } else {
                 return !!this.lookup(sWord.toLowerCase());
             }
         }
         return false;
-    };
+    }
 
     _convBytesToInteger (aBytes) {
         // Byte order = Big Endian (bigger first)
         let nVal = 0;
         let nWeight = (aBytes.length - 1) * 8;
@@ -134,11 +136,11 @@
         for (let n of aBytes) {
             nVal += n << nWeight;
             nWeight = nWeight - 8;
         }
         return nVal;
-    };
+    }
 
     lookup (sWord) {
         // returns true if sWord in dictionary (strict verification)
         let iAddr = 0;
         for (let c of sWord) {
@@ -149,11 +151,11 @@
             if (iAddr === null) {
                 return false;
             }
         }
         return Boolean(this._convBytesToInteger(this.byDic.slice(iAddr, iAddr+this.nBytesArc)) & this._finalNodeMask);
-    };
+    }
 
     getMorph (sWord) {
         // retrieves morphologies list, different casing allowed
         let l = this.morph(sWord);
         if (sWord[0].gl_isUpperCase()) {
@@ -161,15 +163,15 @@
             if (sWord.gl_isUpperCase() && sWord.length > 1) {
                 l = l.concat(this.morph(sWord.gl_toCapitalize()));
             }
         }
         return l;
-    };
+    }
 
     // morph (sWord) {
     //     is defined in constructor
-    // };
+    // }
     
     // VERSION 1
     _morph1 (sWord) {
         // returns morphologies of sWord
         let iAddr = 0;
@@ -205,11 +207,11 @@
                 iAddr = iEndArcAddr + this.nBytesNodeAddress;
             }
             return l;
         }
         return [];
-    };
+    }
 
     _stem1 (sWord) {
         // returns stems list of sWord
         let iAddr = 0;
         for (let c of sWord) {
@@ -235,11 +237,11 @@
                 iAddr = iEndArcAddr + this.nBytesNodeAddress;
             }
             return l;
         }
         return [];
-    };
+    }
 
     _lookupArcNode1 (nVal, iAddr) {
         // looks if nVal is an arc at the node at iAddr, if yes, returns address of next node else None
         while (true) {
             let iEndArcAddr = iAddr+this.nBytesArc;
@@ -255,39 +257,39 @@
                     return null;
                 }
                 iAddr = iEndArcAddr + this.nBytesNodeAddress;
             }
         }
-    };
+    }
 
     // VERSION 2
     _morph2 (sWord) {
         // to do
-    };
+    }
 
     _stem2 (sWord) {
         // to do
-    };
+    }
 
     _lookupArcNode2 (nVal, iAddr) {
         // to do
-    };
+    }
 
 
     // VERSION 3
     _morph3 (sWord) {
         // to do
-    };
+    }
 
     _stem3 (sWord) {
         // to do
-    };
+    }
 
     _lookupArcNode3 (nVal, iAddr) {
         // to do
-    };
+    }
 }
 
 
 if (typeof(exports) !== 'undefined') {
     exports.IBDAWG = IBDAWG;
 }

Index: gc_core/js/jsex_map.js
==================================================================
--- gc_core/js/jsex_map.js
+++ gc_core/js/jsex_map.js
@@ -6,42 +6,42 @@
         let oNewMap = new Map();
         for (let [key, val] of this.entries()) {
             oNewMap.set(key, val);
         }
         return oNewMap;
-    }
+    };
 
     Map.prototype.gl_get = function (key, defaultValue) {
         let res = this.get(key);
         if (res !== undefined) {
             return res;
         }
         return defaultValue;
-    }
+    };
 
     Map.prototype.gl_toString = function () {
         // Default .toString() gives nothing useful
         let sRes = "{ ";
         for (let [k, v] of this.entries()) {
             sRes += (typeof k === "string") ? '"' + k + '": ' : k.toString() + ": ";
             sRes += (typeof v === "string") ? '"' + v + '", ' : v.toString() + ", ";
         }
-        sRes = sRes.slice(0, -2) + " }"
+        sRes = sRes.slice(0, -2) + " }";
         return sRes;
-    }
+    };
 
     Map.prototype.gl_update = function (dDict) {
         for (let [k, v] of dDict.entries()) {
             this.set(k, v);
         }
-    }
+    };
 
     Map.prototype.gl_updateOnlyExistingKeys = function (dDict) {
         for (let [k, v] of dDict.entries()) {
             if (this.has(k)){
                 this.set(k, v);
             }
         }
-    }
+    };
 
     Map.prototype.grammalecte = true;
 }

Index: gc_core/js/jsex_regex.js
==================================================================
--- gc_core/js/jsex_regex.js
+++ gc_core/js/jsex_regex.js
@@ -40,23 +40,23 @@
                             // at the end of the pattern
                             m.start.push(this.lastIndex - m[i].length);
                             m.end.push(this.lastIndex);
                         } else if (codePos === "w") {
                             // word in the middle of the pattern
-                            iPos = m[0].search("[ ’,()«»“”]"+m[i]+"[ ,’()«»“”]") + 1 + m.index
+                            iPos = m[0].search("[ ’,()«»“”]"+m[i]+"[ ,’()«»“”]") + 1 + m.index;
                             m.start.push(iPos);
-                            m.end.push(iPos + m[i].length)
+                            m.end.push(iPos + m[i].length);
                         } else if (codePos === "*") {
                             // anywhere
                             iPos = m[0].indexOf(m[i]) + m.index;
                             m.start.push(iPos);
-                            m.end.push(iPos + m[i].length)
+                            m.end.push(iPos + m[i].length);
                         } else if (codePos === "**") {
                             // anywhere after previous group
                             iPos = m[0].indexOf(m[i], m.end[i-1]-m.index) + m.index;
                             m.start.push(iPos);
-                            m.end.push(iPos + m[i].length)
+                            m.end.push(iPos + m[i].length);
                         } else if (codePos.startsWith(">")) {
                             // >x:_
                             // todo: look in substring x
                             iPos = m[0].indexOf(m[i]) + m.index;
                             m.start.push(iPos);
@@ -81,9 +81,9 @@
             } else {
                 console.error(e);
             }
         }
         return m;
-    }
+    };
 
     RegExp.prototype.grammalecte = true;
 }

Index: gc_core/js/jsex_string.js
==================================================================
--- gc_core/js/jsex_string.js
+++ gc_core/js/jsex_string.js
@@ -13,45 +13,45 @@
         while ((iPos = this.indexOf(sSearch, iPos)) >= 0) {
             nOccur++;
             iPos += nStep;
         }
         return nOccur;
-    }
+    };
     String.prototype.gl_isDigit = function () {
         return (this.search(/^[0-9⁰¹²³⁴⁵⁶⁷⁸⁹]+$/) !== -1);
-    }
+    };
     String.prototype.gl_isLowerCase = function () {
         return (this.search(/^[a-zà-öø-ÿ0-9-]+$/) !== -1);
-    }
+    };
     String.prototype.gl_isUpperCase = function () {
         return (this.search(/^[A-ZÀ-ÖØ-ߌ0-9-]+$/) !== -1);
-    }
+    };
     String.prototype.gl_isTitle = function () {
         return (this.search(/^[A-ZÀ-ÖØ-ߌ][a-zà-öø-ÿ'’-]+$/) !== -1);
-    }
+    };
     String.prototype.gl_toCapitalize = function () {
         return this.slice(0,1).toUpperCase() + this.slice(1).toLowerCase();
-    }
+    };
     String.prototype.gl_expand = function (oMatch) {
         let sNew = this;
         for (let i = 0; i < oMatch.length ; i++) {
             let z = new RegExp("\\\\"+parseInt(i), "g");
             sNew = sNew.replace(z, oMatch[i]);
         }
         return sNew;
-    }
+    };
     String.prototype.gl_trimRight = function (sChars) {
         let z = new RegExp("["+sChars+"]+$");
         return this.replace(z, "");
-    }
+    };
     String.prototype.gl_trimLeft = function (sChars) {
         let z = new RegExp("^["+sChars+"]+");
         return this.replace(z, "");
-    }
+    };
     String.prototype.gl_trim = function (sChars) {
         let z1 = new RegExp("^["+sChars+"]+");
         let z2 = new RegExp("["+sChars+"]+$");
         return this.replace(z1, "").replace(z2, "");
-    }
+    };
 
     String.prototype.grammalecte = true;
 }

Index: gc_core/js/lang_core/gc_engine.js
==================================================================
--- gc_core/js/lang_core/gc_engine.js
+++ gc_core/js/lang_core/gc_engine.js
@@ -1,11 +1,28 @@
 // Grammar checker engine
 
+"use strict";
+
 ${string}
 ${regex}
 ${map}
 
+
+if (typeof(require) !== 'undefined') {
+    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 = helpers.echo;
+}
+else if (typeof(console) !== "undefined") {
+    var echo = function (o) { console.log(o); return true; };
+}
+else {
+    var echo = function () { return true; }
+}
 
 function capitalizeArray (aArray) {
     // can’t map on user defined function??
     let aNew = [];
     for (let i = 0; i < aArray.length; i = i + 1) {
@@ -12,407 +29,411 @@
         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
+
+
+var 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();
+                //helpers.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) {
+                                helpers.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 {
+                                    //helpers.echo(oEvalFunc[sFuncCond]);
+                                    bCondMemo = (!sFuncCond || oEvalFunc[sFuncCond](s, sx, m, dDA, sCountry, bCondMemo));
+                                    if (bCondMemo) {
+                                        switch (cActionType) {
+                                            case "-":
+                                                // grammar error
+                                                //helpers.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
+                                                //helpers.echo("-> text processor by " + sLineId + "\nzRegex: " + zRegex.source);
+                                                s = this._rewrite(s, sWhat, eAct[0], m, bUppercase);
+                                                bChange = true;
+                                                if (bDebug) {
+                                                    helpers.echo("~ " + s + "  -- " + m[eAct[0]] + "  # " + sLineId);
+                                                }
+                                                break;
+                                            case "=":
+                                                // disambiguation
+                                                //helpers.echo("-> disambiguation by " + sLineId + "\nzRegex: " + zRegex.source);
+                                                oEvalFunc[sWhat](s, m, dDA);
+                                                if (bDebug) {
+                                                    helpers.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:
+                                                helpers.echo("# error: unknown action at " + sLineId);
+                                        }
+                                    } else {
+                                        if (cActionType == ">") {
+                                            break;
+                                        }
+                                    }
+                                }
+                                catch (e) {
+                                    helpers.echo(s);
+                                    helpers.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.slice(0,1) === "=") {
+            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
+        let sMessage = "";
+        if (sMsg.slice(0,1) === "=") {
+            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);
+        }
+        //helpers.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 (let [sOption, lRuleGroup] of this._getRules(true)) {
+                for (let [,, sLineId, sRuleId,,] of lRuleGroup) {
+                    if (!sFilter || sRuleId.test(sFilter)) {
+                        yield [sOption, sLineId, sRuleId];
+                    }
+                }
+            }
+            for (let [sOption, lRuleGroup] of this._getRules(false)) {
+                for (let [,, 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", sPath="") {
+        try {
+            if (typeof(require) !== 'undefined') {
+                var ibdawg = require("resource://grammalecte/ibdawg.js");
+                _oDict = new ibdawg.IBDAWG("${dic_name}.json");
+            } else {
+                _oDict = new IBDAWG("${dic_name}.json", sPath);
+            }
+            _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);
 }
 
 function displayInfo (dDA, aWord) {
     // for debugging: info of word
     if (!aWord) {
-        echo("> nothing to find");
+        helpers.echo("> nothing to find");
         return true;
     }
     if (!_dAnalyses.has(aWord[1]) && !_storeMorphFromFSA(aWord[1])) {
-        echo("> not in FSA");
+        helpers.echo("> not in FSA");
         return true;
     }
     if (dDA.has(aWord[0])) {
-        echo("DA: " + dDA.get(aWord[0]));
+        helpers.echo("DA: " + dDA.get(aWord[0]));
     }
-    echo("FSA: " + _dAnalyses.get(aWord[1]));
+    helpers.echo("FSA: " + _dAnalyses.get(aWord[1]));
     return true;
 }
 
 function _storeMorphFromFSA (sWord) {
     // retrieves morphologies list from _oDict -> _dAnalyses
-    //echo("register: "+sWord + " " + _oDict.getMorph(sWord).toString())
+    //helpers.echo("register: "+sWord + " " + _oDict.getMorph(sWord).toString())
     _dAnalyses.set(sWord, _oDict.getMorph(sWord));
     return !!_dAnalyses.get(sWord);
 }
 
 function morph (dDA, aWord, sPattern, bStrict=true, bNoWord=false) {
     // analyse a tuple (position, word), return true if sPattern in morphologies (disambiguation on)
     if (!aWord) {
-        //echo("morph: noword, returns " + bNoWord);
+        //helpers.echo("morph: noword, returns " + bNoWord);
         return bNoWord;
     }
-    //echo("aWord: "+aWord.toString());
+    //helpers.echo("aWord: "+aWord.toString());
     if (!_dAnalyses.has(aWord[1]) && !_storeMorphFromFSA(aWord[1])) {
         return false;
     }
     let lMorph = dDA.has(aWord[0]) ? dDA.get(aWord[0]) : _dAnalyses.get(aWord[1]);
-    //echo("lMorph: "+lMorph.toString());
+    //helpers.echo("lMorph: "+lMorph.toString());
     if (lMorph.length === 0) {
         return false;
     }
-    //echo("***");
+    //helpers.echo("***");
     if (bStrict) {
         return lMorph.every(s  =>  (s.search(sPattern) !== -1));
     }
     return lMorph.some(s  =>  (s.search(sPattern) !== -1));
 }
 
 function morphex (dDA, aWord, sPattern, sNegPattern, bNoWord=false) {
     // analyse a tuple (position, word), returns true if not sNegPattern in word morphologies and sPattern in word morphologies (disambiguation on)
     if (!aWord) {
-        //echo("morph: noword, returns " + bNoWord);
+        //helpers.echo("morph: noword, returns " + bNoWord);
         return bNoWord;
     }
-    //echo("aWord: "+aWord.toString());
+    //helpers.echo("aWord: "+aWord.toString());
     if (!_dAnalyses.has(aWord[1]) && !_storeMorphFromFSA(aWord[1])) {
         return false;
     }
     let lMorph = dDA.has(aWord[0]) ? dDA.get(aWord[0]) : _dAnalyses.get(aWord[1]);
-    //echo("lMorph: "+lMorph.toString());
+    //helpers.echo("lMorph: "+lMorph.toString());
     if (lMorph.length === 0) {
         return false;
     }
-    //echo("***");
+    //helpers.echo("***");
     // check negative condition
     if (lMorph.some(s  =>  (s.search(sNegPattern) !== -1))) {
         return false;
     }
     // search sPattern
@@ -449,60 +470,56 @@
         return [];
     }
     if (!_dAnalyses.has(sWord) && !_storeMorphFromFSA(sWord)) {
         return [];
     }
-    return [ for (s of _dAnalyses.get(sWord))  s.slice(1, s.indexOf(" ")) ];
+    return _dAnalyses.get(sWord).map( s => s.slice(1, s.indexOf(" ")) );
 }
 
 
 //// functions to get text outside pattern scope
 
 // warning: check compile_rules.py to understand how it works
 
 function nextword (s, iStart, n) {
     // get the nth word of the input string or empty string
-    let z = new RegExp("^( +[a-zà-öA-Zø-ÿÀ-Ö0-9Ø-ßĀ-ʯfi-st%_-]+){" + (n-1).toString() + "} +([a-zà-öA-Zø-ÿÀ-Ö0-9Ø-ßĀ-ʯfi-st%_-]+)", "i");
+    let z = new RegExp("^(?: +[a-zà-öA-Zø-ÿÀ-Ö0-9Ø-ßĀ-ʯfi-st%_-]+){" + (n-1).toString() + "} +([a-zà-öA-Zø-ÿÀ-Ö0-9Ø-ßĀ-ʯfi-st%_-]+)", "ig");
     let m = z.exec(s.slice(iStart));
     if (!m) {
         return null;
     }
-    return [iStart + RegExp.lastIndex - m[2].length, m[2]];
+    return [iStart + z.lastIndex - m[1].length, m[1]];
 }
 
 function prevword (s, iEnd, n) {
     // get the (-)nth word of the input string or empty string
-    let z = new RegExp("([a-zà-öA-Zø-ÿÀ-Ö0-9Ø-ßĀ-ʯfi-st%_-]+) +([a-zà-öA-Zø-ÿÀ-Ö0-9Ø-ßĀ-ʯfi-st%_-]+ +){" + (n-1).toString() + "}$", "i");
+    let z = new RegExp("([a-zà-öA-Zø-ÿÀ-Ö0-9Ø-ßĀ-ʯfi-st%_-]+) +(?:[a-zà-öA-Zø-ÿÀ-Ö0-9Ø-ßĀ-ʯfi-st%_-]+ +){" + (n-1).toString() + "}$", "i");
     let m = z.exec(s.slice(0, iEnd));
     if (!m) {
         return null;
     }
     return [m.index, m[1]];
 }
 
-const _zNextWord = new RegExp ("^ +([a-zà-öA-Zø-ÿÀ-Ö0-9Ø-ßĀ-ʯfi-st_][a-zà-öA-Zø-ÿÀ-Ö0-9Ø-ßĀ-ʯfi-st_-]*)", "i");
-const _zPrevWord = new RegExp ("([a-zà-öA-Zø-ÿÀ-Ö0-9Ø-ßĀ-ʯfi-st_][a-zà-öA-Zø-ÿÀ-Ö0-9Ø-ßĀ-ʯfi-st_-]*) +$", "i");
-
 function nextword1 (s, iStart) {
     // get next word (optimization)
+    let _zNextWord = new RegExp ("^ +([a-zà-öA-Zø-ÿÀ-Ö0-9Ø-ßĀ-ʯfi-st_][a-zà-öA-Zø-ÿÀ-Ö0-9Ø-ßĀ-ʯfi-st_-]*)", "ig");
     let m = _zNextWord.exec(s.slice(iStart));
     if (!m) {
         return null;
     }
-    return [iStart + RegExp.lastIndex - m[1].length, m[1]];
+    return [iStart + _zNextWord.lastIndex - m[1].length, m[1]];
 }
+
+const _zPrevWord = new RegExp ("([a-zà-öA-Zø-ÿÀ-Ö0-9Ø-ßĀ-ʯfi-st_][a-zà-öA-Zø-ÿÀ-Ö0-9Ø-ßĀ-ʯfi-st_-]*) +$", "i");
 
 function prevword1 (s, iEnd) {
     // get previous word (optimization)
-    //echo("prev1, s:"+s);
-    //echo("prev1, s.slice(0, iEnd):"+s.slice(0, iEnd));
     let m = _zPrevWord.exec(s.slice(0, iEnd));
-    //echo("prev1, m:"+m);
     if (!m) {
         return null;
     }
-    //echo("prev1: " + m.index + " " + m[1]);
     return [m.index, m[1]];
 }
 
 function look (s, zPattern, zNegPattern=null) {
     // seek zPattern in s (before/after/fulltext), if antipattern zNegPattern not in s
@@ -549,16 +566,14 @@
         return true;
     }
     if (!_dAnalyses.has(sWord) && !_storeMorphFromFSA(sWord)) {
         return true;
     }
-    //echo("morph: "+_dAnalyses.get(sWord).toString());
     if (_dAnalyses.get(sWord).length === 1) {
         return true;
     }
-    let lSelect = [ for (sMorph of _dAnalyses.get(sWord))  if (sMorph.search(sPattern) !== -1)  sMorph ];
-    //echo("lSelect: "+lSelect.toString());
+    let lSelect = _dAnalyses.get(sWord).filter( sMorph => sMorph.search(sPattern) !== -1 );
     if (lSelect.length > 0) {
         if (lSelect.length != _dAnalyses.get(sWord).length) {
             dDA.set(nPos, lSelect);
         }
     } else if (lDefault) {
@@ -578,12 +593,11 @@
         return true;
     }
     if (_dAnalyses.get(sWord).length === 1) {
         return true;
     }
-    let lSelect = [ for (sMorph of _dAnalyses.get(sWord))  if (sMorph.search(sPattern) === -1)  sMorph ];
-    //echo("lSelect: "+lSelect.toString());
+    let lSelect = _dAnalyses.get(sWord).filter( sMorph => sMorph.search(sPattern) === -1 );
     if (lSelect.length > 0) {
         if (lSelect.length != _dAnalyses.get(sWord).length) {
             dDA.set(nPos, lSelect);
         }
     } else if (lDefault) {
@@ -594,10 +608,11 @@
 
 function define (dDA, nPos, lMorph) {
     dDA.set(nPos, lMorph);
     return true;
 }
+
 
 //////// GRAMMAR CHECKER PLUGINS
 
 ${pluginsJS}
 
@@ -605,20 +620,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};
+
+var 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};
+var 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);
-}
+var 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,26 +1,29 @@
 // JavaScript
 
 "use strict";
 
 
-const helpers = require("resource://grammalecte/helpers.js");
+if (typeof(require) !== 'undefined') {
+    var helpers = require("resource://grammalecte/helpers.js");
+}
 
 
 class TestGrammarChecking {
 
-    constructor (gce) {
+    constructor (gce, spfTests="") {
         this.gce = gce;
+        this.spfTests = spfTests;
         this._aRuleTested = new Set();
-    };
+    }
 
     * testParse (bDebug=false) {
         const t0 = Date.now();
-        const aData = JSON.parse(helpers.loadFile("resource://grammalecte/"+this.gce.lang+"/tests_data.json")).aData;
-        //const aData = require("resource://grammalecte/"+this.gce.lang+"/tests_data.js").aData;
-        let nInvalid = 0
-        let nTotal = 0
+        let sURL = (this.spfTests !== "") ? this.spfTests : "resource://grammalecte/"+this.gce.lang+"/tests_data.json";
+        const aData = JSON.parse(helpers.loadFile(sURL)).aData;
+        let nInvalid = 0;
+        let nTotal = 0;
         let sErrorText;
         let sSugg;
         let sExpectedErrors;
         let sTextToCheck;
         let sFoundErrors;
@@ -59,11 +62,11 @@
                               "\n# Line num: " + sLineNum +
                               "\n> to check: " + sTextToCheck +
                               "\n  expected: " + sExpectedErrors +
                               "\n  found:    " + sFoundErrors +
                               "\n  errors:   \n" + sListErr;
-                        nInvalid = nInvalid + 1
+                        nInvalid = nInvalid + 1;
                     }
                     nTotal = nTotal + 1;
                 }
                 i = i + 1;
                 if (i % 1000 === 0) {
@@ -76,11 +79,11 @@
             helpers.logerror(e);
         }
 
         if (bShowUntested) {
             i = 0;
-            for (let [sOpt, sLineId, sRuleId] of gce.listRules()) {
+            for (let [sOpt, sLineId, sRuleId] of this.gce.listRules()) {
                 if (!this._aRuleTested.has(sLineId) && !/^[0-9]+[sp]$|^[pd]_/.test(sRuleId)) {
                     sUntestedRules += sRuleId + ", ";
                     i += 1;
                 }
             }
@@ -90,11 +93,11 @@
         }
 
         const t1 = Date.now();
         yield "Tests parse finished in " + ((t1-t0)/1000).toString()
             + " s\nTotal errors: " + nInvalid.toString() + " / " + nTotal.toString();
-    };
+    }
 
     _getExpectedErrors (sLine) {
         try {
             let sRes = " ".repeat(sLine.length);
             let z = /\{\{.+?\}\}/g;
@@ -116,19 +119,19 @@
         }
         catch (e) {
             helpers.logerror(e);
         }
         return " ".repeat(sLine.length);
-    };
+    }
 
     _getFoundErrors (sLine, bDebug, sOption) {
         try {
             let aErrs = [];
             if (sOption) {
-                gce.setOption(sOption, true);
+                this.gce.setOption(sOption, true);
                 aErrs = this.gce.parse(sLine, "FR", bDebug);
-                gce.setOption(sOption, false);
+                this.gce.setOption(sOption, false);
             } else {
                 aErrs = this.gce.parse(sLine, "FR", bDebug);
             }
             let sRes = " ".repeat(sLine.length);
             let sListErr = "";
@@ -141,13 +144,13 @@
         }
         catch (e) {
             helpers.logerror(e);
         }
         return [" ".repeat(sLine.length), ""];
-    };
+    }
 
 }
 
 
 if (typeof(exports) !== 'undefined') {
     exports.TestGrammarChecking = TestGrammarChecking;
 }

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");
+}
+
+
+var 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'],
@@ -33,22 +37,22 @@
             [/^\d\d?[hm]\d\d\b/, 'HOUR'],
             [/^\d+(?:er|nd|e|de|ième|ème|eme)s?\b/, 'ORDINAL'],
             [/^-?\d+(?:[.,]\d+|)/, 'NUM'],
             [/^[a-zA-Zà-öÀ-Ö0-9ø-ÿØ-ßĀ-ʯfi-st]+(?:[’'`-][a-zA-Zà-öÀ-Ö0-9ø-ÿØ-ßĀ-ʯfi-st]+)*/, 'WORD']
         ]
-}
+};
 
 
 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;
         while (sText) {
@@ -72,11 +76,11 @@
                 }
             }
             i += nCut;
             sText = sText.slice(nCut);
         }
-    };
+    }
 
     getSpellingErrors (sText, oDict) {
         let aSpellErr = [];
         for (let oToken of this.genTokens(sText)) {
             if (oToken.sType === 'WORD' && !oDict.isValidToken(oToken.sValue)) {

Index: gc_core/py/lang_core/gc_engine.py
==================================================================
--- gc_core/py/lang_core/gc_engine.py
+++ gc_core/py/lang_core/gc_engine.py
@@ -472,19 +472,19 @@
 
 # warning: check compile_rules.py to understand how it works
 
 def nextword (s, iStart, n):
     "get the nth word of the input string or empty string"
-    m = re.match("( +[\\w%-]+){" + str(n-1) + "} +([\\w%-]+)", s[iStart:])
+    m = re.match("(?: +[\\w%-]+){" + str(n-1) + "} +([\\w%-]+)", s[iStart:])
     if not m:
         return None
-    return (iStart+m.start(2), m.group(2))
+    return (iStart+m.start(1), m.group(1))
 
 
 def prevword (s, iEnd, n):
     "get the (-)nth word of the input string or empty string"
-    m = re.search("([\\w%-]+) +([\\w%-]+ +){" + str(n-1) + "}$", s[:iEnd])
+    m = re.search("([\\w%-]+) +(?:[\\w%-]+ +){" + str(n-1) + "}$", s[:iEnd])
     if not m:
         return None
     return (m.start(1), m.group(1))
 
 

Index: gc_lang/fr/config.ini
==================================================================
--- gc_lang/fr/config.ini
+++ gc_lang/fr/config.ini
@@ -29,13 +29,14 @@
 
 # Firefox
 fx_identifier = French-GC@grammalecte.net
 fx_name = Grammalecte [fr]
 
-fx_standard_path = C:\Program Files\Mozilla Firefox\firefox.exe
-fx_beta_path = C:\Program Files\Mozilla Firefox Beta\firefox.exe
-fx_nightly_path = C:\Program Files (x86)\Nightly\firefox.exe
+win_fx_dev_path = C:\Program Files\Firefox Developer Edition\firefox.exe
+win_fx_nightly_path = C:\Program Files (x86)\Nightly\firefox.exe
+linux_fx_dev_path = /usr/bin/firefox
+linux_fx_nightly_path = /usr/bin/firefox
 
 
 # Thunderbird
 tb_identifier = French-GC-TB@grammalecte.net
 tb_name = Grammalecte [fr]

Index: gc_lang/fr/modules-js/conj.js
==================================================================
--- gc_lang/fr/modules-js/conj.js
+++ gc_lang/fr/modules-js/conj.js
@@ -4,187 +4,175 @@
 "use strict";
 
 ${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 + " ##";
-    }
-}
+var conj = {
+    _lVtyp: [],
+    _lTags: [],
+    _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 (sMorph.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 {
 
     constructor (sVerb) {
@@ -191,107 +179,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";
@@ -310,11 +298,11 @@
         }
         if (sInfo === "") {
             sInfo = "# erreur - code : " + this._sRawInfo;
         }
         return sGroup + " · " + sInfo;
-    };
+    }
 
     infinitif (bPro, bNeg, bTpsCo, bInt, bFem) {
         let sInfi;
         if (bTpsCo) {
             sInfi = (bPro) ? "être" : this.sVerbAux;
@@ -323,11 +311,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;
         }
@@ -336,30 +324,30 @@
         }
         if (bInt) {
             sInfi += " … ?";
         }
         return sInfi;
-    };
+    }
 
     participePasse (sWho) {
         return this.dConj.get(":PQ").get(sWho);
-    };
+    }
 
     participePresent (bPro, bNeg, bTpsCo, bInt, bFem) {
         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") : conj.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;
@@ -373,11 +361,11 @@
         }
         if (bInt) {
             sPartPre += " … ?";
         }
         return sPartPre;
-    };
+    }
 
     conjugue (sTemps, sWho, bPro, bNeg, bTpsCo, bInt, bFem) {
         if (!this.dConj.get(sTemps).get(sWho)) {
             return "";
         }
@@ -384,30 +372,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) : conj.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) {
@@ -424,11 +412,11 @@
         }
         if (bInt) {
             sConj += " … ?";
         }
         return sConj;
-    };
+    }
 
     _getPronom (sWho, bFem) {
         if (sWho == ":3s") {
             if (this._sRawInfo[5] == "r") {
                 return "on";
@@ -436,45 +424,45 @@
                 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) : conj.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;
-    };
+    }
 
     _seekPpas (bPro, bFem, bPlur) {
         if (!bPro && this.sVerbAux == "avoir") {
             return this.dConj.get(":PQ").get(":Q1");
         }
@@ -486,19 +474,53 @@
         }
         return (this.dConj.get(":PQ").get(":Q4")) ? this.dConj.get(":PQ").get(":Q4") : this.dConj.get(":PQ").get(":Q1");
     }
 }
 
+
+// Initialization
+if (typeof(browser) !== 'undefined') {
+    // WebExtension (but not in Worker)
+    conj.init(helpers.loadFile(browser.extension.getURL("grammalecte/fr/conj_data.json")));
+} else if (typeof(require) !== 'undefined') {
+    // Add-on SDK and Thunderbird
+    let helpers = require("resource://grammalecte/helpers.js");
+    conj.init(helpers.loadFile("resource://grammalecte/fr/conj_data.json"));
+} else if (typeof(self) !== 'undefined' && typeof(self.port) !== 'undefined' && typeof(self.port.on) !== "undefined") {
+    // used within Firefox content script (conjugation panel).
+    // can’t load JSON from here, so we do it in ui.js and send it here.
+    self.port.on("provideConjData", function (sJSONData) {
+        conj.init(sJSONData);
+    });    
+} else {
+    console.log("Module conj non initialisé");
+}
+
 
 if (typeof(exports) !== 'undefined') {
-    // Used for Grammalecte library.
-    // In content scripts, these variable are directly reachable
+    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,64 +56,64 @@
     // 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;
 }
 
 function isVeryAmbiguousAndWrong (sWord1, sWord2, sReqMorphNA, sReqMorphConj, bLastHopeCond) {
     //// use it if sWord1 can be also a verb; word2 is assumed to be true via isAmbiguousNAV
     // 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)
+    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;
 }
 
 function checkAgreement (sWord1, sWord2) {
     // 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)
+    let a2 = _dAnalyses.gl_get(sWord2, null);
     if (!a2 || a2.length === 0) {
         return true;
     }
     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) {
@@ -15,12 +17,12 @@
         if (tTags) {
             // we get the tense
             let aTense = new Set();
             for (let sMorph of _dAnalyses.gl_get(sFlex, [])) {
                 let m;
-                let zVerb = new RegExp (sStem+" .*?(:(?:Y|I[pqsf]|S[pq]|K))", "g");
-                while (m = zVerb.exec(sMorph)) {
+                let zVerb = new RegExp (">"+sStem+" .*?(:(?:Y|I[pqsf]|S[pq]|K))", "g");
+                while ((m = zVerb.exec(sMorph)) !== null) {
                     // stem must be used in regex to prevent confusion between different verbs (e.g. sauras has 2 stems: savoir and saurer)
                     if (m) {
                         if (m[1] === ":Y") {
                             aTense.add(":Ip");
                             aTense.add(":Iq");
@@ -139,12 +141,11 @@
     }
     return "";
 }
 
 function suggVerbInfi (sFlex) {
-    //return stem(sFlex).join("|");
-    return [ for (sStem of stem(sFlex)) if (conj.isVerb(sStem)) sStem ].join("|");
+    return stem(sFlex).filter(sStem => conj.isVerb(sStem)).join("|");
 }
 
 
 const _dQuiEst = new Map ([
     ["je", ":1s"], ["j’", ":1s"], ["j’en", ":1s"], ["j’y", ":1s"],
@@ -195,11 +196,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 +262,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 +298,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 +339,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 +373,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') {
+    var helpers = require("resource://grammalecte/helpers.js");
+}
 
 const _dTAGS = new Map ([
     [':G', "[mot grammatical]"],
     [':N', " nom,"],
     [':A', " adjectif,"],
@@ -197,11 +197,11 @@
     constructor (oDict) {
         this.oDict = oDict;
         this._zElidedPrefix = new RegExp ("^([dljmtsncç]|quoiqu|lorsqu|jusqu|puisqu|qu)['’](.+)", "i");
         this._zCompoundWord = new RegExp ("([a-zA-Zà-ö0-9À-Öø-ÿØ-ßĀ-ʯ]+)-((?:les?|la)-(?:moi|toi|lui|[nv]ous|leur)|t-(?:il|elle|on)|y|en|[mts][’'](?:y|en)|les?|l[aà]|[mt]oi|leur|lui|je|tu|ils?|elles?|on|[nv]ous)$", "i");
         this._zTag = new RegExp ("[:;/][a-zA-Zà-ö0-9À-Öø-ÿØ-ßĀ-ʯ*][^:;/]*", "g");
-    };
+    }
 
     getInfoForToken (oToken) {
         // Token: .sType, .sValue, .nStart, .nEnd
         // return a list [type, token_string, values]
         let m = null;
@@ -224,17 +224,23 @@
                     if (oToken.sValue.gl_count("-") > 4) {
                         return { sType: "COMPLEX", sValue: oToken.sValue, aLabel: ["élément complexe indéterminé"] };
                     }
                     else if (this.oDict.isValidToken(oToken.sValue)) {
                         let lMorph = this.oDict.getMorph(oToken.sValue);
-                        let aElem = [ for (s of lMorph) if (s.includes(":")) this._formatTags(s) ];
+                        let aElem = [];
+                        for (let s of lMorph){
+                            if (s.includes(":"))  aElem.push( this._formatTags(s) );
+                        }
                         return { sType: oToken.sType, sValue: oToken.sValue, aLabel: aElem};
                     }
                     else if (m = this._zCompoundWord.exec(oToken.sValue)) {
                         // mots composés
                         let lMorph = this.oDict.getMorph(m[1]);
-                        let aElem = [ for (s of lMorph) if (s.includes(":")) this._formatTags(s) ];
+                        let aElem = [];
+                        for (let s of lMorph){
+                            if (s.includes(":"))  aElem.push( this._formatTags(s) );
+                        }
                         aElem.push("-" + m[2] + ": " + this._formatSuffix(m[2].toLowerCase()));
                         return { sType: oToken.sType, sValue: oToken.sValue, aLabel: aElem };
                     }
                     else {
                         return { sType: "UNKNOWN", sValue: oToken.sValue, aLabel: ["inconnu du dictionnaire"] };
@@ -244,11 +250,11 @@
         }
         catch (e) {
             helpers.logerror(e);
         }
         return null;
-    };
+    }
 
     _formatTags (sTags) {
         let sRes = "";
         sTags = sTags.replace(/V([0-3][ea]?)[itpqnmr_eaxz]+/, "V$1");
         let m;
@@ -265,11 +271,11 @@
             sRes = "#Erreur. Étiquette inconnue : [" + sTags + "]";
             helpers.echo(sRes);
             return sRes;
         }
         return sRes.gl_trimRight(",");
-    };
+    }
 
     _formatSuffix (s) {
         if (s.startsWith("t-")) {
             return "“t” euphonique +" + _dAD.get(s.slice(2));
         }
@@ -279,12 +285,12 @@
         if (s.endsWith("ous")) {
             s += '2';
         }
         let nPos = s.indexOf("-");
         return _dAD.get(s.slice(0, nPos)) + " +" + _dAD.get(s.slice(nPos+1));
-    };
+    }
 }
 
 
 if (typeof(exports) !== 'undefined') {
     exports.Lexicographe = Lexicographe;
 }

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,126 @@
 // Grammalecte
 
 "use strict";
 
-const helpers = require("resource://grammalecte/helpers.js");
-const echo = helpers.echo;
-
-const oData = JSON.parse(helpers.loadFile("resource://grammalecte/fr/mfsp_data.json"));
-
-// list of affix codes
-const _lTagMiscPlur = oData.lTagMiscPlur;
-const _lTagMasForm = oData.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);
-
-// 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 + " ##";
-    }
+
+if (typeof(require) !== 'undefined') {
+    var helpers = require("resource://grammalecte/helpers.js");
+}
+
+
+var mfsp = {
+    // list of affix codes
+    _lTagMiscPlur: [],
+    _lTagMasForm: [],
+    // dictionary of words with uncommon plurals (-x, -ux, english, latin and italian plurals) and tags to generate them
+    _dMiscPlur: new Map(),
+    // dictionary of feminine forms and tags to generate masculine forms (singular and plural)
+    _dMasForm: new Map(),
+
+    init: function (sJSONData) {
+        try {
+            let _oData = JSON.parse(sJSONData);
+            this._lTagMiscPlur = _oData.lTagMiscPlur;
+            this._lTagMasForm = _oData.lTagMasForm;
+            this._dMiscPlur = helpers.objectToMap(_oData.dMiscPlur);
+            this._dMasForm = helpers.objectToMap(_oData.dMasForm);
+        }
+        catch (e) {
+            console.error(e);
+        }
+    },
+
+    isFemForm: function (sWord) {
+        // returns True if sWord exists in this._dMasForm
+        return this._dMasForm.has(sWord);
+    },
+
+    getMasForm: function (sWord, bPlur) {
+        // returns masculine form with feminine form
+        if (this._dMasForm.has(sWord)) {
+            let aMasForm = [];
+            for (let sTag of this._whatSuffixCode(sWord, bPlur)){
+                aMasForm.push( this._modifyStringWithSuffixCode(sWord, sTag) );
+            }
+            return aMasForm;
+        }
+        return [];
+    },
+
+    hasMiscPlural: function (sWord) {
+        // returns True if sWord exists in dMiscPlur
+        return this._dMiscPlur.has(sWord);
+    },
+
+    getMiscPlural: function (sWord) {
+        // returns plural form with singular form
+        if (this._dMiscPlur.has(sWord)) {
+            let aMiscPlural = [];
+            for (let sTag of this._lTagMiscPlur[this._dMiscPlur.get(sWord)].split("|")){
+                aMiscPlural.push( this._modifyStringWithSuffixCode(sWord, sTag) );
+            }
+            return aMiscPlural;
+        }
+        return [];
+    },
+
+    _whatSuffixCode: function (sWord, bPlur) {
+        // necessary only for dMasFW
+        let sSfx = this._lTagMasForm[this._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 + " ##";
+        }
+    }
+};
+
+
+// Initialization
+if (typeof(browser) !== 'undefined') {
+    // WebExtension
+    mfsp.init(helpers.loadFile(browser.extension.getURL("grammalecte/fr/mfsp_data.json")));
+} else if (typeof(require) !== 'undefined') {
+    // Add-on SDK and Thunderbird
+    mfsp.init(helpers.loadFile("resource://grammalecte/fr/mfsp_data.json"));
+} else {
+    console.log("Module mfsp non initialisé");
 }
 
 
 if (typeof(exports) !== 'undefined') {
-    exports.isFemForm = isFemForm;
-    exports.getMasForm = getMasForm;
-    exports.hasMiscPlural = hasMiscPlural;
-    exports.getMiscPlural = getMiscPlural;
+    exports._lTagMiscPlur = mfsp._lTagMiscPlur;
+    exports._lTagMasForm = mfsp._lTagMasForm;
+    exports._dMiscPlur = mfsp._dMiscPlur;
+    exports._dMasForm = mfsp._dMasForm;
+    exports.init = mfsp.init;
+    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,103 @@
 // 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(require) !== 'undefined') {
+    var helpers = require("resource://grammalecte/helpers.js");
+}
+
+
+var phonet = {
+    _dWord: new Map(),
+    _lSet: [],
+    _dMorph: new Map(),
+
+    init: function (sJSONData) {
+        try {
+            let _oData = JSON.parse(sJSONData);
+            this._dWord = helpers.objectToMap(_oData.dWord);
+            this._lSet = _oData.lSet;
+            this._dMorph = helpers.objectToMap(_oData.dMorph);
+        }
+        catch (e) {
+            console.error(e);
+        }
+    },
+
+    hasSimil: function (sWord, sPattern=null) {
+        // return True if there is list of words phonetically similar to sWord
+        if (!sWord) {
+            return false;
+        }
+        if (this._dWord.has(sWord)) {
+            if (sPattern) {
+                return this.getSimil(sWord).some(sSimil => this._dMorph.gl_get(sSimil, []).some(sMorph => sMorph.search(sPattern) >= 0));
+            }
+            return true;
+        }
+        if (sWord.slice(0,1).gl_isUpperCase()) {
+            sWord = sWord.toLowerCase();
+            if (this._dWord.has(sWord)) {
+                if (sPattern) {
+                    return this.getSimil(sWord).some(sSimil => this._dMorph.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 (this._dWord.has(sWord)) {
+            return this._lSet[this._dWord.get(sWord)];
+        }
+        if (sWord.slice(0,1).gl_isUpperCase()) {
+            sWord = sWord.toLowerCase();
+            if (this._dWord.has(sWord)) {
+                return this._lSet[this._dWord.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 this._dMorph.gl_get(sSimil, [])) {
+                if (sMorph.search(sPattern) >= 0) {
+                    aSelect.add(sSimil);
+                }
+            }
+        }
+        return aSelect;
+    }
+};
+
+
+// Initialization
+if (typeof(browser) !== 'undefined') {
+    // WebExtension
+    phonet.init(helpers.loadFile(browser.extension.getURL("grammalecte/fr/phonet_data.json")));
+} else if (typeof(require) !== 'undefined') {
+    // Add-on SDK and Thunderbird
+    phonet.init(helpers.loadFile("resource://grammalecte/fr/phonet_data.json"));
+} else {
+    console.log("Module phonet non initialisé");
 }
 
 
 if (typeof(exports) !== 'undefined') {
-    exports.hasSimil = hasSimil;
-    exports.getSimil = getSimil;
-    exports.selectSimil = selectSimil;
+    exports._dWord = phonet._dWord;
+    exports._lSet = phonet._lSet;
+    exports._dMorph = phonet._dMorph;
+    exports.init = phonet.init;
+    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,38 +249,38 @@
     ["ma_word", true],
     ["ma_1letter_lowercase", false],
     ["ma_1letter_uppercase", false]
 ]);
 
-const dOptions = dDefaultOptions.gl_shallowCopy();
+const dTFOptions = dTFDefaultOptions.gl_shallowCopy();
 
 
 class TextFormatter {
 
     constructor () {
         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);
                 }
             }
         }
         return sText;
-    };
+    }
 
     getDefaultOptions () {
-        return dDefaultOptions;
+        return dTFDefaultOptions;
     }
 }
 
 
 if (typeof(exports) !== 'undefined') {
     exports.TextFormatter = TextFormatter;
     exports.oReplTable = oReplTable;
 }

Index: gc_lang/fr/modules/gce_suggestions.py
==================================================================
--- gc_lang/fr/modules/gce_suggestions.py
+++ gc_lang/fr/modules/gce_suggestions.py
@@ -13,11 +13,11 @@
         tTags = conj._getTags(sStem)
         if tTags:
             # we get the tense
             aTense = set()
             for sMorph in _dAnalyses.get(sFlex, []): # we don’t check if word exists in _dAnalyses, for it is assumed it has been done before
-                for m in re.finditer(sStem+" .*?(:(?:Y|I[pqsf]|S[pq]|K|P))", sMorph):
+                for m in re.finditer(">"+sStem+" .*?(:(?:Y|I[pqsf]|S[pq]|K|P))", sMorph):
                     # stem must be used in regex to prevent confusion between different verbs (e.g. sauras has 2 stems: savoir and saurer)
                     if m:
                         if m.group(1) == ":Y":
                             aTense.add(":Ip")
                             aTense.add(":Iq")

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();

ADDED   gc_lang/fr/webext/background.js
Index: gc_lang/fr/webext/background.js
==================================================================
--- /dev/null
+++ gc_lang/fr/webext/background.js
@@ -0,0 +1,121 @@
+// Background 
+
+"use strict";
+
+function showError (e) {
+    console.error(e.fileName + "\n" + e.name + "\nline: " + e.lineNumber + "\n" + e.message);
+}
+
+/*
+    Worker (separate thread to avoid freezing Firefox)
+*/
+let xGCEWorker = new Worker("gce_worker.js");
+
+xGCEWorker.onmessage = function (e) {
+    // https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent
+    try {
+        switch (e.data[0]) {
+            case "grammar_errors":
+                console.log("GRAMMAR ERRORS");
+                console.log(e.data[1].aGrammErr);
+                browser.runtime.sendMessage({sCommand: "grammar_errors", aGrammErr: e.data[1].aGrammErr});
+                break;
+            case "spelling_and_grammar_errors":
+                console.log("SPELLING AND GRAMMAR ERRORS");
+                console.log(e.data[1].aSpellErr);
+                console.log(e.data[1].aGrammErr);
+                break;
+            case "text_to_test_result":
+                browser.runtime.sendMessage({sCommand: "text_to_test_result", sResult: e.data[1]});
+                break;
+            case "fulltests_result":
+                console.log("TESTS RESULTS");
+                browser.runtime.sendMessage({sCommand: "fulltests_result", sResult: e.data[1]});
+                break;
+            case "options":
+                console.log("OPTIONS");
+                console.log(e.data[1]);
+                break;
+            case "tokens":
+                console.log("TOKENS");
+                console.log(e.data[1]);
+                browser.browserAction.setPopup({popup: "panel/main.html"});
+                browser.runtime.sendMessage({sCommand: "show_tokens", oResult: e.data[1]});
+                break;
+            case "error":
+                console.log("ERROR");
+                console.log(e.data[1]);
+                break;
+            default:
+                console.log("Unknown command: " + e.data[0]);
+        }
+    }
+    catch (e) {
+        showError(e);
+    }
+};
+
+xGCEWorker.postMessage(["init", {sExtensionPath: browser.extension.getURL("."), sOptions: "", sContext: "Firefox"}]);
+
+
+/*
+    Messages from the extension (not the Worker)
+*/
+function handleMessage (oRequest, xSender, sendResponse) {
+    //console.log(xSender);
+    switch(oRequest.sCommand) {
+        case "parse":
+            xGCEWorker.postMessage(["parse", {sText: oRequest.sText, sCountry: "FR", bDebug: false, bContext: false}]);
+            break;
+        case "parse_and_spellcheck":
+            xGCEWorker.postMessage(["parseAndSpellcheck", {sText: oRequest.sText, sCountry: "FR", bDebug: false, bContext: false}]);
+            break;
+        case "get_list_of_tokens":
+            xGCEWorker.postMessage(["getListOfTokens", {sText: oRequest.sText}]);
+            break;
+        case "text_to_test":
+            xGCEWorker.postMessage(["textToTest", {sText: oRequest.sText, sCountry: "FR", bDebug: false, bContext: false}]);
+            break;
+        case "fulltests":
+            xGCEWorker.postMessage(["fullTests"]);
+            break;
+    }
+    //sendResponse({response: "response from background script"});
+}
+
+browser.runtime.onMessage.addListener(handleMessage);
+
+
+/*
+    Context Menu
+*/
+browser.contextMenus.create({
+    id: "grammar_checking",
+    title: "Correction grammaticale",
+    contexts: ["selection", "editable", "page"]
+});
+
+browser.contextMenus.create({
+    id: "lexicographer",
+    title: "Lexicographe",
+    contexts: ["selection", "editable", "page"]
+});
+
+browser.contextMenus.onClicked.addListener(function (xInfo, xTab) {
+    // xInfo = https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/contextMenus/OnClickData
+    // xTab = https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/tabs/Tab
+    console.log(xInfo);
+    console.log(xTab);
+    console.log("Item " + xInfo.menuItemId + " clicked in tab " + xTab.id);
+    console.log("editable: " + xInfo.editable + " · selected: " + xInfo.selectionText);
+    // confusing: no way to get the node where we click?!
+    switch (xInfo.menuItemId) {
+        case "grammar_checking":
+            break;
+        case "lexicographer":
+            if (xInfo.selectionText) {
+                xGCEWorker.postMessage(["getListOfTokens", {sText: xInfo.selectionText}]);
+            }
+            break;
+    }
+});

Index: gc_lang/fr/webext/gce_worker.js
==================================================================
--- gc_lang/fr/webext/gce_worker.js
+++ gc_lang/fr/webext/gce_worker.js
@@ -1,139 +1,229 @@
+/*
+    WORKER:
+    https://developer.mozilla.org/en-US/docs/Web/API/Worker
+    https://developer.mozilla.org/en-US/docs/Web/API/DedicatedWorkerGlobalScope
+
+
+    JavaScript sucks.
+    No module available in WebExtension at the moment! :(
+    No require, no import/export.
+
+    In Worker, we have importScripts() which imports everything in this scope.
+
+    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…
+
+    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
+*/
+
+"use strict";
+
+
+console.log("GC Engine Worker [start]");
+//console.log(self);
+
+importScripts("grammalecte/helpers.js");
+importScripts("grammalecte/str_transform.js");
+importScripts("grammalecte/ibdawg.js");
+importScripts("grammalecte/text.js");
+importScripts("grammalecte/tokenizer.js");
+importScripts("grammalecte/fr/conj.js");
+importScripts("grammalecte/fr/mfsp.js");
+importScripts("grammalecte/fr/phonet.js");
+importScripts("grammalecte/fr/cregex.js");
+importScripts("grammalecte/fr/gc_options.js");
+importScripts("grammalecte/fr/gc_rules.js");
+importScripts("grammalecte/fr/gc_engine.js");
+importScripts("grammalecte/fr/lexicographe.js");
+importScripts("grammalecte/tests.js");
+/*
+    Warning.
+    Initialization can’t be completed at startup of the worker,
+    for we need the path of the extension to load data stored in JSON files.
+    This path is retrieved in background.js and passed with the event “init”.
+*/
 
 
 /*
-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;
-
-let oTokenizer = null;
+    Message Event Object
+    https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent
+*/
+onmessage = function (e) {
+    let oParam = e.data[1];
+    switch (e.data[0]) {
+        case "init":
+            loadGrammarChecker(oParam.sExtensionPath, oParam.sOptions, oParam.sContext);
+            break;
+        case "parse":
+            parse(oParam.sText, oParam.sCountry, oParam.bDebug, oParam.bContext);
+            break;
+        case "parseAndSpellcheck":
+            parseAndSpellcheck(oParam.sText, oParam.sCountry, oParam.bDebug, oParam.bContext);
+            break;
+        case "getOptions":
+            getOptions();
+            break;
+        case "getDefaultOptions":
+            getDefaultOptions();
+            break;
+        case "setOptions":
+            setOptions(oParam.sOptions);
+            break;
+        case "setOption":
+            setOption(oParam.sOptName, oParam.bValue);
+            break;
+        case "resetOptions":
+            resetOptions();
+            break;
+        case "textToTest":
+            textToTest(oParam.sText, oParam.sCountry, oParam.bDebug, oParam.bContext);
+            break;
+        case "fullTests":
+            fullTests();
+            break;
+        case "getListOfTokens":
+            getListOfTokens(oParam.sText);
+            break;
+        default:
+            console.log("Unknown command: " + e.data[0]);
+    }
+}
+
+
 let oDict = null;
-let oLxg = null;
-
-function loadGrammarChecker (sGCOptions="", sContext="JavaScript") {
-    if (gce === null) {
-        try {
-            gce = require("resource://grammalecte/fr/gc_engine.js");
-            helpers = require("resource://grammalecte/helpers.js");
-            text = require("resource://grammalecte/text.js");
-            tkz = require("resource://grammalecte/tokenizer.js");
-            lxg = require("resource://grammalecte/fr/lexicographe.js");
-            oTokenizer = new tkz.Tokenizer("fr");
-            helpers.setLogOutput(console.log);
-            gce.load(sContext);
-            oDict = gce.getDictionary();
-            oLxg = new lxg.Lexicographe(oDict);
-            if (sGCOptions !== "") {
-                gce.setOptions(helpers.objectToMap(JSON.parse(sGCOptions)));
-            }
-            // we always retrieve options from the gce, for setOptions filters obsolete options
-            return gce.getOptions()._toString();
-        }
-        catch (e) {
-            console.log("# Error: " + e.fileName + "\n" + e.name + "\nline: " + e.lineNumber + "\n" + e.message);
-        }
-    }
-}
-
-function parse (sText, sLang, bDebug, bContext) {
-    let aGrammErr = gce.parse(sText, sLang, bDebug, bContext);
-    return JSON.stringify(aGrammErr);
-}
-
-function parseAndSpellcheck (sText, sLang, bDebug, bContext) {
-    let aGrammErr = gce.parse(sText, sLang, bDebug, bContext);
-    let aSpellErr = oTokenizer.getSpellingErrors(sText, oDict);
-    return JSON.stringify({ aGrammErr: aGrammErr, aSpellErr: aSpellErr });
+let oTokenizer = null;
+let oLxg = null;
+let oTest = null;
+
+
+function loadGrammarChecker (sExtensionPath, sGCOptions="", sContext="JavaScript") {
+    try {
+        console.log("Loading… Extension path: " + sExtensionPath);
+        conj.init(helpers.loadFile(sExtensionPath + "/grammalecte/fr/conj_data.json"));
+        phonet.init(helpers.loadFile(sExtensionPath + "/grammalecte/fr/phonet_data.json"));
+        mfsp.init(helpers.loadFile(sExtensionPath + "/grammalecte/fr/mfsp_data.json"));
+        console.log("Modules have been initialized…");
+        gc_engine.load(sContext, sExtensionPath+"grammalecte/_dictionaries");
+        oDict = gc_engine.getDictionary();
+        oTest = new TestGrammarChecking(gc_engine, sExtensionPath+"/grammalecte/fr/tests_data.json");
+        oLxg = new Lexicographe(oDict);
+        if (sGCOptions !== "") {
+            gc_engine.setOptions(helpers.objectToMap(JSON.parse(sGCOptions)));
+        }
+        oTokenizer = new Tokenizer("fr");
+        tests();
+        // we always retrieve options from the gc_engine, for setOptions filters obsolete options
+        postMessage(["options", gc_engine.getOptions().gl_toString()]);
+    }
+    catch (e) {
+        console.error(e.fileName + "\n" + e.name + "\nline: " + e.lineNumber + "\n" + e.message);
+        postMessage(["error", e.message]);
+    }
+}
+
+function parse (sText, sCountry, bDebug, bContext) {
+    let aGrammErr = gc_engine.parse(sText, sCountry, bDebug, bContext);
+    postMessage(["grammar_errors", {aGrammErr: aGrammErr}]);
+}
+
+function parseAndSpellcheck (sText, sCountry, bDebug, bContext) {
+    let aGrammErr = gc_engine.parse(sText, sCountry, bDebug, bContext);
+    let aSpellErr = oTokenizer.getSpellingErrors(sText, oDict);
+    postMessage(["spelling_and_grammar_errors", {aGrammErr: aGrammErr, aSpellErr: aSpellErr}]);
 }
 
 function getOptions () {
-    return gce.getOptions()._toString();
+    postMessage(["options", gc_engine.getOptions().gl_toString()]);
 }
 
 function getDefaultOptions () {
-    return gce.getDefaultOptions()._toString();
+    postMessage(["options", gc_engine.getDefaultOptions().gl_toString()]);
 }
 
 function setOptions (sGCOptions) {
-    gce.setOptions(helpers.objectToMap(JSON.parse(sGCOptions)));
-    return gce.getOptions()._toString();
+    gc_engine.setOptions(helpers.objectToMap(JSON.parse(sGCOptions)));
+    postMessage(["options", gc_engine.getOptions().gl_toString()]);
 }
 
 function setOption (sOptName, bValue) {
-    gce.setOptions(new Map([ [sOptName, bValue] ]));
-    return gce.getOptions()._toString();
+    gc_engine.setOptions(new Map([ [sOptName, bValue] ]));
+    postMessage(["options", gc_engine.getOptions().gl_toString()]);
 }
 
 function resetOptions () {
-    gce.resetOptions();
-    return gce.getOptions()._toString();
-}
-
-function fullTests (sGCOptions="") {
-    if (!gce || !oDict) {
-        return "# Error: grammar checker or dictionary not loaded."
-    }
-    let dMemoOptions = gce.getOptions();
-    if (sGCOptions) {
-        gce.setOptions(helpers.objectToMap(JSON.parse(sGCOptions)));
-    }
-    let tests = require("resource://grammalecte/tests.js");
-    let oTest = new tests.TestGrammarChecking(gce);
-    let sAllRes = "";
-    for (let sRes of oTest.testParse()) {
-        dump(sRes+"\n");
-        sAllRes += sRes+"\n";
-    }
-    gce.setOptions(dMemoOptions);
-    return sAllRes;
+    gc_engine.resetOptions();
+    postMessage(["options", gc_engine.getOptions().gl_toString()]);
+}
+
+function tests () {
+    console.log(conj.getConj("devenir", ":E", ":2s"));
+    console.log(mfsp.getMasForm("emmerdeuse", true));
+    console.log(mfsp.getMasForm("pointilleuse", false));
+    console.log(phonet.getSimil("est"));
+    let aRes = gc_engine.parse("Je suit...");
+    for (let oErr of aRes) {
+        console.log(text.getReadableError(oErr));
+    }
+}
+
+function textToTest (sText, sCountry, bDebug, bContext) {
+    if (!gc_engine || !oDict) {
+        postMessage(["error", "# Error: grammar checker or dictionary not loaded."]);
+        return;
+    }
+    let aGrammErr = gc_engine.parse(sText, sCountry, bDebug, bContext);
+    let sMsg = "";
+    for (let oErr of aGrammErr) {
+        sMsg += text.getReadableError(oErr) + "\n";
+    }
+    postMessage(["text_to_test_result", sMsg]);
+}
+
+function fullTests (sGCOptions='{"nbsp":true, "esp":true, "unit":true, "num":true}') {
+    if (!gc_engine || !oDict) {
+        postMessage(["error", "# Error: grammar checker or dictionary not loaded."]);
+        return;
+    }
+    let dMemoOptions = gc_engine.getOptions();
+    if (sGCOptions) {
+        gc_engine.setOptions(helpers.objectToMap(JSON.parse(sGCOptions)));
+    }
+    let sMsg = "";
+    for (let sRes of oTest.testParse()) {
+        sMsg += sRes + "\n";
+        console.log(sRes);
+    }
+    gc_engine.setOptions(dMemoOptions);
+    postMessage(["fulltests_result", sMsg]);
 }
 
 
 // Lexicographer
 
-function getListOfElements (sText) {
+function getListOfTokens (sText) {
     try {
         let aElem = [];
         let aRes = null;
         for (let oToken of oTokenizer.genTokens(sText)) {
             aRes = oLxg.getInfoForToken(oToken);
             if (aRes) {
                 aElem.push(aRes);
             }
         }
-        return JSON.stringify(aElem);
+        postMessage(["tokens", aElem]);
     }
     catch (e) {
         helpers.logerror(e);
-    }
-    return JSON.stringify([]);
-}
-
-
-function handleMessage (oRequest, xSender, sendResponse) {
-  console.log(`[background] received: ${oRequest.content}`);
-  sendResponse({response: "response from background script"});
-}
-
-browser.runtime.onMessage.addListener(handleMessage);
-
-
+        postMessage(["error", e.message]);
+    }
+}

Index: gc_lang/fr/webext/manifest.json
==================================================================
--- gc_lang/fr/webext/manifest.json
+++ gc_lang/fr/webext/manifest.json
@@ -11,11 +11,10 @@
     }
   },
 
   "author": "Olivier R.",
   "homepage_url": "https://grammalecte.net",
-  "offline_enabled": true,
 
   "description": "Correcteur grammatical pour le français.",
 
   "icons": { "16": "img/logo-16.png",
              "32": "img/logo-32.png",
@@ -28,16 +27,21 @@
     "default_popup": "panel/main.html",
     "default_title": "Grammalecte [fr]",
     "browser_style": false
   },
   "background": {
-    "scripts": ["require.js", "grammalecte/helpers.js", "gce_worker.js"]
+    "scripts": [
+      "background.js"
+    ]
   },
   "web_accessible_resources": [
-    "beasts/frog.jpg",
-    "beasts/turtle.jpg",
-    "beasts/snake.jpg"
+    "grammalecte/_dictionaries/French.json",
+    "grammalecte/fr/conj_data.json",
+    "grammalecte/fr/mfsp_data.json",
+    "grammalecte/fr/phonet_data.json",
+    "grammalecte/fr/tests_data.json"
   ],
   "permissions": [
-    "activeTab"
+    "activeTab",
+    "contextMenus"
   ]
 }

Index: gc_lang/fr/webext/panel/main.css
==================================================================
--- gc_lang/fr/webext/panel/main.css
+++ gc_lang/fr/webext/panel/main.css
@@ -67,51 +67,62 @@
 /* Main classes */
 
 html {
     box-sizing: border-box;
     width: 530px;
-    height: 880px;
+    height: 500px;
     font-family: "Trebuchet MS", "Liberation Sans", sans-serif;
 }
 body {
     width: 530px;
-    height: 880px;
+    height: 500px;
 }
+/* 
+    Maximal height of a panel in WebExtention seems to be 500px.
+    When going over this limit, a scrollbar appears which destructs the
+    horizontal balance of elements.
+    --> vertical scrolling is done with overflow in #page.
+        #page must have the same height than body.
+*/
 
 #main {
-    display: flex;
-    flex-direction: row;
-    flex-wrap: nowrap;
-    align-items: stretch;
     background-color: hsl(210, 0%, 100%);
     min-height: 100%;
 }
 
-#left {
-    width: 54px;
+#menu {
+    position: fixed;
+    left: 5px;
+    width: 50px;
+    border-left: solid 2px hsl(210, 0%, 70%);
+    border-bottom: solid 2px hsl(210, 0%, 70%);
+    border-right: solid 2px hsl(210, 0%, 70%);
+    border-radius: 0 0 5px 5px;
     background-color: hsl(210, 10%, 96%);
-    border-right: solid 1px hsl(210, 0%, 70%);
     color: hsl(210, 10%, 96%);
 }
 #logo {
-  padding: 10px;
-}
-#left li {
-  padding: 10px 5px;
-  border-bottom: 1px solid hsl(210, 10%, 90%);
-  text-align: center;
-  cursor: pointer;
-  color: hsl(210, 10%, 50%);
-  list-style-type: none;
-}
-#left li:hover {
-  background-color: hsl(210, 10%, 92%);
-
-}
+    padding: 10px;
+}
+#menu li {
+    padding: 10px 5px;
+    border-bottom: 1px solid hsl(210, 10%, 90%);
+    text-align: center;
+    cursor: pointer;
+    color: hsl(210, 10%, 50%);
+    list-style-type: none;
+}
+#menu li:hover {
+    background-color: hsl(210, 10%, 92%);
+}
+
 
 #page {
+    padding-left: 60px;
     background-color: hsl(210, 0%, 100%);
+    height: 500px;
+    overflow: auto;
 }
 #page h1 {
     margin: 0 0 10px 0;
     color: hsl(210, 70%, 70%);
     font: bold 30px 'Yanone Kaffeesatz', "Liberation Sans Narrow", sans-serif;
@@ -119,33 +130,33 @@
 #page p {
     margin: 10px 0 5px 0;
 }
 
 #home_page {
-  display: block;
-  padding: 20px;
+    display: block;
+    padding: 20px;
 }
 
 #tf_page {
-  display: none;
-  padding: 20px;
+    display: none;
+    padding: 20px;
 }
 #gc_page {
-  display: none;
-  padding: 20px 20px 30px 20px;
+    display: none;
+    padding: 20px 20px 30px 20px;
 }
 #gc_options_page {
-  display: none;
-  padding: 20px;
+    display: none;
+    padding: 20px;
 }
 #sc_options_page {
-  display: none;
-  padding: 20px;
+    display: none;
+    padding: 20px;
 }
 #lxg_page {
-  display: none;
-  padding: 20px;
+    display: none;
+    padding: 20px;
 }
 
 
 /*
   Conjugueur page
@@ -243,21 +254,20 @@
 #test_page {
   display: none;
 }
 #test_cmd {
     padding: 15px;
-    background-color: hsl(0, 0%, 92%);
-    border-bottom: 1px solid hsl(0, 0%, 86%);
+    border-bottom: 1px solid hsl(0, 0%, 90%);
 }
 #test_cmd textarea {
     width: 100%;
     border: 2px solid hsl(0, 0%, 89%);
     border-radius: 3px;
     resize: vertical;
 }
 
-#test_results {
+#tests_result {
     padding: 15px;
     background-color: hsl(0, 0%, 96%);
 }
 
 #test_page .button {

Index: gc_lang/fr/webext/panel/main.html
==================================================================
--- gc_lang/fr/webext/panel/main.html
+++ gc_lang/fr/webext/panel/main.html
@@ -7,12 +7,12 @@
   </head>
 
   <body>
     <div id="main">
 
-      <header id="left">
-        <nav id="menu">
+      <header id="menu">
+        <nav>
           <header id="logo">
             <img src="../img/logo-32.png">
           </header>
           <ul>
             <li class="select" data-page="home_page"><i class="fa fa-home icon"></i> 1.</li>
@@ -25,16 +25,18 @@
             <li class="select" data-page="test_page"><i class="fa fa-keyboard-o icon"></i> TST</li>
           </ul>
         </nav>
       </header> <!-- #left -->
 
+      <!--
       <div id="movewindow">
         <div id="resize_h_bigger" class="arrow" style="border-radius: 2px 0 0 0">↓</div>
         <div id="resize_h_smaller" class="arrow">↑</div>
         <div id="resize_w_bigger" class="arrow">←</div>
         <div id="resize_w_smaller" class="arrow" style="border-radius: 0 0 0 2px">→</div>
       </div>
+      -->
 
       <div id="page">
 
         <section id="home_page" class="page">
           <h1>GRAMMALECTE</h1>
@@ -59,22 +61,21 @@
         </section>
 
         <section id="test_page" class="page">
           <div id="test_cmd">
             <h1>TESTS</h1>
-            <textarea id="text" rows="10"></textarea>
-            <div id="testall" class="button blue">Tests complets</div> <div id="parse" class="button green fright">Analyser</div>
+            <textarea id="text_to_test" rows="10"></textarea>
+            <div id="fulltests" class="button blue">Tests complets</div> <div id="text_to_test" class="button green fright">Analyser</div>
           </div>
-          <div id="test_results">
-          </div>
+          <pre id="tests_result"></pre>
         </section>
 
         <section id="conj_page" class="page">
           <h1>CONJUGUEUR</h1>
           <p class="right" style="margin: 10px 30px 0 0">
             <input type="text" id="verb" name="verb" maxlength="40" value="" placeholder="entrez un verbe" autofocus />
-            <a id="conjugate" href="#" onclick="return false;">Conjuguer</a>
+            <a id="conjugate" href="#">Conjuguer</a>
           <p>
 
           <div class="clearer"></div>
 
           <p id="conj_smallnote" hidden>Ce verbe n’a pas encore été vérifié. C’est pourquoi les options “pronominal” et “temps composés” sont désactivées.</p>

Index: gc_lang/fr/webext/panel/main.js
==================================================================
--- gc_lang/fr/webext/panel/main.js
+++ gc_lang/fr/webext/panel/main.js
@@ -1,70 +1,186 @@
+// Main panel
+
+"use strict";
 
+/*
+    Common functions
+*/
 function showError (e) {
-  console.error(e.fileName + "\n" + e.name + "\nline: " + e.lineNumber + "\n" + e.message);
-}
-
-function beastNameToURL(beastName) {
-  switch (beastName) {
-    case "Frog":
-      return browser.extension.getURL("beasts/frog.jpg");
-    case "Snake":
-      return browser.extension.getURL("beasts/snake.jpg");
-    case "Turtle":
-      return browser.extension.getURL("beasts/turtle.jpg");
-  }
-}
-
-window.addEventListener(
-  "click",
-  function (xEvent) {
-    let xElem = xEvent.target;
-    if (xElem.id) {
-      if (xElem.id) {
-
-      }
-    } else if (xElem.className === "select") {
-      showPage(xElem.dataset.page);
-    } else if (xElem.tagName === "A") {
-      openURL(xElem.getAttribute("href"));
-    }
-  },
-  false
-);
+    console.error(e.fileName + "\n" + e.name + "\nline: " + e.lineNumber + "\n" + e.message);
+}
 
 function showPage (sPageName) {
-  try {
-    // hide them all
-    for (let xNodePage of document.getElementsByClassName("page")) {
-      xNodePage.style.display = "None";
-    }
-    // show the one
-    document.getElementById(sPageName).style.display = "block";
-    sendMessage("Mon message");
-    // specific modifications
-    if (sPageName === "conj_page") {
-      document.body.style.width = "600px";
-      document.documentElement.style.width = "600px";
-      document.getElementById("movewindow").style.display = "none";
-    } else {
-      document.body.style.width = "530px";
-      document.documentElement.style.width = "530px";
-      document.getElementById("movewindow").style.display = "block";
-    }
-  }
-  catch (e) {
-    showError(e);
-  }
-}
-
-function handleResponse(message) {
-  console.log(`[Panel] received: ${message.response}`);
-}
-
-function handleError(error) {
-  console.log(`[Panel] Error: ${error}`);
-}
-
-function sendMessage (sMessage) {
-  let sending = browser.runtime.sendMessage({content: sMessage});
-  sending.then(handleResponse, handleError);  
+    try {
+        // hide them all
+        for (let xNodePage of document.getElementsByClassName("page")) {
+            xNodePage.style.display = "none";
+        }
+        // show the selected one
+        document.getElementById(sPageName).style.display = "block";
+        // specific modifications
+        if (sPageName === "conj_page") {
+            document.body.style.width = "600px";
+            document.documentElement.style.width = "600px";
+            //document.getElementById("movewindow").style.display = "none";
+        } else {
+            document.body.style.width = "530px";
+            document.documentElement.style.width = "530px";
+            //document.getElementById("movewindow").style.display = "block";
+        }
+    }
+    catch (e) {
+        showError(e);
+    }
+}
+
+function startWaitIcon () {
+    document.getElementById("waiticon").hidden = false;
+}
+
+function stopWaitIcon () {
+    document.getElementById("waiticon").hidden = true;
+}
+
+
+/*
+    Events
+*/
+window.addEventListener(
+    "click",
+    function (xEvent) {
+        let xElem = xEvent.target;
+        if (xElem.id) {
+            switch (xElem.id) {
+                case "text_to_test":
+                    browser.runtime.sendMessage({sCommand: "text_to_test", sText: document.getElementById("text_to_test").value});
+                    break;
+                case "fulltests":
+                    document.getElementById("tests_result").textContent = "Veuillez patienter…";
+                    browser.runtime.sendMessage({sCommand: "fulltests"});
+                    break;
+            }
+        } else if (xElem.className === "select") {
+            showPage(xElem.dataset.page);
+        } else if (xElem.tagName === "A") {
+            openURL(xElem.getAttribute("href"));
+        }
+    },
+    false
+);
+
+
+/* 
+    Message sender
+    and response handling
+*/
+function handleResponse (oResponse) {
+    console.log(`[Panel] received:`);
+    console.log(oResponse);
+}
+
+function handleError (error) {
+    console.log(`[Panel] Error:`);
+    console.log(error);
+}
+
+function sendMessageAndWaitResponse (oData) {
+    let xPromise = browser.runtime.sendMessage(oData);
+    xPromise.then(handleResponse, handleError);  
+}
+
+
+/*
+    Messages received
+*/
+function handleMessage (oMessage, xSender, sendResponse) {
+    console.log(xSender);
+    switch(oMessage.sCommand) {
+        case "show_tokens":
+            console.log("show tokens");
+            addParagraphOfTokens(oMessage.oResult);
+            break;
+        case "text_to_test_result":
+            showTestResult(oMessage.sResult);
+            break;
+        case "fulltests_result":
+            showTestResult(oMessage.sResult);
+            break;
+    }
+    sendResponse({sCommand: "none", sResult: "done"});
+}
+
+browser.runtime.onMessage.addListener(handleMessage);
+
+
+/*
+
+    DEDICATED FUNCTIONS 
+
+*/
+
+
+/*
+    Test page
+*/
+function showTestResult (sText) {
+    document.getElementById("tests_result").textContent = sText;
+}
+
+
+/*
+    Lexicographer page
+*/
+
+function addSeparator (sText) {
+    if (document.getElementById("tokens_list").textContent !== "") {
+        let xElem = document.createElement("p");
+        xElem.className = "separator";
+        xElem.textContent = sText;
+        document.getElementById("tokens_list").appendChild(xElem);
+    }
+}
+
+function addMessage (sClass, sText) {
+    let xNode = document.createElement("p");
+    xNode.className = sClass;
+    xNode.textContent = sText;
+    document.getElementById("tokens_list").appendChild(xNode);
+}
+
+function addParagraphOfTokens (lElem) {
+    try {
+        let xNodeDiv = document.createElement("div");
+        xNodeDiv.className = "paragraph";
+        for (let oToken of lElem) {
+            xNodeDiv.appendChild(createTokenNode(oToken));
+        }
+        document.getElementById("tokens_list").appendChild(xNodeDiv);
+    }
+    catch (e) {
+        showError(e);
+    }
+}
+
+function createTokenNode (oToken) {
+    let xTokenNode = document.createElement("div");
+    xTokenNode.className = "token " + oToken.sType;
+    let xTokenValue = document.createElement("b");
+    xTokenValue.className = oToken.sType;
+    xTokenValue.textContent = oToken.sValue;
+    xTokenNode.appendChild(xTokenValue);
+    let xSep = document.createElement("s");
+    xSep.textContent = " : ";
+    xTokenNode.appendChild(xSep);
+    if (oToken.aLabel.length === 1) {
+        xTokenNode.appendChild(document.createTextNode(oToken.aLabel[0]));
+    } else {
+        let xTokenList = document.createElement("ul");
+        for (let sLabel of oToken.aLabel) {
+            let xTokenLine = document.createElement("li");
+            xTokenLine.textContent = sLabel;
+            xTokenList.appendChild(xTokenLine);
+        }
+        xTokenNode.appendChild(xTokenList);
+    }
+    return xTokenNode;
 }

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;";

Index: gc_lang/fr/xpi/gce_worker.js
==================================================================
--- gc_lang/fr/xpi/gce_worker.js
+++ gc_lang/fr/xpi/gce_worker.js
@@ -77,17 +77,17 @@
             worker.log("# Error: " + e.fileName + "\n" + e.name + "\nline: " + e.lineNumber + "\n" + e.message);
         }
     }
 }
 
-function parse (sText, sLang, bDebug, bContext) {
-    let aGrammErr = gce.parse(sText, sLang, bDebug, bContext);
+function parse (sText, sCountry, bDebug, bContext) {
+    let aGrammErr = gce.parse(sText, sCountry, bDebug, bContext);
     return JSON.stringify(aGrammErr);
 }
 
-function parseAndSpellcheck (sText, sLang, bDebug, bContext) {
-    let aGrammErr = gce.parse(sText, sLang, bDebug, bContext);
+function parseAndSpellcheck (sText, sCountry, bDebug, bContext) {
+    let aGrammErr = gce.parse(sText, sCountry, bDebug, bContext);
     let aSpellErr = oTokenizer.getSpellingErrors(sText, oDict);
     return JSON.stringify({ aGrammErr: aGrammErr, aSpellErr: aSpellErr });
 }
 
 function getOptions () {

Index: make.py
==================================================================
--- make.py
+++ make.py
@@ -11,10 +11,11 @@
 import datetime
 import argparse
 import importlib
 import unittest
 import json
+import platform
 
 from distutils import dir_util, file_util
 
 import dialog_bundled
 import compile_rules
@@ -348,15 +349,17 @@
                         tests.perf(sVersion, hDst)
 
             # Firefox
             if xArgs.firefox:
                 with helpers.cd("_build/xpi/"+sLang):
-                    os.system("jpm run -b nightly")
+                    spfFirefox = dVars['win_fx_dev_path']  if platform.system() == "Windows"  else dVars['linux_fx_dev_path']
+                    os.system('jpm run -b "' + spfFirefox + '"')
 
             if xArgs.web_ext:
                 with helpers.cd("_build/webext/"+sLang):
-                    os.system(r'web-ext run --firefox="' + dVars['fx_beta_path'] + '" --browser-console')            
+                    spfFirefox = dVars['win_fx_nightly_path']  if platform.system() == "Windows"  else dVars['linux_fx_nightly_path']
+                    os.system(r'web-ext run --firefox="' + spfFirefox + '" --browser-console')            
 
             # Thunderbird
             if xArgs.thunderbird:
                 os.system("thunderbird -jsconsole -P debug")
         else: