Index: gc_core/js/helpers.js
==================================================================
--- gc_core/js/helpers.js
+++ gc_core/js/helpers.js
@@ -1,98 +1,101 @@
 
 // HELPERS
+/*jslint esversion: 6*/
+/*global console,require,exports,XMLHttpRequest*/
 
 "use strict";
 
 // 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.overrideMimeType('text/json');
+            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,16 @@
 //// IBDAWG
+/*jslint esversion: 6*/
+/*global console,require,exports*/
 
 "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 +18,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 +43,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 +76,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 +97,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 +127,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 +138,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 +153,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 +165,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 +209,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 +239,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 +259,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
@@ -1,47 +1,48 @@
 
 // Map
+/*jslint esversion: 6*/
 
 if (Map.prototype.grammalecte === undefined) {
     Map.prototype.gl_shallowCopy = function () {
         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
@@ -1,7 +1,8 @@
 
 // regex
+/*jslint esversion: 6*/
 
 if (RegExp.prototype.grammalecte === undefined) {
     RegExp.prototype.gl_exec2 = function (sText, aGroupsPos, aNegLookBefore=null) {
         let m;
         while ((m = this.exec(sText)) !== null) {
@@ -40,23 +41,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 +82,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
@@ -1,7 +1,8 @@
 
 // String
+/*jslint esversion: 6*/
 
 if (String.prototype.grammalecte === undefined) {
     String.prototype.gl_count = function (sSearch, bOverlapping) {
         // http://jsperf.com/string-ocurrence-split-vs-match/8
         if (sSearch.length <= 0) {
@@ -13,45 +14,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,30 @@
 // Grammar checker engine
+/*jslint esversion: 6*/
+/*global console,require,exports*/
+
+"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 +31,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 +472,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 +568,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 +595,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 +610,11 @@
 
 function define (dDA, nPos, lMorph) {
     dDA.set(nPos, lMorph);
     return true;
 }
+
 
 //////// GRAMMAR CHECKER PLUGINS
 
 ${pluginsJS}
 
@@ -605,20 +622,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,33 @@
 // Options for Grammalecte
+/*jslint esversion: 6*/
+/*global exports*/
 
 ${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
@@ -1,15 +1,20 @@
 // Grammar checker rules
+/*jslint esversion: 6*/
+/*global exports*/
+
 "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,33 @@
 //// 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ûùüú"]
-]);
+/*jslint esversion: 6*/
 
 // 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,31 @@
 // JavaScript
+/*jslint esversion: 6*/
+/*global console,require,exports*/
 
 "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 +64,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 +81,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 +95,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 +121,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 +146,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,71 @@
 // JavaScript
+/*jslint esversion: 6*/
+/*global require,exports*/
 
 "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(require) !== '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\n", "\n").replace("\r", "\n");
+        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,19 @@
 // JavaScript
 // Very simple tokenizer
+/*jslint esversion: 6*/
+/*global require,exports*/
 
 "use strict";
 
-const helpers = require("resource://grammalecte/helpers.js");
+
+if (typeof(require) !== 'undefined') {
+    var helpers = require("resource://grammalecte/helpers.js");
+}
+
 
-const aPatterns = {
+const aTkzPatterns = {
     // All regexps must start with ^.
     "default":
         [
             [/^[   \t]+/, 'SPACE'],
             [/^[,.;:!?…«»“”‘’"(){}\[\]/·–—]+/, 'SEPARATOR'],
@@ -33,22 +39,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 +78,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_core/py/text.py
==================================================================
--- gc_core/py/text.py
+++ gc_core/py/text.py
@@ -5,10 +5,11 @@
 
 
 def getParagraph (sText):
     "generator: returns paragraphs of text"
     iStart = 0
+    sText = sText.replace("\r\n", "\n").replace("\r", "\n")
     iEnd = sText.find("\n", iStart)
     while iEnd != -1:
         yield sText[iStart:iEnd]
         iStart = iEnd + 1
         iEnd = sText.find("\n", iStart)

Index: gc_lang/fr/config.ini
==================================================================
--- gc_lang/fr/config.ini
+++ gc_lang/fr/config.ini
@@ -3,11 +3,11 @@
 lang_name = French
 locales = fr_FR fr_BE fr_CA fr_CH fr_LU fr_MC fr_BF fr_CI fr_SN fr_ML fr_NE fr_TG fr_BJ
 country_default = FR
 name = Grammalecte
 implname = grammalecte
-version = 0.5.18
+version = 0.6
 author = Olivier R.
 provider = Dicollecte
 link = http://grammalecte.net
 description = Correcteur grammatical pour le français.
 extras = README_fr.txt
@@ -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
@@ -1,190 +1,186 @@
 // Grammalecte - Conjugueur
 // License: GPL 3
+/*jslint esversion: 6*/
+/*global console,require,exports,self,browser*/
 
 "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 + " ##";
-    }
-}
+if (typeof(require) !== 'undefined') {
+    var helpers = require("resource://grammalecte/helpers.js");
+}
+
+var conj = {
+    _lVtyp: [],
+    _lTags: [],
+    _dPatternConj: {},
+    _dVerb: {},
+
+    bInit: false,
+    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;
+            this.bInit = true;
+        }
+        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 +187,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 +306,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 +319,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 +332,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 +369,11 @@
         }
         if (bInt) {
             sPartPre += " … ?";
         }
         return sPartPre;
-    };
+    }
 
     conjugue (sTemps, sWho, bPro, bNeg, bTpsCo, bInt, bFem) {
         if (!this.dConj.get(sTemps).get(sWho)) {
             return "";
         }
@@ -384,30 +380,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 +420,11 @@
         }
         if (bInt) {
             sConj += " … ?";
         }
         return sConj;
-    };
+    }
 
     _getPronom (sWho, bFem) {
         if (sWho == ":3s") {
             if (this._sRawInfo[5] == "r") {
                 return "on";
@@ -436,45 +432,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 +482,54 @@
         }
         return (this.dConj.get(":PQ").get(":Q4")) ? this.dConj.get(":PQ").get(":Q4") : this.dConj.get(":PQ").get(":Q1");
     }
 }
 
+
+// Initialization
+if (!conj.bInit && typeof(browser) !== 'undefined') {
+    // WebExtension (but not in Worker)
+    conj.init(helpers.loadFile(browser.extension.getURL("grammalecte/fr/conj_data.json")));
+} else if (!conj.bInit && typeof(require) !== 'undefined') {
+    // Add-on SDK and Thunderbird
+    conj.init(helpers.loadFile("resource://grammalecte/fr/conj_data.json"));
+} else if ( !conj.bInit && typeof(self) !== 'undefined' && typeof(self.port) !== 'undefined' && typeof(self.port.on) !== "undefined") {
+    // used within Firefox content script (conjugation panel).
+    // can’t load JSON from here, so we do it in ui.js and send it here.
+    self.port.on("provideConjData", function (sJSONData) {
+        conj.init(sJSONData);
+    });    
+} else if (conj.bInit){
+    console.log("Module conj déjà initialisé");
+} else {
+    console.log("Module conj non initialisé");
+}
+
 
 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,349 @@
 //// 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));
-}
+/*jslint esversion: 6*/
+
+
+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
@@ -1,6 +1,7 @@
 //// GRAMMAR CHECKING ENGINE PLUGIN: Parsing functions for French language
+/*jslint esversion: 6*/
 
 function rewriteSubject (s1, s2) {
     // s1 is supposed to be prn/patr/npr (M[12P])
     if (s2 == "lui") {
         return "ils";
@@ -20,11 +21,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 +33,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 +57,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_date_verif.js
==================================================================
--- gc_lang/fr/modules-js/gce_date_verif.js
+++ gc_lang/fr/modules-js/gce_date_verif.js
@@ -1,6 +1,7 @@
 //// GRAMMAR CHECKING ENGINE PLUGIN
+/*jslint esversion: 6*/
 
 // Check date validity
 
 // WARNING: when creating a Date, month must be between 0 and 11
 

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,14 @@
 //// GRAMMAR CHECKING ENGINE PLUGIN: Suggestion mechanisms
+/*jslint esversion: 6*/
+/*global require*/
 
-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(require) !== 'undefined') {
+    var conj = require("resource://grammalecte/fr/conj.js");
+    var mfsp = require("resource://grammalecte/fr/mfsp.js");
+    var phonet = require("resource://grammalecte/fr/phonet.js");
+}
 
 
 //// verbs
 
 function suggVerb (sFlex, sWho, funcSugg2=null) {
@@ -15,12 +19,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 +143,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 +198,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 +264,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 +300,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 +341,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 +375,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
@@ -1,17 +1,19 @@
 // Grammalecte - Lexicographe
 // License: MPL 2
+/*jslint esversion: 6*/
+/*global require,exports*/
 
 "use strict";
 
 ${string}
 ${map}
 
 
-const helpers = require("resource://grammalecte/helpers.js");
-const tkz = require("resource://grammalecte/tokenizer.js");
-
+if (typeof(require) !== 'undefined') {
+    var helpers = require("resource://grammalecte/helpers.js");
+}
 
 const _dTAGS = new Map ([
     [':G', "[mot grammatical]"],
     [':N', " nom,"],
     [':A', " adjectif,"],
@@ -197,11 +199,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 +226,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 +252,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 +273,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 +287,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,132 @@
 // Grammalecte
+/*jslint esversion: 6*/
+/*global console,require,exports,browser*/
 
 "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(),
+
+    bInit: false,
+    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);
+            this.bInit = true;
+        }
+        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 (!mfsp.bInit && typeof(browser) !== 'undefined') {
+    // WebExtension
+    mfsp.init(helpers.loadFile(browser.extension.getURL("grammalecte/fr/mfsp_data.json")));
+} else if (!mfsp.bInit && typeof(require) !== 'undefined') {
+    // Add-on SDK and Thunderbird
+    mfsp.init(helpers.loadFile("resource://grammalecte/fr/mfsp_data.json"));
+} else if (mfsp.bInit){
+    console.log("Module mfsp déjà initialisé");
+} 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,108 @@
 // 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;
+/*jslint esversion: 6*/
+
+if (typeof(require) !== 'undefined') {
+    var helpers = require("resource://grammalecte/helpers.js");
+}
+
+
+var phonet = {
+    _dWord: new Map(),
+    _lSet: [],
+    _dMorph: new Map(),
+
+    bInit: false,
+    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);
+            this.bInit = true;
+        }
+        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 (!phonet.bInit && typeof(browser) !== 'undefined') {
+    // WebExtension
+    phonet.init(helpers.loadFile(browser.extension.getURL("grammalecte/fr/phonet_data.json")));
+} else if (!phonet.bInit && typeof(require) !== 'undefined') {
+    // Add-on SDK and Thunderbird
+    phonet.init(helpers.loadFile("resource://grammalecte/fr/phonet_data.json"));
+} else if (phonet.bInit){
+    console.log("Module phonet déjà initialisé");
+} 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
@@ -1,6 +1,8 @@
 // Grammalecte - text formatter
+/*jslint esversion: 6*/
+/*global exports*/
 
 "use strict";
 
 ${map}
 
@@ -196,11 +198,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 +251,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,225 @@
+// 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 {
+        let {sActionDone, result, dInfo} = e.data;
+        switch (sActionDone) {
+            case "init":
+                console.log("INIT DONE");
+                break;
+            case "parse":
+            case "parseAndSpellcheck":
+            case "parseAndSpellcheck1":
+            case "getListOfTokens":
+                console.log("Action done: " + sActionDone);
+                if (typeof(dInfo.iReturnPort) === "number") {
+                    let xPort = aConnx[dInfo.iReturnPort];
+                    xPort.postMessage(e.data);
+                } else {
+                    console.log("[background] don’t know where to send results");
+                    console.log(e.data);
+                }
+                break;
+            case "textToTest":
+                console.log("TEXT TO TEXT RESULTS");
+                browser.runtime.sendMessage({sCommand: "text_to_test_result", sResult: result});
+                break;
+            case "fullTests":
+                console.log("FULL TESTS RESULTS");
+                browser.runtime.sendMessage({sCommand: "fulltests_result", sResult: result});
+                break;
+            case "getOptions":
+            case "getDefaultOptions":
+            case "setOptions":
+            case "setOption":
+                console.log("OPTIONS");
+                break;
+            default:
+                console.log("Unknown command: " + sActionDone);
+                console.log(result);
+        }
+    }
+    catch (e) {
+        showError(e);
+    }
+};
+
+
+xGCEWorker.postMessage({sCommand: "init", dParam: {sExtensionPath: browser.extension.getURL("."), sOptions: "", sContext: "Firefox"}, dInfo: {}});
+
+
+/*
+    Ports from content-scripts
+*/
+
+let aConnx = [];
+
+
+/*
+    Messages from the extension (not the Worker)
+*/
+function handleMessage (oRequest, xSender, sendResponse) {
+    //console.log(xSender);
+    console.log("[background] received:");
+    console.log(oRequest);
+    switch (oRequest.sCommand) {
+        case "parse":
+        case "parseAndSpellcheck":
+        case "parseAndSpellcheck1":
+        case "getListOfTokens":
+        case "textToTest":
+        case "getOptions":
+        case "getDefaultOptions":
+        case "setOptions":
+        case "setOption":
+        case "fullTests":
+            xGCEWorker.postMessage(oRequest);
+            break;
+        default:
+            console.log("[background] Unknown command: " + oRequest.sCommand);
+    }
+    //sendResponse({response: "response from background script"});
+}
+
+browser.runtime.onMessage.addListener(handleMessage);
+
+
+function handleConnexion (p) {
+    var xPort = p;
+    let iPortId = aConnx.length; // identifier for the port: each port can be found at aConnx[iPortId]
+    aConnx.push(xPort);
+    console.log("Port: " + p.name + ", id: " + iPortId);
+    xPort.onMessage.addListener(function (oRequest) {
+        console.log("[background] message via connexion:");
+        console.log(oRequest);
+        switch (oRequest.sCommand) {
+            case "getCurrentTabId":
+                xPort.postMessage({sActionDone: "getCurrentTabId", result: "getCurrentTabId()", dInfo: null, bError: false});
+                break;
+            case "parse":
+            case "parseAndSpellcheck":
+            case "parseAndSpellcheck1":
+            case "getListOfTokens":
+                oRequest.dInfo.iReturnPort = iPortId; // we pass the id of the return port to receive answer
+                console.log(oRequest);
+                xGCEWorker.postMessage(oRequest);
+                break;
+            default:
+                console.log("[background] Unknown command: " + oRequest.sCommand);
+        }
+    });
+    xPort.postMessage({sActionDone: "newId", result: iPortId});
+}
+
+browser.runtime.onConnect.addListener(handleConnexion);
+
+
+/*
+    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.create({
+    id: "conjugueur_panel",
+    title: "Conjugueur [fenêtre]",
+    contexts: ["all"]
+});
+browser.contextMenus.create({
+    id: "conjugueur_tab",
+    title: "Conjugueur [onglet]",
+    contexts: ["all"]
+});
+
+function onCreated(windowInfo) {
+    console.log(`Created window: ${windowInfo.id}`);
+}
+
+function onError(error) {
+    console.log(`Error: ${error}`);
+}
+
+let xConjWindow = null;
+let xConjTab = null;
+
+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;
+        case "conjugueur_panel":
+            xConjWindow = browser.windows.create({
+                url: browser.extension.getURL("panel/conjugueur.html"),
+                type: "detached_panel",
+                width: 710,
+                height: 980
+            });
+            xConjWindow.then(onCreated, onError);
+            break;
+        case "conjugueur_tab":
+            xConjTab = browser.tabs.create({
+                url: browser.extension.getURL("panel/conjugueur.html"),
+                pinned: true
+            });
+            xConjTab.then(onCreated, onError);
+            break;
+    }    
+});
+
+
+async function getCurrentTabId () {
+    let xTab = await browser.tabs.getCurrent();
+    return xTab.id;
+}
+
+/*
+    TESTS ONLY
+*/
+async function newwin () {
+    // test for popup window-like, which doesn’t close when losing the focus
+    console.log("Async on");
+    const getActive = browser.tabs.query({ currentWindow: true, active: true, });
+    const xWindowInfo = await browser.windows.getLastFocused();
+    const width = 710, height = 980; // the maximum size for panels is somewhere around 700x800. Firefox needs some additional pixels: 14x42 for FF54 on Win 10 with dpi 1.25
+    const left = Math.round(xWindowInfo.left + xWindowInfo.width - width - 25);
+    const top = Math.round(xWindowInfo.top + 74); // the actual frame height of the main window varies, but 74px should place the pop-up at the bottom if the button
+    const xWin = await browser.windows.create({
+        type: 'panel', url: browser.extension.getURL("panel/conjugueur.html"), top: top, left: left, width: width, height: height,
+    });
+    browser.windows.update(xWin.id, { top:top, left:left, }); // firefox currently ignores top and left in .create(), so move it here
+    console.log("Async done");
+}
+
+//newwin();

ADDED   gc_lang/fr/webext/content_scripts/content_modifier.js
Index: gc_lang/fr/webext/content_scripts/content_modifier.js
==================================================================
--- /dev/null
+++ gc_lang/fr/webext/content_scripts/content_modifier.js
@@ -0,0 +1,198 @@
+// Modify page
+
+/*
+    JS sucks (again and again and again and again…)
+    Not possible to load content from within the extension:
+    https://bugzilla.mozilla.org/show_bug.cgi?id=1267027
+    No SharedWorker, no images allowed for now…
+*/
+
+"use strict";
+
+console.log("[Content script] Start");
+
+
+let nTadId = null;
+let nWrapper = 0;
+
+let oConjPanel = null;
+let oTFPanel = null;
+let oLxgPanel = null;
+let oGCPanel = null;
+
+
+function showError (e) {
+    console.error(e.fileName + "\n" + e.name + "\nline: " + e.lineNumber + "\n" + e.message);
+}
+
+function wrapTextareas () {
+    let lNode = document.getElementsByTagName("textarea");
+    for (let xNode of lNode) {
+        createWrapper(xNode);
+    }
+}
+
+function createWrapper (xTextArea) {
+    try {
+        let xParentElement = xTextArea.parentElement;
+        let xWrapper = document.createElement("div");
+        xWrapper.className = "grammalecte_wrapper";
+        xWrapper.id = nWrapper + 1;
+        nWrapper += 1;
+        xParentElement.insertBefore(xWrapper, xTextArea);
+        xWrapper.appendChild(xTextArea); // move textarea in wrapper
+        let xToolbar = createWrapperToolbar(xTextArea);
+        xWrapper.appendChild(xToolbar);
+    }
+    catch (e) {
+        showError(e);
+    }
+}
+
+function createWrapperToolbar (xTextArea) {
+    try {
+        let xToolbar = createNode("div", {className: "grammalecte_wrapper_toolbar"});
+        let xConjButton = createNode("div", {className: "grammalecte_wrapper_button", textContent: "Conjuguer"});
+        xConjButton.onclick = function() { createConjPanel(); };
+        let xTFButton = createNode("div", {className: "grammalecte_wrapper_button", textContent: "Formater"});
+        xTFButton.onclick = function() { createTFPanel(xTextArea); };
+        let xLxgButton = createNode("div", {className: "grammalecte_wrapper_button", textContent: "Analyser"});
+        xLxgButton.onclick = function() {
+            createLxgPanel();
+            xPort.postMessage({
+                sCommand: "getListOfTokens",
+                dParam: {sText: xTextArea.value},
+                dInfo: {sTextAreaId: xTextArea.id}
+            });
+        };
+        let xGCButton = createNode("div", {className: "grammalecte_wrapper_button", textContent: "Corriger"});
+        xGCButton.onclick = function() {
+            createGCPanel();
+            xPort.postMessage({
+                sCommand: "parseAndSpellcheck",
+                dParam: {sText: xTextArea.value, sCountry: "FR", bDebug: false, bContext: false},
+                dInfo: {sTextAreaId: xTextArea.id}
+            });
+        };
+        // Create
+        //xToolbar.appendChild(createNode("img", {scr: browser.extension.getURL("img/logo-16.png")}));
+        // can’t work, due to content-script policy: https://bugzilla.mozilla.org/show_bug.cgi?id=1267027
+        xToolbar.appendChild(createLogo());
+        xToolbar.appendChild(document.createTextNode("Grammalecte"));
+        xToolbar.appendChild(xConjButton);
+        xToolbar.appendChild(xTFButton);
+        xToolbar.appendChild(xLxgButton);
+        xToolbar.appendChild(xGCButton);
+        return xToolbar;
+    }
+    catch (e) {
+        showError(e);
+    }
+}
+
+function createConjPanel () {
+    console.log("Conjugueur");
+    if (oConjPanel !== null) {
+        oConjPanel.show();
+    } else {
+        // create the panel
+        oConjPanel = new GrammalectePanel("grammalecte_conj_panel", "Conjugueur", 600, 600);
+        oConjPanel.insertIntoPage();
+    }
+}
+
+function createTFPanel (xTextArea) {
+    console.log("Formateur de texte");
+    if (oTFPanel !== null) {
+        oTFPanel.show();
+    } else {
+        // create the panel
+        oTFPanel = new GrammalectePanel("grammalecte_tf_panel", "Formateur de texte", 800, 600, false);
+        oTFPanel.logInnerHTML();
+        oTFPanel.setContentNode(createTextFormatter(xTextArea));
+        oTFPanel.insertIntoPage();
+    }
+}
+
+function createLxgPanel () {
+    console.log("Lexicographe");
+    if (oLxgPanel !== null) {
+        oLxgPanelContent.clear();
+        oLxgPanel.show();
+    } else {
+        // create the panel
+        oLxgPanel = new GrammalectePanel("grammalecte_lxg_panel", "Lexicographe", 500, 700);
+        oLxgPanel.setContentNode(oLxgPanelContent.init());
+        oLxgPanel.insertIntoPage();
+    }
+}
+
+function createGCPanel () {
+    console.log("Correction grammaticale");
+    if (oGCPanel !== null) {
+        oGCPanelContent.clear();
+        oGCPanel.show();
+    } else {
+        // create the panel
+        oGCPanel = new GrammalectePanel("grammalecte_gc_panel", "Correcteur", 500, 700);
+        oGCPanel.setContentNode(oGCPanelContent.init());
+        oGCPanel.insertIntoPage();
+    }
+}
+
+
+/*
+    Simple message
+*/
+function handleMessage (oMessage, xSender, sendResponse) {
+    console.log("[Content script] received:");
+    console.log(oMessage);
+    //change(request.myparam);
+    //browser.runtime.onMessage.removeListener(handleMessage);
+    sendResponse({response: "response from content script"});
+}
+
+browser.runtime.onMessage.addListener(handleMessage);
+
+
+/*
+    Connexion to the background
+*/
+let xPort = browser.runtime.connect({name: "content-script port"});
+
+xPort.onMessage.addListener(function (oMessage) {
+    console.log("[Content script] received…");
+    let {sActionDone, result, dInfo, bError} = oMessage;
+    switch (sActionDone) {
+        case "getCurrentTabId":
+            console.log("[Content script] tab id: " + result);
+            nTadId = result;
+            break;
+        case "parseAndSpellcheck":
+            console.log("[content script] received: parseAndSpellcheck");
+            oGCPanelContent.addParagraphResult(result);
+            break;
+        case "parseAndSpellcheck1":
+            console.log("[content script] received: parseAndSpellcheck1");
+            oGCPanelContent.refreshParagraph(dInfo.sParagraphId, result);
+            break;
+        case "getListOfTokens":
+            console.log("[content script] received: getListOfTokens");
+            oLxgPanelContent.addListOfTokens(result);
+            break;
+        default:
+            console.log("[Content script] Unknown command: " + sActionDone);
+    }
+});
+
+xPort.postMessage({
+    sCommand: "getCurrentTabId",
+    dParam: {},
+    dInfo: {}
+});
+
+/*document.body.addEventListener("click", function () {
+    xPort.postMessage({greeting: "they clicked the page!"});
+});*/
+
+wrapTextareas();

ADDED   gc_lang/fr/webext/content_scripts/content_panels.css
Index: gc_lang/fr/webext/content_scripts/content_panels.css
==================================================================
--- /dev/null
+++ gc_lang/fr/webext/content_scripts/content_panels.css
@@ -0,0 +1,142 @@
+/*
+    CSS
+    Content panels for Grammalecte
+*/
+
+
+/*
+    Wrapper
+*/
+.grammalecte_wrapper {
+    padding: 5px;
+    border-radius: 3px;
+    background-color: hsl(210, 50%, 50%);
+    font-family: "Trebuchet MS", "Liberation Sans", sans-serif;
+    color: hsl(210, 10%, 90%);
+}
+
+.grammalecte_wrapper_button {
+    display: inline-block;
+    padding: 0 5px;
+    margin-left: 5px;
+    background-color: hsl(210, 50%, 60%);
+    border-radius: 2px;
+    color: hsl(210, 0%, 96%);
+    cursor: pointer;
+}
+.grammalecte_wrapper_button:hover {
+    background-color: hsl(210, 50%, 55%);
+    box-shadow: 0 0 1px 1px hsl(210, 50%, 20%);
+    color: hsl(210, 0%, 100%);
+}
+.grammalecte_wrapper_toolbar {
+    display: flex;
+    justify-content: flex-end;
+    margin-top: 5px;
+    padding: 5px 10px;
+}
+
+/*
+    Panels
+*/
+.grammalecte_panel {
+    padding: 0;
+    margin: 0;
+    position: fixed;
+    z-index: 100;
+    border: 2px solid hsl(210, 0%, 50%);
+    border-radius: 10px 10px 10px 10px;
+    background-color: hsl(210, 20%, 100%);
+    color: hsl(210, 10%, 4%);
+    font-family: "Trebuchet MS", "Liberation Sans", sans-serif;
+    box-shadow: 0 0 4px 2px hsl(210, 0%, 50%);
+}
+
+.grammalecte_panel_bar {
+    position: sticky;
+    width: 100%;
+    background-color: hsl(210, 0%, 90%);
+    border-radius: 10px 10px 0 0;
+    border-bottom: 1px solid hsl(210, 10%, 80%);
+    font-size: 20px;
+}
+.grammalecte_panel_title {
+    padding: 10px 20px;
+}
+.grammalecte_panel_label {
+    display: inline-block;
+    padding: 0 10px;
+}
+
+.grammalecte_panel_commands {
+    float: right;
+}
+.grammalecte_move_button {
+    display: inline-block;
+    padding: 2px 5px;
+    background-color: hsl(180, 80%, 50%);
+    font-size: 22px;
+    font-weight: bold;
+    color: hsl(210, 0%, 100%);
+    text-align: center;
+    cursor: pointer;
+}
+.grammalecte_move_button:hover {
+    background-color: hsl(180, 100%, 60%);
+}
+.grammalecte_close_button {
+    display: inline-block;
+    padding: 2px 10px;
+    border-radius: 0 8px 0 0;
+    background-color: hsl(0, 80%, 50%);
+    font-size: 22px;
+    font-weight: bold;
+    color: hsl(210, 0%, 100%);
+    text-align: center;
+    cursor: pointer;
+}
+.grammalecte_close_button:hover {
+    background-color: hsl(0, 100%, 60%);
+}
+
+.grammalecte_panel_content {
+    height: calc(100% - 55px); /* panel height - title_bar */
+    overflow: auto;
+}
+
+
+/*
+    CSS Spinner
+    Double bounce
+    http://tobiasahlin.com/spinkit/
+*/
+.grammalecte_spinner {
+    width: 30px;
+    height: 30px;
+    position: absolute;
+    top: 2px;
+    right: 120px;
+}
+.grammalecte_spinner .bounce1,
+.grammalecte_spinner .bounce2 {
+    width: 100%;
+    height: 100%;
+    border-radius: 50%;
+    background-color: hsl(180, 50%, 75%);
+    opacity: 0.6;
+    position: absolute;
+    top: 0;
+    left: 0;
+    animation: grammalecte-sk-bounce 2.0s infinite ease-in-out;
+}
+.bounce2 {
+    animation-delay: -1.0s;
+}
+
+@keyframes grammalecte-sk-bounce {
+    0%, 100% { 
+        transform: scale(0.0);
+    } 50% { 
+        transform: scale(1.0);
+    }
+}

ADDED   gc_lang/fr/webext/content_scripts/gc_content.css
Index: gc_lang/fr/webext/content_scripts/gc_content.css
==================================================================
--- /dev/null
+++ gc_lang/fr/webext/content_scripts/gc_content.css
@@ -0,0 +1,432 @@
+/*
+    Grammar checker
+*/
+#grammalecte_gc_panel_content {
+    padding: 5px;
+}
+
+.grammalecte_paragraph_block {
+    margin: 0 0 10px 0;
+}
+
+.grammalecte_paragraph {
+    background-color: hsla(0, 0%, 90%, 1);
+    padding: 10px;
+    border-radius: 2px;
+    font-size: 14px;
+    font-family : "Courier New", Courier, "Lucida Sans Typewriter", "Lucida Typewriter", monospace;
+    color: hsl(0, 0%, 0%);
+}
+
+.grammalecte_paragraph a {
+    background-color: hsla(210, 50%, 50%, 1);
+    padding: 1px 5px;
+    border-radius: 2px;
+    color: hsla(210, 0%, 96%, 1);
+    cursor: pointer;
+    text-decoration: none;
+}
+.grammalecte_paragraph a:hover {
+    background-color: hsla(210, 60%, 40%, 1);
+    color: hsla(0, 0%, 100%, 1);
+    text-shadow: 0 0 3px hsl(210, 30%, 60%);
+}
+
+.grammalecte_paragraph u {
+    text-decoration: none;
+}
+
+.grammalecte_paragraph u.corrected,
+.grammalecte_paragraph u.ignored {
+    background-color: hsla(120, 50%, 70%, 1);
+    color: hsla(0, 0%, 4%, 1);
+    border-radius: 2px;
+    text-decoration: none;
+}
+.grammalecte_paragraph u.ignored {
+    background-color: hsla(30, 20%, 80%, 1);
+}
+
+.grammalecte_paragraph u.error {
+    position: relative;
+    cursor: pointer;
+    border-radius: 2px;
+    text-decoration: none; /* to remove for wavy underlines */
+}
+.grammalecte_paragraph u.error:hover {
+    cursor: pointer;
+}
+
+
+.grammalecte_paragraph_actions {
+    margin: 0 0 5px 10px;
+}
+.grammalecte_paragraph_actions .close_button {
+    background-color: hsl(0, 0%, 50%);
+    text-align: center;
+    float: right;
+    margin-left: 2px;
+    padding: 1px 4px 3px 4px;
+    cursor: pointer;
+    font-size: 14px;
+    color: hsl(0, 0%, 96%);
+    border-radius: 2px;
+    background-color: hsl(0, 30%, 50%);
+    color: hsl(0, 0%, 96%);
+}
+
+
+/*
+    Action buttons
+*/
+
+.grammalecte_actions {
+    float: right;
+    margin: 0 0 5px 10px;
+}
+
+.grammalecte_actions .button {
+    display: inline-block;
+    background-color: hsl(0, 0%, 50%);
+    text-align: center;
+    margin-left: 2px;
+    padding: 1px 4px 3px 4px;
+    cursor: pointer;
+    font-size: 14px;
+    color: hsl(0, 0%, 96%);
+    border-radius: 2px;
+}
+.grammalecte_actions .button:hover {
+    background-color: hsl(0, 0%, 40%);
+    color: hsl(0, 0%, 100%);
+}
+
+.grammalecte_actions .green {
+    background-color: hsl(120, 30%, 50%);
+    color: hsl(0, 0%, 96%);
+}
+.grammalecte_actions .green:hover {
+    background-color: hsl(120, 50%, 40%);
+    color: hsl(0, 0%, 100%);
+}
+.grammalecte_actions .red {
+    background-color: hsl(0, 30%, 50%);
+    color: hsl(0, 0%, 96%);
+}
+.grammalecte_actions .red:hover {
+    background-color: hsl(0, 50%, 40%);
+    color: hsl(0, 0%, 100%);
+}
+.grammalecte_actions .orange {
+    background-color: hsl(30, 50%, 50%);
+    color: hsl(30, 0%, 96%);
+}
+.grammalecte_actions .orange:hover {
+    background-color: hsl(30, 70%, 40%);
+    color: hsl(30, 0%, 100%);
+}
+.grammalecte_actions .bold {
+    font-weight: bold;
+}
+
+
+
+/* 
+    TOOLTIP
+*/
+#grammalecte_tooltip_arrow {
+    position: absolute;
+    display: none;
+}
+
+#grammalecte_tooltip {
+    position: absolute;
+    display: none;
+    width: 300px;
+    border-radius: 5px;
+    box-shadow: 0 0 6px hsla(0, 0%, 0%, 0.3);
+    font-family: Tahoma, "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", sans-serif;
+    font-size: 12px;
+    line-height: 18px;
+    cursor: default;
+    text-decoration: none;
+    border: 3px solid hsl(210, 50%, 30%);
+    color: hsla(210, 10%, 20%, 1);
+    z-index: 1000;
+}
+
+#grammalecte_tooltip_message_block {
+    padding: 5px 10px 10px 10px;
+    background-color: hsl(210, 50%, 30%);
+    color: hsl(210, 50%, 96%);
+}
+#grammalecte_tooltip_rule_id {
+    display: none;
+    margin-bottom: 5px;
+    border: 1px solid hsl(210, 50%, 60%);
+    background-color: hsl(210, 50%, 40%);
+    padding: 2px 5px;
+    border-radius: 2px;
+    color: hsla(210, 0%, 96%, 1);
+    font-size: 11px;
+    font-style: normal;
+    text-align: center;
+}
+#grammalecte_tooltip_message {
+    font-size: 15px;
+    margin-bottom: 5px;
+}
+a#grammalecte_tooltip_ignore {
+    padding: 0 2px;
+    background-color: hsla(30, 30%, 40%, 1);
+    color: hsla(30, 0%, 96%, 1);
+    border-radius: 2px;
+    cursor: pointer;
+    text-decoration: none;
+}
+a#grammalecte_tooltip_ignore:hover {
+    background-color: hsla(30, 30%, 50%, 1);
+    color: hsla(0, 0%, 100%, 1);
+    text-shadow: 0 0 3px hsl(30, 30%, 60%);
+}
+a#grammalecte_tooltip_url {
+    padding: 0 2px;
+    background-color: hsla(210, 50%, 50%, 1);
+    color: hsla(210, 0%, 96%, 1);
+    border-radius: 2px;
+    cursor: pointer;
+    text-decoration: none;
+}
+a#grammalecte_tooltip_url:hover {
+    background-color: hsla(210, 50%, 60%, 1);
+    color: hsla(0, 0%, 100%, 1);
+    text-shadow: 0 0 3px hsl(210, 30%, 60%);
+}
+#grammalecte_tooltip_sugg_title {
+    padding: 0 10px;
+    background-color: hsl(210, 10%, 90%);
+    color: hsl(210, 50%, 30%);
+    font-size: 10px;
+    font-weight: bold;
+}
+#grammalecte_tooltip_sugg_block {
+    padding: 10px;
+    background-color: hsl(210, 10%, 96%);
+    border-radius: 0 0 2px 2px;
+    line-height: 20px;
+}
+#grammalecte_tooltip_sugg_block a.sugg {
+    padding: 1px 6px;
+    background-color: hsla(180, 60%, 40%, 1);
+    color: hsla(180, 0%, 96%, 1);
+    border-radius: 2px;
+    cursor: pointer;
+    text-decoration: none;
+}
+#grammalecte_tooltip_sugg_block a.sugg:hover {
+    background-color: hsla(180, 70%, 45%, 1);
+    color: hsla(0, 0%, 100%, 1);
+}
+
+
+/*
+    Action buttons
+*/
+
+.grammalecte_paragraph_actions {
+    margin: 0 0 5px 10px;
+}
+
+.grammalecte_paragraph_actions .button {
+    background-color: hsl(0, 0%, 50%);
+    text-align: center;
+    float: right;
+    margin-left: 2px;
+    padding: 1px 4px 3px 4px;
+    /*width: 18px;
+    height: 18px;*/
+    cursor: pointer;
+    font-size: 14px;
+    color: hsl(0, 0%, 96%);
+    border-radius: 2px;
+}
+.grammalecte_paragraph_actions .button:hover {
+    background-color: hsl(0, 0%, 40%);
+    color: hsl(0, 0%, 100%);
+}
+
+.grammalecte_paragraph_actions .green {
+    background-color: hsl(120, 30%, 50%);
+    color: hsl(0, 0%, 96%);
+}
+.grammalecte_paragraph_actions .green:hover {
+    background-color: hsl(120, 50%, 40%);
+    color: hsl(0, 0%, 100%);
+}
+.grammalecte_paragraph_actions .red {
+    background-color: hsl(0, 30%, 50%);
+    color: hsl(0, 0%, 96%);
+}
+.grammalecte_paragraph_actions .red:hover {
+    background-color: hsl(0, 50%, 40%);
+    color: hsl(0, 0%, 100%);
+}
+.grammalecte_paragraph_actions .orange {
+    background-color: hsl(30, 50%, 50%);
+    color: hsl(30, 0%, 96%);
+}
+.grammalecte_paragraph_actions .orange:hover {
+    background-color: hsl(30, 70%, 40%);
+    color: hsl(30, 0%, 100%);
+}
+.grammalecte_paragraph_actions .bold {
+    font-weight: bold;
+}
+
+
+/*
+    ERRORS
+*/
+
+.grammalecte_paragraph .error {
+    /* default color */
+    background-color: hsl(240, 10%, 50%);
+    color: hsl(240, 0%, 96%);
+}
+.grammalecte_paragraph .error:hover {
+    background-color: hsl(240, 10%, 40%);
+    color: hsl(240, 0%, 100%);
+}
+
+/* elems */
+.grammalecte_paragraph .WORD {
+    background-color: hsl(0, 50%, 50%);
+    color: hsl(0, 0%, 96%);
+    /*text-decoration: underline wavy hsl(0, 50%, 50%);*/
+}
+.grammalecte_paragraph .WORD:hover {
+    background-color: hsl(0, 60%, 40%);
+    color: hsl(0, 0%, 100%);
+}
+
+/* elems */
+.grammalecte_paragraph .typo, 
+.grammalecte_paragraph .esp, 
+.grammalecte_paragraph .nbsp, 
+.grammalecte_paragraph .eif, 
+.grammalecte_paragraph .maj, 
+.grammalecte_paragraph .virg, 
+.grammalecte_paragraph .tu, 
+.grammalecte_paragraph .num, 
+.grammalecte_paragraph .unit, 
+.grammalecte_paragraph .nf, 
+.grammalecte_paragraph .liga, 
+.grammalecte_paragraph .mapos, 
+.grammalecte_paragraph .chim {
+    background-color: hsl(30, 70%, 50%);
+    color: hsl(30, 10%, 96%);
+    /*text-decoration: underline wavy hsl(30, 70%, 50%);*/
+}
+
+.grammalecte_paragraph .typo:hover, 
+.grammalecte_paragraph .esp:hover, 
+.grammalecte_paragraph .nbsp:hover, 
+.grammalecte_paragraph .eif:hover, 
+.grammalecte_paragraph .maj:hover, 
+.grammalecte_paragraph .virg:hover, 
+.grammalecte_paragraph .tu:hover, 
+.grammalecte_paragraph .num:hover, 
+.grammalecte_paragraph .unit:hover, 
+.grammalecte_paragraph .nf:hover, 
+.grammalecte_paragraph .liga:hover, 
+.grammalecte_paragraph .mapos:hover, 
+.grammalecte_paragraph .chim:hover {
+    background-color: hsl(30, 80%, 45%);
+    color: hsl(30, 10%, 96%);
+}
+
+/* elems */
+.grammalecte_paragraph .apos {
+    background-color: hsl(40, 90%, 50%);
+    color: hsl(40, 10%, 96%);
+    /*text-decoration: underline wavy hsl(40, 70%, 45%);*/
+}
+.grammalecte_paragraph .apos:hover {
+    background-color: hsl(40, 100%, 45%);
+    color: hsl(40, 10%, 96%);
+}
+
+/* elems */
+.grammalecte_paragraph .gn,
+.grammalecte_paragraph .sgpl {
+    background-color: hsl(210, 50%, 50%);
+    color: hsl(210, 10%, 96%);
+    /*text-decoration: underline wavy hsl(210, 50%, 50%);*/
+}
+.grammalecte_paragraph .gn:hover,
+.grammalecte_paragraph .sgpl:hover {
+    background-color: hsl(210, 60%, 40%);
+    color: hsl(210, 10%, 96%);
+}
+
+/* elems */
+.grammalecte_paragraph .conj, 
+.grammalecte_paragraph .infi, 
+.grammalecte_paragraph .imp, 
+.grammalecte_paragraph .inte, 
+.grammalecte_paragraph .ppas, 
+.grammalecte_paragraph .vmode  {
+    background-color: hsl(300, 30%, 40%);
+    color: hsl(300, 10%, 96%);
+    /*text-decoration: underline wavy hsl(300, 40%, 40%);*/
+}
+
+.grammalecte_paragraph .conj:hover, 
+.grammalecte_paragraph .infi:hover, 
+.grammalecte_paragraph .imp:hover, 
+.grammalecte_paragraph .inte:hover, 
+.grammalecte_paragraph .ppas:hover, 
+.grammalecte_paragraph .vmode:hover {
+    background-color: hsl(300, 40%, 30%);
+    color: hsl(300, 10%, 96%);
+}
+
+/* elems */
+.grammalecte_paragraph .conf, 
+.grammalecte_paragraph .ocr {
+    background-color: hsl(270, 40%, 30%);
+    color: hsl(270, 10%, 96%);
+    /*text-decoration: underline wavy hsl(270, 40%, 30%);*/
+}
+
+.grammalecte_paragraph .conf:hover, 
+.grammalecte_paragraph .ocr:hover {
+    background-color: hsl(270, 50%, 20%);
+    color: hsl(270, 10%, 96%);
+}
+
+/* elems */
+.grammalecte_paragraph .bs, 
+.grammalecte_paragraph .pleo, 
+.grammalecte_paragraph .neg, 
+.grammalecte_paragraph .redon1, 
+.grammalecte_paragraph .redon2, 
+.grammalecte_paragraph .mc, 
+.grammalecte_paragraph .date, 
+.grammalecte_paragraph .notype {
+    background-color: hsl(180, 50%, 40%);
+    color: hsl(180, 10%, 96%);
+    /*text-decoration: underline wavy hsl(180, 50%, 40%);*/
+}
+
+.grammalecte_paragraph .bs:hover, 
+.grammalecte_paragraph .pleo:hover, 
+.grammalecte_paragraph .neg:hover, 
+.grammalecte_paragraph .redon1:hover, 
+.grammalecte_paragraph .redon2:hover, 
+.grammalecte_paragraph .mc:hover, 
+.grammalecte_paragraph .date:hover, 
+.grammalecte_paragraph .notype:hover {
+    background-color: hsl(180, 60%, 30%);
+    color: hsl(180, 10%, 96%);
+}

