Index: gc_lang/fr/build.py
==================================================================
--- gc_lang/fr/build.py
+++ gc_lang/fr/build.py
@@ -70,44 +70,30 @@
     spfZip = "_build/" + dVars['tb_identifier'] + "-v" + dVars['version'] + '.mailext.xpi'
     hZip = zipfile.ZipFile(spfZip, mode='w', compression=zipfile.ZIP_DEFLATED)
     _copyGrammalecteJSPackageInZipFile(hZip, sLang)
     for spf in ["LICENSE.txt", "LICENSE.fr.txt"]:
         hZip.write(spf)
-    dVars = _createOptionsForThunderbird(dVars)
+    helpers.addFolderToZipAndFileFile(hZip, "gc_lang/"+sLang+"/webext/gce_worker.js")
     helpers.addFolderToZipAndFileFile(hZip, "gc_lang/"+sLang+"/mailext", "", dVars, True)
     helpers.addFolderToZipAndFileFile(hZip, "gc_lang/"+sLang+"/webext/3rd", "3rd", dVars, True)
     helpers.addFolderToZipAndFileFile(hZip, "gc_lang/"+sLang+"/webext/_locales", "_locales", dVars, True)
     helpers.addFolderToZipAndFileFile(hZip, "gc_lang/"+sLang+"/webext/content_scripts", "content_scripts", dVars, True)
     helpers.addFolderToZipAndFileFile(hZip, "gc_lang/"+sLang+"/webext/fonts", "fonts", dVars, True)
     helpers.addFolderToZipAndFileFile(hZip, "gc_lang/"+sLang+"/webext/img", "img", dVars, True)
     helpers.addFolderToZipAndFileFile(hZip, "gc_lang/"+sLang+"/webext/panel", "panel", dVars, True)
     hZip.close()
-    #spExtension = dVars['win_tb_debug_extension_path']  if platform.system() == "Windows"  else dVars['linux_tb_debug_extension_path']
-    #if os.path.isdir(spExtension):
-    #    file_util.copy_file(spfZip, spExtension + "/" + dVars['tb_identifier']+ ".xpi")  # Filename for TB is just <identifier.xpi>
-    #    print(f"TB extension copied in <{spExtension}>")
-    #spExtension = dVars['win_tb_beta_extension_path']  if platform.system() == "Windows"  else dVars['linux_tb_beta_extension_path']
-    #if os.path.isdir(spExtension):
-    #    print(f"TB extension copied in <{spExtension}>")
-    #    file_util.copy_file(spfZip, spExtension + "/" + dVars['tb_identifier']+ ".xpi")  # Filename for TB is just <identifier.xpi>
-
-
-def _createOptionsForThunderbird (dVars):
-    dVars['sXULTabs'] = ""
-    dVars['sXULTabPanels'] = ""
-    # dialog options
-    for sSection, lOpt in dVars['lStructOpt']:
-        dVars['sXULTabs'] += '    <tab label="&option.label.'+sSection+';"/>\n'
-        dVars['sXULTabPanels'] += '    <tabpanel orient="vertical">\n      <label class="section" value="&option.label.'+sSection+';" />\n'
-        for lLineOpt in lOpt:
-            for sOpt in lLineOpt:
-                dVars['sXULTabPanels'] += '      <checkbox id="option_'+sOpt+'" class="option" label="&option.label.'+sOpt+';" />\n'
-        dVars['sXULTabPanels'] += '    </tabpanel>\n'
-    # translation data
-    for sLang in dVars['dOptLabel'].keys():
-        dVars['gc_options_labels_'+sLang] = "\n".join( [ "<!ENTITY option.label." + sOpt + ' "' + dVars['dOptLabel'][sLang][sOpt][0] + '">'  for sOpt in dVars['dOptLabel'][sLang] ] )
-    return dVars
+    # Note about copying Thunderbird extension directly into the profile:
+    # In Options > Configuration editor (about:config), deactivate option <xpinstall.whitelist.required>
+    # If <manifest.json> is changed, you must reinstall the extension manually
+    spExtension = dVars['win_tb_debug_extension_path']  if platform.system() == "Windows"  else dVars['linux_tb_debug_extension_path']
+    if os.path.isdir(spExtension):
+        file_util.copy_file(spfZip, f"{spExtension}/{dVars['tb_identifier']}.xpi")  # Filename for TB is just <identifier.xpi>
+        print(f"Thunderbird extension copied in <{spExtension}>")
+    spExtension = dVars['win_tb_beta_extension_path']  if platform.system() == "Windows"  else dVars['linux_tb_beta_extension_path']
+    if os.path.isdir(spExtension):
+        file_util.copy_file(spfZip, f"{spExtension}/{dVars['tb_identifier']}.xpi")  # Filename for TB is just <identifier.xpi>
+        print(f"Thunderbird extension copied in <{spExtension}>")
 
 
 def _copyGrammalecteJSPackageInZipFile (hZip, sLang, sAddPath=""):
     for sf in os.listdir("grammalecte-js"):
         if not os.path.isdir("grammalecte-js/"+sf):

Index: gc_lang/fr/config.ini
==================================================================
--- gc_lang/fr/config.ini
+++ gc_lang/fr/config.ini
@@ -57,11 +57,10 @@
 tb_name = Grammalecte [fr]
 win_tb_path = C:\Program Files\Mozilla Thunderbird\thunderbird.exe
 win_tb_beta_path = C:\Program Files\Thunderbird Daily\thunderbird.exe
 linux_tb_path = /usr/bin/thunderbird
 linux_tb_beta_path = /usr/bin/thunderbird
-# useless now
 win_tb_debug_extension_path = D:\_temp\tb-debug.profile\extensions
 linux_tb_debug_extension_path = ~/tb-debug.profile/extensions
 win_tb_beta_extension_path = D:\_temp\tb-beta.profile\extensions
 linux_tb_beta_extension_path = ~/tb-beta.profile/extensions
 # Set Thunderbird folder in your PATH variable

