which-dialect 0.1.1 → 0.2.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.
- package/README.md +73 -7
- package/dist/check.d.ts +45 -0
- package/dist/check.js +369 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +9 -0
- package/dist/types.d.ts +29 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,6 +18,8 @@ await tr.translate('cool', { from: 'en', to: 'vi', toRegion: 'Southern', meaning
|
|
|
18
18
|
|
|
19
19
|
Each result is a list of **groups, one per meaning** of the source word, each with its part of speech, its definitions and its translations, so a word's meanings never get mixed up.
|
|
20
20
|
|
|
21
|
+
**Data:** Wiktionary data of 2026-09-25 for both languages. See [`DATA_UPDATES.md`](DATA_UPDATES.md) for each language's dates and update history.
|
|
22
|
+
|
|
21
23
|
You install one small package. Dictionary data is **not bundled**: each lookup fetches one small file (usually 5–100 kB compressed) from the language's data package on the jsDelivr CDN, and caches it.
|
|
22
24
|
|
|
23
25
|
## Languages
|
|
@@ -60,11 +62,12 @@ Loads each language's data on demand. The options are only needed to self-host t
|
|
|
60
62
|
| `exclude` | Labels to leave out (default: vulgar, offensive, derogatory, archaic, obsolete, dated, historical, rare, abbreviation). |
|
|
61
63
|
| `limit` | Translations per meaning (default 5). |
|
|
62
64
|
| `allSenses` | Return every meaning, including ones with no translation, instead of only useful ones. For letting users pick a meaning. |
|
|
65
|
+
| `picks` | Use the target language's [hand-picked words](#hand-picked-words) (default `true`). `false` returns the ranking alone. |
|
|
63
66
|
|
|
64
67
|
It returns `TranslationGroup[]`, most relevant meaning first. Each group has:
|
|
65
68
|
|
|
66
69
|
- `source`: the meaning being translated: `lemma` (the base or standard word, e.g. *say* for *said*), `pos`, `glosses` (its English definitions), `regions`, `labels`, and `via` when the word led there ("simple past of say", "Southern Vietnam form of không").
|
|
67
|
-
- `translations`: target words, best first, each with its own `pos`, `gloss`, `regions`, `labels`, a `score`, and `bridge` (how it was found; `'pronouns'` for the listener's words, which also carry `relationship`, e.g. "Your parents").
|
|
70
|
+
- `translations`: target words, best first, each with its own `pos`, `gloss`, `regions`, `labels`, a `score`, and `bridge` (how it was found; `'picked'` for [hand-picked words](#hand-picked-words); `'pronouns'` for the listener's words, which also carry `relationship`, e.g. "Your parents").
|
|
68
71
|
- `pronounUses` (source words in the source language's pronoun table): the relationships the word is used in with this meaning, `{ id, label, person: 'self' | 'addressee', speaker?, regions, note?, warning? }`. Uses no definition covers get a group of their own, translated as "I" or "you": Vietnamese *em* → "I/me, when talking to: someone a bit older (man); …; your teacher" and "you, when talking to: someone younger; …"; *ba* → "you, when talking to: your parents". Pass `pos: 'pron'` to get only these.
|
|
69
72
|
- `relationships` (singular "I"/"you" meanings into a language with a pronoun table): the word for each relationship, `{ id, label, words, warning? }`, filtered by `toRegion`, `speaker` and `exclude`, for showing "it depends on who you're talking to".
|
|
70
73
|
|
|
@@ -83,19 +86,69 @@ Every language's data defines its words in English, so English is the bridge bet
|
|
|
83
86
|
5. **Within one language, synonyms are direct equivalents.** Wiktionary lists dialect words as synonyms (*ngô* → *bắp*, *lợn* → *heo*, *lift* → *elevator*), in either direction (*dạ* lists *vâng*).
|
|
84
87
|
6. **Ranking** favors **common words** ([word frequencies](#word-frequencies): *anh* over the niche *cô nương* for "you"), target words whose definition shares the source definition's details, whose *main* meaning is the match, that are tagged for the target region, and that keep the **register**: a polite word translates to a polite word (Northern *vâng* → Southern *dạ*), slang to slang.
|
|
85
88
|
|
|
89
|
+
**Hand-picked words** come before all of these for the meanings that have them (see below).
|
|
90
|
+
|
|
86
91
|
Meanings are ordered by `meaning` (if given), then by being tagged for `fromRegion`, then the dictionary's own order, which lists main meanings first. That order is a guess when a word has several parts of speech: English *just* is listed as an adjective ("fair") before the adverb. Pass `pos` or `meaning` when you know which one you want.
|
|
87
92
|
|
|
93
|
+
### Hand-picked words
|
|
94
|
+
|
|
95
|
+
For common words, a speaker of the target language knows the natural word even where the data ranks another first: *get* "To fetch, bring, take" ranks Vietnamese *đưa* ("to hand, to bring") above *lấy*, because *đưa*'s definition matches more of the English words and no *lấy* definition says "fetch". Each language can list **hand-picked words for English meanings** (`picks` in `languages/<code>.ts`, built into `picks.json`). In a meaning that has picks, the picked words come first (`bridge: 'picked'`), then the ranked words; other meanings are unchanged. Picks can carry a region tag, like translation tables: with `toRegion`, picks tagged for it come first and picks tagged only for other regions are left to the ranking. A pick can also mark its meaning as the one to put first when no `pos` or `meaning` is given.
|
|
96
|
+
|
|
97
|
+
Picks are keyed by English meaning, so each language needs one list however many languages there are: translating from any other language reaches them through the English meaning (as with translation tables). They're optional: a language without picks uses the ranking alone. `picks: false` shows the ranking alone, and `npm run evaluate` reports both. Vietnamese picks so far: *get* "fetch" → *lấy*, *mang*; *get* "obtain" → *lấy*.
|
|
98
|
+
|
|
88
99
|
### How accurate is it?
|
|
89
100
|
|
|
90
101
|
`npm run evaluate` runs known-correct translations across language pairs and dialect pairs (English → Vietnamese by region, including pronouns, Northern → Southern Vietnamese, US ↔ UK, Vietnamese → English). A case passes when a correct word is in the top 3 of the first meaning:
|
|
91
102
|
|
|
92
103
|
| Set | Correct word in top 3 | Correct word first |
|
|
93
104
|
|---|---|---|
|
|
94
|
-
| Tuning set (used while developing) |
|
|
105
|
+
| Tuning set (used while developing) | 49 / 49 (100%) | 49 / 49 (100%); 48 / 49 without picks |
|
|
95
106
|
| Held-out set (written afterwards, not tuned against) | 17 / 17 (100%) | 16 / 17 (94%) |
|
|
96
107
|
|
|
97
108
|
The cases involving Spanish were removed with it. The held-out set has since been looked at, so new held-out cases are needed before relying on it (see [`FUTURE_IMPROVEMENTS.md`](FUTURE_IMPROVEMENTS.md)).
|
|
98
109
|
|
|
110
|
+
## Checking text
|
|
111
|
+
|
|
112
|
+
### `createChecker({ baseUrl?, load?, dictionary? })` → `checker.check(text, options)`
|
|
113
|
+
|
|
114
|
+
Checks a learner's sentence in one language and returns issues with the exact place in the text, a short explanation and suggested fixes. It runs in the browser like the rest of the library: no API calls, no cost.
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import { createChecker } from 'which-dialect'
|
|
118
|
+
|
|
119
|
+
const checker = createChecker()
|
|
120
|
+
const { text, issues } = await checker.check('Tôi muốn ăn thịt lợn và bắp khong?', {
|
|
121
|
+
lang: 'vi', region: 'Southern', listener: 'parent',
|
|
122
|
+
})
|
|
123
|
+
// “Tôi”: Talking to your parents, say “con” for “I”, not “Tôi”. → Con
|
|
124
|
+
// “lợn”: “lợn” is Northern; in Southern Vietnamese, say “heo”. → heo, cúi, ỉn
|
|
125
|
+
// “khong”: “khong” isn't a Vietnamese syllable. Did you mean “không”, “khổng” or “khống”?
|
|
126
|
+
// “khong”: Talking to your parents, end the sentence with “ạ” to sound polite. (suggestion)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
| Option | Meaning |
|
|
130
|
+
|---|---|
|
|
131
|
+
| `lang` | The text's language. |
|
|
132
|
+
| `region` | The region the text should be in. Without it, words that don't fit the rest of the text's region are pointed out (*lợn*, Northern, next to *nha*, Southern). |
|
|
133
|
+
| `listener` | Who you're talking to, as a [pronoun-table](#looking-words-up) row id (`'parent'`, `'teacher'`, `'friend'`, …): pronouns and polite endings are checked against it. |
|
|
134
|
+
| `about` | Who you're talking about, for "he/she" and "they" pronouns. |
|
|
135
|
+
| `speaker` | `'male'` or `'female'`, for pronouns that depend on it. |
|
|
136
|
+
| `rules` | Turn checks off, e.g. `{ 'polite-ending': false }`. |
|
|
137
|
+
|
|
138
|
+
Each issue has `rule`, `severity` (`'error'`: not a word of the language; `'warning'`: wrong for the chosen region or relationship; `'suggestion'`: often better), `start`/`end` (offsets into the returned `text`, the input in Unicode NFC form), `text`, `message` and `suggestions` (replacements for `text`, best first).
|
|
139
|
+
|
|
140
|
+
The checks:
|
|
141
|
+
|
|
142
|
+
- **`spelling`**: syllables that aren't in the language, with the accented forms they could be (*khong* → *không*). Plain-letter words with no accented form are left alone (names, foreign words). Languages written in syllables only (Vietnamese).
|
|
143
|
+
- **`dialect`**: words whose main meaning is tagged for another region (*lợn* in Southern text → *heo*; *muỗng* in Northern → *thìa*; in English, US *apartment* in UK text → *flat*). Words with an untagged main meaning are never flagged (*má* is "cheek" everywhere).
|
|
144
|
+
- **`pronoun-relationship`**: pronouns that don't fit the listener (*tôi* to a parent → *con*; to a teacher → *em*).
|
|
145
|
+
- **`pronoun-pair`**: "I" and "you" pronouns that never go together (*tao* goes with *mày*, not *bạn*), when no listener is given.
|
|
146
|
+
- **`polite-ending`**: a suggestion to end sentences with *ạ* when talking to parents, elders and teachers (not for exclamations).
|
|
147
|
+
|
|
148
|
+
The checker only speaks up when it's confident, so **no issues means none of the checks found anything, not that the text is correct**. It doesn't check word order, classifiers or whether a sentence sounds natural yet. Only reliable pronouns are checked: kinship words like *con* (also "child", and a classifier in *con chó*) aren't, and *bạn* (also "friend") only gets suggestions.
|
|
149
|
+
|
|
150
|
+
**Every language uses the same checks.** They read only the language's data and its `checker` settings in `languages/<code>.ts` (how text splits into words, which pronouns are reliable, polite endings), so a check whose data a language doesn't have is skipped. A language without checker settings still gets the `dialect` check.
|
|
151
|
+
|
|
99
152
|
## Looking words up
|
|
100
153
|
|
|
101
154
|
### `createDictionary({ lang, baseUrl?, load? })`
|
|
@@ -104,6 +157,8 @@ One language's dictionary. `baseUrl` defaults to `https://cdn.jsdelivr.net/npm/w
|
|
|
104
157
|
|
|
105
158
|
- `dictionary.lookup(word)`: all entries for a word, with every sense's definitions, regions, labels, synonyms and variant links (`altOf`).
|
|
106
159
|
- `dictionary.searchEnglish(term, { region?, pos?, exclude?, limit?, allSenses? })`: words for an English term in this language, one per word, best first (no meaning handling: use the translator for that). `allSenses: true` returns every matching sense of those words instead.
|
|
160
|
+
- `dictionary.checker()` / `dictionary.syllables()`: the language's checker settings and syllable list (with frequencies), used by `createChecker` (`{}` for languages without them).
|
|
161
|
+
- `dictionary.picks()`: the language's [hand-picked words](#hand-picked-words), `{ word, pos, gloss, picks: [{ word, tags? }], first? }` per English meaning (empty for languages without any).
|
|
107
162
|
- `dictionary.meta()`: the language's name, regions, region groups, source, license, build date and counts.
|
|
108
163
|
- `dictionary.pronouns({ listener?, region?, speaker?, exclude? })`: how to say "I", "you", "he/she", "we", plural "you" and "they" depending on who you're talking to (or about), one row per relationship, for languages whose pronouns depend on it (so far Vietnamese; empty for others). Each row has the columns `self`, `addressee`, `third`, `selfPlural`, `addresseePlural` and `thirdPlural`. Each word says where it comes from: a dictionary definition (`source: 'gloss'`, with the `gloss`), a grammar rule for a regular compound the dictionary doesn't list (`source: 'rule'`, e.g. *các anh*, with a `note`), or a hand-written override (`source: 'override'`, with a `note`); and, when it has one, the `gender` of the person it refers to and whether a "we" is `inclusive`.
|
|
109
164
|
|
|
@@ -172,17 +227,26 @@ If you **redistribute modified data**, it has to stay under CC BY-SA 4.0. Using
|
|
|
172
227
|
|
|
173
228
|
Requires Node 22.6+ (TypeScript scripts run with `--experimental-strip-types`).
|
|
174
229
|
|
|
230
|
+
Generated data (`packages/<code>/data`) isn't committed, so a fresh clone needs it first. Two ways:
|
|
231
|
+
|
|
232
|
+
- **Quick start: `npm run fetch:data`.** Downloads the published data packages from npm (a few seconds), then rebuilds hand-picked words from the repo's configs. You get exactly the published data, so tests and `npm run evaluate` match the numbers in this README. Use this unless you're changing how data is built.
|
|
233
|
+
- **Build from Wiktionary: `npm run build:data:all`.** Downloads Kaikki's files (English 3.3 GB, Vietnamese 79 MB; cached in `.cache/`) and builds every language, **English first**: English is the bridge between languages, and other languages' hand-picked words are checked against it. Kaikki always serves its latest dump, so this can be newer than the published data; the build records the dates in [`DATA_UPDATES.md`](DATA_UPDATES.md). Building one language alone (`npm run build:data -- vi`) also works once English is built.
|
|
234
|
+
|
|
175
235
|
```sh
|
|
176
236
|
npm install
|
|
177
|
-
npm run
|
|
178
|
-
npm run build:data
|
|
237
|
+
npm run fetch:data # quick start: the published data (-- --force replaces data you already have)
|
|
238
|
+
npm run build:data:all # or build every language from Kaikki, English first (-- --refresh re-downloads)
|
|
239
|
+
npm run build:data -- vi # build one language (after English); -- vi --refresh re-downloads first
|
|
179
240
|
npm test # unit tests, plus checks against whichever languages are built
|
|
180
|
-
npm run evaluate # translation accuracy on known-correct cases (-- --verbose to see every case)
|
|
241
|
+
npm run evaluate # translation accuracy on known-correct cases, with and without picks (-- --verbose to see every case)
|
|
242
|
+
npm run build:picks -- vi # rebuild only a language's hand-picked words (after editing `picks` in languages/vi.ts)
|
|
243
|
+
npm run picks-sheet -- vi # review sheet (.cache/picks-vi.csv) of the top English words' meanings; -- --words file.txt, -- --top 500
|
|
244
|
+
npm run picks-sheet -- vi --apply sheet.csv # config rows for the rows whose your_pick is filled in
|
|
181
245
|
npm run typecheck
|
|
182
246
|
npm run build # compile the API to packages/core/dist
|
|
183
247
|
```
|
|
184
248
|
|
|
185
|
-
Generated data isn't committed (it would bloat git history); it's built before publishing.
|
|
249
|
+
Generated data isn't committed (it would bloat git history); it's built before publishing. Builds take seconds (English: about 25 s and 1.4 GB of memory).
|
|
186
250
|
|
|
187
251
|
### Adding a language
|
|
188
252
|
|
|
@@ -190,13 +254,15 @@ Generated data isn't committed (it would bloat git history); it's built before p
|
|
|
190
254
|
2. Copy a data package (`packages/vi`) to `packages/<code>` and update its `package.json` and `README.md`.
|
|
191
255
|
3. Add the language's code to `translationLangs` in `languages/en.ts` (so English keeps its translation tables for it) and rebuild English.
|
|
192
256
|
4. `npm run build:data -- <code>`, check the results, add a few real-data tests and evaluation cases, and publish.
|
|
257
|
+
5. Optional: [checker](#checking-text) settings (`checker` in `languages/<code>.ts`): `units: 'syllables'` for languages written in syllables (also builds the syllable list for accent suggestions), `maxWordUnits`, the reliable `pronouns` per pronoun-table column, `ambiguousPronouns`, and `politeEndings`. Without them, the language is read as space-separated words and gets the region check.
|
|
258
|
+
6. Optional, later: [hand-picked words](#hand-picked-words). A speaker runs `npm run picks-sheet -- <code>`, fills in `your_pick` only where the first choice is wrong, and pastes the `--apply` output into `picks`.
|
|
193
259
|
|
|
194
260
|
Languages in non-Latin scripts (Chinese, Arabic, Russian, …) will need a script-aware version of `shardKey` first: today files are split by the first two Latin letters.
|
|
195
261
|
|
|
196
262
|
### Publishing
|
|
197
263
|
|
|
198
264
|
```sh
|
|
199
|
-
npm run build:data --
|
|
265
|
+
npm run build:data:all -- --refresh && npm test # records the new dates in DATA_UPDATES.md: commit it
|
|
200
266
|
npm publish -w which-dialect-en
|
|
201
267
|
npm publish -w which-dialect-vi
|
|
202
268
|
npm publish -w which-dialect
|
package/dist/check.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type TranslatorOptions } from './translate.ts';
|
|
2
|
+
export type CheckRule = 'spelling' | 'dialect' | 'pronoun-relationship' | 'pronoun-pair' | 'polite-ending';
|
|
3
|
+
export type CheckIssue = {
|
|
4
|
+
rule: CheckRule;
|
|
5
|
+
/**
|
|
6
|
+
* 'error': almost certainly wrong (not a word of the language). 'warning': wrong for the chosen region
|
|
7
|
+
* or relationship. 'suggestion': often better; worth a look.
|
|
8
|
+
*/
|
|
9
|
+
severity: 'error' | 'warning' | 'suggestion';
|
|
10
|
+
/** Character offsets of `text` in the checked text (`CheckResult.text`, the input in Unicode NFC form). */
|
|
11
|
+
start: number;
|
|
12
|
+
end: number;
|
|
13
|
+
/** The text the issue is about. */
|
|
14
|
+
text: string;
|
|
15
|
+
/** One sentence for the learner, e.g. “khong” isn't a Vietnamese syllable. Did you mean “không”? */
|
|
16
|
+
message: string;
|
|
17
|
+
/** Replacements for `text`, best first; empty when there's nothing specific to suggest. */
|
|
18
|
+
suggestions: string[];
|
|
19
|
+
};
|
|
20
|
+
export type CheckOptions = {
|
|
21
|
+
/** Language code of the text, e.g. "vi". */
|
|
22
|
+
lang: string;
|
|
23
|
+
/** Region or region group the text should be in, e.g. "Southern". Without it, mixed regions are flagged. */
|
|
24
|
+
region?: string;
|
|
25
|
+
/** Who you're talking to, as a pronoun-table row id (see Dictionary.pronouns), e.g. "parent". */
|
|
26
|
+
listener?: string;
|
|
27
|
+
/** Who you're talking about, as a pronoun-table row id, for "he/she" and "they" pronouns. */
|
|
28
|
+
about?: string;
|
|
29
|
+
/** The speaker's gender, for pronouns that depend on it. */
|
|
30
|
+
speaker?: 'male' | 'female';
|
|
31
|
+
/** Turn rules off, e.g. { 'polite-ending': false }. All are on by default. */
|
|
32
|
+
rules?: Partial<Record<CheckRule, boolean>>;
|
|
33
|
+
};
|
|
34
|
+
export type CheckResult = {
|
|
35
|
+
/** The checked text: the input in Unicode NFC form, which the issues' offsets refer to. */
|
|
36
|
+
text: string;
|
|
37
|
+
/** Sorted by position. */
|
|
38
|
+
issues: CheckIssue[];
|
|
39
|
+
};
|
|
40
|
+
export type Checker = {
|
|
41
|
+
check(text: string, options: CheckOptions): Promise<CheckResult>;
|
|
42
|
+
};
|
|
43
|
+
/** Same as the translator's: where each language's data comes from. */
|
|
44
|
+
export type CheckerOptions = TranslatorOptions;
|
|
45
|
+
export declare function createChecker(options?: CheckerOptions): Checker;
|
package/dist/check.js
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
// Checking text in one language: spelling and accents, words from another region, and pronouns and
|
|
2
|
+
// polite endings that fit who you're talking to.
|
|
3
|
+
//
|
|
4
|
+
// Every rule reads only the language's data: its dictionary, pronoun table, checker.json and
|
|
5
|
+
// syllables.json (see CheckerConfig). Adding a language means adding data and settings in
|
|
6
|
+
// languages/<lang>.ts, not code here. A rule whose data a language doesn't have is skipped, so a language
|
|
7
|
+
// without checker settings is still checked for words from another region.
|
|
8
|
+
//
|
|
9
|
+
// The checker only speaks up when it's confident and stays silent otherwise, so no issues means none of
|
|
10
|
+
// the checks found anything, not that the text is correct.
|
|
11
|
+
import { createDictionary, resolveRegion } from "./index.js";
|
|
12
|
+
import { createTranslator } from "./translate.js";
|
|
13
|
+
import { PRONOUN_PERSONS, } from "./types.js";
|
|
14
|
+
// How each pronoun-table column is named in messages.
|
|
15
|
+
const PERSON_NAMES = {
|
|
16
|
+
self: 'I', addressee: 'you', third: 'he/she', selfPlural: 'we', addresseePlural: 'you (plural)', thirdPlural: 'they',
|
|
17
|
+
};
|
|
18
|
+
// Columns chosen by `about` (who you're talking about); the rest by `listener`.
|
|
19
|
+
const ABOUT_PERSONS = new Set(['third', 'thirdPlural']);
|
|
20
|
+
const SELF_PERSONS = new Set(['self', 'selfPlural']);
|
|
21
|
+
const ADDRESSEE_PERSONS = new Set(['addressee', 'addresseePlural']);
|
|
22
|
+
// A run of letters (with their accents), digits and apostrophes: a word, or a syllable in syllable languages.
|
|
23
|
+
const UNIT = /[\p{L}\p{M}\p{N}'’]+/gu;
|
|
24
|
+
const SENTENCE_END = /[.!?…]+|\n+/g;
|
|
25
|
+
const quote = (s) => `“${s}”`;
|
|
26
|
+
const lowerFirst = (s) => s.charAt(0).toLowerCase() + s.slice(1);
|
|
27
|
+
const listOf = (items, conjunction = 'and') => items.length <= 1 ? items.join('') : `${items.slice(0, -1).join(', ')} ${conjunction} ${items[items.length - 1]}`;
|
|
28
|
+
// Keeps a capital first letter when replacing a capitalized word ("Tôi" → "Con").
|
|
29
|
+
const matchCase = (original, replacement) => original.charAt(0) !== original.charAt(0).toLowerCase() ? replacement.charAt(0).toUpperCase() + replacement.slice(1) : replacement;
|
|
30
|
+
// Without accents, for finding the accented forms of a syllable: "không" → "khong", "đi" → "di".
|
|
31
|
+
const plain = (s) => s.normalize('NFD').replace(/\p{M}/gu, '').replace(/đ/g, 'd').replace(/Đ/g, 'D').normalize('NFC');
|
|
32
|
+
const unique = (items) => [...new Set(items)];
|
|
33
|
+
function units(text) {
|
|
34
|
+
return [...text.matchAll(UNIT)].map((m) => ({ text: m[0], start: m.index, end: m.index + m[0].length }));
|
|
35
|
+
}
|
|
36
|
+
function sentencesOf(text, all) {
|
|
37
|
+
const ends = [...text.matchAll(SENTENCE_END)].map((m) => m.index);
|
|
38
|
+
const out = [[]];
|
|
39
|
+
let next = 0;
|
|
40
|
+
for (const u of all) {
|
|
41
|
+
while (next < ends.length && ends[next] < u.start) {
|
|
42
|
+
if (out[out.length - 1].length)
|
|
43
|
+
out.push([]);
|
|
44
|
+
next++;
|
|
45
|
+
}
|
|
46
|
+
out[out.length - 1].push(u);
|
|
47
|
+
}
|
|
48
|
+
return out.filter((s) => s.length);
|
|
49
|
+
}
|
|
50
|
+
// Units join into one word only when just spaces separate them ("thịt heo", not "thịt, heo").
|
|
51
|
+
const joined = (text, a, b) => /^[^\S\n]+$/.test(text.slice(a.end, b.start));
|
|
52
|
+
// Longest match against the dictionary: at each position, the longest run of units that's a headword
|
|
53
|
+
// ("kết quả" is one word, so its "quả" isn't checked on its own). Unknown single units become words with
|
|
54
|
+
// no entries.
|
|
55
|
+
async function segment(text, list, maxUnits, lookup) {
|
|
56
|
+
const words = [];
|
|
57
|
+
for (let i = 0; i < list.length;) {
|
|
58
|
+
let taken = null;
|
|
59
|
+
for (let k = Math.min(maxUnits, list.length - i); k >= 1; k--) {
|
|
60
|
+
const span = list.slice(i, i + k);
|
|
61
|
+
if (span.some((u, j) => j > 0 && !joined(text, span[j - 1], u)))
|
|
62
|
+
continue;
|
|
63
|
+
const phrase = span.map((u) => u.text).join(' ');
|
|
64
|
+
const entries = /\d/.test(phrase) ? [] : await lookup(phrase);
|
|
65
|
+
if (entries.length || k === 1) {
|
|
66
|
+
const start = span[0].start;
|
|
67
|
+
const end = span[k - 1].end;
|
|
68
|
+
taken = { text: text.slice(start, end), lower: phrase.toLowerCase(), start, end, units: span, entries };
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
words.push(taken);
|
|
73
|
+
i += taken.units.length;
|
|
74
|
+
}
|
|
75
|
+
return words;
|
|
76
|
+
}
|
|
77
|
+
// --- Rules -----------------------------------------------------------------------------------------------
|
|
78
|
+
// Spelling and accents, for languages written in syllables with a syllable list: a syllable that isn't in
|
|
79
|
+
// the language is flagged, with the accented forms it could be ("khong" → "không"). Plain-letter
|
|
80
|
+
// syllables with no accented form are left alone: usually a name or a foreign word ("email").
|
|
81
|
+
async function spelling(ctx) {
|
|
82
|
+
if (ctx.config.units !== 'syllables' || !ctx.meta.syllables)
|
|
83
|
+
return [];
|
|
84
|
+
const syllables = await ctx.dict.syllables();
|
|
85
|
+
const byPlain = new Map();
|
|
86
|
+
for (const s of Object.keys(syllables))
|
|
87
|
+
byPlain.set(plain(s), [...(byPlain.get(plain(s)) ?? []), s]);
|
|
88
|
+
for (const list of byPlain.values())
|
|
89
|
+
list.sort((a, b) => syllables[b] - syllables[a]);
|
|
90
|
+
const issues = [];
|
|
91
|
+
for (const word of ctx.sentences.flatMap((s) => s.words)) {
|
|
92
|
+
if (word.entries.length)
|
|
93
|
+
continue;
|
|
94
|
+
for (const u of word.units) {
|
|
95
|
+
const lower = u.text.toLowerCase().replace(/[’']/g, '');
|
|
96
|
+
if (/\d/.test(lower) || lower in syllables)
|
|
97
|
+
continue;
|
|
98
|
+
const candidates = (byPlain.get(plain(lower)) ?? []).slice(0, 3);
|
|
99
|
+
const accented = lower !== plain(lower);
|
|
100
|
+
if (!candidates.length && !accented)
|
|
101
|
+
continue;
|
|
102
|
+
issues.push({
|
|
103
|
+
rule: 'spelling',
|
|
104
|
+
// Missing accents on plain letters is the common learner slip; a wrong accent is more surely wrong.
|
|
105
|
+
severity: accented ? 'error' : 'warning',
|
|
106
|
+
start: u.start,
|
|
107
|
+
end: u.end,
|
|
108
|
+
text: u.text,
|
|
109
|
+
message: candidates.length
|
|
110
|
+
? `${quote(u.text)} isn't a ${ctx.meta.name} syllable. Did you mean ${listOf(candidates.map((c) => quote(matchCase(u.text, c))), 'or')}?`
|
|
111
|
+
: `${quote(u.text)} isn't a ${ctx.meta.name} syllable. Check the spelling and accents.`,
|
|
112
|
+
suggestions: candidates.map((c) => matchCase(u.text, c)),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return issues;
|
|
117
|
+
}
|
|
118
|
+
// Words from another region: a word whose main meaning (its first sense) is tagged for regions that don't
|
|
119
|
+
// include the text's region. With `region`, against that; without, against the regions most of the text's
|
|
120
|
+
// regional words share (so "lợn" stands out in otherwise Southern text). Words with an untagged main
|
|
121
|
+
// meaning are never flagged (Vietnamese "má" is "cheek" everywhere, "mother" only in the South).
|
|
122
|
+
async function dialect(ctx) {
|
|
123
|
+
const all = new Set(ctx.meta.regions);
|
|
124
|
+
const regional = ctx.sentences
|
|
125
|
+
.flatMap((s) => s.words)
|
|
126
|
+
.map((word) => ({ word, sense: word.entries[0]?.senses[0] }))
|
|
127
|
+
.filter((x) => Boolean(x.sense?.regionTagged && x.sense.regions.length < all.size));
|
|
128
|
+
if (!regional.length)
|
|
129
|
+
return [];
|
|
130
|
+
const issue = (word, regions, message, suggestions, severity) => ({
|
|
131
|
+
rule: 'dialect',
|
|
132
|
+
severity,
|
|
133
|
+
start: word.start,
|
|
134
|
+
end: word.end,
|
|
135
|
+
text: word.text,
|
|
136
|
+
message: `${quote(word.text)} is ${listOf(regions)}${message}` +
|
|
137
|
+
// The best word only; the others (often niche: Southern "cúi", "ỉn" after "heo") are in suggestions.
|
|
138
|
+
(suggestions.length ? `, say ${quote(suggestions[0])}.` : '.'),
|
|
139
|
+
suggestions,
|
|
140
|
+
});
|
|
141
|
+
if (ctx.options.region) {
|
|
142
|
+
const target = resolveRegion(ctx.meta, ctx.options.region);
|
|
143
|
+
const where = listOf(ctx.meta.regions.filter((r) => target.has(r)));
|
|
144
|
+
const issues = [];
|
|
145
|
+
for (const { word, sense } of regional) {
|
|
146
|
+
if (sense.regions.some((r) => target.has(r)))
|
|
147
|
+
continue;
|
|
148
|
+
const suggestions = await ctx.suggestRegional(word, ctx.options.region);
|
|
149
|
+
issues.push(issue(word, sense.regions, `; in ${where} ${ctx.meta.name}`, suggestions, 'warning'));
|
|
150
|
+
}
|
|
151
|
+
return issues;
|
|
152
|
+
}
|
|
153
|
+
// No region given: the regions most regional words share are the text's region, and words that fit none
|
|
154
|
+
// of them stand out. When no region has a majority but some words can't go together ("lợn", Northern,
|
|
155
|
+
// with "nha", Central and Southern), every regional word is pointed out, without a guess.
|
|
156
|
+
const counts = new Map();
|
|
157
|
+
for (const { sense } of regional)
|
|
158
|
+
for (const r of sense.regions)
|
|
159
|
+
counts.set(r, (counts.get(r) ?? 0) + 1);
|
|
160
|
+
const most = Math.max(...counts.values());
|
|
161
|
+
const majority = new Set([...counts].filter(([, n]) => n === most).map(([r]) => r));
|
|
162
|
+
const outliers = regional.filter(({ sense }) => !sense.regions.some((r) => majority.has(r)));
|
|
163
|
+
if (outliers.length) {
|
|
164
|
+
const where = ctx.meta.regions.filter((r) => majority.has(r));
|
|
165
|
+
return Promise.all(outliers.map(async ({ word, sense }) => issue(word, sense.regions, `, but the rest of the text is ${listOf(where)}; there`, await ctx.suggestRegional(word, where[0]), 'suggestion')));
|
|
166
|
+
}
|
|
167
|
+
const clash = regional.some((a) => regional.some((b) => !a.sense.regions.some((r) => b.sense.regions.includes(r))));
|
|
168
|
+
if (!clash)
|
|
169
|
+
return [];
|
|
170
|
+
const mixed = listOf(regional.map(({ word, sense }) => `${quote(word.text)} (${listOf(sense.regions)})`));
|
|
171
|
+
return regional.map(({ word, sense }) => issue(word, sense.regions, `. This text mixes regions: ${mixed}. Pick one region`, [], 'suggestion'));
|
|
172
|
+
}
|
|
173
|
+
// Pronouns that don't fit who you're talking to (`listener`) or about (`about`): "tôi" to a parent, where
|
|
174
|
+
// it's "con". Only the language's reliable pronouns are checked (CheckerConfig.pronouns); ones that are
|
|
175
|
+
// also common nouns ("bạn": friend) only get a hedged suggestion.
|
|
176
|
+
async function pronounRelationship(ctx) {
|
|
177
|
+
if (!ctx.options.listener && !ctx.options.about)
|
|
178
|
+
return [];
|
|
179
|
+
const rows = await ctx.rows();
|
|
180
|
+
const issues = [];
|
|
181
|
+
for (const m of ctx.pronounMatches()) {
|
|
182
|
+
const about = ABOUT_PERSONS.has(m.person);
|
|
183
|
+
const row = rows.find((r) => r.id === (about ? ctx.options.about : ctx.options.listener));
|
|
184
|
+
const fit = unique((row?.[m.person] ?? []).map((c) => c.word));
|
|
185
|
+
if (!row || !fit.length || fit.includes(m.lower))
|
|
186
|
+
continue;
|
|
187
|
+
const ambiguous = ctx.config.ambiguousPronouns?.includes(m.lower);
|
|
188
|
+
const who = `${about ? 'Talking about' : 'Talking to'} ${lowerFirst(row.label)}`;
|
|
189
|
+
issues.push({
|
|
190
|
+
rule: 'pronoun-relationship',
|
|
191
|
+
severity: ambiguous ? 'suggestion' : 'warning',
|
|
192
|
+
start: m.start,
|
|
193
|
+
end: m.end,
|
|
194
|
+
text: m.text,
|
|
195
|
+
message: (ambiguous ? `If ${quote(m.text)} means “${PERSON_NAMES[m.person]}” here: ${lowerFirst(who)}` : who) +
|
|
196
|
+
`, say ${listOf(fit.map(quote), 'or')} for “${PERSON_NAMES[m.person]}”, not ${quote(m.text)}.`,
|
|
197
|
+
suggestions: fit.map((w) => matchCase(m.text, w)),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
return issues;
|
|
201
|
+
}
|
|
202
|
+
// "I" and "you" pronouns that never go together (Vietnamese "tao" goes with "mày", not "bạn"): no row of the
|
|
203
|
+
// pronoun table has both. Only when no listener is given; with one, pronoun-relationship covers it.
|
|
204
|
+
async function pronounPair(ctx) {
|
|
205
|
+
if (ctx.options.listener)
|
|
206
|
+
return [];
|
|
207
|
+
const matches = ctx.pronounMatches();
|
|
208
|
+
const selves = matches.filter((m) => SELF_PERSONS.has(m.person));
|
|
209
|
+
const yous = matches.filter((m) => ADDRESSEE_PERSONS.has(m.person));
|
|
210
|
+
if (!selves.length || !yous.length)
|
|
211
|
+
return [];
|
|
212
|
+
const rows = await ctx.rows();
|
|
213
|
+
const words = (r, persons) => [...persons].flatMap((p) => r[p].map((c) => c.word));
|
|
214
|
+
const issues = [];
|
|
215
|
+
for (const you of yous) {
|
|
216
|
+
for (const me of selves) {
|
|
217
|
+
const withMe = rows.filter((r) => words(r, SELF_PERSONS).includes(me.lower));
|
|
218
|
+
if (!withMe.length || withMe.some((r) => words(r, ADDRESSEE_PERSONS).includes(you.lower)))
|
|
219
|
+
continue;
|
|
220
|
+
const partners = unique(withMe.flatMap((r) => r[you.person].map((c) => c.word)));
|
|
221
|
+
issues.push({
|
|
222
|
+
rule: 'pronoun-pair',
|
|
223
|
+
severity: ctx.config.ambiguousPronouns?.includes(you.lower) ? 'suggestion' : 'warning',
|
|
224
|
+
start: you.start,
|
|
225
|
+
end: you.end,
|
|
226
|
+
text: you.text,
|
|
227
|
+
message: `${quote(me.text)} and ${quote(you.text)} don't go together` +
|
|
228
|
+
(partners.length ? `: with ${quote(me.text)}, say ${listOf(partners.map(quote), 'or')}.` : '.'),
|
|
229
|
+
suggestions: partners.map((w) => matchCase(you.text, w)),
|
|
230
|
+
});
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return issues;
|
|
235
|
+
}
|
|
236
|
+
// Polite endings when speaking up (the listener's row is marked `respect`): Vietnamese sentences to parents,
|
|
237
|
+
// elders and teachers usually end in "ạ". A suggestion, not an error: it isn't always needed.
|
|
238
|
+
async function politeEnding(ctx) {
|
|
239
|
+
const endings = ctx.config.politeEndings ?? [];
|
|
240
|
+
if (!ctx.options.listener || !endings.length)
|
|
241
|
+
return [];
|
|
242
|
+
const row = (await ctx.rows()).find((r) => r.id === ctx.options.listener);
|
|
243
|
+
if (!row?.respect)
|
|
244
|
+
return [];
|
|
245
|
+
const issues = [];
|
|
246
|
+
for (const s of ctx.sentences) {
|
|
247
|
+
const last = s.units[s.units.length - 1];
|
|
248
|
+
if (endings.includes(last.text.toLowerCase()))
|
|
249
|
+
continue;
|
|
250
|
+
// Exclamations ("Con nhớ má quá!") are often fine without it.
|
|
251
|
+
if (/^[^\p{L}\p{N}]*!/u.test(ctx.text.slice(last.end)))
|
|
252
|
+
continue;
|
|
253
|
+
issues.push({
|
|
254
|
+
rule: 'polite-ending',
|
|
255
|
+
severity: 'suggestion',
|
|
256
|
+
start: last.start,
|
|
257
|
+
end: last.end,
|
|
258
|
+
text: last.text,
|
|
259
|
+
message: `Talking to ${lowerFirst(row.label)}, end the sentence with ${listOf(endings.map(quote), 'or')} to sound polite.`,
|
|
260
|
+
suggestions: endings.map((e) => `${last.text} ${e}`),
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return issues;
|
|
264
|
+
}
|
|
265
|
+
const RULES = {
|
|
266
|
+
spelling,
|
|
267
|
+
dialect,
|
|
268
|
+
'pronoun-relationship': pronounRelationship,
|
|
269
|
+
'pronoun-pair': pronounPair,
|
|
270
|
+
'polite-ending': politeEnding,
|
|
271
|
+
};
|
|
272
|
+
const SEVERITY_ORDER = { error: 0, warning: 1, suggestion: 2 };
|
|
273
|
+
export function createChecker(options = {}) {
|
|
274
|
+
const cache = new Map();
|
|
275
|
+
const dict = (lang) => {
|
|
276
|
+
let d = cache.get(lang);
|
|
277
|
+
if (!d) {
|
|
278
|
+
d = options.dictionary?.(lang) ?? createDictionary({ lang, baseUrl: options.baseUrl?.(lang), load: options.load?.(lang) });
|
|
279
|
+
cache.set(lang, d);
|
|
280
|
+
}
|
|
281
|
+
return d;
|
|
282
|
+
};
|
|
283
|
+
const translator = createTranslator({ ...options, dictionary: dict });
|
|
284
|
+
const lookups = new Map();
|
|
285
|
+
return {
|
|
286
|
+
async check(input, opts) {
|
|
287
|
+
const text = input.normalize('NFC');
|
|
288
|
+
const d = dict(opts.lang);
|
|
289
|
+
const [meta, config] = await Promise.all([d.meta(), d.checker()]);
|
|
290
|
+
// Unknown listener/about ids throw, listing the valid ones (as the translator does).
|
|
291
|
+
if (meta.pronouns) {
|
|
292
|
+
if (opts.listener)
|
|
293
|
+
await d.pronouns({ listener: opts.listener });
|
|
294
|
+
if (opts.about)
|
|
295
|
+
await d.pronouns({ listener: opts.about });
|
|
296
|
+
}
|
|
297
|
+
const lookup = (phrase) => {
|
|
298
|
+
const key = `${opts.lang}\u0000${phrase}`;
|
|
299
|
+
let pending = lookups.get(key);
|
|
300
|
+
if (!pending) {
|
|
301
|
+
pending = d.lookup(phrase);
|
|
302
|
+
lookups.set(key, pending);
|
|
303
|
+
}
|
|
304
|
+
return pending;
|
|
305
|
+
};
|
|
306
|
+
const sentences = [];
|
|
307
|
+
for (const list of sentencesOf(text, units(text))) {
|
|
308
|
+
sentences.push({ units: list, words: await segment(text, list, config.maxWordUnits ?? 3, lookup) });
|
|
309
|
+
}
|
|
310
|
+
let rows;
|
|
311
|
+
let matches;
|
|
312
|
+
const ctx = {
|
|
313
|
+
text,
|
|
314
|
+
options: opts,
|
|
315
|
+
meta,
|
|
316
|
+
config,
|
|
317
|
+
sentences,
|
|
318
|
+
dict: d,
|
|
319
|
+
rows: () => (rows ??= meta.pronouns ? d.pronouns({ region: opts.region, speaker: opts.speaker }) : Promise.resolve([])),
|
|
320
|
+
// The language's reliable pronouns in the text, longest first ("chúng tôi" before "tôi"), matched on
|
|
321
|
+
// units directly so pronouns the dictionary doesn't list as one word ("tụi mày") are found too.
|
|
322
|
+
pronounMatches: () => (matches ??= findPronouns(text, sentences, config)),
|
|
323
|
+
suggestRegional: async (word, region) => {
|
|
324
|
+
try {
|
|
325
|
+
const [group] = await translator.translate(word.lower, {
|
|
326
|
+
from: opts.lang, to: opts.lang, toRegion: region, pos: word.entries[0]?.pos, limit: 3,
|
|
327
|
+
});
|
|
328
|
+
return (group?.translations ?? []).map((t) => t.word).filter((w) => w !== word.lower);
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
return [];
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
const results = await Promise.all(Object.keys(RULES).filter((r) => opts.rules?.[r] !== false).map((r) => RULES[r](ctx)));
|
|
336
|
+
const issues = results.flat().sort((a, b) => a.start - b.start || SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]);
|
|
337
|
+
return { text, issues };
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
function findPronouns(text, sentences, config) {
|
|
342
|
+
const byWord = new Map();
|
|
343
|
+
for (const person of PRONOUN_PERSONS)
|
|
344
|
+
for (const w of config.pronouns?.[person] ?? [])
|
|
345
|
+
byWord.set(w.toLowerCase(), person);
|
|
346
|
+
if (!byWord.size)
|
|
347
|
+
return [];
|
|
348
|
+
const longest = Math.max(...[...byWord.keys()].map((w) => w.split(' ').length));
|
|
349
|
+
const out = [];
|
|
350
|
+
sentences.forEach((s, sentence) => {
|
|
351
|
+
for (let i = 0; i < s.units.length;) {
|
|
352
|
+
let step = 1;
|
|
353
|
+
for (let k = Math.min(longest, s.units.length - i); k >= 1; k--) {
|
|
354
|
+
const span = s.units.slice(i, i + k);
|
|
355
|
+
if (span.some((u, j) => j > 0 && !joined(text, span[j - 1], u)))
|
|
356
|
+
continue;
|
|
357
|
+
const lower = span.map((u) => u.text.toLowerCase()).join(' ');
|
|
358
|
+
const person = byWord.get(lower);
|
|
359
|
+
if (person) {
|
|
360
|
+
out.push({ text: text.slice(span[0].start, span[k - 1].end), lower, start: span[0].start, end: span[k - 1].end, person, sentence });
|
|
361
|
+
step = k;
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
i += step;
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
return out;
|
|
369
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type PronounRow, type Entry, type Hit, type LanguageMeta, type PickRow } from './types.ts';
|
|
1
|
+
import { type CheckerConfig, type PronounRow, type Entry, type Hit, type LanguageMeta, type PickRow } from './types.ts';
|
|
2
2
|
export * from './types.ts';
|
|
3
3
|
export * from './pos.ts';
|
|
4
4
|
/**
|
|
@@ -67,6 +67,10 @@ export type Dictionary = {
|
|
|
67
67
|
pronouns(options?: PronounOptions): Promise<PronounRow[]>;
|
|
68
68
|
/** Hand-picked first choices for English meanings (see PickRow). Empty for languages without any. */
|
|
69
69
|
picks(): Promise<PickRow[]>;
|
|
70
|
+
/** How the checker reads this language (see CheckerConfig); `{}` for languages without settings. */
|
|
71
|
+
checker(): Promise<CheckerConfig>;
|
|
72
|
+
/** Syllable → Zipf frequency (0 when unknown), for languages written in syllables; `{}` otherwise. */
|
|
73
|
+
syllables(): Promise<Record<string, number>>;
|
|
70
74
|
};
|
|
71
75
|
/**
|
|
72
76
|
* The regions a region or region-group name stands for (e.g. English "North America" -> US, Canada),
|
|
@@ -76,3 +80,4 @@ export declare function resolveRegion(meta: LanguageMeta, region: string | undef
|
|
|
76
80
|
export declare function dataUrl(lang: string): string;
|
|
77
81
|
export declare function createDictionary(options: DictionaryOptions): Dictionary;
|
|
78
82
|
export * from './translate.ts';
|
|
83
|
+
export * from './check.ts';
|
package/dist/index.js
CHANGED
|
@@ -107,6 +107,14 @@ export function createDictionary(options) {
|
|
|
107
107
|
const m = await meta();
|
|
108
108
|
return m.picks ? loadOnce('picks.json') : [];
|
|
109
109
|
},
|
|
110
|
+
async checker() {
|
|
111
|
+
const m = await meta();
|
|
112
|
+
return m.checker ? loadOnce('checker.json') : {};
|
|
113
|
+
},
|
|
114
|
+
async syllables() {
|
|
115
|
+
const m = await meta();
|
|
116
|
+
return m.syllables ? loadOnce('syllables.json') : {};
|
|
117
|
+
},
|
|
110
118
|
async pronouns({ listener, region, speaker, exclude = DEFAULT_EXCLUDED_LABELS } = {}) {
|
|
111
119
|
const m = await meta();
|
|
112
120
|
if (!m.pronouns)
|
|
@@ -132,3 +140,4 @@ export function createDictionary(options) {
|
|
|
132
140
|
};
|
|
133
141
|
}
|
|
134
142
|
export * from "./translate.js";
|
|
143
|
+
export * from "./check.js";
|
package/dist/types.d.ts
CHANGED
|
@@ -110,6 +110,33 @@ export type LanguageMeta = {
|
|
|
110
110
|
pronouns?: boolean;
|
|
111
111
|
/** true when the language has hand-picked translations (picks.json, see PickRow). */
|
|
112
112
|
picks?: boolean;
|
|
113
|
+
/** true when the language has checker settings (checker.json, see CheckerConfig). */
|
|
114
|
+
checker?: boolean;
|
|
115
|
+
/** true when the language has a syllable list (syllables.json: syllable → Zipf frequency, 0 when unknown). */
|
|
116
|
+
syllables?: boolean;
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* How the checker reads one language (languages/<lang>.ts `checker`, stored as data/checker.json). Every
|
|
120
|
+
* field is optional: a language without it is read as space-separated words, and gets the checks that
|
|
121
|
+
* only need its dictionary (dialect mixing).
|
|
122
|
+
*/
|
|
123
|
+
export type CheckerConfig = {
|
|
124
|
+
/**
|
|
125
|
+
* What spaces separate: 'words' (English), or 'syllables' that group into dictionary words (Vietnamese
|
|
126
|
+
* "thịt heo", "kết quả"). With 'syllables' the build also writes syllables.json for accent suggestions.
|
|
127
|
+
*/
|
|
128
|
+
units?: 'words' | 'syllables';
|
|
129
|
+
/** The longest dictionary word to look for, in units (default 3). */
|
|
130
|
+
maxWordUnits?: number;
|
|
131
|
+
/**
|
|
132
|
+
* Pronouns that are reliably pronouns, by pronoun-table column ("tôi", "tao", "mày"). The pronoun checks
|
|
133
|
+
* look only at these: kinship words like Vietnamese "con" (also "child", a classifier) aren't listed.
|
|
134
|
+
*/
|
|
135
|
+
pronouns?: Partial<Record<PronounPerson, string[]>>;
|
|
136
|
+
/** Listed pronouns that are also common nouns ("bạn": you, or friend); checked, but only as suggestions. */
|
|
137
|
+
ambiguousPronouns?: string[];
|
|
138
|
+
/** Sentence endings expected when speaking up (pronoun-table rows marked `respect`), e.g. Vietnamese "ạ". */
|
|
139
|
+
politeEndings?: string[];
|
|
113
140
|
};
|
|
114
141
|
/**
|
|
115
142
|
* Hand-picked first choices for one English meaning, in one target language (picks.json). A speaker's
|
|
@@ -181,6 +208,8 @@ export type PronounRow = {
|
|
|
181
208
|
thirdPlural: PronounChoice[];
|
|
182
209
|
/** The row to use when the relationship isn't known (neutral words: Vietnamese "tôi", "bạn"). */
|
|
183
210
|
default?: boolean;
|
|
211
|
+
/** Speaking up to this person (parents, elders, teachers): polite sentence endings are expected. */
|
|
212
|
+
respect?: boolean;
|
|
184
213
|
/** A caution for the whole row, e.g. that "tao"/"mày" are rude outside close friendships. */
|
|
185
214
|
warning?: string;
|
|
186
215
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "which-dialect",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Translate words between languages and dialects, meaning by meaning, starting with Vietnamese (Northern, Central, Southern) and English, with Wiktionary data loaded on demand. More languages are planned.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|