Obtener todas las formas plurales disponibles en una localización

Descubre qué categorías plurales necesitas proporcionar para las traducciones

Introducción

Al construir aplicaciones multilingües, necesitas proporcionar diferentes formas de texto para diferentes cantidades. En inglés, escribes "1 item" y "2 items". Esto parece simple hasta que empiezas a dar soporte a otros idiomas.

El ruso utiliza tres formas diferentes dependiendo del conteo. El árabe utiliza seis. Algunos idiomas utilizan la misma forma para todos los conteos. Antes de poder proporcionar traducciones para estas formas, necesitas saber qué formas existen en cada idioma.

JavaScript proporciona una manera de descubrir qué categorías de plurales utiliza un locale. El método resolvedOptions() en una instancia de PluralRules devuelve una propiedad pluralCategories que enumera todas las formas plurales que necesita el locale. Esto te indica exactamente qué traducciones proporcionar sin tener que adivinar o mantener tablas de reglas específicas para cada idioma.

Qué son las categorías de plurales

Las categorías de plurales son nombres estandarizados para diferentes formas plurales utilizadas en los distintos idiomas. El CLDR (Common Locale Data Repository) de Unicode define seis categorías: zero, one, two, few, many y other.

No todos los idiomas utilizan las seis categorías. El inglés utiliza solo dos: one y other. La categoría one se aplica al conteo 1, y other se aplica a todo lo demás.

El árabe utiliza las seis categorías. La categoría zero se aplica al 0, one al 1, two al 2, few a conteos como 3-10, many a conteos como 11-99, y other a conteos como 100 y superiores.

El ruso utiliza tres categorías: one para conteos que terminan en 1 (excepto 11), few para conteos que terminan en 2-4 (excepto 12-14), y many para todo lo demás.

El japonés y el chino utilizan solo la categoría other porque estos idiomas no distinguen entre formas singulares y plurales.

Estas categorías representan las reglas lingüísticas de cada idioma. Cuando proporcionas traducciones, creas una cadena para cada categoría que utiliza el idioma.

Obtener categorías plurales con resolvedOptions

El método resolvedOptions() en una instancia de PluralRules devuelve un objeto que contiene información sobre las reglas, incluyendo qué categorías plurales utiliza el idioma.

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

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

La propiedad pluralCategories es un array de strings. Cada string es uno de los seis nombres de categorías estándar. El array contiene solo las categorías que el idioma realmente utiliza.

Para el inglés, el array contiene one y other porque el inglés distingue entre formas singulares y plurales.

Para un idioma con reglas más complejas, el array contiene más categorías:

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

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

El árabe utiliza las seis categorías, por lo que el array contiene los seis valores.

Visualización de categorías plurales para diferentes idiomas

Diferentes idiomas tienen diferentes reglas de pluralización, lo que significa que utilizan diferentes conjuntos de categorías. Compara varios idiomas para ver la variación:

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(', ')}]`);
});

// Resultado:
// 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]

El inglés tiene dos categorías. El árabe tiene seis. El ruso y el polaco tienen cuatro cada uno. El japonés y el chino tienen solo una porque no distinguen formas plurales en absoluto.

Esta variación muestra por qué no puedes asumir que todos los idiomas funcionan como el inglés. Necesitas verificar qué categorías utiliza cada idioma y proporcionar traducciones apropiadas para cada una.

Entendiendo qué significan las categorías para cada localización

El mismo nombre de categoría significa cosas diferentes en distintos idiomas. La categoría one en inglés se aplica solo al número 1. En ruso, one se aplica a números que terminan en 1 excepto el 11, por lo que incluye 1, 21, 31, 101, y así sucesivamente.

Prueba qué números corresponden a qué categorías en diferentes localizaciones:

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 inglés, solo el 1 usa la categoría one. En ruso, 1 y 21 usan one porque terminan en 1. Los números 2, 3 y 22 usan few porque terminan en 2-4. Los números 0, 5, 11 y 100 usan many.

Esto demuestra que no puedes predecir qué categoría se aplica a un número sin conocer las reglas del idioma. El array pluralCategories te indica qué categorías existen, y el método select() te dice qué categoría se aplica a cada número.

Obteniendo categorías para números ordinales

Los números ordinales como 1º, 2º, 3º tienen sus propias reglas de pluralización que difieren de los números cardinales. Crea una instancia de PluralRules con type: 'ordinal' para obtener las categorías para números ordinales:

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

Los números cardinales en inglés usan dos categorías. Los números ordinales en inglés usan cuatro categorías porque los ordinales necesitan distinguir entre 1st, 2nd, 3rd y todos los demás.

Las categorías ordinales corresponden a los sufijos ordinales:

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 categoría one corresponde al sufijo st (1st, 21st), two a nd (2nd, 22nd), few a rd (3rd, 23rd), y other a th (4th, 11th).

Diferentes idiomas tienen diferentes categorías 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]

El español usa solo una categoría ordinal porque los ordinales en español siguen un patrón más simple. El francés usa dos categorías para distinguir el primero de todas las demás posiciones.

Uso de categorías plurales para construir mapas de traducción

Cuando conoces qué categorías utiliza un locale, puedes construir un mapa de traducción con exactamente el número correcto de entradas:

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

Esta función verifica que hayas proporcionado traducciones para todas las categorías requeridas y te advierte si falta alguna. Esto previene errores en tiempo de ejecución cuando se utiliza una categoría pero no tiene traducción.

Validación de la integridad de las traducciones

Utiliza las categorías plurales para verificar que tus traducciones incluyan todas las formas necesarias antes de implementarlas en producción:

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

Esta validación detecta traducciones faltantes durante el desarrollo en lugar de descubrirlas cuando los usuarios encuentran texto sin traducir.

Construcción de interfaces de traducción dinámicas

Al construir herramientas para traductores, consulta las categorías de plurales para mostrar exactamente qué formas necesitan traducción:

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

Esto genera un formulario con el número correcto de campos de entrada para cada localización. El inglés obtiene dos campos (one y other), mientras que el árabe obtiene seis campos (zero, one, two, few, many y other).

Comparación de categorías entre localizaciones

Al gestionar traducciones para múltiples localizaciones, compara qué categorías utilizan para entender la complejidad de la traducción:

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

Esto muestra que el inglés y el español tienen las mismas categorías de plurales, lo que facilita la reutilización de estructuras de traducción entre ellos. El árabe requiere significativamente más trabajo de traducción porque utiliza seis categorías.

Comprobando si un locale usa una categoría específica

Antes de usar una categoría plural específica en tu código, comprueba si el locale realmente la utiliza:

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

Esto evita que asumas que cada locale tiene una categoría zero o una categoría one. Utiliza esta comprobación para implementar lógica específica de categoría de forma segura.

Entendiendo la categoría other

Todos los idiomas utilizan la categoría other. Esta categoría sirve como caso predeterminado cuando no se aplica ninguna otra categoría.

En inglés, other cubre todos los conteos excepto 1. En árabe, other cubre números grandes como 100 y superiores. En japonés, other cubre todos los conteos porque el japonés no distingue formas plurales.

Siempre proporciona una traducción para la categoría other. Esta categoría está garantizada que existe en cada locale y se utilizará cuando ninguna categoría más específica coincida.

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

Obteniendo todas las opciones resueltas juntas

El método resolvedOptions() devuelve más que solo categorías plurales. Incluye información sobre el locale, tipo y opciones de formato de números:

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 propiedad pluralCategories es una parte de la información en el objeto de opciones resueltas. Las otras propiedades te indican la configuración exacta que utiliza la instancia PluralRules, incluidas las opciones que se establecieron con valores predeterminados.

Almacenamiento en caché de categorías plurales para mejorar el rendimiento

Crear instancias de PluralRules y llamar a resolvedOptions() tiene un costo. Almacena en caché los resultados para cada localización en lugar de consultarlos repetidamente:

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

// Las llamadas subsiguientes utilizan resultados almacenados en caché
const enCardinal2 = getPluralCategories('en-US', 'cardinal');
// No se crea una nueva instancia de PluralRules

Este patrón es especialmente importante en aplicaciones que formatean muchas cadenas pluralizadas o que soportan muchas localizaciones.

Soporte del navegador y compatibilidad

La propiedad pluralCategories en resolvedOptions() se añadió a JavaScript en 2020. Es compatible con Chrome 106+, Firefox 116+, Safari 15.4+ y Edge 106+.

Los navegadores más antiguos que soportan Intl.PluralRules pero no pluralCategories devolverán undefined para esta propiedad. Verifica su existencia antes de usarla:

function getPluralCategories(locale) {
  const rules = new Intl.PluralRules(locale);
  const options = rules.resolvedOptions();

  if (options.pluralCategories) {
    return options.pluralCategories;
  }

  // Alternativa para navegadores más antiguos
  return ['one', 'other'];
}

Esta alternativa asume un sistema simple de dos categorías, que funciona para el inglés y muchos idiomas europeos, pero puede no ser correcto para idiomas con reglas más complejas. Para una mejor compatibilidad, proporciona alternativas específicas para cada idioma o utiliza un polyfill.