Intl.Segmenter: Text korrekt in Woerter, Saetze und Grapheme-Cluster zerlegen
AI generated
JS
() =>
JavaScript · Internationalisierung · Textverarbeitung
Text richtig zerlegen, nicht nur am Leerzeichen:
Intl.Segmenter fuer Woerter, Saetze und Grapheme

String.split(' ') scheitert an Emoji, chinesischen Zeichen und zusammengesetzten Zeichen. Intl.Segmenter zerlegt Text sprachbewusst und regelkonform, ganz ohne externe Bibliothek.

16 Min. Lesezeit Intl API Unicode Internationalisierung

1. Warum String.split() fuer Textsegmentierung nicht ausreicht

Die naheliegende Loesung, einen Text in Woerter zu zerlegen, ist text.split(' '). Fuer einfachen englischen oder deutschen Fliesstext funktioniert das oberflaechlich, scheitert aber sofort an mehreren realen Anforderungen: Satzzeichen bleiben am Wort haengen ('Hallo,' statt 'Hallo'), mehrfache Leerzeichen erzeugen leere Eintraege, und vor allem: viele Sprachen der Welt trennen Woerter ueberhaupt nicht durch Leerzeichen. Chinesisch, Japanisch und Thai schreiben Woerter direkt aneinander, ein Leerzeichen-Split liefert dort schlicht den gesamten Satz als ein einziges 'Wort' zurueck.

Noch subtiler ist das Problem bei der Zeichen-fuer-Zeichen-Iteration mittels [...text] oder einer for...of-Schleife. Diese respektiert zwar Surrogatpaare korrekt und zerlegt Emoji wie ???? nicht in kaputte Haelften, versagt aber bei zusammengesetzten Emoji wie Familien-Emoji mit Zero-Width-Joinern (????‍????‍????‍????) oder bei Buchstaben mit kombinierenden Akzentzeichen: was fuer den Menschen ein einzelnes visuelles Zeichen ist, ein sogenanntes Grapheme-Cluster, kann aus mehreren Unicode-Codepunkten bestehen, die eine naive Iteration faelschlicherweise als separate 'Zeichen' zaehlt.

2. Grundlagen: Intl.Segmenter instanziieren und nutzen

Intl.Segmenter ist Teil der ECMA-402-Internationalisierungs-APIs und wird mit einem Locale-String sowie einem Optionsobjekt instanziiert, das ueber die granularity-Option festlegt, auf welcher Ebene segmentiert werden soll: 'grapheme' fuer visuelle Zeichen-Cluster, 'word' fuer Woerter, oder 'sentence' fuer Saetze. Die Methode .segment(text) gibt dann ein iterierbares Segments-Objekt zurueck, dessen einzelne Eintraege neben dem eigentlichen Textsegment auch Metainformationen wie die Startposition und, bei Wortsegmentierung, ein isWordLike-Flag enthalten, das zwischen echten Woertern und reinen Trennzeichen wie Leerzeichen oder Satzzeichen unterscheidet.

