異なる言語で正しい複数形をどのように選びますか?

JavaScript の Intl.PluralRules を使用して、言語固有のルールに基づいて1つのアイテム、2つのアイテム、少数のアイテム、多数のアイテムを選択します

はじめに

数量を含むテキストを表示する場合、異なる数に対して異なるメッセージが必要です。英語では「1 file」と書きますが、「2 files」と書きます。最も単純なアプローチは、数字と単語を連結し、必要に応じて「s」を追加することです。

function formatFileCount(count) {
  return count === 1 ? `${count} file` : `${count} files`;
}

このアプローチには3つの問題があります。まず、ゼロの場合(「0 files」は「no files」とすべき)に正しい英語を生成しません。次に、「1 child, 2 children」や「1 person, 2 people」のような複雑な複数形では機能しません。そして最も重要なのは、他の言語には全く異なる複数形のルールがあり、このコードでは対応できないことです。

JavaScriptは、この問題を解決するためにIntl.PluralRulesを提供しています。このAPIは、世界中のプロフェッショナルな翻訳システムで使用されているUnicode CLDR標準に従って、任意の言語の任意の数値に対してどの複数形を使用するかを決定します。

なぜ異なる言語には異なる複数形が必要なのか

英語では2つの複数形を使用します。「1 book」と「2 books」と書きます。数がちょうど1の場合と他の数の場合で単語が変化します。

他の言語は異なる仕組みを持っています。ポーランド語は複雑なルールに基づいて3つの形式を使用します。ロシア語は4つの形式を使用します。アラビア語は6つの形式を使用します。一部の言語ではすべての数量に対して1つの形式のみを使用します。

以下は、異なる言語で「りんご」という単語が数量に基づいてどのように変化するかを示す例です:

英語: 1 apple, 2 apples, 5 apples, 0 apples

ポーランド語: 1 jabłko, 2 jabłka, 5 jabłek, 0 jabłek

ロシア語: 1 яблоко, 2 яблока, 5 яблок, 0 яблок

アラビア語: ゼロ、1つ、2つ、少数、多数、その他の数量かによって6つの異なる形式を使用

Unicode CLDRは、各言語でいつどの形式を使用するかの正確なルールを定義しています。これらのルールを暗記したり、アプリケーションにハードコードしたりすることはできません。それらを知っているAPIが必要です。

CLDR複数形カテゴリとは

Unicode CLDR標準では、すべての言語をカバーする6つの複数形カテゴリを定義しています:

  • zero:一部の言語で、ちょうど0個のアイテムに使用
  • one:単数形に使用
  • two:双数形がある言語で使用
  • few:一部の言語で少量を表すのに使用
  • many:一部の言語でより大きな量や分数を表すのに使用
  • other:デフォルトの形式、他のカテゴリが適用されない場合に使用

すべての言語はotherカテゴリを使用します。ほとんどの言語は合計で2つか3つのカテゴリのみを使用します。カテゴリは直接数量に対応するわけではありません。例えば、ポーランド語では、数字の5はmanyカテゴリを使用しますが、0、25、1.5も同様です。

特定の数字がどのカテゴリにマッピングされるかのルールは言語によって異なります。JavaScriptでは、この複雑さをIntl.PluralRules APIを通じて処理します。

どの複数形を使用するかを決定する方法

Intl.PluralRulesオブジェクトは、特定の言語において数字がどの複数形カテゴリに属するかを決定します。ロケールを指定してPluralRulesオブジェクトを作成し、そのselect()メソッドに数字を渡して呼び出します。

const rules = new Intl.PluralRules('en-US');
console.log(rules.select(0));  // "other"
console.log(rules.select(1));  // "one"
console.log(rules.select(2));  // "other"
console.log(rules.select(5));  // "other"

英語では、select()は数字1に対して"one"を返し、それ以外のすべてに対して"other"を返します。

ポーランド語ではより複雑なルールで3つのカテゴリを使用します:

const rules = new Intl.PluralRules('pl-PL');
console.log(rules.select(0));   // "many"
console.log(rules.select(1));   // "one"
console.log(rules.select(2));   // "few"
console.log(rules.select(5));   // "many"
console.log(rules.select(22));  // "few"
console.log(rules.select(25));  // "many"

アラビア語では6つのカテゴリを使用します:

const rules = new Intl.PluralRules('ar-EG');
console.log(rules.select(0));   // "zero"
console.log(rules.select(1));   // "one"
console.log(rules.select(2));   // "two"
console.log(rules.select(3));   // "few"
console.log(rules.select(11));  // "many"
console.log(rules.select(100)); // "other"