ADDED   gc_lang/fr/webext/content_scripts/gc_content.js
Index: gc_lang/fr/webext/content_scripts/gc_content.js
==================================================================
--- /dev/null
+++ gc_lang/fr/webext/content_scripts/gc_content.js
@@ -0,0 +1,372 @@
+// JavaScript
+
+"use strict";
+
+function onGrammalecteGCPanelClick (xEvent) {
+    try {
+        let xElem = xEvent.target;
+        if (xElem.id) {
+            if (xElem.id.startsWith("grammalecte_sugg")) {
+                oGCPanelContent.applySuggestion(xElem.id);
+            } else if (xElem.id === "grammalecte_tooltip_ignore") {
+                oGCPanelContent.ignoreError(xElem.id);
+            } else if (xElem.id.startsWith("grammalecte_check")) {
+                oGCPanelContent.recheckParagraph(xElem.id.slice(17));
+            } else if (xElem.id.startsWith("grammalecte_hide")) {
+                document.getElementById(xElem.id).parentNode.parentNode.style.display = "none";
+            } else if (xElem.tagName === "U" && xElem.id.startsWith("grammalecte_err")
+                       && xElem.className !== "corrected" && xElem.className !== "ignored") {
+                oGrammalecteTooltip.show(xElem.id);
+            } else if (xElem.id === "grammalecte_tooltip_url") {
+                oGCPanelContent.openURL(xElem.getAttribute("href"));
+            } else {
+                oGrammalecteTooltip.hide();
+            }
+        } else if (xElem.tagName === "A") {
+            oGCPanelContent.openURL(xElem.getAttribute("href"));
+        } else {
+            oGrammalecteTooltip.hide();
+        }
+    }
+    catch (e) {
+        showError(e);
+    }
+}
+
+
+const oGCPanelContent = {
+    /*
+        KEYS for identifiers:
+            grammalecte_paragraph{Id} : [paragraph number]
+            grammalecte_check{Id}     : [paragraph number]
+            grammalecte_hide{Id}      : [paragraph number]
+            grammalecte_error{Id}     : [paragraph number]-[error_number]
+            grammalecte_sugg{Id}      : [paragraph number]-[error_number]--[suggestion_number]
+    */
+
+    bInitDone: false,
+
+    xContentNode: null,
+    xParagraphList: null,
+
+    aIgnoredErrors: new Set(),
+
+    init: function () {
+        this.xContentNode = createNode("div", {id: "grammalecte_gc_panel_content"});
+        this.xParagraphList = createNode("div", {id: "grammalecte_paragraph_list"});
+        this.xContentNode.appendChild(this.xParagraphList);
+        this.xContentNode.addEventListener("click", onGrammalecteGCPanelClick, false);
+        oGrammalecteTooltip.init();
+        this.xContentNode.appendChild(oGrammalecteTooltip.xTooltip);
+        this.bInitDone = true;
+        return this.xContentNode;
+    },
+
+    clear: function () {
+        while (this.xParagraphList.firstChild) {
+            this.xParagraphList.removeChild(this.xParagraphList.firstChild);
+        }
+        this.aIgnoredErrors.clear();
+    },
+
+    addParagraphResult: function (oResult) {
+        try {
+            if (oResult) {
+                let xNodeDiv = createNode("div", {className: "grammalecte_paragraph_block"});
+                // actions
+                let xActionsBar = createNode("div", {className: "grammalecte_actions"});
+                xActionsBar.appendChild(createNode("div", {id: "grammalecte_check" + oResult.sParaNum, className: "button green", textContent: "Réanalyser"}));
+                xActionsBar.appendChild(createNode("div", {id: "grammalecte_hide" + oResult.sParaNum, className: "button red bold", textContent: "×"}));
+                // paragraph
+                let xParagraph = createNode("p", {id: "grammalecte_paragraph"+oResult.sParaNum, lang: "fr", contentEditable: "true"});
+                xParagraph.setAttribute("spellcheck", "false"); // doesn’t seem possible to use “spellcheck” as a common attribute.
+                xParagraph.className = (oResult.aGrammErr.length || oResult.aSpellErr.length) ? "grammalecte_paragraph softred" : "grammalecte_paragraph";
+                this._tagParagraph(xParagraph, oResult.sParagraph, oResult.sParaNum, oResult.aGrammErr, oResult.aSpellErr);
+                // creation
+                xNodeDiv.appendChild(xActionsBar);
+                xNodeDiv.appendChild(xParagraph);
+                this.xParagraphList.appendChild(xNodeDiv);
+            }
+        }
+        catch (e) {
+            showError(e);
+        }
+    },
+
+    recheckParagraph: function (sParagraphNum) {
+        //startWaitIcon();
+        let sParagraphId = "grammalecte_paragraph" + sParagraphNum;
+        let xParagraph = document.getElementById(sParagraphId);
+        xPort.postMessage({
+            sCommand: "parseAndSpellcheck1",
+            dParam: {sText: this.getPurgedTextOfParagraph(xParagraph.textContent), sCountry: "FR", bDebug: false, bContext: false},
+            dInfo: {sParagraphId: sParagraphId}
+        });
+        //stopWaitIcon();
+    },
+
+    refreshParagraph: function (sParagraphId, oResult) {
+        try {
+            let xParagraph = document.getElementById(sParagraphId);
+            xParagraph.className = (oResult.aGrammErr.length || oResult.aSpellErr.length) ? "grammalecte_paragraph softred" : "grammalecte_paragraph";
+            xParagraph.textContent = "";
+            this._tagParagraph(xParagraph, oResult.sParagraph, sParagraphId.slice(21), oResult.aGrammErr, oResult.aSpellErr);
+        }
+        catch (e) {
+            showError(e);
+        }
+    },
+
+    _tagParagraph: function (xParagraph, sParagraph, sParaNum, aSpellErr, aGrammErr) {
+        try {
+            if (aGrammErr.length === 0  &&  aSpellErr.length === 0) {
+                xParagraph.textContent = sParagraph;
+                return
+            }
+            aGrammErr.push(...aSpellErr);
+            aGrammErr.sort(function (a, b) {
+                if (a["nStart"] < b["nStart"])
+                    return -1;
+                if (a["nStart"] > b["nStart"])
+                    return 1;
+                return 0;
+            });
+            let nErr = 0; // we count errors to give them an identifier
+            let nEndLastErr = 0;
+            for (let oErr of aGrammErr) {
+                let nStart = oErr["nStart"];
+                let nEnd = oErr["nEnd"];
+                if (nStart >= nEndLastErr) {
+                    oErr['sErrorId'] = sParaNum + "-" + nErr.toString(); // error identifier
+                    oErr['sIgnoredKey'] = sParaNum + ":" + nStart.toString() + ":" + sParagraph.slice(nStart, nEnd);
+                    if (nEndLastErr < nStart) {
+                        xParagraph.appendChild(document.createTextNode(sParagraph.slice(nEndLastErr, nStart)));
+                    }
+                    xParagraph.appendChild(this._createError(sParagraph.slice(nStart, nEnd), oErr));
+                    xParagraph.insertAdjacentHTML("beforeend", "<!-- err_end -->");
+                    nEndLastErr = nEnd;
+                }
+                nErr += 1;
+            }
+            if (nEndLastErr <= sParagraph.length) {
+                xParagraph.appendChild(document.createTextNode(sParagraph.slice(nEndLastErr)));
+            }
+        }
+        catch (e) {
+            showError(e);
+        }
+    },
+
+    _createError: function (sUnderlined, oErr) {
+        let xNodeErr = document.createElement("u");
+        xNodeErr.id = "grammalecte_err" + oErr['sErrorId'];
+        xNodeErr.textContent = sUnderlined;
+        xNodeErr.dataset.error_id = oErr['sErrorId'];
+        xNodeErr.dataset.ignored_key = oErr['sIgnoredKey'];
+        xNodeErr.dataset.error_type = (oErr['sType'] === "WORD") ? "spelling" : "grammar";
+        if (xNodeErr.dataset.error_type === "grammar") {
+            xNodeErr.dataset.gc_message = oErr['sMessage'];
+            xNodeErr.dataset.gc_url = oErr['URL'];
+            if (xNodeErr.dataset.gc_message.includes(" #")) {
+                xNodeErr.dataset.line_id = oErr['sLineId'];
+                xNodeErr.dataset.rule_id = oErr['sRuleId'];
+            }
+            xNodeErr.dataset.suggestions = oErr["aSuggestions"].join("|");
+        }
+        xNodeErr.className = (this.aIgnoredErrors.has(xNodeErr.dataset.ignored_key)) ? "ignored" : "error " + oErr['sType'];
+        return xNodeErr;
+    },
+
+    applySuggestion: function (sNodeSuggId) { // sugg
+        try {
+            console.log(sNodeSuggId);
+            let sErrorId = document.getElementById(sNodeSuggId).dataset.error_id;
+            //let sParaNum = sErrorId.slice(0, sErrorId.indexOf("-"));
+            console.log("grammalecte_err"+sErrorId);
+            let xNodeErr = document.getElementById("grammalecte_err" + sErrorId);
+            xNodeErr.textContent = document.getElementById(sNodeSuggId).textContent;
+            xNodeErr.className = "corrected";
+            xNodeErr.removeAttribute("style");
+            oGrammalecteTooltip.hide();
+        }
+        catch (e) {
+            showError(e);
+        }
+    },
+
+    ignoreError: function (sIgnoreButtonId) {  // ignore
+        try {
+            console.log(sIgnoreButtonId);
+            let sErrorId = document.getElementById(sIgnoreButtonId).dataset.error_id;
+            console.log("grammalecte_err"+sErrorId);
+            let xNodeErr = document.getElementById("grammalecte_err" + sErrorId);
+            this.aIgnoredErrors.add(xNodeErr.dataset.ignored_key);
+            xNodeErr.className = "ignored";
+            oGrammalecteTooltip.hide();
+        }
+        catch (e) {
+            showError(e);
+        }
+    },
+
+    getPurgedTextOfParagraph: function (sText) {
+        return sText.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
+    },
+
+    addSummary: function () {
+        // todo
+    },
+
+    addMessage: function (sMessage) {
+        let xNode = createNode("div", {className: "grammalecte_gc_panel_message", textContent: sMessage});
+        this.xParagraphList.appendChild(xNode);
+    },
+
+    copyToClipboard: function () {
+        startWaitIcon();
+        try {
+            let xClipboardButton = document.getElementById("grammalecte_clipboard_button");
+            xClipboardButton.textContent = "copie en cours…";
+            let sText = "";
+            for (let xNode of document.getElementById("grammalecte_paragraph_list").getElementsByClassName("grammalecte_paragraph")) {
+                sText += xNode.textContent + "\n";
+            }
+            self.port.emit('copyToClipboard', sText);
+            xClipboardButton.textContent = "-> presse-papiers";
+            window.setTimeout(function() { xClipboardButton.textContent = "∑"; } , 3000);
+        }
+        catch (e) {
+            console.log(e.lineNumber + ": " +e.message);
+        }
+        stopWaitIcon();
+    }
+}
+
+
+
+const oGrammalecteTooltip = {
+
+    xTooltip: null,
+
+    xTooltipArrow: createNode("img", {
+        id: "grammalecte_tooltip_arrow",
+        src: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAECAYAAACzzX7wAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAOwQAADsEBuJFr7QAAABl0RVh0U29mdHdhcmUAcGFpbnQubmV0IDQuMC4xNkRpr/UAAAAlSURBVBhXY/j//z8cq/kW/wdhZDEMSXRFWCVhGKwAmwQCF/8HAGUkScGH4cM8AAAAAElFTkSuQmCC",
+        alt: "^"
+    }),
+
+    xTooltipSuggBlock: null,
+
+    init: function () {
+        this.xTooltip = createNode("div", {id: "grammalecte_tooltip"});
+        let xMessageBlock = createNode("div", {id: "grammalecte_tooltip_message_block"});
+        xMessageBlock.appendChild(createNode("p", {id: "grammalecte_tooltip_rule_id"}));
+        xMessageBlock.appendChild(createNode("p", {id: "grammalecte_tooltip_message", textContent: "Erreur."}));
+        xMessageBlock.appendChild(createNode("a", {id: "grammalecte_tooltip_ignore", href: "#", onclick: "return false;", textContent: "Ignorer"}));
+        xMessageBlock.appendChild(createNode("a", {id: "grammalecte_tooltip_url", href: "#", onclick: "return false;", textContent: "Voulez-vous en savoir plus ?…"}));
+        this.xTooltip.appendChild(xMessageBlock);
+        this.xTooltip.appendChild(createNode("div", {id: "grammalecte_tooltip_sugg_title", textContent: "SUGGESTIONS :"}));
+        this.xTooltipSuggBlock = createNode("div", {id: "grammalecte_tooltip_sugg_block"});
+        this.xTooltip.appendChild(this.xTooltipSuggBlock);
+    },
+
+    show: function (sNodeErrorId) {  // err
+        try {
+            let xNodeErr = document.getElementById(sNodeErrorId);
+            let nLimit = 500 - 330; // paragraph width - tooltip width
+            this.xTooltipArrow.style.top = (xNodeErr.offsetTop + 16) + "px"
+            this.xTooltipArrow.style.left = (xNodeErr.offsetLeft + Math.floor((xNodeErr.offsetWidth / 2))-4) + "px" // 4 is half the width of the arrow.
+            this.xTooltip.style.top = (xNodeErr.offsetTop + 20) + "px";
+            this.xTooltip.style.left = (xNodeErr.offsetLeft > nLimit) ? nLimit + "px" : xNodeErr.offsetLeft + "px";
+            if (xNodeErr.dataset.error_type === "grammar") {
+                // grammar error
+                if (xNodeErr.dataset.gc_message.includes(" ##")) {
+                    let n = xNodeErr.dataset.gc_message.indexOf(" ##");
+                    document.getElementById("grammalecte_tooltip_message").textContent = xNodeErr.dataset.gc_message.slice(0, n);
+                    document.getElementById("grammalecte_tooltip_rule_id").textContent = "Règle : " + xNodeErr.dataset.gc_message.slice(n+2);
+                    document.getElementById("grammalecte_tooltip_rule_id").style.display = "block";
+                } else {
+                    document.getElementById("grammalecte_tooltip_message").textContent = xNodeErr.dataset.gc_message;
+                    document.getElementById("grammalecte_tooltip_rule_id").style.display = "none";
+                }
+                if (xNodeErr.dataset.gc_url != "") {
+                    document.getElementById("grammalecte_tooltip_url").style.display = "inline";
+                    document.getElementById("grammalecte_tooltip_url").setAttribute("href", xNodeErr.dataset.gc_url);
+                } else {
+                    document.getElementById("grammalecte_tooltip_url").style.display = "none";
+                }
+                document.getElementById("grammalecte_tooltip_ignore").dataset.error_id = xNodeErr.dataset.error_id;
+                let iSugg = 0;
+                let xGCSugg = document.getElementById("grammalecte_tooltip_sugg_block");
+                xGCSugg.textContent = "";
+                if (xNodeErr.dataset.suggestions.length > 0) {
+                    for (let sSugg of xNodeErr.dataset.suggestions.split("|")) {
+                        xGCSugg.appendChild(this._createSuggestion(xNodeErr.dataset.error_id, iSugg, sSugg));
+                        xGCSugg.appendChild(document.createTextNode(" "));
+                        iSugg += 1;
+                    }
+                } else {
+                    xGCSugg.textContent = "Aucune.";
+                }
+            }
+            this.xTooltipArrow.style.display = "block";
+            this.xTooltip.style.display = "block";
+            if (xNodeErr.dataset.error_type === "spelling") {
+                // spelling mistake
+                document.getElementById("grammalecte_tooltip_message").textContent = "Mot inconnu du dictionnaire.";
+                document.getElementById("grammalecte_tooltip_ignore").dataset.error_id = xNodeErr.dataset.error_id;
+                while (this.xTooltipSuggBlock.firstChild) {
+                    this.xTooltipSuggBlock.removeChild(this.xTooltipSuggBlock.firstChild);
+                }
+                //console.log("getSuggFor: " + xNodeErr.textContent.trim() + " // error_id: " + xNodeErr.dataset.error_id);
+                //self.port.emit("getSuggestionsForTo", xNodeErr.textContent.trim(), xNodeErr.dataset.error_id);
+                this.setSpellSuggestionsFor(xNodeErr.textContent.trim(), "", xNodeErr.dataset.error_id);
+            }
+        }
+        catch (e) {
+            showError(e);
+        }
+    },
+
+    setTooltipColor: function (bBlue) {
+        // todo
+    },
+
+    hide () {
+        this.xTooltipArrow.style.display = "none";
+        this.xTooltip.style.display = "none";
+    },
+
+    _createSuggestion: function (sErrId, iSugg, sSugg) {
+        let xNodeSugg = document.createElement("a");
+        xNodeSugg.id = "grammalecte_sugg" + sErrId + "--" + iSugg.toString();
+        xNodeSugg.className = "sugg";
+        xNodeSugg.dataset.error_id = sErrId;
+        xNodeSugg.textContent = sSugg;
+        return xNodeSugg;
+    },
+
+    setSpellSuggestionsFor: function (sWord, sSuggestions, sErrId) {
+        // spell checking suggestions
+        try {
+            // console.log("setSuggestionsFor: " + sWord + " > " + sSuggestions + " // " + sErrId);
+            let xSuggBlock = document.getElementById("grammalecte_tooltip_sugg_block");
+            xSuggBlock.textContent = "";
+            if (sSuggestions === "") {
+                xSuggBlock.appendChild(document.createTextNode("Aucune."));
+            } else if (sSuggestions.startsWith("#")) {
+                xSuggBlock.appendChild(document.createTextNode(sSuggestions));
+            } else {
+                let lSugg = sSuggestions.split("|");
+                let iSugg = 0;
+                for (let sSugg of lSugg) {
+                    xSuggBlock.appendChild(this._createSuggestion(sErrId, iSugg, sSugg));
+                    xSuggBlock.appendChild(document.createTextNode(" "));
+                    iSugg += 1;
+                }
+            }
+        }
+        catch (e) {
+            showError(e);
+        }
+    }
+}

