@kensio/pinyinjs 0.1.0-beta.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/NOTICE +7 -0
  3. package/README.md +144 -19
  4. package/data/full.entries +13400 -13400
  5. package/data/manifest.json +4 -4
  6. package/data/standard.entries +2492 -2492
  7. package/dist/cli/arguments.d.ts +67 -0
  8. package/dist/cli/arguments.d.ts.map +1 -1
  9. package/dist/cli/arguments.js +79 -0
  10. package/dist/cli/arguments.js.map +1 -1
  11. package/dist/cli/colour.d.ts +77 -0
  12. package/dist/cli/colour.d.ts.map +1 -0
  13. package/dist/cli/colour.js +117 -0
  14. package/dist/cli/colour.js.map +1 -0
  15. package/dist/cli/commands.d.ts +49 -1
  16. package/dist/cli/commands.d.ts.map +1 -1
  17. package/dist/cli/commands.js +404 -42
  18. package/dist/cli/commands.js.map +1 -1
  19. package/dist/cli/environment.d.ts.map +1 -1
  20. package/dist/cli/environment.js +20 -0
  21. package/dist/cli/environment.js.map +1 -1
  22. package/dist/cli/help.d.ts.map +1 -1
  23. package/dist/cli/help.js +43 -16
  24. package/dist/cli/help.js.map +1 -1
  25. package/dist/cli/run.d.ts +11 -0
  26. package/dist/cli/run.d.ts.map +1 -1
  27. package/dist/cli/run.js +4 -2
  28. package/dist/cli/run.js.map +1 -1
  29. package/dist/decode/convert.d.ts +20 -0
  30. package/dist/decode/convert.d.ts.map +1 -1
  31. package/dist/decode/convert.js +170 -12
  32. package/dist/decode/convert.js.map +1 -1
  33. package/dist/decode/decode.d.ts +3 -2
  34. package/dist/decode/decode.d.ts.map +1 -1
  35. package/dist/decode/decode.js +17 -4
  36. package/dist/decode/decode.js.map +1 -1
  37. package/dist/decode/reading-rules.d.ts +56 -0
  38. package/dist/decode/reading-rules.d.ts.map +1 -0
  39. package/dist/decode/reading-rules.js +107 -0
  40. package/dist/decode/reading-rules.js.map +1 -0
  41. package/dist/decode/rules.d.ts +66 -0
  42. package/dist/decode/rules.d.ts.map +1 -0
  43. package/dist/decode/rules.js +138 -0
  44. package/dist/decode/rules.js.map +1 -0
  45. package/dist/decode/sandhi.d.ts +1 -1
  46. package/dist/decode/sandhi.d.ts.map +1 -1
  47. package/dist/decode/sandhi.js +60 -1
  48. package/dist/decode/sandhi.js.map +1 -1
  49. package/dist/decode/word.d.ts +18 -0
  50. package/dist/decode/word.d.ts.map +1 -1
  51. package/dist/dictionary/artifact.d.ts.map +1 -1
  52. package/dist/dictionary/artifact.js +28 -1
  53. package/dist/dictionary/artifact.js.map +1 -1
  54. package/dist/dictionary/dictionary.d.ts +9 -0
  55. package/dist/dictionary/dictionary.d.ts.map +1 -1
  56. package/dist/dictionary/dictionary.js +7 -0
  57. package/dist/dictionary/dictionary.js.map +1 -1
  58. package/dist/dictionary/entry.d.ts +12 -0
  59. package/dist/dictionary/entry.d.ts.map +1 -1
  60. package/dist/dictionary/entry.js.map +1 -1
  61. package/dist/dictionary/merge.d.ts +3 -1
  62. package/dist/dictionary/merge.d.ts.map +1 -1
  63. package/dist/dictionary/merge.js +19 -1
  64. package/dist/dictionary/merge.js.map +1 -1
  65. package/dist/format/transcription.d.ts +52 -0
  66. package/dist/format/transcription.d.ts.map +1 -0
  67. package/dist/format/transcription.js +88 -0
  68. package/dist/format/transcription.js.map +1 -0
  69. package/dist/index.d.ts +24 -3
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +15 -1
  72. package/dist/index.js.map +1 -1
  73. package/dist/numerals/cardinal.d.ts +39 -0
  74. package/dist/numerals/cardinal.d.ts.map +1 -0
  75. package/dist/numerals/cardinal.js +111 -0
  76. package/dist/numerals/cardinal.js.map +1 -0
  77. package/dist/numerals/characters.d.ts +31 -0
  78. package/dist/numerals/characters.d.ts.map +1 -0
  79. package/dist/numerals/characters.js +80 -0
  80. package/dist/numerals/characters.js.map +1 -0
  81. package/dist/numerals/numerals.d.ts +76 -0
  82. package/dist/numerals/numerals.d.ts.map +1 -0
  83. package/dist/numerals/numerals.js +119 -0
  84. package/dist/numerals/numerals.js.map +1 -0
  85. package/dist/numerals/text.d.ts +52 -0
  86. package/dist/numerals/text.d.ts.map +1 -0
  87. package/dist/numerals/text.js +248 -0
  88. package/dist/numerals/text.js.map +1 -0
  89. package/dist/orthography/grouping.d.ts +56 -11
  90. package/dist/orthography/grouping.d.ts.map +1 -1
  91. package/dist/orthography/grouping.js +109 -35
  92. package/dist/orthography/grouping.js.map +1 -1
  93. package/dist/orthography/idiom-list.d.ts +56 -0
  94. package/dist/orthography/idiom-list.d.ts.map +1 -0
  95. package/dist/orthography/idiom-list.js +296 -0
  96. package/dist/orthography/idiom-list.js.map +1 -0
  97. package/dist/orthography/idioms.d.ts +18 -0
  98. package/dist/orthography/idioms.d.ts.map +1 -0
  99. package/dist/orthography/idioms.js +22 -0
  100. package/dist/orthography/idioms.js.map +1 -0
  101. package/dist/orthography/reduplication.d.ts +52 -0
  102. package/dist/orthography/reduplication.d.ts.map +1 -0
  103. package/dist/orthography/reduplication.js +99 -0
  104. package/dist/orthography/reduplication.js.map +1 -0
  105. package/dist/orthography/rule.d.ts +50 -0
  106. package/dist/orthography/rule.d.ts.map +1 -0
  107. package/dist/orthography/rule.js +101 -0
  108. package/dist/orthography/rule.js.map +1 -0
  109. package/dist/sources/cedict.d.ts +23 -0
  110. package/dist/sources/cedict.d.ts.map +1 -1
  111. package/dist/sources/cedict.js +28 -0
  112. package/dist/sources/cedict.js.map +1 -1
  113. package/dist/syllable/syllable.d.ts +1 -1
  114. package/dist/syllable/syllable.d.ts.map +1 -1
  115. package/dist/syllable/syllable.js +2 -37
  116. package/dist/syllable/syllable.js.map +1 -1
  117. package/dist/tone/tone-mark.d.ts +23 -0
  118. package/dist/tone/tone-mark.d.ts.map +1 -1
  119. package/dist/tone/tone-mark.js +49 -0
  120. package/dist/tone/tone-mark.js.map +1 -1
  121. package/dist/transcription/bopomofo.d.ts +46 -0
  122. package/dist/transcription/bopomofo.d.ts.map +1 -0
  123. package/dist/transcription/bopomofo.js +371 -0
  124. package/dist/transcription/bopomofo.js.map +1 -0
  125. package/dist/transcription/gwoyeu.d.ts +32 -0
  126. package/dist/transcription/gwoyeu.d.ts.map +1 -0
  127. package/dist/transcription/gwoyeu.js +491 -0
  128. package/dist/transcription/gwoyeu.js.map +1 -0
  129. package/dist/transcription/ipa.d.ts +40 -0
  130. package/dist/transcription/ipa.d.ts.map +1 -0
  131. package/dist/transcription/ipa.js +221 -0
  132. package/dist/transcription/ipa.js.map +1 -0
  133. package/dist/transcription/spelling-index.d.ts +23 -0
  134. package/dist/transcription/spelling-index.d.ts.map +1 -0
  135. package/dist/transcription/spelling-index.js +61 -0
  136. package/dist/transcription/spelling-index.js.map +1 -0
  137. package/dist/transcription/wade-giles.d.ts +108 -0
  138. package/dist/transcription/wade-giles.d.ts.map +1 -0
  139. package/dist/transcription/wade-giles.js +656 -0
  140. package/dist/transcription/wade-giles.js.map +1 -0
  141. package/dist/transcription/yale.d.ts +43 -0
  142. package/dist/transcription/yale.d.ts.map +1 -0
  143. package/dist/transcription/yale.js +242 -0
  144. package/dist/transcription/yale.js.map +1 -0
  145. package/package.json +9 -5
