API Intl.ListFormat

Formatea arrays en listas legibles adaptadas a la configuración regional

Introducción

Al mostrar múltiples elementos a los usuarios, los desarrolladores suelen unir arrays con comas y añadir "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 de forma rígida las reglas de puntuación del inglés y falla en otros idiomas. El japonés usa 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 configuración regional.

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 pies, 2 pulgadas")

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 configuración regional 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, incluidos casos extremos:

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

Usa 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 todos.

Listas de disyunción

Usa 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

Usa 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 verboso 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 formatean las listas los diferentes idiomas

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

El inglés usa comas, espacios y conjunciones:

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

El alemán usa 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 usa diferentes separadores y partículas:

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

El chino usa puntuación completamente diferente:

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

Estas diferencias se extienden más allá de la puntuación a las reglas de espaciado, colocación de conjunciones y partículas gramaticales. Codificar cualquier enfoque único falla para otros idiomas.

Usar formatToParts para renderizado personalizado

El método formatToParts() devuelve un array de objetos en lugar de una cadena. Cada objeto representa una pieza 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 value. El type es "element" para elementos de lista o "literal" para puntuación de formato y conjunciones.

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

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 la configuración regional mientras aplica una presentación personalizada a los elementos de la lista.

Reutilización de formateadores para mejorar el rendimiento

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

// Create once
const listFormatter = new Intl.ListFormat("en", { type: "conjunction" });

// Reuse many times
function displayUsers(users) {
  return listFormatter.format(users.map(u => u.name));
}

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

Para aplicaciones con múltiples configuraciones regionales, 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 costes de inicialización repetida mientras admite múltiples configuraciones regionales y configuraciones.

Formateo de mensajes de error

La validación de formularios a menudo produce múltiples errores. Formatéalos 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("at least 8 characters");
  }
  if (!/[A-Z]/.test(password)) {
    errors.push("an uppercase letter");
  }
  if (!/[0-9]/.test(password)) {
    errors.push("a number");
  }

  if (errors.length > 0) {
    return `Password must contain ${formatter.format(errors)}.`;
  }

  return null;
}

console.log(validatePassword("weak"));
// "Password must contain at least 8 characters, an uppercase letter, or a number."

La lista de disyunción aclara que los usuarios deben corregir cualquiera de estos problemas, y el formato se adapta a las convenciones de cada configuración regional.

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 files selected";
  }

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

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

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

Este patrón funciona para selecciones de archivos, opciones de filtros, 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 {
  // Fallback for older browsers
  return items.join(", ");
}

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

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

Dado el soporte actual de los navegadores, la mayoría de las aplicaciones pueden usar Intl.ListFormat directamente sin polyfills.

Errores comunes que evitar

Crear formateadores nuevos repetidamente desperdicia recursos:

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

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

Usar array.join() para texto orientado al usuario crea problemas de localización:

// Breaks in other languages
const text = items.join(", ");

// Works across languages
const formatter = new Intl.ListFormat(userLocale);
const text = formatter.format(items);

Asumir que las reglas de conjunción del inglés se aplican universalmente conduce a resultados incorrectos en otras configuraciones regionales. Siempre pasa la configuración regional del usuario al constructor.

No manejar arrays vacíos puede causar resultados inesperados:

// Defensive
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 del estado vacío mejora la experiencia del usuario.

Cuándo usar Intl.ListFormat

Usa Intl.ListFormat siempre que muestres múltiples elementos en prosa. 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. Esos componentes tienen sus propios requisitos de formato fuera de las reglas de listas en prosa.

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