Count words correctly in JavaScript — including Chinese, Japanese, and Korean

Count words correctly in JavaScript — including Chinese, Japanese, and Korean

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.

关于 text.split(' ') 的问题:几乎你在网上找到的每一个“字数统计”工具都做着同样的事情:按空格拆分文本,然后计算片段数量。这对于英语来说是有效的——直到它失效为止。试着在一个带有撇号的句子上运行它:“don’t worry”会被拆分成两个词而不是一个。试着在中文上运行它:“你好世界”完全没有空格,所以你会得到一个词而不是四个。如果文本中包含多个空格或换行符,你的统计结果会因为空字符串而虚高。以下是如何跨语言正确统计字数,以及实际存在的边界情况。

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.

两种书写系统的问题:世界上大多数文本属于以下两类之一:空格分隔的书写系统(英语、法语、德语、西班牙语等):词汇由空白字符分隔。一个词是一串字母和数字,中间可能包含撇号或连字符(如 don’t, well-known)。基于字符的书写系统(中文、日语、韩语):没有词汇边界。每个字符——每个汉字、每个假名、每个韩文字节——都算作一个词。一个正确的字数统计工具需要同时处理这两者,并且需要在同一段文本中处理它们。如果一段文字中混合了英语和中文(技术文档中经常出现这种情况),你需要同时运行两种算法。

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.

实现代码:// CJK 脚本:汉字、日文假名、韩文 Hangul const CJK = /[\u1100-\u11FF\u3040-\u30FF\u3130-\u318F\u3400-\u4DBF\u4E00-\u9FFF\uAC00-\uD7AF\uF900-\uFAFF]/g; // 空格分隔的词:带有内部撇号和连字符的字母/数字 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; } 诀窍是:先剔除所有 CJK 字符(用空格替换它们,以免意外合并相邻的拉丁词),然后用词汇模式计算剩余部分。将两个计数相加。

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.

有两个细节值得注意:1. Unicode 属性转义。\p{L} 匹配任何书写系统中的任何字母;\p{N} 匹配任何数字字符。这可以处理带重音的拉丁文(如 café)、西里尔文、希腊文、阿拉伯文、天城文——涵盖一切。像 [a-zA-Z]+ 这样的正则表达式会漏掉世界上大部分文本。2. 撇号处理。don’t 应该是一个词,而不是两个。模式 (?:['\u2019-][\p{L}\p{N}]+)* 允许内部撇号(包括直撇号 ’ 和弯撇号 \u2019)以及连字符,但前提是它们必须被字母包围。位于开头或结尾的撇号不计入。

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.

阅读时间:不仅仅是字数。一旦你有了字数统计,下一个问题通常是“读完需要多久?”标准答案是每分钟 200 个词——但这只对空格分隔的书写系统准确。CJK 文本是逐字阅读的,普通读者的阅读速度大约是每分钟 400 个字符。因此,正确的公式是:function estimateReadingMinutes(cjkCharacters, words) { return cjkCharacters / 400 + words / 200; } 一篇 1000 个字符的中文文章大约需要 2.5 分钟阅读。一篇 500 个词的英文文章也大约需要 2.5 分钟。如果混合在一起,只需将两个时间相加即可。

Three tests for any word counter Before you trust a tool: “你好世界” should give 4 words. If you get 1, the tool doesn’t handle CJK. “don’t worry” should give 2 words. If you get 3, the tool splits on apostrophes. ” 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.

任何字数统计工具的三个测试:在你信任一个工具之前:输入“你好世界”应该得到 4 个词。如果你得到 1,说明该工具无法处理 CJK。输入“don’t worry”应该得到 2 个词。如果你得到 3,说明该工具在撇号处进行了拆分。输入“ multiple spaces ”应该得到 2 个词。如果你得到 5 或更多,说明该工具将空格拆分出的空字符串也计算在内了。如果一个工具未能通过其中任何一项测试,它的字数统计就是错误的。

Words are not tokens 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.

词不是 Token:如果文本是输入给 LLM 而不是人类,请停止使用字数统计。词是一个语言学单位;Token 是 BPE 词汇表条目,它们会根据书写系统产生偏差:英语大约是每个 Token 对应 1.3 个词,而 CJK 则接近每个字符对应一个 Token 甚至更糟。估算和精确计数是不同的问题,声称两者都能做到的计数器是在瞎猜。我在 ToolStack Token Counter 中提供了诚实的版本:当供应商映射到我们可以加载的编码时,使用真实的 o200k_base / cl100k_base 字节对编码,并在词汇表私有时提供清晰标注的统计估算。

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 server-side storage of anything you paste or type.

我的成果:我将这套流程应用到了一个页面上,它可以统计字数、字符数、句子数、段落数、行数以及预估阅读时间——并随着你的输入实时更新:ToolStack Word Counter。它能正确处理 CJK 和拉丁书写系统,忽略首尾空格,且完全在浏览器中运行——你的文本永远不会离开标签页。这是 125 个浏览器本地工具集的一部分,支持 7 种语言,无需注册。披露信息(因为我也会在别人的页面上检查这一点):已启用 GA4 页面级分析;文本输入永远不会离开标签页,且不会在服务器端存储你粘贴或输入的任何内容。