Skip to content

Sandhi

Tone sandhi is applied to the syllable array rather than to a string, so it can be switched off, works across word boundaries, and cannot be confused by the spelling.

import { applySandhi, readWord } from "@kensio/pinyinjs";
const buShi = readWord("bùshì") ?? [];
applySandhi(buShi); // bú shì — 不 flattens before a fourth tone
applySandhi(buShi, { yiBu: false }); // unchanged
const niHao = readWord("nǐhǎo") ?? [];
applySandhi(niHao); // unchanged by default
applySandhi(niHao, { thirdTone: true }); // ní hǎo

The dictionary stores underlying tones — the source data has sandhi baked in, and the build normalises it out — which is what makes any of this optional. A package that stored 不 as could never give you back.

On by default.

is , and flattens to before a fourth tone:

convert(dictionary, "不是"); // "bú shì"
convert(dictionary, "不对"); // "bú duì"
convert(dictionary, "不行"); // "bùxíng" — 行 is second tone here, so no change

is , and becomes before tones 1, 2 and 3, before tone 4, and stays in ordinals and in final position:

convert(dictionary, "一天"); // "yì tiān" — before first tone
convert(dictionary, "一起"); // "yìqǐ" — before third tone
convert(dictionary, "一个"); // "yí gè" — before fourth tone
convert(dictionary, "一样"); // "yíyàng"
convert(dictionary, "第一"); // "dìyī" — ordinal, unchanged

Turn both off with sandhi: { yiBu: false }, or --no-sandhi at the command line.

Off by default.

A third tone before another third tone is said as a second tone, so 你好 is spoken ní hǎo. Standard orthography writes the underlying tones anyway, which is why this is not on:

convert(dictionary, "好好"); // "hǎohǎo"
convert(dictionary, "好好", { sandhi: { thirdTone: true } }); // "háohǎo"

Turn it on when you are transcribing how something is said — a pronunciation guide, a speech exercise, subtitles for a listening task — and leave it off when you are writing pinyin as text.

const henHao = readWord("hěnhǎo") ?? [];
applySandhi(henHao); // hěn hǎo
applySandhi(henHao, { thirdTone: true }); // hén hǎo

Because the pass runs over the syllable array rather than per word, a sandhi trigger works across a boundary the decoder put in. 不 followed by a fourth tone in the next word still flattens.

applySandhi(syllables, options?) takes the same object as the sandhi field of ConvertOptions:

Field Default Does
yiBu true 一 and 不 tone changes
thirdTone false third tone before third tone

It is merged with the defaults, so { thirdTone: true } leaves yiBu on.

儿化 is handled, but as a dictionary fact rather than as a sandhi rule — see orthography. The half-third-tone allophone (a third tone before a non-third tone, said as a low fall with no rise) is not written, because it has no distinct pinyin spelling to write.

Terminal window
$ pinyinjs sandhi bùshì
bùshì bú shì
$ pinyinjs sandhi --third-tone nǐhǎo
nǐhǎo ní hǎo

sandhi takes written pinyin and needs no dictionary. The same two flags — --no-sandhi and --third-tone — also work on convert, html and explain.