API Intl.PluralRules
Cómo manejar correctamente las formas plurales en JavaScript
Introducción
La pluralización es el proceso de mostrar texto diferente según un recuento. 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 un condicional simple que añade una "s" para recuentos distintos de uno.
Este enfoque falla para idiomas distintos del inglés. El polaco utiliza formas diferentes 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 quedándose en inglés, los plurales irregulares como "person" a "people" requieren un manejo especial.
La API Intl.PluralRules resuelve esto proporcionando la categoría de forma plural para cualquier número en cualquier idioma. Proporcionas un recuento y la API te indica qué forma usar según las reglas del idioma de destino. Esto te permite escribir código preparado para internacionalización que funciona correctamente en todos los idiomas sin codificar manualmente reglas específicas del 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 te encuentras con 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 recuentos que terminan en 2, 3 o 4 (pero no 12, 13 o 14). Todos los demás recuentos 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 límites numéricos diferentes.
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 usando nombres de categorías estandarizados basados en las reglas de pluralización Unicode CLDR. Las seis categorías son: zero, one, two, few, many y other. No todos los idiomas usan las seis categorías. El inglés solo usa one y other. El árabe usa 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 de 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 un desperdicio. Almacena la instancia en una variable o usa 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 "1.º lugar, 2.º lugar".
Usar select() para obtener la categoría de plural para un número
El método select() toma un número y devuelve a qué categoría de 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 de retorno 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 usa el inglés.
Para el árabe, que tiene reglas más complejas, ves 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}`;
}
Proporciona siempre entradas para cada categoría que utilice 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 de plural de Unicode CLDR o prueba con la API usando diferentes números.
Maneja conteos decimales y fraccionarios
El método select() funciona con números decimales. El inglés trata los decimales como plural, 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 usan reglas más matizadas basadas en la parte fraccionaria.
Si tu interfaz 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 redondee el valor mostrado.
Formatea 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 los 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 de género. La API proporciona la categoría, y tú proporcionas las formas localizadas.
Maneja 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'
Usa selectRange() cuando muestres rangos explícitamente en tu interfaz. Para conteos individuales, usa select().
Combina con Intl.NumberFormat para mostrar números localizados
Las formas plurales a menudo aparecen junto a números formateados. Usa Intl.NumberFormat para formatear el número según las convenciones de la configuración regional, 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 el alemán, que usa 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 la configuración regional.
Maneja el caso cero explícitamente cuando sea necesario
Cómo 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 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 se mapea a la forma plural:
const enRules = new Intl.PluralRules('en-US');
enRules.select(0); // 'other'
En árabe, el 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 el inglés, es posible que desees mostrar "No items" en lugar de "0 items" para una mejor experiencia de usuario. Maneja esto antes de llamar a las reglas plurales:
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 de 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 los datos de reglas plurales. Haz esto una vez por locale, no en cada llamada de función o ciclo de renderizado.
// Good: create once, reuse
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 coste de inicialización a lo largo 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 admitir 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 usas selectRange() y necesitas admitir versiones de navegador más antiguas.
Evitar codificar formas plurales en la lógica
No verifiques 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.
// Avoid this pattern
function formatItems(count) {
if (count === 1) {
return `${count} item`;
}
return `${count} items`;
}
Usa Intl.PluralRules y una estructura de datos para almacenar formas. Esto mantiene tu código independiente del idioma y facilita agregar nuevos idiomas proporcionando nuevas traducciones.
// Prefer this pattern
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 forma idéntica para cualquier idioma. Solo cambian la instancia de reglas y el mapa de formas.
Prueba con múltiples configuraciones regionales y casos extremos
Las reglas de pluralización tienen casos extremos que son fáciles de pasar por alto cuando solo se prueba en inglés. Prueba tu lógica de pluralización con al menos un idioma que use más de dos formas, como polaco o árabe.
Prueba cantidades que activen diferentes categorías:
- Cero
- Uno
- Dos
- Algunos (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 usas reglas ordinales, prueba números que activen diferentes sufijos: 1, 2, 3, 4, 11, 21, 22, 23. Esto garantiza que manejes los casos especiales correctamente.
Probar con múltiples configuraciones regionales desde el principio previene sorpresas cuando agregues nuevos idiomas más adelante. También valida que tu estructura de datos incluya todas las categorías necesarias y que tu lógica las maneje correctamente.