ロケールで利用可能なすべての複数形を取得する

翻訳を提供する必要がある複数形カテゴリを発見する

はじめに

多言語アプリケーションを構築する際、数量に応じて異なるテキスト形式を提供する必要があります。英語では「1 item」と「2 items」と書きます。これは単純に見えますが、他の言語をサポートし始めると複雑になります。

ロシア語では数に応じて3つの異なる形式を使用します。アラビア語では6つの形式を使用します。一部の言語ではすべての数に対して同じ形式を使用します。これらの形式の翻訳を提供する前に、各言語にどの形式が存在するかを知る必要があります。

JavaScriptでは、ロケールがどの複数形カテゴリを使用するかを発見する方法を提供しています。PluralRulesインスタンスのresolvedOptions()メソッドは、そのロケールが必要とするすべての複数形を列挙するpluralCategoriesプロパティを返します。これにより、推測や言語固有のルールテーブルを維持することなく、提供すべき翻訳を正確に知ることができます。

複数形カテゴリとは

複数形カテゴリは、言語間で使用される異なる複数形の標準化された名称です。Unicode CLDR(共通ロケールデータリポジトリ)は、zero、one、two、few、many、otherの6つのカテゴリを定義しています。

すべての言語が6つのカテゴリすべてを使用するわけではありません。英語では2つだけ使用します:oneとotherです。oneカテゴリは数1に適用され、otherはそれ以外のすべてに適用されます。

アラビア語は6つのカテゴリすべてを使用します。zeroカテゴリは0に、oneは1に、twoは2に、fewは3-10のような数に、manyは11-99のような数に、otherは100以上のような数に適用されます。

ロシア語は3つのカテゴリを使用します:1で終わる数(11を除く)にはone、2-4で終わる数(12-14を除く)にはfew、それ以外のすべてにはmanyです。

日本語と中国語は単数形と複数形を区別しないため、otherカテゴリのみを使用します。

これらのカテゴリは各言語の言語学的ルールを表しています。翻訳を提供する際、その言語が使用する各カテゴリに対して1つの文字列を作成します。

resolvedOptionsを使用した複数形カテゴリの取得

PluralRulesインスタンスのresolvedOptions()メソッドは、ロケールが使用する複数形カテゴリを含む、ルールに関する情報を含むオブジェクトを返します。

const enRules = new Intl.PluralRules('en-US');
const options = enRules.resolvedOptions();

console.log(options.pluralCategories);
// 出力: ["one", "other"]

pluralCategoriesプロパティは文字列の配列です。各文字列は6つの標準カテゴリ名のいずれかです。配列にはロケールが実際に使用するカテゴリのみが含まれます。

英語の場合、配列には「one」と「other」が含まれています。これは英語が単数形と複数形を区別するためです。

より複雑なルールを持つ言語の場合、配列にはより多くのカテゴリが含まれます:

const arRules = new Intl.PluralRules('ar-EG');
const options = arRules.resolvedOptions();

console.log(options.pluralCategories);
// 出力: ["zero", "one", "two", "few", "many", "other"]

アラビア語は6つのカテゴリすべてを使用するため、配列には6つの値すべてが含まれています。

異なるロケールの複数形カテゴリを確認する

異なる言語には異なる複数形ルールがあり、異なるカテゴリセットを使用します。いくつかの言語を比較して、その違いを確認してみましょう:

const locales = ['en-US', 'ar-EG', 'ru-RU', 'pl-PL', 'ja-JP', 'zh-CN'];

locales.forEach(locale => {
  const rules = new Intl.PluralRules(locale);
  const categories = rules.resolvedOptions().pluralCategories;
  console.log(`${locale}: [${categories.join(', ')}]`);
});

// 出力:
// en-US: [one, other]
// ar-EG: [zero, one, two, few, many, other]
// ru-RU: [one, few, many, other]
// pl-PL: [one, few, many, other]
// ja-JP: [other]
// zh-CN: [other]

英語には2つのカテゴリがあります。アラビア語には6つあります。ロシア語とポーランド語にはそれぞれ4つあります。日本語と中国語は複数形を区別しないため、1つしかありません。

この違いは、すべての言語が英語のように機能すると仮定できないことを示しています。各ロケールがどのカテゴリを使用するかを確認し、それぞれに適切な翻訳を提供する必要があります。

各ロケールにおけるカテゴリの意味を理解する

同じカテゴリ名でも言語によって意味が異なります。英語における「one」カテゴリは数字の1にのみ適用されます。ロシア語では、「one」は11を除く1で終わる数字(1、21、31、101など)に適用されます。