select()メソッドはカテゴリを識別する文字列を返します。この文字列を使用して、表示に適切なメッセージを選択します。

複数形カテゴリをメッセージにマッピングする方法

複数形カテゴリを決定した後、ユーザーに表示する正しいメッセージを選択する必要があります。各カテゴリをそのメッセージにマッピングするオブジェクトを作成し、カテゴリ文字列を使用してメッセージを検索します。

const messages = {
  one: '{count} file',
  other: '{count} files'
};

function formatFileCount(count, locale) {
  const rules = new Intl.PluralRules(locale);
  const category = rules.select(count);
  const message = messages[category];
  return message.replace('{count}', count);
}

console.log(formatFileCount(1, 'en-US'));  // "1 file"
console.log(formatFileCount(5, 'en-US'));  // "5 files"

このパターンはどの言語でも機能します。ポーランド語の場合、その言語が使用する3つのカテゴリすべてにメッセージを提供します:

const messages = {
  one: '{count} plik',
  few: '{count} pliki',
  many: '{count} plików'
};

function formatFileCount(count, locale) {
  const rules = new Intl.PluralRules(locale);
  const category = rules.select(count);
  const message = messages[category];
  return message.replace('{count}', count);
}

console.log(formatFileCount(1, 'pl-PL'));   // "1 plik"
console.log(formatFileCount(2, 'pl-PL'));   // "2 pliki"
console.log(formatFileCount(5, 'pl-PL'));   // "5 plików"
console.log(formatFileCount(22, 'pl-PL'));  // "22 pliki"

コード構造はすべての言語で同一です。messagesオブジェクトのみが変更されます。この分離により、翻訳者はコードを変更することなく、自分の言語に適したメッセージを提供できます。

欠落している複数形カテゴリの処理方法

messagesオブジェクトには、可能な6つのカテゴリすべてが含まれていない場合があります。ほとんどの言語は2つか3つのカテゴリしか使用しません。select()がmessagesオブジェクトに含まれていないカテゴリを返す場合は、otherカテゴリにフォールバックします。

function formatFileCount(count, locale, messages) {
  const rules = new Intl.PluralRules(locale);
  const category = rules.select(count);
  const message = messages[category] || messages.other;
  return message.replace('{count}', count);
}

const englishMessages = {
  one: '{count} file',
  other: '{count} files'
};

console.log(formatFileCount(1, 'en-US', englishMessages));  // "1 file"
console.log(formatFileCount(5, 'en-US', englishMessages));  // "5 files"

このパターンにより、messagesオブジェクトが不完全な場合でもコードが機能することが保証されます。otherカテゴリはすべての言語に常に存在するため、安全なフォールバックとなります。

序数詞での複数形ルールの使用方法

Intl.PluralRulesコンストラクタは、カテゴリの決定方法を変更するtypeオプションを受け付けます。デフォルトのタイプは"cardinal"で、アイテムを数える際に使用されます。type: "ordinal"を設定すると、「1st」、「2nd」、「3rd」などの序数詞の複数形を決定できます。

const cardinalRules = new Intl.PluralRules('en-US', { type: 'cardinal' });
console.log(cardinalRules.select(1));  // "one"
console.log(cardinalRules.select(2));  // "other"
console.log(cardinalRules.select(3));  // "other"

const ordinalRules = new Intl.PluralRules('en-US', { type: 'ordinal' });
console.log(ordinalRules.select(1));   // "one"
console.log(ordinalRules.select(2));   // "two"
console.log(ordinalRules.select(3));   // "few"
console.log(ordinalRules.select(4));   // "other"

基数詞ルールは「1 item, 2 items」を決定します。序数詞ルールは「1st place, 2nd place, 3rd place, 4th place」を決定します。文法パターンが異なるため、返されるカテゴリも異なります。

分数量のフォーマット方法

select()メソッドは小数にも対応しています。言語によって、分数が複数形カテゴリにマッピングされる特定のルールがあります。

const rules = new Intl.PluralRules('en-US');
console.log(rules.select(1));    // "one"
console.log(rules.select(1.0));  // "one"
console.log(rules.select(1.5));  // "other"
console.log(rules.select(0.5));  // "other"

英語では、1.0は単数形を使用しますが、1.5は複数形を使用します。言語によっては分数に対して異なるルールがあり、別のカテゴリとして扱うこともあります。

const messages = {
  one: '{count} file',
  other: '{count} files'
};