ADDED   gc_lang/fr/webext/content_scripts/lxg_content.css
Index: gc_lang/fr/webext/content_scripts/lxg_content.css
==================================================================
--- /dev/null
+++ gc_lang/fr/webext/content_scripts/lxg_content.css
@@ -0,0 +1,83 @@
+/*
+    Lexicographer
+*/
+#grammalecte_lxg_panel_content {
+    padding: 5px;
+    font-size: 13px;
+}
+
+.grammalecte_lxg_list_of_tokens {
+    margin: 5px 0 10px 0;
+    padding: 10px;
+    background-color: hsla(0, 0%, 96%, 1);
+    border-radius: 2px;
+}
+.grammalecte_lxg_list_of_tokens .num {
+    float: right;
+    margin: -12px 0 5px 10px;
+    padding: 5px 10px;
+    font-weight: bold;
+    border-radius: 0 0 4px 4px;
+    background-color: hsl(0, 50%, 50%);
+    color: hsl(0, 10%, 96%);
+}
+.grammalecte_token  {
+    padding: 4px 0;
+}
+.grammalecte_token .separator {
+    margin: 20px 0;
+    padding: 5px 5px;
+    background-color: hsla(0, 0%, 75%, 1);
+    color: hsla(0, 0%, 96%, 1);
+    border-radius: 5px;
+    text-align: center;
+    font-size: 20px;
+}
+.grammalecte_token ul {
+    margin: 0 0 5px 5px;
+}
+.grammalecte_token b {
+    background-color: hsla(150, 10%, 50%, 1);
+    color: hsla(0, 0%, 96%, 1);
+    padding: 2px 5px;
+    border-radius: 2px;
+    text-decoration: none;
+}
+.grammalecte_token b.WORD {
+    background-color: hsla(150, 50%, 50%, 1);
+}
+.grammalecte_token b.ELPFX {
+    background-color: hsla(150, 30%, 50%, 1);
+}
+.grammalecte_token b.UNKNOWN {
+    background-color: hsla(0, 50%, 50%, 1);
+}
+.grammalecte_token b.NUM {
+    background-color: hsla(180, 50%, 50%, 1);
+}
+.grammalecte_token b.COMPLEX {
+    background-color: hsla(60, 50%, 50%, 1);
+}
+.grammalecte_token b.SEPARATOR {
+    background-color: hsla(210, 50%, 50%, 1);
+}
+.grammalecte_token b.LINK {
+    background-color: hsla(270, 50%, 50%, 1);
+}
+.grammalecte_token s {
+    color: hsla(0, 0%, 60%, 1);
+    text-decoration: none;
+}
+.grammalecte_token .textline {
+    text-decoration: bold;
+}
+
+.grammalecte_token p.message {
+    margin-top: 20px;
+    padding: 10px 10px;
+    background-color: hsla(240, 10%, 50%, 1);
+    font-size: 18px;
+    color: hsla(240, 0%, 96%, 1);
+    border-radius: 3px;
+    text-align: center;
+}

ADDED   gc_lang/fr/webext/content_scripts/lxg_content.js
Index: gc_lang/fr/webext/content_scripts/lxg_content.js
==================================================================
--- /dev/null
+++ gc_lang/fr/webext/content_scripts/lxg_content.js
@@ -0,0 +1,70 @@
+// JavaScript
+
+"use strict";
+
+const oLxgPanelContent = {
+
+    _xContentNode: createNode("div", {id: "grammalecte_lxg_panel_content"}),
+
+    _nCount: 0,
+
+    init: function () {
+        return this._xContentNode;
+    },
+
+    clear: function () {
+        this._nCount = 0;
+        while (this._xContentNode.firstChild) {
+            this._xContentNode.removeChild(this._xContentNode.firstChild);
+        }
+    },
+
+    addSeparator: function (sText) {
+        if (this._xContentNode.textContent !== "") {
+            this._xContentNode.appendChild(createNode("div", {className: "grammalecte_lxg_separator", textContent: sText}));
+        }
+    },
+
+    addMessage: function (sClass, sText) {
+        this._xContentNode.appendChild(createNode("div", {className: sClass, textContent: sText}));
+    },
+
+    addListOfTokens: function (lTokens) {
+        try {
+            if (lTokens) {
+                this._nCount += 1;
+                let xNodeDiv = createNode("div", {className: "grammalecte_lxg_list_of_tokens"});
+                xNodeDiv.appendChild(createNode("div", {className: "num", textContent: this._nCount}));
+                for (let oToken of lTokens) {
+                    xNodeDiv.appendChild(this._createTokenNode(oToken));
+                }
+                this._xContentNode.appendChild(xNodeDiv);
+            }
+        }
+        catch (e) {
+            showError(e);
+        }
+    },
+
+    _createTokenNode: function (oToken) {
+        let xTokenNode = createNode("div", {className: "grammalecte_token"});
+        xTokenNode.appendChild(createNode("b", {className: oToken.sType, textContent: oToken.sValue}));
+        xTokenNode.appendChild(createNode("s", {textContent: " : "}));
+        if (oToken.aLabel.length === 1) {
+            xTokenNode.appendChild(document.createTextNode(oToken.aLabel[0]));
+        } else {
+            let xTokenList = document.createElement("ul");
+            for (let sLabel of oToken.aLabel) {
+                xTokenList.appendChild(createNode("li", {textContent: sLabel}));
+            }
+            xTokenNode.appendChild(xTokenList);
+        }
+        return xTokenNode;
+    },
+
+    setHidden: function (sClass, bHidden) {
+        for (let xNode of document.getElementsByClassName(sClass)) {
+            xNode.hidden = bHidden;
+        }
+    }
+}

