# anyword β€” words & graphemes > Splits text into words, characters or sentences the way people see them β€” emoji stay whole, Thai and Chinese split without spaces β€” and counts or truncates on those boundaries. - wraps: Intl.Segmenter - install: npm install anyword - size: 0.8 kB gzip, zero dependencies, ESM + CJS, TypeScript types included - runtime: Node 16+, Chrome 87+, Safari 14.1+, Firefox 125+ β€” branch on anyword.supported for older engines. - docs: https://anyfamily.site/docs/anyword - demo: https://anyfamily.site/anyword - playground: https://stackblitz.com/github/kirilinsky/anyfamily/tree/main/examples/vanilla?file=src/main.ts - npm: https://www.npmjs.com/package/anyword - source: https://github.com/kirilinsky/anyfamily/tree/main/packages/anyword - family: https://anyfamily.site/llms.txt ## why "πŸ‘¨β€πŸ‘©β€πŸ‘§".length is 8, split(" ") finds one word in a Thai sentence, slice() cuts an emoji in half. anyword hands you the runtime's real boundaries β€” for counters, previews and highlights β€” in ~0.8 kB. ## usage ```ts anyword("don't stop δΈ–η•Œ") // ["don't", "stop", "δΈ–η•Œ"] anyword("πŸ‘¨β€πŸ‘©β€πŸ‘§ hi", { by: "grapheme" }) // ["πŸ‘¨β€πŸ‘©β€πŸ‘§", " ", "h", "i"] anyword.count("hΓ©llo", { by: "grapheme" }) // 5 β€” what a character counter should show anyword.truncate("hΓ©llo πŸ‘¨β€πŸ‘©β€πŸ‘§", 5, { ellipsis: "…" }) // "hΓ©llo…" anyword.parts("δΈ–η•Œ test") // [{ segment: "δΈ–η•Œ", index: 0, isWordLike: true }, …] anyword.supported // Intl.Segmenter present? ``` ## not for Stemming, tokenising for search, or language detection. --- # anyword Micro text segmenter built on native Intl. Split, count and truncate text by word, grapheme or sentence β€” in any locale. β–Έ live demo Β· β–Έ full docs Β· β–Έ any family --- **One export. Correct boundaries. Any locale. Zero dependencies.** Naive JS quietly gets text wrong: `.length` miscounts emoji and accents, `.split(" ")` finds no words in Chinese or Thai, `[...str]` rips πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦ into pieces. The browser already knows where the real boundaries are β€” anyword is the thin wrapper. No rule tables, no locale files, no config. ```ts import { anyword } from "anyword"; anyword("don't stop δΈ–η•Œ"); // ["don't", "stop", "δΈ–η•Œ"] anyword("πŸ‘¨β€πŸ‘©β€πŸ‘§ hi", { by: "grapheme" }); // ["πŸ‘¨β€πŸ‘©β€πŸ‘§", " ", "h", "i"] anyword.count("δΈ–η•Œ test"); // 2 anyword.count("πŸ‘¨β€πŸ‘©β€πŸ‘§", { by: "grapheme" }); // 1 β€” "πŸ‘¨β€πŸ‘©β€πŸ‘§".length is 8 anyword.truncate("hΓ©llo πŸ‘¨β€πŸ‘©β€πŸ‘§", 5, { ellipsis: "…" }); // "hΓ©llo…" ``` --- ## install ```bash npm install anyword ``` --- ## usage ```ts anyword(text); anyword(text, options); ``` Returns the segments as plain strings, in order. Everything else hangs off the same name: ```ts anyword.parts(text, options?); // { segment, index, isWordLike? }[] anyword.count(text, options?); // number anyword.truncate(text, limit, options?); // string anyword.supported; // boolean ``` `anyword.parts()` carries offsets into the original text, so you can highlight or slice without searching again. ```tsx anyword.parts("δΈ–η•Œ test"); // [{ segment: "δΈ–η•Œ", index: 0, isWordLike: true }, { segment: "test", index: 3, isWordLike: true }] anyword.parts(text, { raw: true }).map((p, i) => p.segment === query ? {p.segment} : p.segment, ); ``` --- ## recipes Copy, paste, move on. ```tsx // Word counter anyword.count(post.body); // 412 // Character counter users agree with (πŸ‘¨β€πŸ‘©β€πŸ‘§ counts as 1, not 8) anyword.count(input, { by: "grapheme" }); // Safe preview / char-limit cut anyword.truncate(bio, 140, { ellipsis: "…" }); // Word-limited excerpt anyword.truncate(article, 30, { by: "word", ellipsis: " …" }); // Per-character animation, emoji intact anyword(title, { by: "grapheme" }).map((c, i) => {c}); // Safe reverse anyword(text, { by: "grapheme" }).reverse().join(""); // Initials anyword(fullName).slice(0, 2).map((w) => anyword(w, { by: "grapheme" })[0]).join(""); // Split into sentences anyword(text, { by: "sentence" }); ``` anyword is pure and synchronous β€” no clock, no state β€” so it renders the same on server and client. Pass an explicit `locale` to keep it that way. --- ## granularity `by` maps straight to `Intl.Segmenter`: | `by` | Unit | Example | | --- | --- | --- | | `"word"` | words (default) | `"don't stop δΈ–η•Œ"` β†’ `["don't", "stop", "δΈ–η•Œ"]` | | `"grapheme"` | user-perceived characters | `"πŸ‘¨β€πŸ‘©β€πŸ‘§ hi"` β†’ `["πŸ‘¨β€πŸ‘©β€πŸ‘§", " ", "h", "i"]` | | `"sentence"` | sentences | `"Hi. Go now!"` β†’ `["Hi. ", "Go now!"]` | Word mode drops the segments between words β€” spaces and punctuation. Set `raw: true` to keep them, and the pieces join back into the original string. ```ts anyword("hi, there!"); // ["hi", "there"] anyword("hi, there!", { raw: true }); // ["hi", ",", " ", "there", "!"] ``` Grapheme and sentence modes never drop anything, so `raw` does nothing there. β†’ [Every granularity in detail](https://anyfamily.site/docs/anyword#granularity) --- ## options | Option | Type | Default | Notes | | --- | --- | --- | --- | | `by` | `"word" \| "grapheme" \| "sentence"` | `"word"` | `truncate` defaults to `"grapheme"` | | `locale` | `string \| string[]` | runtime locale | BCP 47 tag or fallback array | | `raw` | `boolean` | `false` | word mode only β€” keep spaces and punctuation | | `ellipsis` | `string` | `""` | `truncate` only β€” appended when text was cut | `truncate` cuts on a segment boundary, so an emoji or an accented letter is never split. The ellipsis does not count toward the limit, and short input comes back untouched. β†’ [What each option does, with examples](https://anyfamily.site/docs/anyword#options) --- ## locales Any valid BCP 47 tag, and fallback arrays. The locale matters most for word breaking in scripts without spaces. When omitted, native `Intl` uses the runtime locale. ```ts anyword("γ“γ‚Œγ―ζ—₯本θͺžγ§γ™", { locale: "ja" }); // ["γ“γ‚Œ", "は", "ζ—₯本θͺž", "です"] anyword("ΰΈͺΰΈ§ΰΈ±ΰΈͺΰΈ”ΰΈ΅ΰΈŠΰΈ²ΰΈ§ΰΉ‚ΰΈ₯ก", { locale: "th" }); // ["ΰΈͺΰΈ§ΰΈ±ΰΈͺΰΈ”ΰΈ΅", "ชาว", "ΰΉ‚ΰΈ₯ก"] β€” no spaces needed anyword("don't stop", { locale: "en" }); // ["don't", "stop"] ``` --- ## vs the alternatives | | anyword | grapheme-splitter | words-count + lodash | | --- | :---: | :---: | :---: | | gzip | **< 1kb** | ~10kb | ~25kb | | unicode data bundled | **no** | yes | yes | | boundary rules | **native Intl** | bundled tables | regex | | word / sentence mode | **yes** | grapheme only | spaces only | | dependencies | **0** | 0 | 1+ | anyword is not an NLP toolkit β€” it does one thing. Reach for a tokenizer or a full i18n framework when you need stemming, stop words or message catalogs. --- ## stability anyword follows [semver](https://semver.org/). The public API is a single export β€” `anyword`, with `parts`, `count`, `truncate` and `supported` on it β€” plus `AnywordOptions`, `Granularity` and the exported types. It only changes shape in a major release. Segment lists come from the runtime's ICU data and may vary between Node versions, browsers and OSes β€” especially for CJK and Thai. Test behaviour, not exact arrays. ### migrating from 1.x 2.0 removed the separate `anywordParts`, `anywordCount`, `anywordTruncate` and `supported` exports. All four are the same values, now reached through the one name the package exports: ```diff - import { anyword, anywordCount, anywordTruncate, supported } from "anyword"; + import { anyword } from "anyword"; - anywordCount(text); + anyword.count(text); - anywordTruncate(text, 20); + anyword.truncate(text, 20); - supported ? anyword(text) : text.split(/\s+/); + anyword.supported ? anyword(text) : text.split(/\s+/); ``` Arguments, return values and throwing behaviour are unchanged. Every `any*` package follows this shape from 2.0 on: the bare call does the job, everything else hangs off the same name. --- ## compatibility `Intl.Segmenter` landed late β€” Firefox 125, Safari 14.1. On engines without it every call throws; branch on `anyword.supported` if you target them. Node.js 18+ Β· Chrome 87+ Β· Firefox 125+ Β· Safari 14.1+ Β· Edge Runtime Β· Cloudflare Workers Β· Deno CI runs the full suite on Node 20, 22 and 24. --- ## the any family anyword is part of **any family** β€” tiny, zero-dependency wrappers over native `Intl`, one API per package. | | | | | --- | --- | --- | | [anywhen](https://anyfamily.site/anywhen) | dates & relative time | `Intl.DateTimeFormat` | | [anyamount](https://anyfamily.site/anyamount) | numbers, currency, units | `Intl.NumberFormat` | | [anymany](https://anyfamily.site/anymany) | lists | `Intl.ListFormat` | | [anyaround](https://anyfamily.site/anyaround) | names & flags | `Intl.DisplayNames` | | [anylong](https://anyfamily.site/anylong) | durations | `Intl.DurationFormat` | | [anyplural](https://anyfamily.site/anyplural) | plurals | `Intl.PluralRules` | | [**anyword**](https://anyfamily.site/anyword) | words & graphemes | `Intl.Segmenter` | Want all of them? [`anyfamily`](https://www.npmjs.com/package/anyfamily) is one install for the lot, and [`anyfamily-react`](https://www.npmjs.com/package/anyfamily-react) wraps each as a hook with a shared locale provider. ```bash npm install anyfamily ``` --- MIT Β© [kirilinsky](https://github.com/kirilinsky)