waffle
Version:
シンプルなWEBアプリケーションフレームワークです。(ALL YOUR NODE ARE BELONG TO US)
366 lines (319 loc) • 9.62 kB
JavaScript
/*
* Copyright 2012 Katsunori Koyanagi
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
/**
* @overview 多言語リソースを取り扱う機能を提供します。
*/
"use strict";
require("./patch");
var Properties = require("./Properties");
var lang = require("./Lang");
var fs = require("fs");
/**
* 多言語リソースを取り扱う機能を提供します。
* <p>
* 引数のパスで示されたディレクトリ内に含まれる、ディレクトリ名と同じ名前のファイルを多言語リソースとして取り扱います。
* 例えば、パスが/foo/barであれば、bar以下に、bar.jsonやbar_ja_JP.propertiesのようなファイル名を対象とします。
* 名前の後ろにアンダースコアで区切られたサフィックスは、ロケール情報になります。 ロケール情報を持たないファイルはデフォルトのロケールとして扱われます。
* </p>
* <p>
* ロケールは、言語_国_バリアントの3つのセクションで構成されます。内部では大文字小文字は区別されません。
* このリソースに設定されたロケールに基づいて、適切なエントリを取得して、多言語文字列として扱うことができます。
* </p>
*
* @class 多言語リソースを取り扱う機能を提供します。
* @constructor
* @param {String}
* path パス
*/
function ResourceBundle(path) {
this.invalid = true;
this.path = path;
this.basename = fs.basename(path);
}
/**
* デフォルトのロケールです。
* <p>
* デフォルトは環境変数LANGの値を元に構成されます。ピリオドを含む場合はピリオド以前を認識します。
* 環境変数LANGが取得できない場合はnullになります。
* </p>
*
* @type String
*/
ResourceBundle.defaultLocale = (function() {
var lang = process.env.LANG;
if (lang) {
var index = lang.indexOf(".");
if (index > -1) {
lang = lang.substr(0, index);
}
} else {
lang = null;
}
return lang;
})();
/**
* このリソースのロケールです。
* <p>
* リソースのエントリを取得する際に、現在のリソースのロケールが参照されます。 nullの場合は、{@link ResourceBundle.defaultLocale}にフォールバックされます。デフォルトではnullです。
* </p>
*
* @type String
*/
ResourceBundle.prototype.locale = null;
/**
* リソースの変更監視用のウォッチャーです。
*
* @property watcher
* @type FSWatcher
*/
// JSDOC用のダミー
ResourceBundle.prototype.watcher = null;
lang.defineProperty(ResourceBundle.prototype, "watcher", null,
function(watcher) {
if (this.__watcher__ === watcher) {
return;
}
if (this.__watcher__) {
this.__watcher__.destroy();
}
if (watcher != null) {
var self = this;
watcher.add(this.path);
watcher.on("change", function(file) {
self.invalid = true;
});
watcher.on("remove", function(file) {
self.invalid = true;
});
}
this.__watcher__ = watcher;
});
/**
* リソースのエントリを取得して返します。キーに該当するリソースが存在しない場合は空文字列として評価するエントリが返されます。
*
* @param {String}
* key リソースのキー
* @param {Object}
* args 文字列をフォーマットするための可変長パラメータ
* @return {ResourceEntry} リソースのエントリ
*/
ResourceBundle.prototype.getEntry = function(key) {
var args = Array.prototype.slice.call(arguments, 1);
return new ResourceEntry(this, key, args, this.locale);
};
/**
* リソースを取得して文字列で返します。キーに該当するリソースが存在しない場合は空文字列が返されます。
*
* @param {String}
* key リソースのキー
* @param {Object}
* args 文字列をフォーマットするための可変長パラメータ
* @return {String} 文字列
*/
ResourceBundle.prototype.getString = function(key) {
return this.getEntry(key).toString();
};
/**
* @private
*/
ResourceBundle.prototype.format = function(key, args, locale) {
if (this.invalid) {
this.updateResources();
this.invalid = false;
}
var locale2 = canonicalize(locale || ResourceBundle.defaultLocale);
var node = this.registry;
for ( var i = 0, j = locale2.length; i < j; i++) {
var section = locale2[i];
if (!(section in node.children)) {
break;
}
node = node.children[section];
}
var value = null;
while (node != null) {
var properties = node.properties;
if (properties == null) {
node = node.parent;
continue;
}
if (!properties.hasProperty(key)) {
node = node.parent;
continue;
}
value = properties.getString(key);
break;
}
if (value == null) {
return "";
}
return format(value, args);
};
/**
* @private
*/
ResourceBundle.prototype.updateResources = function() {
var files = fs.readdirSync(this.path);
var base = this.basename;
var baselen = base.length;
var locales = {};
for ( var i = 0, j = files.length; i < j; i++) {
var file = files[i];
if (file.indexOf(this.basename) !== 0) {
continue;
}
var ext = fs.extname(file);
if (!ext) {
continue;
}
if (!ext in require.extensions) {
continue;
}
var name = file.substring(baselen, file.length - ext.length);
if (name !== "" && name[0] !== "_") {
continue;
}
name = name.substr(1).toLowerCase();
if (name in locales) {
continue;
}
locales[name] = fs.join(this.path, file);
}
var registry = new ResourceTree();
var current;
registry.path = locales[""];
delete locales[""];
for ( var locale in locales) {
var path = locales[locale];
locale = canonicalize(locale);
current = registry;
for ( var i = 0, j = locale.length; i < j; i++) {
var section = locale[i];
var child = current.children[section];
if (child == null) {
child = current.children[section] = new ResourceTree();
child.parent = current;
if (i === j - 1) {
child.path = path;
}
}
current = child;
}
}
this.registry = registry;
};
//
// リソースツリー
//
function ResourceTree() {
this.children = {};
}
ResourceTree.prototype.path = null;
ResourceTree.prototype.children = null;
ResourceTree.prototype.parent = null;
ResourceTree.prototype.__properties__ = null;
//
// 遅延ローディングのためのgetter
//
ResourceTree.prototype.__defineGetter__("properties", function() {
if (this.__properties__ != null) {
return this.__properties__;
}
if (!this.path == null) {
return null;
}
delete require.cache[this.path];
var values = require(this.path);
this.__properties__ = new Properties();
this.__properties__.loadObject(values);
return this.__properties__;
});
/**
* リソースのエントリを表現します。
*
* @class リソースのエントリを表現します。
* @constructor
* @param {ResourceBundle}
* resource リソースバンドル
* @param {String}
* key リソースのキー
* @param {Array}
* args リソースのメッセージフォーマットの引数
* @param {String}
* locale ロケール
* @property {ResourceBundle} resource リソースバンドル
* @property {String} key リソースのキー
* @property {Array} args リソースのメッセージフォーマットの引数
* @property {String} locale ロケール
*/
function ResourceEntry(resource, key, args, locale) {
this.resource = resource;
this.key = key;
this.args = args;
this.locale = locale;
}
/**
* 現在のエントリの状態に基づいて、メッセージのフォーマットを行った結果を返します。
*
* @return {String} 文字列
*/
ResourceEntry.prototype.toString = function() {
return this.resource.format(this.key, this.args, this.locale);
};
//
// フォーマット
//
var format = (function() {
var cache = [];
return function(str, args) {
for ( var i = 0, j = args.length; i < j; i++) {
var regexp = cache[i];
if (regexp == null) {
regexp = new RegExp("\\{" + i + "\\}", "g");
cache[i] = regexp;
}
str = str.replace(regexp, args[i]);
}
return str;
};
})();
//
// ロケールを正規化する
//
var canonicalize = (function() {
var pattern = /([a-zA-Z])+/g;
return function(locale) {
if (!locale) {
return [];
}
var match = locale.match(pattern);
if (match) {
if (match.length > 3) {
match.length = 3;
}
} else {
match = [];
}
for ( var i = 0, j = match.length; i < j; i++) {
match[i] = match[i].toLowerCase();
}
return match;
}
})();
//
// expose
//
module.exports = ResourceBundle;