異なるロケールで数字がどのカテゴリにマッピングされるかをテストしてみましょう:

const enRules = new Intl.PluralRules('en-US');
const ruRules = new Intl.PluralRules('ru-RU');

const numbers = [0, 1, 2, 3, 5, 11, 21, 22, 100];

console.log('English:');
numbers.forEach(n => {
  console.log(`  ${n}: ${enRules.select(n)}`);
});

console.log('Russian:');
numbers.forEach(n => {
  console.log(`  ${n}: ${ruRules.select(n)}`);
});

// Output:
// English:
//   0: other
//   1: one
//   2: other
//   3: other
//   5: other
//   11: other
//   21: other
//   22: other
//   100: other
// Russian:
//   0: many
//   1: one
//   2: few
//   3: few
//   5: many
//   11: many
//   21: one
//   22: few
//   100: many

英語では、1だけが「one」カテゴリを使用します。ロシア語では、1と21は両方とも1で終わるため「one」を使用します。2、3、22は2-4で終わるため「few」を使用します。0、5、11、100は「many」を使用します。

これは、言語ルールを知らなければ、数字にどのカテゴリが適用されるかを予測できないことを示しています。pluralCategories配列はどのカテゴリが存在するかを教えてくれ、select()メソッドは各数字にどのカテゴリが適用されるかを教えてくれます。

序数のカテゴリを取得する

1st、2nd、3rdなどの序数は、基数とは異なる複数形ルールを持っています。序数のカテゴリを取得するには、type: 'ordinal'を指定してPluralRulesインスタンスを作成します:

const enCardinalRules = new Intl.PluralRules('en-US', { type: 'cardinal' });
const enOrdinalRules = new Intl.PluralRules('en-US', { type: 'ordinal' });

console.log('Cardinal:', enCardinalRules.resolvedOptions().pluralCategories);
// Output: Cardinal: ["one", "other"]

console.log('Ordinal:', enOrdinalRules.resolvedOptions().pluralCategories);
// Output: Ordinal: ["one", "two", "few", "other"]

英語の基数は2つのカテゴリを使用します。英語の序数は4つのカテゴリを使用します。これは序数が1st、2nd、3rd、およびその他すべてを区別する必要があるためです。

序数カテゴリは序数の接尾辞にマッピングされます:

const enOrdinalRules = new Intl.PluralRules('en-US', { type: 'ordinal' });

const numbers = [1, 2, 3, 4, 11, 21, 22, 23];

numbers.forEach(n => {
  const category = enOrdinalRules.select(n);
  console.log(`${n}: ${category}`);
});

// Output:
// 1: one
// 2: two
// 3: few
// 4: other
// 11: other
// 21: one
// 22: two
// 23: few

カテゴリ「one」はst接尾辞(1st、21st)、「two」はnd(2nd、22nd)、「few」はrd(3rd、23rd)、「other」はth(4th、11th)に対応します。

言語によって序数カテゴリは異なります:

const locales = ['en-US', 'es-ES', 'fr-FR'];

locales.forEach(locale => {
  const rules = new Intl.PluralRules(locale, { type: 'ordinal' });
  const categories = rules.resolvedOptions().pluralCategories;
  console.log(`${locale}: [${categories.join(', ')}]`);
});

// Output:
// en-US: [one, two, few, other]
// es-ES: [other]
// fr-FR: [one, other]

スペイン語はより単純なパターンに従うため、序数カテゴリを1つしか使用しません。フランス語は最初の位置とその他すべての位置を区別するために2つのカテゴリを使用します。

複数形カテゴリを使用して翻訳マップを構築する

ロケールがどのカテゴリを使用するかを把握したら、必要な数のエントリを持つ翻訳マップを構築できます:

function buildTranslationMap(locale, translations) {
  const rules = new Intl.PluralRules(locale);
  const categories = rules.resolvedOptions().pluralCategories;

  const map = new Map();

  categories.forEach(category => {
    if (translations[category]) {
      map.set(category, translations[category]);
    } else {
      console.warn(`Missing translation for category "${category}" in locale "${locale}"`);
    }
  });

  return map;
}

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

const arTranslations = {
  zero: 'لا توجد عناصر',
  one: 'عنصر واحد',
  two: 'عنصران',
  few: 'عناصر',
  many: 'عنصرًا',
  other: 'عنصر'
};

const enMap = buildTranslationMap('en-US', enTranslations);
const arMap = buildTranslationMap('ar-EG', arTranslations);

console.log(enMap);
// 出力: Map(2) { 'one' => 'item', 'other' => 'items' }

