¿Cómo elegir la forma plural correcta para diferentes idiomas?

Utiliza Intl.PluralRules de JavaScript para seleccionar entre un elemento, dos elementos, pocos elementos, muchos elementos según las reglas específicas del idioma

Introducción

Cuando muestras texto con cantidades, necesitas diferentes mensajes para diferentes conteos. En inglés, escribes "1 file" pero "2 files". El enfoque más simple concatena un número con una palabra y añade una "s" cuando es necesario.

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

Este enfoque falla de tres maneras. Primero, produce inglés incorrecto para cero ("0 files" debería ser "no files"). Segundo, no funciona con plurales complejos como "1 child, 2 children" o "1 person, 2 people". Tercero, y más crítico, otros idiomas tienen reglas de pluralización completamente diferentes que este código no puede manejar.

JavaScript proporciona Intl.PluralRules para resolver este problema. Esta API determina qué forma plural usar para cualquier número en cualquier idioma, siguiendo el estándar CLDR de Unicode utilizado por sistemas de traducción profesionales en todo el mundo.

Por qué diferentes idiomas necesitan diferentes formas plurales

El inglés utiliza dos formas plurales. Escribes "1 book" y "2 books". La palabra cambia cuando el conteo es exactamente uno versus cualquier otro número.

Otros idiomas funcionan de manera diferente. El polaco utiliza tres formas basadas en reglas complejas. El ruso utiliza cuatro formas. El árabe utiliza seis formas. Algunos idiomas utilizan solo una forma para todas las cantidades.

Aquí hay ejemplos que muestran cómo la palabra para "manzana" cambia según la cantidad en diferentes idiomas:

Inglés: 1 apple, 2 apples, 5 apples, 0 apples

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

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

Árabe: Utiliza seis formas diferentes dependiendo de si tienes cero, uno, dos, unos pocos, muchos u otras cantidades

El CLDR de Unicode define las reglas exactas para cuándo usar cada forma en cada idioma. No puedes memorizar estas reglas o codificarlas directamente en tu aplicación. Necesitas una API que las conozca.

¿Qué son las categorías plurales CLDR

El estándar Unicode CLDR define seis categorías plurales que abarcan todos los idiomas:

  • zero: Utilizado en algunos idiomas para exactamente cero elementos
  • one: Utilizado para formas singulares
  • two: Utilizado en idiomas con forma dual
  • few: Utilizado para pequeñas cantidades en algunos idiomas
  • many: Utilizado para cantidades mayores o fracciones en algunos idiomas
  • other: La forma predeterminada, utilizada cuando no aplica ninguna otra categoría

Todos los idiomas utilizan la categoría other. La mayoría de los idiomas utilizan solo dos o tres categorías en total. Las categorías no corresponden directamente a cantidades. Por ejemplo, en polaco, el número 5 utiliza la categoría many, pero también lo hacen 0, 25 y 1.5.

Las reglas específicas para qué números corresponden a qué categorías difieren según el idioma. JavaScript maneja esta complejidad a través de la API Intl.PluralRules.

Cómo determinar qué forma plural utilizar

El objeto Intl.PluralRules determina a qué categoría plural pertenece un número en un idioma específico. Se crea un objeto PluralRules con una configuración regional, luego se llama a su método select() con un número.

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 inglés, select() devuelve "one" para el número 1 y "other" para todo lo demás.

El polaco utiliza tres categorías con reglas más complejas:

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"

El árabe utiliza seis categorías:

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"

El método select() devuelve una cadena que identifica la categoría. Se utiliza esta cadena para elegir el mensaje apropiado para mostrar.

Cómo mapear categorías de plurales a mensajes

Después de determinar la categoría de plural, necesitas seleccionar el mensaje correcto para mostrar al usuario. Crea un objeto que mapee cada categoría a su mensaje, luego usa la cadena de categoría para buscar el mensaje.

