Add-on SDK で始めるアドオン開発(自作モジュール編)

前回の記事:Add-on SDK で始めるアドオン開発(Context-menu 編)

今回の記事では、前回作成したコンテクストメニューの拡張機能を題材として、自作モジュールを作成する方法を紹介します。
今回の自作モジュールでは、今まで利用してきた addon-kit ライブラリだけでなく、よりプリミティブな機能を提供する api-utils ライブラリを用いた実装を行います。

また、拡張機能に表示する文字列などを各言語ごとに用意しておき、ユーザの環境に応じて拡張機能のローカライズを行う方法も紹介します。

メソッドをモジュールに分割

まず初めに、前回作成したコンテクストメニューの拡張機能を、単純に2つのファイルに分割してみます。
拡張機能のパッケージフォルダを開き、 lib フォルダの中に、新しく history-menu.js ファイルを作成します。
history-menu.js には、前回 main.js で記述したコードのうち、以下の部分をコピーしておきます。

history-menu.js

const contextMenu = require("context-menu");
const storage = require("simple-storage").storage;

var storageMenu;
var backMenu;

var createNewBackMenu = function(length){
    …
}

var clearOldStorage = function(){
    …
}

var storeHistory = function(title, url){
    …
}

var createNewStorageMenu = function(length){
    …
}

exports.createNewBackMenu = createNewBackMenu;
exports.clearOldStorage = clearOldStorage;
exports.storeHistory = storeHistory;
exports.createNewStorageMenu = createNewStorageMenu;

history-menu.js で定義したメソッドを外部から利用するために、利用するメソッドを 「exports.メソッド名 = …」 という形式で記述します(23~26行目)。

次に、 main.js を以下のように書き換えて、 history-menu モジュールのメソッドを呼び出してみます。

main.js

const historyMenu = require("history-menu");
const PageMod = require("page-mod").PageMod;

exports.main = function(){
    var maxItemLength = 10;

    historyMenu.clearOldStorage();

    PageMod({
        include: ["*"],
        contentScript: 'self.postMessage(history.length);',
        onAttach: function onAttach(worker) {
            historyMenu.storeHistory(worker.tab.title, worker.tab.url);
            historyMenu.createNewStorageMenu(maxItemLength);

            worker.on('message', function(length){
                historyMenu.createNewBackMenu(length - 1);
            });
        }
    });

}

今まで使ってきたモジュールと同様に、 require で history-menu モジュールを読み込むことにより、 exports…で指定したメソッドを利用することができます。

この拡張機能を実行すると、前回実装した2つのサブメニューが、コンテクストメニューに表示されます。

api-utils モジュールの利用

Add-on SDK には、大きく分けて addon-kitapi-utils の2つのライブラリがあります。
addon-kit ライブラリは、今まで使ってきた context-menu や widget モジュールなど、拡張機能の開発に必要な高レベルのモジュールを提供します。
一方、 api-utils ライブラリは、 addon-kit のモジュールで使われる要素技術やユーティリティなどを提供するライブラリです。api-utils ライブラリを活用することにより、addon-kit のモジュールだけでは実現できない拡張機能を実装することが可能です。

api-utils ライブラリは、多くのモジュールやメソッドを提供しているので、すべてを紹介することはできませんが、ここではメソッドの引数を検証する api-utils.validatedOptions メソッドと、ローカライズに利用できる各モジュールを実際に使ってみます。

api-utils.validatedOptions 引数の検証

api-utils ライブラリで提供されている api-utils モジュールは、自作モジュールの実装において役立つ様々なメソッドを実装しています。
ここでは、その中の validatedOptions メソッドを用いて、自作モジュールのメソッドの引数を検証するコードを紹介します。

history-menu.js において、 api-utils モジュールを読み込んで、 storeHistory メソッドを次のように書き換えてみます。

history-menu.js

…
const apiUtils = require("api-utils");

var storeHistory = function(title, url){
    var options = {
        title: title,
        url: url,
        time: new Date().toLocaleString()
    }
    var requirements = {
        title: {
            map: function(v) v ? v.toString() : "No Title",
            is: ["string"],
            ok: function(v) v.length > 0,
            msg: "title must be a non-empty string."
        },
        url: {
            is: ["string"],
            ok: function(v) v.match(/^(https?):\/\/.+$/),
            msg: "url must be start http(s)://."
        },
        time: {
            is: ["string"],
            msg: "time must be a string."
        }
    };
    var validatedOptions = apiUtils.validateOptions(options, requirements);
    if (storage.history) {
        storage.history.unshift(validatedOptions);
    } else{
        console.error('storage.history is undefined');
    }
}
…

validateOptions メソッド(27行目)は、第1引数のオブジェクト (options) の各プロパティを、第2引数に指定した条件 (requirements) で検証します。
requirements オブジェクトには、options のプロパティと同じプロパティ名を指定します。
requirements オブジェクトに指定されていないプロパティ名は無視されてしまうので注意してください。

requirements オブジェクトのそれぞれのプロパティには、 map, is, ok, msg のうち、任意のプロパティを指定することができます。