const rules = new Intl.PluralRules('en-US');
const count = 1.5;
const category = rules.select(count);
const message = messages[category];
console.log(message.replace('{count}', count));  // "1.5 files"

小数を直接select()に渡してください。このメソッドは自動的に正しい言語ルールを適用します。

再利用可能な複数形フォーマッタの作成方法

アプリケーション全体で同じパターンを繰り返す代わりに、複数形選択ロジックをカプセル化した再利用可能な関数を作成しましょう。

class PluralFormatter {
  constructor(locale) {
    this.locale = locale;
    this.rules = new Intl.PluralRules(locale);
  }

  format(count, messages) {
    const category = this.rules.select(count);
    const message = messages[category] || messages.other;
    return message.replace('{count}', count);
  }
}

const formatter = new PluralFormatter('en-US');

const fileMessages = {
  one: '{count} file',
  other: '{count} files'
};

const itemMessages = {
  one: '{count} item',
  other: '{count} items'
};

console.log(formatter.format(1, fileMessages));  // "1 file"
console.log(formatter.format(5, fileMessages));  // "5 files"
console.log(formatter.format(1, itemMessages));  // "1 item"
console.log(formatter.format(3, itemMessages));  // "3 items"

このクラスはPluralRulesオブジェクトを一度作成し、複数のフォーマット操作で再利用します。メッセージに挿入する前にIntl.NumberFormatでカウントをフォーマットするなど、より高度な機能をサポートするように拡張することもできます。

複数形ルールを翻訳システムと統合する方法

プロフェッショナルな翻訳システムでは、複数形カテゴリのプレースホルダーを含むメッセージを保存します。テキストを翻訳する際、対象言語に必要なすべての複数形を提供します。

const translations = {
  'en-US': {
    fileCount: {
      one: '{count} file',
      other: '{count} files'
    },
    downloadComplete: {
      one: 'Download of {count} file complete',
      other: 'Download of {count} files complete'
    }
  },
  'pl-PL': {
    fileCount: {
      one: '{count} plik',
      few: '{count} pliki',
      many: '{count} plików'
    },
    downloadComplete: {
      one: 'Pobieranie {count} pliku zakończone',
      few: 'Pobieranie {count} plików zakończone',
      many: 'Pobieranie {count} plików zakończone'
    }
  }
};

class Translator {
  constructor(locale, translations) {
    this.locale = locale;
    this.translations = translations[locale] || {};
    this.rules = new Intl.PluralRules(locale);
  }

  translate(key, count) {
    const messages = this.translations[key];
    if (!messages) return key;

    const category = this.rules.select(count);
    const message = messages[category] || messages.other;
    return message.replace('{count}', count);
  }
}

const translator = new Translator('en-US', translations);
console.log(translator.translate('fileCount', 1));         // "1 file"
console.log(translator.translate('fileCount', 5));         // "5 files"
console.log(translator.translate('downloadComplete', 1));  // "Download of 1 file complete"
console.log(translator.translate('downloadComplete', 5));  // "Download of 5 files complete"

const polishTranslator = new Translator('pl-PL', translations);
console.log(polishTranslator.translate('fileCount', 1));   // "1 plik"
console.log(polishTranslator.translate('fileCount', 2));   // "2 pliki"
console.log(polishTranslator.translate('fileCount', 5));   // "5 plików"

このパターンは翻訳データをコードロジックから分離します。翻訳者は各言語で使用される複数形カテゴリごとにメッセージを提供します。コードは自動的にルールを適用します。

ロケールがどの複数形カテゴリを使用するかを確認する方法

resolvedOptions()メソッドはPluralRulesオブジェクトに関する情報を返しますが、ロケールが使用するカテゴリのリストは提供しません。ロケールが使用するすべてのカテゴリを見つけるには、一連の数値をテストして、返される一意のカテゴリを収集します。

function getPluralCategories(locale) {
  const rules = new Intl.PluralRules(locale);
  const categories = new Set();

  for (let i = 0; i <= 100; i++) {
    categories.add(rules.select(i));
    categories.add(rules.select(i + 0.5));
  }

  return Array.from(categories).sort();
}

console.log(getPluralCategories('en-US'));  // ["one", "other"]
console.log(getPluralCategories('pl-PL'));  // ["few", "many", "one"]
console.log(getPluralCategories('ar-EG'));  // ["few", "many", "one", "other", "two", "zero"]

この手法は整数と0.5刻みの値を範囲内でテストします。これにより、特定のロケールに対してメッセージオブジェクトに含める必要があるカテゴリを把握できます。