console.log(arMap);
// 出力: Map(6) { 'zero' => 'لا توجد عناصر', 'one' => 'عنصر واحد', ... }

この関数は、必要なすべてのカテゴリに対して翻訳が提供されているかを確認し、不足しているものがあれば警告します。これにより、カテゴリが使用されているのに翻訳がない場合の実行時エラーを防ぎます。

翻訳の完全性を検証する

複数形カテゴリを使用して、本番環境にデプロイする前に翻訳に必要なすべての形式が含まれていることを確認します:

function validateTranslations(locale, translations) {
  const rules = new Intl.PluralRules(locale);
  const requiredCategories = rules.resolvedOptions().pluralCategories;
  const providedCategories = Object.keys(translations);

  const missing = requiredCategories.filter(cat => !providedCategories.includes(cat));
  const extra = providedCategories.filter(cat => !requiredCategories.includes(cat));

  if (missing.length > 0) {
    console.error(`Locale ${locale} is missing categories: ${missing.join(', ')}`);
    return false;
  }

  if (extra.length > 0) {
    console.warn(`Locale ${locale} has unused categories: ${extra.join(', ')}`);
  }

  return true;
}

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

const incompleteArTranslations = {
  one: 'عنصر واحد',
  other: 'عنصر'
};

validateTranslations('en-US', enTranslations);
// 出力: true

validateTranslations('ar-EG', incompleteArTranslations);
// 出力: Locale ar-EG is missing categories: zero, two, few, many
// 出力: false

この検証により、ユーザーが未翻訳のテキストに遭遇する前に、開発段階で不足している翻訳を発見することができます。

動的翻訳インターフェースの構築

翻訳者向けのツールを構築する際は、複数形カテゴリを照会して、どの形式の翻訳が必要かを正確に表示します:

function generateTranslationForm(locale, key) {
  const rules = new Intl.PluralRules(locale);
  const categories = rules.resolvedOptions().pluralCategories;

  const form = document.createElement('div');
  form.className = 'translation-form';

  const heading = document.createElement('h3');
  heading.textContent = `Translate "${key}" for ${locale}`;
  form.appendChild(heading);

  categories.forEach(category => {
    const label = document.createElement('label');
    label.textContent = `${category}:`;

    const input = document.createElement('input');
    input.type = 'text';
    input.name = `${key}.${category}`;
    input.placeholder = `Enter ${category} form`;

    const wrapper = document.createElement('div');
    wrapper.appendChild(label);
    wrapper.appendChild(input);
    form.appendChild(wrapper);
  });

  return form;
}

const enForm = generateTranslationForm('en-US', 'items');
const arForm = generateTranslationForm('ar-EG', 'items');

document.body.appendChild(enForm);
document.body.appendChild(arForm);

これにより、各ロケールに対して正確な数の入力フィールドを持つフォームが生成されます。英語では2つのフィールド(oneとother)が生成され、アラビア語では6つのフィールド(zero、one、two、few、many、およびother)が生成されます。

ロケール間でのカテゴリの比較

複数のロケールの翻訳を管理する際は、それぞれが使用するカテゴリを比較して翻訳の複雑さを理解します:

function compareLocalePluralCategories(locales) {
  const comparison = {};

  locales.forEach(locale => {
    const rules = new Intl.PluralRules(locale);
    const categories = rules.resolvedOptions().pluralCategories;
    comparison[locale] = categories;
  });

  return comparison;
}

const locales = ['en-US', 'es-ES', 'ar-EG', 'ru-RU', 'ja-JP'];
const comparison = compareLocalePluralCategories(locales);

console.log(comparison);
// 出力:
// {
//   'en-US': ['one', 'other'],
//   'es-ES': ['one', 'other'],
//   'ar-EG': ['zero', 'one', 'two', 'few', 'many', 'other'],
//   'ru-RU': ['one', 'few', 'many', 'other'],
//   'ja-JP': ['other']
// }

これにより、英語とスペイン語は同じ複数形カテゴリを持っているため、それらの間で翻訳構造を再利用することが容易であることがわかります。アラビア語は6つのカテゴリを使用するため、より多くの翻訳作業が必要になります。

ロケールが特定のカテゴリを使用しているかの確認

コード内で特定の複数形カテゴリを使用する前に、そのロケールが実際にそれを使用しているかどうかを確認してください:

function localeUsesCategory(locale, category) {
  const rules = new Intl.PluralRules(locale);
  const categories = rules.resolvedOptions().pluralCategories;
  return categories.includes(category);
}

console.log(localeUsesCategory('en-US', 'zero'));
// 出力: false

