ファイル形式・標準規格Read this article in English

ICU MessageFormat の構文 — plural・select と言語別の CLDR カテゴリ

ICU MessageFormat は、ICU(International Components for Unicode)プロジェクトが定義したメッセージテンプレートの構文です。複数形・性別・序数・数値や日付の書式といった「文が分岐する部分」を翻訳対象の文字列の中に閉じ込め、周りのコードがどの言語の文法も知らずに済むようにします。

構文の大半はこの一点から説明できます。plural の分岐は数値そのものではなく、その数値が対象言語でどの CLDR 複数形カテゴリに入るかで選ばれます。英語は2つ、日本語は1つ、ロシア語は4つ、アラビア語は6つです。だから「You have 1 items」は誤字ではなく文法のバグであり、ロシア語版の分岐が何本必要かを決めるのは英語を書いた人ではなくロシア語の側です。

構文の全体像 — 引数・型・スタイルと、波括弧を文字として出す書き方

引数は波括弧の中の名前です。2つめのフィールドを足すと型になり、3つめを足すと型にスタイルが付きます。

number・date・time の型は、自前の書式規則を持っていません。値を対象言語の CLDR 書式データに渡すだけです。そこが要点で、テンプレートは1つのまま、区切り記号・数字の字形・項目の並びが言語ごとに変わります。フランス語の桁区切りは通常の空白ではなくナロー・ノーブレークスペース(U+202F)なので、普通の空白を期待した比較は外れます。

新しめの ICU は、コロン2つに続けてスケルトン(欲しい出力の記述)を書く形も受け付けます。使えるかどうかはライブラリがリンクしている ICU のバージョン次第なので、ドキュメントで先に確認してください。

波括弧が構文そのものなので、波括弧を文字として表示したいときはエスケープが必要です。ICU の既定の挙動では、ASCII のアポストロフィはエスケープが必要な文字が直後に来たときだけ引用を開始し、アポストロフィ2つは常に文字としてのアポストロフィ1つになり、普通の文中のアポストロフィはそのまま残ります。実装によっては、すべてのアポストロフィが引用を開始する厳格モードもあります。規則は ICU 公式ドキュメントの Quoting and Escaping の章が基準です。

{playerName}                      引数。そのまま置換される
{score, number}                   型
{ratio, number, percent}          型 + スタイル
{releasedOn, date, long}          date/time のスタイル: short medium long full
{startsAt, time, short}
{price, number, ::currency/JPY}   number スケルトン(新しい ICU のみ)
{count, plural, ...}              分岐する型
{gender, select, ...}
{rank, selectordinal, ...}

同じテンプレート、違うロケール(Intl 経由の CLDR 出力):
  {n, number}       1234567.5   en-US  1,234,567.5
                                de-DE  1.234.567,5
                                fr-FR  1 234 567,5  (separator: U+202F)
                                ar-EG  ١٬٢٣٤٬٥٦٧٫٥
  {d, date, short}              en-US  9/12/26
                                ja-JP  2026/09/12
                                de-DE  12.09.26
  {d, date, long}               en-US  September 12, 2026
                                ja-JP  2026年9月12日
  {t, time, short}              en-US  3:04 PM
                                ja-JP  15:04

波括弧とアポストロフィを文字として出す:
  '{'          ->   {
  '{count}'    ->   {count}   (置換されず、そのまま表示される)
  ''           ->   '
  It's fine    ->   It's fine  (アポストロフィはそのまま)

plural — =0 の完全一致、カテゴリ、# と offset

plural 引数は分岐を並べたもので、セレクタには2種類あります。等号に数値を続けた完全一致セレクタは、その値そのものに一致します。キーワードセレクタは CLDR カテゴリに一致します。完全一致が先に試されるので、英語で zero が文法カテゴリであるかのように扱わずに、0件のときだけ別の文を出せます。plural には other の分岐が必須です。

分岐の中では、# が数値に置き換わります。# を使わずに数字を直接書いてしまうのはよくある失敗です。どのロケールでも桁区切りのない数値になり、しかも翻訳者が動かしてよい唯一のトークンが消えます。

offset 機能は、カテゴリを選ぶ前に一定数を引きます。「1人を名前で出して残りを数える」ように、表示する数と文法を決める数がずれる言い回しのためにあります。キーワードの選択と # は、どちらも offset を引いた後の値を使います。完全一致セレクタは、引く前の入力値に対して照合されます。この挙動は ICU 公式ドキュメントの Plural Formatting の章の例がそのまま基準になります。

{count, plural,
  =0    {Your inventory is empty}
  one   {# item}
  other {# items}
}

offset: 文は1人を名前で出し、文法は残りの人数を数える
{guests, plural, offset:1
  =0    {Nobody is coming}
  =1    {{host} is coming}
  =2    {{host} and one other person are coming}
  other {{host} and # other people are coming}
}
  guests = 1  ->  =1 は入力値に一致する(offset は引かれない)
  guests = 4  ->  other の分岐。# は 3 になる(4 から offset を引いた値)
  ここで one の分岐が要らないのは、英語では offset 適用後に one に入る
  入力値が 2 だけで、それは =2 が先に一致するため。ロシア語では
  分岐がもっと必要になる。

必要なカテゴリは言語ごとに違う(実測した対応表)

ある言語がどのカテゴリを使い、どの数値がどこに入るかは CLDR のデータで、この記事を信用せず自分で確認できます。JavaScript ランタイムの Intl.PluralRules は ICU の上に実装されているので、同じ規則で答えます。データの成り立ちは Unicode CLDR の解説 にあります。

この表から、メッセージの書き方が変わる結論が5つ出ます。

日本語のカテゴリは other だけです。日本語訳に one の分岐があっても間違いというより死んだコードで、選ばれることがありません。中国語・韓国語も同じです。そして日本語をソース言語にしている場合、複数形の問題は英語ビルドまで見えません。

ロシア語は4本必要で、しかも one は「1」を意味しません。21 は one、22 は few、5 と 11 と 100 は many です。one と other だけのロシア語文字列は、ほとんどの数値で壊れて見えます。

フランス語は 0 を one に入れます。単数形が 0 と 1 の両方を受け持つので、英語の2形ルールを埋め込むと 0 で誤った形になります。ポーランド語は似た見た目の数値をさらに別に分け、22 は few ですが 25 は many です。

アラビア語は zero と two を含む6カテゴリすべてを使います。分岐を1本省くと、どこかの数値で必ず崩れます。

序数は基数とは別の規則です。英語は基数が2カテゴリなのに序数は4カテゴリです。11th・12th・13th は other に入り、21st は one に戻り、111th はまた other です。

n のカテゴリ(基数。Intl.PluralRules = ICU/CLDR データで実測)

  n          en      ja      ru      ar      pl      fr
  0          other   other   many    zero    many    one
  1          one     other   one     one     one     one
  2          other   other   few     two     few     other
  5          other   other   many    few     many    other
  11         other   other   many    many    many    other
  21         other   other   one     many    many    other
  22         other   other   few     many    few     other
  25         other   other   many    many    many    other
  100        other   other   many    other   many    other
  1000000    other   other   many    other   many    many

そのロケールが実際に使うカテゴリ(基数)

  en   one other
  ja   other
  ru   one few many other
  ar   zero one two few many other
  pl   one few many other
  fr   one many other

序数のカテゴリ

  en   one two few other     1st 2nd 3rd 4th ... 11th 12th 13th ... 21st
  ja   other
  ru   other

select と selectordinal、そしてネストの代償

select は汎用の分岐です。引数の値を、CLDR ではなくコード側が決めた名前と照合します。使う理由として多いのは文法上の性で、多くの言語では動詞の形・冠詞・形容詞が主語に一致しますが、英語原文には一致させる手がかりがありません。

selectordinal は plural と見た目も挙動も似ていますが、基数ではなく序数の規則から選びます。順位・ランキング・日付の「何日」を出す文字列で必要になります。

引数は入れ子にできます。select の分岐の中に plural を置けば、1つのメッセージで性と個数を同時に扱えます。同時にそこが、得より損が大きくなり始める地点です。3階層目の翻訳者は目の前の断片がどの条件の組み合わせから出るのかを自力で復元しなければならず、レビュアーも全組み合わせを通さなければ全出力を見られません。

多くの場合、安いのは分けるほうです。性ごとに1つのメッセージを用意してコード側でキーを選び、各メッセージの中はフラットな plural にします。テキストは少し重複しますが、代わりにどの分岐も単独で読めて、訳せて、テストできます。ネストは分岐が本当に相互作用するメッセージだけに残します。この判断は、文字列キーの命名設計 の側にすでに区別が入っていれば格段に楽になります。

{gender, select,
  female {She picked up the sword}
  male   {He picked up the sword}
  other  {They picked up the sword}
}

{rank, selectordinal,
  one   {#st place}
  two   {#nd place}
  few   {#rd place}
  other {#th place}
}

ネスト — 正しく動くが、訳しにくくレビューしにくい:
{gender, select,
  female {{count, plural, one {She found # key}  other {She found # keys}}}
  male   {{count, plural, one {He found # key}   other {He found # keys}}}
  other  {{count, plural, one {They found # key} other {They found # keys}}}
}

分割 — 出力は同じで、フラットなメッセージ3本:
found.female = {count, plural, one {She found # key}  other {She found # keys}}
found.male   = {count, plural, one {He found # key}   other {He found # keys}}
found.other  = {count, plural, one {They found # key} other {They found # keys}}

翻訳者に渡すときの指示と、スクリプトで検出できること

MessageFormat の文字列は、1つのフィールドにコードと文章が同居しています。どちらがどちらか伝えられていない翻訳者は、当然ながら両方を編集します。指示は3つです。

構造は変えない。引数名・型のキーワード・波括弧・# は、そのまま残す必要があります。引数名を訳すと、その引数は値に置き換わりません。

カテゴリの集合は変える。これは不具合ではなく翻訳者の仕事です。英語にあった分岐を埋めることしかできない翻訳者は、正しいロシア語文字列を作れません。ファイル形式と作業手順の両方が、分岐の追加と削除を許す形になっていなければなりません。

# は語順に合わせて動かしてよい。数値が英語と同じ位置にある必要はなく、言語によってはそこに置けません。分岐の中の他の引数の位置についても同じです。

機械的な失敗の大半は、人が開く前にスクリプトで検出できます。いちばん安いレビューです。

  • 波括弧の対応が取れていて、plural と select のすべてに other の分岐がある。
  • 原文のすべての引数名が、同じ綴りで訳文にも出てくる。
  • 原文が # だった位置に、数字が直接書かれた分岐がない。
  • キーワードセレクタが、その言語が実際に使うカテゴリだけである(日本語に残った one や、ロシア語に足りない few が引っかかる)。
  • ネストの深さが原文より増えていない。
文字列と一緒に渡す指示書

  変えない    {count, plural, ...}   {playerName}   #   型のキーワード
  変える      波括弧の中のテキスト
  必ず変える  セレクタの集合を、その言語に合わせる
                ja  other
                ru  one few many other
                ar  zero one two few many other
  動かしてよい  # と {引数} を、語順に必要な位置へ

MessageFormat を使うべきとき、使わないほうがよいとき

MessageFormat が複雑さに見合うのは、対象言語の文法が実行時の値で変わるときです。文の中の個数、性の一致、序数です。プレイヤー名に固定の述語が続くだけのメッセージは、パーサを増やすだけで正しさは増えません。

plural 引数より優れた代替が2つあります。1つは文を作らないことです。ラベルと数値を別のセルに置いたり、名詞の後ろに括弧付きで件数を出したりする形です。どの言語でも壊れる文法がなく、狭い UI でも好都合です。もう1つはケース別のキー分割です。どちらも、文字列を ハードコードされた文字列の外部化 する段階で検討する価値があります。

対応状況はばらついています。ICU 自身が Java と C のライブラリで MessageFormat を提供しており、これが参照実装です。JavaScript の国際化ライブラリのいくつかが同じ構文を実装していますが、サブセットである場合もあります。正確な答えは、そのライブラリのドキュメントのバージョン別対応表にあります。

Android の strings.xml の文字列リソースは plurals 要素を使い、quantity のキーワードは同じ CLDR の集合から来ていますが、ファイルの構文は MessageFormat ではありません。gettext の PO 形式 は、文字列の中のキーワードではなくファイルヘッダの式で番号付きの形を選びます。ゲームエンジンのローカライズ機構は、一般に MessageFormat の文字列をそのままでは受け付けません。独自の複数形構文を持つものもあり、何も持たずに分岐をコードやライブラリに任せるものもあります。どちらかは推測せず、エンジンのローカライズのドキュメントで確認してください。

関連記事