map は、関数を用いてプロパティの値を書き換えたり、型変換したりする際に指定します。
上の例では、タイトルの値を自動的に文字列へ変換し、空文字列の場合は “No Title” を使うように指定しています(12行目)。
is には、値がとりうる型名の配列を指定します。
ok は、関数を用いて値の検証を行う際に指定します。
上の例では、タイトルの長さが 0 より大きく、 URL の値が 「http(s)://」 で始まることを検証しています(14,19行目)。

ローカライズ

これまでの実装では、拡張機能のラベルなどに表示する文字列は、プログラムのなかにハードコーディングしていました。
しかし、拡張機能を複数の言語に対応させようと思うならば、使用する文字列を言語ごとに別ファイルに用意しておき、ユーザの環境に応じて表示する文字列を選択するという方法が効果的です。
ここでは、 api-utils ライブラリを用いて、上記のような方法で拡張機能のローカライズを実装してみます。

まず、日本語と英語(デフォルト)の2つの語種に対応する properties ファイルを、以下のように作成し、 data フォルダに保存します。
ここで、%1$S の部分は、後に値を置換するためのプレースホルダです。

data/en-US.properties

storageMenuLabel = Move to the...
backMenuLabel = Back to the...
backItemLabel = %1$S-previous page
titleValidationMessage = title must be a non-empty string.
urlValidationMessage = url must be start with http(s)://.
timeValidationMessage = time must be a string.
storageNotFound = %1$S is undefined

data/ja.properties

storageMenuLabel = 以前のページへ移動
backMenuLabel = 履歴を戻る
backItemLabel = %1$S つ前のページへ戻る
titleValidationMessage = タイトルは空でない文字列である必要があります。
urlValidationMessage = URL は http(s):// で始まる文字列である必要があります。
timeValidationMessage = 訪問時刻は文字列である必要があります。
storageNotFound = %1$S が定義されていません。

次に、ユーザの Firefox の言語設定に応じた properties ファイルから値を取得するコードを、以下のように追加します。
ここでは、 createNewBackMenu メソッドを例にして、ローカライズされた値を利用する方法を説明します。

history-menu.js

…
// localize
var locale = require("preferences-service").get("general.useragent.locale");
var bundleFileName;
switch (locale) {
    case "ja": bundleFileName = "ja.properties"; break;
    case "ja-JP-mac": bundleFileName = "ja.properties"; break;  // for Mac
    default  : bundleFileName = "en-US.properties"; break;
}
var bundleURL = require("self").data.url(bundleFileName);
var bundle = require("app-strings").StringBundle(bundleURL);

…

var createNewBackMenu = function(length){
    if(backMenu !== undefined) backMenu.destroy();
    if(length > 0){
        var oldPageItems = [];
        var maxItemLength =length;
        var menuLabel = bundle.get("backMenuLabel");
        var script = 'self.on("click", function (node, data) { ' +
                     '    history.go(data);' +
                     '});';
        for(var i = 1; i <= maxItemLength; i++){
            var itemLabel = bundle.get("backItemLabel", );
            oldPageItems.push(
                contextMenu.Item({
                    label: itemLabel,
                    data: -1 * i
                }));
        }
        backMenu = contextMenu.Menu({
            label: menuLabel,
            contentScript: script,
            items: oldPageItems
        });
    }
}
…

まず、 Firefox の言語設定を調べるために、 preferences-service モジュールを用いて、 general.useragent.locale の値を調べます(3行目)。
続いて、 locate の値に応じて適切な properties ファイル名を指定し、 self モジュールを用いて、そのファイルの URL を取得します(4~10行目)。
最後に、ファイルの URL から app-string モジュールを用いて StringBundle オブジェクトを取得します(11行目)。
これにより、 StringBundle オブジェクトから、ローカライズされた値を利用できるようになりました。

ローカライズされた文字列は、 StringBundle オブジェクトの get メソッドで取得することができます(20行目)。
25行目の例では、ローカライズ文字列中にプレースホルダとして指定した %1$S を、 i.toString() の値で置換しています。
参考:MDN Docs > XPCOM > Interface Reference > nsIStringBundle

createNewBackMenu 以外のメソッドも同様にして書き換え、このプログラムを実行してみます。

動作例


日本語に設定した場合


英語に設定した場合

Tips: Firefox のロケール設定を変更

Firefox のロケール設定を変更するには、ロケーションバーに 「about:config」 と入力して、 general.useragent.locale の値を 「ja」 や 「en-US」 などに指定します。


about:config

ロケール設定を変更し、アドオンマネージャから、拡張機能を「無効化」→「有効化」することにより、設定が拡張機能に反映されます。

その他の api-utils ライブラリ

api-utils ライブラリには、ここで紹介した以外にも、多くのモジュールが提供されています。
詳細は、 cfx docs コマンド、または、こちらのページで閲覧できるドキュメントを参照してください。

次回:XPCOM コンポーネントの利用

次回の記事では、より発展的な拡張機能の開発手法として、Add-on SDK から XPCOM コンポーネントを利用する方法を紹介します。
XPCOM の機能を利用することにより、例えば、 Firefox が利用しているデータベースにアクセスして、 Web ページの履歴やブックマーク、フォームの入力履歴などの情報を取得することができます。
題材として、Firefox のデータベースから取得した履歴情報をコンテクストメニューに表示する拡張機能を実装します。

次回の記事:Add-on SDK で始めるアドオン開発 ( XPCOM 編)