異なる言語に適した複数形をどのように選択しますか?

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: 一部の言語でアイテムがちょうどゼロの場合に使用
  • 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"

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

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

メッセージオブジェクトには、6つの可能なカテゴリすべてが含まれていない場合があります。ほとんどの言語は2つまたは3つのカテゴリのみを使用します。select()がメッセージオブジェクトに存在しないカテゴリを返す場合は、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"

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

序数詞で複数形ルールを使用する方法

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

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個のアイテム、2個のアイテム」を決定します。序数ルールは「1位、2位、3位、4位」を決定します。文法パターンが異なるため、返されるカテゴリも異なります。

分数量の書式設定方法

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"]

この手法は、範囲内の整数と半値をテストします。特定のロケールに対してメッセージオブジェクトに含める必要があるカテゴリを取得します。