DELETED gc_lang/fr/mailext/README.txt
Index: gc_lang/fr/mailext/README.txt
==================================================================
--- gc_lang/fr/mailext/README.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-
-= GRAMMALECTE =
-
-French grammar checker
-By Olivier R. (olivier /at/ grammalecte /dot/ net)
-
-Website: https://grammalecte.net/
-
-License: GPL 3 -- http://www.gnu.org/copyleft/gpl.html
-
-Grammalecte for Firefox is a derivative tool born from the version
-for LibreOffice written in Python.
-
-Written in JavaScript ES6/ES7.

Index: gc_lang/fr/mailext/background.js
==================================================================
--- gc_lang/fr/mailext/background.js
+++ gc_lang/fr/mailext/background.js
@@ -1,10 +1,29 @@
 // Background
 
+/* jshint esversion:6, -W097 */
+/* jslint esversion:6 */
+/* global helpers, showError, Worker, chrome, console */
+
 "use strict";
 
-// Draft for later
+
+console.log("[Grammalecte] background");
+
+
+// Chrome don’t follow the W3C specification:
+// https://browserext.github.io/browserext/
+let bChrome = false;
+let bThunderbird = false;
+if (typeof(browser) !== "object") {
+    var browser = chrome;
+    bChrome = true;
+}
+if (typeof(messenger) === "object") {
+    bThunderbird = true;
+}
+
 
 const oWorkerHandler = {
     xGCEWorker: null,
 
     nLastTimeWorkerResponse: 0,  // milliseconds since 1970-01-01
@@ -107,20 +126,63 @@
 
 
 const oInitHandler = {
 
     initUIOptions: function () {
+        if (bChrome) {
+            browser.storage.local.get("ui_options", this._initUIOptions);
+            browser.storage.local.get("autorefresh_option", this._initUIOptions);
+            return;
+        }
         browser.storage.local.get("ui_options").then(this._initUIOptions, showError);
         browser.storage.local.get("autorefresh_option").then(this._initUIOptions, showError);
     },
 
     initGrammarChecker: function () {
+        if (bChrome) {
+            browser.storage.local.get("gc_options", this._initGrammarChecker);
+            browser.storage.local.get("main_dic_name", this._setSpellingDictionaries);
+            browser.storage.local.get("personal_dictionary", this._setSpellingDictionaries);
+            browser.storage.local.get("community_dictionary", this._setSpellingDictionaries);
+            browser.storage.local.get("oPersonalDictionary", this._setSpellingDictionaries); // deprecated
+            browser.storage.local.get("sc_options", this._initSCOptions);
+            return;
+        }
         browser.storage.local.get("gc_options").then(this._initGrammarChecker, showError);
+        browser.storage.local.get("main_dic_name", this._setSpellingDictionaries);
         browser.storage.local.get("personal_dictionary").then(this._setSpellingDictionaries, showError);
         browser.storage.local.get("community_dictionary").then(this._setSpellingDictionaries, showError);
+        browser.storage.local.get("oPersonalDictionary").then(this._setSpellingDictionaries, showError); // deprecated
         browser.storage.local.get("sc_options").then(this._initSCOptions, showError);
     },
+
+    registerComposeScripts: async function () {
+        // For Thunderbird only
+        if (bThunderbird) {
+            let xRegisteredScripts = await browser.composeScripts.register({
+                /*css: [
+                    // Any number of code or file objects could be listed here.
+                    { code: "body { background-color: red; }" },
+                    { file: "compose.css" },
+                ],*/
+                js: [
+                    // Any number of code or file objects could be listed here.
+                    //{ code: `document.body.textContent = "Hey look, the script ran!";` },
+                    { file: "content_scripts/html_src.js" },
+                    { file: "content_scripts/panel.js" },
+                    { file: "grammalecte/fr/textformatter.js" },
+                    { file: "content_scripts/panel_tf.js" },
+                    { file: "content_scripts/panel_gc.js" },
+                    { file: "content_scripts/message_box.js" },
+                    { file: "content_scripts/menu.js" },
+                    { file: "content_scripts/init.js" }
+                ]
+            });
+            // To unregister scripts:
+            // await xRegisteredScripts.unregister();
+        }
+    },
 
     _initUIOptions: function (oSavedOptions) {
         if (!oSavedOptions.hasOwnProperty("ui_options")) {
             browser.storage.local.set({"ui_options": {
                 textarea: true,
@@ -150,10 +212,20 @@
             showError(e);
         }
     },
 
     _setSpellingDictionaries: function (oData) {
+        if (oData.hasOwnProperty("oPersonalDictionary")) {
+            // deprecated (to be removed in 2020)
+            console.log("personal dictionary migration");
+            browser.storage.local.set({ "personal_dictionary": oData["oPersonalDictionary"] });
+            oWorkerHandler.xGCEWorker.postMessage({ sCommand: "setDictionary", oParam: { sDictionary: "personal", oDict: oData["oPersonalDictionary"] }, oInfo: {} });
+            browser.storage.local.remove("oPersonalDictionary");
+        }
+        if (oData.hasOwnProperty("main_dic_name")) {
+            oWorkerHandler.xGCEWorker.postMessage({ sCommand: "setDictionary", oParam: { sDictionary: "main", oDict: oData["main_dic_name"] }, oInfo: {sExtPath: browser.extension.getURL("")} });
+        }
         if (oData.hasOwnProperty("community_dictionary")) {
             oWorkerHandler.xGCEWorker.postMessage({ sCommand: "setDictionary", oParam: { sDictionary: "community", oDict: oData["community_dictionary"] }, oInfo: {} });
         }
         if (oData.hasOwnProperty("personal_dictionary")) {
             oWorkerHandler.xGCEWorker.postMessage({ sCommand: "setDictionary", oParam: { sDictionary: "personal", oDict: oData["personal_dictionary"] }, oInfo: {} });
@@ -170,20 +242,224 @@
             oWorkerHandler.xGCEWorker.postMessage({ sCommand: "setDictionaryOnOff", oParam: { sDictionary: "personal", bActivate: true }, oInfo: {} });
         } else {
             oWorkerHandler.xGCEWorker.postMessage({ sCommand: "setDictionaryOnOff", oParam: { sDictionary: "community", bActivate: oData.sc_options["community"] }, oInfo: {} });
             oWorkerHandler.xGCEWorker.postMessage({ sCommand: "setDictionaryOnOff", oParam: { sDictionary: "personal", bActivate: oData.sc_options["personal"] }, oInfo: {} });
         }
-    }
+    },
 }
 
 // start the Worker for the GC
 oWorkerHandler.start();
 
 // init the options stuff and start the GC
 oInitHandler.initUIOptions();
 oInitHandler.initGrammarChecker();
+oInitHandler.registerComposeScripts(); // Thunderbird only
+
+
+// When the extension is installed or updated
+browser.runtime.onInstalled.addListener(function (oDetails) {
+    // launched at installation or update
+    // https://developer.mozilla.org/fr/Add-ons/WebExtensions/API/runtime/onInstalled
+    if (oDetails.reason == "update"  ||  oDetails.reason == "installed") {
+        // todo
+        //browser.tabs.create({url: "http://grammalecte.net"});
+    }
+});
+
+
+
+/*
+    Ports from content-scripts
+*/
+
+let dConnx = new Map();
+
+
+/*
+    Messages from the extension (not the Worker)
+*/
+function handleMessage (oRequest, xSender, sendResponse) {
+    // message from panels
+    //console.log(xSender);
+    let {sCommand, oParam, oInfo} = oRequest;
+    switch (sCommand) {
+        case "getOptions":
+        case "getDefaultOptions":
+        case "setOptions":
+        case "setOption":
+        case "resetOptions":
+        case "textToTest":
+        case "fullTests":
+        case "setDictionary":
+        case "setDictionaryOnOff":
+            oWorkerHandler.xGCEWorker.postMessage(oRequest);
+            break;
+        case "restartWorker":
+            oWorkerHandler.restart(oParam["nDelayLimit"]);
+            break;
+        case "openURL":
+            browser.tabs.create({url: oParam.sURL});
+            break;
+        case "openConjugueurTab":
+            openConjugueurTab();
+            break;
+        case "openLexiconEditor":
+            openLexiconEditor(oParam["dictionary"]);
+            break;
+        case "openDictionaries":
+            openDictionaries();
+            break;
+        default:
+            console.log("[background] Unknown command: " + sCommand);
+            console.log(oRequest);
+    }
+    //sendResponse({response: "response from background script"});
+}
+
+browser.runtime.onMessage.addListener(handleMessage);
+
+
+function handleConnexion (xPort) {
+    // Messages from tabs
+    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) {
+        let {sCommand, oParam, oInfo} = oRequest;
+        switch (sCommand) {
+            case "ping":
+                //console.log("[background] ping");
+                xPort.postMessage({sActionDone: "ping", result: null, bInfo: null, bEnd: true, bError: false});
+                break;
+            case "parse":
+            case "parseAndSpellcheck":
+            case "parseAndSpellcheck1":
+            case "parseFull":
+            case "getListOfTokens":
+            case "getSpellSuggestions":
+            case "getVerb":
+                oRequest.oInfo.iReturnPort = iPortId; // we pass the id of the return port to receive answer
+                oWorkerHandler.xGCEWorker.postMessage(oRequest);
+                break;
+            case "restartWorker":
+                oWorkerHandler.restart(oParam["nDelayLimit"]);
+                break;
+            case "openURL":
+                browser.tabs.create({url: oParam.sURL});
+                break;
+            case "openConjugueurTab":
+                openConjugueurTab();
+                break;
+            case "openConjugueurWindow":
+                openConjugueurWindow();
+                break;
+            case "openLexiconEditor":
+                openLexiconEditor();
+                break;
+            default:
+                console.log("[background] Unknown command: " + sCommand);
+                console.log(oRequest);
+        }
+    });
+    //xPort.postMessage({sActionDone: "newId", result: iPortId});
+    xPort.postMessage({sActionDone: "init", sUrl: browser.extension.getURL("")});
+}
+
+browser.runtime.onConnect.addListener(handleConnexion);
+
+
+/*
+    Context Menu
+    (not for MailExtension)
+*/
+if (!bThunderbird) {
+    // Analyze
+    browser.contextMenus.create({ id: "grammar_checker_editable",   title: "Analyser cette zone de texte",              contexts: ["editable"] });
+    browser.contextMenus.create({ id: "grammar_checker_selection",  title: "Analyser la sélection",                     contexts: ["selection"] });
+    browser.contextMenus.create({ id: "grammar_checker_iframe",     title: "Analyser le contenu de ce cadre",           contexts: ["frame"] });
+    browser.contextMenus.create({ id: "grammar_checker_page",       title: "Analyser la page",                          contexts: ["all"] });
+    browser.contextMenus.create({ id: "separator_tools",            type: "separator",                                  contexts: ["all"] });
+    // Tools
+    browser.contextMenus.create({ id: "conjugueur_tab",             title: "Conjugueur [onglet]",                       contexts: ["all"] });
+    browser.contextMenus.create({ id: "conjugueur_window",          title: "Conjugueur [fenêtre]",                      contexts: ["all"] });
+    //browser.contextMenus.create({ id: "dictionaries",               title: "Dictionnaires",                             contexts: ["all"] });
+    browser.contextMenus.create({ id: "lexicon_editor",             title: "Éditeur lexical",                           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
+        // confusing: no way to get the node where we click?!
+        switch (xInfo.menuItemId) {
+            // analyze
+            case "grammar_checker_editable":
+            case "grammar_checker_page":
+                sendCommandToTab(xTab.id, xInfo.menuItemId);
+                break;
+            case "grammar_checker_iframe":
+                sendCommandToTab(xTab.id, xInfo.menuItemId, xInfo.frameId);
+                break;
+            case "grammar_checker_selection":
+                sendCommandToTab(xTab.id, xInfo.menuItemId, xInfo.selectionText);
+                break;
+            // tools
+            case "conjugueur_window":
+                openConjugueurWindow();
+                break;
+            case "conjugueur_tab":
+                openConjugueurTab();
+                break;
+            case "lexicon_editor":
+                openLexiconEditor();
+                break;
+            case "dictionaries":
+                openDictionaries();
+                break;
+            default:
+                console.log("[Background] Unknown menu id: " + xInfo.menuItemId);
+                console.log(xInfo);
+                console.log(xTab);
+        }
+    });
+}
+
+
+
+/*
+    Keyboard shortcuts
+*/
+browser.commands.onCommand.addListener(function (sCommand) {
+    switch (sCommand) {
+        case "grammar_checker":
+            sendCommandToCurrentTab("shortcutGrammarChecker");
+            break;
+        case "conjugueur_tab":
+            openConjugueurTab();
+            break;
+        case "lexicon_editor":
+            openLexiconEditor();
+            break;
+        case "dictionaries":
+            openDictionaries();
+            break;
+    }
+});
+
+
+/*
+    Tabs
+*/
+let nTabLexiconEditor = null;
+let nTabDictionaries = null;
+let nTabConjugueur = null;
 
+browser.tabs.onRemoved.addListener(function (nTabId, xRemoveInfo) {
+    switch (nTabId) {
+        case nTabLexiconEditor: nTabLexiconEditor = null; break;
+        case nTabDictionaries:  nTabDictionaries = null; break;
+        case nTabConjugueur:    nTabConjugueur = null; break;
+    }
+});
 
 
 /*
     Actions
 */
@@ -192,11 +468,124 @@
     if (dOptions instanceof Map) {
         dOptions = helpers.mapToObject(dOptions);
     }
     browser.storage.local.set({"gc_options": dOptions});
 }