DELETED gc_lang/fr/webext/content_scripts/modify_page.js
Index: gc_lang/fr/webext/content_scripts/modify_page.js
==================================================================
--- gc_lang/fr/webext/content_scripts/modify_page.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import { echo } from "../mymodule";
-
-echo("CONTENT SCRIPRT!!!");
-
-function handleMessage2 (oRequest, xSender, sendResponse) {
-  console.log(`[Content script] received: ${oRequest.content}`);
-  change(request.myparam);
-  //browser.runtime.onMessage.removeListener(handleMessage);
-  sendResponse({response: "response from content script"});
-}
-
-function removeEverything () {
-  while (document.body.firstChild) {
-    document.body.firstChild.remove();
-  }
-}
-
-function change (param) {
-  document.getElementById("title").setAttribute("background-color", "#809060");
-  console.log("param: " + param);
-  document.getElementById("title").setAttribute("background-color", "#FF0000");
-}
-
-
-/*
-  Assign do_something() as a listener for messages from the extension.
-*/
-browser.runtime.onMessage.addListener(handleMessage2);

ADDED   gc_lang/fr/webext/content_scripts/panel_creator.js
Index: gc_lang/fr/webext/content_scripts/panel_creator.js
==================================================================
--- /dev/null
+++ gc_lang/fr/webext/content_scripts/panel_creator.js
@@ -0,0 +1,170 @@
+// JavaScript
+// Panel creator
+
+"use strict";
+
+console.log("[Content script] Panel creator");
+
+
+class GrammalectePanel {
+
+    constructor (sId, sTitle, nWidth, nHeight, bMovable=true) {
+        this.sId = sId;
+        this.sContentId = sId+"_content";
+        this.nWidth = nWidth;
+        this.nHeight = nHeight;
+        this.bMovable = bMovable;
+        this.xContentNode = createNode("div", {className: "grammalecte_panel_content"});
+        this.xPanelNode = this._createPanel(sTitle);
+        this.center();
+    }
+
+    _createPanel (sTitle) {
+        try {
+            let xPanel = createNode("div", {id: this.sId, className: "grammalecte_panel"});
+            let xBar = createNode("div", {className: "grammalecte_panel_bar"});
+            xBar.appendChild(this._createWaitIcon());
+            xBar.appendChild(this._createButtons());
+            let xTitle = createNode("div", {className: "grammalecte_panel_title"});
+            xTitle.appendChild(createLogo());
+            xTitle.appendChild(createNode("div", {className: "grammalecte_panel_label", textContent: sTitle}));
+            xBar.appendChild(xTitle);
+            xPanel.appendChild(xBar);
+            //xPanel.appendChild(createNode("div", {className: "grammalecte_empty_space_under_title_bar"}));
+            xPanel.appendChild(this.xContentNode);
+            return xPanel;
+        }
+        catch (e) {
+            showError(e);
+        }
+    }
+
+    _createWaitIcon () {
+        let xWaitIcon = createNode("div", {id: "grammalecte_wait_icon", className: "grammalecte_spinner"});
+        xWaitIcon.appendChild(createNode("div", {id: "bounce1"}));
+        xWaitIcon.appendChild(createNode("div", {id: "bounce2"}));
+        return xWaitIcon;
+    }
+
+    _createButtons () {
+        let xButtonLine = createNode("div", {className: "grammalecte_panel_commands"});
+        
+
+        if (this.bMovable) {
+            xButtonLine.appendChild(this._createMoveButton("stickToTop", "¯", "Coller en haut"));
+            xButtonLine.appendChild(this._createMoveButton("stickToLeft", "«", "Coller à gauche"));
+            xButtonLine.appendChild(this._createMoveButton("center", "•", "Centrer"));
+            xButtonLine.appendChild(this._createMoveButton("stickToRight", "»", "Coller à droite"));
+            xButtonLine.appendChild(this._createMoveButton("stickToBottom", "_", "Coller en bas"));
+        }
+        xButtonLine.appendChild(this._createCloseButton());
+        return xButtonLine;
+    }
+
+    _createMoveButton (sAction, sLabel, sTitle) {
+        let xButton = createNode("div", {className: "grammalecte_move_button", textContent: sLabel, title: sTitle});
+        xButton.onclick = function () { this[sAction](); }.bind(this);
+        return xButton;
+    }
+
+    _createCloseButton () {
+        let xButton = createNode("div", {className: "grammalecte_close_button", textContent: "×", title: "Fermer la fenêtre"});
+        xButton.onclick = function () { this.hide(); }.bind(this);  // better than writing “let that = this;” before the function?
+        return xButton;
+    }
+
+    setContentNode (xNode) {
+        this.xContentNode.appendChild(xNode);
+    }
+
+    insertIntoPage () {
+        document.body.appendChild(this.xPanelNode);
+    }
+
+    show () {
+        this.xPanelNode.style.display = "block";
+    }
+
+    hide () {
+        this.xPanelNode.style.display = "none";
+    }
+
+    center () {
+        let nHeight = window.innerHeight-100;
+        this.xPanelNode.style = `top: 50%; left: 50%; width: ${this.nWidth}px; height: ${nHeight}px; margin-top: -${nHeight/2}px; margin-left: -${this.nWidth/2}px;`;
+    }
+
+    stickToLeft () {
+        let nHeight = window.innerHeight-100;
+        this.xPanelNode.style = `top: 50%; left: -2px; width: ${this.nWidth}px; height: ${nHeight}px; margin-top: -${nHeight/2}px;`;
+    }
+
+    stickToRight () {
+        let nHeight = window.innerHeight-100;
+        this.xPanelNode.style = `top: 50%; right: -2px; width: ${this.nWidth}px; height: ${nHeight}px; margin-top: -${nHeight/2}px;`;
+    }
+
+    stickToTop () {
+        let nWidth = Math.floor(window.innerWidth/2);
+        this.xPanelNode.style = `top: -2px; left: 50%; width: ${nWidth}px; height: ${Math.floor(window.innerHeight*0.45)}px; margin-left: -${nWidth/2}px;`;
+    }
+
+    stickToBottom () {
+        let nWidth = Math.floor(window.innerWidth/2);
+        this.xPanelNode.style = `bottom: -2px; left: 50%; width: ${nWidth}px; height: ${Math.floor(window.innerHeight*0.45)}px; margin-left: -${nWidth/2}px;`;
+    }
+
+    reduce () {
+        // todo
+    }
+
+    logInnerHTML () {
+        // for debugging
+        console.log(this.xPanelNode.innerHTML);
+    }
+    
+    startWaitIcon () {
+        document.getElementById("grammalecte_waiticon").hidden = false;
+    }
+
+    stopWaitIcon () {
+        document.getElementById("grammalecte_waiticon").hidden = true;
+    }
+}
+
+
+/*
+    Common functions
+*/
+function createNode (sType, oAttr, oDataset=null) {
+    try {
+        let xNode = document.createElement(sType);
+        Object.assign(xNode, oAttr);
+        if (oDataset) {
+            Object.assign(xNode.dataset, oDataset);
+        }
+        return xNode;
+    }
+    catch (e) {
+        showError(e);
+    }
+}
+
+function createLogo () {
+    let xImg = document.createElement("img");
+    xImg.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAA3XAAAN1wFCKJt4AAAC8UlEQVQ4jX3TbUgTcRwH8P89ddu5u9tt082aZmpFEU4tFz0QGTUwCi0heniR9MSUIKRaD0RvIlKigsooo+iNFa0XJYuwIjEK19OcDtPElsG0ktyp591t7u7+vUh7MPX3+vf5/n8/+P0BmKJIPUUVlh2rdVVeesWlzEybqg+bFOsoylnqPmNavGFfknV2Omu2Lvja3vxAURKJib3opHizu8riLK6gjRyuKgmoSoMRFENRUqfXTzvBGK62LC2uoFkOl4RhjQ8+qWt7dPNE3sbdp+2LXbsGe9qb4rIo/BfwFy6nWQ4ThWGNDzbcfu29dMDh2nHU7CypYNLmzTda0/L5cNuzmDQi/A4Y27k6eQxLI79wS/11D0AAMNvs6XT6ojVJjJEgTbMy2BT77xBMp09KcpaWV1uc41jQoi0NdUHfjeOO9WWn7AVF7s7n986SithPJGeupBh2PCSP/xxqxAp3eq6wuUV7Wc6MSZIEhA8vHjbfOe/OcW3zmAuKy+nUzAyD2bow8ODaEROFq8AyZ5WBYdEZXGqGxZ61HJV+9HYCJRbTNA0QBA40HWunaKN5dKg/DBKxeCIe09Th/m4MJwiMSZmLEzMQAABQRuNqgu8NYX3doTcMpvCkLbtQZ2AJkrPOZG1zlnY13T+Hy9EehY90h57eqcorcZ/lctZuMzAsOjLEqwNv66/6vZcPYRBC+C3cGaBxhSet2av1BpYgTTY7k5y2JPT41slIR6Axv8R9nnOs+4Pf+2r992uOxGVJwgAAAEINfgt3BGgsESWtWas1iGDyl+CT/u7WpvxNFRc4x7qtBoZFhSFejb7z1fq9NYfjsiT+cwcQavBruCOgU4SIGo18amuoq3Js3FNlynVtH385+s53ze+t8cRkURx3yMTTRBAEQVAUXbFlf3XystJKA2NExeFBdWASDAAA+MQACCEEmqbJ0b6PMC7JwhDU8YFHV5u9NZ64LErT/oW/63tPV6uJwmKoOND78u7Fg5NhAAD4CVbzY9cwrWQrAAAAAElFTkSuQmCC";
+    return xImg;
+}
+
+function loadImage (sContainerClass, sImagePath) {
+    let xRequest = new XMLHttpRequest();
+    xRequest.open('GET', browser.extension.getURL("")+sImagePath, false);
+    xRequest.responseType = "arraybuffer";
+    xRequest.send();
+    let blobTxt = new Blob([xRequest.response], {type: 'image/png'});
+    let img = document.createElement('img');
+    img.src = (URL || webkitURL).createObjectURL(blobTxt); // webkitURL is obsolete: https://bugs.webkit.org/show_bug.cgi?id=167518
+    Array.filter(document.getElementsByClassName(sContainerClass), function (oElem) {
+        oElem.appendChild(img);
+    });
+}

ADDED   gc_lang/fr/webext/content_scripts/tf_content.css
Index: gc_lang/fr/webext/content_scripts/tf_content.css
==================================================================
--- /dev/null
+++ gc_lang/fr/webext/content_scripts/tf_content.css
@@ -0,0 +1,79 @@
+/*
+    Text Formatter
+*/
+#grammalecte_tf_options {
+    display: flex;
+    padding: 10px;
+}
+#grammalecte_tf_options .underline:hover {
+    background-color: hsl(210, 10%, 86%);
+    border-radius: 2px;
+}
+.grammalecte_tf_column {
+    flex-grow: 1;
+    width: 360px;
+    padding: 0 5px;
+}
+#grammalecte_tf_options legend label {
+    font-size: 20px;
+    color: hsla(210, 20%, 50%, .8);
+    font-weight: bold;
+}
+#grammalecte_tf_options fieldset {
+    padding: 2px 10px 10px 13px;
+    margin-bottom: 5px;
+    background-color: hsl(210, 10%, 96%);
+    border-color: hsl(210, 20%, 80%);
+    border-radius: 3px;
+}
+#grammalecte_tf_options legend .option {
+    margin: 7px 5px 0 4px;
+    float: left;
+}
+#grammalecte_tf_options label {
+    font-size: 13px;
+}
+#grammalecte_tf_options .grammalecte_tf_result {
+    float: right;
+    margin-right: 3px;
+    font-size: 13px;
+}
+
+#grammalecte_tf_actions {
+    background-color: hsl(120, 10%, 92%);
+    padding: 15px;
+    border-top: 1px solid hsl(120, 20%, 86%);
+}
+
+.grammalecte_button {
+    display: inline-block;
+    padding: 5px 10px;
+    width: 100px;
+    border-radius: 3px;
+    font-size: 16px;
+    font-weight: bold;
+    text-align: center;
+    cursor: pointer;
+}
+
+#grammalecte_tf_reset {
+    background-color: hsl(210, 100%, 50%);
+    color: hsl(210, 0%, 100%);
+}
+#grammalecte_tf_progressbar {
+    width: 250px;
+}
+#grammalecte_tf_time_res {
+    width: 40px;
+    padding: 5px 10px;
+    width: 25px;
+}
+#grammalecte_tf_apply {
+    background-color: hsl(150, 100%, 50%);
+    color: hsl(150, 0%, 100%);
+}
+
+#grammalecte_progressbarbox {
+    display: inline-block;
+    padding: 10px 20px;
+}

ADDED   gc_lang/fr/webext/content_scripts/tf_content.js
Index: gc_lang/fr/webext/content_scripts/tf_content.js
==================================================================
--- /dev/null
+++ gc_lang/fr/webext/content_scripts/tf_content.js
@@ -0,0 +1,167 @@
+// JavaScript
+// Text formatter
+
+"use strict";
+
+function createTextFormatter (xTextArea) {
+    let xTFNode = document.createElement("div");
+    try {
+        // Options
+        let xOptions = createNode("div", {id: "grammalecte_tf_options"});
+        let xColumn1 = createNode("div", {className: "grammalecte_tf_column"});
+        let xSSP = createFieldset("group_ssp", true, "Espaces surnuméraires");
+        xSSP.appendChild(createOptionInputAndLabel("o_start_of_paragraph", true, "En début de paragraphe"));
+        xSSP.appendChild(createOptionInputAndLabel("o_end_of_paragraph", true, "En fin de paragraphe"));
+        xSSP.appendChild(createOptionInputAndLabel("o_between_words", true, "Entre les mots"));
+        xSSP.appendChild(createOptionInputAndLabel("o_before_punctuation", true, "Avant les points (.), les virgules (,)"));
+        xSSP.appendChild(createOptionInputAndLabel("o_within_parenthesis", true, "À l’intérieur des parenthèses"));
+        xSSP.appendChild(createOptionInputAndLabel("o_within_square_brackets", true, "À l’intérieur des crochets"));
+        xSSP.appendChild(createOptionInputAndLabel("o_within_quotation_marks", true, "À l’intérieur des guillemets “ et ”"));
+        let xSpace = createFieldset("group_space", true, "Espaces manquants");
+        xSpace.appendChild(createOptionInputAndLabel("o_add_space_after_punctuation", true, "Après , ; : ? ! . …"));
+        xSpace.appendChild(createOptionInputAndLabel("o_add_space_around_hyphens", true, "Autour des tirets d’incise"));
+        let xNBSP = createFieldset("group_nbsp", true, "Espaces insécables");
+        xNBSP.appendChild(createOptionInputAndLabel("o_nbsp_before_punctuation", true, "Avant : ; ? et !"));
+        xNBSP.appendChild(createOptionInputAndLabel("o_nbsp_within_quotation_marks", true, "Avec les guillemets « et »"));
+        xNBSP.appendChild(createOptionInputAndLabel("o_nbsp_before_symbol", true, "Avant % ‰ € $ £ ¥ ˚C"));
+        xNBSP.appendChild(createOptionInputAndLabel("o_nbsp_within_numbers", true, "À l’intérieur des nombres"));
+        xNBSP.appendChild(createOptionInputAndLabel("o_nbsp_before_units", true, "Avant les unités de mesure"));
+        let xDelete = createFieldset("group_delete", true, "Suppressions");
+        xDelete.appendChild(createOptionInputAndLabel("o_erase_non_breaking_hyphens", true, "Tirets conditionnels"));
+        let xColumn2 = createNode("div", {className: "grammalecte_tf_column"});
+        let xTypo = createFieldset("group_typo", true, "Signes typographiques");
+        xTypo.appendChild(createOptionInputAndLabel("o_ts_apostrophe", true, "Apostrophe (’)"));
+        xTypo.appendChild(createOptionInputAndLabel("o_ts_ellipsis", true, "Points de suspension (…)"));
+        xTypo.appendChild(createOptionInputAndLabel("o_ts_dash_middle", true, "Tirets d’incise :"));
+        xTypo.appendChild(createRadioBoxHyphens("hyphen1", "o_ts_m_dash_middle", "o_ts_n_dash_middle", false));
+        xTypo.appendChild(createOptionInputAndLabel("o_ts_dash_start", true, "Tirets en début de paragraphe :"));
+        xTypo.appendChild(createRadioBoxHyphens("hyphen2", "o_ts_m_dash_start", "o_ts_n_dash_start", true));
+        xTypo.appendChild(createOptionInputAndLabel("o_ts_quotation_marks", true, "Modifier les guillemets droits (\" et ')"));
+        xTypo.appendChild(createOptionInputAndLabel("o_ts_units", true, "Points médians des unités (N·m, Ω·m…)"));
+        xTypo.appendChild(createOptionInputAndLabel("o_ts_spell", true, "Ligatures (cœur…) et diacritiques (ça, État…)"));
+        xTypo.appendChild(createRadioBoxLigatures());
+        xTypo.appendChild(createLigaturesSelection());
+        let xMisc = createFieldset("group_misc", true, "Divers");
+        xMisc.appendChild(createOptionInputAndLabel("o_ordinals_no_exponant", true, "Ordinaux (15e, XXIe…)"));
+        xMisc.appendChild(createOptionInputAndLabel("o_etc", true, "Et cætera, etc."));
+        xMisc.appendChild(createOptionInputAndLabel("o_missing_hyphens", true, "Traits d’union manquants"));
+        xMisc.appendChild(createOptionInputAndLabel("o_ma_word", true, "Apostrophes manquantes"));
+        let xStruct = createFieldset("group_struct", false, "Restructuration [!]");
+        xStruct.appendChild(createOptionInputAndLabel("o_remove_hyphens_at_end_of_paragraphs", false, "Enlever césures en fin de ligne/paragraphe [!]"));
+        xStruct.appendChild(createOptionInputAndLabel("o_merge_contiguous_paragraphs", false, "Fusionner les paragraphes contigus [!]"));
+        xColumn1.appendChild(xSSP);
+        xColumn1.appendChild(xSpace);
+        xColumn1.appendChild(xNBSP);
+        xColumn1.appendChild(xDelete);
+        xColumn2.appendChild(xTypo);
+        xColumn2.appendChild(xMisc);
+        xColumn2.appendChild(xStruct);
+        xOptions.appendChild(xColumn1);
+        xOptions.appendChild(xColumn2);
+        // Actions
+        let xActions = createNode("div", {id: "grammalecte_tf_actions"});
+        xActions.appendChild(createNode("div", {id: "grammalecte_tf_reset", textContent: "Par défaut", className: "grammalecte_button", style: "background-color: hsl(210, 50%, 50%)"}));
+        xActions.appendChild(createNode("progress", {id: "grammalecte_tf_progressbar"}));
+        xActions.appendChild(createNode("span", {id: "grammalecte_tf_time_res"}));
+        xActions.appendChild(createNode("div", {id: "grammalecte_tf_apply", textContent: "Appliquer", className: "grammalecte_button", style: "background-color: hsl(180, 50%, 50%)"}));
+        //xActions.appendChild(createNode("div", {id: "grammalecte_infomsg", textContent: "blabla"}));
+        // create result
+        xTFNode.appendChild(xOptions);
+        xTFNode.appendChild(xActions);
+    }
+    catch (e) {
+        //console.error(e);
+        showError(e);
+    }
+    return xTFNode;
+}
+
+
+/*
+    Common options
+*/
+function createFieldset (sId, bDefault, sLabel) {
+    let xFieldset = createNode("fieldset", {id: sId, className: "groupblock"});
+    let xLegend = document.createElement("legend");
+    xLegend.appendChild(createNode("input", {type: "checkbox", id: "o_"+sId, className: "option"}, {default: bDefault}));
+    xLegend.appendChild(createNode("label", {htmlFor: "o_"+sId, textContent: sLabel}));
+    xFieldset.appendChild(xLegend);
+    return xFieldset;
+}
+
+function createOptionInputAndLabel (sId, bDefault, sLabel) {
+    let xOption = createNode("div", {className: "blockopt underline"});
+    xOption.appendChild(createNode("input", {type: "checkbox", id: sId, className: "option"}, {default: bDefault}));
+    xOption.appendChild(createNode("label", {htmlFor: sId, textContent: sLabel, className: "opt_lbl largew"}));
+    xOption.appendChild(createNode("div", {id: "res_"+sId, className: "grammalecte_tf_result", textContent: "9999"}));
+    return xOption;
+}
+
+
+/*
+    Hyphens
+*/
+function createRadioBoxHyphens (sName, sIdEmDash, sIdEnDash, bDefaultEmDash) {
+    let xLine = createNode("div", {className: "blockopt"});
+    xLine.appendChild(createNode("input", {type: "radio", id: sIdEmDash, name: sName, className:"option"}, {default: bDefaultEmDash}));
+    xLine.appendChild(createNode("label", {htmlFor: sIdEmDash, className: "opt_lbl", textContent: "cadratin (—)"}));
+    xLine.appendChild(createNode("input", {type: "radio", id: sIdEnDash, name: sName, className:"option"}, {default: !bDefaultEmDash}));
+    xLine.appendChild(createNode("label", {htmlFor: sIdEnDash, className: "opt_lbl", textContent: "demi-cadratin (–)"}));
+    return xLine;
+}
+
+
+/*
+    Ligatures
+*/
+function createRadioBoxLigatures () {
+    let xLine = createNode("div", {className: "blockopt underline"});
+    xLine.appendChild(createOptionInputAndLabel("o_ts_ligature", true, "Ligatures"));
+    xLine.appendChild(createNode("input", {type: "radio", id: "o_ts_ligature_do", name: "liga", className:"option"}, {default: false}));
+    xLine.appendChild(createNode("label", {htmlFor: "o_ts_ligature_do", className: "opt_lbl", textContent: "faire"}));
+    xLine.appendChild(createNode("input", {type: "radio", id: "o_ts_ligature_undo", name: "liga", className:"option"}, {default: true}));
+    xLine.appendChild(createNode("label", {htmlFor: "o_ts_ligature_undo", className: "opt_lbl", textContent: "défaire"}));
+    return xLine;
+}
+
+function createLigaturesSelection () {
+    let xLine = createNode("div", {className: "blockopt"});
+    xLine.appendChild(createLigatureCheckboxAndLabel("o_ts_ligature_ff", "ff", true));
+    xLine.appendChild(createLigatureCheckboxAndLabel("o_ts_ligature_fi", "fi", true));
+    xLine.appendChild(createLigatureCheckboxAndLabel("o_ts_ligature_ffi", "ffi", true));
+    xLine.appendChild(createLigatureCheckboxAndLabel("o_ts_ligature_fl", "fl", true));
+    xLine.appendChild(createLigatureCheckboxAndLabel("o_ts_ligature_ffl", "ffl", true));
+    xLine.appendChild(createLigatureCheckboxAndLabel("o_ts_ligature_ft", "ft", true));
+    xLine.appendChild(createLigatureCheckboxAndLabel("o_ts_ligature_st", "st", false));
+    return xLine;
+}
+
+function createLigatureCheckboxAndLabel (sId, sLabel, bDefault) {
+    let xInlineBlock = createNode("div", {style: "display: inline-block;"});
+    xInlineBlock.appendChild(createNode("input", {type: "checkbox", id: sId, className: "option"}, {default: bDefault}));
+    xInlineBlock.appendChild(createNode("label", {htmlFor: sId, className: "opt_lbl", textContent: sLabel}));
+    return xInlineBlock;
+}
+
+let sTFinnerHTML = ' \
+<!-- Misc --> \
+    <div class="blockopt underline"> \
+      <div id="res_o_ordinals_no_exponant" class="result fright"></div> \
+      <input type="checkbox" id="o_ordinals_no_exponant" class="option" data-default="true" /> \
+      <label for="o_ordinals_no_exponant" class="opt_lbl reducedw" data-l10n-en="tf_ordinals_no_exponant">${tf_ordinals_no_exponant}</label> \
+      <div class="secondoption"> \
+        <input type="checkbox" id="o_ordinals_exponant" class="option" data-default="true" /> \
+        <label for="o_ordinals_exponant" class="opt_lbl smallw" data-l10n-en="tf_ordinals_exponant">${tf_ordinals_exponant}</label> \
+      </div> \
+    </div> \
+    <div class="blockopt"> \
+      <div class="inlineblock indent"> \
+        <input type="checkbox" id="o_ma_1letter_lowercase" class="option" /> \
+        <label for="o_ma_1letter_lowercase" class="opt_lbl" data-l10n-en="tf_ma_1letter_lowercase">${tf_ma_1letter_lowercase}</label> \
+      </div> \
+      <div class="inlineblock indent"> \
+        <input type="checkbox" id="o_ma_1letter_uppercase" class="option" /> \
+        <label for="o_ma_1letter_uppercase" class="opt_lbl" data-l10n-en="tf_ma_1letter_uppercase">${tf_ma_1letter_uppercase}</label> \
+      </div> \
+    </div> \
+';

