単数複数の世界。- ChoiceFormatとgettextの比較 -
"%d日"相当の日本語メッセージを英語に翻訳する時、"%d days"にしてしまうと、1日の時の英語が"1 days"になってしまいます。どうですかと聞かれて、不自然、と答えましたが、どう書くべきかよく分かりませんでした。
Javaとgettextの両方で単数複数の問題への対応方法があります。
Javaではjava.text.ChoiceFormatを使います。ベタに使ったサンプルは次のようになります。
// ChoiceFormatのベタなサンプル import java.util.*; import java.text.*; class My { public static void main(String[] args) { // prepare ResourceBundle resourceBundle = ResourceBundle.getBundle("appname"); String msg = resourceBundle.getString("Message.Reminder"); MessageFormat format = new MessageFormat(msg); ChoiceFormat dayform = new ChoiceFormat(new double[]{1, 2}, new String[]{resourceBundle.getString("Message.Day"), resourceBundle.getString("Message.Days")}); format.setFormatByArgumentIndex(0, dayform); // use msg = format.format(new Integer[]{ 1 }); System.out.println(msg); msg = format.format(new Integer[]{ 2 }); System.out.println(msg); } }
対応するリソースファイルを英語版と日本語版で次のように用意します。
# appname_en.properties Message.Reminder={0} remains Message.Days={0} days Message.Day={0} day
# appname_ja.properties.in Message.Reminder=残り{0} Message.Days={0}日 Message.Day={0}日
ロケールを変えて実行してみると、次のように英語版でdayとdaysが切り替わります。ChoiceFormatをはさむことで、formatメソッドに渡した数値でMessage.DayキーもしくはMessage.Daysキーの{0}を置換し、その置換後の文字列でMessage.Reminderキーの{0}を置換します。
$ native2ascii appname_ja_JP.properties.in > appname_ja_JP.properties $ LANG=en_US java My 1 day remains 2 days remains $ LANG=ja_JP java My 残り1日 残り2日
長い上に複雑なのでコードの説明をします。最初の2行は一般的なリソースファイルの読み込みのコードです。これで、appname.propertiesファイルの中からMessage.Reminderをキーにして出力メッセージを探します。
Cの書式化処理の%dなどに相当する処理を担当するのがMessageFormatです。Message.Reminderキーでひける文字列の{0}の部分を与えた引数で置き換えます。ChoiceFormatが無ければ、format.format(new Integer[]{ 1 });で "1 remains"を出力、format.format(new Integer[]{ 2 });で "2 remains"を出力します。
ChoiceFormatのコンストラクタの引数はトリッキーです。最初の引数のnew double[]{1, 2}が、「1以上2未満」と「2以上」を指定しています。2番目の引数の文字列の配列の要素がそれぞれ「1以下2未満」および「2以上」に対応する文字列キーです。つまり「1以上2未満」だと"Message.Day"キーを使い、「2以上」だと"Message.Days"キーを使うという指示をしています。ふたつの配列の同じインデックス同士の要素を暗黙に対応させるという、強引(と言うか手抜き)なメソッドです。ちなみに、{2,5,10}のように渡すと、「2以上5未満」「5以上10未満」「10以上」の指定になります。
0を渡すと0 days remainsになるので「1以上2未満」と「2以上」は正確には「1以上2未満」と「その他」の動作をするようです。
さて、この方法は結局、元のJavaコードが日本語版であれば、英語の単数複数に対応するためソースコードの書き換えを意味します。これは困ります。ChoiceFormatのクールなところは、ソースコードの書き換えなしで、リソースファイル側だけで対応可能な点です。次のコードはChoiceFormatを陽に使っていません。単数複数をまったく意識していないコードです。
// ChoiceFormatを陽に使わないサンプル import java.util.*; import java.text.*; class My { public static void main(String[] args) { ResourceBundle resourceBundle = ResourceBundle.getBundle("appname"); String msg = resourceBundle.getString("Message.Reminder"); MessageFormat format = new MessageFormat(msg); msg = format.format(new Integer[]{ 1 }); System.out.println(msg); msg = format.format(new Integer[]{ 2 }); System.out.println(msg); } }
日本語リソースファイルは次のようになっています。これも単数複数をまったく意識していません。
# appname_ja.properties.in Message.Reminder=残り{0}日
英語リソースファイルを次のようにします。これで先ほどChoiceFormatを使った時と同じ動作をします。
# appname_en.properties Message.Reminder={0,choice,1#{0} day|1<{0} days} remains
{0} remains と書くはずのところで、{}の中が謎の表記になっています。カンマの後のchoiceという予約語により%d相当の引数の数値(nと呼びます)での動作の切り替えを指示します。1#と1<が条件式相当です。1#はnが1以上を意味します。1<はnが1より大きいを意味します。結局、|の前後で「1以上2未満」「2以上」の条件分岐ができます。それぞれに書式化文字列として{0} dayと{0} daysを指定しています。{0}は%d相当なのでnに置き換わります。全体としては1 day remainsや2 days remainsになります。
奇妙な表記が発明された印象ですが、ソースコードに手をいれずに対応できます。
gettextは更にすごいです。
gettext付属のツールのソースコードから引用すると次のような感じです。ngettextで単数と複数の場合の英語メッセージを指定します。一見、ソースコードのレベルで単数と複数を意識するのがJavaより劣って見えますが、gettextの思想は英語メッセージは書いたままそのまま使えることなので、これはこれで思想どおりです。
char *msg2 = xasprintf (ngettext ("but some messages have only one plural form", "but some messages have only %lu plural forms", min_nplurals), min_nplurals);
日本語の翻訳ファイル(poファイル)の該当部分は次のようになっています。単数複数の区別がないので翻訳メッセージは1種類だけです。
msgid "but some messages have only one plural form" msgid_plural "but some messages have only %lu plural forms" msgstr[0] "しかしいくつかのメッセージには %lu 個だけ複数形があります"
単数複数の区別が無いことを示すためにpoファイルの先頭に次の指示があります。
"Plural-Forms: nplurals=1; plural=0;\n"
これだけ見るとよく分かりませんが、単数複数の区別が3つ以上の言語を見ると病的さがわかります。たとえばsr.poは次のようになっています。Webブラウザでは文字化けするので翻訳メッセージ部分は...で省略します。
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : (n%10>=2 && n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" msgid "but some messages have only one plural form" msgid_plural "but some messages have only %lu plural forms" msgstr[0] "... %lu ..." msgstr[1] "... %lu ..." msgstr[2] "... %lu ..."
nplurals=3が単数複数のパターンが3種類あることを指定しています。それぞれの翻訳メッセージはmsgstr[n]で指定します。3パターンの場合分けを、なんとCの3項演算子で行います。上の例だと「1の位が1で10の位が1以外の場合(n%10==1 && n%100!=11)」「1の位が2,3,4で10の位が1以外(n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20)」「それ以外」の3パターンです(たぶん合っていると思いますが、自信が持てないぐらい複雑です)。3項演算子の分岐の3パターンに0,1,2を割り当てます。これはmsgstr配列のインデックスに当たります。
3項演算子相当のパースのためにgettextはbison(yacc)でパーサコードまで書いています。Javaを越える病気っぷりです。
gettextのルールで表記すると英語は
Plural-Forms: nplurals=2; plural=n != 1;
で、フランス語は
Plural-Forms: nplurals=2; plural=n>1;
のようです。一見、同じようですが0を複数形にするかどうかの違いがあります。英語の場合、0は複数形です。zero day attackとか言う気がしますが、英語のルールも謎です。
- Category(s)
- カテゴリなし
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/inoue/plural-choice-gettext/tbping