¿Cómo dividir texto en oraciones?

Utiliza Intl.Segmenter para dividir texto en oraciones con detección de límites adaptada al idioma que maneja puntuación, abreviaturas y reglas específicas del lenguaje.

Introducción

Cuando procesas texto para traducción, análisis o visualización, a menudo necesitas dividirlo en oraciones individuales. Un enfoque ingenuo utilizando expresiones regulares falla porque los límites de las oraciones son más complejos que puntos seguidos de espacios. Las oraciones pueden terminar con signos de interrogación, exclamación o puntos suspensivos. Los puntos aparecen en abreviaturas como "Dr." o "Inc." sin finalizar oraciones. Diferentes idiomas utilizan diferentes signos de puntuación como terminadores de oraciones.

La API Intl.Segmenter resuelve este problema proporcionando detección de límites de oraciones adaptada al idioma. Comprende las reglas para identificar los límites de las oraciones en diferentes idiomas y maneja automáticamente casos especiales como abreviaturas, números y puntuación compleja.

El problema de dividir por puntos

Puedes intentar dividir el texto en oraciones separando por puntos seguidos de espacios.

const text = "Hello world. How are you? I am fine.";
const sentences = text.split(". ");
console.log(sentences);
// ["Hello world", "How are you? I am fine."]

Este enfoque tiene múltiples problemas. Primero, no maneja signos de interrogación o exclamación. Segundo, falla con abreviaturas que contienen puntos. Tercero, elimina el punto de cada oración excepto la última. Cuarto, no funciona cuando hay múltiples espacios después de los puntos.

const text = "Dr. Smith works at Acme Inc. He starts at 9 a.m.";
const sentences = text.split(". ");
console.log(sentences);
// ["Dr", "Smith works at Acme Inc", "He starts at 9 a.m."]

El texto se divide incorrectamente en "Dr." e "Inc." porque estas abreviaturas contienen puntos. Necesitas un enfoque más inteligente que comprenda las reglas de límites de oraciones.

Uso de una expresión regular más compleja

Puedes mejorar la regex para manejar más casos.

const text = "Hello world. How are you? I am fine!";
const sentences = text.split(/[.?!]\s+/);
console.log(sentences);
// ["Hello world", "How are you", "I am fine", ""]

Esto divide el texto en puntos, signos de interrogación y signos de exclamación seguidos de espacios en blanco. Maneja más casos pero aún falla con abreviaturas y crea cadenas vacías. También elimina la puntuación de cada oración.

const text = "Dr. Smith works at Acme Inc. He starts at 9 a.m.";
const sentences = text.split(/[.?!]\s+/);
console.log(sentences);
// ["Dr", "Smith works at Acme Inc", "He starts at 9 a", "m", ""]

El enfoque de regex no puede distinguir de manera confiable entre puntos que terminan oraciones y puntos que aparecen en abreviaturas. Crear una regex completa que maneje todos los casos extremos se vuelve poco práctico. Necesitas una solución que comprenda las reglas lingüísticas.

Uso de Intl.Segmenter para dividir oraciones

El constructor Intl.Segmenter crea un segmentador que divide el texto basándose en reglas específicas del idioma. Especificas un idioma y estableces la opción granularity a "sentence".

const segmenter = new Intl.Segmenter("en", { granularity: "sentence" });
const text = "Hello world. How are you? I am fine!";
const segments = segmenter.segment(text);

for (const segment of segments) {
  console.log(segment.segment);
}
// "Hello world. "
// "How are you? "
// "I am fine!"

El método segment() devuelve un iterable que produce objetos de segmento. Cada objeto de segmento tiene una propiedad segment que contiene el texto de ese segmento. El segmentador preserva la puntuación y los espacios en blanco al final de cada oración.

Puedes convertir los segmentos en un array usando Array.from().

const segmenter = new Intl.Segmenter("en", { granularity: "sentence" });
const text = "Hello world. How are you? I am fine!";
const segments = segmenter.segment(text);
const sentences = Array.from(segments, s => s.segment);
console.log(sentences);
// ["Hello world. ", "How are you? ", "I am fine!"]

Esto crea un array donde cada elemento es una oración con su puntuación y espaciado originales.

Cómo Intl.Segmenter maneja las abreviaturas

El segmentador comprende los patrones comunes de abreviatura y no divide en los puntos que aparecen dentro de las abreviaturas.

const segmenter = new Intl.Segmenter("en", { granularity: "sentence" });
const text = "Dr. Smith works at Acme Inc. He starts at 9 a.m.";
const segments = segmenter.segment(text);
const sentences = Array.from(segments, s => s.segment);
console.log(sentences);
// ["Dr. Smith works at Acme Inc. ", "He starts at 9 a.m."]

El texto se divide correctamente en dos oraciones. Los puntos en "Dr.", "Inc." y "a.m." no desencadenan separaciones de oraciones porque el segmentador reconoce estos como abreviaturas. Este manejo automático de casos especiales es por lo que Intl.Segmenter es superior a los enfoques con expresiones regulares.

Eliminando espacios en blanco de las oraciones

El segmentador incluye espacios en blanco finales en cada oración. Puedes eliminar estos espacios en blanco si es necesario.

const segmenter = new Intl.Segmenter("en", { granularity: "sentence" });
const text = "Hello world. How are you? I am fine!";
const segments = segmenter.segment(text);
const sentences = Array.from(segments, s => s.segment.trim());
console.log(sentences);
// ["Hello world.", "How are you?", "I am fine!"]

El método trim() elimina los espacios en blanco iniciales y finales de cada oración. Esto es útil cuando necesitas límites de oración limpios sin espacios adicionales.

Obteniendo metadatos de segmentos

Cada objeto de segmento incluye metadatos sobre la posición del segmento en el texto original.

const segmenter = new Intl.Segmenter("en", { granularity: "sentence" });
const text = "Hello world. How are you?";
const segments = segmenter.segment(text);

for (const segment of segments) {
  console.log({
    text: segment.segment,
    index: segment.index,
    input: segment.input
  });
}
// { text: "Hello world. ", index: 0, input: "Hello world. How are you?" }
// { text: "How are you?", index: 13, input: "Hello world. How are you?" }

La propiedad index indica dónde comienza el segmento en el texto original. La propiedad input contiene el texto original completo. Estos metadatos son útiles cuando necesitas rastrear las posiciones de las oraciones o reconstruir el texto original.

División de oraciones en diferentes idiomas

Diferentes idiomas tienen diferentes reglas de límites de oraciones. El segmentador adapta su comportamiento según la configuración regional especificada.

En japonés, las oraciones pueden terminar con un punto de ancho completo llamado kuten.

const segmenter = new Intl.Segmenter("ja", { granularity: "sentence" });
const text = "私は猫です。名前はまだない。";
const segments = segmenter.segment(text);
const sentences = Array.from(segments, s => s.segment);
console.log(sentences);
// ["私は猫です。", "名前はまだない。"]

El texto se divide correctamente en los terminadores de oraciones japonesas. Un segmentador configurado para inglés no reconocería estos límites correctamente.

En hindi, las oraciones pueden terminar con una barra vertical llamada purna viram.

const segmenter = new Intl.Segmenter("hi", { granularity: "sentence" });
const text = "यह एक वाक्य है। यह दूसरा वाक्य है।";
const segments = segmenter.segment(text);
const sentences = Array.from(segments, s => s.segment);
console.log(sentences);
// ["यह एक वाक्य है। ", "यह दूसरा वाक्य है।"]

El segmentador reconoce el punto completo devanagari como límite de oración. Este comportamiento adaptado a la configuración regional es crítico para el procesamiento de texto internacionalizado.

Uso de la configuración regional correcta para texto multilingüe

Cuando procesas texto que contiene múltiples idiomas, elige la configuración regional que coincida con el idioma principal del texto. El segmentador utiliza la configuración regional especificada para determinar qué reglas de límites aplicar.

const englishText = "Hello world. How are you?";
const japaneseText = "私は猫です。名前はまだない。";

const englishSegmenter = new Intl.Segmenter("en", { granularity: "sentence" });
const japaneseSegmenter = new Intl.Segmenter("ja", { granularity: "sentence" });

const englishSentences = Array.from(
  englishSegmenter.segment(englishText),
  s => s.segment
);

const japaneseSentences = Array.from(
  japaneseSegmenter.segment(japaneseText),
  s => s.segment
);

console.log(englishSentences);
// ["Hello world. ", "How are you?"]

console.log(japaneseSentences);
// ["私は猫です。", "名前はまだない。"]

Crear segmentadores separados para cada idioma garantiza una detección correcta de los límites. Si procesas texto donde el idioma es desconocido, puedes usar una configuración regional genérica como "en" como respaldo, aunque esto reduce la precisión para textos que no están en inglés.

Manejo de texto sin límites de oración

Cuando el texto no contiene terminadores de oración, el segmentador devuelve todo el texto como un único segmento.

const segmenter = new Intl.Segmenter("en", { granularity: "sentence" });
const text = "Hello world";
const segments = segmenter.segment(text);
const sentences = Array.from(segments, s => s.segment);
console.log(sentences);
// ["Hello world"]

Este comportamiento es correcto porque el texto no contiene límites de oración. El segmentador no divide artificialmente el texto que forma una única oración.

Manejo de cadenas vacías

El segmentador maneja las cadenas vacías devolviendo un iterador vacío.

const segmenter = new Intl.Segmenter("en", { granularity: "sentence" });
const text = "";
const segments = segmenter.segment(text);
const sentences = Array.from(segments, s => s.segment);
console.log(sentences);
// []

Esto produce un array vacío, que es el resultado esperado para una entrada vacía.

Reutilización de segmentadores para un mejor rendimiento

Crear un segmentador tiene cierta sobrecarga. Cuando necesites segmentar múltiples textos con el mismo locale y opciones, crea el segmentador una vez y reutilízalo.

const segmenter = new Intl.Segmenter("en", { granularity: "sentence" });

const texts = [
  "First text. With two sentences.",
  "Second text. With three sentences. And more.",
  "Third text."
];

texts.forEach(text => {
  const sentences = Array.from(segmenter.segment(text), s => s.segment);
  console.log(sentences);
});
// ["First text. ", "With two sentences."]
// ["Second text. ", "With three sentences. ", "And more."]
// ["Third text."]

Reutilizar el segmentador es más eficiente que crear uno nuevo para cada texto.

Construcción de una función para contar oraciones

Puedes utilizar el segmentador para contar oraciones en un texto.

function countSentences(text, locale = "en") {
  const segmenter = new Intl.Segmenter(locale, { granularity: "sentence" });
  const segments = segmenter.segment(text);
  return Array.from(segments).length;
}

console.log(countSentences("Hello world. How are you?"));
// 2

console.log(countSentences("Dr. Smith works at Acme Inc. He starts at 9 a.m."));
// 2

console.log(countSentences("Single sentence"));
// 1

console.log(countSentences("私は猫です。名前はまだない。", "ja"));
// 2

Esta función crea un segmentador, divide el texto y devuelve el número de segmentos. Maneja correctamente las abreviaturas y los límites específicos del idioma.

Construyendo una función de extracción de oraciones

Puedes crear una función que extraiga una oración específica de un texto por su índice.

function getSentence(text, index, locale = "en") {
  const segmenter = new Intl.Segmenter(locale, { granularity: "sentence" });
  const segments = Array.from(segmenter.segment(text), s => s.segment);
  return segments[index] || null;
}

const text = "First sentence. Second sentence. Third sentence.";

console.log(getSentence(text, 0));
// "First sentence. "

console.log(getSentence(text, 1));
// "Second sentence. "

console.log(getSentence(text, 2));
// "Third sentence."

console.log(getSentence(text, 3));
// null

Esta función devuelve la oración en el índice especificado, o null si el índice está fuera de los límites.

Comprobando la compatibilidad del navegador y el entorno de ejecución

La API Intl.Segmenter está disponible en navegadores modernos y Node.js. Se convirtió en parte de la base de la plataforma web en abril de 2024 y es compatible con todos los motores de navegador principales.

Puedes verificar si la API está disponible antes de usarla.

if (typeof Intl.Segmenter !== "undefined") {
  const segmenter = new Intl.Segmenter("en", { granularity: "sentence" });
  const text = "Hello world. How are you?";
  const sentences = Array.from(segmenter.segment(text), s => s.segment);
  console.log(sentences);
} else {
  console.log("Intl.Segmenter is not supported");
}

Para entornos sin soporte, necesitas proporcionar una alternativa. Una alternativa simple utiliza una división básica con expresiones regulares, aunque esto pierde la precisión de la segmentación adaptada al idioma.

function splitSentences(text, locale = "en") {
  if (typeof Intl.Segmenter !== "undefined") {
    const segmenter = new Intl.Segmenter(locale, { granularity: "sentence" });
    return Array.from(segmenter.segment(text), s => s.segment);
  }

  // Alternativa para entornos más antiguos
  return text.split(/[.!?]\s+/).filter(s => s.length > 0);
}

console.log(splitSentences("Hello world. How are you?"));
// ["Hello world. ", "How are you?"]

Esta función utiliza Intl.Segmenter cuando está disponible y recurre a la división por expresiones regulares en entornos más antiguos. La alternativa pierde características como el manejo de abreviaturas y la detección de límites específicos del idioma, pero proporciona una funcionalidad básica.