which-dialect 0.5.0 → 0.5.2

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 CHANGED
@@ -138,8 +138,8 @@ The result has `text` (the input in Unicode NFC form), `issues` and `parts` (the
138
138
 
139
139
  **On by default (the spellchecker):**
140
140
 
141
- - **`spelling`**: a doubtful syllable is first tried with its neighbors: if fixing its accents makes a dictionary word with the syllables next to it, that word is suggested (*hom nay* → *hôm nay* "today", where *nay* alone would become *này* "this"; *an com* → *ăn cơm*; *Chung toi* → *Chúng tôi*), changing only the doubtful syllables. Otherwise, syllables that aren't in the language get the accented forms they could be (*khong* → *không*); rare plain-letter words next to a far more common accented form too (*toi* → *tôi*, *hom* → *hôm*), and, in a sentence typed without accents, any more common accented form (*an* → *ăn*, *di* → *đi*; not *con* → *còn*, about as common). Names (capitalized mid-sentence), the language's listed pronouns (*tui*) and plain-letter words with no accented form (*email*) are left alone. Languages written in syllables only (Vietnamese).
142
- - **`foreign-word`**: words in the learner's own language get the target-language word, in the region and register (*market* → *chợ*, *ice cream* → *kem*). Phrases the base dictionary knows are translated as one, falling back to single words. Words the language has no word for are suggested to be left out (`leaveOut` in its checker settings: Vietnamese has no *the*, *a*, *an*). Which language each word is in comes from the two dictionaries: an English word that's Vietnamese only with accents added (*but*: *bút*) is English; Vietnamese typed without accents (*khong*) is Vietnamese; words both languages have go by how common they are in each (*em*, *di*, *tui* are Vietnamese even between English words; *the* is English), and close calls (*an*, *to*) follow their neighbors, leaning to the target language.
141
+ - **`spelling`**: a doubtful syllable is first tried with its neighbors: if fixing its accents makes a dictionary word with the syllables next to it, that word is suggested (*hom nay* → *hôm nay* "today", where *nay* alone would become *này* "this"; *an com* → *ăn cơm*; *Chung toi* → *Chúng tôi*), changing only the doubtful syllables. Less common words written without accents can change the same way when one dictionary word clearly wins (*mua sam* → *mua sắm* "shopping", *nha sach* → *nhà sách*), even common ones when the whole word has no accents (*nha bang* → *nhà băng* "bank"), while a common one left plain next to accented syllables is taken as meant (*tháng sau* stays), *cho con* never becomes *chó con* (not a headword) and runs that could be two words equally are left alone (*ban an*: *bản án* or *bàn ăn*). Otherwise, syllables that aren't in the language get the accented forms they could be (*khong* → *không*); rare plain-letter words next to a far more common accented form too (*toi* → *tôi*, *hom* → *hôm*), and, in a sentence typed without accents, any more common accented form (*an* → *ăn*, *di* → *đi*; not *con* → *còn*, about as common). Names (capitalized mid-sentence), the language's listed pronouns (*tui*) and plain-letter words with no accented form (*email*) are left alone. Languages written in syllables only (Vietnamese).
142
+ - **`foreign-word`**: words in the learner's own language get the target-language word, in the region and register (*market* → *chợ*, *ice cream* → *kem*). Phrases the base dictionary knows are translated as one, falling back to single words. Words the language has no word for are suggested to be left out (`leaveOut` in its checker settings: Vietnamese has no *the*, *a*, *an*). Which language each word is in comes from the two dictionaries: an English word that's also Vietnamese once accents are added is weighed by how common each is (*rat* is *rất*, far more common than English *rat*; *but* stays English, far more common than *bút*), and close calls (*met*: *mệt* or English *met*) are Vietnamese only among Vietnamese words; Vietnamese typed without accents (*khong*) is Vietnamese; words both languages have go by how common they are in each (*em*, *di*, *tui* are Vietnamese even between English words; *the* is English), and close calls (*an*, *to*) follow their neighbors, leaning to the target language.
143
143
 
144
144
  **Opt-in (`rules`):**
145
145
 
package/dist/check.js CHANGED
@@ -60,6 +60,15 @@ const ACCENTS_GAP_UNACCENTED_TEXT = 0.2;
60
60
  // combinations are looked up at most.
61
61
  const COMPOUND_FORMS_PER_SYLLABLE = 5;
62
62
  const COMPOUND_MAX_COMBINATIONS = 64;
63
+ // When none of a run's syllables is doubtful on its own (all real words written without accents, "mua sam"),
64
+ // its accented form is suggested only if one dictionary word clearly wins: ahead of the next by this many
65
+ // Zipf points ("ban an" could be "bản án" or "bàn ăn", equally common, so it's left alone).
66
+ const PLAIN_RUN_MARGIN = 0.5;
67
+ // In a run that has accents elsewhere, only syllables less common than this (Zipf) may change that way: a
68
+ // common word left without accents next to accented ones is usually meant ("tháng sau", next month, isn't
69
+ // "tháng sáu", June). A run with no accents at all ("nha bang") was probably typed without them, so any of
70
+ // its syllables may change ("nhà băng", bank), still only when one word clearly wins.
71
+ const PLAIN_RUN_MAX_ZIPF = 5;
63
72
  async function spelling(ctx) {
64
73
  if (ctx.config.units !== 'syllables' || !ctx.meta.syllables)
65
74
  return [];
@@ -94,8 +103,11 @@ async function spelling(ctx) {
94
103
  };
95
104
  const suspect = (u) => unknown.has(u) || (plainWords.has(u) && betterForms(u).length > 0);
96
105
  // First, runs of syllables with a doubtful one that make a word once their accents are fixed: "hom nay"
97
- // → "hôm nay", "an com" → "ăn cơm", "hôm nây" → "hôm nay". Only the doubtful syllables change. The word
98
- // decides between forms a syllable alone can't ("nay" alone could be này, nảy…).
106
+ // → "hôm nay", "an com" → "ăn cơm", "hôm nây" → "hôm nay". The word decides between forms a syllable
107
+ // alone can't ("nay" alone could be này, nảy…). Syllables written without accents may change too
108
+ // ("mua sam" → "mua sắm": "sam" is a word, but "mua sam" isn't, and "mua sắm" is); when none of them is
109
+ // doubtful on its own, only if one word clearly wins. Syllables written with accents never change
110
+ // unless they're doubtful.
99
111
  const covered = new Set();
100
112
  const loneSet = new Set(checked);
101
113
  const maxUnits = ctx.config.maxWordUnits ?? 3;
@@ -104,11 +116,15 @@ async function spelling(ctx) {
104
116
  const span = sentence.units.slice(i, i + k);
105
117
  if (span.some((u, j) => !loneSet.has(u) || covered.has(u) || (j > 0 && !joined(ctx.text, span[j - 1], u))))
106
118
  continue;
107
- if (!span.some(suspect))
119
+ const doubtful = span.some(suspect);
120
+ const allPlain = span.every(isPlain);
121
+ const rarePlain = (u) => plainWords.has(u) && (allPlain || (syllables[lowerOf(u)] ?? 0) < PLAIN_RUN_MAX_ZIPF);
122
+ const mayChange = (u) => suspect(u) || rarePlain(u);
123
+ if (!doubtful && !span.some(rarePlain))
108
124
  continue;
109
125
  const written = span.map(lowerOf);
110
126
  const combos = accentCombinations(written, byPlain, {
111
- fixed: span.map((u) => !suspect(u)),
127
+ fixed: span.map((u) => !mayChange(u)),
112
128
  perUnit: COMPOUND_FORMS_PER_SYLLABLE,
113
129
  max: COMPOUND_MAX_COMBINATIONS,
114
130
  }).filter((c) => c !== written.join(' '));
@@ -119,12 +135,14 @@ async function spelling(ctx) {
119
135
  .slice(0, 3);
120
136
  if (!found.length)
121
137
  continue;
138
+ if (!doubtful && found.length > 1 && found[0].frequency - found[1].frequency < PLAIN_RUN_MARGIN)
139
+ continue;
122
140
  const at = { start: span[0].start, end: span[k - 1].end, text: ctx.text.slice(span[0].start, span[k - 1].end) };
123
141
  const suggestions = found.map((f) => matchCase(at.text, f.word));
124
142
  const quoted = listOf(suggestions.map(quote), 'or');
125
143
  issues.push({
126
144
  rule: 'spelling', severity: 'warning', ...at, suggestions,
127
- message: span.filter(suspect).every(isPlain)
145
+ message: span.filter(mayChange).every(isPlain)
128
146
  ? `${quote(at.text)} looks typed without accents. Did you mean ${quoted}?`
129
147
  : `${quote(at.text)} isn't spelled right. Did you mean ${quoted}?`,
130
148
  });
package/dist/detect.js CHANGED
@@ -7,15 +7,21 @@ import { plain } from "./text.js";
7
7
  * in each: clearly more common in the target, the target's (Vietnamese "em", "di" next to English
8
8
  * "hello", "shopping": English has rare "em", "di"); far more common in the base, the base's ("the");
9
9
  * otherwise, and for units neither knows (names), the nearest decided neighbors' language when they agree,
10
- * else the target's (the text is mainly the target language: "an" next to English "the"). In languages written in syllables, a base
11
- * word that's a target syllable only once accents are added ("but": bút) is the base's; a word only the
12
- * target knows without its accents ("khong") is still the target's. The base is known through the translator, so
10
+ * else the target's (the text is mainly the target language: "an" next to English "the"). In languages
11
+ * written in syllables, a base word that's a target word only once accents are added is weighed the same
12
+ * way, using its accented form's frequency: "rat" is "rất" (far more common than English "rat"); close
13
+ * calls ("met": mệt or English met) go to the target only when their neighbors are the target's ("Tôi met
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. The base is known through the translator, so
13
16
  * inflected forms count ("went", "cooked").
14
17
  */
15
18
  // Zipf points by which a word both languages know must be more common in one to count as that language's
16
19
  // whatever its neighbors: less for the target, since the text is mainly the target language.
17
20
  const TARGET_MARGIN = 1;
18
21
  const BASE_MARGIN = 2;
22
+ // For a base word that's a target word only with accents added, a smaller lead keeps it the base's: it was
23
+ // typed exactly as the base word ("but" is 1.9 more common in English than "bút" in Vietnamese).
24
+ const BASE_MARGIN_ACCENTS = 1;
19
25
  export async function markLanguages(list, o) {
20
26
  const syllables = await o.target.syllables();
21
27
  const syllableLanguage = Object.keys(syllables).length > 0;
@@ -37,9 +43,18 @@ export async function markLanguages(list, o) {
37
43
  if (t === 'exact')
38
44
  return b ? 'both' : o.lang;
39
45
  if (b)
40
- return o.base;
46
+ return t === 'accents' ? 'accents' : o.base;
41
47
  return t === 'accents' ? o.lang : '?';
42
48
  }));
49
+ // The most common form a plain-letter unit takes once accents are added ("rat" → rất, 6.4).
50
+ const accentedFrequency = (u) => {
51
+ const bare = plain(u.text.toLowerCase());
52
+ let best = 0;
53
+ for (const [s, f] of Object.entries(syllables))
54
+ if (f > best && plain(s) === bare)
55
+ best = f;
56
+ return best;
57
+ };
43
58
  // How common a word is in each language (Zipf; 0 when unknown).
44
59
  const targetFrequency = async (u) => {
45
60
  const lower = u.text.toLowerCase();
@@ -48,18 +63,29 @@ export async function markLanguages(list, o) {
48
63
  return (await o.target.lookup(lower))[0]?.frequency ?? 0;
49
64
  };
50
65
  const baseFrequency = async (u) => (await o.baseDictionary.lookup(u.text.toLowerCase()))[0]?.frequency ?? 0;
51
- // Words both languages know, decided by frequency when it's clear.
66
+ // Words both languages know, decided by frequency when it's clear. Base words that are target words with
67
+ // accents added use that form's frequency, and close calls are marked '?accents' (decided by neighbors,
68
+ // more strictly).
52
69
  const decided = await Promise.all(marks.map(async (m, i) => {
53
- if (m !== 'both')
70
+ if (m !== 'both' && m !== 'accents')
54
71
  return m;
55
- const [t, b] = await Promise.all([targetFrequency(list[i]), baseFrequency(list[i])]);
56
- return t - b >= TARGET_MARGIN ? o.lang : b - t >= BASE_MARGIN ? o.base : '?';
72
+ const [t, b] = await Promise.all([m === 'accents' ? accentedFrequency(list[i]) : targetFrequency(list[i]), baseFrequency(list[i])]);
73
+ if (t - b >= TARGET_MARGIN)
74
+ return o.lang;
75
+ if (b - t >= (m === 'accents' ? BASE_MARGIN_ACCENTS : BASE_MARGIN))
76
+ return o.base;
77
+ return m === 'accents' ? '?accents' : '?';
57
78
  }));
79
+ const known = (x) => x !== '?' && x !== '?accents';
58
80
  return decided.map((m, i) => {
59
- if (m !== '?')
81
+ if (known(m))
60
82
  return m;
61
- const left = decided.slice(0, i).reverse().find((x) => x !== '?');
62
- const right = decided.slice(i + 1).find((x) => x !== '?');
83
+ const left = decided.slice(0, i).reverse().find(known);
84
+ const right = decided.slice(i + 1).find(known);
85
+ // A word written as typed in the base language needs every decided neighbor to be the target's to be
86
+ // read as the target's without its accents ("Tôi met quá": mệt); otherwise it's the base's.
87
+ if (m === '?accents')
88
+ return (left ?? o.lang) === o.lang && (right ?? o.lang) === o.lang && (left || right) ? o.lang : o.base;
63
89
  return left && right && left !== right ? o.lang : left ?? right ?? o.lang;
64
90
  });
65
91
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "which-dialect",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
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",