console.log(localeUsesCategory('ar-EG', 'zero'));
// 出力: true

console.log(localeUsesCategory('ja-JP', 'one'));
// 出力: false

これにより、すべてのロケールにzeroカテゴリやoneカテゴリがあると想定することを防ぎます。このチェックを使用して、カテゴリ固有のロジックを安全に実装してください。

otherカテゴリの理解

すべての言語はotherカテゴリを使用します。このカテゴリは、他のカテゴリが適用されない場合のデフォルトケースとして機能します。

英語では、otherは1以外のすべての数をカバーします。アラビア語では、otherは100以上の大きな数をカバーします。日本語では、日本語は複数形を区別しないため、otherはすべての数をカバーします。

常にotherカテゴリの翻訳を提供してください。このカテゴリはすべてのロケールに確実に存在し、より具体的なカテゴリが一致しない場合に使用されます。

const locales = ['en-US', 'ar-EG', 'ru-RU', 'ja-JP'];

locales.forEach(locale => {
  const rules = new Intl.PluralRules(locale);
  const categories = rules.resolvedOptions().pluralCategories;
  const hasOther = categories.includes('other');
  console.log(`${locale} uses "other": ${hasOther}`);
});

// 出力:
// en-US uses "other": true
// ar-EG uses "other": true
// ru-RU uses "other": true
// ja-JP uses "other": true

すべての解決されたオプションを一緒に取得する

resolvedOptions()メソッドは複数形カテゴリだけでなく、ロケール、タイプ、数値フォーマットオプションに関する情報も返します:

const rules = new Intl.PluralRules('de-DE', {
  type: 'cardinal',
  minimumFractionDigits: 2,
  maximumFractionDigits: 2
});

const options = rules.resolvedOptions();

console.log(options);
// 出力:
// {
//   locale: 'de-DE',
//   type: 'cardinal',
//   pluralCategories: ['one', 'other'],
//   minimumIntegerDigits: 1,
//   minimumFractionDigits: 2,
//   maximumFractionDigits: 2,
//   minimumSignificantDigits: undefined,
//   maximumSignificantDigits: undefined
// }

pluralCategoriesプロパティは解決されたオプションオブジェクト内の情報の一部です。他のプロパティは、デフォルト値に設定されたオプションを含め、PluralRulesインスタンスが使用する正確な構成を示します。

パフォーマンスのための複数形カテゴリのキャッシング

PluralRulesインスタンスの作成とresolvedOptions()の呼び出しにはコストがかかります。各ロケールの結果を繰り返し照会するのではなく、キャッシュしましょう:

const categoriesCache = new Map();

function getPluralCategories(locale, type = 'cardinal') {
  const key = `${locale}:${type}`;

  if (categoriesCache.has(key)) {
    return categoriesCache.get(key);
  }

  const rules = new Intl.PluralRules(locale, { type });
  const categories = rules.resolvedOptions().pluralCategories;

  categoriesCache.set(key, categories);

  return categories;
}

const enCardinal = getPluralCategories('en-US', 'cardinal');
const enOrdinal = getPluralCategories('en-US', 'ordinal');
const arCardinal = getPluralCategories('ar-EG', 'cardinal');

console.log('en-US cardinal:', enCardinal);
console.log('en-US ordinal:', enOrdinal);
console.log('ar-EG cardinal:', arCardinal);

// 後続の呼び出しはキャッシュされた結果を使用
const enCardinal2 = getPluralCategories('en-US', 'cardinal');
// 新しいPluralRulesインスタンスは作成されない

このパターンは、多くの複数形文字列をフォーマットするアプリケーションや多くのロケールをサポートするアプリケーションで特に重要です。

ブラウザサポートと互換性

resolvedOptions()pluralCategoriesプロパティは2020年にJavaScriptに追加されました。Chrome 106+、Firefox 116+、Safari 15.4+、Edge 106+でサポートされています。

Intl.PluralRulesはサポートしているがpluralCategoriesをサポートしていない古いブラウザでは、このプロパティに対してundefinedが返されます。使用する前に存在を確認してください:

function getPluralCategories(locale) {
  const rules = new Intl.PluralRules(locale);
  const options = rules.resolvedOptions();

  if (options.pluralCategories) {
    return options.pluralCategories;
  }

  // 古いブラウザ用のフォールバック
  return ['one', 'other'];
}

このフォールバックは単純な2カテゴリシステムを想定しており、英語や多くのヨーロッパ言語では機能しますが、より複雑なルールを持つ言語では正確でない場合があります。より良い互換性のために、言語固有のフォールバックを提供するか、ポリフィルを使用してください。