「Catyの国際化をどうしようか」と話していたとき、Kuwataさんが面白いアイディアを出してくれたので、それを以下に書きます。Catyでは、この方式を採用するつもりです。
内容:
- 前提
- 例題と使い方
- 原理
- 問題点と改善案
- 完全グラフと部分写像
前提
対象となるテキストは、UIで使うラベルとかエラーメッセージなどです。ヘルプテキストとかマニュアルのような長いテキストは対象外です。
例題と使い方
説明にはJavaScriptを使います。例題はメニュー項目。
メニュー項目を次のように作るとします。
var startItem = new MenuItem("start", app.startIt);
MenuItemがコンストラクタで、その第1引数がラベルテキスト、第2引数はメニュー項目が選択されたときに呼ばれる関数とします。
問題はラベルテキスト"start"です。このまま表示してしまうと英語だけになってしまいます。そこで、MenuItem内で次のようにします。
function MenuItem(label, func) {
var displayLabel = i18n.getText(label);
// ..
}
基本的にはこれだけで、displayLabelに、実行時の環境に応じた言語によるラベルがセットされます。
実行時の言語がなんであるか(たとえば日本語だとか)は、環境変数とか大域的な設定から取りますが、明示的に指定していたいなら次のように書くことにします。
var displayLabel = i18n.getText(label, lang.JA);
原理
getTextが何を行うかを、少し抽象化して述べます。言語が、英語(en)、日本語(ja)、フランス語(fr)の3種ある状況を例とします。
英語のラベルテキストの集合をL(en)とします。L(ja), L(fr)はそれぞれ日本語、フランス語のラベル集合です。より具体的に、次のような集合だとします。
- L(en) = {"start", "stop", "welcome"}
- L(ja) = {"開始", "停止"}
- F(fr) = {"bienvenue"}
翻訳による対応関係は次のようだとしましょう。
英語 | 日本語 | フランス語 |
---|---|---|
start | 開始 | |
stop | 停止 | |
welcome | bienvenue |
t(x, y) は、言語xから言語yへの翻訳を行う関数とします。方向がハッキリと分かるように、カンマの代わりに矢印も使えるとして、t(en->ja) のように書きます。例えば:
- t(en->ja)("stop") = "停止"
- t(ja->en)("開始") = "start"
- t(en->fr)("welcome") = "bienvenue"
定義されてないことを示すには記号⊥を使うことにして:
- t(en->ja)("welcome") = ⊥
- t(ja->fr)("停止") = ⊥
さて、i18n.getText(s, y) がどう動くかを説明します。sがテキスト文字列、yは言語(を表すコード)です。
- テキスト文字列sが、L(en), L(ja), L(fr) のどれに所属するかを探す。
- テキスト文字列sが、どの集合にも所属しないならエラー。適当に対処して終り。
- テキスト文字列sが所属する言語(を表すコード)をxとする。
- xとyが等しいなら、sを出力して終り。
- t(x->y)(s) を求める。
- t(x->y)(s) が未定義なら、sを出力して終り。(改善した処理は後述)
- t(x->y)(s) 定義されているなら、それを出力して終り。
具定例を出すと次のようです。
- i18n.getText("hoge", lnag.JA) = ⊥
- i18n.getText("stop", lnag.EN) = "stop"
- i18n.getText("stop", lnag.FR) = "stop"
- i18n.getText("stop", lnag.JA) = "停止"
問題点と改善案
今述べた方法では、L(en), L(ja), L(fr) という3つの集合が互いに排他的(共通部分がない)と暗黙に仮定しています。しかし、日本語と中国語などでは、L(ja)∩L(zh_CN) が空ではない可能性があります。すると、「どの集合に所属するかを探す」部分が曖昧になって、うまくいきません。
例えば、中国語の「項目」にはプロジェクトの意味があるようです。i18n.getText("項目", lnag.EN) = "project" となって欲しいときに、ラベルテキスト"項目"が、集合L(ja)で先に見つかると i18n.getText("項目", lnag.EN) = "item" となってしまうでしょう。
この問題点に対処するには、"項目"が日本語であるか中国語であるかを示す情報が必要です。i18n.getText関数の引数を増やす方法もありますが、単なる文字列データの代わりに {"zh_CN" : "項目"} のような「言語コード付きテキスト」を渡す方法もあります。i18n.getTextは言語コードを見て、そのテキストがどの言語のものであるかを判断します。言語コードが指定されていなかったら、ラベルの集合を端から調べることになります。
別な問題として、先に述べたアルゴリズムだと、i18n.getText("停止", lang.FR) = "停止" となります。なぜなら、"停止"に対応するフランス語ラベルがないので、日本語がそのまま使用されるからです。
この場合、フランス語がないときは英語のほうが良いでしょう。このようなフォールバック動作をさせるには、各言語ごとにフォールバック・チェーンを定義しておきます。フランス語のフォールバック・チェーンは次だとしましょう。
- fr, en, ja
こう定義しておけば、i18n.getText("停止", lang.FR) = "stop" となります。
完全グラフと部分写像
i18n.getTextが利用するデータは、各言語ごとのテキストの集合と、それらのあいだの翻訳関係です。このデータ構造をグラフと圏論の言葉で説明してみましょう(余興です ^^;)。
言語コード en, ja, fr を頂点とする有向完全グラフを考えます。自分自身に戻る有向辺も許すと、辺の総数は9本です。列挙してみます。
- en->en
- en->ja
- en-fr
- ja->en
- ja->ja
- ja->fr
- fr->en
- fr->ja
- fr->fr
3つの頂点 {en, ja, fr}と上記の9本の辺からなる有向グラフをGとします。2本の辺の結合(composition)を (en->ja);(ja->fr) = (en->fr) のように定義します。すると、圏になります。
- 対象: Obj(G) = |G| = {en, ja, fr}
- 射: Mor(G) = {en->en, ..., fr->fr}
- 恒等: id(x) = (x->x)
- 域: dom(x->y) = x
- 余域: cod(x->y) = y
- 結合: (x->y);(y->z) = (x->z)
Partialは、集合と部分写像の圏とします。部分写像とは未定義も許す写像のことです。翻訳を表す関数 t(en->ja) などは部分写像です。未定義であることは記号⊥で表したのでした。翻訳関数 t(en->ja) は部分写像で、その域はL(en) 、余域はL(ja)です。より一般に、Lとtを組み合わせると、F:G→Part という関手となります。
- x∈Obj(G) のとき、F(x) := L(x)
- (x->y)∈Mor(G) のとき、F((x->y)) := t(x->y)。t(x->y):L(x)→L(y) in Partial
この関手Fが、ラベルと翻訳関係のデータ構造を表現しています。i18n.getTextは、関手Fをバックエンドとして使うわけです。