+
+function sendCommandToTab (iTab, sCommand, result=null) {
+    let xTabPort = dConnx.get(iTab);
+    xTabPort.postMessage({sActionDone: sCommand, result: result, oInfo: null, bEnd: false, bError: false});
+}
+
+function sendCommandToCurrentTab (sCommand) {
+    if (bChrome) {
+        browser.tabs.query({ currentWindow: true, active: true }, (lTabs) => {
+            for (let xTab of lTabs) {
+                //console.log(xTab);
+                browser.tabs.sendMessage(xTab.id, {sActionRequest: sCommand});
+            }
+        });
+        return;
+    }
+    browser.tabs.query({ currentWindow: true, active: true }).then((lTabs) => {
+        for (let xTab of lTabs) {
+            //console.log(xTab);
+            browser.tabs.sendMessage(xTab.id, {sActionRequest: sCommand});
+        }
+    }, showError);
+}
+
+function sendCommandToAllTabs (sCommand) {
+    for (let [iTab, xTabPort] of dConnx.entries()) {
+        xTabPort.postMessage({sActionDone: sCommand, result: null, oInfo: null, bEnd: false, bError: false});
+    }
+}
+
+function openLexiconEditor (sName="__personal__") {
+    if (nTabLexiconEditor === null) {
+        if (bChrome) {
+            browser.tabs.create({
+                url: browser.extension.getURL("panel/lex_editor.html")
+            }, onLexiconEditorOpened);
+            return;
+        }
+        let xLexEditor = browser.tabs.create({
+            url: browser.extension.getURL("panel/lex_editor.html")
+        });
+        xLexEditor.then(onLexiconEditorOpened, showError);
+    }
+    else {
+        browser.tabs.update(nTabLexiconEditor, {active: true});
+    }
+}
+
+function onLexiconEditorOpened (xTab) {
+    nTabLexiconEditor = xTab.id;
+}
+
+function openDictionaries () {
+    if (nTabDictionaries === null) {
+        if (bChrome) {
+            browser.tabs.create({
+                url: browser.extension.getURL("panel/dictionaries.html")
+            }, onDictionariesOpened);
+            return;
+        }
+        let xLexEditor = browser.tabs.create({
+            url: browser.extension.getURL("panel/dictionaries.html")
+        });
+        xLexEditor.then(onDictionariesOpened, showError);
+    }
+    else {
+        browser.tabs.update(nTabDictionaries, {active: true});
+    }
+}
+
+function onDictionariesOpened (xTab) {
+    nTabDictionaries = xTab.id;
+}
+
+function openConjugueurTab () {
+    if (nTabConjugueur === null) {
+        if (bChrome) {
+            browser.tabs.create({
+                url: browser.extension.getURL("panel/conjugueur.html")
+            }, onConjugueurOpened);
+            return;
+        }
+        let xConjTab = browser.tabs.create({
+            url: browser.extension.getURL("panel/conjugueur.html")
+        });
+        xConjTab.then(onConjugueurOpened, showError);
+    }
+    else {
+        browser.tabs.update(nTabConjugueur, {active: true});
+    }
+}
+
+function onConjugueurOpened (xTab) {
+    nTabConjugueur = xTab.id;
+}
+
+function openConjugueurWindow () {
+    if (bChrome) {
+        browser.windows.create({
+            url: browser.extension.getURL("panel/conjugueur.html"),
+            type: "popup",
+            width: 710,
+            height: 980
+        });
+        return;
+    }
+    let xConjWindow = browser.windows.create({
+        url: browser.extension.getURL("panel/conjugueur.html"),
+        type: "popup",
+        width: 710,
+        height: 980
+    });
+}
 
 
 function showError (e) {
     console.error(e);
     //console.error(e.fileName + "\n" + e.name + "\nline: " + e.lineNumber + "\n" + e.message);
 }

