Obtenir toutes les formes plurielles disponibles dans une locale

Découvrez quelles catégories de pluriel vous devez fournir pour les traductions

Introduction

Lors de la création d'applications multilingues, vous devez fournir différentes formes de texte pour différentes quantités. En anglais, vous écrivez "1 item" et "2 items". Cela semble simple jusqu'à ce que vous commenciez à prendre en charge d'autres langues.

Le russe utilise trois formes différentes selon le nombre. L'arabe en utilise six. Certaines langues utilisent la même forme pour tous les nombres. Avant de pouvoir fournir des traductions pour ces formes, vous devez savoir quelles formes existent dans chaque langue.

JavaScript fournit un moyen de découvrir quelles catégories de pluriel une locale utilise. La méthode resolvedOptions() sur une instance PluralRules renvoie une propriété pluralCategories qui liste toutes les formes plurielles dont la locale a besoin. Cela vous indique exactement quelles traductions fournir sans deviner ni maintenir des tables de règles spécifiques à chaque langue.

Que sont les catégories de pluriel

Les catégories de pluriel sont des noms standardisés pour différentes formes plurielles utilisées dans toutes les langues. Le CLDR Unicode (Common Locale Data Repository) définit six catégories : zero, one, two, few, many et other.

Toutes les langues n'utilisent pas les six catégories. L'anglais n'en utilise que deux : one et other. La catégorie one s'applique au nombre 1, et other s'applique à tout le reste.

L'arabe utilise les six catégories. La catégorie zero s'applique à 0, one à 1, two à 2, few aux nombres comme 3-10, many aux nombres comme 11-99, et other aux nombres comme 100 et au-delà.

Le russe utilise trois catégories : one pour les nombres se terminant par 1 (sauf 11), few pour les nombres se terminant par 2-4 (sauf 12-14), et many pour tout le reste.

Le japonais et le chinois utilisent uniquement la catégorie other car ces langues ne distinguent pas les formes singulières et plurielles.

Ces catégories représentent les règles linguistiques de chaque langue. Lorsque vous fournissez des traductions, vous créez une chaîne pour chaque catégorie utilisée par la langue.

Obtenir les catégories de pluriel avec resolvedOptions

La méthode resolvedOptions() sur une instance PluralRules renvoie un objet contenant des informations sur les règles, y compris les catégories de pluriel utilisées par la locale.

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

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

La propriété pluralCategories est un tableau de chaînes. Chaque chaîne est l'un des six noms de catégories standard. Le tableau contient uniquement les catégories réellement utilisées par la locale.

Pour l'anglais, le tableau contient one et other car l'anglais distingue les formes singulières et plurielles.

Pour une langue avec des règles plus complexes, le tableau contient davantage de catégories :

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

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

L'arabe utilise les six catégories, donc le tableau contient les six valeurs.

Visualiser les catégories de pluriel pour différentes locales

Différentes langues ont des règles de pluriel différentes, ce qui signifie qu'elles utilisent des ensembles de catégories différents. Comparez plusieurs langues pour voir la variation :

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]

L'anglais a deux catégories. L'arabe en a six. Le russe et le polonais en ont quatre chacun. Le japonais et le chinois n'en ont qu'une seule car ils ne distinguent pas du tout les formes plurielles.

Cette variation montre pourquoi vous ne pouvez pas supposer que chaque langue fonctionne comme l'anglais. Vous devez vérifier quelles catégories chaque locale utilise et fournir des traductions appropriées pour chacune.

Comprendre ce que signifient les catégories pour chaque locale

Le même nom de catégorie signifie des choses différentes dans différentes langues. La catégorie one en anglais s'applique uniquement au nombre 1. En russe, one s'applique aux nombres se terminant par 1 sauf 11, donc elle inclut 1, 21, 31, 101, et ainsi de suite.

Testez quels nombres correspondent à quelles catégories dans différentes locales :

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

En anglais, seul 1 utilise la catégorie one. En russe, 1 et 21 utilisent tous deux one car ils se terminent par 1. Les nombres 2, 3 et 22 utilisent few car ils se terminent par 2-4. Les nombres 0, 5, 11 et 100 utilisent many.

Cela démontre que vous ne pouvez pas prédire quelle catégorie s'applique à un nombre sans connaître les règles de la langue. Le tableau pluralCategories vous indique quelles catégories existent, et la méthode select() vous indique quelle catégorie s'applique à chaque nombre.

Obtenir les catégories pour les nombres ordinaux

Les nombres ordinaux comme 1er, 2e, 3e ont leurs propres règles de pluriel qui diffèrent des nombres cardinaux. Créez une instance PluralRules avec type: 'ordinal' pour obtenir les catégories des nombres ordinaux :

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