ADDED   gc_lang/fr/webext/gce_sharedworker.js
Index: gc_lang/fr/webext/gce_sharedworker.js
==================================================================
--- /dev/null
+++ gc_lang/fr/webext/gce_sharedworker.js
@@ -0,0 +1,271 @@
+/*
+    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 SharedWorker [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”.
+*/
+
+
+/*
+    Message Event Object
+    https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent
+*/
+
+let xPort = null;
+
+function createResponse (sActionDone, result, dInfo, bError=false) {
+    return {
+        "sActionDone": sActionDone,
+        "result": result, // can be of any type
+        "dInfo": dInfo,
+        "bError": bError
+    };
+}
+
+function createErrorResult (e, sDescr="no description") {
+    return {
+        "sType": "error",
+        "sDescription": sDescr,
+        "sMessage": e.fileName + "\n" + e.name + "\nline: " + e.lineNumber + "\n" + e.message
+    };
+}
+
+function showData (e) {
+    for (let sParam in e) {
+        console.log(sParam);
+        console.log(e[sParam]);
+    }
+}
+
+onconnect = function (e) {
+    console.log("[Sharedworker] START CONNECTION");
+    xPort = e.ports[0];
+
+    xPort.onmessage = function (e) {
+        console.log("[Sharedworker] ONMESSAGE");
+        let {sCommand, dParam, dInfo} = e.data;
+        console.log(e.data);
+        switch (sCommand) {
+            case "init":
+                init(dParam.sExtensionPath, dParam.sOptions, dParam.sContext, dInfo);
+                break;
+            case "parse":
+                parse(dParam.sText, dParam.sCountry, dParam.bDebug, dParam.bContext, dInfo);
+                break;
+            case "parseAndSpellcheck":
+                parseAndSpellcheck(dParam.sText, dParam.sCountry, dParam.bDebug, dParam.bContext, dInfo);
+                break;
+            case "getOptions":
+                getOptions(dInfo);
+                break;
+            case "getDefaultOptions":
+                getDefaultOptions(dInfo);
+                break;
+            case "setOptions":
+                setOptions(dParam.sOptions, dInfo);
+                break;
+            case "setOption":
+                setOption(dParam.sOptName, dParam.bValue, dInfo);
+                break;
+            case "resetOptions":
+                resetOptions(dInfo);
+                break;
+            case "textToTest":
+                textToTest(dParam.sText, dParam.sCountry, dParam.bDebug, dParam.bContext, dInfo);
+                break;
+            case "fullTests":
+                fullTests('{"nbsp":true, "esp":true, "unit":true, "num":true}', dInfo);
+                break;
+            case "getListOfTokens":
+                getListOfTokens(dParam.sText, dInfo);
+                break;
+            default:
+                console.log("Unknown command: " + sCommand);
+                showData(e.data);
+        }
+    }
+    //xPort.start();
+}
+
+let bInitDone = false;
+
+let oDict = null;
+let oTokenizer = null;
+let oLxg = null;
+let oTest = null;
+
+
+function init (sExtensionPath, sGCOptions="", sContext="JavaScript", dInfo={}) {
+    try {
+        if (!bInitDone) {
+            console.log("[Sharedworker] 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("[Sharedworker] 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();
+            bInitDone = true;
+        } else {
+            console.log("[Sharedworker] Already initialized…")
+        }
+        // we always retrieve options from the gc_engine, for setOptions filters obsolete options
+        xPort.postMessage(createResponse("init", gc_engine.getOptions().gl_toString(), dInfo));
+    }
+    catch (e) {
+        helpers.logerror(e);
+        xPort.postMessage(createResponse("init", createErrorResult(e, "init failed"), dInfo, true));
+    }
+}
+
+function parse (sText, sCountry, bDebug, bContext, dInfo={}) {
+    let aGrammErr = gc_engine.parse(sText, sCountry, bDebug, bContext);
+    xPort.postMessage({sActionDone: "parse", result: aGrammErr, dInfo: dInfo});
+}
+
+function parseAndSpellcheck (sText, sCountry, bDebug, bContext, dInfo={}) {
+    let aGrammErr = gc_engine.parse(sText, sCountry, bDebug, bContext);
+    let aSpellErr = oTokenizer.getSpellingErrors(sText, oDict);
+    xPort.postMessage(createResponse("parseAndSpellcheck", {aGrammErr: aGrammErr, aSpellErr: aSpellErr}, dInfo));
+}
+
+function getOptions (dInfo={}) {
+    xPort.postMessage(createResponse("getOptions", gc_engine.getOptions().gl_toString(), dInfo));
+}
+
+function getDefaultOptions (dInfo={}) {
+    xPort.postMessage(createResponse("getDefaultOptions", gc_engine.getDefaultOptions().gl_toString(), dInfo));
+}
+
+function setOptions (sGCOptions, dInfo={}) {
+    gc_engine.setOptions(helpers.objectToMap(JSON.parse(sGCOptions)));
+    xPort.postMessage(createResponse("setOptions", gc_engine.getOptions().gl_toString(), dInfo));
+}
+
+function setOption (sOptName, bValue, dInfo={}) {
+    gc_engine.setOptions(new Map([ [sOptName, bValue] ]));
+    xPort.postMessage(createResponse("setOption", gc_engine.getOptions().gl_toString(), dInfo));
+}
+
+function resetOptions (dInfo={}) {
+    gc_engine.resetOptions();
+    xPort.postMessage(createResponse("resetOptions", gc_engine.getOptions().gl_toString(), dInfo));
+}
+
+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, dInfo={}) {
+    if (!gc_engine || !oDict) {
+        xPort.postMessage(createResponse("textToTest", "# Grammar checker or dictionary not loaded.", dInfo));
+        return;
+    }
+    let aGrammErr = gc_engine.parse(sText, sCountry, bDebug, bContext);
+    let sMsg = "";
+    for (let oErr of aGrammErr) {
+        sMsg += text.getReadableError(oErr) + "\n";
+    }
+    xPort.postMessage(createResponse("textToTest", sMsg, dInfo));
+}
+
+function fullTests (sGCOptions="", dInfo={}) {
+    if (!gc_engine || !oDict) {
+        xPort.postMessage(createResponse("fullTests", "# Grammar checker or dictionary not loaded.", dInfo));
+        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);
+    xPort.postMessage(createResponse("fullTests", sMsg, dInfo));
+}
+
+
+// Lexicographer
+
+function getListOfTokens (sText, dInfo={}) {
+    try {
+        let aElem = [];
+        let aRes = null;
+        for (let oToken of oTokenizer.genTokens(sText)) {
+            aRes = oLxg.getInfoForToken(oToken);
+            if (aRes) {
+                aElem.push(aRes);
+            }
+        }
+        xPort.postMessage(createResponse("getListOfTokens", aElem, dInfo));
+    }
+    catch (e) {
+        helpers.logerror(e);
+        xPort.postMessage(createResponse("getListOfTokens", createErrorResult(e, "no tokens"), dInfo, true));
+    }
+}

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,297 @@
+/*
+    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("[Worker] 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”.
+*/
+
+
+function createResponse (sActionDone, result, dInfo, bEnd, bError=false) {
+    return {
+        "sActionDone": sActionDone,
+        "result": result, // can be of any type
+        "dInfo": dInfo,
+        "bEnd": bEnd,
+        "bError": bError
+    };
+}
+
+function createErrorResult (e, sDescr="no description") {
+    return {
+        "sType": "error",
+        "sDescription": sDescr,
+        "sMessage": e.fileName + "\n" + e.name + "\nline: " + e.lineNumber + "\n" + e.message
+    };
+}
+
+function showData (e) {
+    for (let sParam in e) {
+        console.log(sParam);
+        console.log(e[sParam]);
+    }
+}
+
+
+/*
+    Message Event Object
+    https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent
+*/
+onmessage = function (e) {
+    console.log("[Worker] message received");
+    let {sCommand, dParam, dInfo} = e.data;
+    switch (sCommand) {
+        case "init":
+            init(dParam.sExtensionPath, dParam.sOptions, dParam.sContext, dInfo);
+            break;
+        case "parse":
+            parse(dParam.sText, dParam.sCountry, dParam.bDebug, dParam.bContext, dInfo);
+            break;
+        case "parseAndSpellcheck":
+            parseAndSpellcheck(dParam.sText, dParam.sCountry, dParam.bDebug, dParam.bContext, dInfo);
+            break;
+        case "parseAndSpellcheck1":
+            parseAndSpellcheck1(dParam.sText, dParam.sCountry, dParam.bDebug, dParam.bContext, dInfo);
+        case "getOptions":
+            getOptions(dInfo);
+            break;
+        case "getDefaultOptions":
+            getDefaultOptions(dInfo);
+            break;
+        case "setOptions":
+            setOptions(dParam.sOptions, dInfo);
+            break;
+        case "setOption":
+            setOption(dParam.sOptName, dParam.bValue, dInfo);
+            break;
+        case "resetOptions":
+            resetOptions(dInfo);
+            break;
+        case "textToTest":
+            textToTest(dParam.sText, dParam.sCountry, dParam.bDebug, dParam.bContext, dInfo);
+            break;
+        case "fullTests":
+            fullTests('{"nbsp":true, "esp":true, "unit":true, "num":true}', dInfo);
+            break;
+        case "getListOfTokens":
+            getListOfTokens(dParam.sText, dInfo);
+            break;
+        default:
+            console.log("Unknown command: " + sCommand);
+            showData(e.data);
+    }
+}
+
+
+
+let bInitDone = false;
+
+let oDict = null;
+let oTokenizer = null;
+let oLxg = null;
+let oTest = null;
 
 
 /*
-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;
-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 });
-}
-
-function getOptions () {
-    return gce.getOptions()._toString();
-}
-
-function getDefaultOptions () {
-    return gce.getDefaultOptions()._toString();
-}
-
-function setOptions (sGCOptions) {
-    gce.setOptions(helpers.objectToMap(JSON.parse(sGCOptions)));
-    return gce.getOptions()._toString();
-}
-
-function setOption (sOptName, bValue) {
-    gce.setOptions(new Map([ [sOptName, bValue] ]));
-    return gce.getOptions()._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;
-}
+    Technical note:
+    This worker don’t work as a PromiseWorker (which returns a promise),  so when we send request
+    to this worker, we can’t wait the return of the answer just after the request made.
+    The answer is received by the background in another function (onmessage).
+    That’s why the full text to analyze is send in one block, but analyse is returned paragraph
+    by paragraph.
+*/
+
+function init (sExtensionPath, sGCOptions="", sContext="JavaScript", dInfo={}) {
+    try {
+        if (!bInitDone) {
+            console.log("[Worker] 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("[Worker] 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();
+            bInitDone = true;
+        } else {
+            console.log("[Worker] Already initialized…")
+        }
+        // we always retrieve options from the gc_engine, for setOptions filters obsolete options
+        postMessage(createResponse("init", gc_engine.getOptions().gl_toString(), dInfo, true));
+    }
+    catch (e) {
+        helpers.logerror(e);
+        postMessage(createResponse("init", createErrorResult(e, "init failed"), dInfo, true, true));
+    }
+}
+
+
+function parse (sText, sCountry, bDebug, bContext, dInfo={}) {
+    for (let sParagraph of text.getParagraph(sText)) {
+        let aGrammErr = gc_engine.parse(sParagraph, sCountry, bDebug, bContext);
+        postMessage(createResponse("parse", aGrammErr, dInfo, false));
+    }
+    postMessage(createResponse("parse", null, dInfo, true));
+}
+
+function parseAndSpellcheck (sText, sCountry, bDebug, bContext, dInfo={}) {
+    let n = 0;
+    for (let sParagraph of text.getParagraph(sText)) {
+        let aGrammErr = gc_engine.parse(sParagraph, sCountry, bDebug, bContext);
+        let aSpellErr = oTokenizer.getSpellingErrors(sParagraph, oDict);
+        n += 1;
+        postMessage(createResponse("parseAndSpellcheck", {sParagraph: sParagraph, sParaNum: n.toString(), aGrammErr: aGrammErr, aSpellErr: aSpellErr}, dInfo, false));
+    }
+    postMessage(createResponse("parseAndSpellcheck", null, dInfo, true));
+}
+
+function parseAndSpellcheck1 (sParagraph, sCountry, bDebug, bContext, dInfo={}) {
+    let aGrammErr = gc_engine.parse(sParagraph, sCountry, bDebug, bContext);
+    let aSpellErr = oTokenizer.getSpellingErrors(sParagraph, oDict);
+    postMessage(createResponse("parseAndSpellcheck1", {sParagraph: sParagraph, aGrammErr: aGrammErr, aSpellErr: aSpellErr}, dInfo, true));
+}
+
+function getOptions (dInfo={}) {
+    postMessage(createResponse("getOptions", gc_engine.getOptions().gl_toString(), dInfo, true));
+}
+
+function getDefaultOptions (dInfo={}) {
+    postMessage(createResponse("getDefaultOptions", gc_engine.getDefaultOptions().gl_toString(), dInfo, true));
+}
+
+function setOptions (sGCOptions, dInfo={}) {
+    gc_engine.setOptions(helpers.objectToMap(JSON.parse(sGCOptions)));
+    postMessage(createResponse("setOptions", gc_engine.getOptions().gl_toString(), dInfo, true));
+}
+
+function setOption (sOptName, bValue, dInfo={}) {
+    gc_engine.setOptions(new Map([ [sOptName, bValue] ]));
+    postMessage(createResponse("setOption", gc_engine.getOptions().gl_toString(), dInfo, true));
+}
+
+function resetOptions (dInfo={}) {
+    gc_engine.resetOptions();
+    postMessage(createResponse("resetOptions", gc_engine.getOptions().gl_toString(), dInfo, true));
+}
+
+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, dInfo={}) {
+    if (!gc_engine || !oDict) {
+        postMessage(createResponse("textToTest", "# Grammar checker or dictionary not loaded.", dInfo, true));
+        return;
+    }
+    let aGrammErr = gc_engine.parse(sText, sCountry, bDebug, bContext);
+    let sMsg = "";
+    for (let oErr of aGrammErr) {
+        sMsg += text.getReadableError(oErr) + "\n";
+    }
+    postMessage(createResponse("textToTest", sMsg, dInfo, true));
+}
+
+function fullTests (sGCOptions="", dInfo={}) {
+    if (!gc_engine || !oDict) {
+        postMessage(createResponse("fullTests", "# Grammar checker or dictionary not loaded.", dInfo, true));
+        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(createResponse("fullTests", sMsg, dInfo, true));
+}
+
 
 
 // Lexicographer
 
-function getListOfElements (sText) {
+function getListOfTokens (sText, dInfo={}) {
     try {
-        let aElem = [];
-        let aRes = null;
-        for (let oToken of oTokenizer.genTokens(sText)) {
-            aRes = oLxg.getInfoForToken(oToken);
-            if (aRes) {
-                aElem.push(aRes);
+        for (let sParagraph of text.getParagraph(sText)) {
+            if (sParagraph.trim() !== "") {
+                let aElem = [];
+                let aRes = null;
+                for (let oToken of oTokenizer.genTokens(sParagraph)) {
+                    aRes = oLxg.getInfoForToken(oToken);
+                    if (aRes) {
+                        aElem.push(aRes);
+                    }
+                }
+                postMessage(createResponse("getListOfTokens", aElem, dInfo, false));
             }
         }
-        return JSON.stringify(aElem);
+        postMessage(createResponse("getListOfTokens", null, dInfo, true));
     }
     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(createResponse("getListOfTokens", createErrorResult(e, "no tokens"), dInfo, true, true));
+    }
+}

ADDED   gc_lang/fr/webext/img/lines.png
Index: gc_lang/fr/webext/img/lines.png
==================================================================
--- /dev/null
+++ gc_lang/fr/webext/img/lines.png
cannot compute difference between binary files

ADDED   gc_lang/fr/webext/img/logo-80.png
Index: gc_lang/fr/webext/img/logo-80.png
==================================================================
--- /dev/null
+++ gc_lang/fr/webext/img/logo-80.png
cannot compute difference between binary files

Index: gc_lang/fr/webext/manifest.json
==================================================================
--- gc_lang/fr/webext/manifest.json
+++ gc_lang/fr/webext/manifest.json
@@ -5,39 +5,71 @@
   "version": "0.6",
 
   "applications": {
     "gecko": {
       "id": "French-GC@grammalecte.net",
-      "strict_min_version": "54.0"
+      "strict_min_version": "55.0"
     }
   },
 
   "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",
              "48": "img/logo-48.png",
              "64": "img/logo-64.png",
+             "80": "img/logo-80.png",
              "96": "img/logo-96.png" },
 
   "browser_action": {
     "default_icon": "img/logo-32.png",
     "default_popup": "panel/main.html",
     "default_title": "Grammalecte [fr]",
-    "browser_style": false
+    "browser_style": true
   },
   "background": {
-    "scripts": ["require.js", "grammalecte/helpers.js", "gce_worker.js"]
+    "scripts": [
+      "background.js"
+    ]
   },
+  "content_scripts": [
+    {
+      "matches": ["<all_urls>"],
+      "css": [
+        "content_scripts/content_panels.css",
+        "content_scripts/tf_content.css",
+        "content_scripts/gc_content.css",
+        "content_scripts/lxg_content.css"
+      ],
+      "js": [
+        "content_scripts/panel_creator.js",
+        "content_scripts/tf_content.js",
+        "content_scripts/gc_content.js",
+        "content_scripts/lxg_content.js",
+        "content_scripts/content_modifier.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",
+    "img/logo-16.png"
   ],
   "permissions": [
-    "activeTab"
-  ]
+    "activeTab",
+    "contextMenus"
+  ],
+  "chrome_settings_overrides": {
+    "search_provider": {
+      "name": "Grammalecte",
+      "search_url": "https://www.dicollecte.org/dictionary.php?prj=fr&lemma={searchTerms}",
+      "keyword": "disc",
+      "favicon_url": "https://www.dicollecte.org/favicon.ico"
+    }
+  }
 }

ADDED   gc_lang/fr/webext/panel/conjugueur.css
Index: gc_lang/fr/webext/panel/conjugueur.css
==================================================================
--- /dev/null
+++ gc_lang/fr/webext/panel/conjugueur.css
@@ -0,0 +1,157 @@
+/*
+   CSS Document
+   White
+   Design par Olivier R.
+*/
+
+* { margin: 0; padding: 0; }
+img { border: none; }
+
+
+/* Generic classes */
+
+.fleft {
+    float: left;
+}
+.fright {
+    float: right;
+}
+.center {
+    text-align: center;
+}
+.right {
+    text-align: right;
+}
+.left {
+    text-align: left;
+}
+.justify {
+    text-align: justify;
+}
+.hidden {
+    display: none;
+}
+
+.clearer { 
+    clear: both;
+    font-size: 0;
+    height: 0;
+}
+
+body {
+    background: #FFFFFF url(../img/lines.png);
+    font: normal 16px Tahoma, "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", sans-serif;
+    color: #505050;
+    /*text-rendering: optimizeLegibility;*/
+}
+
+.mainflow {
+    width: 600px;
+    margin: 0 auto 0 auto;
+}
+
+.logo {
+    position: absolute;
+    background-color: #FFFFFF;
+    border: 3px solid #F0F0F0;
+    border-radius: 96px;
+    padding: 4px 4px 0 4px;
+}
+
+
+/* MAIN ====================================================================== */
+
+#main .inbox {
+    width: 600px;
+    margin: 20px auto 10px auto;
+    padding: 10px 30px 30px 30px;
+    background: #FFFFFF;
+    border: 2px solid #F0F0F0;
+    border-radius: 20px;
+}
+
+#main h1 {
+    margin: 5px 0 2px 0;
+    color: hsl(210, 50%, 50%);
+    font: bold 30px Tahoma, "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", sans-serif;
+}
+#main h2 {
+    margin: 5px 0 2px 0;
+    color: hsl(0, 50%, 50%);
+    font: bold 16px Tahoma, "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", sans-serif;
+}
+#main h3 {
+    margin: 5px 0 2px 0;
+    color: hsl(210, 50%, 50%);
+    font: bold 14px Tahoma, "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", sans-serif;
+}
+
+#main .container {
+    display: flex;
+    flex-direction: row;
+    flex-wrap: nowrap;
+    justify-content: center;
+    align-items: flex-start;
+}
+
+#main .colonne {
+    width: 280px;
+    padding: 0 10px;
+}
+
+.colonne p {
+    font-size: 12px;
+}
+
+/*.box {
+    border-left: 2px solid #8BC5EF;
+    border-bottom: 2px solid #8BC5EF;
+    border-radius: 5px;
+    padding: 0 0 2px 10px;
+}*/
+
+input#verb {
+    display: inline-block;
+    width: 230px;
+    margin-left: 5px;
+    padding: 5px 10px;
+    border: 2px solid hsl(0, 0%, 80%);
+    border-radius: 3px;
+    height: 24px;
+    background: transparent;
+    font: normal 20px Tahoma, "Ubuntu Condensed";
+    color: hsl(0, 0%, 30%);
+}
+input[placeholder]#verb {
+    color: hsl(0, 0%, 70%);
+}
+
+a#conjugate {
+    display: inline-block;
+    padding: 7px 10px;
+    font-size: 20px;
+    background-color: hsl(0, 30%, 30%);
+    color: hsl(0, 30%, 60%);
+    border-radius: 3px;
+    text-transform: uppercase;
+    text-align: center;
+    text-decoration: none;
+}
+a#conjugate:hover {
+    background-color: hsl(0, 60%, 40%);
+    color: hsl(0, 60%, 70%);
+    box-shadow: 0 0 2px hsl(0, 60%, 50%);
+}
+
+#options {
+    margin: 10px 0 0 5px;
+    font-size: 16px;
+    text-align: center;
+}
+
+#smallnote {
+    margin: 10px 0 10px 0;
+    font-size: 11px;
+    color: hsl(0, 0%, 60%);
+    text-align: center;
+}

