Translate

Archives

Firefox 4 Restartless Add-ons

Prior to Firefox 4, extensions (more commonly called add-ons) that modified or added to the browser user interface (UI) required one or more UI overlays which the browser loaded from the add-on and applied atop its own user interface. While this technology made creating add-ons that modified the Firefox UI relatively easy, it also meant that updating, installing, or disabling an extension required that Firefox had to be restarted.

UI overlays for Firefox are written in XUL (pronounced “zool”), an XML-based markup language created by Mozilla for specifying user interfaces. XUL provides a number of largely platform independent widgets from which to construct a UI. For example, here is the overlay file used with a Firefox add-on called HTML5toggle prior to its conversion to a restartless add-on.

<?xml version="1.0" encoding="UTF-8"?>

<?xml-stylesheet href="chrome://html5toggle/skin/overlay.css" type="text/css"?>
<!DOCTYPE overlay SYSTEM "chrome://html5toggle/locale/overlay.dtd">

<overlay id="html5toggle-overlay" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
  <script type="application/javascript;version=1.8" src="overlay.js"/>

  <stringbundleset>
     <stringbundle id="html5toggle-string-bundle" src="chrome://html5toggle/locale/overlay.properties"/>
  </stringbundleset>

  <keyset>
     <key id="html5toggle-shortcut" modifiers="control alt" key="H" oncommand="html5toggle.onToolbarButton();" />
  </keyset>

  <menupopup id="menu_ToolsPopup">
     <menuitem id="html5toggle-menuitem" key="html5toggle-shortcut" class="menuitem-iconic" 
          label="Disable HTLM5" oncommand="html5toggle.onMenuItem();" />
  </menupopup>

  <toolbarpalette id="BrowserToolbarPalette">
     <toolbarbutton id="html5toggle-toolbar-button" class="toolbarbutton-1 chromeclass-toolbar-additional"
         label="&html5toggleToolbarButton.label;" tooltiptext="&html5toggleToolbarButton.tooltip;"
         oncommand="html5toggle.onToolbarButton()"/>
  </toolbarpalette>

</overlay>


Firefox 4 uses the Gecko 2.0 layout engine which supports bootstrapped (more commonly called restartless) add-ons. A restartless add-on does not use an overlay such as shown above to apply its UI on top of Firefox’s UI. Instead it programmatically inserts itself into Firefox using JavaScript. This is done using a well-known JavaScript file called bootstrap.js in the root of the add-on which contains, among other things, four well-known APIs (bootstrap methods) that the browser invokes to direct the add-on to install itself, uninstall itself, start up, or shut down.

A simple example should help to make things clearer. Let us create a rebootless add-on called HelloWorld which displays a simple alert (modal message box) upon add-on startup and shutdown.

First create an subdirectory called HelloWorld for this extension. In this directory create an install manifest file called install.rdf with the following contents:

<?xml version="1.0" encoding="UTF-8"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 
    xmlns:em="http://www.mozilla.org/2004/em-rdf#">
  <Description about="urn:mozilla:install-manifest">
    <em:id>helloworld@fpmurphy.com</em:id>
    <em:type>2</em:type>
    <em:version>1.0</em:version>
    <em:bootstrap>true</em:bootstrap>
    <em:name>HelloWorld</em:name>
    <em:creator>Finnbarr P. Murphy</em:creator>
    <em:description>Restartless Hello World</em:description>
    <!-- Firefox -->
    <em:targetApplication>
      <Description>
        <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
        <em:minVersion>4.0b4</em:minVersion>
        <em:maxVersion>4.0.*</em:maxVersion>
      </Description>
    </em:targetApplication>
  </Description>
</RDF>


To make the HelloWorld add-on restartless, you must inform the add-on installer that your add-on is restartless by adding an element called bootstrap with a value of true to the install manifest. This is the only difference between a regular install manifest and restartless install manifest.

All that Firefox does in the case of a restartless add-on is make calls into bootstrap.js to well-known APIs. The add-on is responsible for adding and removing its user interface, obtaining and releasing resources and handling any other setup and shutdown tasks it requires. Note that bootstrap.js gets executed in a privileged sandbox which is cached until the add-on is shut down.

In the HelloWorld subdirectory directory create a file called bootstep.js containing the following code:

Components.utils.import("resource://gre/modules/Services.jsm");

function install() {}

function uninstall() {}

function startup(data, reason) {
   Services.prompt.alert(null, "Restartless Demo", "Hello world.");
}

function shutdown(data, reason) {
   Services.prompt.alert(null, "Restartless Demo", "Goodbye world.");
}


Zip up these two files into a package called HelloWorld.xpi. Note the extension must be .xpi! Here is a short video which shows how to install and uninstall the add-on.

Go ahead and try building and installed the HelloWorld add-on.

The following table details the four well-known APIs which Firefox can invoke in bootstrap.js.

APIPURPOSE
startup()[MANDATORY] Called when the add-on needs to start itself up. This happens at Firefox launch time, when the add-on is enabled after being disabled or after the add-on has been shut down in order to install an update. As such, this can be called many times during the lifetime of the Firefox session. This is where user interface components should be injected.
shutdown()[MANDATORY] Called when the add-on needs to shut itself down, such as when Firefox is terminating or when the add-on is about to be upgraded or disabled. Any user interface components that has been injected must be removed, tasks shut down, and objects disposed of.
install()[OPTIONAL] Called by Firefox before the first call to startup() after the add-on is installed, upgraded, or downgraded.
uninstall()[OPTIONAL] Called by Firefox after the last call to shutdown() before a particular version of an add-on is uninstalled. Not called if install() was never called.

Each of the above APIs takes two parameters:
PARAMETERPURPOSE
dataA JavaScript object containing basic information about the add-on including id, version, and installPath
reasoncodeA constant indicating why the API is being called

Here is the current list of reasoncode constants:
CONSTANTVALUEDESCRIPTIONUSED WITH
APP_STARTUP1The application is starting upstartup()
APP_SHUTDOWN2The application is shutting downshutdown()
ADDON_ENABLE3The add-on is being enabledstartup()
ADDON_DISABLE4The add-on is being disabledshutdown()
ADDON_INSTALL5The add-on is being installedstartup()
install()
ADDON_UNINSTALL6The add-on is being uninstalledshutdown()
uninstall()
ADDON_UPGRADE7The add-on is being upgradedstartup()
shutdown()
install()
uninstall()
ADDON_DOWNGRADE8The add-on is being downgradedstartup()
shutdown()
install()
uninstall()

As an aside, here is the method that Firefox 4.0 uses to call the four well-known APIs in browser.js. See XPIProvider.jsm for more details.

 /**
   * Calls a bootstrap method for an add-on.
   *
   * @param  aId
   *         The ID of the add-on
   * @param  aVersion
   *         The version of the add-on
   * @param  aFile
   *         The nsILocalFile for the add-on
   * @param  aMethod
   *         The name of the bootstrap method to call
   * @param  aReason
   *         The reason flag to pass to the bootstrap's startup method
   */
  callBootstrapMethod: function XPI_callBootstrapMethod(aId, aVersion, aFile,
                                                        aMethod, aReason) {
    // Never call any bootstrap methods in safe mode
    if (Services.appinfo.inSafeMode)
      return;

    // Load the scope if it hasn't already been loaded
    if (!(aId in this.bootstrapScopes))
      this.loadBootstrapScope(aId, aFile, aVersion);

    if (!(aMethod in this.bootstrapScopes[aId])) {
      WARN("Add-on " + aId + " is missing bootstrap method " + aMethod);
      return;
    }

    let params = {
      id: aId,
      version: aVersion,
      installPath: aFile.clone()
    };

    LOG("Calling bootstrap method " + aMethod + " on " + aId + " version " +
        aVersion);
    try {
      this.bootstrapScopes[aId][aMethod](params, aReason);
    }
    catch (e) {
      WARN("Exception running bootstrap method " + aMethod + " on " +
           aId, e);
    }
  },

Let us now examine a more sophisticated bootstrap.js. It from my HTML5toggle rebootless add-on. The purpose of this add-on is to simply toggle HTML5 support on or off in Firefox 4 via the html5.enable preference. I wrote this add-on to enable me to continue to use Firefox 4 to access my Hotmail account despite the fact that some versions of the Firefox 4 beta automatically refresh the Hotmail page every 1 to 2 seconds – which quickly becomes very annoying. The problem relates to browser detection via the new user agent format that Firefox 4 is using.

Here is the source code:

/* ***** BEGIN LICENSE BLOCK *****
 * Version: MIT/X11 License
 * 
 * Copyright (c) 2011 Finnbarr P. Murphy
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * Contributor(s):
 *   Finnbarr P. Murphy <fpm@hotmail.com> (Original Author)
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * ***** END LICENSE BLOCK ***** */

/* ***** BEGIN LICENSE BLOCK *****
 * Version: MIT/X11 License
 * 
 * Copyright (c) 2010 Erik Vold
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * Contributor(s):
 *   Erik Vold <erikvvold@gmail.com> (Original Author)
 *   Greg Parris <greg.parris@gmail.com>
 *   Nils Maier <maierman@web.de>
 *
 * ***** END LICENSE BLOCK ***** */

const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/AddonManager.jsm");

const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const APPMENU_ID    = "html5toggle-appmenuid",
      MENU_ID       = "html5toggle-menuitemid",
      BUTTON_ID     = "html5toggle-buttonid",
      KEY_ID        = "html5toggle-keyid",
      KEYSET_ID     = "html5toggle-keysetid"
      PREF_TOOLBAR  = "toolbar",
      PREF_NEXTITEM = "nextitem";

const PREF_BRANCH_HTML5TOGGLE = Services.prefs.getBranch("extensions.html5toggle.");
const PREF_BRANCH_HTML5       = Services.prefs.getBranch("html5.");

const PREFS = {
   key:       "H",
   modifiers: "control,alt",
   enable:    false,
   nextitem:  "bookmarks-menu-button-container",
   toolbar:   "nav-bar"
};

let PREF_OBSERVER = {
  observe: function(aSubject, aTopic, aData) {
    if ("nsPref:changed" != aTopic || !PREFS[aData]) return;
    runOnWindows(function(win) {
      win.document.getElementById(KEY_ID).setAttribute(aData, getPref(aData));
      addMenuItem(win);
    });
  }
}

let logo16on = "", logo16off = "";

(function(global) global.include = function include(src) (
    Services.scriptloader.loadSubScript(src, global)))(this);

function setInitialPrefs() {
   let branch = PREF_BRANCH_HTML5TOGGLE;
   for (let [key, val] in Iterator(PREFS)) {
      switch (typeof val) {
         case "boolean":
            branch.setBoolPref(key, val);
            break;
         case "number":
            branch.setIntPref(key, val);
            break;
         case "string":
            branch.setCharPref(key, val);
            break;
      }
   }

   // save the current value of the html5.enable preference
   let value = PREF_BRANCH_HTML5.getBoolPref("enable");
   PREF_BRANCH_HTML5TOGGLE.setBoolPref('enable', value);
}


function getPref(name) {
   try {
      return PREF_BRANCH_HTML5TOGGLE.getComplexValue(name, Ci.nsISupportsString).data;
   } catch(e){}
   return PREFS[name];
}


function $(node, childId) {
   if (node.getElementById) {
      return node.getElementById(childId);
   } else {
      return node.querySelector("#" + childId);
   }
}


function toggle() {
   let value = PREF_BRANCH_HTML5.getBoolPref("enable");
   PREF_BRANCH_HTML5.setBoolPref("enable", !value); 

   let doc = Services.wm.getMostRecentWindow('navigator:browser').document;

   let menuitem = $(doc, MENU_ID);
   if (menuitem) {
      if (value) {
         menuitem.setAttribute("checked", "true"); 
      } else {
         menuitem.setAttribute("checked", "false"); 
      }
   }

   let toggleButton = $(doc, BUTTON_ID);
   if (toggleButton) { 
      if (value) {
         toggleButton.tooltipText = getLocalizedStr("offString");
         toggleButton.style.listStyleImage = "url('" + logo16off + "')";
      } else {
         toggleButton.tooltipText = getLocalizedStr("onString");
         toggleButton.style.listStyleImage = "url('" + logo16on + "')";
      }
   }

   let appMenu = $(doc, APPMENU_ID);
   if (appMenu) {
      if (value) {
         appMenu.style.listStyleImage = "url('" + logo16off + "')";
      } else {
         appMenu.style.listStyleImage = "url('" + logo16on + "')";
      }
   }

   return true;
}


function addMenuItem(win) {
   let doc = win.document;

   function removeMI() {
      let menuitem = $(doc, MENU_ID);
      menuitem && menuitem.parentNode.removeChild(menuitem);
   }

   removeMI();

   // add the new menuitem to the Tools menu
   let (toggleMI = win.document.createElementNS(NS_XUL, "menuitem")) {
      toggleMI.setAttribute("id", MENU_ID);
      toggleMI.setAttribute("label", getLocalizedStr("label"));
      toggleMI.setAttribute("accesskey", "H");
      toggleMI.setAttribute("key", KEY_ID);
      toggleMI.setAttribute("checked", PREF_BRANCH_HTML5.getBoolPref("enable")); 
      toggleMI.setAttribute("class", "menuitem-iconic");
      toggleMI.addEventListener("command", toggle, true);
      $(doc, "menu_ToolsPopup").insertBefore(toggleMI, $(doc, "javascriptConsole"));
   }

   unload(removeMI, win);
}


function main(win) {
   let doc = win.document;
   let value = PREF_BRANCH_HTML5.getBoolPref("enable");

   let toggleKeyset = doc.createElementNS(NS_XUL, "keyset");
   toggleKeyset.setAttribute("id", KEYSET_ID);

   // add hotkey
   let (toggleKey = doc.createElementNS(NS_XUL, "key")) {
      toggleKey.setAttribute("id", KEY_ID);
      toggleKey.setAttribute("key", getPref("key"));
      toggleKey.setAttribute("modifiers", getPref("modifiers"));
      toggleKey.setAttribute("oncommand", "void(0);");
      toggleKey.addEventListener("command", toggle, true);
      $(doc, "mainKeyset").parentNode.appendChild(toggleKeyset).appendChild(toggleKey);
   }

   // add menuitem to Tools menu
   addMenuItem(win);

   // add menuitem to Firefox button options
   if ((appMenu = $(doc, "appmenu_customizeMenu"))) {
      let toggleAMI = $(doc, MENU_ID).cloneNode(false);
      toggleAMI.setAttribute("id", APPMENU_ID);
      toggleAMI.setAttribute("class", "menuitem-iconic menuitem-iconic-tooltip");
      if (value) {
         toggleAMI.style.listStyleImage = "url('" + logo16on + "')";
      } else {
         toggleAMI.style.listStyleImage = "url('" + logo16off + "')";
      }
      toggleAMI.addEventListener("command", toggle, true);
      appMenu.appendChild(toggleAMI);
   }

   // add iconized button
   let (toggleButton = doc.createElementNS(NS_XUL, "toolbarbutton")) {
      toggleButton.setAttribute("id", BUTTON_ID);
      toggleButton.setAttribute("label", getLocalizedStr("label"));
      toggleButton.setAttribute("class", "toolbarbutton-1 chromeclass-toolbar-additional");
      if (value) {
         toggleButton.setAttribute("tooltiptext", getLocalizedStr("onString"));
         toggleButton.style.listStyleImage = "url('" + logo16on + "')";
      } else {
         toggleButton.setAttribute("tooltiptext", getLocalizedStr("offString"));
         toggleButton.style.listStyleImage = "url('" + logo16off + "')";
      }
      toggleButton.addEventListener("command", toggle, true);
      $(doc, "navigator-toolbox").palette.appendChild(toggleButton);
 
      // move to location specified in prefs
      let toolbarId = PREF_BRANCH_HTML5TOGGLE.getCharPref(PREF_TOOLBAR);
      let toolbar = toolbarId && $(doc, toolbarId);
      if (toolbar) {
         let nextItem = $(doc, PREF_BRANCH_HTML5TOGGLE.getCharPref(PREF_NEXTITEM));
         toolbar.insertItem(BUTTON_ID, nextItem &&
            nextItem.parentNode.id == toolbarId && nextItem);
      }

      win.addEventListener("aftercustomization", toggleCustomize, false);
   }

   unload(function() {
      appMenu && appMenu.removeChild(APPMENU_ID);
      toggleKeyset.parentNode.removeChild(toggleKeyset);

      let button = $(doc, BUTTON_ID) || $($(doc,"navigator-toolbox").palette, BUTTON_ID);
      button && button.parentNode.removeChild(button);
 
      win.removeEventListener("aftercustomization", toggleCustomize, false);
   }, win);
}


function toggleCustomize(event) {
   let toolbox = event.target, toolbarId, nextItemId;
   let button = $(toolbox.parentNode, BUTTON_ID);
   if (button) {
      let parent = button.parentNode,
          nextItem = button.nextSibling;
      if (parent && parent.localName == "toolbar") {
          toolbarId = parent.id;
          nextItemId = nextItem && nextItem.id;
      }
   }
   PREF_BRANCH_HTML5TOGGLE.setCharPref(PREF_TOOLBAR,  toolbarId || "");
   PREF_BRANCH_HTML5TOGGLE.setCharPref(PREF_NEXTITEM, nextItemId || "");
}


function install(){
   setInitialPrefs();
}


function uninstall(){
   let value = PREF_BRANCH_HTML5TOGGLE.getBoolPref("enable");
   PREF_BRANCH_HTML5TOGGLE.deleteBranch("");             
   PREF_BRANCH_HTML5.setBoolPref('enable', value);
}


function startup(data) AddonManager.getAddonByID(data.id, function(addon) {
   include(addon.getResourceURI("includes/utils.js").spec);
   include(addon.getResourceURI("includes/locale.js").spec);

   initLocalization(addon, "html5toggle.properties");
   logo16on = addon.getResourceURI("images/HTML5on16N.png").spec;
   logo16off = addon.getResourceURI("images/HTML5off16N.png").spec;

   watchWindows(main);

   let prefs = PREF_BRANCH_HTML5TOGGLE;
   prefs = prefs.QueryInterface(Components.interfaces.nsIPrefBranch2);
   prefs.addObserver("", PREF_OBSERVER, false);

   unload(function() prefs.removeObserver("", PREF_OBSERVER));
});


function shutdown(data, reason) { if (reason !== APP_SHUTDOWN) unload(); }


The Erik Vold copyright notice was included in this source file because the starting point for my particular bootstrap.js was the bootstrap.js file created by Erik Vold for his Restartless Restart add-on. Eric was one of the first developers to start experimenting with restartless add-ons. He wrote a series of blog posts on this subject which you should read.

I assume that you are familiar with JavaScript. However, you may not be familiar with the let keyword. This keyword was introduced in JavaScript 1.7 which is a Mozilla only extension. Variables declared by let have as their scope the block in which they are defined as well as any sub-blocks in which they are not redefined. Contrast that scope with variables declared by var which have as their scope the entire enclosing function. Thus the let keyword provides a way to provide local scoping.

Note the inclusion of the Services.jsm and AddonManager.jsm JavaScript code modules. Use of these modules simplifies many routine tasks such as setting and getting preferences. Resources such as icons need to be accessed using getResourceURI. Menuitems, hotkeys, and buttons are created by manipulating the browser DOM model. I assume you are familiar with manipulating DOM if you are reading this post. By the way, the DOM Inspector add-on tool is very useful for identifying the structure and IDs of various menus. Just point it at chrome://browser/content/browser.xul.

One of the areas where you may run into trouble is string bundles and localization. Currently there is no standard way to handle string bundles. I use the following code to handle string localization.

/* ***** BEGIN LICENSE BLOCK *****
 * Version: MIT/X11 License
 * 
 * Copyright (c) 2010 Erik Vold
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * Contributor(s):
 *   Erik Vold <erikvvold@gmail.com> (Original Author)
 *   Finnbarr P. Murphy <fpm@hotmail.com>
 *
 * ***** END LICENSE BLOCK ***** */


var initLocalization = (function(global) {
   let regex = /(\w+)-\w+/;

   // get user's locale
   let locale = Cc["@mozilla.org/chrome/chrome-registry;1"]
      .getService(Ci.nsIXULChromeRegistry).getSelectedLocale("global");

   function getStr(aStrBundle, aKey) {
      if (!aStrBundle) return false;
      try {
         return aStrBundle.GetStringFromName(aKey);
      } catch (e) {}
      return "";
   }

   return function(addon, filename) {
      defaultLocale = "en";
      function filepath(locale) addon.getResourceURI("locale/" + locale + "/" + filename).spec

      let defaultBundle = Services.strings.createBundle(filepath(locale));
      let defaultBasicBundle;
      let (locale_base = locale.match(regex)) {
         if (locale_base) {
            defaultBasicBundle = Services.strings.createBundle(filepath(locale_base[1]));
         }
      }

      let addonsDefaultBundle = Services.strings.createBundle(filepath(defaultLocale));

      return global.getLocalizedStr = function l10n_underscore(aKey, aLocale) {
         let localeBundle, localeBasicBundle;
         if (aLocale) {
            localeBundle = Services.strings.createBundle(filepath(aLocale));
            let locale_base = aLocale.match(splitter);
            if (locale_base) {
               localeBasicBundle = Services.strings.createBundle(filepath(locale_base[1]));
            }
         }

         return getStr(localeBundle, aKey)
            || getStr(localeBasicBundle, aKey)
            || (defaultBundle && (getStr(defaultBundle, aKey) || (defaultBundle = null)))
            || (defaultBasicBundle && (getStr(defaultBasicBundle, aKey) || (defaultBasicBundle = null)))
            || getStr(addonsDefaultBundle, aKey);
      }
   }
})(this);


Strings must be in a property file rather than in a DTD. For example, here is the en-US locale property file for HTML5toggle.

label=Toggle HTML5
tooltip=Toggle HTML5 support on or off
onString=Turn HTML5 support off 
offString=Turn HTML5 support on
 


A localized string is retrieved using getLocalizedStr(). For example

toggleButton.tooltipText = getLocalizedStr("offString");


sets the HTML5toggle button tooltip text to “Turn HTML5 support off” in the en-US locale. An alternative method is to use Edward Lee’s getStrings() localization code.

String bundles and string localization is probably the one major area of restartless add-ons which needs to be standardized. There is no reason that it should not simply be a case of including another JavaScript code module in your add-on.

You must be very careful to perform the correct cleanup after you uninstall, disable or shutdown an add-on. For example, any stylesheets registered via loadAndRegisterSheet must be released using unregisterSheet. Properties added to any DOM node must be removed by the add-on at shutdown. DOM nodes added to existing windows, including script and style nodes, must be removed when the add-on is disabled. Any attribute changes to existing nodes must likewise be undone. See Kris Maglione’s article on this issue for more information.

By now you should have enough information to start writing your own restartless add-on or converting an existing add-on to become restartless. For more information about restartless add-ons, I suggest you look at Edward Lee’s restartless add-on code examples and Mark Finkle‘s weblog.

Should you come across anything of importance which I should have included in this post and which would have made your task easier, please let me know via a comment.

Comments are closed.