Die entscheidende Staerke gegenueber einer selbst geschriebenen Loesung ist, dass Intl.Segmenter den vollen Unicode Text Segmentation Algorithm (UAX #29) implementiert, ein vom Unicode-Konsortium spezifiziertes, sprachuebergreifendes Regelwerk, das kontinuierlich mit neuen Unicode-Versionen aktualisiert wird. Diese Regeln zu implementieren wuerde in eigenem Code Hunderte von Sonderfaellen erfordern, von denen die wenigsten Entwickler ueberhaupt wissen, dass sie existieren.


const segmenter = new Intl.Segmenter("de", { granularity: "word" });
const text = "Hallo, wie geht es dir?";

for (const { segment, isWordLike } of segmenter.segment(text)) {
  console.log(`"${segment}" -- Wort: ${isWordLike}`);
}
// "Hallo" -- Wort: true
// "," -- Wort: false
// " " -- Wort: false
// "wie" -- Wort: true
// ...

// Nur echte Woerter herausfiltern
const nurWoerter = [...segmenter.segment(text)]
  .filter(s => s.isWordLike)
  .map(s => s.segment);
console.log(nurWoerter); // ["Hallo", "wie", "geht", "es", "dir"]

3. Grapheme-Cluster: Emoji und Akzente korrekt zaehlen

Mit granularity: 'grapheme' zerlegt Intl.Segmenter Text in genau die Einheiten, die ein Mensch intuitiv als 'ein Zeichen' wahrnimmt. Ein zusammengesetztes Familien-Emoji, technisch aus vier einzelnen Personen-Emoji und drei Zero-Width-Joinern bestehend, wird korrekt als ein einziges Grapheme-Cluster erkannt, waehrend eine naive [...text]-Iteration es faelschlich in sieben separate 'Zeichen' zerlegen wuerde. Dasselbe gilt fuer Buchstaben mit kombinierenden diakritischen Zeichen, etwa ein 'e' gefolgt von einem eigenstaendigen Akut-Akzent-Codepunkt statt eines vorkomponierten 'é'.

Diese korrekte Zeichenzaehlung ist keine akademische Feinheit, sondern hat direkte praktische Konsequenzen: Ein Zeichenlimit fuer ein Kommentarfeld, das mittels text.length oder naiver Codepunkt-Iteration berechnet wird, kann ein einzelnes komplexes Emoji als fuenf oder sieben Zeichen zaehlen und damit ein fuer den Nutzer voellig unverstaendliches Limit erzeugen. Mit Grapheme-Segmentierung entspricht die gezaehlte Anzahl exakt dem, was der Nutzer auf dem Bildschirm sieht.


const familie = "????‍????‍????‍????"; // ein visuelles Zeichen, technisch 7 Codepunkte

console.log([...familie].length); // 7 -- naive Iteration zaehlt falsch
console.log(familie.length);       // 11 -- .length zaehlt UTF-16-Einheiten, noch falscher

const graphemeSegmenter = new Intl.Segmenter("de", { granularity: "grapheme" });
const graphemes = [...graphemeSegmenter.segment(familie)];
console.log(graphemes.length); // 1 -- korrekt: EIN Grapheme-Cluster

// Praktisch fuer ein korrektes Zeichenlimit
function korrekteZeichenanzahl(text) {
  return [...graphemeSegmenter.segment(text)].length;
}
console.log(korrekteZeichenanzahl("Hallo ????‍????‍????‍????!")); // 8 statt 17

4. Wortgrenzen in CJK-Sprachen ohne Leerzeichen

Chinesisch und Japanisch schreiben zusammenhaengenden Text vollstaendig ohne Leerzeichen zwischen den Woertern, ein Leerzeichen-Split liefert bei diesen Sprachen keinerlei sinnvolle Segmentierung. Intl.Segmenter loest dieses Problem mithilfe eines Woerterbuch-basierten Algorithmus, der fuer die jeweilige Locale die wahrscheinlichsten Wortgrenzen innerhalb der zusammenhaengenden Zeichenkette bestimmt, basierend auf sprachspezifischen Haeufigkeits- und Regelmodellen, die im ICU-Datensatz (International Components for Unicode) hinterlegt sind, auf dem die meisten JavaScript-Engines aufbauen.

Fuer Anwendungen mit Textsuche, Autovervollstaendigung oder Textumbruch in Anzeigen mit chinesischem oder japanischem Content ist das essenziell: ohne korrekte Wortgrenzenerkennung koennte eine Suchfunktion nicht zwischen zwei benachbarten, aber inhaltlich unabhaengigen Woertern unterscheiden, und ein automatischer Zeilenumbruch wuerde mitten in einem zusammengehoerigen Wort umbrechen, was fuer muttersprachliche Leser sofort als Fehler auffaellt.


const chinesischerText = "我喜欢学习编程语言";

// Leerzeichen-Split funktioniert hier ueberhaupt nicht
console.log(chinesischerText.split(" ")); // ["我喜欢学习编程语言"] -- ein einziges "Wort"

const zhSegmenter = new Intl.Segmenter("zh", { granularity: "word" });
const woerter = [...zhSegmenter.segment(chinesischerText)]
  .filter(s => s.isWordLike)
  .map(s => s.segment);

console.log(woerter); // ["我", "喜欢", "学习", "编程", "语言"]
// ich, mag, lernen, Programmier(en), Sprache -- korrekt in Woerter zerlegt

5. Satzsegmentierung: mehr als nur am Punkt trennen

Mit granularity: 'sentence' zerlegt Intl.Segmenter Text in einzelne Saetze, und zwar deutlich robuster als ein naives Splitten am Punktzeichen. Ein einfacher text.split('.')-Ansatz zerbricht bei Abkuerzungen wie 'z.B.', Dezimalzahlen wie '3.14' oder Titeln wie 'Dr.', da diese Punkte enthalten, ohne einen Satzabschluss zu markieren. Die Satzsegmentierung von Intl.Segmenter beruecksichtigt genau solche sprachspezifischen Ausnahmen ueber den vollen UAX-29-Regelsatz.

Praktisch relevant ist das etwa fuer Text-Vorschau-Snippets, die nur den ersten vollstaendigen Satz anzeigen sollen, fuer Sprachlern-Anwendungen, die Text satzweise zum Ueben aufteilen, oder fuer Text-to-Speech-Vorverarbeitung, bei der jeder Satz einzeln an eine Sprachsynthese-API uebergeben wird und eine falsche Satzgrenze zu einer unnatuerlichen Sprechpause mitten im Satz fuehren wuerde.


const text = "Dr. Müller kam um 9.30 Uhr. Er hatte 3.5 kg Unterlagen dabei. Alles war ok.";

const sentenceSegmenter = new Intl.Segmenter("de", { granularity: "sentence" });
const saetze = [...sentenceSegmenter.segment(text)].map(s => s.segment.trim());

console.log(saetze);
// ["Dr. Müller kam um 9.30 Uhr.", "Er hatte 3.5 kg Unterlagen dabei.", "Alles war ok."]
// -- "Dr." und "9.30" wurden korrekt NICHT als Satzende erkannt

6. Direkter Vergleich: Intl.Segmenter versus String.split()

Der Unterschied zwischen beiden Ansaetzen laesst sich am deutlichsten mit einem Text zeigen, der mehrere Problemfaelle gleichzeitig kombiniert: Emoji, ein CJK-Wort ohne umgebende Leerzeichen und eine Abkuerzung mit Punkt. Waehrend split(' ') nur an tatsaechlich vorhandenen Leerzeichen trennt und alles andere ignoriert, liefert Intl.Segmenter in jedem Fall linguistisch sinnvolle Grenzen, unabhaengig davon, ob die jeweilige Sprache ueberhaupt Leerzeichen zur Worttrennung verwendet.

Ein weiterer praktischer Unterschied: split() verwirft die Trennzeichen selbst, waehrend Intl.Segmenter bei Wortsegmentierung auch die Trennzeichen (Leerzeichen, Satzzeichen) als eigene Segmente mit isWordLike: false zurueckgibt. Das ermoeglicht es, einen Text vollstaendig verlustfrei zu segmentieren und bei Bedarf, etwa fuer Syntax-Highlighting oder Text-Markup, wieder exakt in der Originalform zusammenzusetzen.

7. Performance-Ueberlegungen und praktische Einsatzmuster

Die Instanziierung eines Intl.Segmenter-Objekts ist im Vergleich zum eigentlichen Segmentieren relativ teuer, da dabei Locale-Daten geladen und Regelwerke vorbereitet werden. Fuer wiederholte Segmentierung im selben Locale und derselben Granularitaet sollte daher immer eine einzige Segmenter-Instanz wiederverwendet werden, statt bei jedem Aufruf einen neuen Segmenter zu erzeugen, ganz aehnlich wie bei anderen Intl-APIs wie Intl.NumberFormat oder Intl.DateTimeFormat.

Fuer sehr performancekritische Anwendungsfaelle mit extrem hohem Durchsatz, etwa Echtzeit-Textanalyse ueber Millionen Zeichen pro Sekunde, lohnt sich ein Benchmark gegen spezialisierte native Bibliotheken. Fuer die weit ueberwiegende Mehrheit typischer Anwendungsfaelle, Formularvalidierung, Textvorschau, Zeichenlimit-Berechnung, Suchindexierung, ist die native Performance von Intl.Segmenter jedoch mehr als ausreichend und erspart die zusaetzliche Bundle-Groesse einer externen Bibliothek vollstaendig.


// Segmenter einmal erstellen und wiederverwenden
const wordSegmenter = new Intl.Segmenter(undefined, { granularity: "word" });

function zaehleWoerter(text) {
  let anzahl = 0;
  for (const { isWordLike } of wordSegmenter.segment(text)) {
    if (isWordLike) anzahl++;
  }
  return anzahl;
}

console.log(zaehleWoerter("Das ist ein Test-Satz, oder?")); // 5

8. Browser-Support und Vergleich mit dem Status quo

Intl.Segmenter wird von allen aktuellen Versionen von Chrome, Edge, Safari und Node.js ab Version 16 nativ unterstuetzt, Firefox zog mit Version 125 nach. Fuer Projekte, die noch aeltere Firefox-Versionen oder sehr alte Node-LTS-Releases bedienen muessen, existieren JavaScript-Implementierungen wie intl-segmenter-polyfill, die dieselbe API-Oberflaeche mit eigens generierten Unicode-Regeltabellen nachbilden, allerdings mit spuerbar groesserer Bundle-Groesse als die native Loesung.

Vor Intl.Segmenter mussten Projekte, die korrekte Grapheme-Zaehlung oder CJK-Wortsegmentierung benoetigten, auf externe Bibliotheken wie grapheme-splitter oder segmentit zurueckgreifen, die zusaetzliches Bundle-Gewicht mitbrachten und oft nur einen Teilbereich der vollen UAX-29-Spezifikation abdeckten. Die native Loesung deckt alle drei Granularitaeten (Grapheme, Wort, Satz) in einer einzigen, in jedem modernen Browser bereits vorhandenen API ab.

9. Referenztabelle: Granularitaeten im Vergleich

Die folgende Tabelle fasst die drei Granularitaetsstufen von Intl.Segmenter zusammen und ordnet sie jeweils einem typischen Anwendungsfall zu, um die Auswahl der richtigen Option fuer das eigene Projekt zu erleichtern.

Als Faustregel: 'grapheme' immer dann, wenn es um korrektes Zaehlen oder Cursor-Bewegung im Text geht, 'word' fuer Suchindizierung und Textanalyse insbesondere bei mehrsprachigen Inhalten, und 'sentence' fuer Vorschau-Snippets, Sprachausgabe-Vorverarbeitung oder Lernmaterial-Aufbereitung.

Granularitaet Segmentiert nach Typischer Anwendungsfall Loest Problem von
grapheme Visuellen Zeichen-Clustern Zeichenlimit, Cursor-Navigation [...text] bei Emoji
word Woertern (locale-abhaengig) Suchindizierung, Textanalyse split(' ') bei CJK-Sprachen
sentence Saetzen (UAX-29-Regeln) Vorschau-Snippets, TTS-Vorverarbeitung split('.') bei Abkuerzungen
Intl.Segmenter allgemein Alle drei ueber eine API Ersetzt externe Bibliotheken Fehlende Unicode-Konformitaet

Mironsoft

Moderne Browser-APIs, Performance und wartbares JavaScript

JavaScript, das im echten Browser robust bleibt, nicht nur im Tutorial?

Wir prüfen bestehenden Frontend-Code auf veraltete Patterns, unnötige Bibliotheken und Performance-Fallen und ersetzen sie durch moderne, native Browser-APIs, die weniger Bundle-Gewicht und weniger Wartungslast bedeuten.

Code-Review

Veraltete Patterns, unnötige Dependencies und Memory Leaks systematisch aufspüren.

Performance-Optimierung

Bundle-Größe, Ladezeit und Runtime-Performance mit modernen APIs verbessern.

Modernisierung

Native Browser-APIs statt schwerer Bibliotheken gezielt einführen.

10. Zusammenfassung

Intl.Segmenter auf einen Blick

grapheme

Zerlegt Text in visuelle Zeichen-Cluster, zaehlt zusammengesetzte Emoji korrekt als ein Zeichen statt mehrere Codepunkte.

word

Findet Wortgrenzen locale-bewusst, funktioniert auch bei CJK-Sprachen ohne Leerzeichen zwischen Woertern.

sentence

Erkennt Satzgrenzen unter Beruecksichtigung von Abkuerzungen und Dezimalzahlen, robuster als ein Punkt-Split.

Vorteil gegenueber split()

Voll Unicode-konform nach UAX #29, keine externe Bibliothek noetig, native Browser- und Node-Unterstuetzung.

11. FAQ: Intl.Segmenter auf einen Blick

1Warum liefert [...text].length ein falsches Ergebnis bei Emoji?
Manche Emoji bestehen aus mehreren Unicode-Codepunkten, die per Zero-Width-Joiner zu einem einzigen visuellen Zeichen kombiniert werden. Eine einfache Codepunkt-Iteration zaehlt jeden Codepunkt einzeln statt das zusammengesetzte Zeichen als Ganzes.
2Muss ich fuer jede Segmentierung eine neue Intl.Segmenter-Instanz erzeugen?
Nein, im Gegenteil: Die Instanziierung ist vergleichsweise teuer, daher sollte eine Segmenter-Instanz pro Locale und Granularitaet erstellt und fuer alle folgenden Aufrufe wiederverwendet werden.
3Funktioniert Intl.Segmenter ohne Angabe eines Locale-Strings?
Ja, wird undefined uebergeben, verwendet Intl.Segmenter das Standard-Locale der Laufzeitumgebung. Fuer sprachabhaengige Ergebnisse wie CJK-Wortsegmentierung sollte das Locale aber explizit gesetzt werden.
4Was bedeutet das isWordLike-Flag bei Wortsegmentierung genau?
Es unterscheidet echte Woerter von reinen Trennzeichen wie Leerzeichen oder Satzzeichen. Nur Segmente mit isWordLike: true sind tatsaechliche Woerter im linguistischen Sinn.
5Kann Intl.Segmenter auch fuer Thai oder andere Sprachen ohne Leerzeichen genutzt werden?
Ja, Thai gehoert wie Chinesisch und Japanisch zu den Sprachen ohne Leerzeichen zwischen Woertern und wird ebenfalls ueber woerterbuchbasierte Algorithmen im ICU-Datensatz korrekt segmentiert.
6Ist Intl.Segmenter langsamer als ein einfaches String.split()?
Fuer die reine Ausfuehrung ja, geringfuegig, da mehr linguistische Logik beteiligt ist. Bei Wiederverwendung derselben Segmenter-Instanz ist der Unterschied fuer die meisten Anwendungsfaelle jedoch vernachlaessigbar.
7Ersetzt Intl.Segmenter Bibliotheken wie grapheme-splitter vollstaendig?
Fuer die allermeisten Anwendungsfaelle ja, da die native API dieselbe UAX-29-Konformitaet bietet, ohne zusaetzliches Bundle-Gewicht. Nur bei sehr spezialisierten Sonderanforderungen kann eine externe Bibliothek noch Mehrwert bieten.
8Wie gehe ich mit aelteren Browsern um, die Intl.Segmenter nicht unterstuetzen?
Ein Polyfill wie intl-segmenter-polyfill bildet dieselbe API nach, bringt aber ein groesseres Bundle mit. Ein Feature-Check vor dem Laden des Polyfills spart in modernen Umgebungen unnoetigen Overhead.
9Kann ich mit Intl.Segmenter auch die Position eines Segments im Originaltext ermitteln?
Ja, jedes Segment-Objekt enthaelt zusaetzlich zum Textinhalt eine index-Eigenschaft mit der Startposition im Originaltext, was praezises Highlighting oder Cursor-Positionierung ermoeglicht.
10Ist Intl.Segmenter Teil des ECMAScript-Standards oder einer separaten Spezifikation?
Intl.Segmenter ist Teil von ECMA-402, der Internationalisierungs-API-Spezifikation, die parallel zum eigentlichen ECMAScript-Sprachstandard (ECMA-262) gepflegt wird, aber ebenfalls von allen grossen JavaScript-Engines implementiert wird.