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.
Inhaltsverzeichnis
- 1. Warum String.split() fuer Textsegmentierung nicht ausreicht
- 2. Grundlagen: Intl.Segmenter instanziieren und nutzen
- 3. Grapheme-Cluster: Emoji und Akzente korrekt zaehlen
- 4. Wortgrenzen in CJK-Sprachen ohne Leerzeichen
- 5. Satzsegmentierung: mehr als nur am Punkt trennen
- 6. Direkter Vergleich: Intl.Segmenter versus String.split()
- 7. Performance-Ueberlegungen und praktische Einsatzmuster
- 8. Browser-Support und Vergleich mit dem Status quo
- 9. Referenztabelle: Granularitaeten im Vergleich
- 10. Zusammenfassung
- 11. FAQ
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.