Les nombres cardinaux anglais utilisent deux catégories. Les nombres ordinaux anglais utilisent quatre catégories car les ordinaux doivent distinguer entre 1er, 2e, 3e et tous les autres.

Les catégories ordinales correspondent aux suffixes ordinaux :

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

La catégorie one correspond au suffixe st (1st, 21st), two à nd (2nd, 22nd), few à rd (3rd, 23rd), et other à th (4th, 11th).

Différentes langues ont différentes catégories ordinales :

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]

L'espagnol utilise une seule catégorie ordinale car les ordinaux espagnols suivent un schéma plus simple. Le français utilise deux catégories pour distinguer premier de toutes les autres positions.

Utiliser les catégories de pluriel pour construire des cartes de traduction

Lorsque vous savez quelles catégories une locale utilise, vous pouvez construire une carte de traduction avec exactement le bon nombre d'entrées :

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' => 'عنصر واحد', ... }

Cette fonction vérifie que vous avez fourni des traductions pour toutes les catégories requises et vous avertit si certaines sont manquantes. Cela évite les erreurs d'exécution lorsqu'une catégorie est utilisée mais n'a pas de traduction.

Validation de l'exhaustivité des traductions

Utilisez les catégories de pluriel pour vérifier que vos traductions incluent toutes les formes nécessaires avant le déploiement en production :

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

Cette validation détecte les traductions manquantes pendant le développement au lieu de les découvrir lorsque les utilisateurs rencontrent du texte non traduit.

Construction d'interfaces de traduction dynamiques

Lors de la création d'outils pour les traducteurs, interrogez les catégories de pluriel pour afficher exactement quelles formes nécessitent une traduction :

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

Cela génère un formulaire avec le nombre correct de champs de saisie pour chaque locale. L'anglais obtient deux champs (one et other), tandis que l'arabe obtient six champs (zero, one, two, few, many et other).

Comparaison des catégories entre locales

Lors de la gestion des traductions pour plusieurs locales, comparez les catégories qu'elles utilisent pour comprendre la complexité de la traduction :

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']
// }

Cela montre que l'anglais et l'espagnol ont les mêmes catégories de pluriel, ce qui facilite la réutilisation des structures de traduction entre eux. L'arabe nécessite beaucoup plus de travail de traduction car il utilise six catégories.

Vérification si une locale utilise une catégorie spécifique

Avant d'utiliser une catégorie de pluriel spécifique dans votre code, vérifiez si la locale l'utilise réellement :

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

Cela vous évite de supposer que chaque locale possède une catégorie zero ou one. Utilisez cette vérification pour implémenter une logique spécifique aux catégories en toute sécurité.

Comprendre la catégorie other

Chaque langue utilise la catégorie other. Cette catégorie sert de cas par défaut lorsqu'aucune autre catégorie ne s'applique.

En anglais, other couvre tous les nombres sauf 1. En arabe, other couvre les grands nombres comme 100 et au-delà. En japonais, other couvre tous les nombres car le japonais ne distingue pas les formes plurielles.

Fournissez toujours une traduction pour la catégorie other. Cette catégorie est garantie d'exister dans toutes les locales et sera utilisée lorsqu'aucune catégorie plus spécifique ne correspond.

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

Obtenir toutes les options résolues ensemble

La méthode resolvedOptions() renvoie plus que de simples catégories de pluriel. Elle inclut des informations sur la locale, le type et les options de formatage des nombres :

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

La propriété pluralCategories est une information parmi d'autres dans l'objet des options résolues. Les autres propriétés vous indiquent la configuration exacte utilisée par l'instance PluralRules, y compris toutes les options définies sur des valeurs par défaut.

Mise en cache des catégories de pluriel pour les performances

Créer des instances PluralRules et appeler resolvedOptions() a un coût. Mettez en cache les résultats pour chaque locale au lieu de les interroger de manière répétée :

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

Ce modèle est particulièrement important dans les applications qui formatent de nombreuses chaînes pluralisées ou prennent en charge de nombreuses locales.

Compatibilité et prise en charge des navigateurs

La propriété pluralCategories sur resolvedOptions() a été ajoutée à JavaScript en 2020. Elle est prise en charge dans Chrome 106+, Firefox 116+, Safari 15.4+ et Edge 106+.

Les navigateurs plus anciens qui prennent en charge Intl.PluralRules mais pas pluralCategories renverront undefined pour cette propriété. Vérifiez son existence avant de l'utiliser :

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

Ce repli suppose un système simple à deux catégories, qui fonctionne pour l'anglais et de nombreuses langues européennes mais peut ne pas être correct pour les langues avec des règles plus complexes. Pour une meilleure compatibilité, fournissez des replis spécifiques à chaque langue ou utilisez un polyfill.