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

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

はじめに

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

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

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

複数形カテゴリとは

複数形カテゴリは、言語間で使用されるさまざまな複数形の標準化された名称です。Unicode CLDR(Common Locale Data Repository)は、zero、one、two、few、many、otherの6つのカテゴリを定義しています。

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

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

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

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

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

resolvedOptionsで複数形カテゴリを取得する

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

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

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

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

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

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

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

console.log(options.pluralCategories);
// Output: ["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(', ')}]`);
});

// Output:
// 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つのカテゴリを使用します。英語の序数は、1st、2nd、3rd、およびその他を区別する必要があるため、4つのカテゴリを使用します。

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

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);
// Output: Map(2) { 'one' => 'item', 'other' => 'items' }

console.log(arMap);
// Output: 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);
// Output: true

validateTranslations('ar-EG', incompleteArTranslations);
// Output: Locale ar-EG is missing categories: zero, two, few, many
// Output: 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);
// Output:
// {
//   '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'));
// Output: false

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

console.log(localeUsesCategory('ja-JP', 'one'));
// Output: 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}`);
});

// Output:
// 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);
// Output:
// {
//   locale: 'de-DE',
//   type: 'cardinal',
//   pluralCategories: ['one', 'other'],
//   minimumIntegerDigits: 1,
//   minimumFractionDigits: 2,
//   maximumFractionDigits: 2,
//   minimumSignificantDigits: undefined,
//   maximumSignificantDigits: undefined
// }

pluralCategoriesプロパティは、解決済みオプションオブジェクト内の1つの情報です。他のプロパティは、デフォルト値に設定されたオプションを含め、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);

// Subsequent calls use cached results
const enCardinal2 = getPluralCategories('en-US', 'cardinal');
// No new PluralRules instance created

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

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

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;
  }

  // Fallback for older browsers
  return ['one', 'other'];
}

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