which-dialect 0.5.8 → 0.5.9
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/dist/check.js +56 -0
- package/dist/detect.js +11 -4
- package/dist/index.d.ts +3 -2
- package/dist/index.js +55 -6
- package/dist/types.d.ts +20 -1
- package/package.json +1 -1
package/dist/check.js
CHANGED
|
@@ -82,6 +82,8 @@ const PLAIN_TYPO_MIN_ZIPF = 4;
|
|
|
82
82
|
// Candidates kept per misspelled syllable.
|
|
83
83
|
const SPELLING_CANDIDATES = 3;
|
|
84
84
|
async function spelling(ctx) {
|
|
85
|
+
if (ctx.config.units === 'words')
|
|
86
|
+
return wordSpelling(ctx);
|
|
85
87
|
if (ctx.config.units !== 'syllables' || !ctx.meta.syllables)
|
|
86
88
|
return [];
|
|
87
89
|
const syllables = await ctx.dict.syllables();
|
|
@@ -222,6 +224,60 @@ async function spelling(ctx) {
|
|
|
222
224
|
}
|
|
223
225
|
return issues;
|
|
224
226
|
}
|
|
227
|
+
// Wiktionary's own misspelling entries ("misspelling of también", "common misspelling of …"). A word counts
|
|
228
|
+
// as misspelt when it's a misspelling and otherwise only an out-of-use spelling ("tambien": misspelling, and
|
|
229
|
+
// obsolete spelling, of también).
|
|
230
|
+
const MISSPELLING = /\bmisspelling of ([\p{L}\p{M}'-]+)/iu;
|
|
231
|
+
const NOT_IN_USE_SPELLING = /\b(misspelling|obsolete spelling|archaic spelling|dated spelling|obsolete form|archaic form) of\b/i;
|
|
232
|
+
// Zipf points below the most common look-alike beyond which another isn't suggested.
|
|
233
|
+
const RARE_VARIANT_GAP = 2;
|
|
234
|
+
// Spelling and accents for languages written in words (Spanish), from the dictionary alone: a word it doesn't
|
|
235
|
+
// know, as a headword or an inflected form (the forms index), that matches known words once accents are
|
|
236
|
+
// dropped gets the most common of them ("cancion" → "canción", "tambien" → "también"). Known words are left
|
|
237
|
+
// alone even when an accented one is more
|
|
238
|
+
// common ("esta" is a word, as "está" is), and so are unknown words with no such match (names, other
|
|
239
|
+
// languages, typos this can't fix), and words capitalized mid-sentence (names).
|
|
240
|
+
async function wordSpelling(ctx) {
|
|
241
|
+
const issues = [];
|
|
242
|
+
for (const sentence of ctx.sentences) {
|
|
243
|
+
for (const w of sentence.words) {
|
|
244
|
+
if (w.units.length !== 1)
|
|
245
|
+
continue;
|
|
246
|
+
// Words the dictionary lists only as misspellings ("tambien": "misspelling of también") count as unknown,
|
|
247
|
+
// with what they're a misspelling of.
|
|
248
|
+
const senses = w.entries.flatMap((e) => e.senses);
|
|
249
|
+
const misspelt = senses.some((s) => MISSPELLING.test(s.glosses[0] ?? '')) && senses.every((s) => NOT_IN_USE_SPELLING.test(s.glosses[0] ?? ''));
|
|
250
|
+
if (w.entries.length && !misspelt)
|
|
251
|
+
continue;
|
|
252
|
+
const u = w.units[0];
|
|
253
|
+
if (ctx.inBase(u) || !/\p{L}/u.test(u.text))
|
|
254
|
+
continue;
|
|
255
|
+
if (u !== sentence.units[0] && /\p{Lu}/u.test(u.text.charAt(0)))
|
|
256
|
+
continue;
|
|
257
|
+
const variants = await ctx.dict.variants(u.text, { limit: 3 });
|
|
258
|
+
// Rare look-alikes don't crowd out the word meant ("nino": niño, not also ñiño).
|
|
259
|
+
const best = variants[0]?.frequency;
|
|
260
|
+
const found = unique([
|
|
261
|
+
...(misspelt ? senses.map((s) => s.altOf ?? MISSPELLING.exec(s.glosses[0] ?? '')?.[1]) : []).filter((x) => Boolean(x)),
|
|
262
|
+
...variants.filter((v) => best === undefined || (v.frequency ?? -Infinity) >= best - RARE_VARIANT_GAP).map((v) => v.word),
|
|
263
|
+
]).slice(0, 3);
|
|
264
|
+
if (!found.length)
|
|
265
|
+
continue;
|
|
266
|
+
// Missing accents is the common learner slip; a wrong accent is more surely wrong.
|
|
267
|
+
const accented = plain(u.text) !== u.text;
|
|
268
|
+
issues.push({
|
|
269
|
+
rule: 'spelling',
|
|
270
|
+
severity: accented ? 'error' : 'warning',
|
|
271
|
+
start: u.start,
|
|
272
|
+
end: u.end,
|
|
273
|
+
text: u.text,
|
|
274
|
+
message: `${quote(u.text)} isn't a ${ctx.meta.name} word. Did you mean ${listOf(found.map(quote), 'or')}?`,
|
|
275
|
+
suggestions: found.map((c) => matchCase(u.text, c)),
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return issues;
|
|
280
|
+
}
|
|
225
281
|
// Words in the learner's own language (English by default), with the target-language word to use: "market"
|
|
226
282
|
// → "chợ". Runs of base units are split into the longest phrases the base dictionary knows ("ice cream"),
|
|
227
283
|
// each translated in the region and register. Words the language usually has no word for
|
package/dist/detect.js
CHANGED
|
@@ -12,7 +12,8 @@ import { plain } from "./text.js";
|
|
|
12
12
|
* way, using its accented form's frequency: "rat" is "rất" (far more common than English "rat"); close
|
|
13
13
|
* calls ("met": mệt or English met) go to the target only when their neighbors are the target's ("Tôi met
|
|
14
14
|
* quá"), so English stays English among English ("kem but it was": but, not bút). A word only the target
|
|
15
|
-
* knows without its accents ("khong") is still the target's.
|
|
15
|
+
* knows without its accents ("khong") is still the target's. Languages written in words do the same with
|
|
16
|
+
* whole words and their inflected forms (Spanish "cancion" → canción, "tambien" → también). The base is known through the translator, so
|
|
16
17
|
* inflected forms count ("went", "cooked").
|
|
17
18
|
*/
|
|
18
19
|
// Zipf points by which a word both languages know must be more common in one to count as that language's
|
|
@@ -29,8 +30,12 @@ export async function markLanguages(list, o) {
|
|
|
29
30
|
// 'exact': a target word as written; 'accents': one only with accents added ("khong" → "không").
|
|
30
31
|
const targetKnows = async (u) => {
|
|
31
32
|
const lower = u.text.toLowerCase();
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
// Languages written in words compare whole words: "cancion" is Spanish "canción" without its accent.
|
|
34
|
+
if (!syllableLanguage) {
|
|
35
|
+
if ((await o.target.lookup(lower)).length > 0)
|
|
36
|
+
return 'exact';
|
|
37
|
+
return (await o.target.variants(lower, { limit: 1 })).length > 0 ? 'accents' : false;
|
|
38
|
+
}
|
|
34
39
|
if (lower in syllables || lower !== plain(lower))
|
|
35
40
|
return 'exact';
|
|
36
41
|
return plainSyllables.has(lower) ? 'accents' : false;
|
|
@@ -47,7 +52,9 @@ export async function markLanguages(list, o) {
|
|
|
47
52
|
return t === 'accents' ? o.lang : '?';
|
|
48
53
|
}));
|
|
49
54
|
// The most common form a plain-letter unit takes once accents are added ("rat" → rất, 6.4).
|
|
50
|
-
const accentedFrequency = (u) => {
|
|
55
|
+
const accentedFrequency = async (u) => {
|
|
56
|
+
if (!syllableLanguage)
|
|
57
|
+
return (await o.target.variants(u.text.toLowerCase(), { limit: 1 }))[0]?.frequency ?? 0;
|
|
51
58
|
const bare = plain(u.text.toLowerCase());
|
|
52
59
|
let best = 0;
|
|
53
60
|
for (const [s, f] of Object.entries(syllables))
|
package/dist/index.d.ts
CHANGED
|
@@ -99,8 +99,9 @@ export type Dictionary = {
|
|
|
99
99
|
/**
|
|
100
100
|
* Other words written with the same letters but other accents, that the dictionary has, most common
|
|
101
101
|
* first: "muộn" → muốn, mượn, …; for words of several syllables, every syllable varies ("hom nay" →
|
|
102
|
-
* "hôm nay").
|
|
103
|
-
* itself isn't
|
|
102
|
+
* "hôm nay"). Languages written in words (with no syllable list) compare whole words, headwords and
|
|
103
|
+
* inflected forms alike: Spanish "cancion" → canción, "esta" → está, ésta. The word itself isn't
|
|
104
|
+
* included.
|
|
104
105
|
*/
|
|
105
106
|
variants(word: string, options?: {
|
|
106
107
|
limit?: number;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PERSONAL_NAME, PRONOUN_PERSONS, fromStored, normalizeEnglish, shardKey, } from "./types.js";
|
|
2
|
-
import { accentCombinations, segment as segmentUnits, syllablesByPlain, units } from "./text.js";
|
|
2
|
+
import { accentCombinations, plain, segment as segmentUnits, syllablesByPlain, units } from "./text.js";
|
|
3
3
|
export * from "./types.js";
|
|
4
4
|
export * from "./pos.js";
|
|
5
5
|
/**
|
|
@@ -67,16 +67,40 @@ export function createDictionary(options) {
|
|
|
67
67
|
async function shard(kind, term) {
|
|
68
68
|
const m = await meta();
|
|
69
69
|
const key = shardKey(term, m.shardLength);
|
|
70
|
-
return m.shards[kind]
|
|
70
|
+
return m.shards[kind]?.includes(key) ? loadOnce(`${kind}/${key}.json`) : null;
|
|
71
|
+
}
|
|
72
|
+
// An inflected form from the forms index as an entry: one sense, "first-person singular present
|
|
73
|
+
// indicative of hablar", pointing at its lemma (altOf), used in every region like the lemma.
|
|
74
|
+
function formEntries(m, word, forms) {
|
|
75
|
+
const descriptions = m.forms?.descriptions ?? [];
|
|
76
|
+
return forms.map(([lemma, pos, description, frequency]) => ({
|
|
77
|
+
word,
|
|
78
|
+
pos,
|
|
79
|
+
senses: [
|
|
80
|
+
{
|
|
81
|
+
glosses: [(descriptions[description] ?? 'form of {}').replace('{}', lemma)],
|
|
82
|
+
regions: m.regions,
|
|
83
|
+
regionTagged: false,
|
|
84
|
+
labels: [],
|
|
85
|
+
altOf: lemma,
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
...(frequency !== undefined ? { frequency } : {}),
|
|
89
|
+
}));
|
|
71
90
|
}
|
|
72
91
|
async function lookup(word) {
|
|
73
92
|
const w = word.trim();
|
|
74
|
-
const [m, data] = await Promise.all([meta(), shard('words', w)]);
|
|
93
|
+
const [m, data, forms] = await Promise.all([meta(), shard('words', w), shard('forms', w)]);
|
|
75
94
|
// The word as written, else lowercase ("Mẹ" → "mẹ"), else capitalized for names written in lowercase
|
|
76
95
|
// ("nhật" → "Nhật", Japan; "nhật bản" → "Nhật Bản"), which spellchecker suggestions and learners do.
|
|
77
96
|
const titled = w.toLowerCase().replace(/(^|[\s-])(\p{Ll})/gu, (_, sep, c) => sep + c.toUpperCase());
|
|
78
|
-
const
|
|
79
|
-
|
|
97
|
+
const key = [w, w.toLowerCase(), titled].find((k) => data?.[k] || forms?.[k]);
|
|
98
|
+
if (!key)
|
|
99
|
+
return [];
|
|
100
|
+
// Headword entries first, then the word as an inflected form of others ("música": the noun, then the
|
|
101
|
+
// feminine of "músico").
|
|
102
|
+
const entries = (data?.[key] ?? []).map((e) => ({ ...e, senses: e.senses.map((s) => fromStored(s, m.regions)) }));
|
|
103
|
+
return [...entries, ...formEntries(m, key, forms?.[key] ?? [])];
|
|
80
104
|
}
|
|
81
105
|
const checker = async () => {
|
|
82
106
|
const m = await meta();
|
|
@@ -86,6 +110,31 @@ export function createDictionary(options) {
|
|
|
86
110
|
const m = await meta();
|
|
87
111
|
return m.syllables ? loadOnce('syllables.json') : {};
|
|
88
112
|
};
|
|
113
|
+
// Variants for languages written in words: other headwords and inflected forms with the same letters
|
|
114
|
+
// once accents are dropped. Shards are keyed without accents, so they're all in the word's own shards.
|
|
115
|
+
async function wordVariants(word, limit) {
|
|
116
|
+
const written = word.trim().normalize('NFC').toLowerCase();
|
|
117
|
+
const target = plain(written);
|
|
118
|
+
if (!target)
|
|
119
|
+
return [];
|
|
120
|
+
const [data, forms] = await Promise.all([shard('words', written), shard('forms', written)]);
|
|
121
|
+
const candidates = new Set();
|
|
122
|
+
for (const key of [...Object.keys(data ?? {}), ...Object.keys(forms ?? {})]) {
|
|
123
|
+
const lower = key.toLowerCase();
|
|
124
|
+
if (lower !== written && plain(lower) === target)
|
|
125
|
+
candidates.add(lower);
|
|
126
|
+
}
|
|
127
|
+
const found = await Promise.all([...candidates].map(async (c) => ({ word: c, entries: await lookup(c) })));
|
|
128
|
+
const frequency = (entries) => {
|
|
129
|
+
const known = entries.map((e) => e.frequency).filter((f) => f !== undefined);
|
|
130
|
+
return known.length ? Math.max(...known) : undefined;
|
|
131
|
+
};
|
|
132
|
+
return found
|
|
133
|
+
.filter((v) => v.entries.length)
|
|
134
|
+
.map((v) => ({ ...v, frequency: frequency(v.entries) }))
|
|
135
|
+
.sort((a, b) => (b.frequency ?? -1) - (a.frequency ?? -1))
|
|
136
|
+
.slice(0, limit);
|
|
137
|
+
}
|
|
89
138
|
return {
|
|
90
139
|
meta,
|
|
91
140
|
lookup,
|
|
@@ -100,7 +149,7 @@ export function createDictionary(options) {
|
|
|
100
149
|
async variants(word, { limit = 5 } = {}) {
|
|
101
150
|
const list = await syllables();
|
|
102
151
|
if (!Object.keys(list).length)
|
|
103
|
-
return
|
|
152
|
+
return wordVariants(word, limit);
|
|
104
153
|
const written = word.trim().normalize('NFC').toLowerCase();
|
|
105
154
|
const parts = written.split(/\s+/);
|
|
106
155
|
const combos = accentCombinations(parts, syllablesByPlain(list), {
|
package/dist/types.d.ts
CHANGED
|
@@ -101,10 +101,11 @@ export type LanguageMeta = {
|
|
|
101
101
|
regionTaggedSenses: number;
|
|
102
102
|
englishTerms: number;
|
|
103
103
|
};
|
|
104
|
-
/** Shard file names (without .json) in words/ and en
|
|
104
|
+
/** Shard file names (without .json) in words/ and en/, and forms/ when the language has a forms index. */
|
|
105
105
|
shards: {
|
|
106
106
|
words: string[];
|
|
107
107
|
en: string[];
|
|
108
|
+
forms?: string[];
|
|
108
109
|
};
|
|
109
110
|
/** Letters per shard key; see shardKey. Omitted means 2. */
|
|
110
111
|
shardLength?: number;
|
|
@@ -118,6 +119,16 @@ export type LanguageMeta = {
|
|
|
118
119
|
syllables?: boolean;
|
|
119
120
|
/** true when the language has sentence frames (frames.json, see FramesData). */
|
|
120
121
|
frames?: boolean;
|
|
122
|
+
/**
|
|
123
|
+
* The language's inflected forms are in a compact index (forms/<shard>.json, see FormShard) rather than
|
|
124
|
+
* stored as entries: Spanish's conjugations and plurals are most of its source and would triple its
|
|
125
|
+
* data. `descriptions` are what each form is, with "{}" where the lemma goes ("first-person singular
|
|
126
|
+
* present indicative of {}"); a stored form points into it.
|
|
127
|
+
*/
|
|
128
|
+
forms?: {
|
|
129
|
+
descriptions: string[];
|
|
130
|
+
count: number;
|
|
131
|
+
};
|
|
121
132
|
};
|
|
122
133
|
/**
|
|
123
134
|
* Pronoun slots in sentence frames ({I}, {YOU}, …) and the pronoun-table column each one fills from, for
|
|
@@ -308,6 +319,14 @@ export type StoredHit = Omit<Hit, 'regions' | 'regionTagged' | 'labels'> & {
|
|
|
308
319
|
};
|
|
309
320
|
/** words/<shard>.json: entries keyed by headword. */
|
|
310
321
|
export type WordShard = Record<string, StoredEntry[]>;
|
|
322
|
+
/**
|
|
323
|
+
* One inflected form in forms/<shard>.json: [lemma, part of speech, index into meta.forms.descriptions,
|
|
324
|
+
* Zipf frequency (omitted when unknown)]. lookup() turns it into an entry with one sense whose gloss is
|
|
325
|
+
* the description and whose `altOf` is the lemma, so the translator follows it to the lemma's meanings.
|
|
326
|
+
*/
|
|
327
|
+
export type StoredForm = [lemma: string, pos: string, description: number, frequency?: number];
|
|
328
|
+
/** forms/<shard>.json: a language's inflected forms keyed by form ("hablo" → hablar). */
|
|
329
|
+
export type FormShard = Record<string, StoredForm[]>;
|
|
311
330
|
/** en/<shard>.json: hits keyed by lowercase English term. */
|
|
312
331
|
export type EnglishShard = Record<string, StoredHit[]>;
|
|
313
332
|
/** Converts a full sense or hit to its stored form. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "which-dialect",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.9",
|
|
4
4
|
"description": "Translate words between languages and dialects, meaning by meaning, starting with Vietnamese (Northern, Central, Southern), Spanish (by country) and English, with Wiktionary data loaded on demand. More languages are planned.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|