ADDED   gc_lang/fr/webext/panel/conjugueur.html
Index: gc_lang/fr/webext/panel/conjugueur.html
==================================================================
--- /dev/null
+++ gc_lang/fr/webext/panel/conjugueur.html
@@ -0,0 +1,175 @@
+<!DOCTYPE HTML>
+<html>
+    <head>
+        <link rel="stylesheet" type="text/css" href="conjugueur.css" />
+        <title>Grammalecte · Conjugueur</title>
+        <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
+    </head>
+    
+    <body>
+        <header>
+            <div class="mainflow">
+                <div class="logo" style="margin: -10px 0 0 0;">
+                    <img src="../img/logo-96.png" alt="" />
+                </div>
+            </div>
+        </header>
+
+        <div id="main">
+            <div class="inbox">
+
+                <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>
+                <p>
+
+                <h1 id="verb_title" class="center">&nbsp;</h1>
+                <p id="info" class="center">&nbsp;</p>
+
+                <p id="options">
+                    <label for="oneg">Négative</label> <input type="checkbox" id="oneg" name="oneg" value="ON"  /> 
+                    · <label for="oint">Interrogative</label> <input type="checkbox" id="oint" name="oint" value="ON"  />
+                    · <label for="ofem">Féminin</label> <input type="checkbox" id="ofem" name="ofem" value="ON"  />
+                    · <label id="opro_lbl" for="opro">Pronominal</label> <input type="checkbox" id="opro" name="opro" value="ON"  />
+                    · <label id="otco_lbl" for="otco">Temps composés</label> <input type="checkbox" id="otco" name="otco" value="ON"  />
+                </p>
+                <p id="smallnote">❦</p>
+                    
+                <div class="clearer"></div>
+
+                <!-- section 1 -->
+                <div class="container">
+                    <div class="colonne">
+                        <div id="infinitif" class="box">
+                            <h2 id="infinitif_title">Infinitif</h2>
+                            <p id="infi">&nbsp;</p>
+                        </div>
+                        <div id="imperatif" class="box">
+                            <h2 id="imperatif_title">Impératif</h2>
+                            <h3 id="impe_temps">Présent</h3>
+                            <p id="impe1">&nbsp;</p>
+                            <p id="impe2">&nbsp;</p>
+                            <p id="impe3">&nbsp;</p>
+                        </div>
+                    </div>
+                    
+                    <div class="colonne">
+                        <div id="partpre" class="box">
+                            <h2 id="partpre_title">Participe présent</h2>
+                            <p id="ppre">&nbsp;</p>
+                        </div>
+                        <div id="partpas" class="box">
+                            <h2 id="partpas_title">Participes passés</h2>
+                            <p id="ppas1">&nbsp;</p>
+                            <p id="ppas2">&nbsp;</p>
+                            <p id="ppas3">&nbsp;</p>
+                            <p id="ppas4">&nbsp;</p>
+                        </div>
+                    </div>
+                </div>
+
+                <div class="clearer"></div>
+
+                <!-- section 2 -->
+                <div class="container">
+                    <div class="colonne">
+                        <div id="indicatif" class="box">
+                            <h2 id="indicatif_title">Indicatif</h2>
+                            <div id="ipre">
+                                <h3 id="ipre_temps">Présent</h3>
+                                <p id="ipre1">&nbsp;</p>
+                                <p id="ipre2">&nbsp;</p>
+                                <p id="ipre3">&nbsp;</p>
+                                <p id="ipre4">&nbsp;</p>
+                                <p id="ipre5">&nbsp;</p>
+                                <p id="ipre6">&nbsp;</p>
+                            </div>
+                            <div id="iimp">
+                                <h3 id="iimp_temps">Imparfait</h3>
+                                <p id="iimp1">&nbsp;</p>
+                                <p id="iimp2">&nbsp;</p>
+                                <p id="iimp3">&nbsp;</p>
+                                <p id="iimp4">&nbsp;</p>
+                                <p id="iimp5">&nbsp;</p>
+                                <p id="iimp6">&nbsp;</p>
+                            </div>
+                            <div id="ipsi">
+                                <h3 id="ipsi_temps">Passé simple</h3>
+                                <p id="ipsi1">&nbsp;</p>
+                                <p id="ipsi2">&nbsp;</p>
+                                <p id="ipsi3">&nbsp;</p>
+                                <p id="ipsi4">&nbsp;</p>
+                                <p id="ipsi5">&nbsp;</p>
+                                <p id="ipsi6">&nbsp;</p>
+                            </div>
+                            <div id="ifut">
+                                <h3 id="ifut_temps">Futur</h3>
+                                <p id="ifut1">&nbsp;</p>
+                                <p id="ifut2">&nbsp;</p>
+                                <p id="ifut3">&nbsp;</p>
+                                <p id="ifut4">&nbsp;</p>
+                                <p id="ifut5">&nbsp;</p>
+                                <p id="ifut6">&nbsp;</p>
+                            </div>
+                        </div>
+                    </div>
+                    
+                    <div class="colonne">
+                        <div id="subjonctif" class="box">
+                            <h2 id="subjontif_title">Subjonctif</h2>
+                            <div id="spre">
+                                <h3 id="spre_temps">Présent</h3>
+                                <p id="spre1">&nbsp;</p>
+                                <p id="spre2">&nbsp;</p>
+                                <p id="spre3">&nbsp;</p>
+                                <p id="spre4">&nbsp;</p>
+                                <p id="spre5">&nbsp;</p>
+                                <p id="spre6">&nbsp;</p>
+                            </div>
+                            <div id="simp">
+                                <h3 id="simp_temps">Imparfait</h3>
+                                <p id="simp1">&nbsp;</p>
+                                <p id="simp2">&nbsp;</p>
+                                <p id="simp3">&nbsp;</p>
+                                <p id="simp4">&nbsp;</p>
+                                <p id="simp5">&nbsp;</p>
+                                <p id="simp6">&nbsp;</p>
+                            </div>
+                        </div>
+                        <div id="conditionnel" class="box">
+                            <h2 id="conditionnel_title">Conditionnel</h2>
+                            <div id="conda">
+                                <h3 id="conda_temps">Présent</h3>
+                                <p id="conda1">&nbsp;</p>
+                                <p id="conda2">&nbsp;</p>
+                                <p id="conda3">&nbsp;</p>
+                                <p id="conda4">&nbsp;</p>
+                                <p id="conda5">&nbsp;</p>
+                                <p id="conda6">&nbsp;</p>
+                            </div>
+                            <div id="condb">
+                                <h3 id="condb_temps">&nbsp;</h3>
+                                <p id="condb1">&nbsp;</p>
+                                <p id="condb2">&nbsp;</p>
+                                <p id="condb3">&nbsp;</p>
+                                <p id="condb4">&nbsp;</p>
+                                <p id="condb5">&nbsp;</p>
+                                <p id="condb6">&nbsp;</p>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+
+                <div class="clearer"></div>
+
+            </div>
+
+
+        </div>
+
+        <script src="../grammalecte/helpers.js"></script>
+        <script src="../grammalecte/fr/conj.js"></script>
+        <script src="conjugueur.js"></script>
+    </body>
+    
+</html>

ADDED   gc_lang/fr/webext/panel/conjugueur.js
Index: gc_lang/fr/webext/panel/conjugueur.js
==================================================================
--- /dev/null
+++ gc_lang/fr/webext/panel/conjugueur.js
@@ -0,0 +1,255 @@
+// JavaScript
+
+let oVerb = null;
+
+// button
+document.getElementById('conjugate').addEventListener("click", function (event) {
+    createVerbAndConjugate(document.getElementById('verb').value);
+});
+
+// text field
+document.getElementById('verb').addEventListener("change", function (event) {
+    createVerbAndConjugate(document.getElementById('verb').value);
+});
+
+// options
+document.getElementById('oneg').addEventListener("click", function (event) {
+    _displayResults();
+});
+document.getElementById('opro').addEventListener("click", function (event) {
+    _displayResults();
+});
+document.getElementById('oint').addEventListener("click", function (event) {
+    _displayResults();
+});
+document.getElementById('ofem').addEventListener("click", function (event) {
+    _displayResults();
+});
+document.getElementById('otco').addEventListener("click", function (event) {
+    _displayResults();
+});
+
+function createVerbAndConjugate (sVerb) {
+    try {
+        document.getElementById('oneg').checked = false;
+        document.getElementById('opro').checked = false;
+        document.getElementById('oint').checked = false;
+        document.getElementById('otco').checked = false;
+        document.getElementById('ofem').checked = false;
+
+        // request analyzing
+        sVerb = sVerb.trim().toLowerCase().replace(/’/g, "'").replace(/  +/g, " ");
+        if (sVerb) {
+            if (sVerb.startsWith("ne pas ")) {
+                document.getElementById('oneg').checked = true;
+                sVerb = sVerb.slice(7);
+            }
+            if (sVerb.startsWith("se ")) {
+                document.getElementById('opro').checked = true;
+                sVerb = sVerb.slice(3);
+            } else if (sVerb.startsWith("s'")) {
+                document.getElementById('opro').checked = true;
+                sVerb = sVerb.slice(2);
+            }
+            if (sVerb.endsWith("?")) {
+                document.getElementById('oint').checked = true;
+                sVerb = sVerb.slice(0,-1).trim();
+            }
+
+            if (!conj.isVerb(sVerb)) {
+                document.getElementById('verb').style = "color: #BB4411;";
+            } else {
+                document.getElementById('verb_title').textContent = sVerb;
+                document.getElementById('verb').style = "color: #999999;";
+                document.getElementById('verb').value = "";
+                oVerb = new Verb(sVerb);
+                let sRawInfo = oVerb._sRawInfo;
+                document.getElementById('info').textContent = oVerb.sInfo;
+                document.getElementById('opro').textContent = "pronominal";
+                if (sRawInfo.endsWith("zz")) {
+                    document.getElementById('opro').checked = false;
+                    document.getElementById('opro').disabled = true;
+                    document.getElementById('opro_lbl').style = "color: #CCC;";
+                    document.getElementById('otco').checked = false;
+                    document.getElementById('otco').disabled = true;
+                    document.getElementById('otco_lbl').style = "color: #CCC;";
+                    document.getElementById('smallnote').textContent = "Ce verbe n’a pas encore été vérifié. C’est pourquoi les options “pronominal” et “temps composés” sont désactivées.";
+                } else {
+                    document.getElementById('smallnote').textContent = "❦";
+                    if (sRawInfo[5] == "_") {
+                        document.getElementById('opro').checked = false;
+                        document.getElementById('opro').disabled = true;
+                        document.getElementById('opro_lbl').style = "color: #CCC;";
+                    } else if (["q", "u", "v", "e"].includes(sRawInfo[5])) {
+                        document.getElementById('opro').checked = false;
+                        document.getElementById('opro').disabled = false;
+                        document.getElementById('opro_lbl').style = "color: #000;";
+                    } else if (sRawInfo[5] == "p" || sRawInfo[5] == "r") {
+                        document.getElementById('opro').checked = true;
+                        document.getElementById('opro').disabled = true;
+                        document.getElementById('opro_lbl').style = "color: #CCC;";
+                    } else if (sRawInfo[5] == "x") {
+                        document.getElementById('opro').textContent = "cas particuliers";
+                        document.getElementById('opro').checked = false;
+                        document.getElementById('opro').disabled = true;
+                        document.getElementById('opro_lbl').style = "color: #CCC;";
+                    } else {
+                        document.getElementById('opro').textContent = "# erreur #";
+                        document.getElementById('opro').checked = false;
+                        document.getElementById('opro').disabled = true;
+                        document.getElementById('opro_lbl').style = "color: #CCC;";
+                    }
+                    document.getElementById('otco').disabled = false;
+                    document.getElementById('otco_lbl').style = "color: #000;";
+                }
+                _displayResults();
+            }
+        }
+    }
+    catch (e) {
+        console.error(e.fileName + "\n" + e.name + "\nline: " + e.lineNumber + "\n" + e.message);
+    }
+}
+
+function _displayResults () {
+    if (oVerb === null) {
+        return;
+    }
+    try {
+        let opro = document.getElementById('opro').checked;
+        let oneg = document.getElementById('oneg').checked;
+        let otco = document.getElementById('otco').checked;
+        let oint = document.getElementById('oint').checked;
+        let ofem = document.getElementById('ofem').checked;
+        // titles
+        _setTitles()
+        // participes passés
+        document.getElementById('ppas1').textContent = oVerb.participePasse(":Q1") || " "; // something or nbsp
+        document.getElementById('ppas2').textContent = oVerb.participePasse(":Q2") || " ";
+        document.getElementById('ppas3').textContent = oVerb.participePasse(":Q3") || " ";
+        document.getElementById('ppas4').textContent = oVerb.participePasse(":Q4") || " ";
+        // infinitif
+        document.getElementById('infi').textContent = oVerb.infinitif(opro, oneg, otco, oint, ofem);
+        // participe présent
+        document.getElementById('ppre').textContent = oVerb.participePresent(opro, oneg, otco, oint, ofem) || " ";
+        // conjugaisons
+        document.getElementById('ipre1').textContent = oVerb.conjugue(":Ip", ":1s", opro, oneg, otco, oint, ofem) || " ";
+        document.getElementById('ipre2').textContent = oVerb.conjugue(":Ip", ":2s", opro, oneg, otco, oint, ofem) || " ";
+        document.getElementById('ipre3').textContent = oVerb.conjugue(":Ip", ":3s", opro, oneg, otco, oint, ofem) || " ";
+        document.getElementById('ipre4').textContent = oVerb.conjugue(":Ip", ":1p", opro, oneg, otco, oint, ofem) || " ";
+        document.getElementById('ipre5').textContent = oVerb.conjugue(":Ip", ":2p", opro, oneg, otco, oint, ofem) || " ";
+        document.getElementById('ipre6').textContent = oVerb.conjugue(":Ip", ":3p", opro, oneg, otco, oint, ofem) || " ";
+        document.getElementById('iimp1').textContent = oVerb.conjugue(":Iq", ":1s", opro, oneg, otco, oint, ofem) || " ";
+        document.getElementById('iimp2').textContent = oVerb.conjugue(":Iq", ":2s", opro, oneg, otco, oint, ofem) || " ";
+        document.getElementById('iimp3').textContent = oVerb.conjugue(":Iq", ":3s", opro, oneg, otco, oint, ofem) || " ";
+        document.getElementById('iimp4').textContent = oVerb.conjugue(":Iq", ":1p", opro, oneg, otco, oint, ofem) || " ";
+        document.getElementById('iimp5').textContent = oVerb.conjugue(":Iq", ":2p", opro, oneg, otco, oint, ofem) || " ";
+        document.getElementById('iimp6').textContent = oVerb.conjugue(":Iq", ":3p", opro, oneg, otco, oint, ofem) || " ";
+        document.getElementById('ipsi1').textContent = oVerb.conjugue(":Is", ":1s", opro, oneg, otco, oint, ofem) || " ";
+        document.getElementById('ipsi2').textContent = oVerb.conjugue(":Is", ":2s", opro, oneg, otco, oint, ofem) || " ";
+        document.getElementById('ipsi3').textContent = oVerb.conjugue(":Is", ":3s", opro, oneg, otco, oint, ofem) || " ";
+        document.getElementById('ipsi4').textContent = oVerb.conjugue(":Is", ":1p", opro, oneg, otco, oint, ofem) || " ";
+        document.getElementById('ipsi5').textContent = oVerb.conjugue(":Is", ":2p", opro, oneg, otco, oint, ofem) || " ";
+        document.getElementById('ipsi6').textContent = oVerb.conjugue(":Is", ":3p", opro, oneg, otco, oint, ofem) || " ";
+        document.getElementById('ifut1').textContent = oVerb.conjugue(":If", ":1s", opro, oneg, otco, oint, ofem) || " ";
+        document.getElementById('ifut2').textContent = oVerb.conjugue(":If", ":2s", opro, oneg, otco, oint, ofem) || " ";
+        document.getElementById('ifut3').textContent = oVerb.conjugue(":If", ":3s", opro, oneg, otco, oint, ofem) || " ";
+        document.getElementById('ifut4').textContent = oVerb.conjugue(":If", ":1p", opro, oneg, otco, oint, ofem) || " ";
+        document.getElementById('ifut5').textContent = oVerb.conjugue(":If", ":2p", opro, oneg, otco, oint, ofem) || " ";
+        document.getElementById('ifut6').textContent = oVerb.conjugue(":If", ":3p", opro, oneg, otco, oint, ofem) || " ";
+        document.getElementById('conda1').textContent = oVerb.conjugue(":K", ":1s", opro, oneg, otco, oint, ofem) || " ";
+        document.getElementById('conda2').textContent = oVerb.conjugue(":K", ":2s", opro, oneg, otco, oint, ofem) || " ";
+        document.getElementById('conda3').textContent = oVerb.conjugue(":K", ":3s", opro, oneg, otco, oint, ofem) || " ";
+        document.getElementById('conda4').textContent = oVerb.conjugue(":K", ":1p", opro, oneg, otco, oint, ofem) || " ";
+        document.getElementById('conda5').textContent = oVerb.conjugue(":K", ":2p", opro, oneg, otco, oint, ofem) || " ";
+        document.getElementById('conda6').textContent = oVerb.conjugue(":K", ":3p", opro, oneg, otco, oint, ofem) || " ";
+        if (!oint) {
+            document.getElementById('spre1').textContent = oVerb.conjugue(":Sp", ":1s", opro, oneg, otco, oint, ofem) || " ";
+            document.getElementById('spre2').textContent = oVerb.conjugue(":Sp", ":2s", opro, oneg, otco, oint, ofem) || " ";
+            document.getElementById('spre3').textContent = oVerb.conjugue(":Sp", ":3s", opro, oneg, otco, oint, ofem) || " ";
+            document.getElementById('spre4').textContent = oVerb.conjugue(":Sp", ":1p", opro, oneg, otco, oint, ofem) || " ";
+            document.getElementById('spre5').textContent = oVerb.conjugue(":Sp", ":2p", opro, oneg, otco, oint, ofem) || " ";
+            document.getElementById('spre6').textContent = oVerb.conjugue(":Sp", ":3p", opro, oneg, otco, oint, ofem) || " ";
+            document.getElementById('simp1').textContent = oVerb.conjugue(":Sq", ":1s", opro, oneg, otco, oint, ofem) || " ";
+            document.getElementById('simp2').textContent = oVerb.conjugue(":Sq", ":2s", opro, oneg, otco, oint, ofem) || " ";
+            document.getElementById('simp3').textContent = oVerb.conjugue(":Sq", ":3s", opro, oneg, otco, oint, ofem) || " ";
+            document.getElementById('simp4').textContent = oVerb.conjugue(":Sq", ":1p", opro, oneg, otco, oint, ofem) || " ";
+            document.getElementById('simp5').textContent = oVerb.conjugue(":Sq", ":2p", opro, oneg, otco, oint, ofem) || " ";
+            document.getElementById('simp6').textContent = oVerb.conjugue(":Sq", ":3p", opro, oneg, otco, oint, ofem) || " ";
+            document.getElementById('impe1').textContent = oVerb.imperatif(":2s", opro, oneg, otco, ofem) || " ";
+            document.getElementById('impe2').textContent = oVerb.imperatif(":1p", opro, oneg, otco, ofem) || " ";
+            document.getElementById('impe3').textContent = oVerb.imperatif(":2p", opro, oneg, otco, ofem) || " ";
+        } else {
+            document.getElementById('spre_temps').textContent = " ";
+            document.getElementById('spre1').textContent = " ";
+            document.getElementById('spre2').textContent = " ";
+            document.getElementById('spre3').textContent = " ";
+            document.getElementById('spre4').textContent = " ";
+            document.getElementById('spre5').textContent = " ";
+            document.getElementById('spre6').textContent = " ";
+            document.getElementById('simp_temps').textContent = " ";
+            document.getElementById('simp1').textContent = " ";
+            document.getElementById('simp2').textContent = " ";
+            document.getElementById('simp3').textContent = " ";
+            document.getElementById('simp4').textContent = " ";
+            document.getElementById('simp5').textContent = " ";
+            document.getElementById('simp6').textContent = " ";
+            document.getElementById('impe_temps').textContent = " ";
+            document.getElementById('impe1').textContent = " ";
+            document.getElementById('impe2').textContent = " ";
+            document.getElementById('impe3').textContent = " ";
+        }
+        if (otco) {
+            document.getElementById('condb1').textContent = oVerb.conjugue(":Sq", ":1s", opro, oneg, otco, oint, ofem) || " ";
+            document.getElementById('condb2').textContent = oVerb.conjugue(":Sq", ":2s", opro, oneg, otco, oint, ofem) || " ";
+            document.getElementById('condb3').textContent = oVerb.conjugue(":Sq", ":3s", opro, oneg, otco, oint, ofem) || " ";
+            document.getElementById('condb4').textContent = oVerb.conjugue(":Sq", ":1p", opro, oneg, otco, oint, ofem) || " ";
+            document.getElementById('condb5').textContent = oVerb.conjugue(":Sq", ":2p", opro, oneg, otco, oint, ofem) || " ";
+            document.getElementById('condb6').textContent = oVerb.conjugue(":Sq", ":3p", opro, oneg, otco, oint, ofem) || " ";
+        } else {
+            document.getElementById('condb1').textContent = " ";
+            document.getElementById('condb2').textContent = " ";
+            document.getElementById('condb3').textContent = " ";
+            document.getElementById('condb4').textContent = " ";
+            document.getElementById('condb5').textContent = " ";
+            document.getElementById('condb6').textContent = " ";
+        }
+        document.getElementById('verb').Text = "";
+    }
+    catch (e) {
+        console.error(e.fileName + "\n" + e.name + "\nline: " + e.lineNumber + "\n" + e.message);
+    }
+}
+
+function _setTitles () {
+    try {
+        if (!document.getElementById('otco').checked) {
+            document.getElementById('ipre_temps').textContent = "Présent";
+            document.getElementById('ifut_temps').textContent = "Futur";
+            document.getElementById('iimp_temps').textContent = "Imparfait";
+            document.getElementById('ipsi_temps').textContent = "Passé simple";
+            document.getElementById('spre_temps').textContent = "Présent";
+            document.getElementById('simp_temps').textContent = "Imparfait";
+            document.getElementById('conda_temps').textContent = "Présent";
+            document.getElementById('condb_temps').textContent = " ";
+            document.getElementById('impe_temps').textContent = "Présent";
+        } else {
+            document.getElementById('ipre_temps').textContent = "Passé composé";
+            document.getElementById('ifut_temps').textContent = "Futur antérieur";
+            document.getElementById('iimp_temps').textContent = "Plus-que-parfait";
+            document.getElementById('ipsi_temps').textContent = "Passé antérieur";
+            document.getElementById('spre_temps').textContent = "Passé";
+            document.getElementById('simp_temps').textContent = "Plus-que-parfait";
+            document.getElementById('conda_temps').textContent = "Passé (1ʳᵉ forme)";
+            document.getElementById('condb_temps').textContent = "Passé (2ᵉ forme)";
+            document.getElementById('impe_temps').textContent = "Passé";
+        }
+    }
+    catch (e) {
+        console.error(e.fileName + "\n" + e.name + "\nline: " + e.lineNumber + "\n" + e.message);
+    }
+}
+
+createVerbAndConjugate("être");
+
+document.getElementById("verb").focus();

