Text Segmentation Is a Locale Problem: Intl.Segmenter in Practice
Intl.Segmenter replaces the whitespace split with locale-aware segmentation: graphemes, words and sentences as the browser defines them.
Every word count, character counter, and reading-time estimate in a web app rests on a hidden assumption about where one unit of text ends and the next begins. In English that assumption is a space, so str.split(" ") reads as text processing rather than what it actually is: one locale's typography, hardcoded.
Why split() Is a Locale Assumption
Many non-Latin languages, such as Chinese and Japanese, don't use spaces to separate words, so using the JavaScript split() method on whitespace to split text into words will return incorrect results[4]. The MDN reference states the same failure more precisely: splitting on whitespace would not get the correct result if the locale of the text does not use whitespaces between words, which is the case for Japanese, Chinese, Thai, Lao, Khmer, and Myanmar[1].
This is not a rounding error you can tune away. Hand a Japanese sentence to split(" ") and you get one element back, because there was never a delimiter to find[1]. The bug is structural: whitespace is a property of some writing systems, not of text. Segmenting the same sentence by word granularity instead yields the eight words it actually contains[3].
const ja = "吾輩は猫である。名前はたぬき。";
// One locale's typography, applied to every locale.
ja.split(" ").length; // 1
// The same string, segmented by its own rules.
const words = [...new Intl.Segmenter("ja-JP", { granularity: "word" })
.segment(ja)]
.filter((s) => s.isWordLike);
words.length; // 8Three Granularities, One Object
Intl.Segmenter is the platform's answer, and it is deliberately small. The object enables locale-sensitive text segmentation, giving you meaningful items — graphemes, words, or sentences — from a string[1]. Those three options are the entire configuration surface: the granularity slot is one of the string values "grapheme", "word", or "sentence", identifying the kind of text element to segment[7].
Creating one takes a locale and an options bag, and the granularity you get by saying nothing is worth remembering. If you don't pass any options, the default behavior is to split by grapheme, which is the user-perceived character[3]. That default matters later.
What a Segment Actually Carries
Calling segment() returns a new iterable Segments object containing the segments of the input string, using the segmenter's locale and granularity[2]. The word "iterable" is doing real work here: nothing is materialized until you consume it, so the cost of segmenting a large document stays proportional to how much of it you actually look at[2].
Each item in that iteration is a plain object rather than a bare string[3]. You get the segment, its index as it appears in the original string, the full input string, and a boolean isWordLike to disambiguate words from punctuation[3]. That last field is what saves you from regex-shaped guesswork. isWordLike is true when granularity is "word" and the segment consists of letters, numbers, or ideographs; false when it consists of spaces or punctuation; and undefined when granularity is not "word"[5].
The MDN French example shows why the field earns its keep[2]. Segmenting "Moi ? N'est-ce pas ?" by word yields a single segment for «N'est» spanning code units 6 through 11, then a separate segment for «ce» at 12 through 14[2]. The apostrophe-joined token is one word; the hyphen-joined pair is two[2]. No whitespace rule can express that distinction, and no character class gets it right without a locale.
The Grapheme Trap
str.length is a code-unit count wearing a character count's name, and the gap between the two is not exotic. Take the Hindi word किंतु: str.length reports 5, while grapheme segmentation returns ["किं","तु"] — a length of 2[3].
The reason is that a code point is not a "letter" or a displayed unit on the screen; that designation goes to the grapheme, which can consist of multiple code points[5]. Unicode defines an algorithm for locating those boundaries, and the default clusters it produces are known as extended grapheme clusters[6].
The nesting is strictly ordered, which is what makes graphemes the right base unit. Word boundaries, line boundaries, and sentence boundaries should not occur within a grapheme cluster — a grapheme cluster is atomic with respect to the process of determining those other boundaries[6]. Segment sentences without getting graphemes right underneath, and every downstream boundary inherits the error.
Sentences Are Locale-Specific Too
Sentence splitting is usually written as a split on the period character, which is another locale assumption in disguise. Sentence granularity is convenient precisely because you don't want to keep track of language-specific full-stops[3]. Hindi ends sentences with the danda, not a Western period, and a sentence segmenter returns the two sentences of "वाक्य एक। वाक्य दो।" without ever being told what a danda is[3].
Random Access Without Rebuilding
Iterators are forward-only, which is awkward when what you need is the segment under the user's caret rather than the next one[5]. The Segments object exposes containing(), which returns a segment data object describing the segment that includes the code unit at the specified index, or undefined if the index is out of bounds[5].
That makes hit-testing cheap: ask for the segment at an offset and you get back its start index and its text, with no need to re-run segmentation from the top of the string[5].
// Constructed once, reused across interactions.
const words = new Intl.Segmenter(undefined, { granularity: "word" });
// Which word is the caret inside? No re-scan from index 0.
function wordAt(text: string, caret: number) {
const segments = words.segment(text);
const hit = segments.containing(caret); // undefined past the end
if (!hit) return null;
return { text: hit.segment, start: hit.index, isWord: hit.isWordLike };
}One detail to internalize before you wire that into an editor — the index is a code-unit offset, not a grapheme count. Iterating "Hello, world💙" by words in English starts segments at indexes 0, 5, 6, 7, and 12, the final segment being a surrogate pair of two code units that encodes a single code point[5].
Cache the Segmenter, Not the Segments
Two operational notes fall out of the API shape.
First, construct segmenters once and reuse them[5]. A segmenter holds a resolved locale and a granularity, so a component that builds a new one on every keystroke is redoing locale resolution it already paid for.
Second, check the locale before trusting the output. supportedLocalesOf returns the array of provided locales that are supported without falling back to the runtime's default locale[1]. If the locale you passed is missing from that array, you are segmenting by someone else's rules — sometimes acceptable, but not the behavior you asked for.
Support and the Baseline Window
Intl.Segmenter reached Baseline Newly Available on April 16, 2024, when it became available in all three major browser engines[4]. The floor underneath that date is uneven: Chrome 87, Firefox 125, Safari 14.1[4]. Browser-compat-data records the same Firefox and Safari entries, at 125 and 14.1[8]. Firefox's own release notes confirm the engine side, listing Intl.Segmenter as newly supported for locale-sensitive text segmentation[9].
The feature has not finished climbing its baseline tiers: it has been Newly Available since 2024-04-16 and is expected to reach Widely Available on 2026-10-16[10]. That second date is the practical argument for adopting it now — by the time it arrives, the browsers that lacked Intl.Segmenter will be the ones ageing out of support rather than the ones shipping on new devices.
What It Replaces
The API's own description is the shortest statement of its value — a locale-aware text splitter that can separate a string into meaningful graphemes, words, or sentences[10]. Swapping a whitespace split for it is a small diff and a large correctness change: word counts, character limits, truncation, caret movement, and search highlighting stop depending on the user typing English.
Sources
[1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter — MDN — Intl.Segmenter
[2] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter/segment — MDN — Intl.Segmenter.prototype.segment()
[3] https://developer.mozilla.org/en-US/blog/javascript-intl-segmenter-i18n — MDN Blog — Locale-sensitive text segmentation in JavaScript with Intl.Segmenter
[4] https://web.dev/blog/intl-segmenter — web.dev — The Intl.Segmenter object is now part of Baseline
[5] https://github.com/tc39/proposal-intl-segmenter — TC39 proposal-intl-segmenter — Unicode segmentation in JavaScript (Stage 4)
[6] https://www.unicode.org/reports/tr29 — UAX #29 — Unicode Text Segmentation
[7] https://tc39.es/ecma402 — ECMA-402 — ECMAScript Internationalization API Specification, Segmenter Objects
[8] https://github.com/mdn/browser-compat-data/blob/main/javascript/builtins/Intl/Segmenter.json — mdn/browser-compat-data — javascript/builtins/Intl/Segmenter.json
[9] https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/125 — MDN — Firefox 125 release notes for developers
[10] https://web-platform-dx.github.io/web-features-explorer/features/intl-segmenter — Web platform features explorer — Intl.Segmenter availability