Intl.Segmenter API
Wie man Zeichen zählt, Wörter trennt und Sätze in JavaScript korrekt segmentiert
Einführung
Die string.length-Eigenschaft von JavaScript zählt Code-Einheiten, nicht vom Benutzer wahrgenommene Zeichen. Wenn Benutzer Emojis, Zeichen mit Akzenten oder Text in komplexen Schriftsystemen eingeben, gibt string.length die falsche Anzahl zurück. Die split()-Methode versagt bei Sprachen, die keine Leerzeichen zwischen Wörtern verwenden. Wortgrenzen in regulären Ausdrücken 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 respektiert 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 internationalem Text versagen, was Graphem-Cluster und linguistische Grenzen sind und wie Sie Intl.Segmenter verwenden, 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-Einheit, kein vollständiges Zeichen. Die string.length-Eigenschaft zählt diese Code-Einheiten.
Bei einfachen ASCII-Zeichen entspricht eine Code-Einheit einem Zeichen. Der String "hello" hat eine Länge von 5, was den Erwartungen der Benutzer entspricht.
Bei vielen anderen Zeichen funktioniert dies nicht mehr. Betrachten Sie diese Beispiele:
"😀".length; // 2, not 1
"👨👩👧👦".length; // 11, not 1
"किं".length; // 5, not 2
"🇺🇸".length; // 4, not 1
Benutzer sehen ein Emoji, ein Familien-Emoji, zwei Hindi-Silben oder eine Flagge. JavaScript zählt die zugrunde liegenden Code-Einheiten.
Dies ist wichtig, wenn Sie Zeichenzähler für Texteingaben erstellen, Längenbeschränkungen 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 einzelnes Zeichen wahrnehmen. Es kann bestehen aus:
- Einem einzelnen Code-Point wie
"a" - Einem Basiszeichen plus kombinierenden Zeichen wie
"é"(e + kombinierender Akut-Akzent) - Mehreren Code-Points, die zusammengefügt sind, wie
"👨👩👧👦"(Mann + Frau + Mädchen + Junge, verbunden mit Zero-Width-Joinern) - Emoji mit Hautton-Modifikatoren wie
"👋🏽"(winkende Hand + mittlerer Hautton) - Regional-Indikator-Sequenzen für Flaggen wie
"🇺🇸"(Regional-Indikator U + Regional-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, ein Graphem-Cluster zu löschen. Wenn sich ein Cursor bewegt, sollte er sich um Graphem-Cluster bewegen.
JavaScripts string.length zählt keine Graphem-Cluster. Die Intl.Segmenter-API tut dies.
Graphem-Cluster zählen 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, da das Emoji vier Code-Units verwendet.
Jedes Segment-Objekt enthält:
segment: das Graphem-Cluster als Stringindex: die Position im ursprünglichen String, an der dieses Segment beginntinput: Referenz auf den ursprünglichen String (nicht immer erforderlich)
Sie können über Segmente mit for...of iterieren:
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
const text = "café";
for (const { segment } of segmenter.segment(text)) {
console.log(segment);
}
// Logs: "c", "a", "f", "é"
Einen international funktionierenden Zeichenzähler erstellen
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 with various inputs
getGraphemeCount("hello"); // 5
getGraphemeCount("hello 😀"); // 7
getGraphemeCount("👨👩👧👦"); // 1
getGraphemeCount("किंतु"); // 2
getGraphemeCount("🇺🇸"); // 1
Diese Funktion liefert Zählwerte, die der Benutzerwahrnehmung entsprechen. Ein Benutzer, der ein Familien-Emoji eingibt, 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 mit Graphem-Segmentierung kürzen
Beim Kürzen von Text für die Anzeige dürfen Sie nicht mitten durch einen Graphem-Cluster schneiden. Das Schneiden an einem beliebigen Code-Unit-Index kann Emoji- oder Combining-Character-Sequenzen trennen und ungültige oder fehlerhafte Ausgaben erzeugen.
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-Ausgaben.
Warum split() und Regex bei der Wort-Segmentierung versagen
Der gängige Ansatz zum Aufteilen 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 durch Leerzeichen trennen. Es versagt vollständig bei Sprachen, die keine Leerzeichen zwischen Wörtern verwenden.
Chinesischer, japanischer und thailändischer Text enthält keine Leerzeichen zwischen Wörtern. Das Aufteilen nach Leerzeichen gibt die gesamte Zeichenkette als ein Element zurück:
const text = "你好世界"; // "Hello world" in Chinese
const words = text.split(" "); // ["你好世界"]
Der Benutzer sieht vier unterschiedliche Wörter, aber split() gibt ein Element zurück.
Reguläre Ausdrücke für Wortgrenzen (\b) versagen ebenfalls bei diesen Sprachen, da die Regex-Engine keine Wortgrenzen in Schriftsystemen ohne Leerzeichen erkennt.
Wie Wort-Segmentierung sprachübergreifend 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 isWordLike-Eigenschaft gibt an, ob das Segment ein Wort (Buchstaben, Zahlen, Ideogramme) oder 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 isWordLike-Eigenschaft, 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 verarbeitet 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 liefert präzise Wortzählungen für Inhalte in jeder Sprache.
Ermitteln, welches Wort eine Cursorposition enthält
Die containing()-Methode findet das Segment, das einen bestimmten Index in der Zeichenkette 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 is in "world"
console.log(segment);
// { segment: "world", index: 6, isWordLike: true }
Wenn sich der Index innerhalb von Leerzeichen oder Interpunktion befindet, gibt containing() dieses Segment zurück:
const segment = segments.containing(5); // Index 5 is the space
console.log(segment);
// { segment: " ", index: 5, isWordLike: false }
Verwenden Sie dies für Textbearbeitungsfunktionen, Suchhervorhebung oder kontextbezogene Aktionen basierend auf der Cursorposition.
Sätze für die Textverarbeitung segmentieren
Satzsegmentierung teilt Text an Satzgrenzen auf. Dies ist nützlich für Zusammenfassungen, Text-zu-Sprache-Verarbeitung oder die Navigation in langen Dokumenten.
Einfache Ansätze wie das Aufteilen bei Punkten schlagen fehl, da Punkte in Abkürzungen, Zahlen und anderen Kontexten erscheinen, die keine Satzgrenzen sind:
const text = "Dr. Smith bought 100.5 shares. He sold them later.";
text.split(". "); // Incorrect: breaks at "Dr." and "100."
Die Intl.Segmenter-API versteht Satzgrenzregeln:
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" korrekterweise als Teil des Satzes, nicht als Satzgrenzen.
Bei mehrsprachigem Text variieren 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 = "こんにちは。お元気ですか。"; // Uses Japanese full stop
Array.from(segmenterEn.segment(textEn)).length; // 2
Array.from(segmenterJa.segment(textJa)).length; // 2
Wann welche Granularität verwendet werden sollte
Wählen Sie die Granularität basierend darauf, was Sie zählen oder aufteilen müssen:
-
Graphem: Verwenden Sie dies für Zeichenzählung, Textkürzung, Cursorpositionierung oder jede Operation, bei der Sie die Benutzerwahrnehmung von Zeichen abbilden müssen.
-
Wort: Verwenden Sie dies für Wortzählung, Suche und Hervorhebung, Textanalyse oder jede Operation, die sprachübergreifend linguistische Wortgrenzen benötigt.
-
Satz: Verwenden Sie dies für Text-zu-Sprache-Segmentierung, Zusammenfassung, Dokumentnavigation oder jede Operation, die Text Satz für Satz verarbeitet.
Verwenden Sie keine Graphem-Segmentierung, wenn Sie Wortgrenzen benötigen, und verwenden Sie keine Wort-Segmentierung, wenn Sie Zeichenzählungen benötigen. Jede Granularität dient einem bestimmten Zweck.
Segmentierer erstellen und wiederverwenden
Das Erstellen eines Segmentierers ist kostengünstig, aber Sie können Segmentierer aus Leistungsgründen wiederverwenden:
const graphemeSegmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
const wordSegmenter = new Intl.Segmenter("en", { granularity: "word" });
// Reuse these segmenters for multiple strings
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 Segmentierer speichert Gebietsschemadaten zwischen, sodass die Wiederverwendung derselben Instanz wiederholte Initialisierungen vermeidet.
Browser-Unterstützung prüfen
Die Intl.Segmenter-API erreichte im April 2024 den Baseline-Status. Sie funktioniert in aktuellen Versionen von Chrome, Firefox, Safari und Edge. Ältere Browser unterstützen sie nicht.
Prüfen Sie die Unterstützung vor der Verwendung:
if (typeof Intl.Segmenter !== "undefined") {
// Use Intl.Segmenter
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
// ...
} else {
// Fallback for older browsers
const count = text.length; // Not accurate, but available
}
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 für die Anzeige von Zeichenzählungen an Benutzer. Es liefert falsche Ergebnisse für Emojis, kombinierende Zeichen und komplexe Schriftsysteme.
Teilen Sie nicht bei Leerzeichen auf und verwenden Sie keine Regex-Wortgrenzen für mehrsprachige Wort-Segmentierung. Diese Ansätze funktionieren nur für eine Teilmenge von Sprachen.
Gehen Sie nicht davon aus, dass Wort- oder Satzgrenzen in allen Sprachen gleich sind. Verwenden Sie locale-bewusste Segmentierung.
Vergessen Sie nicht, die isWordLike-Eigenschaft zu überprüfen, wenn Sie Wörter zählen. Das Einbeziehen von Interpunktion und Leerzeichen führt zu überhöhten Zählungen.
Schneiden Sie Strings beim Kürzen nicht an beliebigen Indizes ab. Schneiden Sie immer an Graphemcluster-Grenzen, um die Erzeugung ungültiger 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 grundlegende 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 der Fall.
Für die meiste Textverarbeitung, die benutzerseitige Inhalte betrifft, 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 Internationalization API. Weitere APIs dieser Familie umfassen:
Intl.DateTimeFormat: Formatiert Datum und Uhrzeit entsprechend der LocaleIntl.NumberFormat: Formatiert Zahlen, Währungen und Einheiten entsprechend der LocaleIntl.Collator: Sortiert und vergleicht Strings entsprechend der LocaleIntl.PluralRules: Bestimmt Pluralformen für Zahlen in verschiedenen Sprachen
Zusammen bieten diese APIs die erforderlichen Werkzeuge, um Anwendungen zu erstellen, die für Benutzer weltweit korrekt funktionieren. Verwenden Sie Intl.Segmenter für Textsegmentierung und verwenden Sie die anderen Intl-APIs für Formatierung und Vergleich.
Praktisches Beispiel: Erstellen 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
};
}
// Works for any language
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 Text in jeder Sprache und verwendet dabei die korrekten Segmentierungsregeln für jedes Gebietsschema.