ADDED   gc_lang/fr/webext/panel/lexicographer.css
Index: gc_lang/fr/webext/panel/lexicographer.css
==================================================================
--- /dev/null
+++ gc_lang/fr/webext/panel/lexicographer.css
@@ -0,0 +1,95 @@
+/* lxgpanel.css */
+
+@import url("common.css");
+
+header {
+    background-color: hsl(0, 0%, 96%);
+    padding: 10px 20px;
+    border-bottom: 1px solid hsl(0, 0%, 90%);
+    color: hsl(0, 0%, 0%);
+}
+
+
+body {
+    background-color: hsl(0, 0%, 98%);
+    font-family: "Trebuchet MS", sans-serif;
+    overflow-x: hidden;
+    color: hsl(0, 0%, 0%);
+}
+
+
+
+#tokens_list {
+    padding: 50px 10px 10px 10px;
+}
+
+#tokens_list .paragraph {
+    background-color: hsla(0, 0%, 90%, 1);
+    padding: 10px;
+    border-radius: 2px;
+    margin: 10px 5px;
+}
+
+#tokens_list p {
+    margin: 8px;
+}
+#tokens_list p.separator {
+    margin: 20px 0;
+    padding: 5px 50px;
+    background-color: hsla(0, 0%, 75%, 1);
+    color: hsla(0, 0%, 96%, 1);
+    border-radius: 5px;
+    text-align: center;
+    font-size: 20px;
+}
+#tokens_list .token {
+    margin: 8px;
+}
+#tokens_list ul {
+    margin: 0 0 5px 40px;
+}
+#tokens_list b {
+    background-color: hsla(150, 10%, 50%, 1);
+    color: hsla(0, 0%, 96%, 1);
+    padding: 2px 5px;
+    border-radius: 2px;
+    text-decoration: none;
+}
+#tokens_list b.WORD {
+    background-color: hsla(150, 50%, 50%, 1);
+}
+#tokens_list b.ELPFX {
+    background-color: hsla(150, 30%, 50%, 1);
+}
+#tokens_list b.UNKNOWN {
+    background-color: hsla(0, 50%, 50%, 1);
+}
+#tokens_list b.NUM {
+    background-color: hsla(180, 50%, 50%, 1);
+}
+#tokens_list b.COMPLEX {
+    background-color: hsla(60, 50%, 50%, 1);
+}
+#tokens_list b.SEPARATOR {
+    background-color: hsla(210, 50%, 50%, 1);
+}
+#tokens_list b.LINK {
+    background-color: hsla(270, 50%, 50%, 1);
+}
+#tokens_list s {
+    color: hsla(0, 0%, 60%, 1);
+    text-decoration: none;
+}
+#tokens_list .textline {
+    text-decoration: bold;
+}
+
+#tokens_list p.message {
+    margin-top: 20px;
+    padding: 10px 10px;
+    background-color: hsla(240, 10%, 50%, 1);
+    font-size: 18px;
+    color: hsla(240, 0%, 96%, 1);
+    border-radius: 3px;
+    text-align: center;
+}

ADDED   gc_lang/fr/webext/panel/lexicographer.html
Index: gc_lang/fr/webext/panel/lexicographer.html
==================================================================
--- /dev/null
+++ gc_lang/fr/webext/panel/lexicographer.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<html>
+    <head>
+        <link rel="stylesheet" type="text/css" href="lxg_panel.css" />
+        <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
+    </head>
+    
+    <body class="panel">
+        <div style="position: fixed; width: 100%">
+            <header>
+                <div id="waiticon" class="spinner">
+                  <div class="double-bounce1"></div>
+                  <div class="double-bounce2"></div>
+                </div>
+                <h2>Grammalecte · Lexicographe</h2>
+            </header>
+        </div>
+
+        <div id="tokens_list"></div>
+
+    </body>
+</html>

