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,239 @@
+// 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":
+                break;
+            case "parse":
+            case "parseAndSpellcheck":
+            case "parseAndSpellcheck1":
+            case "getListOfTokens":
+                if (typeof(dInfo.iReturnPort) === "number") {
+                    let xPort = dConnx.get(dInfo.iReturnPort);
+                    xPort.postMessage(e.data);
+                } else {
+                    console.log("[background] don’t know where to send results");
+                    console.log(e.data);
+                }
+                break;
+            case "textToTest":
+                browser.runtime.sendMessage({sCommand: "text_to_test_result", sResult: result});
+                break;
+            case "fullTests":
+                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 dConnx = new Map();
+
+
+/*
+    Messages from the extension (not the Worker)
+*/
+function handleMessage (oRequest, xSender, sendResponse) {
+    //console.log(xSender);
+    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 (xPort) {
+    let iPortId = xPort.sender.tab.id; // identifier for the port: each port can be found at dConnx[iPortId]
+    dConnx.set(iPortId, xPort);
+    xPort.onMessage.addListener(function (oRequest) {
+        switch (oRequest.sCommand) {
+            case "parse":
+            case "parseAndSpellcheck":
+            case "parseAndSpellcheck1":
+            case "getListOfTokens":
+                oRequest.dInfo.iReturnPort = iPortId; // we pass the id of the return port to receive answer
+                xGCEWorker.postMessage(oRequest);
+                break;
+            default:
+                console.log("[background] Unknown command: " + oRequest.sCommand);
+                console.log(oRequest);
+        }
+    });
+    xPort.postMessage({sActionDone: "newId", result: iPortId});
+}
+
+browser.runtime.onConnect.addListener(handleConnexion);
+
+
+/*
+    Context Menu
+*/
+browser.contextMenus.create({
+    id: "parseAndSpellcheck",
+    title: "Correction grammaticale",
+    contexts: ["selection"]
+});
+
+browser.contextMenus.create({
+    id: "getListOfTokens",
+    title: "Lexicographe",
+    contexts: ["selection"]
+});
+
+browser.contextMenus.create({
+    id: "whatever",
+    type: "separator",
+    contexts: ["selection"]
+});
+
+browser.contextMenus.create({
+    id: "conjugueur_window",
+    title: "Conjugueur [fenêtre]",
+    contexts: ["all"]
+});
+
+browser.contextMenus.create({
+    id: "conjugueur_tab",
+    title: "Conjugueur [onglet]",
+    contexts: ["all"]
+});
+
+
+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);
+    // confusing: no way to get the node where we click?!
+    switch (xInfo.menuItemId) {
+        case "parseAndSpellcheck":
+            parseAndSpellcheckSelectedText(xTab.id, xInfo.selectionText);
+            break;
+        case "getListOfTokens": 
+            getListOfTokensFromSelectedText(xTab.id, xInfo.selectionText);
+            break;
+        case "conjugueur_window":
+            openConjugueurWindow();
+            break;
+        case "conjugueur_tab":
+            openConjugueurTab();
+            break;
+    }    
+});
+
+
+/*
+    Keyboard shortcuts
+*/
+browser.commands.onCommand.addListener(function (sCommand) {
+    switch (sCommand) {
+        case "conjugueur_tab":
+            openConjugueurTab();
+            break;
+        case "conjugueur_window":
+            openConjugueurWindow();
+            break;
+    }
+});
+
+
+/*
+    Actions
+*/
+function parseAndSpellcheckSelectedText (iTab, sText) {
+    // send message to the tab
+    let xTabPort = dConnx.get(iTab);
+    xTabPort.postMessage({sActionDone: "openGCPanel", result: null, dInfo: null, bEnd: false, bError: false});
+    // send command to the worker
+    xGCEWorker.postMessage({
+        sCommand: "parseAndSpellcheck",
+        dParam: {sText: sText, sCountry: "FR", bDebug: false, bContext: false},
+        dInfo: {iReturnPort: iTab}
+    });
+}
+
+function getListOfTokensFromSelectedText (iTab, sText) {
+    // send message to the tab
+    let xTabPort = dConnx.get(iTab);
+    xTabPort.postMessage({sActionDone: "openLxgPanel", result: null, dInfo: null, bEnd: false, bError: false});
+    // send command to the worker
+    xGCEWorker.postMessage({
+        sCommand: "getListOfTokens",
+        dParam: {sText: sText},
+        dInfo: {iReturnPort: iTab}
+    });
+}
+
+function openConjugueurTab () {
+    let xConjTab = browser.tabs.create({
+        url: browser.extension.getURL("panel/conjugueur.html")
+    });
+    xConjTab.then(onCreated, onError);
+}
+
+function openConjugueurWindow () {
+    let xConjWindow = browser.windows.create({
+        url: browser.extension.getURL("panel/conjugueur.html"),
+        type: "detached_panel",
+        width: 710,
+        height: 980
+    });
+    xConjWindow.then(onCreated, onError);
+}
+
+
+function onCreated (windowInfo) {
+    console.log(`Created window: ${windowInfo.id}`);
+}
+
+function onError (error) {
+    console.log(`Error: ${error}`);
+}

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,185 @@
+// 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";
+
+
+function showError (e) {
+    console.error(e.fileName + "\n" + e.name + "\nline: " + e.lineNumber + "\n" + e.message);
+}
+
+
+const oGrammalecte = {
+
+    nWrapper: 1,
+
+    oConjPanel: null,
+    oTFPanel: null,
+    oLxgPanel: null,
+    oGCPanel: null,
+
+    wrapTextareas: function () {
+        let lNode = document.getElementsByTagName("textarea");
+        for (let xNode of lNode) {
+            this._createWrapper(xNode);
+        }
+    },
+
+    _createWrapper (xTextArea) {
+        try {
+            let xParentElement = xTextArea.parentElement;
+            let xWrapper = document.createElement("div");
+            xWrapper.className = "grammalecte_wrapper";
+            xWrapper.id = "grammalecte_wrapper" + this.nWrapper;
+            this.nWrapper += 1;
+            xParentElement.insertBefore(xWrapper, xTextArea);
+            xWrapper.appendChild(xTextArea); // move textarea in wrapper
+            xWrapper.appendChild(this._createWrapperToolbar(xTextArea));
+        }
+        catch (e) {
+            showError(e);
+        }
+    },
+
+    _createWrapperToolbar: function (xTextArea) {
+        try {
+            let xToolbar = createNode("div", {className: "grammalecte_wrapper_toolbar"});
+            let xConjButton = createNode("div", {className: "grammalecte_wrapper_button", textContent: "Conjuguer"});
+            xConjButton.onclick = function () {
+                this.createConjPanel();
+                //this.oConjPanel.show();
+            }.bind(this);
+            let xTFButton = createNode("div", {className: "grammalecte_wrapper_button", textContent: "Formater"});
+            xTFButton.onclick = function () {
+                this.createTFPanel();
+                this.oTFPanel.start(xTextArea);
+                this.oTFPanel.show();
+            }.bind(this);
+            let xLxgButton = createNode("div", {className: "grammalecte_wrapper_button", textContent: "Analyser"});
+            xLxgButton.onclick = function () {
+                this.createLxgPanel();
+                this.oLxgPanel.clear();
+                this.oLxgPanel.show();
+                this.oLxgPanel.startWaitIcon();
+                xPort.postMessage({
+                    sCommand: "getListOfTokens",
+                    dParam: {sText: xTextArea.value},
+                    dInfo: {sTextAreaId: xTextArea.id}
+                });
+            }.bind(this);
+            let xGCButton = createNode("div", {className: "grammalecte_wrapper_button", textContent: "Corriger"});
+            xGCButton.onclick = function () {
+                this.createGCPanel();
+                this.oGCPanel.clear();
+                this.oGCPanel.show();
+                this.oGCPanel.start(xTextArea);
+                this.oGCPanel.startWaitIcon();
+                xPort.postMessage({
+                    sCommand: "parseAndSpellcheck",
+                    dParam: {sText: xTextArea.value, sCountry: "FR", bDebug: false, bContext: false},
+                    dInfo: {sTextAreaId: xTextArea.id}
+                });
+            }.bind(this);
+            // 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);
+        }
+    },
+
+    createConjPanel: function () {
+        if (this.oConjPanel === null) {
+            this.oConjPanel = new GrammalectePanel("grammalecte_conj_panel", "Conjugueur", 600, 600);
+            this.oConjPanel.insertIntoPage();
+        }
+    },
+
+    createTFPanel: function () {
+        if (this.oTFPanel === null) {
+            this.oTFPanel = new GrammalecteTextFormatter("grammalecte_tf_panel", "Formateur de texte", 800, 620, false);
+            //this.oTFPanel.logInnerHTML();
+            this.oTFPanel.insertIntoPage();
+        }
+    },
+
+    createLxgPanel: function () {
+        if (this.oLxgPanel === null) {
+            this.oLxgPanel = new GrammalecteLexicographer("grammalecte_lxg_panel", "Lexicographe", 500, 700);
+            this.oLxgPanel.insertIntoPage();
+        }
+    },
+
+    createGCPanel: function () {
+        if (this.oGCPanel === null) {
+            this.oGCPanel = new GrammalecteGrammarChecker("grammalecte_gc_panel", "Grammalecte", 500, 700);
+            this.oGCPanel.insertIntoPage();
+        }
+    }
+}
+
+
+/*
+    Connexion to the background
+*/
+let xPort = browser.runtime.connect({name: "content-script port"});
+
+xPort.onMessage.addListener(function (oMessage) {
+    let {sActionDone, result, dInfo, bEnd, bError} = oMessage;
+    switch (sActionDone) {
+        case "parseAndSpellcheck":
+            if (!bEnd) {
+                oGrammalecte.oGCPanel.addParagraphResult(result);
+            } else {
+                oGrammalecte.oGCPanel.stopWaitIcon();
+            }
+            break;
+        case "parseAndSpellcheck1":
+            oGrammalecte.oGCPanel.refreshParagraph(dInfo.sParagraphId, result);
+            break;
+        case "getListOfTokens":
+            if (!bEnd) {
+                oGrammalecte.oLxgPanel.addListOfTokens(result);
+            } else {
+                oGrammalecte.oLxgPanel.stopWaitIcon();
+            }
+            break;
+        // Design WTF: context menus are made in background, not in content-script.
+        // Commands from context menu received here to initialize panels
+        case "openGCPanel":
+            oGrammalecte.createGCPanel();
+            oGrammalecte.oGCPanel.clear();
+            oGrammalecte.oGCPanel.show();
+            oGrammalecte.oGCPanel.start();
+            oGrammalecte.oGCPanel.startWaitIcon();
+            break;
+        case "openLxgPanel":
+            oGrammalecte.createLxgPanel();
+            oGrammalecte.oLxgPanel.clear();
+            oGrammalecte.oLxgPanel.show();
+            oGrammalecte.oLxgPanel.startWaitIcon();
+            break;
+        default:
+            console.log("[Content script] Unknown command: " + sActionDone);
+    }
+});
+
+
+/*
+    Start
+*/
+oGrammalecte.wrapTextareas();

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,363 @@
+/*
+    Grammar checker
+*/
+#grammalecte_gc_panel_content {
+    margin: 0;
+    padding: 5px;
+}
+
+.grammalecte_paragraph_block {
+    margin: 0 0 5px 0;
+}
+
+.grammalecte_paragraph {
+    margin: 0;
+    padding: 10px;
+    background-color: hsl(0, 0%, 96%);
+    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;
+}
+
+
+/*
+    Action buttons
+*/
+
+.grammalecte_paragraph_actions {
+    float: right;
+    margin: 0 0 5px 10px;
+}
+
+.grammalecte_paragraph_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_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;
+}
+
+
+
+/* 
+    TOOLTIP
+*/
+#grammalecte_tooltip_arrow {
+    position: absolute;
+    display: none;
+}
+
+#grammalecte_tooltip {
+    position: absolute;
+    margin: 0;
+    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 {
+    margin: 0;
+    padding: 5px 10px 10px 10px;
+    background-color: hsl(210, 50%, 30%);
+    color: hsl(210, 50%, 96%);
+}
+#grammalecte_tooltip_rule_id {
+    display: none;
+    margin: 0 0 5px 0;
+    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: 0 0 5px 0;
+}
+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);
+}
+
+
+/*
+    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,449 @@
+// JavaScript
+
+"use strict";
+
+function onGrammalecteGCPanelClick (xEvent) {
+    try {
+        let xElem = xEvent.target;
+        if (xElem.id) {
+            if (xElem.id.startsWith("grammalecte_sugg")) {
+                oGrammalecte.oGCPanel.applySuggestion(xElem.id);
+            } else if (xElem.id === "grammalecte_tooltip_ignore") {
+                oGrammalecte.oGCPanel.ignoreError(xElem.id);
+            } else if (xElem.id.startsWith("grammalecte_check")) {
+                oGrammalecte.oGCPanel.recheckParagraph(parseInt(xElem.id.slice(17)));
+            } else if (xElem.id.startsWith("grammalecte_hide")) {
+                xElem.parentNode.parentNode.style.display = "none";
+            } else if (xElem.tagName === "U" && xElem.id.startsWith("grammalecte_err")
+                       && xElem.className !== "corrected" && xElem.className !== "ignored") {
+                oGrammalecte.oGCPanel.oTooltip.show(xElem.id);
+            } else if (xElem.id === "grammalecte_tooltip_url") {
+                oGrammalecte.oGCPanel.openURL(xElem.getAttribute("href"));
+            } else {
+                oGrammalecte.oGCPanel.oTooltip.hide();
+            }
+        } else if (xElem.tagName === "A") {
+            oGrammalecte.oGCPanel.openURL(xElem.getAttribute("href"));
+        } else {
+            oGrammalecte.oGCPanel.oTooltip.hide();
+        }
+    }
+    catch (e) {
+        showError(e);
+    }
+}
+
+
+class GrammalecteGrammarChecker extends GrammalectePanel {
+    /*
+        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]
+    */
+
+    constructor (...args) {
+        super(...args);
+        this.aIgnoredErrors = new Set();
+        this.xContentNode = createNode("div", {id: "grammalecte_gc_panel_content"});
+        this.xParagraphList = createNode("div", {id: "grammalecte_paragraph_list"});
+        this.xContentNode.appendChild(this.xParagraphList);
+        this.xPanelContent.addEventListener("click", onGrammalecteGCPanelClick, false);
+        this.oTooltip = new GrammalecteTooltip(this.xContentNode);
+        this.xPanelContent.appendChild(this.xContentNode);
+        this.oTAC = new GrammalecteTextAreaControl();
+    }
+
+    start (xTextArea=null) {
+        this.clear();
+        if (xTextArea) {
+            this.oTAC.setTextArea(xTextArea);
+        }
+    }
+
+    clear () {
+        while (this.xParagraphList.firstChild) {
+            this.xParagraphList.removeChild(this.xParagraphList.firstChild);
+        }
+        this.aIgnoredErrors.clear();
+    }
+
+    hide () {
+        this.xPanelNode.style.display = "none";
+        this.oTAC.clear();
+    }
+
+    addParagraphResult (oResult) {
+        try {
+            if (oResult && oResult.sParagraph.trim() !== "" && (oResult.aGrammErr.length > 0 || oResult.aSpellErr.length > 0)) {
+                let xNodeDiv = createNode("div", {className: "grammalecte_paragraph_block"});
+                // actions
+                let xActionsBar = createNode("div", {className: "grammalecte_paragraph_actions"});
+                xActionsBar.appendChild(createNode("div", {id: "grammalecte_check" + oResult.iParaNum, className: "button green", textContent: "Réanalyser"}));
+                xActionsBar.appendChild(createNode("div", {id: "grammalecte_hide" + oResult.iParaNum, className: "button red bold", textContent: "×"}));
+                // paragraph
+                let xParagraph = createNode("p", {id: "grammalecte_paragraph"+oResult.iParaNum, lang: "fr", contentEditable: "true"}, {para_num: oResult.iParaNum});
+                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";
+                xParagraph.addEventListener("keyup", function (xEvent) {
+                    this.oTAC.setParagraph(parseInt(xEvent.target.dataset.para_num), this.purgeText(xEvent.target.textContent));
+                    this.oTAC.write();
+                }.bind(this)
+                , true);
+                this._tagParagraph(xParagraph, oResult.sParagraph, oResult.iParaNum, oResult.aGrammErr, oResult.aSpellErr);
+                // creation
+                xNodeDiv.appendChild(xActionsBar);
+                xNodeDiv.appendChild(xParagraph);
+                this.xParagraphList.appendChild(xNodeDiv);
+            }
+        }
+        catch (e) {
+            showError(e);
+        }
+    }
+
+    recheckParagraph (iParaNum) {
+        let sParagraphId = "grammalecte_paragraph" + iParaNum;
+        let xParagraph = document.getElementById(sParagraphId);
+        this.blockParagraph(xParagraph);
+        let sText = this.purgeText(xParagraph.textContent);
+        xPort.postMessage({
+            sCommand: "parseAndSpellcheck1",
+            dParam: {sText: sText, sCountry: "FR", bDebug: false, bContext: false},
+            dInfo: {sParagraphId: sParagraphId}
+        });
+        this.oTAC.setParagraph(iParaNum, sText);
+        this.oTAC.write();
+    }
+
+    refreshParagraph (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);
+            this.freeParagraph(xParagraph);
+        }
+        catch (e) {
+            showError(e);
+        }
+    }
+
+    _tagParagraph (xParagraph, sParagraph, iParaNum, 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'] = iParaNum + "-" + nErr.toString(); // error identifier
+                    oErr['sIgnoredKey'] = iParaNum + ":" + 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 (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;
+    }
+
+    blockParagraph (xParagraph) {
+        xParagraph.contentEditable = "false";
+        document.getElementById("grammalecte_check"+xParagraph.dataset.para_num).textContent = "Analyse…";
+    }
+
+    freeParagraph (xParagraph) {
+        xParagraph.contentEditable = "true";
+        document.getElementById("grammalecte_check"+xParagraph.dataset.para_num).textContent = "Réanalyser";
+    }
+
+    applySuggestion (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");
+            this.oTooltip.hide();
+            this.recheckParagraph(parseInt(sErrorId.slice(0, sErrorId.indexOf("-"))));
+        }
+        catch (e) {
+            showError(e);
+        }
+    }
+
+    ignoreError (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";
+            this.oTooltip.hide();
+        }
+        catch (e) {
+            showError(e);
+        }
+    }
+
+    purgeText (sText) {
+        return sText.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
+    }
+
+    addSummary () {
+        // todo
+    }
+
+    addMessage (sMessage) {
+        let xNode = createNode("div", {className: "grammalecte_gc_panel_message", textContent: sMessage});
+        this.xParagraphList.appendChild(xNode);
+    }
+
+    copyToClipboard () {
+        this.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);
+        }
+        this.stopWaitIcon();
+    }
+}
+
+
+
+class GrammalecteTooltip {
+
+    constructor (xContentNode) {
+        this.xTooltip = createNode("div", {id: "grammalecte_tooltip"});
+        this.xTooltipArrow = createNode("img", {
+            id: "grammalecte_tooltip_arrow",
+            src: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAECAYAAACzzX7wAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAOwQAADsEBuJFr7QAAABl0RVh0U29mdHdhcmUAcGFpbnQubmV0IDQuMC4xNkRpr/UAAAAlSURBVBhXY/j//z8cq/kW/wdhZDEMSXRFWCVhGKwAmwQCF/8HAGUkScGH4cM8AAAAAElFTkSuQmCC",
+            alt: "^"
+        });
+        this.xTooltipSuggBlock = createNode("div", {id: "grammalecte_tooltip_sugg_block"});
+        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.xTooltip.appendChild(this.xTooltipSuggBlock);
+        xContentNode.appendChild(this.xTooltip);
+        xContentNode.appendChild(this.xTooltipArrow);
+    }
+
+    show (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 () {
+        // todo
+    }
+
+    hide () {
+        this.xTooltipArrow.style.display = "none";
+        this.xTooltip.style.display = "none";
+    }
+
+    _createSuggestion (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 (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);
+        }
+    }
+}
+
+
+class GrammalecteTextAreaControl {
+
+    constructor () {
+        this._xTextArea = null;
+        this._dParagraph = new Map();
+    }
+
+    setTextArea (xNode) {
+        this.clear();
+        this._xTextArea = xNode;
+        this._xTextArea.disabled = true;
+        this._loadText();
+    }
+
+    clear () {
+        if (this._xTextArea !== null) {
+            this._xTextArea.disabled = false;
+            this._xTextArea = null;
+        }
+        this._dParagraph.clear();
+    }
+
+    setParagraph (iParagraph, sText) {
+        if (this._xTextArea !== null) {
+            this._dParagraph.set(iParagraph, sText);
+        }
+    }
+
+    _loadText () {
+        let sText = this._xTextArea.value;
+        let i = 0;
+        let iStart = 0;
+        let iEnd = 0;
+        sText = sText.replace("\r\n", "\n").replace("\r", "\n");
+        while ((iEnd = sText.indexOf("\n", iStart)) !== -1) {
+            this._dParagraph.set(i, sText.slice(iStart, iEnd));
+            i++;
+            iStart = iEnd+1;
+        }
+        this._dParagraph.set(i, sText.slice(iStart));
+        console.log("Paragraphs number: " + (i+1));
+    }
+
+    write () {
+        if (this._xTextArea !== null) {
+            let sText = "";
+            this._dParagraph.forEach(function (val, key) {
+                sText += val + "\n";
+            });
+            this._xTextArea.value = sText.slice(0,-1);
+        }
+    }
+}

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,69 @@
+// JavaScript
+
+"use strict";
+
+class GrammalecteLexicographer extends GrammalectePanel {
+
+    constructor (...args) {
+        super(...args);
+        this._nCount = 0;
+        this._xContentNode = createNode("div", {id: "grammalecte_lxg_panel_content"});
+        this.xPanelContent.appendChild(this._xContentNode);
+    }
+
+    clear () {
+        this._nCount = 0;
+        while (this._xContentNode.firstChild) {
+            this._xContentNode.removeChild(this._xContentNode.firstChild);
+        }
+    }
+
+    addSeparator (sText) {
+        if (this._xContentNode.textContent !== "") {
+            this._xContentNode.appendChild(createNode("div", {className: "grammalecte_lxg_separator", textContent: sText}));
+        }
+    }
+
+    addMessage (sClass, sText) {
+        this._xContentNode.appendChild(createNode("div", {className: sClass, textContent: sText}));
+    }
+
+    addListOfTokens (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 (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 (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/panels_content.css
Index: gc_lang/fr/webext/content_scripts/panels_content.css
==================================================================
--- /dev/null
+++ gc_lang/fr/webext/content_scripts/panels_content.css
@@ -0,0 +1,143 @@
+/*
+    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 {
+    visibility: hidden;
+    width: 40px;
+    height: 40px;
+    position: absolute;
+    top: 2px;
+    right: 150px;
+}
+.grammalecte_spinner .bounce1,
+.grammalecte_spinner .bounce2 {
+    width: 100%;
+    height: 100%;
+    border-radius: 50%;
+    background-color: hsl(0, 50%, 75%);
+    opacity: 0.6;
+    position: absolute;
+    top: 0;
+    left: 0;
+    animation: grammalecte-sk-bounce 2.0s infinite ease-in-out;
+}
+.grammalecte_spinner .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/panels_content.js
Index: gc_lang/fr/webext/content_scripts/panels_content.js
==================================================================
--- /dev/null
+++ gc_lang/fr/webext/content_scripts/panels_content.js
@@ -0,0 +1,168 @@
+// JavaScript
+// Panel creator
+
+"use strict";
+
+console.log("[Content script] Panel creator");
+
+
+class GrammalectePanel {
+
+    constructor (sId, sTitle, nWidth, nHeight, bFlexible=true) {
+        this.sId = sId;
+        this.sContentId = sId+"_content";
+        this.nWidth = nWidth;
+        this.nHeight = nHeight;
+        this.bFlexible = bFlexible;
+        this.xPanelContent = createNode("div", {className: "grammalecte_panel_content"});
+        this.xWaitIcon = this._createWaitIcon();
+        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._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(this.xPanelContent);
+            return xPanel;
+        }
+        catch (e) {
+            showError(e);
+        }
+    }
+
+    _createButtons () {
+        let xButtonLine = createNode("div", {className: "grammalecte_panel_commands"});
+        xButtonLine.appendChild(this.xWaitIcon);
+        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;
+    }
+
+    _createWaitIcon () {
+        let xWaitIcon = createNode("div", {className: "grammalecte_spinner"});
+        xWaitIcon.appendChild(createNode("div", {className: "bounce1"}));
+        xWaitIcon.appendChild(createNode("div", {className: "bounce2"}));
+        return xWaitIcon;
+    }
+
+    _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;
+    }
+
+    insertIntoPage () {
+        document.body.appendChild(this.xPanelNode);
+    }
+
+    show () {
+        this.xPanelNode.style.display = "block";
+    }
+
+    hide () {
+        this.xPanelNode.style.display = "none";
+    }
+
+    center () {
+        let nHeight = (this.bFlexible) ? window.innerHeight-100 : this.nHeight;
+        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 = (this.bFlexible) ? window.innerHeight-100 : this.nHeight;
+        this.xPanelNode.style = `top: 50%; left: -2px; width: ${this.nWidth}px; height: ${nHeight}px; margin-top: -${nHeight/2}px;`;
+    }
+
+    stickToRight () {
+        let nHeight = (this.bFlexible) ? window.innerHeight-100 : this.nHeight;
+        this.xPanelNode.style = `top: 50%; right: -2px; width: ${this.nWidth}px; height: ${nHeight}px; margin-top: -${nHeight/2}px;`;
+    }
+
+    stickToTop () {
+        let nWidth = (this.bFlexible) ? Math.floor(window.innerWidth/2) : this.nWidth;
+        let nHeight = (this.bFlexible) ? Math.floor(window.innerHeight*0.45) : this.nHeight;
+        this.xPanelNode.style = `top: -2px; left: 50%; width: ${nWidth}px; height: ${nHeight}px; margin-left: -${nWidth/2}px;`;
+    }
+
+    stickToBottom () {
+        let nWidth = (this.bFlexible) ? Math.floor(window.innerWidth/2) : this.nWidth;
+        let nHeight = (this.bFlexible) ? Math.floor(window.innerHeight*0.45) : this.nHeight;
+        this.xPanelNode.style = `bottom: -2px; left: 50%; width: ${nWidth}px; height: ${nHeight}px; margin-left: -${nWidth/2}px;`;
+    }
+
+    reduce () {
+        // todo
+    }
+
+    logInnerHTML () {
+        // for debugging
+        console.log(this.xPanelNode.innerHTML);
+    }
+    
+    startWaitIcon () {
+        this.xWaitIcon.style.visibility = "visible";
+    }
+
+    stopWaitIcon () {
+        this.xWaitIcon.style.visibility = "hidden";
+    }
+
+    openURL (sURL) {
+        // todo
+    }
+}
+
+
+/*
+    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,104 @@
+/*
+    CSS for the Text Formatter
+*/
+
+
+/*
+    Options
+*/
+#grammalecte_tf_options {
+    display: flex;
+    padding: 10px;
+}
+.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 .underline:hover {
+    background-color: hsl(210, 10%, 86%);
+    border-radius: 2px;
+}
+#grammalecte_tf_options .blockopt .option {
+    margin: 4px 5px 0 6px;
+    float: left;
+}
+#grammalecte_tf_options .grammalecte_tf_result {
+    float: right;
+    margin: 2px 3px 0 0;
+    font-size: 13px;
+}
+#grammalecte_tf_options .indent {
+    padding-left: 20px;
+}
+#grammalecte_tf_options .inlineblock {
+    display: inline-block;
+    margin-right: 10px;
+}
+#grammalecte_tf_options .rmg10 {
+    margin-right: 10px;
+}
+
+
+
+/*
+    Actions
+*/
+#grammalecte_tf_actions {
+    /*background-color: hsl(120, 10%, 92%);*/
+    display: flex;
+    justify-content: space-between;
+    padding: 15px 15px 10px 15px;
+    border-top: 1px solid hsl(210, 10%, 90%);
+}
+
+.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: 400px;
+}
+#grammalecte_tf_time_res {
+    width: 60px;
+    padding: 5px 10px;
+}
+#grammalecte_tf_apply {
+    background-color: hsl(120, 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,553 @@
+// JavaScript
+// Text formatter
+
+"use strict";
+
+
+class GrammalecteTextFormatter extends GrammalectePanel {
+
+    constructor (...args) {
+        super(...args);
+        this.xTFNode = this._createTextFormatter();
+        this.xPanelContent.appendChild(this.xTFNode);
+        this.xTextArea = null;
+    }
+
+    _createTextFormatter () {
+        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 = this._createFieldset("group_ssp", true, "Espaces surnuméraires");
+            xSSP.appendChild(this._createSimpleOption("o_start_of_paragraph", true, "En début de paragraphe"));
+            xSSP.appendChild(this._createSimpleOption("o_end_of_paragraph", true, "En fin de paragraphe"));
+            xSSP.appendChild(this._createSimpleOption("o_between_words", true, "Entre les mots"));
+            xSSP.appendChild(this._createSimpleOption("o_before_punctuation", true, "Avant les points (.), les virgules (,)"));
+            xSSP.appendChild(this._createSimpleOption("o_within_parenthesis", true, "À l’intérieur des parenthèses"));
+            xSSP.appendChild(this._createSimpleOption("o_within_square_brackets", true, "À l’intérieur des crochets"));
+            xSSP.appendChild(this._createSimpleOption("o_within_quotation_marks", true, "À l’intérieur des guillemets “ et ”"));
+            let xSpace = this._createFieldset("group_space", true, "Espaces manquants");
+            xSpace.appendChild(this._createSimpleOption("o_add_space_after_punctuation", true, "Après , ; : ? ! . …"));
+            xSpace.appendChild(this._createSimpleOption("o_add_space_around_hyphens", true, "Autour des tirets d’incise"));
+            let xNBSP = this._createFieldset("group_nbsp", true, "Espaces insécables");
+            xNBSP.appendChild(this._createSimpleOption("o_nbsp_before_punctuation", true, "Avant : ; ? et !"));
+            xNBSP.appendChild(this._createSimpleOption("o_nbsp_within_quotation_marks", true, "Avec les guillemets « et »"));
+            xNBSP.appendChild(this._createSimpleOption("o_nbsp_before_symbol", true, "Avant % ‰ € $ £ ¥ ˚C"));
+            xNBSP.appendChild(this._createSimpleOption("o_nbsp_within_numbers", true, "À l’intérieur des nombres"));
+            xNBSP.appendChild(this._createSimpleOption("o_nbsp_before_units", true, "Avant les unités de mesure"));
+            let xDelete = this._createFieldset("group_delete", true, "Suppressions");
+            xDelete.appendChild(this._createSimpleOption("o_erase_non_breaking_hyphens", true, "Tirets conditionnels"));
+            let xColumn2 = createNode("div", {className: "grammalecte_tf_column"});
+            let xTypo = this._createFieldset("group_typo", true, "Signes typographiques");
+            xTypo.appendChild(this._createSimpleOption("o_ts_apostrophe", true, "Apostrophe (’)"));
+            xTypo.appendChild(this._createSimpleOption("o_ts_ellipsis", true, "Points de suspension (…)"));
+            xTypo.appendChild(this._createSimpleOption("o_ts_dash_middle", true, "Tirets d’incise :"));
+            xTypo.appendChild(this._createRadioBoxHyphens("hyphen1", "o_ts_m_dash_middle", "o_ts_n_dash_middle", false));
+            xTypo.appendChild(this._createSimpleOption("o_ts_dash_start", true, "Tirets en début de paragraphe :"));
+            xTypo.appendChild(this._createRadioBoxHyphens("hyphen2", "o_ts_m_dash_start", "o_ts_n_dash_start", true));
+            xTypo.appendChild(this._createSimpleOption("o_ts_quotation_marks", true, "Modifier les guillemets droits (\" et ')"));
+            xTypo.appendChild(this._createSimpleOption("o_ts_units", true, "Points médians des unités (N·m, Ω·m…)"));
+            xTypo.appendChild(this._createSimpleOption("o_ts_spell", true, "Ligatures (cœur…) et diacritiques (ça, État…)"));
+            xTypo.appendChild(this._createRadioBoxLigatures());
+            xTypo.appendChild(this._createLigaturesSelection());
+            let xMisc = this._createFieldset("group_misc", true, "Divers");
+            xMisc.appendChild(this._createOrdinalOptions());
+            xMisc.appendChild(this._createSimpleOption("o_etc", true, "Et cætera, etc."));
+            xMisc.appendChild(this._createSimpleOption("o_missing_hyphens", true, "Traits d’union manquants"));
+            xMisc.appendChild(this._createSimpleOption("o_ma_word", true, "Apostrophes manquantes"));
+            xMisc.appendChild(this._createSingleLetterOptions());
+            let xStruct = this._createFieldset("group_struct", false, "Restructuration [!]");
+            xStruct.appendChild(this._createSimpleOption("o_remove_hyphens_at_end_of_paragraphs", false, "Enlever césures en fin de ligne/paragraphe [!]"));
+            xStruct.appendChild(this._createSimpleOption("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"});
+            let xDefaultButton = createNode("div", {id: "grammalecte_tf_reset", textContent: "Par défaut", className: "grammalecte_button", style: "background-color: hsl(210, 50%, 50%)"});
+            xDefaultButton.addEventListener("click", () => { this.reset(); });
+            let xApplyButton = createNode("div", {id: "grammalecte_tf_apply", textContent: "Appliquer", className: "grammalecte_button", style: "background-color: hsl(180, 50%, 50%)"});
+            xApplyButton.addEventListener("click", () => { this.saveOptions(); this.apply(); });
+            xActions.appendChild(xDefaultButton);
+            xActions.appendChild(createNode("progress", {id: "grammalecte_tf_progressbar"}));
+            xActions.appendChild(createNode("span", {id: "grammalecte_tf_time_res", textContent: "…"}));
+            xActions.appendChild(xApplyButton);
+            //xActions.appendChild(createNode("div", {id: "grammalecte_infomsg", textContent: "blabla"}));
+            // create result
+            xTFNode.appendChild(xOptions);
+            xTFNode.appendChild(xActions);
+        }
+        catch (e) {
+            showError(e);
+        }
+        return xTFNode;
+    }
+
+    // Common options
+    _createFieldset (sId, bDefault, sLabel) {
+        let xFieldset = createNode("fieldset", {id: sId, className: "groupblock"});
+        let xLegend = document.createElement("legend");
+        let xGroupOption = createNode("input", {type: "checkbox", id: "o_"+sId, className: "option"}, {default: bDefault});
+        xGroupOption.addEventListener("click", (xEvent) => { this.switchGroup(xEvent.target.id); });
+        xLegend.appendChild(xGroupOption);
+        xLegend.appendChild(createNode("label", {htmlFor: "o_"+sId, textContent: sLabel}));
+        xFieldset.appendChild(xLegend);
+        return xFieldset;
+    }
+
+    _createSimpleOption (sId, bDefault, sLabel) {
+        let xLine = createNode("div", {className: "blockopt underline"});
+        xLine.appendChild(createNode("input", {type: "checkbox", id: sId, className: "option"}, {default: bDefault}));
+        xLine.appendChild(createNode("label", {htmlFor: sId, textContent: sLabel, className: "opt_lbl largew"}));
+        xLine.appendChild(createNode("div", {id: "res_"+sId, className: "grammalecte_tf_result", textContent: "·"}));
+        return xLine;
+    }
+
+    // Hyphens
+    _createRadioBoxHyphens (sName, sIdEmDash, sIdEnDash, bDefaultEmDash) {
+        let xLine = createNode("div", {className: "blockopt indent"});
+        xLine.appendChild(this._createInlineRadioOption(sName, sIdEmDash, "cadratin (—)", bDefaultEmDash));
+        xLine.appendChild(this._createInlineRadioOption(sName, sIdEnDash, "demi-cadratin (—)", !bDefaultEmDash));
+        return xLine;
+    }
+
+    // Ligatures
+    _createRadioBoxLigatures () {
+        let xLine = createNode("div", {className: "blockopt underline"});
+        xLine.appendChild(createNode("div", {id: "res_"+"o_ts_ligature", className: "grammalecte_tf_result", textContent: "·"}));
+        xLine.appendChild(this._createInlineCheckboxOption("o_ts_ligature", "Ligatures", true));
+        xLine.appendChild(this._createInlineRadioOption("liga", "o_ts_ligature_do", "faire", false));
+        xLine.appendChild(this._createInlineRadioOption("liga", "o_ts_ligature_undo", "défaire", true));
+        return xLine;
+    }
+
+    _createLigaturesSelection () {
+        let xLine = createNode("div", {className: "blockopt indent"});
+        xLine.appendChild(this._createInlineCheckboxOption("o_ts_ligature_ff", "ff", true));
+        xLine.appendChild(this._createInlineCheckboxOption("o_ts_ligature_fi", "fi", true));
+        xLine.appendChild(this._createInlineCheckboxOption("o_ts_ligature_ffi", "ffi", true));
+        xLine.appendChild(this._createInlineCheckboxOption("o_ts_ligature_fl", "fl", true));
+        xLine.appendChild(this._createInlineCheckboxOption("o_ts_ligature_ffl", "ffl", true));
+        xLine.appendChild(this._createInlineCheckboxOption("o_ts_ligature_ft", "ft", true));
+        xLine.appendChild(this._createInlineCheckboxOption("o_ts_ligature_st", "st", false));
+        return xLine;
+    }
+
+    // Apostrophes
+    _createSingleLetterOptions () {
+        let xLine = createNode("div", {className: "blockopt indent"});
+        xLine.appendChild(this._createInlineCheckboxOption("o_ma_1letter_lowercase", "lettres isolées (j’ n’ m’ t’ s’ c’ d’ l’)", false));
+        xLine.appendChild(this._createInlineCheckboxOption("o_ma_1letter_uppercase", "Maj.", false));
+        return xLine;
+    }
+
+    // Ordinals
+    _createOrdinalOptions () {
+        let xLine = createNode("div", {className: "blockopt underline"});
+        xLine.appendChild(createNode("div", {id: "res_"+"o_ordinals_no_exponant", className: "grammalecte_tf_result", textContent: "·"}));
+        xLine.appendChild(this._createInlineCheckboxOption("o_ordinals_no_exponant", "Ordinaux (15e, XXIe…)", true));
+        xLine.appendChild(this._createInlineCheckboxOption("o_ordinals_exponant", "e → ᵉ", true));
+        return xLine;
+    }
+    
+
+    // Inline option block
+    _createInlineCheckboxOption (sId, sLabel, bDefault) {
+        let xInlineBlock = createNode("div", {className: "inlineblock"});
+        xInlineBlock.appendChild(createNode("input", {type: "checkbox", id: sId, className: "option"}, {default: bDefault}));
+        xInlineBlock.appendChild(createNode("label", {htmlFor: sId, textContent: sLabel, className: "opt_lbl"}));
+        return xInlineBlock;
+    }
+
+    _createInlineRadioOption (sName, sId, sLabel, bDefault) {
+        let xInlineBlock = createNode("div", {className: "inlineblock"});
+        xInlineBlock.appendChild(createNode("input", {type: "radio", id: sId, name: sName, className:"option"}, {default: bDefault}));
+        xInlineBlock.appendChild(createNode("label", {htmlFor: sId, className: "opt_lbl", textContent: sLabel}));
+        return xInlineBlock;
+    }
+
+
+    /*
+        Actions
+    */
+    start (xTextArea) {
+        this.xTextArea = xTextArea;
+        let xPromise = browser.storage.local.get("tf_options");
+        xPromise.then(this.setOptions.bind(this), this.reset.bind(this));
+    }
+
+    switchGroup (sOptName) {
+        if (document.getElementById(sOptName).checked) {
+            document.getElementById(sOptName.slice(2)).style.opacity = 1;
+        } else {
+            document.getElementById(sOptName.slice(2)).style.opacity = 0.3;
+        }
+        this.resetProgressBar();
+    }
+
+    reset () {
+        this.resetProgressBar();
+        for (let xNode of document.getElementsByClassName("option")) {
+            xNode.checked = (xNode.dataset.default === "true");
+            if (xNode.id.startsWith("o_group_")) {
+                this.switchGroup(xNode.id);
+            }
+        }
+    }
+
+    resetProgressBar () {
+        document.getElementById('grammalecte_tf_progressbar').value = 0;
+        document.getElementById('grammalecte_tf_time_res').textContent = "";
+    }
+
+    setOptions (oOptions) {
+        if (oOptions.hasOwnProperty("tf_options")) {
+            oOptions = oOptions.tf_options;
+        }
+        for (let xNode of document.getElementsByClassName("option")) {
+            //console.log(xNode.id + " > " + oOptions.hasOwnProperty(xNode.id) + ": " + oOptions[xNode.id] + " [" + xNode.dataset.default + "]");
+            xNode.checked = (oOptions.hasOwnProperty(xNode.id)) ? oOptions[xNode.id] : (xNode.dataset.default === "true");
+            if (document.getElementById("res_"+xNode.id) !== null) {
+                document.getElementById("res_"+xNode.id).textContent = "";
+            }
+            if (xNode.id.startsWith("o_group_")) {
+                this.switchGroup(xNode.id);
+            }
+        }
+    }
+
+    saveOptions () {
+        let oOptions = {};
+        for (let xNode of document.getElementsByClassName("option")) {
+            oOptions[xNode.id] = xNode.checked;
+            console.log(xNode.id + ": " + xNode.checked);
+        }
+        browser.storage.local.set({"tf_options": oOptions});
+    }
+
+    apply () {
+        try {
+            const t0 = Date.now();
+            //window.setCursor("wait"); // change pointer
+            this.resetProgressBar();
+            let sText = this.xTextArea.value;
+            document.getElementById('grammalecte_tf_progressbar').max = 7;
+            let n1 = 0, n2 = 0, n3 = 0, n4 = 0, n5 = 0, n6 = 0, n7 = 0;
+            
+            // Restructuration
+            if (document.getElementById("o_group_struct").checked) {
+                if (document.getElementById("o_remove_hyphens_at_end_of_paragraphs").checked) {
+                    [sText, n1] = this.removeHyphenAtEndOfParagraphs(sText);
+                    document.getElementById('res_o_remove_hyphens_at_end_of_paragraphs').textContent = n1;
+                }
+                if (document.getElementById("o_merge_contiguous_paragraphs").checked) {
+                    [sText, n1] = this.mergeContiguousParagraphs(sText);
+                    document.getElementById('res_o_merge_contiguous_paragraphs').textContent = n1;
+                }
+                document.getElementById("o_group_struct").checked = false;
+                this.switchGroup("o_group_struct");
+            }
+            document.getElementById('grammalecte_tf_progressbar').value = 1;
+
+            // espaces surnuméraires
+            if (document.getElementById("o_group_ssp").checked) {
+                if (document.getElementById("o_end_of_paragraph").checked) {
+                    [sText, n1] = this.formatText(sText, "end_of_paragraph");
+                    document.getElementById('res_o_end_of_paragraph').textContent = n1;
+                }
+                if (document.getElementById("o_between_words").checked) {
+                    [sText, n1] = this.formatText(sText, "between_words");
+                    document.getElementById('res_o_between_words').textContent = n1;
+                }
+                if (document.getElementById("o_start_of_paragraph").checked) {
+                    [sText, n1] = this.formatText(sText, "start_of_paragraph");
+                    document.getElementById('res_o_start_of_paragraph').textContent = n1;
+                }
+                if (document.getElementById("o_before_punctuation").checked) {
+                    [sText, n1] = this.formatText(sText, "before_punctuation");
+                    document.getElementById('res_o_before_punctuation').textContent = n1;
+                }
+                if (document.getElementById("o_within_parenthesis").checked) {
+                    [sText, n1] = this.formatText(sText, "within_parenthesis");
+                    document.getElementById('res_o_within_parenthesis').textContent = n1;
+                }
+                if (document.getElementById("o_within_square_brackets").checked) {
+                    [sText, n1] = this.formatText(sText, "within_square_brackets");
+                    document.getElementById('res_o_within_square_brackets').textContent = n1;
+                }
+                if (document.getElementById("o_within_quotation_marks").checked) {
+                    [sText, n1] = this.formatText(sText, "within_quotation_marks");
+                    document.getElementById('res_o_within_quotation_marks').textContent = n1;
+                }
+                document.getElementById("o_group_ssp").checked = false;
+                this.switchGroup("o_group_ssp");
+            }
+            document.getElementById('grammalecte_tf_progressbar').value = 2;
+
+            // espaces typographiques
+            if (document.getElementById("o_group_nbsp").checked) {
+                if (document.getElementById("o_nbsp_before_punctuation").checked) {
+                    [sText, n1] = this.formatText(sText, "nbsp_before_punctuation");
+                    [sText, n2] = this.formatText(sText, "nbsp_repair");
+                    document.getElementById('res_o_nbsp_before_punctuation').textContent = n1 - n2;
+                }
+                if (document.getElementById("o_nbsp_within_quotation_marks").checked) {
+                    [sText, n1] = this.formatText(sText, "nbsp_within_quotation_marks");
+                    document.getElementById('res_o_nbsp_within_quotation_marks').textContent = n1;
+                }
+                if (document.getElementById("o_nbsp_before_symbol").checked) {
+                    [sText, n1] = this.formatText(sText, "nbsp_before_symbol");
+                    document.getElementById('res_o_nbsp_before_symbol').textContent = n1;
+                }
+                if (document.getElementById("o_nbsp_within_numbers").checked) {
+                    [sText, n1] = this.formatText(sText, "nbsp_within_numbers");
+                    document.getElementById('res_o_nbsp_within_numbers').textContent = n1;
+                }
+                if (document.getElementById("o_nbsp_before_units").checked) {
+                    [sText, n1] = this.formatText(sText, "nbsp_before_units");
+                    document.getElementById('res_o_nbsp_before_units').textContent = n1;
+                }
+                document.getElementById("o_group_nbsp").checked = false;
+                this.switchGroup("o_group_nbsp");
+            }
+            document.getElementById('grammalecte_tf_progressbar').value = 3;
+
+            // espaces manquants
+            if (document.getElementById("o_group_typo").checked) {
+                if (document.getElementById("o_ts_units").checked) {
+                    [sText, n1] = this.formatText(sText, "ts_units");
+                    document.getElementById('res_o_ts_units').textContent = n1;
+                }
+            }
+            if (document.getElementById("o_group_space").checked) {
+                if (document.getElementById("o_add_space_after_punctuation").checked) {
+                    [sText, n1] = this.formatText(sText, "add_space_after_punctuation");
+                    [sText, n2] = this.formatText(sText, "add_space_repair");
+                    document.getElementById('res_o_add_space_after_punctuation').textContent = n1 - n2;
+                }
+                if (document.getElementById("o_add_space_around_hyphens").checked) {
+                    [sText, n1] = this.formatText(sText, "add_space_around_hyphens");
+                    document.getElementById('res_o_add_space_around_hyphens').textContent = n1;
+                }
+                document.getElementById("o_group_space").checked = false;
+                this.switchGroup("o_group_space");
+            }
+            document.getElementById('grammalecte_tf_progressbar').value = 4;
+
+            // suppression
+            if (document.getElementById("o_group_delete").checked) {
+                if (document.getElementById("o_erase_non_breaking_hyphens").checked) {
+                    [sText, n1] = this.formatText(sText, "erase_non_breaking_hyphens");
+                    document.getElementById('res_o_erase_non_breaking_hyphens').textContent = n1;
+                }
+                document.getElementById("o_group_delete").checked = false;
+                this.switchGroup("o_group_delete");
+            }
+            document.getElementById('grammalecte_tf_progressbar').value = 5;
+
+            // signes typographiques
+            if (document.getElementById("o_group_typo").checked) {
+                if (document.getElementById("o_ts_apostrophe").checked) {
+                    [sText, n1] = this.formatText(sText, "ts_apostrophe");
+                    document.getElementById('res_o_ts_apostrophe').textContent = n1;
+                }
+                if (document.getElementById("o_ts_ellipsis").checked) {
+                    [sText, n1] = this.formatText(sText, "ts_ellipsis");
+                    document.getElementById('res_o_ts_ellipsis').textContent = n1;
+                }
+                if (document.getElementById("o_ts_dash_start").checked) {
+                    if (document.getElementById("o_ts_m_dash_start").checked) {
+                        [sText, n1] = this.formatText(sText, "ts_m_dash_start");
+                    } else {
+                        [sText, n1] = this.formatText(sText, "ts_n_dash_start");
+                    }
+                    document.getElementById('res_o_ts_dash_start').textContent = n1;
+                }
+                if (document.getElementById("o_ts_dash_middle").checked) {
+                    if (document.getElementById("o_ts_m_dash_middle").checked) {
+                        [sText, n1] = this.formatText(sText, "ts_m_dash_middle");
+                    } else {
+                        [sText, n1] = this.formatText(sText, "ts_n_dash_middle");
+                    }
+                    document.getElementById('res_o_ts_dash_middle').textContent = n1;
+                }
+                if (document.getElementById("o_ts_quotation_marks").checked) {
+                    [sText, n1] = this.formatText(sText, "ts_quotation_marks");
+                    document.getElementById('res_o_ts_quotation_marks').textContent = n1;
+                }
+                if (document.getElementById("o_ts_spell").checked) {
+                    [sText, n1] = this.formatText(sText, "ts_spell");
+                    document.getElementById('res_o_ts_spell').textContent = n1;
+                }
+                if (document.getElementById("o_ts_ligature").checked) {
+                    // ligatures typographiques : fi, fl, ff, ffi, ffl, ft, st
+                    if (document.getElementById("o_ts_ligature_do").checked) {
+                        if (document.getElementById("o_ts_ligature_ffi").checked) {
+                            [sText, n1] = this.formatText(sText, "ts_ligature_ffi_do");
+                        }
+                        if (document.getElementById("o_ts_ligature_ffl").checked) {
+                            [sText, n2] = this.formatText(sText, "ts_ligature_ffl_do");
+                        }
+                        if (document.getElementById("o_ts_ligature_fi").checked) {
+                            [sText, n3] = this.formatText(sText, "ts_ligature_fi_do");
+                        }
+                        if (document.getElementById("o_ts_ligature_fl").checked) {
+                            [sText, n4] = this.formatText(sText, "ts_ligature_fl_do");
+                        }
+                        if (document.getElementById("o_ts_ligature_ff").checked) {
+                            [sText, n5] = this.formatText(sText, "ts_ligature_ff_do");
+                        }
+                        if (document.getElementById("o_ts_ligature_ft").checked) {
+                            [sText, n6] = this.formatText(sText, "ts_ligature_ft_do");
+                        }
+                        if (document.getElementById("o_ts_ligature_st").checked) {
+                            [sText, n7] = this.formatText(sText, "ts_ligature_st_do");
+                        }
+                    }
+                    if (document.getElementById("o_ts_ligature_undo").checked) {
+                        if (document.getElementById("o_ts_ligature_ffi").checked) {
+                            [sText, n1] = this.formatText(sText, "ts_ligature_ffi_undo");
+                        }
+                        if (document.getElementById("o_ts_ligature_ffl").checked) {
+                            [sText, n2] = this.formatText(sText, "ts_ligature_ffl_undo");
+                        }
+                        if (document.getElementById("o_ts_ligature_fi").checked) {
+                            [sText, n3] = this.formatText(sText, "ts_ligature_fi_undo");
+                        }
+                        if (document.getElementById("o_ts_ligature_fl").checked) {
+                            [sText, n4] = this.formatText(sText, "ts_ligature_fl_undo");
+                        }
+                        if (document.getElementById("o_ts_ligature_ff").checked) {
+                            [sText, n5] = this.formatText(sText, "ts_ligature_ff_undo");
+                        }
+                        if (document.getElementById("o_ts_ligature_ft").checked) {
+                            [sText, n6] = this.formatText(sText, "ts_ligature_ft_undo");
+                        }
+                        if (document.getElementById("o_ts_ligature_st").checked) {
+                            [sText, n7] = this.formatText(sText, "ts_ligature_st_undo");
+                        }
+                    }
+                    document.getElementById('res_o_ts_ligature').textContent = n1 + n2 + n3 + n4 + n5 + n6 + n7;
+                }
+                document.getElementById("o_group_typo").checked = false;
+                this.switchGroup("o_group_typo");
+            }
+            document.getElementById('grammalecte_tf_progressbar').value = 6;
+
+            // divers
+            if (document.getElementById("o_group_misc").checked) {
+                if (document.getElementById("o_ordinals_no_exponant").checked) {
+                    if (document.getElementById("o_ordinals_exponant").checked) {
+                        [sText, n1] = this.formatText(sText, "ordinals_exponant");
+                    } else {
+                        [sText, n1] = this.formatText(sText, "ordinals_no_exponant");
+                    }
+                    document.getElementById('res_o_ordinals_no_exponant').textContent = n1;
+                }
+                if (document.getElementById("o_etc").checked) {
+                    [sText, n1] = this.formatText(sText, "etc");
+                    document.getElementById('res_o_etc').textContent = n1;
+                }
+                if (document.getElementById("o_missing_hyphens").checked) {
+                    [sText, n1] = this.formatText(sText, "missing_hyphens");
+                    document.getElementById('res_o_missing_hyphens').textContent = n1;
+                }
+                if (document.getElementById("o_ma_word").checked) {
+                    [sText, n1] = this.formatText(sText, "ma_word");
+                    if (document.getElementById("o_ma_1letter_lowercase").checked) {
+                        [sText, n1] = this.formatText(sText, "ma_1letter_lowercase");
+                        if (document.getElementById("o_ma_1letter_uppercase").checked) {
+                            [sText, n1] = this.formatText(sText, "ma_1letter_uppercase");
+                        }
+                    }
+                    document.getElementById('res_o_ma_word').textContent = n1;
+                }
+                document.getElementById("o_group_misc").checked = false;
+                this.switchGroup("o_group_misc");
+            }
+            document.getElementById('grammalecte_tf_progressbar').value = document.getElementById('grammalecte_tf_progressbar').max;
+            // end of processing
+
+            //window.setCursor("auto"); // restore pointer
+
+            const t1 = Date.now();
+            document.getElementById('grammalecte_tf_time_res').textContent = this.getTimeRes((t1-t0)/1000);
+            this.xTextArea.value = sText;
+        }
+        catch (e) {
+            showError(e);
+        }
+    }
+
+    formatText (sText, sOptName) {
+        let nCount = 0;
+        try {
+            if (!oReplTable.hasOwnProperty(sOptName)) {
+                console.log("# Error. TF: there is no option “" + sOptName+ "”.");
+                return [sText, nCount];
+            }
+            for (let [zRgx, sRep] of oReplTable[sOptName]) {
+                nCount += (sText.match(zRgx) || []).length;
+                sText = sText.replace(zRgx, sRep);
+            }
+        }
+        catch (e) {
+            showError(e);
+        }
+        return [sText, nCount];
+    }
+
+    removeHyphenAtEndOfParagraphs (sText) {
+        let nCount = (sText.match(/-[  ]*\n/gm) || []).length;
+        sText = sText.replace(/-[  ]*\n/gm, "");
+        return [sText, nCount];
+    }
+
+    mergeContiguousParagraphs (sText) {
+        let nCount = 0;
+        sText = sText.replace(/^[  ]+$/gm, ""); // clear empty paragraphs
+        let s = "";
+        for (let sParagraph of this.getParagraph(sText)) {
+            if (sParagraph === "") {
+                s += "\n";
+            } else {
+                s += sParagraph + " ";
+                nCount += 1;
+            }
+        }
+        s = s.replace(/  +/gm, " ").replace(/ $/gm, "");
+        return [s, nCount];
+    }
+
+    * getParagraph (sText) {
+        // generator: returns paragraphs of text
+        let iStart = 0;
+        let iEnd = 0;
+        while ((iEnd = sText.indexOf("\n", iStart)) !== -1) {
+            yield sText.slice(iStart, iEnd);
+            iStart = iEnd + 1;
+        }
+        yield sText.slice(iStart);
+    }
+
+    getTimeRes (n) {
+        // returns duration in seconds as string
+        if (n < 10) {
+            return n.toFixed(3).toString() + " s";
+        }
+        if (n < 100) {
+            return n.toFixed(2).toString() + " s";
+        }
+        if (n < 1000) {
+            return n.toFixed(1).toString() + " s";
+        }
+        return n.toFixed().toString() + " s";
+    }
+}

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,296 @@
+/*
+    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) {
+    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 i = 0;
+    for (let sParagraph of text.getParagraph(sText)) {
+        let aGrammErr = gc_engine.parse(sParagraph, sCountry, bDebug, bContext);
+        let aSpellErr = oTokenizer.getSpellingErrors(sParagraph, oDict);
+        postMessage(createResponse("parseAndSpellcheck", {sParagraph: sParagraph, iParaNum: i, aGrammErr: aGrammErr, aSpellErr: aSpellErr}, dInfo, false));
+        i += 1;
+    }
+    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,87 @@
   "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/panels_content.css",
+        "content_scripts/tf_content.css",
+        "content_scripts/gc_content.css",
+        "content_scripts/lxg_content.css"
+      ],
+      "js": [
+        "content_scripts/panels_content.js",
+        "grammalecte/fr/textformatter.js",
+        "content_scripts/tf_content.js",
+        "content_scripts/gc_content.js",
+        "content_scripts/lxg_content.js",
+        "content_scripts/content_modifier.js"
+      ]
+    }
+  ],
+  "commands": {
+    "conjugueur_tab": {
+      "suggested_key": {
+        "default": "Ctrl+Shift+F6"
+      },
+      "description": "Ouvre le conjugueur dans un onglet"
+    },
+    "conjugueur_window": {
+      "suggested_key": {
+        "default": "Ctrl+Shift+F7"
+      },
+      "description": "Ouvre le conjugueur dans une fenêtre"
+    }
   },
   "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",
+    "storage"
+  ],
+  "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();

Index: gc_lang/fr/webext/panel/main.css
==================================================================
--- gc_lang/fr/webext/panel/main.css
+++ gc_lang/fr/webext/panel/main.css
@@ -66,52 +66,62 @@
 
 /* Main classes */
 
 html {
     box-sizing: border-box;
-    width: 530px;
-    height: 880px;
+    width: 400px;
+    height: 500px;
     font-family: "Trebuchet MS", "Liberation Sans", sans-serif;
 }
 body {
-    width: 530px;
-    height: 880px;
+    width: 400px;
+    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,24 @@
 #page p {
     margin: 10px 0 5px 0;
 }
 
 #home_page {
-  display: block;
-  padding: 20px;
-}
-
-#tf_page {
-  display: none;
-  padding: 20px;
-}
-#gc_page {
-  display: none;
-  padding: 20px 20px 30px 20px;
+    display: block;
+    padding: 20px;
+}
+#help_page {
+    display: none;
+    padding: 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 +155,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 +179,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,489 +7,117 @@
   </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="help_page"><i class="fa fa-coffee icon"></i> Help</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>
+          <p class="center"><img src="../img/logo120_text.png" alt="Grammalecte"></p>
+          <p class="underline center">
+            <b>Version ${version}</b>
+          </p>
+          <p class="underline center">
+            <b>GPL 3 · <a id="website" class="simplelink" href="#" onclick="return false;">Site web</a></b>
+          </p>
+          <p id="links" class="center">
+            <a id="spelling" class="bluebutton" href="#" onclick="return false;">Options</a> ·
+            <a id="conjugueur" class="redbutton" href="#" onclick="return false;">Conjugueur</a>
+          </p>
+          
+          <footer id="thanks">
+              <p class="center" data-l10n-id="thanks1">Grammalecte remercie</p>
+              <p class="center">
+                  <a id="mainsponsor" href="#" onclick="return false;">
+                      <img src="../img/LaMouette.png" alt="La Mouette" style="border: 1px solid #C0C0C0" />
+                  </a>
+              </p>
+              <p class="center">
+                  <a id="mainsponsor2" href="#" onclick="return false;">
+                      <img src="../img/Algoo_logo.png" alt="Algoo" style="border: 1px solid #C0C0C0" />
+                  </a>
+              </p>
+              <p class="center">
+                  <a id="othersponsors" class="link" href="https://www.dicollecte.org/#thanks" onclick="return false;">
+                      <b>et tous ceux qui l’ont soutenu.</b>
+                  </a>
+              </p>
+          </footer>
         </section>
 
-        <section id="gc_page" class="page">
-          <h1>CORRECTEUR GRAMMATICAL</h1>
-          <div id="paragraphs_list"></div>
+        <section id="help_page" class="page">
+          <h1>AIDE</h1>
+          <div id="help_section">
+            <p data-l10n-id="useContextMenu"></p>
+            <p class="right"><img src="img/contextmenu.png" title="menu contextuel sur zone de texte" /></p>
+            <p><b data-l10n-id="shortcuts"></b></p>
+            <p><span class="key" data-l10n-id="keyTF"></span> &#10097; <span class="goto" data-l10n-id="keyLabelTF"></span></p>
+            <p><span class="key" data-l10n-id="keyGC"></span> &#10097; <span class="goto" data-l10n-id="keyLabelGC"></span></p>
+            <p><span class="key" data-l10n-id="keyCJ"></span> &#10097; <span class="goto" data-l10n-id="keyLabelCJ"></span></p>
+          </div>
         </section>
 
         <section id="gc_options_page" class="page">
           <h1>OPTIONS GRAMMATICALES</h1>
+          <div id="grammar_options">
+            ${optionsHTML}
+          </div>
+          <p class="center" style="margin: 10px 0;"><a id="default_options" href="#" onclick="return false;">Options par défaut</a></p>
         </section>
 
         <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>
+          <div id="spelling_options">
+            <p id="dictionaries_info" data-l10n-id="dictionaries_info"></p>
+            <div class="dict_section" id="fr-FR-modern_box">
+                <p><input type="checkbox" id="fr-FR-modern" /> <label for="fr-FR-modern">“Moderne”</label></p>
+                <p class="dict_description">Ce dictionnaire propose l’orthographe telle qu’elle est écrite aujourd’hui le plus couramment. C’est le dictionnaire recommandé si le français n’est pas votre langue maternelle ou si vous ne désirez qu’une seule graphie correcte par mot.</p>
+            </div>
+            <div class="dict_section" id="fr-FR-classic_box">
+                <p><input type="checkbox" id="fr-FR-classic" /> <label for="fr-FR-classic">“Classique” (recommandé)</label></p>
+                <p class="dict_description">Il s’agit du dictionnaire “Moderne”, avec des graphies classiques en sus, certaines encore communément utilisées, d’autres désuètes. C’est le dictionnaire recommandé si le français est votre langue maternelle.</p>
+            </div>
+            <div class="dict_section" id="fr-FR-reform_box">
+                <p><input type="checkbox" id="fr-FR-reform" /> <label for="fr-FR-reform">“Réforme 1990”</label></p>
+                <p class="dict_description">Avec ce dictionnaire, seule l’orthographe réformée est reconnue. Attendu que bon nombre de graphies réformées sont considérées comme erronées par beaucoup, ce dictionnaire est déconseillé. Les graphies passées dans l’usage sont déjà incluses dans le dictionnaire “Moderne”.</p>
+            </div>
+            <div class="dict_section" id="fr-FR-classic-reform_box">
+                <p><input type="checkbox" id="fr-FR-classic-reform" /> <label for="fr-FR-classic-reform">“Toutes variantes”</label></p>
+                <p class="dict_description">Ce dictionnaire contient les variantes graphiques, classiques, réformées, ainsi que d’autres plus rares encore. Ce dictionnaire est déconseillé à ceux qui ne connaissent pas très bien la langue française.</p>
+            </div>
+          </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 -->
+            <div><a href="https://www.dicollecte.org/test.html" target="_blank">Page for tests</a></div>
+            <textarea id="text_to_test" rows="10"></textarea>
+            <div id="fulltests" class="button blue">Tests complets</div> <div id="text_to_test" class="button green fright">Analyser</div>
+          </div>
+          <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: