Intl.Segmenter API

Wie man Zeichen zählt, Wörter trennt und Sätze in JavaScript korrekt segmentiert

Einführung

Die string.length-Eigenschaft in JavaScript zählt Code-Units, nicht die vom Benutzer wahrgenommenen Zeichen. Wenn Benutzer Emojis, Zeichen mit Akzenten oder Text in komplexen Schriftsystemen eingeben, liefert string.length die falsche Anzahl. Die split()-Methode versagt bei Sprachen, die keine Leerzeichen zwischen Wörtern verwenden. Reguläre Ausdrücke für Wortgrenzen funktionieren nicht für chinesischen, japanischen oder thailändischen Text.

Die Intl.Segmenter-API löst diese Probleme. Sie segmentiert Text gemäß Unicode-Standards und berücksichtigt dabei die linguistischen Regeln jeder Sprache. Sie können Grapheme (vom Benutzer wahrgenommene Zeichen) zählen, Text unabhängig von der Sprache in Wörter aufteilen oder Text in Sätze unterteilen.

Dieser Artikel erklärt, warum grundlegende String-Operationen bei internationalen Texten versagen, was Graphem-Cluster und linguistische Grenzen sind und wie man Intl.Segmenter verwendet, um Text für alle Benutzer korrekt zu verarbeiten.

Warum string.length beim Zeichenzählen versagt

JavaScript-Strings verwenden UTF-16-Kodierung. Jedes Element in einem JavaScript-String ist eine 16-Bit-Code-Unit, nicht ein vollständiges Zeichen. Die Eigenschaft string.length zählt diese Code-Units.

Für einfache ASCII-Zeichen entspricht eine Code-Unit einem Zeichen. Der String "hello" hat eine Länge von 5, was den Benutzererwartungen entspricht.

Für viele andere Zeichen funktioniert dies nicht. Betrachten Sie diese Beispiele:

"😀".length; // 2, nicht 1
"👨‍👩‍👧‍👦".length; // 11, nicht 1
"किं".length; // 5, nicht 2
"🇺🇸".length; // 4, nicht 1

Benutzer sehen ein Emoji, ein Familien-Emoji, zwei Hindi-Silben oder eine Flagge. JavaScript zählt die zugrundeliegenden Code-Units.

Dies ist wichtig, wenn Sie Zeichenzähler für Texteingaben erstellen, Längenbegrenzungen validieren oder Text für die Anzeige kürzen. Die von JavaScript gemeldete Anzahl stimmt nicht mit dem überein, was Benutzer sehen.

Was Graphem-Cluster sind

Ein Graphem-Cluster ist das, was Benutzer als ein einzelnes Zeichen wahrnehmen. Es kann bestehen aus:

  • Einem einzelnen Codepunkt wie "a"
  • Einem Basiszeichen plus kombinierenden Zeichen wie "é" (e + kombinierendes Akutzeichen)
  • Mehreren verbundenen Codepunkten wie "👨‍👩‍👧‍👦" (Mann + Frau + Mädchen + Junge, verbunden mit Nullbreite-Verbindern)
  • Emojis mit Hautton-Modifikatoren wie "👋🏽" (winkende Hand + mittlerer Hautton)
  • Regionale Indikatorsequenzen für Flaggen wie "🇺🇸" (regionaler Indikator U + regionaler Indikator S)

Der Unicode-Standard definiert erweiterte Graphem-Cluster in UAX 29. Diese Regeln bestimmen, wo Benutzer Grenzen zwischen Zeichen erwarten. Wenn ein Benutzer die Rücktaste drückt, erwartet er, dass ein Graphem-Cluster gelöscht wird. Wenn sich ein Cursor bewegt, sollte er sich nach Graphem-Clustern bewegen.

JavaScripts string.length zählt keine Graphem-Cluster. Die Intl.Segmenter-API tut dies.

Zählen von Graphem-Clustern mit Intl.Segmenter

Erstellen Sie einen Segmenter mit Graphem-Granularität, um vom Benutzer wahrgenommene Zeichen zu zählen:

const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
const text = "Hello 👋🏽";
const segments = segmenter.segment(text);
const graphemes = Array.from(segments);

console.log(graphemes.length); // 7
console.log(text.length); // 10

Der Benutzer sieht sieben Zeichen: fünf Buchstaben, ein Leerzeichen und ein Emoji. Der Graphem-Segmenter gibt sieben Segmente zurück. JavaScripts string.length gibt zehn zurück, weil das Emoji vier Codeeinheiten verwendet.

Jedes Segment-Objekt enthält:

  • segment: den Graphem-Cluster als String
  • index: die Position im ursprünglichen String, an der dieses Segment beginnt
  • input: Referenz auf den ursprünglichen String (nicht immer erforderlich)

Sie können mit for...of über Segmente iterieren:

const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
const text = "café";

for (const { segment } of segmenter.segment(text)) {
  console.log(segment);
}
// Gibt aus: "c", "a", "f", "é"

Einen Zeichenzähler erstellen, der international funktioniert

Verwenden Sie Graphem-Segmentierung, um präzise Zeichenzähler zu erstellen:

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

// Test mit verschiedenen Eingaben
getGraphemeCount("hello"); // 5
getGraphemeCount("hello 😀"); // 7
getGraphemeCount("👨‍👩‍👧‍👦"); // 1
getGraphemeCount("किंतु"); // 2
getGraphemeCount("🇺🇸"); // 1

Diese Funktion liefert Zählungen, die der Benutzerwahrnehmung entsprechen. Ein Benutzer, der ein Familien-Emoji eintippt, sieht ein Zeichen, und der Zähler zeigt ein Zeichen an.

Verwenden Sie für die Validierung von Texteingaben Graphem-Zählungen anstelle von string.length:

function validateInput(text, maxGraphemes) {
  const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
  const count = Array.from(segmenter.segment(text)).length;
  return count <= maxGraphemes;
}

Text sicher kürzen mit Graphem-Segmentierung

Beim Kürzen von Text für die Anzeige dürfen Sie nicht durch einen Graphem-Cluster schneiden. Das Schneiden an einem beliebigen Code-Unit-Index kann Emoji oder kombinierende Zeichensequenzen teilen, was zu ungültiger oder fehlerhafter Ausgabe führt.

Verwenden Sie Graphem-Segmentierung, um sichere Kürzungspunkte zu finden:

function truncateText(text, maxGraphemes) {
  const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
  const segments = Array.from(segmenter.segment(text));

  if (segments.length <= maxGraphemes) {
    return text;
  }

  const truncated = segments
    .slice(0, maxGraphemes)
    .map(s => s.segment)
    .join("");

  return truncated + "…";
}

truncateText("Hello 👨‍👩‍👧‍👦 world", 7); // "Hello 👨‍👩‍👧‍👦…"
truncateText("Hello world", 7); // "Hello w…"

Dies bewahrt vollständige Graphem-Cluster und erzeugt gültige Unicode-Ausgabe.

Warum split() und Regex für die Wortsegmentierung versagen

Der übliche Ansatz zur Aufteilung von Text in Wörter verwendet split() mit einem Leerzeichen oder Whitespace-Muster:

const text = "Hello world";
const words = text.split(" "); // ["Hello", "world"]

Dies funktioniert für Englisch und andere Sprachen, die Wörter mit Leerzeichen trennen. Es versagt jedoch vollständig bei Sprachen, die keine Leerzeichen zwischen Wörtern verwenden.

Chinesischer, japanischer und thailändischer Text enthält keine Leerzeichen zwischen Wörtern. Die Aufteilung nach Leerzeichen gibt die gesamte Zeichenfolge als ein Element zurück:

const text = "你好世界"; // "Hello world" auf Chinesisch
const words = text.split(" "); // ["你好世界"]

Der Benutzer sieht vier verschiedene Wörter, aber split() gibt ein Element zurück.

Regulären Ausdrücken mit Wortgrenzen (\b) schlagen ebenfalls für diese Sprachen fehl, da die Regex-Engine keine Wortgrenzen in Schriften ohne Leerzeichen erkennt.

Wie die Wortsegmentierung in verschiedenen Sprachen funktioniert

Die Intl.Segmenter-API verwendet Unicode-Wortgrenzenregeln, die in UAX 29 definiert sind. Diese Regeln verstehen Wortgrenzen für alle Schriftsysteme, einschließlich solcher ohne Leerzeichen.

Erstellen Sie einen Segmenter mit Wortgranularität:

const segmenter = new Intl.Segmenter("zh", { granularity: "word" });
const text = "你好世界";
const segments = Array.from(segmenter.segment(text));

segments.forEach(({ segment, isWordLike }) => {
  console.log(segment, isWordLike);
});
// "你好" true
// "世界" true

Der Segmenter identifiziert Wortgrenzen korrekt basierend auf dem Gebietsschema und der Schrift. Die Eigenschaft isWordLike gibt an, ob das Segment ein Wort (Buchstaben, Zahlen, Ideogramme) oder ein Nicht-Wort-Inhalt (Leerzeichen, Interpunktion) ist.

Für englischen Text gibt der Segmenter sowohl Wörter als auch Leerzeichen zurück:

const segmenter = new Intl.Segmenter("en", { granularity: "word" });
const text = "Hello world!";
const segments = Array.from(segmenter.segment(text));

segments.forEach(({ segment, isWordLike }) => {
  console.log(segment, isWordLike);
});
// "Hello" true
// " " false
// "world" true
// "!" false

Verwenden Sie die Eigenschaft isWordLike, um Wortsegmente von Interpunktion und Leerzeichen zu filtern:

function getWords(text, locale) {
  const segmenter = new Intl.Segmenter(locale, { granularity: "word" });
  const segments = segmenter.segment(text);
  return Array.from(segments)
    .filter(s => s.isWordLike)
    .map(s => s.segment);
}

getWords("Hello, world!", "en"); // ["Hello", "world"]
getWords("你好世界", "zh"); // ["你好", "世界"]
getWords("สวัสดีครับ", "th"); // ["สวัสดี", "ครับ"] (Thai)

Diese Funktion funktioniert für jede Sprache und behandelt sowohl durch Leerzeichen getrennte als auch nicht durch Leerzeichen getrennte Schriften.

Wörter präzise zählen

Erstellen Sie einen Wortzähler, der international funktioniert:

function countWords(text, locale) {
  const segmenter = new Intl.Segmenter(locale, { granularity: "word" });
  const segments = segmenter.segment(text);
  return Array.from(segments).filter(s => s.isWordLike).length;
}

countWords("Hello world", "en"); // 2
countWords("你好世界", "zh"); // 2
countWords("Bonjour le monde", "fr"); // 3

Dies erzeugt genaue Wortzählungen für Inhalte in jeder Sprache.

Finden, welches Wort eine Cursorposition enthält

Die Methode containing() findet das Segment, das einen bestimmten Index im String enthält. Dies ist nützlich, um zu bestimmen, in welchem Wort sich der Cursor befindet oder welches Segment eine Klickposition enthält.

const segmenter = new Intl.Segmenter("en", { granularity: "word" });
const text = "Hello world";
const segments = segmenter.segment(text);

const segment = segments.containing(7); // Index 7 ist in "world"
console.log(segment);
// { segment: "world", index: 6, isWordLike: true }

Wenn der Index innerhalb eines Leerzeichens oder Satzzeichens liegt, gibt containing() dieses Segment zurück:

const segment = segments.containing(5); // Index 5 ist das Leerzeichen
console.log(segment);
// { segment: " ", index: 5, isWordLike: false }

Verwenden Sie dies für Textbearbeitungsfunktionen, Suchhervorhebungen oder kontextbezogene Aktionen basierend auf der Cursorposition.

Segmentierung von Sätzen für die Textverarbeitung

Die Satzsegmentierung teilt Text an Satzgrenzen. Dies ist nützlich für Zusammenfassungen, Text-zu-Sprache-Verarbeitung oder Navigation in langen Dokumenten.

Einfache Ansätze wie das Aufteilen bei Punkten scheitern, da Punkte in Abkürzungen, Zahlen und anderen Kontexten vorkommen, die keine Satzgrenzen darstellen:

const text = "Dr. Smith bought 100.5 shares. He sold them later.";
text.split(". "); // Falsch: bricht bei "Dr." und "100." ab

Die Intl.Segmenter-API versteht die Regeln für Satzgrenzen:

const segmenter = new Intl.Segmenter("en", { granularity: "sentence" });
const text = "Dr. Smith bought 100.5 shares. He sold them later.";
const segments = Array.from(segmenter.segment(text));

segments.forEach(({ segment }) => {
  console.log(segment);
});
// "Dr. Smith bought 100.5 shares. "
// "He sold them later."

Der Segmenter behandelt "Dr." und "100.5" korrekt als Teil des Satzes, nicht als Satzgrenzen.

Bei mehrsprachigem Text variieren die Satzgrenzen je nach Gebietsschema. Die API behandelt diese Unterschiede:

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

const textEn = "Hello. How are you?";
const textJa = "こんにちは。お元気ですか。"; // Verwendet japanischen Punkt

Array.from(segmenterEn.segment(textEn)).length; // 2
Array.from(segmenterJa.segment(textJa)).length; // 2

Wann welche Granularität zu verwenden ist

Wählen Sie die Granularität basierend auf dem, was Sie zählen oder aufteilen möchten:

  • Graphem: Verwenden Sie dies für Zeichenzählung, Textkürzung, Cursorpositionierung oder jede Operation, bei der Sie die Benutzerwahrnehmung von Zeichen berücksichtigen müssen.

  • Wort: Verwenden Sie dies für Wortzählung, Suche und Hervorhebung, Textanalyse oder jede Operation, die sprachliche Wortgrenzen über verschiedene Sprachen hinweg benötigt.

  • Satz: Verwenden Sie dies für Text-zu-Sprache-Segmentierung, Zusammenfassung, Dokumentnavigation oder jede Operation, die Text satzweise verarbeitet.

Verwenden Sie keine Graphem-Segmentierung, wenn Sie Wortgrenzen benötigen, und keine Wort-Segmentierung, wenn Sie Zeichenzählungen benötigen. Jede Granularität dient einem bestimmten Zweck.

Erstellen und Wiederverwenden von Segmentern

Das Erstellen eines Segmenters ist kostengünstig, aber Sie können Segmenter für bessere Leistung wiederverwenden:

const graphemeSegmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
const wordSegmenter = new Intl.Segmenter("en", { granularity: "word" });

// Verwenden Sie diese Segmenter für mehrere Strings wieder
function processTexts(texts) {
  return texts.map(text => ({
    text,
    graphemes: Array.from(graphemeSegmenter.segment(text)).length,
    words: Array.from(wordSegmenter.segment(text)).filter(s => s.isWordLike).length
  }));
}

Der Segmenter speichert Locale-Daten im Cache, sodass die Wiederverwendung derselben Instanz wiederholte Initialisierungen vermeidet.

Überprüfung der Browser-Unterstützung

Die Intl.Segmenter-API hat im April 2024 den Baseline-Status erreicht. Sie funktioniert in aktuellen Versionen von Chrome, Firefox, Safari und Edge. Ältere Browser unterstützen sie nicht.

Überprüfen Sie die Unterstützung vor der Verwendung:

if (typeof Intl.Segmenter !== "undefined") {
  // Verwenden Sie Intl.Segmenter
  const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
  // ...
} else {
  // Fallback für ältere Browser
  const count = text.length; // Nicht genau, aber verfügbar
}

Für Produktionsanwendungen, die auf ältere Browser abzielen, sollten Sie die Verwendung eines Polyfills oder die Bereitstellung eingeschränkter Funktionalität in Betracht ziehen.

Häufige Fehler, die vermieden werden sollten

Verwenden Sie nicht string.length zur Anzeige von Zeichenanzahlen für Benutzer. Es liefert falsche Ergebnisse für Emojis, kombinierende Zeichen und komplexe Schriften.