DELETED gc_lang/fr/mailext/gce_worker.js
Index: gc_lang/fr/mailext/gce_worker.js
==================================================================
--- gc_lang/fr/mailext/gce_worker.js
+++ /dev/null
@@ -1,428 +0,0 @@
-/*
-    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/graphspell/helpers.js");
-importScripts("grammalecte/graphspell/str_transform.js");
-importScripts("grammalecte/graphspell/char_player.js");
-importScripts("grammalecte/graphspell/suggest.js");
-importScripts("grammalecte/graphspell/ibdawg.js");
-importScripts("grammalecte/graphspell/spellchecker.js");
-importScripts("grammalecte/text.js");
-importScripts("grammalecte/graphspell/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_rules_graph.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, oInfo, bEnd, bError=false) {
-    return {
-        "sActionDone": sActionDone,
-        "result": result, // can be of any type
-        "oInfo": oInfo,
-        "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, oParam, oInfo} = e.data;
-    switch (sCommand) {
-        case "init":
-            init(oParam.sExtensionPath, oParam.dOptions, oParam.sContext, oInfo);
-            break;
-        case "parse":
-            parse(oParam.sText, oParam.sCountry, oParam.bDebug, oParam.bContext, oInfo);
-            break;
-        case "parseAndSpellcheck":
-            parseAndSpellcheck(oParam.sText, oParam.sCountry, oParam.bDebug, oParam.bContext, oInfo);
-            break;
-        case "parseAndSpellcheck1":
-            parseAndSpellcheck1(oParam.sText, oParam.sCountry, oParam.bDebug, oParam.bContext, oInfo);
-            break;
-        case "parseFull":
-            parseFull(oParam.sText, oParam.sCountry, oParam.bDebug, oParam.bContext, oInfo);
-            break;
-        case "getListOfTokens":
-            getListOfTokens(oParam.sText, oInfo);
-            break;
-        case "getOptions":
-            getOptions(oInfo);
-            break;
-        case "getDefaultOptions":
-            getDefaultOptions(oInfo);
-            break;
-        case "setOptions":
-            setOptions(oParam.sOptions, oInfo);
-            break;
-        case "setOption":
-            setOption(oParam.sOptName, oParam.bValue, oInfo);
-            break;
-        case "resetOptions":
-            resetOptions(oInfo);
-            break;
-        case "textToTest":
-            textToTest(oParam.sText, oParam.sCountry, oParam.bDebug, oParam.bContext, oInfo);
-            break;
-        case "fullTests":
-            fullTests(oInfo);
-            break;
-        case "setDictionary":
-            setDictionary(oParam.sDictionary, oParam.oDict, oInfo);
-            break;
-        case "setDictionaryOnOff":
-            setDictionaryOnOff(oParam.sDictionary, oParam.bActivate, oInfo);
-            break;
-        case "getSpellSuggestions":
-            getSpellSuggestions(oParam.sWord, oInfo);
-            break;
-        case "getVerb":
-            getVerb(oParam.sVerb, oParam.bPro, oParam.bNeg, oParam.bTpsCo, oParam.bInt, oParam.bFem, oInfo);
-            break;
-        default:
-            console.log("[Worker] Unknown command: " + sCommand);
-            showData(e.data);
-    }
-}
-
-
-
-let bInitDone = false;
-
-let oSpellChecker = null;
-let oTokenizer = null;
-let oLxg = null;
-let oTest = null;
-let oLocution = null;
-
-
-/*
-    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, dOptions=null, sContext="JavaScript", oInfo={}) {
-    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, "sCSS", sExtensionPath+"grammalecte/graphspell/_dictionaries");
-            oSpellChecker = gc_engine.getSpellChecker();
-            oTest = new TestGrammarChecking(gc_engine, sExtensionPath+"/grammalecte/fr/tests_data.json");
-            oTokenizer = new Tokenizer("fr");
-            oLocution =  helpers.loadFile(sExtensionPath + "/grammalecte/fr/locutions_data.json");
-            oLxg = new Lexicographe(oSpellChecker, oTokenizer, oLocution);
-            if (dOptions !== null) {
-                if (!(dOptions instanceof Map)) {
-                    dOptions = helpers.objectToMap(dOptions);
-                }
-                gc_engine.setOptions(dOptions);
-            }
-            //tests();
-            bInitDone = true;
-        } else {
-            console.log("[Worker] Already initialized…")
-        }
-        // we always retrieve options from the gc_engine, for setOptions filters obsolete options
-        dOptions = helpers.mapToObject(gc_engine.getOptions());
-        postMessage(createResponse("init", dOptions, oInfo, true));
-    }
-    catch (e) {
-        console.error(e);
-        postMessage(createResponse("init", createErrorResult(e, "init failed"), oInfo, true, true));
-    }
-}
-
-
-function parse (sText, sCountry, bDebug, bContext, oInfo={}) {
-    sText = sText.replace(/­/g, "").normalize("NFC");
-    for (let sParagraph of text.getParagraph(sText)) {
-        let aGrammErr = gc_engine.parse(sParagraph, sCountry, bDebug, bContext);
-        postMessage(createResponse("parse", aGrammErr, oInfo, false));
-    }
-    postMessage(createResponse("parse", null, oInfo, true));
-}
-
-function parseAndSpellcheck (sText, sCountry, bDebug, bContext, oInfo={}) {
-    let i = 0;
-    sText = sText.replace(/­/g, "").normalize("NFC");
-    for (let sParagraph of text.getParagraph(sText)) {
-        let aGrammErr = gc_engine.parse(sParagraph, sCountry, bDebug, null, bContext);
-        let aSpellErr = oSpellChecker.parseParagraph(sParagraph);
-        postMessage(createResponse("parseAndSpellcheck", {sParagraph: sParagraph, iParaNum: i, aGrammErr: aGrammErr, aSpellErr: aSpellErr}, oInfo, false));
-        i += 1;
-    }
-    postMessage(createResponse("parseAndSpellcheck", null, oInfo, true));
-}
-
-function parseAndSpellcheck1 (sParagraph, sCountry, bDebug, bContext, oInfo={}) {
-    sParagraph = sParagraph.replace(/­/g, "").normalize("NFC");
-    let aGrammErr = gc_engine.parse(sParagraph, sCountry, bDebug, null, bContext);
-    let aSpellErr = oSpellChecker.parseParagraph(sParagraph);
-    postMessage(createResponse("parseAndSpellcheck1", {sParagraph: sParagraph, aGrammErr: aGrammErr, aSpellErr: aSpellErr}, oInfo, true));
-}
-
-function parseFull (sText, sCountry, bDebug, bContext, oInfo={}) {
-    let i = 0;
-    sText = sText.replace(/­/g, "").normalize("NFC");
-    for (let sParagraph of text.getParagraph(sText)) {
-        let lSentence = gc_engine.parse(sParagraph, sCountry, bDebug, null, bContext, true);
-        console.log("*", lSentence);
-        postMessage(createResponse("parseFull", {sParagraph: sParagraph, iParaNum: i, lSentence: lSentence}, oInfo, false));
-        i += 1;
-    }
-    postMessage(createResponse("parseFull", null, oInfo, true));
-}
-
-function getListOfTokens (sText, oInfo={}) {
-    // lexicographer
-    try {
-        sText = sText.replace(/­/g, "").normalize("NFC");
-        for (let sParagraph of text.getParagraph(sText)) {
-            if (sParagraph.trim() !== "") {
-                postMessage(createResponse("getListOfTokens", oLxg.getListOfTokensReduc(sParagraph, true), oInfo, false));
-            }
-        }
-        postMessage(createResponse("getListOfTokens", null, oInfo, true));
-    }
-    catch (e) {
-        console.error(e);
-        postMessage(createResponse("getListOfTokens", createErrorResult(e, "no tokens"), oInfo, true, true));
-    }
-}
-
-function getOptions (oInfo={}) {
-    let dOptions = helpers.mapToObject(gc_engine.getOptions());
-    postMessage(createResponse("getOptions", dOptions, oInfo, true));
-}
-
-function getDefaultOptions (oInfo={}) {
-    let dOptions = helpers.mapToObject(gc_engine.getDefaultOptions());
-    postMessage(createResponse("getDefaultOptions", dOptions, oInfo, true));
-}
-
-function setOptions (dOptions, oInfo={}) {
-    if (!(dOptions instanceof Map)) {
-        dOptions = helpers.objectToMap(dOptions);
-    }
-    gc_engine.setOptions(dOptions);
-    dOptions = helpers.mapToObject(gc_engine.getOptions());
-    postMessage(createResponse("setOptions", dOptions, oInfo, true));
-}
-
-function setOption (sOptName, bValue, oInfo={}) {
-    console.log(sOptName+": "+bValue);
-    if (sOptName) {
-        gc_engine.setOption(sOptName, bValue);
-        let dOptions = helpers.mapToObject(gc_engine.getOptions());
-        postMessage(createResponse("setOption", dOptions, oInfo, true));
-    }
-}
-
-function resetOptions (oInfo={}) {
-    gc_engine.resetOptions();
-    let dOptions = helpers.mapToObject(gc_engine.getOptions());
-    postMessage(createResponse("resetOptions", dOptions, oInfo, 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, oInfo={}) {
-    if (!gc_engine) {
-        postMessage(createResponse("textToTest", "# Grammar checker not loaded.", oInfo, true));
-        return;
-    }
-    sText = sText.replace(/­/g, "").normalize("NFC");
-    let aGrammErr = gc_engine.parse(sText, sCountry, bDebug, bContext);
-    let sMsg = "";
-    for (let oErr of aGrammErr) {
-        sMsg += text.getReadableError(oErr) + "\n";
-    }
-    if (sMsg == "") {
-        sMsg =  "Aucune erreur détectée.";
-    }
-    postMessage(createResponse("textToTest", sMsg, oInfo, true));
-}
-
-function fullTests (oInfo={}) {
-    if (!gc_engine) {
-        postMessage(createResponse("fullTests", "# Grammar checker not loaded.", oInfo, true));
-        return;
-    }
-    let dMemoOptions = gc_engine.getOptions();
-    let dTestOptions = gc_engine.getDefaultOptions();
-    dTestOptions.set("nbsp", true);
-    dTestOptions.set("esp", true);
-    dTestOptions.set("unit", true);
-    dTestOptions.set("num", true);
-    gc_engine.setOptions(dTestOptions);
-    let sMsg = "";
-    for (let sRes of oTest.testParse()) {
-        sMsg += sRes + "\n";
-        console.log(sRes);
-    }
-    gc_engine.setOptions(dMemoOptions);
-    postMessage(createResponse("fullTests", sMsg, oInfo, true));
-}
-
-
-// SpellChecker
-
-function setDictionary (sDictionary, oDict, oInfo) {
-    if (!oSpellChecker) {
-        postMessage(createResponse("setDictionary", "# Error. SpellChecker not loaded.", oInfo, true));
-        return;
-    }
-    //console.log("setDictionary", sDictionary);
-    switch (sDictionary) {
-        case "main":
-            oSpellChecker.setMainDictionary(oDict, oInfo["sExtPath"]+"/grammalecte/graphspell/_dictionaries");
-            break;
-        case "community":
-            oSpellChecker.setCommunityDictionary(oDict);
-            break;
-        case "personal":
-            oSpellChecker.setPersonalDictionary(oDict);
-            break;
-        default:
-            console.log("[worker] setDictionary: Unknown dictionary <"+sDictionary+">");
-    }
-    postMessage(createResponse("setDictionary", true, oInfo, true));
-}
-
-function setDictionaryOnOff (sDictionary, bActivate, oInfo) {
-    if (!oSpellChecker) {
-        postMessage(createResponse("setDictionary", "# Error. SpellChecker not loaded.", oInfo, true));
-        return;
-    }
-    //console.log("setDictionaryOnOff", sDictionary, bActivate);
-    switch (sDictionary) {
-        case "community":
-            if (bActivate) {
-                oSpellChecker.activateCommunityDictionary();
-            } else {
-                oSpellChecker.deactivateCommunityDictionary();
-            }
-            break;
-        case "personal":
-            if (bActivate) {
-                oSpellChecker.activatePersonalDictionary();
-            } else {
-                oSpellChecker.deactivatePersonalDictionary();
-            }
-            break;
-        default:
-            console.log("[worker] setDictionaryOnOff: Unknown dictionary <"+sDictionary+">");
-    }
-    postMessage(createResponse("setDictionaryOnOff", true, oInfo, true));
-}
-
-function getSpellSuggestions (sWord, oInfo) {
-    if (!oSpellChecker) {
-        postMessage(createResponse("getSpellSuggestions", "# Error. SpellChecker not loaded.", oInfo, true));
-        return;
-    }
-    let i = 0;
-    for (let aSugg of oSpellChecker.suggest(sWord)) {
-        postMessage(createResponse("getSpellSuggestions", {sWord: sWord, aSugg: aSugg, iSuggBlock: i}, oInfo, true));
-        i += 1;
-    }
-}
-
-
-// Conjugueur
-
-function getVerb (sWord, bPro, bNeg, bTpsCo, bInt, bFem, oInfo) {
-    try {
-        let oVerb = null;
-        let oConjTable = null;
-        if (conj.isVerb(sWord)) {
-            oVerb = new Verb(sWord);
-            oConjTable = oVerb.createConjTable(bPro, bNeg, bTpsCo, bInt, bFem);
-        }
-        postMessage(createResponse("getVerb", { oVerb: oVerb, oConjTable: oConjTable }, oInfo, true));
-    }
-    catch (e) {
-        console.error(e);
-        postMessage(createResponse("getVerb", createErrorResult(e, "no verb"), oInfo, true, true));
-    }
-}

Index: gc_lang/fr/mailext/manifest.json
==================================================================
--- gc_lang/fr/mailext/manifest.json
+++ gc_lang/fr/mailext/manifest.json
@@ -23,26 +23,45 @@
              "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]",
+    "default_popup": "panel/main.html",
     "browser_style": false
   },
 
   "compose_action": {
-    "default_icon": "img/logo-32.png",
     "default_area": "maintoolbar",
-    "default_popup": "panel/main.html",
+    "default_icon": "img/logo-32.png",
     "default_title": "Analyser",
     "browser_style": false
   },
 
   "background": {
     "scripts": [
       "grammalecte/graphspell/helpers.js",
       "background.js"
     ]
-  }
+  },
+
+  "commands": {
+    "grammar_checker": {
+      "suggested_key": { "default": "Ctrl+Shift+F" },
+      "description": "Ouvre le correcteur grammatical"
+    },
+    "conjugueur_tab": {
+      "suggested_key": { "default": "Ctrl+Shift+6" },
+      "description": "Ouvre le conjugueur"
+    },
+    "lexicon_editor": {
+      "suggested_key": { "default": "Ctrl+Shift+7" },
+      "description": "Ouvre l’éditeur lexical"
+    }
+  },
 
+  "permissions": [
+    "compose",
+    "downloads",
+    "storage"
+  ]
 }

Index: gc_lang/fr/webext/content_scripts/init.js
==================================================================
--- gc_lang/fr/webext/content_scripts/init.js
+++ gc_lang/fr/webext/content_scripts/init.js
@@ -20,15 +20,18 @@
 }
 
 // Chrome don’t follow the W3C specification:
 // https://browserext.github.io/browserext/
 let bChrome = false;
+let bThunderbird = false;
 if (typeof(browser) !== "object") {
     var browser = chrome;
     bChrome = true;
 }
-
+if (typeof(messenger) === "object") {
+    bThunderbird = true;
+}
 
 /*
 function loadImage (sContainerClass, sImagePath) {
     let xRequest = new XMLHttpRequest();
     xRequest.open('GET', browser.extension.getURL("")+sImagePath, false);
@@ -284,11 +287,11 @@
     bConnected: false,
 
     xConnect: browser.runtime.connect({name: "content-script port"}),
 
     start: function () {
-        //console.log("[Grammalecte] background port: start.");
+        console.log("[Grammalecte] background port: start.");
         this.listen();
         this.listen2();
         //this.ping();
     },
 
@@ -499,75 +502,95 @@
 
 
 oGrammalecteBackgroundPort.start();
 
 
+/*
+    ComposeAction
+    (Thunderbird only)
+*/
+if (bThunderbird) {
+    console.log("Listen...");
+    try {
+        browser.composeAction.onClicked.addListener(function (oTab, oData) {
+            console.log("START");
+            oGrammalecte.startGCPanel("J'en aie mare...");
+        });
+    }
+    catch (e) {
+        showError(e)
+    }
+    console.log("Done.");
+}
+
 
 /*
     Callable API for the webpage.
-
-*/
-document.addEventListener("GrammalecteCall", function (xEvent) {
-    // GrammalecteCall events are dispatched by functions in the API script
-    // The script is loaded below.
-    try {
-        let oCommand = JSON.parse(xEvent.detail);
-        switch (oCommand.sCommand) {
-            case "openPanelForNode":
-                if (oCommand.sNodeId && document.getElementById(oCommand.sNodeId)) {
-                    oGrammalecte.startGCPanel(document.getElementById(oCommand.sNodeId));
-                }
-                break;
-            case "openPanelForText":
-                if (oCommand.sText) {
-                    if (oCommand.sText && oCommand.sNodeId && document.getElementById(oCommand.sNodeId)) {
-                        oGrammalecte.startGCPanel(oCommand.sText, document.getElementById(oCommand.sNodeId));
-                    }
-                    else {
-                        oGrammalecte.startGCPanel(oCommand.sText);
-                    }
-                }
-                break;
-            case "parseNode":
-                if (oCommand.sNodeId && document.getElementById(oCommand.sNodeId)) {
-                    let xNode = document.getElementById(oCommand.sNodeId);
-                    if (xNode.tagName == "TEXTAREA"  ||  xNode.tagName == "INPUT") {
-                        oGrammalecteBackgroundPort.parseAndSpellcheck(xNode.value, oCommand.sNodeId);
-                    }
-                    else if (xNode.tagName == "IFRAME") {
-                        oGrammalecteBackgroundPort.parseAndSpellcheck(xNode.contentWindow.document.body.innerText, oCommand.sNodeId);
-                    }
-                    else {
-                        oGrammalecteBackgroundPort.parseAndSpellcheck(xNode.innerText, oCommand.sNodeId);
-                    }
-                }
-                break;
-            case "parseText":
-                if (oCommand.sText && oCommand.sNodeId) {
-                    oGrammalecteBackgroundPort.parseAndSpellcheck(oCommand.sText, oCommand.sNodeId);
-                }
-                break;
-            case "getSpellSuggestions":
-                if (oCommand.sWord && oCommand.sDestination) {
-                    oGrammalecteBackgroundPort.getSpellSuggestions(oCommand.sWord, oCommand.sDestination, oCommand.sErrorId);
-                }
-                break;
-            default:
-                console.log("[Grammalecte] Event: Unknown command", oCommand.sCommand);
-        }
-    }
-    catch (e) {
-        showError(e);
-    }
-});
-
-// The API script must be injected this way to be callable by the page
-let xScriptGrammalecteAPI = document.createElement("script");
-xScriptGrammalecteAPI.src = browser.extension.getURL("content_scripts/api.js");
-document.documentElement.appendChild(xScriptGrammalecteAPI);
+    (Not for Thunderbird)
+*/
+if (!bThunderbird) {
+    document.addEventListener("GrammalecteCall", function (xEvent) {
+        // GrammalecteCall events are dispatched by functions in the API script
+        // The script is loaded below.
+        try {
+            let oCommand = JSON.parse(xEvent.detail);
+            switch (oCommand.sCommand) {
+                case "openPanelForNode":
+                    if (oCommand.sNodeId && document.getElementById(oCommand.sNodeId)) {
+                        oGrammalecte.startGCPanel(document.getElementById(oCommand.sNodeId));
+                    }
+                    break;
+                case "openPanelForText":
+                    if (oCommand.sText) {
+                        if (oCommand.sText && oCommand.sNodeId && document.getElementById(oCommand.sNodeId)) {
+                            oGrammalecte.startGCPanel(oCommand.sText, document.getElementById(oCommand.sNodeId));
+                        }
+                        else {
+                            oGrammalecte.startGCPanel(oCommand.sText);
+                        }
+                    }
+                    break;
+                case "parseNode":
+                    if (oCommand.sNodeId && document.getElementById(oCommand.sNodeId)) {
+                        let xNode = document.getElementById(oCommand.sNodeId);
+                        if (xNode.tagName == "TEXTAREA"  ||  xNode.tagName == "INPUT") {
+                            oGrammalecteBackgroundPort.parseAndSpellcheck(xNode.value, oCommand.sNodeId);
+                        }
+                        else if (xNode.tagName == "IFRAME") {
+                            oGrammalecteBackgroundPort.parseAndSpellcheck(xNode.contentWindow.document.body.innerText, oCommand.sNodeId);
+                        }
+                        else {
+                            oGrammalecteBackgroundPort.parseAndSpellcheck(xNode.innerText, oCommand.sNodeId);
+                        }
+                    }
+                    break;
+                case "parseText":
+                    if (oCommand.sText && oCommand.sNodeId) {
+                        oGrammalecteBackgroundPort.parseAndSpellcheck(oCommand.sText, oCommand.sNodeId);
+                    }
+                    break;
+                case "getSpellSuggestions":
+                    if (oCommand.sWord && oCommand.sDestination) {
+                        oGrammalecteBackgroundPort.getSpellSuggestions(oCommand.sWord, oCommand.sDestination, oCommand.sErrorId);
+                    }
+                    break;
+                default:
+                    console.log("[Grammalecte] Event: Unknown command", oCommand.sCommand);
+            }
+        }
+        catch (e) {
+            showError(e);
+        }
+    });
+
+    // The API script must be injected this way to be callable by the page
+    let xScriptGrammalecteAPI = document.createElement("script");
+    xScriptGrammalecteAPI.src = browser.extension.getURL("content_scripts/api.js");
+    document.documentElement.appendChild(xScriptGrammalecteAPI);
+}
 
 
 /*
     Note:
     Initialization starts when the background is connected.
     See: oGrammalecteBackgroundPort.listen() -> case "init"
 */

