API Intl.ListFormat

Formatear arrays en listas legibles adaptadas al idioma local

Introducción

Al mostrar múltiples elementos a los usuarios, los desarrolladores a menudo unen arrays con comas y añaden "y" antes del último elemento:

const users = ["Alice", "Bob", "Charlie"];
const message = users.slice(0, -1).join(", ") + ", and " + users[users.length - 1];
// "Alice, Bob, and Charlie"

Este enfoque codifica las reglas de puntuación en inglés y falla en otros idiomas. El japonés utiliza partículas diferentes, el alemán tiene reglas de espaciado distintas, y el chino usa separadores diferentes. La API Intl.ListFormat resuelve esto formateando listas según las convenciones de cada localización.

Qué hace Intl.ListFormat

Intl.ListFormat convierte arrays en listas legibles que siguen las reglas gramaticales y de puntuación de cualquier idioma. Maneja tres tipos de listas que aparecen en todos los idiomas:

  • Listas de conjunción usan "y" para conectar elementos ("A, B, y C")
  • Listas de disyunción usan "o" para presentar alternativas ("A, B, o C")
  • Listas de unidades formatean medidas sin conjunciones ("5 ft, 2 in")

La API conoce cómo cada idioma formatea estos tipos de listas, desde la puntuación hasta la elección de palabras y el espaciado.

Uso básico

Crea un formateador con una localización y opciones, luego llama a format() con un array:

const formatter = new Intl.ListFormat("en", {
  type: "conjunction",
  style: "long"
});

const items = ["bread", "milk", "eggs"];
console.log(formatter.format(items));
// "bread, milk, and eggs"

El formateador maneja arrays de cualquier longitud, incluyendo casos especiales:

formatter.format([]);              // ""
formatter.format(["bread"]);       // "bread"
formatter.format(["bread", "milk"]); // "bread and milk"

Los tipos de lista controlan las conjunciones

La opción type determina qué conjunción aparece en la lista formateada.

Listas de conjunción

Utiliza type: "conjunction" para listas donde todos los elementos se aplican juntos. Este es el tipo predeterminado:

const formatter = new Intl.ListFormat("en", { type: "conjunction" });

console.log(formatter.format(["HTML", "CSS", "JavaScript"]));
// "HTML, CSS, and JavaScript"

Los usos comunes incluyen mostrar elementos seleccionados, enumerar características y mostrar múltiples valores que se aplican simultáneamente.

Listas de disyunción

Utiliza type: "disjunction" para listas que presentan alternativas u opciones:

const formatter = new Intl.ListFormat("en", { type: "disjunction" });

console.log(formatter.format(["credit card", "debit card", "PayPal"]));
// "credit card, debit card, or PayPal"

Esto aparece en listas de opciones, mensajes de error con múltiples soluciones y cualquier contexto donde los usuarios eligen un elemento.

Listas de unidades

Utiliza type: "unit" para medidas y valores técnicos que deben aparecer sin conjunciones:

const formatter = new Intl.ListFormat("en", { type: "unit" });

console.log(formatter.format(["5 feet", "2 inches"]));
// "5 feet, 2 inches"

Las listas de unidades funcionan para medidas, especificaciones técnicas y valores compuestos.

Los estilos de lista controlan la verbosidad

La opción style ajusta cuán detallado aparece el formato. Existen tres estilos: long, short y narrow.

const items = ["Monday", "Wednesday", "Friday"];

const long = new Intl.ListFormat("en", { style: "long" });
console.log(long.format(items));
// "Monday, Wednesday, and Friday"

const short = new Intl.ListFormat("en", { style: "short" });
console.log(short.format(items));
// "Monday, Wednesday, and Friday"

const narrow = new Intl.ListFormat("en", { style: "narrow" });
console.log(narrow.format(items));
// "Monday, Wednesday, Friday"

En inglés, long y short producen resultados idénticos para la mayoría de las listas. El estilo narrow omite la conjunción. Otros idiomas muestran más variación entre estilos, particularmente para listas de disyunción.

Cómo los diferentes idiomas formatean listas

Cada idioma tiene reglas distintas para el formato de listas. Intl.ListFormat maneja estas diferencias automáticamente.

El inglés utiliza comas, espacios y conjunciones:

const en = new Intl.ListFormat("en");
console.log(en.format(["Tokyo", "Paris", "London"]));
// "Tokyo, Paris, and London"

El alemán utiliza la misma estructura de comas pero diferentes conjunciones:

const de = new Intl.ListFormat("de");
console.log(de.format(["Tokyo", "Paris", "London"]));
// "Tokyo, Paris und London"

El japonés utiliza diferentes separadores y partículas:

const ja = new Intl.ListFormat("ja");
console.log(ja.format(["東京", "パリ", "ロンドン"]));
// "東京、パリ、ロンドン"

El chino utiliza una puntuación completamente diferente:

const zh = new Intl.ListFormat("zh");
console.log(zh.format(["东京", "巴黎", "伦敦"]));
// "东京、巴黎和伦敦"

Estas diferencias van más allá de la puntuación, abarcando reglas de espaciado, colocación de conjunciones y partículas gramaticales. Codificar de forma rígida cualquier enfoque único no funciona para otros idiomas.

Uso de formatToParts para renderizado personalizado

El método formatToParts() devuelve un array de objetos en lugar de una cadena. Cada objeto representa una parte de la lista formateada:

const formatter = new Intl.ListFormat("en");
const parts = formatter.formatToParts(["red", "green", "blue"]);

console.log(parts);
// [
//   { type: "element", value: "red" },
//   { type: "literal", value: ", " },
//   { type: "element", value: "green" },
//   { type: "literal", value: ", and " },
//   { type: "element", value: "blue" }
// ]

Cada parte tiene un type y un value. El type es "element" para los elementos de la lista o "literal" para la puntuación de formato y las conjunciones.

Esta estructura permite un renderizado personalizado donde los elementos y literales necesitan diferentes estilos:

const formatter = new Intl.ListFormat("en");
const items = ["Alice", "Bob", "Charlie"];

const html = formatter.formatToParts(items)
  .map(part => {
    if (part.type === "element") {
      return `<strong>${part.value}</strong>`;
    }
    return part.value;
  })
  .join("");

console.log(html);
// "<strong>Alice</strong>, <strong>Bob</strong>, and <strong>Charlie</strong>"

Este enfoque mantiene la puntuación correcta según el idioma mientras aplica una presentación personalizada a los elementos reales de la lista.

Reutilización de formateadores para mejorar el rendimiento

Crear instancias de Intl.ListFormat tiene una sobrecarga. Crea formateadores una vez y reutilízalos:

// Crear una vez
const listFormatter = new Intl.ListFormat("en", { type: "conjunction" });

// Reutilizar muchas veces
function displayUsers(users) {
  return listFormatter.format(users.map(u => u.name));
}

function displayTags(tags) {
  return listFormatter.format(tags);
}

Para aplicaciones con múltiples locales, almacena los formateadores en un mapa:

const formatters = new Map();

function getListFormatter(locale, options) {
  const key = `${locale}-${options.type}-${options.style}`;
  if (!formatters.has(key)) {
    formatters.set(key, new Intl.ListFormat(locale, options));
  }
  return formatters.get(key);
}

const formatter = getListFormatter("en", { type: "conjunction", style: "long" });
console.log(formatter.format(["a", "b", "c"]));

Este patrón reduce los costos de inicialización repetida mientras soporta múltiples locales y configuraciones.

Formateo de mensajes de error

La validación de formularios a menudo produce múltiples errores. Formátealos con listas de disyunción para presentar opciones:

const formatter = new Intl.ListFormat("en", { type: "disjunction" });

function validatePassword(password) {
  const errors = [];

  if (password.length < 8) {
    errors.push("al menos 8 caracteres");
  }
  if (!/[A-Z]/.test(password)) {
    errors.push("una letra mayúscula");
  }
  if (!/[0-9]/.test(password)) {
    errors.push("un número");
  }

  if (errors.length > 0) {
    return `La contraseña debe contener ${formatter.format(errors)}.`;
  }

  return null;
}

console.log(validatePassword("weak"));
// "La contraseña debe contener al menos 8 caracteres, una letra mayúscula, o un número."

La lista de disyunción aclara que los usuarios necesitan corregir cualquiera de estos problemas, y el formateo se adapta a las convenciones de cada locale.

Visualización de elementos seleccionados

Cuando los usuarios seleccionan múltiples elementos, formatea la selección con listas de conjunción:

const formatter = new Intl.ListFormat("en", { type: "conjunction" });

function getSelectionMessage(selectedFiles) {
  if (selectedFiles.length === 0) {
    return "No hay archivos seleccionados";
  }

  if (selectedFiles.length === 1) {
    return `${selectedFiles[0]} seleccionado`;
  }

  return `${formatter.format(selectedFiles)} seleccionados`;
}

console.log(getSelectionMessage(["report.pdf", "data.csv", "notes.txt"]));
// "report.pdf, data.csv y notes.txt seleccionados"

Este patrón funciona para selecciones de archivos, opciones de filtro, selecciones de categorías y cualquier interfaz de selección múltiple.

Manejo de listas largas

Para listas con muchos elementos, considera truncar antes de formatear:

const formatter = new Intl.ListFormat("en", { type: "conjunction" });

function formatUserList(users) {
  if (users.length <= 3) {
    return formatter.format(users);
  }

  const visible = users.slice(0, 2);
  const remaining = users.length - 2;

  return `${formatter.format(visible)}, and ${remaining} others`;
}

console.log(formatUserList(["Alice", "Bob", "Charlie", "David", "Eve"]));
// "Alice, Bob, and 3 others"

Esto mantiene la legibilidad mientras indica el recuento total. El umbral exacto depende de las restricciones de tu interfaz.

Compatibilidad con navegadores y alternativas

Intl.ListFormat funciona en todos los navegadores modernos desde abril de 2021. La compatibilidad incluye Chrome 72+, Firefox 78+, Safari 14.1+ y Edge 79+.

Verifica la compatibilidad con detección de características:

if (typeof Intl.ListFormat !== "undefined") {
  const formatter = new Intl.ListFormat("en");
  return formatter.format(items);
} else {
  // Alternativa para navegadores antiguos
  return items.join(", ");
}

Para una compatibilidad más amplia, utiliza un polyfill como @formatjs/intl-listformat. Instálalo solo para entornos que lo necesiten:

if (typeof Intl.ListFormat === "undefined") {
  await import("@formatjs/intl-listformat/polyfill");
}

Dada la compatibilidad actual de los navegadores, la mayoría de las aplicaciones pueden usar Intl.ListFormat directamente sin polyfills.

Errores comunes a evitar

Crear nuevos formateadores repetidamente desperdicia recursos:

// Ineficiente
function display(items) {
  return new Intl.ListFormat("en").format(items);
}

// Eficiente
const formatter = new Intl.ListFormat("en");
function display(items) {
  return formatter.format(items);
}

Usar array.join() para texto visible por el usuario crea problemas de localización:

// Falla en otros idiomas
const text = items.join(", ");

// Funciona en todos los idiomas
const formatter = new Intl.ListFormat(userLocale);
const text = formatter.format(items);

Asumir que las reglas de conjunción del inglés se aplican universalmente lleva a resultados incorrectos en otras localizaciones. Siempre pasa la localización del usuario al constructor.

No manejar arrays vacíos puede causar resultados inesperados:

// Defensivo
function formatItems(items) {
  if (items.length === 0) {
    return "No items";
  }
  return formatter.format(items);
}

Aunque format([]) devuelve una cadena vacía, el manejo explícito de estados vacíos mejora la experiencia del usuario.

Cuándo usar Intl.ListFormat

Utiliza Intl.ListFormat siempre que muestres múltiples elementos en texto. Esto incluye migas de navegación, filtros seleccionados, errores de validación, listas de usuarios, etiquetas de categorías y listas de características.

No lo uses para visualizaciones de datos estructurados como tablas o menús de opciones. Estos componentes tienen sus propios requisitos de formato fuera de las reglas de listas en texto.

La API reemplaza la concatenación manual de cadenas y patrones de unión. Cada vez que escribirías join(", ") para texto visible al usuario, considera si Intl.ListFormat proporciona mejor soporte de localización.