Normalize Vietnamese tone marks so that hòa and hoà stop being two different strings.
$ echo "Khỏe không, thủy thủ? Qúy khách vui lòng chờ." | npx vndau
Khoẻ không, thuỷ thủ? Quý khách vui lòng chờ.
No dependencies, works in Node and the browser, ships with a CLI you can drop into CI.
Vietnamese text in the wild is inconsistent in two ways:
- Tone placement. Both
hòaandhoàare in common use (old vs new style), and typing tools produce whichever they like. People also type outright mistakes likeqúyortóan. - Unicode form. Text copied from macOS, some PDFs, or older input methods is often in NFD
(
a+ combining grave) instead of NFC (à). It looks identical but===fails.
The result is search that misses half the matches, duplicate keys in i18n files, and URLs
that differ by an invisible character. vndau fixes both.
Tóm tắt: thư viện chuẩn hoá dấu tiếng Việt về một kiểu thống nhất (kiểu mới
hoàhoặc kiểu cũhòa), sửa lỗi bỏ dấu sai vị trí (qúy→quý) và chuyển văn bản NFD sang NFC. Có kèm CLI để kiểm tra file dịch / tài liệu trong CI.
npm install vndauThe package is ESM only. Node 20+ for the CLI; the library itself has no Node-specific code
and runs in any modern browser or bundler. From CommonJS on Node 20.19+ / 22.12+, plain
require('vndau') works; on older versions use await import('vndau').
import { normalize, removeTones, removeDiacritics, slugify } from 'vndau';
normalize('Hòa bình, thủy điện'); // 'Hoà bình, thuỷ điện'
normalize('Hoà bình, thuỷ điện', { style: 'old' }); // 'Hòa bình, thủy điện'
normalize('tóan học, qúy'); // 'toán học, quý'
removeTones('Tiếng Việt'); // 'Tiêng Viêt'
removeDiacritics('Đà Nẵng'); // 'Da Nang'
slugify('Hà Nội mùa thu'); // 'ha-noi-mua-thu'Words that aren't valid Vietnamese syllables are left alone, so mixed text is safe:
normalize('café ở Zürich, hòa giải'); // 'café ở Zürich, hoà giải'Normalize both sides before comparing. For accent-insensitive search, strip diacritics too:
const key = (s) => removeDiacritics(normalize(s)).toLowerCase();
key('Thủy') === key('thuỷ'); // true
key('Thủy') === key('THUY'); // trueFor sorting, the built-in Intl.Collator('vi') already knows Vietnamese alphabet order, so
this library doesn't try to replace it.
vndau check <path...> report words with misplaced tones or NFD characters
vndau fix <path...> rewrite files in place
vndau < input.txt normalize stdin to stdout
-s, --style <new|old> tone placement style (default: new)
-e, --ext <list> extensions to scan in directories
-f, --format <text|json|github> output format for check (default: text)
-x, --exclude <pattern> skip files matching pattern (repeatable)
-q, --quiet only print the summary
Patterns for --exclude use * (matches within a path segment) or ** (matches across segments), or plain substring:
$ npx vndau check docs/ --exclude '*/fixtures/*' --exclude generated.md
You can also list patterns in a .vndauignore file in the current directory (one pattern per line, # for comments):
# .vndauignore
*/fixtures/*
generated.md
$ npx vndau check locales/
locales/vi.json:2:13 Hòa -> Hoà
locales/vi.json:3:12 Thủy -> Thuỷ
locales/vi.json:3:25 qúy -> quý
found 3 issue(s) in 1 of 1 file(s)
check exits with code 1 when it finds something, so it works as a CI step:
- run: npx vndau check docs/ locales/Use --format github to get inline annotations on pull requests:
- run: npx vndau check docs/ locales/ --format githubUse --format json to get machine-readable output (one JSON object per line):
$ npx vndau check locales/ --format json
{"file":"locales/vi.json","line":2,"column":13,"found":"Hòa","expected":"Hoà","kind":"tone"}
Directories are scanned recursively, skipping node_modules, .git, dist, build and
coverage. Files passed by name are always checked regardless of extension.
The two styles only disagree on open syllables ending in oa, oe, uy:
| new (default) | old | |
|---|---|---|
| oa | hoà, hoá | hòa, hóa |
| oe | khoẻ | khỏe |
| uy | thuỷ, luỹ | thủy, lũy |
Everything else has one correct position, applied in this order:
- One vowel: on it.
- A vowel with a modifier (
ă â ê ô ơ) takes the tone:người,thuế,chuyện,thuở. - Otherwise
ưtakes it:cứu,lửa,gửi. - Closed syllable (ends in a consonant): last vowel, e.g.
hoàng,toán,huýt. - Three vowels, open: the middle one, e.g.
ngoài,khuỷu. - Two vowels, open: the first one, e.g.
của,kìa,máy.
qu and gi count as consonants when followed by another vowel (quý, già, giữa),
but gi on its own is g + i (gì, gìn).
Letter case is preserved per position, so HÒA becomes HOÀ.
| Function | Description |
|---|---|
normalize(text, { style? }) |
NFC + tone placement for every word |
normalizeWord(word, style?) |
Same, for a single word |
findIssues(text, style?) |
{ line, column, found, expected, kind }[], what check prints |
removeTones(text) |
Drop tone marks, keep ă â ê ô ơ ư đ |
removeDiacritics(text) |
Drop everything, đ → d |
slugify(text, { separator?, lowercase? }) |
URL slug |
parseSyllable(word) |
{ onset, nucleus, coda } or null, lowercase toneless input |
TypeScript types are included.
- Only Unicode input. Legacy encodings (VNI, TCVN3/ABC) need converting first.
- Validation is structural, not dictionary-based. A typo that still forms a valid syllable shape will be normalized, not flagged.
- Spelling variants like
quí/quýorkỹ/kĩare a different question from tone placement and are not changed.
Bug reports with a concrete word that comes out wrong are the most useful thing. See CONTRIBUTING.md.
vndau chuẩn hoá cách bỏ dấu tiếng Việt về một kiểu thống nhất, để hòa và hoà không còn là hai
chuỗi khác nhau. Thư viện không có dependency, chạy được trên Node 20+ và trình duyệt, kèm CLI.
npm install vndauDùng khi nào:
- Tìm kiếm, khử trùng lặp dữ liệu: chuẩn hoá cả từ khoá lẫn dữ liệu trước khi so sánh (xem mục Searching). Dữ liệu crawl từ nhiều nguồn thường lẫn kiểu dấu cũ, kiểu dấu mới và NFD.
- Kiểm tra file dịch (i18n) và tài liệu trong CI:
npx vndau check locales/trả về mã lỗi 1 khi có từ bỏ dấu sai vị trí nhưqúy,tóan, hoặc ký tự NFD copy từ macOS hay PDF. - Tạo slug URL và tên file:
slugify('Hà Nội mùa thu')cho ra'ha-noi-mua-thu'.
Mặc định là kiểu dấu mới (hoà, thuỷ); dùng { style: 'old' } hoặc --style old nếu dự án của
bạn theo kiểu cũ (hòa, thủy). Hai kiểu chỉ khác nhau ở vần oa, oe, uy (xem
Tone placement rules). URL và địa chỉ email trong văn bản được giữ nguyên.
Báo lỗi tốt nhất là đưa ra một từ cụ thể bị xử lý sai, kèm nguồn cho cách viết đúng: mở issue tại đây.