const messages = {
  one: '{count} archivo',
  other: '{count} archivos'
};

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 archivo"
console.log(formatFileCount(5, 'en-US'));  // "5 archivos"

Este patrón funciona para cualquier idioma. Para el polaco, proporcionas mensajes para las tres categorías que utiliza el idioma:

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 estructura del código permanece idéntica en todos los idiomas. Solo cambia el objeto messages. Esta separación permite a los traductores proporcionar los mensajes correctos para su idioma sin modificar el código.

Cómo manejar categorías de plurales faltantes

Tu objeto messages podría no incluir las seis categorías posibles. La mayoría de los idiomas solo usan dos o tres. Cuando select() devuelve una categoría que no está en tu objeto messages, recurre a la categoría 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} archivo',
  other: '{count} archivos'
};

console.log(formatFileCount(1, 'en-US', englishMessages));  // "1 archivo"
console.log(formatFileCount(5, 'en-US', englishMessages));  // "5 archivos"

Este patrón asegura que tu código funcione incluso cuando el objeto messages está incompleto. La categoría other siempre existe en todos los idiomas, lo que la convierte en una alternativa segura.

Cómo utilizar reglas de pluralización con números ordinales

El constructor Intl.PluralRules acepta una opción type que cambia cómo se determinan las categorías. El tipo predeterminado es "cardinal", utilizado para contar elementos. Establece type: "ordinal" para determinar formas plurales para números ordinales como "1º", "2º", "3º".

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"

Las reglas cardinales determinan "1 elemento, 2 elementos". Las reglas ordinales determinan "1er lugar, 2º lugar, 3er lugar, 4º lugar". Las categorías devueltas difieren porque los patrones gramaticales son diferentes.

Cómo formatear cantidades fraccionarias

El método select() funciona con números decimales. Diferentes idiomas tienen reglas específicas sobre cómo las fracciones se asignan a categorías plurales.

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 inglés, 1.0 utiliza la forma singular, pero 1.5 utiliza el plural. Algunos idiomas tienen diferentes reglas para fracciones, tratándolas como una categoría separada.

const messages = {
  one: '{count} archivo',
  other: '{count} archivos'
};

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

Pasa el número decimal directamente a select(). El método aplica automáticamente las reglas correctas del idioma.

Cómo crear un formateador de plurales reutilizable

En lugar de repetir el mismo patrón en toda tu aplicación, crea una función reutilizable que encapsule la lógica de selección plural.

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} archivo',
  other: '{count} archivos'
};

const itemMessages = {
  one: '{count} elemento',
  other: '{count} elementos'
};

console.log(formatter.format(1, fileMessages));  // "1 archivo"
console.log(formatter.format(5, fileMessages));  // "5 archivos"
console.log(formatter.format(1, itemMessages));  // "1 elemento"
console.log(formatter.format(3, itemMessages));  // "3 elementos"

Esta clase crea el objeto PluralRules una vez y lo reutiliza para múltiples operaciones de formato. Puedes extenderla para soportar características más avanzadas como formatear el contador con Intl.NumberFormat antes de insertarlo en el mensaje.

Cómo integrar reglas de plurales con sistemas de traducción

Los sistemas de traducción profesionales almacenan mensajes con marcadores de posición para categorías plurales. Cuando traduces texto, proporcionas todas las formas plurales que tu idioma necesita.

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"

Este patrón separa los datos de traducción de la lógica del código. Los traductores proporcionan los mensajes para cada categoría plural que utiliza su idioma. Tu código aplica las reglas automáticamente.

Cómo verificar qué categorías plurales utiliza un locale

El método resolvedOptions() devuelve información sobre el objeto PluralRules, pero no enumera qué categorías utiliza el locale. Para encontrar todas las categorías que utiliza un locale, prueba un rango de números y recopila las categorías únicas devueltas.

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

Esta técnica prueba enteros y valores medios en un rango. Captura las categorías que tu objeto de mensajes necesita incluir para un locale determinado.