Index: helpers.py
==================================================================
--- helpers.py
+++ helpers.py
@@ -117,10 +117,17 @@
 def copyAndFileTemplate (spfSrc, spfDst, dVars):
     "write file <spfSrc> as <spfDst> with variables filed with <dVars>"
     sText = Template(open(spfSrc, "r", encoding="utf-8").read()).safe_substitute(dVars)
     open(spfDst, "w", encoding="utf-8", newline="\n").write(sText)
 
+
+def addFileToZipAndFileFile (hZip, spfSrc, spfDst, dVars):
+    if spfSrc.endswith((".py", ".js", ".json", ".html", ".htm", ".css", ".xcu", ".xul", ".rdf", ".dtd", ".properties")):
+        hZip.writestr(spfDst, fileFile(spfSrc, dVars))
+    else:
+        hZip.write(spfSrc, spfDst)
+
 
 def addFolderToZipAndFileFile (hZip, spSrc, spDst, dVars, bRecursive):
     "add folder content to zip archive and file files with <dVars>"
     # recursive function
     spSrc = spSrc.strip("/ ")
@@ -130,9 +137,6 @@
         spfDst = (spDst + "/" + sf).strip("/ ")
         if os.path.isdir(spfSrc):
             if bRecursive:
                 addFolderToZipAndFileFile(hZip, spfSrc, spfDst, dVars, bRecursive)
         else:
-            if spfSrc.endswith((".py", ".js", ".json", ".html", ".htm", ".css", ".xcu", ".xul", ".rdf", ".dtd", ".properties")):
-                hZip.writestr(spfDst, fileFile(spfSrc, dVars))
-            else:
-                hZip.write(spfSrc, spfDst)
+            addFileToZipAndFileFile(hZip, spfSrc, spfDst, dVars)