package/CHANGELOG.md ADDED
@@ -0,0 +1,87 @@
1
+ # Changelog
2
+
3
+ Notable changes to `@kensio/pinyinjs`. Versions follow
4
+ [semantic versioning](https://semver.org) from 1.0.0 onward.
5
+
6
+ Two things are outside the version contract, for the reasons
7
+ [the API page](docs/api/#stability) gives: the readings a conversion produces,
8
+ which a dictionary rebuild can change, and the artifact format under `data/`.
9
+
10
+ ## 1.0.0
11
+
12
+ The first stable release. Everything the
13
+ [API page](docs/api/) lists is now covered by semantic versioning, including the
14
+ decoder internals and the build pipeline.
15
+
16
+ ### Added since 0.1.0-beta.0
17
+
18
+ - **Transcription systems, all five, in both directions.** Bopomofo, Wade-Giles,
19
+ Yale, Gwoyeu Romatzyh and IPA at the syllable level, plus hanzi → any of them
20
+ end to end via `convert --system` and `convertToWadeGiles`. Checked against
21
+ 417 rows of an outside syllabary in every system — 3,336 cells.
22
+ - **Reading ambiguous Wade-Giles.** `readWadeGilesLoosely` allows for the
23
+ apostrophes and diacritics real texts drop, and `splitWadeGiles` puts back the
24
+ hyphens they lose. Measured: 52.07% of written syllables lose their identity
25
+ without their marks, and taking the first candidate recovers 79.05%.
26
+ - **Numbers read aloud** (`src/numerals/`, the `number` command): counting and
27
+ spelling out, 两 against 二, decimals, percentages, fractions, `yāo`, and
28
+ times. `convert` reads the digits it meets and takes the style from what
29
+ follows them.
30
+ - **Orthography rules.** The 重叠 hyphen for AABB and repeated words, the 成语
31
+ hyphen from a curated list of 117, 老王 → `Lǎo Wáng`, and GB/T 16159 5.1 in
32
+ full — 毛泽东 is `Máo Zédōng`, 北京大学 is `Běijīng Dàxué`, on boundaries
33
+ CC-CEDICT's own capitalisation states.
34
+ - **Typed rule overrides on the lattice** (`READING_RULES`), with the modal 得
35
+ and attested 儿化 as the first two.
36
+ - **Tone colour in the terminal**, in MDBG's palette, on every command that
37
+ writes a syllable.
38
+
39
+ ### Fixed since 0.1.0-beta.0
40
+
41
+ - **一 sandhi is about the 一 that counts.** 十一月 is `shíyīyuè` and 第一次 is
42
+ `dìyīcì`; the citation tone survives a final digit and an ordinal. 561
43
+ conversions change over 88,866 lines, 520 of them right.
44
+ - **A colon between digits is a time**, not an identifier: 6:30 is
45
+ `liù diǎn sānshí fēn`.
46
+ - **The documented proper-noun mechanism was wrong.** The capitals page
47
+ described a 500-entry surname list that has never existed; what produces
48
+ `Lǐ Huá` is two single-character dictionary entries each flagged a proper
49
+ noun.
50
+
51
+ ### Changed
52
+
53
+ - **`engines` is now `>=22.0.0`**, down from `>=24.0.0`. The suite and the
54
+ packaged tarball are both exercised on Node 22 and 24 in CI.
55
+ - **No more `beta` dist-tag.** `pnpm add @kensio/pinyinjs` finds it.
56
+ - `src/romanization/` is `src/transcription/`, since bopomofo has a script of
57
+ its own and IPA is a transcription rather than a spelling. The published docs
58
+ path keeps the older name.
59
+ - The build cleans `dist/` before compiling, and `pnpm pack:check` asserts the
60
+ tarball's contents. The beta tarball shipped 24 stale files from a directory
61
+ that had been renamed away.
62
+
63
+ ### Accuracy at 1.0.0
64
+
65
+ Over the 105-case gold corpus, which is committed to the repository — not to
66
+ the published package — and scored by `pnpm accuracy`:
67
+
68
+ | | lattice | greedy baseline |
69
+ | ---------------- | --------: | --------------: |
70
+ | exact match | **97.1%** | 92.4% |
71
+ | reading accuracy | 99.7% | 98.6% |
72
+ | spacing (F1) | **99.7%** | 97.0% |
73
+
74
+ Every figure in that table is asserted against the scorer by
75
+ `src/changelog.test.ts`, because a number nothing executes goes stale — which is
76
+ what happened to this project's README table across three releases.
77
+
78
+ `pnpm polyphones` reports 89.04% for the lattice against greedy's 88.82% over
79
+ CPP's 20,139 hand-labelled polyphones. That corpus is fetched rather than
80
+ committed, so unlike the table above it cannot be asserted here, and it is
81
+ reported rather than guarded.
82
+
83
+ ## 0.1.0-beta.0
84
+
85
+ First published release: the lattice decoder, the tiered dictionaries, the
86
+ syllable layer, sandhi, GB/T 16159 spacing and capitals, HTML output and the
87
+ `pinyinjs` command.
package/NOTICE CHANGED
@@ -31,3 +31,10 @@ Provides: the bulk of the multi-character word readings
31
31
  Source: https://raw.githubusercontent.com/fxsjy/jieba/master/jieba/dict.txt
32
32
  Licence: MIT, though the dictionary's own provenance is not documented upstream; recorded here as an accepted unknown
33
33
  Provides: word frequencies and the part-of-speech tags that mark proper nouns
34
+
35
+ ## Comparison of Standard Chinese transcription systems (Wikipedia)
36
+
37
+ Source: https://en.wikipedia.org/wiki/Comparison_of_Standard_Chinese_transcription_systems
38
+ Licence: CC BY-SA 4.0
39
+ Provides: the 417-row syllabary in test/fixtures/syllabary.ts, which the bopomofo, Wade-Giles, Yale, IPA and Gwoyeu Romatzyh tables are checked against, GR having four columns of its own
40
+ Note: used by the tests only; nothing in data/ is derived from it
package/README.md CHANGED
@@ -5,18 +5,13 @@ validates and writes pinyin syllables on their own, with no dictionary.
5
5
 
6
6
  [https://pinyinjs.dev](https://pinyinjs.dev "PinyinJS docs website")
7
7
 
8
- > **Beta.** Published under the `beta` tag, so a plain `pnpm add
9
- @kensio/pinyinjs` will not find it until 1.0. Wade-Giles, Bopomofo, Yale and
10
- > IPA, and reading numbers aloud, are not built. Everything below works and is
11
- > tested, but the API will change before 1.0.
12
-
13
8
  ## Install
14
9
 
15
10
  ```bash
16
- pnpm add @kensio/pinyinjs@beta
11
+ pnpm add @kensio/pinyinjs
17
12
  ```
18
13
 
19
- Node 24+, or any browser. The core imports no Node built-ins, and the package is
14
+ Node 22+, or any browser. The core imports no Node built-ins, and the package is
20
15
  ESM only.
21
16
 
22
17
  The dictionaries ship inside the package, which is what makes it a 4 MB
@@ -46,17 +41,25 @@ $ pinyinjs syllable nǐhǎo
46
41
  nǐhǎo nǐ hǎo
47
42
  nǐ n + i, tone 3 nǐ ni3 ni³
48
43
  hǎo h + ao, tone 3 hǎo hao3 hao³
44
+
45
+ $ pinyinjs convert --system wade-giles --notation none 我要去北京。
46
+ Wo yao ch'ü Pei-ching.
47
+
48
+ $ pinyinjs transcribe běijīng
49
+ běijīng běijīng ㄅㄟˇ ㄐㄧㄥ pei³-ching¹ běijīng beeijing pei˨˩˦tɕiŋ˥
49
50
  ```
50
51
 
51
- | Command | Does |
52
- | ---------- | --------------------------------------------------- |
53
- | `convert` | hanzi to pinyin |
54
- | `html` | the same, as HTML |
55
- | `explain` | each syllable, how settled it was, and what it beat |
56
- | `lookup` | what the dictionary holds for a word |
57
- | `syllable` | take written pinyin apart |
58
- | `sandhi` | apply tone sandhi to written pinyin |
59
- | `info` | which dictionary is loaded, and how big it is |
52
+ | Command | Does |
53
+ | ------------ | --------------------------------------------------- |
54
+ | `convert` | hanzi to pinyin |
55
+ | `html` | the same, as HTML |
56
+ | `explain` | each syllable, how settled it was, and what it beat |
57
+ | `lookup` | what the dictionary holds for a word |
58
+ | `syllable` | take written pinyin apart |
59
+ | `sandhi` | apply tone sandhi to written pinyin |
60
+ | `number` | read a number as Chinese numerals |
61
+ | `transcribe` | pinyin to bopomofo, Wade-Giles, Yale, GR and IPA |
62
+ | `info` | which dictionary is loaded, and how big it is |
60
63
 
61
64
  Every conversion option below is a flag: `--notation`, `--locale`,
62
65
  `--apostrophe`, `--capitals`, `--punctuation`, `--no-grouping`, `--third-tone`,
@@ -66,6 +69,12 @@ A command given no arguments reads standard input, one text per line, so
66
69
  `cat article.txt | pinyinjs convert` works. `syllable` and `sandhi` need no
67
70
  dictionary at all and start without loading one.
68
71
 
72
+ At a terminal each syllable is written in its tone's colour, in
73
+ [MDBG](https://www.mdbg.net)'s palette — red, yellow, green, blue, and the
74
+ terminal's own colour for the neutral tone. It is off for a pipe, `NO_COLOR` is
75
+ honoured, `--colour` and `--no-colour` force it either way, and `--json` is
76
+ never coloured. See [the command line](docs/cli/#colour).
77
+
69
78
  ### Calling it from something else
70
79
 
71
80
  `convert` writes the pinyin and nothing else, so it drops straight into a
@@ -140,9 +149,20 @@ first, convert with it, and reload as `full` arrives.
140
149
  convert(dictionary, "银行"); // "yínháng"
141
150
  convert(dictionary, "行长"); // "hángzhǎng"
142
151
  convert(dictionary, "我要去北京。"); // "Wǒ yào qù Běijīng."
143
- convert(dictionary, "3D银行"); // "3Dyínháng" — non-Han text is left as written
152
+ convert(dictionary, "3D银行"); // "sān D yínháng" — the digit is read, the letter is not
144
153
  ```
145
154
 
155
+ A reading the dictionary cannot settle on its own is settled by context, with
156
+ typed rules over the lattice rather than tweaks to the output:
157
+
158
+ ```ts
159
+ convert(dictionary, "我得走了"); // "wǒ děi zǒule" — modal 得
160
+ convert(dictionary, "他跑得很快"); // "tā pǎo de hěn kuài" — the particle
161
+ convert(dictionary, "那边儿"); // "nà biānr" — 儿 does not stand on its own
162
+ ```
163
+
164
+ See [converting](docs/converting/#rules-where-the-cost-model-cannot-reach).
165
+
146
166
  ### Options
147
167
 
148
168
  ```ts
@@ -157,6 +177,8 @@ convert(dictionary, text, { notation: "numbers", capitals: "none" });
157
177
  | `capitals` | `"auto"` | `"auto"`, `"proper"`, `"none"` |
158
178
  | `punctuation` | `"latin"` | `"latin"`, `"keep"` |
159
179
  | `grouping` | `true` | `false` turns off GB/T 16159 word spacing |
180
+ | `numbers` | `"read"` | `"keep"` leaves every digit as it was written |
181
+ | `numbers` | `"read"` | `"keep"` leaves every digit as it was written |
160
182
  | `sandhi` | `{ yiBu: true, thirdTone: false }` | `{ yiBu?: boolean; thirdTone?: boolean }` |
161
183
 
162
184
  ```ts
@@ -218,8 +240,36 @@ The list is not a complete 正词法 implementation, so some words it does not
218
240
  cover are written differently: 不但 is `búdàn`, 大米 is `dàmǐ`, 青海 is
219
241
  `Qīnghǎi`.
220
242
 
221
- **Non-Han text** — digits, Latin letters, punctuation — passes through exactly
222
- as written. Reading numbers aloud is a separate package, not yet built.
243
+ **Reduplication** takes a hyphen rather than a space, since it is one word with
244
+ a boundary inside it.
245
+
246
+ ```ts
247
+ convert(dictionary, "干干净净"); // "gāngān-jìngjìng"
248
+ convert(dictionary, "研究研究"); // "yánjiū-yánjiū"
249
+ convert(dictionary, "爸爸妈妈"); // "bàba māma" — that shape, but two words
250
+ ```
251
+
252
+ **A 成语 that can be read as two disyllables** takes the same hyphen, from a
253
+ curated list of 117; the rest are written solid, as the standard writes them.
254
+
255
+ ```ts
256
+ convert(dictionary, "风平浪静"); // "fēngpíng-làngjìng"
257
+ convert(dictionary, "不亦乐乎"); // "búyìlèhū" — cannot be halved
258
+ ```
259
+
260
+ **Digits are read**, and the rest of a non-Han run passes through exactly as
261
+ written:
262
+
263
+ ```ts
264
+ convert(dictionary, "我有3个苹果。"); // "Wǒ yǒu sān gè píngguǒ."
265
+ convert(dictionary, "1988年之后"); // "yī jiǔ bā bā nián zhīhòu"
266
+ convert(dictionary, "95%的人"); // "bǎifēnzhījiǔshíwǔ de rén"
267
+ convert(dictionary, "3D打印"); // "sān D dǎyìn"
268
+ convert(dictionary, "6:30起床"); // "liù diǎn sānshí fēn qǐchuáng"
269
+ convert(dictionary, "16:9的"); // "16:9de" — a ratio is not a quantity
270
+ ```
271
+
272
+ `numbers: "keep"` leaves every digit alone. See [numbers](docs/numerals/).
223
273
 
224
274
  ## Syllable by syllable
225
275
 
@@ -384,6 +434,77 @@ toneFromMarks("hǎo"); // 3
384
434
  `de` in 我的 is neutral (5), whereas the `bei` in a typed `beijing` has no tone
385
435
  written at all.
386
436
 
437
+ ## Numbers
438
+
439
+ Reading a number needs no dictionary — arithmetic and about twenty readings —
440
+ so this works with nothing loaded.
441
+
442
+ ```ts
443
+ import { numeralHanzi, percentHanzi, readNumeral } from "@kensio/pinyinjs";
444
+
445
+ numeralHanzi(12345); // "一万两千三百四十五"
446
+ numeralHanzi(1005); // "一千零五" — a skipped place is spoken
447
+ numeralHanzi(2000); // "两千" — a leading lone 2 before a big unit
448
+ percentHanzi(95); // "百分之九十五" — the order reverses
449
+ ```
450
+
451
+ The same digits are read two ways and nothing in the number says which — 2026年
452
+ is spelled out and 2026个 is counted — so the style is the caller's:
453
+
454
+ ```ts
455
+ numeralHanzi(2026); // "两千零二十六"
456
+ numeralHanzi(2026, { style: "digits" }); // "二〇二六"
457
+ readNumeral(110, { style: "digits", yao: true }); // yāo yāo líng
458
+ ```
459
+
460
+ More in [numbers](docs/numerals/).
461
+
462
+ ## Bopomofo, Wade-Giles, Yale, Gwoyeu Romatzyh and IPA
463
+
464
+ Also dictionary-free: a romanisation is a mapping over about 420 syllables, so
465
+ hanzi → Wade-Giles is hanzi → pinyin → Wade-Giles.
466
+
467
+ ```ts
468
+ import {
469
+ readSyllable,
470
+ readWadeGilesLoosely,
471
+ writeBopomofo,
472
+ writeGwoyeu,
473
+ writeIpa,
474
+ writeWadeGiles,
475
+ writeYale,
476
+ } from "@kensio/pinyinjs";
477
+
478
+ const jiu = readSyllable("jiù");
479
+ writeBopomofo(jiu); // "ㄐㄧㄡˋ"
480
+ writeWadeGiles(jiu); // "chiu⁴"
481
+ writeYale(jiu); // "jyòu"
482
+ writeGwoyeu(jiu); // "jiow"
483
+ writeIpa(jiu); // "tɕiou˥˩"
484
+ ```
485
+
486
+ Gwoyeu Romatzyh is the odd one, and it needs no tone mark because the tone is
487
+ spelled into the syllable — which is why 陝西 is Shaanxi in English and 山西 is
488
+ Shanxi:
489
+
490
+ ```ts
491
+ const gr = (pinyin: string) => writeGwoyeu(readSyllable(pinyin));
492
+ [gr("shān"), gr("shán"), gr("shǎn"), gr("shàn")]; // shan, sharn, shaan, shann
493
+ ```
494
+
495
+ Reading Wade-Giles back gives an **array**, because real text drops the
496
+ apostrophes and diacritics that carry the distinctions:
497
+
498
+ ```ts
499
+ readWadeGilesLoosely("chi¹"); // [jī, qī] — chi is jī, ch'i is qī
500
+ readWadeGilesLoosely("chu¹"); // [zhū, chū, jū, qū]
501
+ ```
502
+
503
+ Measured over the phrase corpus, 52.07% of written syllables have a Wade-Giles
504
+ spelling that merges with another once its marks are dropped, and taking the
505
+ first candidate recovers 79.05% of them. More in
506
+ [romanisation](docs/romanization/).
507
+
387
508
  ## Sandhi
388
509
 
389
510
  The dictionary stores underlying tones, and sandhi is applied to the syllable
@@ -431,6 +552,10 @@ Both scripts are dictionary keys, so only the locale is an option to pass.
431
552
  | `readSyllable` / `writeSyllable` / `isSyllable` | one syllable, no dictionary |
432
553
  | `splitSyllables` / `readWord` | split written pinyin |
433
554
  | `applySandhi` | 一, 不 and optional third-tone sandhi |
555
+ | `writeBopomofo` / `writeWadeGiles` | one syllable, romanised |
556
+ | `writeYale` / `writeGwoyeu` / `writeIpa` | the same, in Yale, GR and IPA |
557
+ | `readBopomofo` / `readWadeGilesLoosely` | and back again |
558
+ | `readYale` / `readGwoyeu` / `readIpa` | and back from those three |
434
559
  | `applyToneMark` / `stripToneMarks` / `toneFromMarks` | tone marks |
435
560
  | `convertGreedily(...)` | the old longest-match decoder, kept as a baseline |
436
561