The problem with text.split(' ')

Almost every "word counter" tool you find online does the same thing: split the text on spaces, count the pieces. That works for English — until it doesn't.

Try it on a sentence with an apostrophe: "don't worry" becomes two words instead of one. Try it on Chinese: "你好世界" has no spaces at all, so you get one word instead of four. Try it on text with multiple spaces or line breaks, and your count inflates with empty strings.

Here is how to count words correctly across languages, and what the edge cases actually are.

The two-script problem

Most of the world's text falls into one of two categories:

Space-separated scripts (English, French, German, Spanish, etc.): Words are delimited by whitespace. A word is a run of letters and digits, possibly with inner apostrophes or hyphens (don't, well-known).

Character-based scripts (Chinese, Japanese, Korean): There are no word boundaries. Each character — each Han ideograph, each kana, each Hangul syllable — counts as one word.

A correct word counter needs to handle both, and it needs to handle them in the same text. Mix English and Chinese in one paragraph (as technical documentation often does), and you need both algorithms firing at once.

The implementation

// CJK scripts: Han ideographs, Japanese kana, Korean Hangul const CJK = /[\u1100-\u11FF\u3040-\u30FF\u3130-\u318F\u3400-\u4DBF\u4E00-\u9FFF\uAC00-\uD7AF\uF900-\uFAFF]/g; // Space-separated words: letters/digits with inner apostrophes and hyphens const WORD = /[\p{L}\p{N}]+(?:['\u2019-][\p{L}\p{N}]+)*/gu; function countWords(text) { const cjk = text.match(CJK)?.length ?? 0; const other = text.replace(CJK, ' ').match(WORD)?.length ?? 0; return cjk + other; } 

The trick: strip out all CJK characters first (replacing them with spaces so we don't accidentally merge adjacent Latin words), then count what's left with the word pattern. Add the two counts together.

Two details worth noting:

1. The Unicode property escapes. \p{L} matches any letter in any script; \p{N} matches any numeric character. This handles accented Latin (café), Cyrillic, Greek, Arabic, Devanagari — everything. A regex like [a-zA-Z]+ would miss most of the world's text.

2. The apostrophe handling. don't should be one word, not two. The pattern (?:['\u2019-][\p{L}\p{N}]+)* allows inner apostrophes (both straight ' and curly \u2019) and hyphens, but only when surrounded by letters. A leading or trailing apostrophe doesn't count.

Reading time: it's not just words

Once you have a word count, the next question is usually "how long will this take to read?" The standard answer is 200 words per minute — but that's only accurate for space-separated scripts.

CJK text is read character by character, at roughly 400 characters per minute for an average reader. So the correct formula is:

function estimateReadingMinutes(cjkCharacters, words) { return cjkCharacters / 400 + words / 200; } 

A 1000-character Chinese article takes about 2.5 minutes to read. A 500-word English article takes about 2.5 minutes. Mix them, and you add the two times together.

Three tests for any word counter

Before you trust a tool:

  1. "你好世界" should give 4 words. If you get 1, the tool doesn't handle CJK.
  2. "don't worry" should give 2 words. If you get 3, the tool splits on apostrophes.
  3. " multiple spaces " should give 2 words. If you get 5 or more, the tool counts empty strings from splitting on whitespace.

If a tool fails any of these, its word count is wrong.

Words are not tokg��

If the text is going into an LLM rather than a human, stop using the word count. A word is a linguistic unit; a token is a BPE vocabulary entry, and they drift apart by script: English lands around 1.3 words per token, CJK is closer to one token per character or worse. The estimate and the exact count are different problems, and a counter that claims both is guessing.

I put the honest version of that behind ToolStack Token Counter: real o200k_base / cl100k_base byte-pair encodings when the vendor maps to one we can load, and a clearly labelled statistical estimate where the vocabulary is private.

What I shipped

I put this exact pipeline behind a page that counts words, characters, sentences, paragraphs, lines, and estimated reading time — with live updates as you type: ToolStack Word Counter.

It handles CJK and Latin scripts correctly, ignores leading/trailing whitespace, and runs entirely in the browser — your text never leaves the tab. Part of a set of 125 browser-local tools in 7 languages, no sign-up.

Disclosure, since I would check it on someone else's page: GA4 page-level analytics is enabled; text input never leaves the tab, and there is no servfWr-side storage of anything you paste or type.