Comment choisir la bonne forme plurielle pour différentes langues ?

Utilisez Intl.PluralRules de JavaScript pour sélectionner entre un élément, deux éléments, quelques éléments, plusieurs éléments selon les règles spécifiques à chaque langue

Introduction

Lorsque vous affichez du texte avec des quantités, vous avez besoin de messages différents pour différents nombres. En anglais, on écrit "1 file" mais "2 files". L'approche la plus simple consiste à concaténer un nombre avec un mot et à ajouter un "s" si nécessaire.

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

Cette approche échoue de trois manières. Premièrement, elle produit un anglais incorrect pour zéro ("0 files" devrait sans doute être "no files"). Deuxièmement, elle ne fonctionne pas avec les pluriels complexes comme "1 child, 2 children" ou "1 person, 2 people". Troisièmement, et de manière plus critique, d'autres langues ont des règles de pluriel entièrement différentes que ce code ne peut pas gérer.

JavaScript fournit Intl.PluralRules pour résoudre ce problème. Cette API détermine quelle forme plurielle utiliser pour n'importe quel nombre dans n'importe quelle langue, en suivant le standard Unicode CLDR utilisé par les systèmes de traduction professionnels dans le monde entier.

Pourquoi différentes langues nécessitent différentes formes plurielles

L'anglais utilise deux formes plurielles. On écrit "1 book" et "2 books". Le mot change lorsque le nombre est exactement un par rapport à tout autre nombre.

D'autres langues fonctionnent différemment. Le polonais utilise trois formes basées sur des règles complexes. Le russe utilise quatre formes. L'arabe utilise six formes. Certaines langues n'utilisent qu'une seule forme pour toutes les quantités.

Voici des exemples montrant comment le mot pour "pomme" change en fonction de la quantité dans différentes langues :

Anglais : 1 apple, 2 apples, 5 apples, 0 apples

Polonais : 1 jabłko, 2 jabłka, 5 jabłek, 0 jabłek

Russe : 1 яблоко, 2 яблока, 5 яблок, 0 яблок

Arabe : utilise six formes différentes selon que vous avez zéro, un, deux, quelques, beaucoup ou d'autres quantités

Le CLDR Unicode définit les règles exactes pour déterminer quand utiliser chaque forme dans chaque langue. Vous ne pouvez pas mémoriser ces règles ni les coder en dur dans votre application. Vous avez besoin d'une API qui les connaît.

Que sont les catégories de pluriel CLDR

La norme CLDR Unicode définit six catégories de pluriel qui couvrent toutes les langues :

  • zero : utilisé dans certaines langues pour exactement zéro élément
  • one : utilisé pour les formes singulières
  • two : utilisé dans les langues avec une forme duelle
  • few : utilisé pour les petites quantités dans certaines langues
  • many : utilisé pour les quantités plus importantes ou les fractions dans certaines langues
  • other : la forme par défaut, utilisée lorsqu'aucune autre catégorie ne s'applique

Chaque langue utilise la catégorie other. La plupart des langues utilisent seulement deux ou trois catégories au total. Les catégories ne correspondent pas directement aux quantités. Par exemple, en polonais, le nombre 5 utilise la catégorie many, tout comme 0, 25 et 1,5.

Les règles spécifiques qui déterminent quels nombres correspondent à quelles catégories diffèrent selon la langue. JavaScript gère cette complexité via l'API Intl.PluralRules.

Comment déterminer quelle forme de pluriel utiliser

L'objet Intl.PluralRules détermine à quelle catégorie de pluriel appartient un nombre dans une langue spécifique. Vous créez un objet PluralRules avec une locale, puis appelez sa méthode select() avec un nombre.

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"

En anglais, select() renvoie "one" pour le nombre 1 et "other" pour tout le reste.

Le polonais utilise trois catégories avec des règles plus complexes :

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"

L'arabe utilise six catégories :

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"

La méthode select() renvoie une chaîne identifiant la catégorie. Vous utilisez cette chaîne pour choisir le message approprié à afficher.

Comment mapper les catégories de pluriel aux messages

Après avoir déterminé la catégorie de pluriel, vous devez sélectionner le message correct à afficher à l'utilisateur. Créez un objet qui mappe chaque catégorie à son message, puis utilisez la chaîne de catégorie pour rechercher le message.

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"

Ce modèle fonctionne pour n'importe quelle langue. Pour le polonais, vous fournissez des messages pour les trois catégories utilisées par la langue :

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"

La structure du code reste identique d'une langue à l'autre. Seul l'objet messages change. Cette séparation permet aux traducteurs de fournir les messages corrects pour leur langue sans modifier le code.

Comment gérer les catégories de pluriel manquantes

Votre objet messages peut ne pas inclure les six catégories possibles. La plupart des langues n'en utilisent que deux ou trois. Lorsque select() renvoie une catégorie absente de votre objet messages, utilisez la catégorie other par défaut.

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"

Ce modèle garantit que votre code fonctionne même lorsque l'objet messages est incomplet. La catégorie other existe toujours dans chaque langue, ce qui en fait un repli sûr.

Comment utiliser les règles de pluriel avec les nombres ordinaux

Le constructeur Intl.PluralRules accepte une option type qui modifie la façon dont les catégories sont déterminées. Le type par défaut est "cardinal", utilisé pour compter les éléments. Définissez type: "ordinal" pour déterminer les formes plurielles des nombres ordinaux comme « 1er », « 2e », « 3e ».

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"

Les règles cardinales déterminent "1 élément, 2 éléments". Les règles ordinales déterminent "1ère place, 2e place, 3e place, 4e place". Les catégories retournées diffèrent car les modèles grammaticaux diffèrent.

Comment formater les quantités fractionnaires

La méthode select() fonctionne avec les nombres décimaux. Différentes langues ont des règles spécifiques pour la façon dont les fractions correspondent aux catégories de pluriel.

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"

En anglais, 1.0 utilise la forme singulière, mais 1.5 utilise le pluriel. Certaines langues ont des règles différentes pour les fractions, les traitant comme une catégorie distincte.

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"

Passez le nombre décimal directement à select(). La méthode applique automatiquement les règles linguistiques correctes.

Comment créer un formateur de pluriel réutilisable

Au lieu de répéter le même modèle dans toute votre application, créez une fonction réutilisable qui encapsule la logique de sélection du pluriel.

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"

Cette classe crée l'objet PluralRules une fois et le réutilise pour plusieurs opérations de formatage. Vous pouvez l'étendre pour prendre en charge des fonctionnalités plus avancées comme le formatage du nombre avec Intl.NumberFormat avant de l'insérer dans le message.

Comment intégrer les règles de pluriel avec les systèmes de traduction

Les systèmes de traduction professionnels stockent les messages avec des espaces réservés pour les catégories de pluriel. Lorsque vous traduisez du texte, vous fournissez toutes les formes plurielles dont votre langue a besoin.

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"

Ce modèle sépare les données de traduction de la logique du code. Les traducteurs fournissent les messages pour chaque catégorie de pluriel utilisée par leur langue. Votre code applique les règles automatiquement.

Comment vérifier quelles catégories de pluriel une locale utilise

La méthode resolvedOptions() retourne des informations sur l'objet PluralRules, mais elle ne liste pas les catégories utilisées par la locale. Pour trouver toutes les catégories utilisées par une locale, testez une plage de nombres et collectez les catégories uniques retournées.

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

Cette technique teste les entiers et les demi-valeurs sur une plage. Elle capture les catégories que votre objet de messages doit inclure pour une locale donnée.