メッセージリソースを別ファイルで管理する
ある程度の規模のWebアプリを作るとき、メッセージリソースの管理が必要になってくる。
用語の統一する、修正漏れを減らすなどの観点から、文言をベタ書きにするのではなく、一箇所にまとめて管理したほうが良い。
例えば、msg.jsonというファイルを作って、angular.jsやhandlebarsでHTMLに埋め込むようにする。
msg.json
{
"TITLE": "オクニシタカドットコム",
"WELCOME_MESSAGE": "オクニシタカドットコムへようこそ"
}
welcome.html
<html>
<head><title>{{msg.TITLE}}</title></head>
<body><h1>{{msg.WELCOME_MESSAGE}}</body>
</html>
メッセージリソースからメッセージリソースを参照したい
さて、これでHTMLから文言のベタ書きが消えた。
しかし、今度はメッセージリソースないで同じような文言があるのが気になる。
これもなんとか統一したい。
手っ取り早いのはロジックを埋め込んでしまう方法。.jsonをやめて.jsにし、文字列結合してしまう。
msg.js
exports.TITLE = "オクニシタカドットコム";
exports.WELCOME_MESSAGE = exports.TITLE + "へようこそ"
うん、読みづらい。
しかも下手にjsにしてしまうと半端にロジックが混ざって後から混乱すること必至。
やっぱりjsonのまま、いい感じに埋め込めるとよい。
すなわち、
msg.json
{
"TITLE": "オクニシタカドットコム",
"WELCOME_MESSAGE": "#{TITLE}へようこそ"
}
的な感じにしたい。
JSON内での埋め込み式を解釈する関数を設計する
よし、それ用の関数を作ろう。
仕様としては
- JSONオブジェトの文字列プロパティに式を埋め込む
- 埋め込まれた式は自身のプロパティを参照する
- Nest構造にもいい感じにする
使い方としてはとしては
var locale = evaljson({
keys: {
"NAME": "My Awesome App"
},
titles: {
"WELCOME_TITLE": "Welcome to #{keys.NAME}!"
},
/*...*/
});
console.log(locale.titles['WELCOME_TITLE']); //-> Welcome to My Awesome App!
JSON内での埋め込み式を解釈する関数を実装する
まず、keys.NAME
みたいなドット繋ぎの文字列指定で、nestオブジェクトのプロパティにアクセスしたい。
アクセスの度にドットを解釈するよりも、事前にアクセス先のオブジェクトを平たくした方が簡単そうだ。
つまり、
var flattened = flatten({
'foo': {'bar': 'baz'}
});
console.log(flattened); // => {'foo.bar': 'baz'}
というふうに、入れ子のオブジェクトをドット繋ぎのキーに変換しておく。
このflatten
関数の実装としては、
function flatten(nested) {
var flattened = {};
Object.keys(nested || {}).forEach(function (key) {
var value = nested[key];
switch (typeof(value)) {
case 'string':
case 'number':
case 'function':
flattened[key] = value;
break;
default:
var subValues = flatten(value);
Object.keys(subValues).forEach(function (subKey) {
var fullKey = [key, subKey].join('.');
flattened[fullKey] = subValues[subKey];
});
break;
}
});
return flattened;
}
のように再帰呼び出しでオブジェクト型のプロパティを、潜っていく感じになる。
これができれば、あとは正規表現で埋め込むだけだ。
"#{keys.NAME}!"
を解釈できるようにするには、
function _embed(src, values) {
var dest = extend({}, src);
Object.keys(dest).forEach(function (key) {
var value = dest[key];
switch (typeof(value)) {
case 'object':
dest[key] = _embed(value, values);
break;
case 'string':
dest[key] = value.replace(/(#\{)(.*?)(\})/g, function ($0, $1, $2) {
var valid = values.hasOwnProperty($2);
if (valid) {
return values[$2];
} else {
throw new Error('Unknown expression:' + $2);
}
});
break;
default:
break;
}
});
return dest;
}
var dict = flatten(src); // Use flatten object as dictionary.
var parsed = _embed(src, flatten(src));
console.log(parsed);
とやれば良い。
npmパッケージ化する
あとはこれのパッケージ化。
関数を整理してテストを書いてnpmにpublishした。
$ npm install evaljson --save
でインストールして、
var evaljson = require('evaljson');
var locale = evaljson({
keys: {
"NAME": "My Awesome App"
},
titles: {
"WELCOME_TITLE": "Welcome to #{keys.NAME}!" // Embed value.
},
/*...*/
});
console.log(locale.titles['WELCOME_TITLE']); //-> Welcome to My Awesome App!
的な感じにした。
flatten
の関数は別のとこでも使えそうなので、逆のexpand
関数も一緒につくって別のnpmパッケージにまとめた。