Splittern Sie nicht nach Leerzeichen oder verwenden Sie keine Regex-Wortgrenzen für mehrsprachige Wortsegmentierung. Diese Ansätze funktionieren nur für eine Teilmenge von Sprachen.

Nehmen Sie nicht an, dass Wort- oder Satzgrenzen in allen Sprachen gleich sind. Verwenden Sie lokalisierungsbewusste Segmentierung.

Vergessen Sie nicht, die Eigenschaft isWordLike bei der Wortzählung zu überprüfen. Das Einbeziehen von Interpunktion und Leerzeichen führt zu überhöhten Zählungen.

Schneiden Sie Strings nicht an beliebigen Indizes ab, wenn Sie kürzen. Schneiden Sie immer an Graphem-Cluster-Grenzen, um ungültige Unicode-Sequenzen zu vermeiden.

Wann Intl.Segmenter nicht verwendet werden sollte

Für einfache ASCII-only Operationen, bei denen Sie wissen, dass der Text nur grundlegende lateinische Zeichen enthält, sind einfache String-Methoden schneller und ausreichend.

Wenn Sie die Byte-Länge eines Strings für Netzwerkoperationen oder Speicherung benötigen, verwenden Sie TextEncoder:

const byteLength = new TextEncoder().encode(text).length;

Wenn Sie die tatsächliche Code-Unit-Anzahl für Low-Level-String-Manipulation benötigen, ist string.length korrekt. Dies ist in Anwendungscode selten.

Für die meisten Textverarbeitungen, die benutzerorientierten Inhalt betreffen, insbesondere in internationalen Anwendungen, verwenden Sie Intl.Segmenter.

Wie Intl.Segmenter mit anderen Internationalisierungs-APIs zusammenhängt

Die Intl.Segmenter-API ist Teil der ECMAScript-Internationalisierungs-API. Andere APIs in dieser Familie umfassen:

  • Intl.DateTimeFormat: Formatiert Datum und Uhrzeit entsprechend der Locale
  • Intl.NumberFormat: Formatiert Zahlen, Währungen und Einheiten entsprechend der Locale
  • Intl.Collator: Sortiert und vergleicht Strings entsprechend der Locale
  • Intl.PluralRules: Bestimmt Pluralformen für Zahlen in verschiedenen Sprachen

Zusammen bieten diese APIs die notwendigen Werkzeuge, um Anwendungen zu erstellen, die für Benutzer weltweit korrekt funktionieren. Verwenden Sie Intl.Segmenter für die Textsegmentierung und die anderen Intl-APIs für Formatierung und Vergleich.

Praktisches Beispiel: Entwicklung einer Textstatistik-Komponente

Kombinieren Sie Graphem- und Wortsegmentierung, um eine Textstatistik-Komponente zu erstellen:

function getTextStatistics(text, locale) {
  const graphemeSegmenter = new Intl.Segmenter(locale, {
    granularity: "grapheme"
  });
  const wordSegmenter = new Intl.Segmenter(locale, {
    granularity: "word"
  });
  const sentenceSegmenter = new Intl.Segmenter(locale, {
    granularity: "sentence"
  });

  const graphemes = Array.from(graphemeSegmenter.segment(text));
  const words = Array.from(wordSegmenter.segment(text))
    .filter(s => s.isWordLike);
  const sentences = Array.from(sentenceSegmenter.segment(text));

  return {
    characters: graphemes.length,
    words: words.length,
    sentences: sentences.length,
    averageWordLength: words.length > 0
      ? graphemes.length / words.length
      : 0
  };
}

// Funktioniert für jede Sprache
getTextStatistics("Hello world! How are you?", "en");
// { characters: 24, words: 5, sentences: 2, averageWordLength: 4.8 }

getTextStatistics("你好世界!你好吗?", "zh");
// { characters: 9, words: 5, sentences: 2, averageWordLength: 1.8 }

Diese Funktion erzeugt aussagekräftige Statistiken für Texte in jeder Sprache und verwendet dabei die korrekten Segmentierungsregeln für jede Locale.