ADDED   gc_lang/fr/webext/panel/lexicographer.js
Index: gc_lang/fr/webext/panel/lexicographer.js
==================================================================
--- /dev/null
+++ gc_lang/fr/webext/panel/lexicographer.js
@@ -0,0 +1,75 @@
+// JavaScript
+
+
+
+/*
+    Actions
+*/
+
+function startWaitIcon () {
+	document.getElementById("waiticon").hidden = false;
+}
+
+function stopWaitIcon () {
+	document.getElementById("waiticon").hidden = true;
+}
+
+function clearList () {
+	document.getElementById("tokens_list").textContent = "";
+}
+
+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 addParagraphElems (sJSON) {
+    try {
+        let xNodeDiv = document.createElement("div");
+        xNodeDiv.className = "paragraph";
+        let lElem = JSON.parse(sJSON);
+        for (let oToken of lElem) {
+            xNodeDiv.appendChild(createTokenNode(oToken));
+        }
+        document.getElementById("tokens_list").appendChild(xNodeDiv);
+    }
+    catch (e) {
+        console.error(e.fileName + "\n" + e.name + "\nline: " + e.lineNumber + "\n" + e.message);
+        console.error(sJSON);
+    }
+}
+
+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/webext/panel/main.css
==================================================================
--- gc_lang/fr/webext/panel/main.css
+++ gc_lang/fr/webext/panel/main.css
@@ -67,51 +67,61 @@
 /* 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,122 +129,21 @@
 #page p {
     margin: 10px 0 5px 0;
 }
 
 #home_page {
-  display: block;
-  padding: 20px;
+    display: block;
+    padding: 20px;
 }
 
-#tf_page {
-  display: none;
-  padding: 20px;
-}
-#gc_page {
-  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;
-}
-#lxg_page {
-  display: none;
-  padding: 20px;
-}
-
-
-/*
-  Conjugueur page
-*/
-
-#conj_page {
-  display: none;
-  padding: 10px;
-}
-
-#conj_page h2 {
-    margin: 5px 0 2px 0;
-    color: hsl(210, 50%, 50%);
-    font: bold 30px Tahoma, "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", sans-serif;
-}
-#conj_page h3 {
-    margin: 5px 0 2px 0;
-    color: hsl(0, 50%, 50%);
-    font: bold 16px Tahoma, "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", sans-serif;
-}
-#conj_page h4 {
-    margin: 5px 0 2px 0;
-    color: hsl(210, 50%, 50%);
-    font: bold 14px Tahoma, "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", sans-serif;
-}
-
-#conj_page .colonne {
-    float: left;
-    width: 240px;
-}
-#conj_page .colsep {
-    float: left;
-    width: 20px;
-}
-
-#conj_page .colonne p {
-    font-size: 12px;
-}
-
-
-#conj_page input#verb {
-    display: inline-block;
-    width: 230px;
-    margin-left: 5px;
-    padding: 5px 10px;
-    border: 2px solid hsl(0, 0%, 80%);
-    border-radius: 3px;
-    height: 24px;
-    background: transparent;
-    font: normal 20px Tahoma, "Ubuntu Condensed";
-    color: hsl(0, 0%, 30%);
-}
-#conj_page input[placeholder]#verb {
-    color: hsl(0, 0%, 70%);
-}
-
-#conj_page a#conjugate {
-    display: inline-block;
-    padding: 7px 10px;
-    font-size: 20px;
-    background-color: hsl(0, 30%, 30%);
-    color: hsl(0, 30%, 60%);
-    border-radius: 3px;
-    text-transform: uppercase;
-    text-align: center;
-    text-decoration: none;
-}
-#conj_page a#conjugate:hover {
-    background-color: hsl(0, 60%, 40%);
-    color: hsl(0, 60%, 70%);
-    box-shadow: 0 0 2px hsl(0, 60%, 50%);
-}
-
-#conj_options {
-    margin: 10px 5px 0 5px;
-    font-size: 16px;
-    text-align: center;
-}
-
-#conj_smallnote {
-    float: right;
-    width: 190px;
-    margin: 15px 0 0 0;
-    padding: 0 5px;
-    font-size: 8.5px;
-    color: hsl(0, 0%, 60%);
-    text-align: center;
+    display: none;
+    padding: 20px;
 }
 
 
 /*
   Test page
@@ -243,21 +152,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 {
@@ -268,160 +176,10 @@
     font-size: 12px;
     text-align: center;
     cursor: pointer;
 }
 
-
-/*
-  Text formatter
-*/
-
-#tf_options {
-    
-}
-
-#tf_options fieldset {
-    margin: 5px 0;
-    padding: 5px 10px 10px 10px;
-    background-color: hsl(0, 0%, 92%);
-    border-radius: 3px;
-}
-
-#tf_options legend {
-    font-size: 20px;
-    color: hsla(210, 20%, 50%, .8);
-    font-weight: bold;
-}
-#tf_options legend span {
-    display: none;
-}
-
-#tf_options fieldset h2 {
-    color: hsl(210, 80%, 40%);
-}
-
-#tf_options fieldset .blockopt {
-    padding: 2px 3px;
-    font-size: 12.5px;
-}
-#tf_options fieldset .underline:hover {
-    background-color: hsl(180, 10%, 86%);
-    border-radius: 2px;
-}
-
-#tf_options fieldset .option {
-    margin: 1px 3px 0 0;
-    float: left;
-}
-#tf_options legend .option {
-    margin: 7px 5px 0 3px;
-    float: left;
-}
-
-#tf_options fieldset .opt_lbl {
-    display: inline-block;
-    color: hsl(0, 0%, 20%);
-}
-
-
-#tf_options fieldset .largew {
-    width: 300px;
-}
-#tf_options fieldset .reducedw {
-    width: 200px;
-}
-#tf_options fieldset .smallw {
-    width: 90px;
-}
-
-#tf_options fieldset .secondoption {
-    display: inline-block;
-}
-
-#tf_options fieldset label span {
-    display: none;
-}
-
-#tf_options .groupblock {
-    opacity: 0.3;
-}
-
-#tf_options .inlineblock {
-    display: inline-block;
-}
-#tf_options .indent {
-    margin-left: 15px;
-}
-
-#tf_actions {
-    background-color: hsl(120, 10%, 92%);
-    padding: 15px;
-    border-top: 1px solid hsl(120, 20%, 86%);
-}
-
-#tf_options .button {
-    display: inline-block;
-    padding: 5px 10px;
-    width: 100px;
-    border-radius: 3px;
-    font-size: 16px;
-    font-weight: bold;
-    text-align: center;
-    cursor: pointer;
-}
-
-#tf_progressbarbox {
-    display: inline-block;
-    padding: 10px 20px;
-}
-
-
-/* 
-  Other elements
-*/
-
-#movewindow {
-    position: fixed;
-    right: 0;
-    top: 50;
-    width: 16px;
-    margin-top: 60px;
-    z-index: 100;
-}
-#movewindow .arrow {
-    background-color: hsl(180, 60%, 50%);
-    cursor: pointer;
-    padding: 1px 3px;
-    font-size: 10px;
-    font-weight: bold;
-    text-align: center;
-    color: hsl(180, 50%, 90%);
-}
-#movewindow .arrow:hover {
-    background-color: hsl(180, 70%, 40%);
-    cursor: hsl(180, 50%, 96%);
-}
-
-#rightcorner {
-    position: absolute;
-    top: 0;
-    right: 0;
-}
-a.rightcornerbutton1 {
-    float: right;
-    padding: 2px 10px 5px 10px;
-    border-radius: 0 0 0 3px;
-    font-size: 18px;
-    text-decoration: none;
-}
-a.rightcornerbutton {
-    float: right;
-    padding: 2px 10px 5px 10px;
-    font-size: 18px;
-    text-decoration: none;
-}
-
 
 /*
     CSS Spinner
     Double bounce
     http://tobiasahlin.com/spinkit/

Index: gc_lang/fr/webext/panel/main.html
==================================================================
--- gc_lang/fr/webext/panel/main.html
+++ gc_lang/fr/webext/panel/main.html
@@ -7,44 +7,29 @@
   </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>
-            <li class="select" data-page="conj_page"><i class="fa fa-star icon"></i> CJ</li>
-            <li class="select" data-page="tf_page"><i class="fa fa-photo icon"></i> TF</li>
-            <li class="select" data-page="gc_page"><i class="fa fa-question-circle icon"></i> CG</li>
             <li class="select" data-page="gc_options_page"><i class="fa fa-coffee icon"></i> OP1</li>
             <li class="select" data-page="sc_options_page"><i class="fa fa-keyboard-o icon"></i> OP2</li>
-            <li class="select" data-page="lxg_page"><i class="fa fa-keyboard-o icon"></i> LXG</li>
             <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>
-        </section>
-
-        <section id="gc_page" class="page">
-          <h1>CORRECTEUR GRAMMATICAL</h1>
-          <div id="paragraphs_list"></div>
+          <div><a href="https://www.dicollecte.org/test.html" target="_blank">TEST</a></div>
         </section>
 
         <section id="gc_options_page" class="page">
           <h1>OPTIONS GRAMMATICALES</h1>
         </section>
@@ -51,445 +36,22 @@
 
         <section id="sc_options_page" class="page">
           <h1>OPTIONS ORTHOGRAPHIQUES</h1>
         </section>
 
-        <section id="lxg_page" class="page">
-          <h1>LEXICOGRAPHE</h1>
-          <div id="tokens_list"></div>
-        </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>
-          </div>
-          <div id="test_results">
-          </div>
-        </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>
-          <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>
-          <p id="conj_options">
-            <label for="oneg">Négation</label> <input type="checkbox" id="oneg" name="oneg" value="ON"  /> 
-            · <label id="opro_lbl" for="opro">Pronominal</label> <input type="checkbox" id="opro" name="opro" value="ON"  />
-            · <label for="ofem">Féminin</label> <input type="checkbox" id="ofem" name="ofem" value="ON"  />
-            <br/> <label for="oint">Interrogatif</label> <input type="checkbox" id="oint" name="oint" value="ON"  />
-            · <label id="otco_lbl" for="otco">Temps composés</label> <input type="checkbox" id="otco" name="otco" value="ON"  />
-          </p>
-
-          <h2 id="verb_title" class="center">&nbsp;</h2>
-          <p id="info" class="center">&nbsp;</p>
-
-          <!-- section 1 -->
-          <div class="colonne">
-            <div id="infinitif" class="box">
-              <h3 id="infinitif_title">Infinitif</h3>
-              <p id="infi">&nbsp;</p>
-            </div>
-            <div id="imperatif" class="box">
-              <h3 id="imperatif_title">Impératif</h3>
-              <h4 id="impe_temps">Présent</h4>
-              <p id="impe1">&nbsp;</p>
-              <p id="impe2">&nbsp;</p>
-              <p id="impe3">&nbsp;</p>
-            </div>
-          </div>
-          
-          <div class="colsep">&nbsp;</div>
-          
-          <div class="colonne">
-            <div id="partpre" class="box">
-              <h3 id="partpre_title">Participe présent</h3>
-              <p id="ppre">&nbsp;</p>
-            </div>
-            <div id="partpas" class="box">
-              <h3 id="partpas_title">Participes passés</h3>
-              <p id="ppas1">&nbsp;</p>
-              <p id="ppas2">&nbsp;</p>
-              <p id="ppas3">&nbsp;</p>
-              <p id="ppas4">&nbsp;</p>
-            </div>
-          </div>
-
-          <div class="clearer"></div>
-
-          <!-- section 2 -->
-          <div class="colonne">
-            <div id="indicatif" class="box">
-              <h3 id="indicatif_title">Indicatif</h3>
-              <div id="ipre">
-                <h4 id="ipre_temps">Présent</h4>
-                <p id="ipre1">&nbsp;</p>
-                <p id="ipre2">&nbsp;</p>
-                <p id="ipre3">&nbsp;</p>
-                <p id="ipre4">&nbsp;</p>
-                <p id="ipre5">&nbsp;</p>
-                <p id="ipre6">&nbsp;</p>
-              </div>
-              <div id="iimp">
-                <h4 id="iimp_temps">Imparfait</h4>
-                <p id="iimp1">&nbsp;</p>
-                <p id="iimp2">&nbsp;</p>
-                <p id="iimp3">&nbsp;</p>
-                <p id="iimp4">&nbsp;</p>
-                <p id="iimp5">&nbsp;</p>
-                <p id="iimp6">&nbsp;</p>
-              </div>
-              <div id="ipsi">
-                <h4 id="ipsi_temps">Passé simple</h4>
-                <p id="ipsi1">&nbsp;</p>
-                <p id="ipsi2">&nbsp;</p>
-                <p id="ipsi3">&nbsp;</p>
-                <p id="ipsi4">&nbsp;</p>
-                <p id="ipsi5">&nbsp;</p>
-                <p id="ipsi6">&nbsp;</p>
-              </div>
-              <div id="ifut">
-                <h4 id="ifut_temps">Futur</h4>
-                <p id="ifut1">&nbsp;</p>
-                <p id="ifut2">&nbsp;</p>
-                <p id="ifut3">&nbsp;</p>
-                <p id="ifut4">&nbsp;</p>
-                <p id="ifut5">&nbsp;</p>
-                <p id="ifut6">&nbsp;</p>
-              </div>
-            </div>
-          </div>
-          
-          <div class="colsep">&nbsp;</div>
-          
-          <div class="colonne">
-            <div id="subjonctif" class="box">
-              <h3 id="subjontif_title">Subjonctif</h3>
-              <div id="spre">
-                <h4 id="spre_temps">Présent</h4>
-                <p id="spre1">&nbsp;</p>
-                <p id="spre2">&nbsp;</p>
-                <p id="spre3">&nbsp;</p>
-                <p id="spre4">&nbsp;</p>
-                <p id="spre5">&nbsp;</p>
-                <p id="spre6">&nbsp;</p>
-              </div>
-              <div id="simp">
-                <h4 id="simp_temps">Imparfait</h4>
-                <p id="simp1">&nbsp;</p>
-                <p id="simp2">&nbsp;</p>
-                <p id="simp3">&nbsp;</p>
-                <p id="simp4">&nbsp;</p>
-                <p id="simp5">&nbsp;</p>
-                <p id="simp6">&nbsp;</p>
-              </div>
-            </div>
-            <div id="conditionnel" class="box">
-              <h3 id="conditionnel_title">Conditionnel</h3>
-              <div id="conda">
-                <h4 id="conda_temps">Présent</h4>
-                <p id="conda1">&nbsp;</p>
-                <p id="conda2">&nbsp;</p>
-                <p id="conda3">&nbsp;</p>
-                <p id="conda4">&nbsp;</p>
-                <p id="conda5">&nbsp;</p>
-                <p id="conda6">&nbsp;</p>
-              </div>
-              <div id="condb">
-                <h4 id="condb_temps">&nbsp;</h4>
-                <p id="condb1">&nbsp;</p>
-                <p id="condb2">&nbsp;</p>
-                <p id="condb3">&nbsp;</p>
-                <p id="condb4">&nbsp;</p>
-                <p id="condb5">&nbsp;</p>
-                <p id="condb6">&nbsp;</p>
-              </div>
-            </div>
-          </div>
-
-          <div class="clearer"></div>
-        </section> <!-- conjugueur -->
-
-        <section id="tf_page" class="page">
-          <h1>FORMATEUR DE TEXTE</h1>
-          <div id="tf_options">
-
-            <!-- Supernumerary spaces -->
-            <fieldset>
-              <legend><input type="checkbox" id="o_group_ssp" class="option" data-default="true" /><label for="o_group_ssp" data-l10n-en="tf_ssp">${tf_ssp}</label></legend>
-              <div id="group_ssp" class="groupblock">
-                <div class="blockopt underline">
-                  <div id="res_o_start_of_paragraph" class="result fright"></div>
-                  <input type="checkbox" id="o_start_of_paragraph" class="option" data-default="true" />
-                  <label for="o_start_of_paragraph" class="opt_lbl largew" data-l10n-en="tf_start_of_paragraph">${tf_start_of_paragraph}</label>
-                </div>
-                <div class="blockopt underline">
-                  <div id="res_o_end_of_paragraph" class="result fright"></div>
-                  <input type="checkbox" id="o_end_of_paragraph" class="option" data-default="true" />
-                  <label for="o_end_of_paragraph" class="opt_lbl largew" data-l10n-en="tf_end_of_paragraph">${tf_end_of_paragraph}</label>
-                </div>
-                <div class="blockopt underline">
-                  <div id="res_o_between_words" class="result fright"></div>
-                  <input type="checkbox" id="o_between_words" class="option" data-default="true" />
-                  <label for="o_between_words" class="opt_lbl largew" data-l10n-en="tf_between_words">${tf_between_words}</label>
-                </div>
-                <div class="blockopt underline">
-                  <div id="res_o_before_punctuation" class="result fright"></div>
-                  <input type="checkbox" id="o_before_punctuation" class="option" data-default="true" />
-                  <label for="o_before_punctuation" class="opt_lbl largew" data-l10n-en="tf_before_punctuation">${tf_before_punctuation}</label>
-                </div>
-                <div class="blockopt underline">
-                  <div id="res_o_within_parenthesis" class="result fright"></div>
-                  <input type="checkbox" id="o_within_parenthesis" class="option" data-default="true" />
-                  <label for="o_within_parenthesis" class="opt_lbl largew" data-l10n-en="tf_within_parenthesis">${tf_within_parenthesis}</label>
-                </div>
-                <div class="blockopt underline">
-                  <div id="res_o_within_square_brackets" class="result fright"></div>
-                  <input type="checkbox" id="o_within_square_brackets" class="option" data-default="true" />
-                  <label for="o_within_square_brackets" class="opt_lbl largew" data-l10n-en="tf_within_square_brackets">${tf_within_square_brackets}</label>
-                </div>
-                <div class="blockopt underline">
-                  <div id="res_o_within_quotation_marks" class="result fright"></div>
-                  <input type="checkbox" id="o_within_quotation_marks" class="option" data-default="true" />
-                  <label for="o_within_quotation_marks" class="opt_lbl largew" data-l10n-en="tf_within_quotation_marks">${tf_within_quotation_marks}</label>
-                </div>
-              </div>
-            </fieldset>
-
-            <!-- Missing spaces -->
-            <fieldset>
-              <legend><input type="checkbox" id="o_group_space" class="option" data-default="true" /><label for="o_group_space" data-l10n-en="tf_space">${tf_space}</label></legend>
-              <div id="group_space" class="groupblock">
-                <div class="blockopt underline">
-                  <div id="res_o_add_space_after_punctuation" class="result fright"></div>
-                  <input type="checkbox" id="o_add_space_after_punctuation" class="option" data-default="true" />
-                  <label for="o_add_space_after_punctuation" class="opt_lbl reducedw" data-l10n-en="tf_add_space_after_punctuation">${tf_add_space_after_punctuation}</label>
-                </div>
-                <div class="blockopt underline">
-                  <div id="res_o_add_space_around_hyphens" class="result fright"></div>
-                  <input type="checkbox" id="o_add_space_around_hyphens" class="option" data-default="true" />
-                  <label for="o_add_space_around_hyphens" class="opt_lbl largew" data-l10n-en="tf_add_space_around_hyphens">${tf_add_space_around_hyphens}</label>
-                </div>
-              </div>
-            </fieldset>
-
-            <!-- Non breaking spaces -->
-            <fieldset>
-              <legend><input type="checkbox" id="o_group_nbsp" class="option" data-default="true" /><label for="o_group_nbsp" data-l10n-en="tf_nbsp">${tf_nbsp}</label></legend>
-              <div id="group_nbsp" class="groupblock">
-                <div class="blockopt underline">
-                  <div id="res_o_nbsp_before_punctuation" class="result fright"></div>
-                  <input type="checkbox" id="o_nbsp_before_punctuation" class="option" data-default="true" />
-                  <label for="o_nbsp_before_punctuation" class="opt_lbl reducedw" data-l10n-en="tf_nbsp_before_punctuation">${tf_nbsp_before_punctuation}</label>
-                  <!--<div class="secondoption">
-                      <input type="checkbox" id="o_nnbsp_before_punctuation" class="option" />
-                      <label for="o_nnbsp_before_punctuation" class="opt_lbl smallw">fines<span>sauf avec “:”</span></label>
-                  </div>-->
-                </div>
-                <div class="blockopt underline">
-                  <div id="res_o_nbsp_within_quotation_marks" class="result fright"></div>
-                  <input type="checkbox" id="o_nbsp_within_quotation_marks" class="option" data-default="true" />
-                  <label for="o_nbsp_within_quotation_marks" class="opt_lbl reducedw" data-l10n-en="tf_nbsp_within_quotation_marks">${tf_nbsp_within_quotation_marks}</label>
-                  <!--<div class="secondoption">
-                      <input type="checkbox" id="o_nnbsp_within_quotation_marks" class="option" />
-                      <label for="o_nnbsp_within_quotation_marks" class="opt_lbl smallw">fines</label>
-                  </div>-->
-                </div>
-                <div class="blockopt underline">
-                  <div id="res_o_nbsp_before_symbol" class="result fright"></div>
-                  <input type="checkbox" id="o_nbsp_before_symbol" class="option" data-default="true" />
-                  <label for="o_nbsp_before_symbol" class="opt_lbl largew" data-l10n-en="tf_nbsp_before_symbol">${tf_nbsp_before_symbol}</label>
-                </div>
-                <div class="blockopt underline">
-                  <div id="res_o_nbsp_within_numbers" class="result fright"></div>
-                  <input type="checkbox" id="o_nbsp_within_numbers" class="option" data-default="true" />
-                  <label for="o_nbsp_within_numbers" class="opt_lbl reducedw" data-l10n-en="tf_nbsp_within_numbers">${tf_nbsp_within_numbers}</label>
-                  <!--<div class="secondoption">
-                      <input type="checkbox" id="o_nnbsp_within_numbers" class="option" />
-                      <label for="o_nnbsp_within_numbers" class="opt_lbl smallw">fines</label>
-                  </div>-->
-                </div>
-                <div class="blockopt underline">
-                  <div id="res_o_nbsp_before_units" class="result fright"></div>
-                  <input type="checkbox" id="o_nbsp_before_units" class="option" data-default="true" />
-                  <label for="o_nbsp_before_units" class="opt_lbl largew" data-l10n-en="tf_nbsp_before_units">${tf_nbsp_before_units}</label>
-                </div>
-              </div>
-            </fieldset>
-
-            <!-- Deletions -->
-            <fieldset>
-              <legend><input type="checkbox" id="o_group_delete" class="option" data-default="true" /><label for="o_group_delete" data-l10n-en="tf_delete">${tf_delete}</label></legend>
-              <div id="group_delete" class="groupblock">
-                <div class="blockopt underline">
-                  <div id="res_o_erase_non_breaking_hyphens" class="result fright"></div>
-                  <input type="checkbox" id="o_erase_non_breaking_hyphens" class="option" data-default="true" />
-                  <label for="o_erase_non_breaking_hyphens" class="opt_lbl largew" data-l10n-en="tf_erase_non_breaking_hyphens">${tf_erase_non_breaking_hyphens}</label>
-                </div>
-              </div>
-            </fieldset>
-
-            <!-- Typographical signs -->
-            <fieldset>
-              <legend><input type="checkbox" id="o_group_typo" class="option" data-default="true" /><label for="o_group_typo" data-l10n-en="tf_typo">${tf_typo}</label></legend>
-              <div id="group_typo" class="groupblock">
-                <div class="blockopt underline">
-                  <div id="res_o_ts_apostrophe" class="result fright"></div>
-                  <input type="checkbox" id="o_ts_apostrophe" class="option" data-default="true" />
-                  <label for="o_ts_apostrophe" class="opt_lbl largew" data-l10n-en="tf_ts_apostrophe">${tf_ts_apostrophe}</label>
-                </div>
-                <div class="blockopt underline">
-                  <div id="res_o_ts_ellipsis" class="result fright"></div>
-                  <input type="checkbox" id="o_ts_ellipsis" class="option" data-default="true" />
-                  <label for="o_ts_ellipsis" class="opt_lbl largew" data-l10n-en="tf_ts_ellipsis">${tf_ts_ellipsis}</label>
-                </div>
-                <div class="blockopt underline">
-                  <div id="res_o_ts_dash_middle" class="result fright"></div>
-                  <input type="checkbox" id="o_ts_dash_middle" class="option" data-default="true" />
-                  <label for="o_ts_dash_middle" class="opt_lbl largew" data-l10n-en="tf_ts_dash_middle">${tf_ts_dash_middle}</label>
-                </div>
-                <div class="blockopt">
-                  <div class="inlineblock indent">
-                    <input type="radio" name="hyphen1" id="o_ts_m_dash_middle" class="option" data-default="false" /><label for="o_ts_m_dash_middle" class="opt_lbl" data-l10n-en="tf_emdash">${tf_emdash}</label>
-                  </div>
-                  <div class="inlineblock indent">
-                    <input type="radio" name="hyphen1" id="o_ts_n_dash_middle" class="option" data-default="true" /><label for="o_ts_n_dash_middle" class="opt_lbl" data-l10n-en="tf_endash">${tf_endash}</label>
-                  </div>
-                </div>
-                <div class="blockopt underline">
-                  <div id="res_o_ts_dash_start" class="result fright"></div>
-                  <input type="checkbox" id="o_ts_dash_start" class="option" data-default="true" />
-                  <label for="o_ts_dash_start" class="opt_lbl largew" data-l10n-en="tf_ts_dash_start">${tf_ts_dash_start}</label>
-                </div>
-                <div class="blockopt">
-                  <div class="inlineblock indent">
-                    <input type="radio" name="hyphen2" id="o_ts_m_dash_start" class="option"  data-default="true" /><label for="o_ts_m_dash_start" class="opt_lbl" data-l10n-en="tf_emdash">${tf_emdash}</label>
-                  </div>
-                  <div class="inlineblock indent">
-                    <input type="radio" name="hyphen2" id="o_ts_n_dash_start" class="option" data-default="false" /><label for="o_ts_n_dash_start" class="opt_lbl" data-l10n-en="tf_endash">${tf_endash}</label>
-                  </div>
-                </div>
-                <div class="blockopt underline">
-                  <div id="res_o_ts_quotation_marks" class="result fright"></div>
-                  <input type="checkbox" id="o_ts_quotation_marks" class="option" data-default="true" />
-                  <label for="o_ts_quotation_marks" class="opt_lbl largew" data-l10n-en="tf_ts_quotation_marks">${tf_ts_quotation_marks}</label>
-                </div>
-                <div class="blockopt underline">
-                  <div id="res_o_ts_units" class="result fright"></div>
-                  <input type="checkbox" id="o_ts_units" class="option" data-default="true" />
-                  <label for="o_ts_units" class="opt_lbl largew" data-l10n-en="tf_ts_units">${tf_ts_units}</label>
-                </div>
-                <div class="blockopt underline">
-                  <div id="res_o_ts_spell" class="result fright"></div>
-                  <input type="checkbox" id="o_ts_spell" class="option" data-default="true" />
-                  <label for="o_ts_spell" class="opt_lbl largew" data-l10n-en="tf_ts_spell">${tf_ts_spell}</label>
-                </div>
-                <div class="blockopt underline">
-                  <div id="res_o_ts_ligature" class="result fright"></div>
-                  <div class="inlineblock">
-                    <input type="checkbox" id="o_ts_ligature" class="option" data-default="false" />
-                    <label for="o_ts_ligature" class="opt_lbl" data-l10n-en="tf_ts_ligature">${tf_ts_ligature}</label>
-                  </div>
-                  <div class="inlineblock indent">
-                    <input type="radio" id="o_ts_ligature_do" name="liga" class="option" data-default="false" />
-                    <label for="o_ts_ligature_do" class="opt_lbl" data-l10n-en="tf_ts_ligature_do">${tf_ts_ligature_do}</label>
-                  </div>
-                  <div class="inlineblock indent">
-                    <input type="radio" id="o_ts_ligature_undo" name="liga" class="option" data-default="true" />
-                    <label for="o_ts_ligature_undo" class="opt_lbl" data-l10n-en="tf_ts_ligature_undo">${tf_ts_ligature_undo}</label>
-                  </div>
-                </div>
-
-                <div class="blockopt">
-                  <div class="inlineblock indent"><input type="checkbox" id="o_ts_ligature_ff" class="option" data-default="true" /><label for="o_ts_ligature_ff" class="opt_lbl">ff</label></div>
-                  &nbsp; <div class="inlineblock"><input type="checkbox" id="o_ts_ligature_fi" class="option" data-default="true" /><label for="o_ts_ligature_fi" class="opt_lbl">fi</label></div>
-                  &nbsp; <div class="inlineblock"><input type="checkbox" id="o_ts_ligature_ffi" class="option" data-default="true" /><label for="o_ts_ligature_ffi" class="opt_lbl">ffi</label></div>
-                  &nbsp; <div class="inlineblock"><input type="checkbox" id="o_ts_ligature_fl" class="option" data-default="true" /><label for="o_ts_ligature_fl" class="opt_lbl">fl</label></div>
-                  &nbsp; <div class="inlineblock"><input type="checkbox" id="o_ts_ligature_ffl" class="option" data-default="true" /><label for="o_ts_ligature_ffl" class="opt_lbl">ffl</label></div>
-                  &nbsp; <div class="inlineblock"><input type="checkbox" id="o_ts_ligature_ft" class="option" data-default="true" /><label for="o_ts_ligature_ft" class="opt_lbl">ft</label></div>
-                  &nbsp; <div class="inlineblock"><input type="checkbox" id="o_ts_ligature_st" class="option" data-default="false" /><label for="o_ts_ligature_st" class="opt_lbl">st</label></div>
-                </div>
-              </div>
-            </fieldset>
-
-            <!-- Misc -->
-            <fieldset>
-              <legend><input type="checkbox" id="o_group_misc" class="option" data-default="true" /><label for="o_group_misc" data-l10n-en="tf_misc">${tf_misc}</label></legend>
-              <div id="group_misc" class="groupblock">
-                <div class="blockopt underline">
-                  <div id="res_o_ordinals_no_exponant" class="result fright"></div>
-                  <input type="checkbox" id="o_ordinals_no_exponant" class="option" data-default="true" />
-                  <label for="o_ordinals_no_exponant" class="opt_lbl reducedw" data-l10n-en="tf_ordinals_no_exponant">${tf_ordinals_no_exponant}</label>
-                  <div class="secondoption">
-                    <input type="checkbox" id="o_ordinals_exponant" class="option" data-default="true" />
-                    <label for="o_ordinals_exponant" class="opt_lbl smallw" data-l10n-en="tf_ordinals_exponant">${tf_ordinals_exponant}</label>
-                  </div>
-                </div>
-                <div class="blockopt underline">
-                  <div id="res_o_etc" class="result fright"></div>
-                  <input type="checkbox" id="o_etc" class="option" data-default="true" />
-                  <label for="o_etc" class="opt_lbl largew" data-l10n-en="tf_etc">${tf_etc}</label>
-                </div>
-                <div class="blockopt underline">
-                  <div id="res_o_missing_hyphens" class="result fright"></div>
-                  <input type="checkbox" id="o_missing_hyphens" class="option" data-default="true" />
-                  <label for="o_missing_hyphens" class="opt_lbl largew" data-l10n-en="tf_missing_hyphens">${tf_missing_hyphens}</label>
-                </div>
-                <div class="blockopt underline">
-                  <div id="res_o_ma_word" class="result fright"></div>
-                  <input type="checkbox" id="o_ma_word" class="option" data-default="true" />
-                  <label for="o_ma_word" class="opt_lbl largew" data-l10n-en="tf_ma_word">${tf_ma_word}</label>
-                </div>
-                <div class="blockopt">
-                  <div class="inlineblock indent">
-                    <input type="checkbox" id="o_ma_1letter_lowercase" class="option" />
-                    <label for="o_ma_1letter_lowercase" class="opt_lbl" data-l10n-en="tf_ma_1letter_lowercase">${tf_ma_1letter_lowercase}</label>
-                  </div>
-                  <div class="inlineblock indent">
-                    <input type="checkbox" id="o_ma_1letter_uppercase" class="option" />
-                    <label for="o_ma_1letter_uppercase" class="opt_lbl" data-l10n-en="tf_ma_1letter_uppercase">${tf_ma_1letter_uppercase}</label>
-                  </div>
-                </div>
-              </div>
-            </fieldset>
-
-            <!-- Restructuration -->
-            <fieldset>
-              <legend><input type="checkbox" id="o_group_struct" class="option" data-default="false" /><label for="o_group_struct" data-l10n-en="tf_struct">${tf_struct}</label></legend>
-              <div id="group_struct" class="groupblock">
-                <div class="blockopt underline">
-                  <div id="res_o_remove_hyphens_at_end_of_paragraphs" class="result fright"></div>
-                  <input type="checkbox" id="o_remove_hyphens_at_end_of_paragraphs" class="option" data-default="false" />
-                  <label for="o_remove_hyphens_at_end_of_paragraphs" class="opt_lbl largew"  data-l10n-en="tf_remove_hyphens_at_end_of_paragraphs">${tf_remove_hyphens_at_end_of_paragraphs}</label>
-                </div>
-                <div class="blockopt underline">
-                  <div id="res_o_merge_contiguous_paragraphs" class="result fright"></div>
-                  <input type="checkbox" id="o_merge_contiguous_paragraphs" class="option" data-default="false" />
-                  <label for="o_merge_contiguous_paragraphs" class="opt_lbl largew" data-l10n-en="tf_merge_contiguous_paragraphs">${tf_merge_contiguous_paragraphs}</label>
-                </div>
-              </div>
-            </fieldset>
-          </div>
-
-          <div id="tf_actions">
-              <div id="tf_reset" class="button blue" data-l10n-en="Default">Par défaut</div>
-              <div id="tf_apply" class="button green fright" data-l10n-en="Apply">Appliquer</div>
-              <div id="tf_progressbarbox"><progress id="progressbar" style="width: 400px;"></progress> <span id="time_res"></span></div>
-              <!--<div class="clearer"></div>
-              <div id="infomsg" data-l10n-id="tf_infomsg"></div>-->
-          </div>
-        </section> <!-- text formatter -->
+            <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>
+          <pre id="tests_result"></pre>
+        </section>
 
       </div> <!-- #page -->
 
     </div> <!-- #main -->
 
     <script src="main.js"></script>
   </body>
 
 </html>

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,176 @@
+// 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";
+    }
+    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: "textToTest", dParam: {sText: document.getElementById("text_to_test").value, sCountry: "FR", bDebug: false, bContext: false}, dInfo: {}});
+                    break;
+                case "fulltests":
+                    document.getElementById("tests_result").textContent = "Veuillez patienter…";
+                    browser.runtime.sendMessage({sCommand: "fullTests", dParam: {}, dInfo: {}});
+                    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/data/gc_panel.js
==================================================================
--- gc_lang/fr/xpi/data/gc_panel.js
+++ gc_lang/fr/xpi/data/gc_panel.js
@@ -199,11 +199,11 @@
 
 function _tagParagraph (sParagraph, xParagraph, iParagraph, aSpellErr, aGrammErr) {
     try {
         if (aGrammErr.length === 0  &&  aSpellErr.length === 0) {
             xParagraph.textContent = sParagraph;
-            return
+            return;
         }
         aGrammErr.push(...aSpellErr);
         aGrammErr.sort(function (a, b) {
             if (a["nStart"] < b["nStart"])
                 return -1;

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: gc_lang/fr/xpi/package.json
==================================================================
--- gc_lang/fr/xpi/package.json
+++ gc_lang/fr/xpi/package.json
@@ -1,10 +1,10 @@
 {
   "name": "grammalecte-fr",
   "title": "Grammalecte [fr]",
   "id": "French-GC@grammalecte.net",
-  "version": "0.5.18",
+  "version": "0.5.19",
   "description": "Correcteur grammatical pour le français",
   "homepage": "http://www.dicollecte.org/grammalecte",
   "main": "ui.js",
   "icon": "data/img/icon-48.png",
   "scripts": {

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: