API Intl.PluralRules

Cómo manejar correctamente las formas plurales en JavaScript

Introducción

La pluralización es el proceso de mostrar diferentes textos basados en una cantidad. En inglés, podrías mostrar "1 item" para un solo elemento y "2 items" para múltiples elementos. La mayoría de los desarrolladores manejan esto con una simple condicional que añade una "s" para cantidades distintas de uno.

Este enfoque falla para idiomas distintos del inglés. El polaco utiliza diferentes formas para 1, 2-4, y 5 o más. El árabe tiene formas para cero, uno, dos, pocos y muchos. El galés tiene seis formas distintas. Incluso en inglés, los plurales irregulares como "person" a "people" requieren un tratamiento especial.

La API Intl.PluralRules resuelve esto proporcionando la categoría de forma plural para cualquier número en cualquier idioma. Proporcionas una cantidad, y la API te indica qué forma usar según las reglas del idioma objetivo. Esto te permite escribir código preparado para la internacionalización que funciona correctamente en todos los idiomas sin tener que codificar manualmente reglas específicas de cada idioma.

Cómo manejan los idiomas las formas plurales

Los idiomas difieren ampliamente en cómo expresan la cantidad. El inglés tiene dos formas: singular para uno, plural para todo lo demás. Esto parece sencillo hasta que encuentras idiomas con sistemas diferentes.

El ruso y el polaco utilizan tres formas. El singular se aplica a un elemento. Una forma especial se aplica a cantidades que terminan en 2, 3 o 4 (pero no 12, 13 o 14). Todas las demás cantidades utilizan una tercera forma.

El árabe utiliza seis formas: cero, uno, dos, pocos (3-10), muchos (11-99) y otros (100+). El galés también tiene seis formas con diferentes límites numéricos.

Algunos idiomas como el chino y el japonés no distinguen entre singular y plural en absoluto. La misma forma funciona para cualquier cantidad.

La API Intl.PluralRules abstrae estas diferencias utilizando nombres de categorías estandarizados basados en las reglas plurales de Unicode CLDR. Las seis categorías son: zero, one, two, few, many y other. No todos los idiomas utilizan las seis categorías. El inglés solo usa one y other. El árabe utiliza las seis.

Crear una instancia de PluralRules para un locale

El constructor Intl.PluralRules toma un identificador de locale y devuelve un objeto que puede determinar qué categoría plural se aplica a un número dado.

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

Crea una instancia por locale y reutilízala. Construir una nueva instancia para cada pluralización es ineficiente. Almacena la instancia en una variable o utiliza un mecanismo de caché.

El tipo predeterminado es cardinal, que maneja el conteo de objetos. También puedes crear reglas para números ordinales pasando un objeto de opciones.

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

Las reglas cardinales se aplican a conteos como "1 manzana, 2 manzanas". Las reglas ordinales se aplican a posiciones como "1er lugar, 2do lugar".

Usar select() para obtener la categoría plural de un número

El método select() toma un número y devuelve a qué categoría plural pertenece en el idioma objetivo.

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

enRules.select(0);  // 'other'
enRules.select(1);  // 'one'
enRules.select(2);  // 'other'
enRules.select(5);  // 'other'

El valor devuelto es siempre uno de los seis nombres de categoría: zero, one, two, few, many, u other. El inglés solo devuelve one y other porque esas son las únicas formas que utiliza el inglés.

Para el árabe, que tiene reglas más complejas, se ven las seis categorías en uso:

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

arRules.select(0);   // 'zero'
arRules.select(1);   // 'one'
arRules.select(2);   // 'two'
arRules.select(6);   // 'few'
arRules.select(18);  // 'many'
arRules.select(100); // 'other'

Mapear categorías a cadenas localizadas

La API solo te indica qué categoría se aplica. Tú proporcionas el texto real para cada categoría. Almacena las formas de texto en un Map u objeto, indexado por nombre de categoría.

const enRules = new Intl.PluralRules('en-US');
const enForms = new Map([
  ['one', 'item'],
  ['other', 'items'],
]);

function formatItems(count) {
  const category = enRules.select(count);
  const form = enForms.get(category);
  return `${count} ${form}`;
}

formatItems(1);  // '1 item'
formatItems(5);  // '5 items'

Este patrón separa la lógica de los datos. La instancia de PluralRules maneja las reglas. El Map contiene las traducciones. La función las combina.

Para idiomas con más categorías, añade más entradas al Map:

const arRules = new Intl.PluralRules('ar-EG');
const arForms = new Map([
  ['zero', 'عناصر'],
  ['one', 'عنصر واحد'],
  ['two', 'عنصران'],
  ['few', 'عناصر'],
  ['many', 'عنصرًا'],
  ['other', 'عنصر'],
]);

function formatItems(count) {
  const category = arRules.select(count);
  const form = arForms.get(category);
  return `${count} ${form}`;
}

Siempre proporciona entradas para cada categoría que utiliza el idioma. Las categorías faltantes causan búsquedas indefinidas. Si no estás seguro de qué categorías utiliza un idioma, consulta las reglas plurales de Unicode CLDR o prueba con la API con diferentes números.

Manejar conteos decimales y fraccionarios

El método select() funciona con números decimales. El inglés trata los decimales como plurales, incluso para valores entre 0 y 2.

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

enRules.select(1);    // 'one'
enRules.select(1.0);  // 'one'
enRules.select(1.5);  // 'other'
enRules.select(0.5);  // 'other'

Otros idiomas tienen reglas diferentes para los decimales. Algunos tratan cualquier decimal como plural, mientras que otros utilizan reglas más matizadas basadas en la parte fraccionaria.

Si tu interfaz de usuario muestra cantidades fraccionarias como "1.5 GB" o "2.7 millas", pasa el número fraccionario directamente a select(). No redondees primero a menos que tu interfaz de usuario redondee el valor mostrado.

Formatear números ordinales como 1º, 2º, 3º

Los números ordinales indican posición o rango. El inglés forma ordinales añadiendo sufijos: 1st, 2nd, 3rd, 4th. El patrón no es simplemente "añadir th" porque 1, 2 y 3 tienen formas especiales, y los números que terminan en 1, 2 o 3 siguen reglas especiales (21st, 22nd, 23rd) excepto cuando terminan en 11, 12 o 13 (11th, 12th, 13th).

La API Intl.PluralRules maneja estas reglas cuando especificas type: 'ordinal'.

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

enOrdinalRules.select(1);   // 'one'
enOrdinalRules.select(2);   // 'two'
enOrdinalRules.select(3);   // 'few'
enOrdinalRules.select(4);   // 'other'
enOrdinalRules.select(11);  // 'other'
enOrdinalRules.select(21);  // 'one'
enOrdinalRules.select(22);  // 'two'
enOrdinalRules.select(23);  // 'few'

Mapea las categorías a sufijos ordinales:

const enOrdinalRules = new Intl.PluralRules('en-US', { type: 'ordinal' });
const enOrdinalSuffixes = new Map([
  ['one', 'st'],
  ['two', 'nd'],
  ['few', 'rd'],
  ['other', 'th'],
]);

function formatOrdinal(n) {
  const category = enOrdinalRules.select(n);
  const suffix = enOrdinalSuffixes.get(category);
  return `${n}${suffix}`;
}

formatOrdinal(1);   // '1st'
formatOrdinal(2);   // '2nd'
formatOrdinal(3);   // '3rd'
formatOrdinal(4);   // '4th'
formatOrdinal(11);  // '11th'
formatOrdinal(21);  // '21st'

Otros idiomas tienen sistemas ordinales completamente diferentes. El francés usa "1er" para primero y "2e" para todos los demás. El español tiene ordinales específicos según el género. La API proporciona la categoría, y tú proporcionas las formas localizadas.

Manejar rangos con selectRange()

El método selectRange() determina la categoría plural para un rango de números, como "1-5 elementos" o "10-20 resultados". Algunos idiomas tienen reglas plurales diferentes para rangos que para conteos individuales.

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

enRules.selectRange(1, 5);   // 'other'
enRules.selectRange(0, 1);   // 'other'

En inglés, los rangos son casi siempre plurales, incluso cuando el rango comienza en 1. Otros idiomas tienen reglas de rango más complejas.

const slRules = new Intl.PluralRules('sl');

slRules.selectRange(102, 201);  // 'few'

const ptRules = new Intl.PluralRules('pt');

ptRules.selectRange(102, 102);  // 'other'

Utiliza selectRange() cuando muestres rangos explícitamente en tu interfaz de usuario. Para conteos individuales, utiliza select().

Combinar con Intl.NumberFormat para visualización de números localizados

Las formas plurales a menudo aparecen junto a números formateados. Utiliza Intl.NumberFormat para formatear el número según las convenciones del locale, luego usa Intl.PluralRules para elegir el texto correcto.

const locale = 'en-US';
const numberFormat = new Intl.NumberFormat(locale);
const pluralRules = new Intl.PluralRules(locale);
const forms = new Map([
  ['one', 'item'],
  ['other', 'items'],
]);

function formatCount(count) {
  const formattedNumber = numberFormat.format(count);
  const category = pluralRules.select(count);
  const form = forms.get(category);
  return `${formattedNumber} ${form}`;
}

formatCount(1);      // '1 item'
formatCount(1000);   // '1,000 items'
formatCount(1.5);    // '1.5 items'

Para alemán, que utiliza puntos como separadores de miles y comas como separadores decimales:

const locale = 'de-DE';
const numberFormat = new Intl.NumberFormat(locale);
const pluralRules = new Intl.PluralRules(locale);
const forms = new Map([
  ['one', 'Artikel'],
  ['other', 'Artikel'],
]);

function formatCount(count) {
  const formattedNumber = numberFormat.format(count);
  const category = pluralRules.select(count);
  const form = forms.get(category);
  return `${formattedNumber} ${form}`;
}

formatCount(1);      // '1 Artikel'
formatCount(1000);   // '1.000 Artikel'
formatCount(1.5);    // '1,5 Artikel'

Este patrón asegura que tanto el formato del número como la forma del texto coincidan con las expectativas del usuario para el locale.

Manejar el caso cero explícitamente cuando sea necesario

La forma en que se pluraliza el cero varía según el idioma. El inglés típicamente usa la forma plural: "0 items", "0 results". Algunos idiomas usan la forma singular para cero. Otros tienen una categoría distinta para el cero.

La API Intl.PluralRules devuelve la categoría apropiada para cero según las reglas del idioma. En inglés, cero devuelve 'other', que corresponde a la forma plural:

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

enRules.select(0);  // 'other'

En árabe, cero tiene su propia categoría:

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

arRules.select(0);  // 'zero'

Tu texto debe tener esto en cuenta. Para inglés, es posible que quieras mostrar "No items" en lugar de "0 items" para una mejor experiencia de usuario. Maneja esto antes de llamar a las reglas de pluralización:

function formatItems(count) {
  if (count === 0) {
    return 'No items';
  }
  const category = enRules.select(count);
  const form = enForms.get(category);
  return `${count} ${form}`;
}

Para árabe, proporciona una forma específica para cero en tus traducciones:

const arForms = new Map([
  ['zero', 'لا توجد عناصر'],
  ['one', 'عنصر واحد'],
  ['two', 'عنصران'],
  ['few', 'عناصر'],
  ['many', 'عنصرًا'],
  ['other', 'عنصر'],
]);

Esto respeta las convenciones lingüísticas de cada idioma mientras te permite personalizar el caso cero para una mejor experiencia de usuario.

Reutilizar instancias de PluralRules para mejorar el rendimiento

Crear una instancia de PluralRules implica analizar el locale y cargar datos de reglas de pluralización. Haz esto una vez por locale, no en cada llamada a función o ciclo de renderizado.

// Bueno: crear una vez, reutilizar
const enRules = new Intl.PluralRules('en-US');
const enForms = new Map([
  ['one', 'item'],
  ['other', 'items'],
]);

function formatItems(count) {
  const category = enRules.select(count);
  const form = enForms.get(category);
  return `${count} ${form}`;
}

Si admites múltiples locales, crea instancias para cada locale y almacénalas en un Map o caché:

const rulesCache = new Map();

function getPluralRules(locale) {
  if (!rulesCache.has(locale)) {
    rulesCache.set(locale, new Intl.PluralRules(locale));
  }
  return rulesCache.get(locale);
}

const rules = getPluralRules('en-US');

Este patrón amortiza el costo de inicialización a través de muchas llamadas.

Compatibilidad y soporte de navegadores

Intl.PluralRules es compatible con todos los navegadores modernos desde 2019. Esto incluye Chrome 63+, Firefox 58+, Safari 13+ y Edge 79+. No es compatible con Internet Explorer.

Para aplicaciones dirigidas a navegadores modernos, puedes usar Intl.PluralRules sin un polyfill. Si necesitas dar soporte a navegadores más antiguos, hay polyfills disponibles a través de paquetes como intl-pluralrules en npm.

El método selectRange() es más reciente y tiene un soporte ligeramente más limitado. Está disponible en Chrome 106+, Firefox 116+, Safari 15.4+ y Edge 106+. Verifica la compatibilidad si utilizas selectRange() y necesitas dar soporte a versiones anteriores de navegadores.

Evita codificar formas plurales en la lógica

No compruebes el conteo y ramifiques en el código para seleccionar una forma plural. Este enfoque no escala a idiomas con más de dos formas y acopla tu lógica a las reglas del inglés.

// Evita este patrón
function formatItems(count) {
  if (count === 1) {
    return `${count} item`;
  }
  return `${count} items`;
}

Utiliza Intl.PluralRules y una estructura de datos para almacenar formas. Esto mantiene tu código independiente del idioma y facilita la adición de nuevos idiomas proporcionando nuevas traducciones.

// Prefiere este patrón
const rules = new Intl.PluralRules('en-US');
const forms = new Map([
  ['one', 'item'],
  ['other', 'items'],
]);

function formatItems(count) {
  const category = rules.select(count);
  const form = forms.get(category);
  return `${count} ${form}`;
}

Este patrón funciona de manera idéntica para cualquier idioma. Solo cambian la instancia de rules y el Map de forms.

Prueba con múltiples locales y casos extremos

Las reglas de pluralización tienen casos extremos que son fáciles de pasar por alto cuando se prueba solo en inglés. Prueba tu lógica de pluralización con al menos un idioma que use más de dos formas, como el polaco o el árabe.

Prueba conteos que activen diferentes categorías:

  • Cero
  • Uno
  • Dos
  • Unos pocos (3-10 en árabe)
  • Muchos (11-99 en árabe)
  • Números grandes (100+)
  • Valores decimales (0.5, 1.5, 2.3)
  • Números negativos si tu interfaz los muestra

Si utilizas reglas ordinales, prueba números que activen diferentes sufijos: 1, 2, 3, 4, 11, 21, 22, 23. Esto asegura que manejes correctamente los casos especiales.

Probar con múltiples locales desde el principio evita sorpresas cuando agregas nuevos idiomas más tarde. También valida que tu estructura de datos incluya todas las categorías necesarias y que tu lógica las maneje correctamente.