which-dialect 0.5.6 → 0.5.8

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  Translate words **between languages and between dialects**, meaning by meaning: Northern Vietnamese *ngô* into Southern *bắp*, English *cool* (as in "awesome") into Southern Vietnamese *ngầu*, English "I" into the right Vietnamese pronoun for who you're talking to, US *truck* into UK *lorry*. Dictionary data comes from [Wiktionary](https://en.wiktionary.org/) (via [Kaikki.org](https://kaikki.org/)) and loads only when you need it.
4
4
 
5
- **Status:** which-dialect is starting with **Vietnamese**, with English as the bridge language, and the current focus is cleaning up Vietnamese translations. **More languages are planned.** The translator and data format are language-general, so adding one is a config file plus a data package (see [Adding a language](#adding-a-language)). Spanish was supported early on and has been removed for now so the work can focus on Vietnamese.
5
+ **Status:** which-dialect is starting with **Vietnamese**, with English as the bridge language, and the current focus is cleaning up Vietnamese translations. **More languages are planned.** The translator and data format are language-general, so adding one is a config file plus a data package (see [Adding a language](#adding-a-language)). **Spanish** is supported too, by country and region group (Spain, Mexico, Latin America, Río de la Plata, …).
6
6
 
7
7
  ```ts
8
8
  import { createTranslator } from 'which-dialect'
@@ -18,7 +18,7 @@ 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.
21
+ **Data:** Wiktionary data of 2026-09-25 (Vietnamese: 2026-09-28). See [`DATA_UPDATES.md`](DATA_UPDATES.md) for each language's dates and update history.
22
22
 
23
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.
24
24
 
@@ -27,9 +27,10 @@ You install one small package. Dictionary data is **not bundled**: each lookup f
27
27
  | Language | Code | Data package | Regions | Entries | Region-tagged senses |
28
28
  |---|---|---|---|---|---|
29
29
  | Vietnamese | `vi` | [`which-dialect-vi`](packages/vi) | Northern, Central, Southern | 35,394 | 1,330 of 42,419 (3.1%) |
30
+ | Spanish | `es` | [`which-dialect-es`](packages/es) | 23 countries (Spain, Mexico, Argentina, Cuba, …), plus groups: Latin America, Central America, Caribbean, South America, Río de la Plata | 123,995 | 11,940 of 153,963 (7.8%) |
30
31
  | English | `en` | [`which-dialect-en`](packages/en) | 15 countries/areas (US, UK, Australia, India, …), plus groups: North America, British Isles, Commonwealth, … | 594,544 | 37,339 of 776,085 (4.8%) |
31
32
 
32
- Vietnamese and English can be translated in both directions, and between any of their regions, including between two dialects of one language (Northern → Southern Vietnamese, US → UK English). English is also the bridge every language's definitions are written in.
33
+ Any of these languages can be translated into any other, in both directions and between any of their regions, including between two dialects of one language (Northern → Southern Vietnamese, US → UK English, Spain → Mexico Spanish) and dialect to dialect across languages (Mexican Spanish → Southern Vietnamese). English is also the bridge every language's definitions are written in.
33
34
 
34
35
  More languages are planned. Each one is a config file in [`languages/`](languages) plus a data package in `packages/<code>`; see [Adding a language](#adding-a-language).
35
36
 
@@ -92,20 +93,20 @@ Meanings are ordered by `meaning` (if given), then by being tagged for `fromRegi
92
93
 
93
94
  ### Hand-picked words
94
95
 
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
+ 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. A pick can be a phrase the dictionary doesn't list (*chưa bao giờ*, "never"; marked `phrase: true`), and a row can `exclude` words that should never be given for its meaning (*mát mặt* for *well* "in good health").
96
97
 
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
+ 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*; a few grammar words; and 54 meanings from a native speaker's review of the 300 most common English words (*can* "know how to" → *biết*; *how* → *như thế nào*, Southern *làm sao*; *life* "a living being" → *mạng*).
98
99
 
99
100
  ### How accurate is it?
100
101
 
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:
102
+ `npm run evaluate` runs known-correct translations across language pairs and dialect pairs (English → Vietnamese by region, including pronouns, English → Spanish by country, Spanish ↔ Vietnamese, Northern → Southern Vietnamese, Spain ↔ Mexico, US ↔ UK, Vietnamese → English). A case passes when a correct word is in the top 3 of the first meaning:
102
103
 
103
104
  | Set | Correct word in top 3 | Correct word first |
104
105
  |---|---|---|
105
- | Tuning set (used while developing) | 49 / 49 (100%) | 49 / 49 (100%); 48 / 49 without picks |
106
- | Held-out set (written afterwards, not tuned against) | 17 / 17 (100%) | 16 / 17 (94%) |
106
+ | Tuning set (used while developing) | 88 / 91 (97%) | 82 / 91 (90%); 81 / 91 without picks |
107
+ | Held-out set (written afterwards, not tuned against) | 29 / 30 (97%) | 28 / 30 (93%) |
107
108
 
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)).
109
+ With Spanish back (2026-10-29), its cases are back too; the misses are Spanish (*cerdo* without a part of speech, and Mexican "popcorn" and "cat"; see [`FUTURE_IMPROVEMENTS.md`](FUTURE_IMPROVEMENTS.md)). 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)).
109
110
 
110
111
  ## Checking text
111
112
 
@@ -208,7 +209,7 @@ Each sentence has `original`, `parts`, `corrected`, `changes` (`{ from, to, why,
208
209
 
209
210
  ### `createDictionary({ lang, baseUrl?, load? })`
210
211
 
211
- One language's dictionary. `baseUrl` defaults to `https://cdn.jsdelivr.net/npm/which-dialect-<lang>@0.1/data`.
212
+ One language's dictionary. `baseUrl` defaults to `https://cdn.jsdelivr.net/npm/which-dialect-<lang>@0.2/data`.
212
213
 
213
214
  - `dictionary.lookup(word)`: all entries for a word, with every sense's definitions, regions, labels, synonyms and variant links (`altOf`). The word as written, else lowercase, else capitalized, so names written in lowercase are found (*nhật* → *Nhật*, Japan; *nhật bản* → *Nhật Bản*).
214
215
  - `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.
@@ -216,7 +217,7 @@ One language's dictionary. `baseUrl` defaults to `https://cdn.jsdelivr.net/npm/w
216
217
  - `dictionary.segment(text)`: the text split into the language's words, longest dictionary match first (up to the checker's `maxWordUnits`, 3 for Vietnamese): `Hôm nay tôi ăn cơm.` → *Hôm nay* · *tôi* · *ăn cơm*, each `{ text, start, end, entries }` (offsets into the NFC text; `entries` empty for words the dictionary doesn't have). Use it to treat a word of several syllables as one, e.g. to show its meaning.
217
218
  - `dictionary.variants(word, { limit? })`: other words with the same letters but other accents that the dictionary has, most common first, each `{ word, entries, frequency? }`: *muộn* → *muốn*, *mượn*, *muôn*…; for words of several syllables every syllable varies (*hom nay* → *hôm nay*). Capitalized headwords count too, unless they're only a person's name: *nhat* → *nhất*, *Nhật* (Japan), *nhạt*…; *nhat ban* → *Nhật Bản*. For apps that let learners pick the word they meant: a real word with the wrong tone passes the spellchecker. Languages written in syllables only (`[]` otherwise).
218
219
  - `dictionary.checker()` / `dictionary.syllables()`: the language's checker settings and syllable list (with frequencies), used by `createChecker` (`{}` for languages without them).
219
- - `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).
220
+ - `dictionary.picks()`: the language's [hand-picked words](#hand-picked-words), `{ word, pos, gloss, picks: [{ word, tags? }], exclude?, first? }` per English meaning (`gloss` is the sense's own definition: the last one, for a sense nested under a heading) (empty for languages without any).
220
221
  - `dictionary.meta()`: the language's name, regions, region groups, source, license, build date and counts.
221
222
  - `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`.
222
223
 
@@ -311,7 +312,7 @@ Generated data isn't committed (it would bloat git history); it's built before p
311
312
 
312
313
  ### Adding a language
313
314
 
314
- 1. Add `languages/<code>.ts`: the Kaikki language name, the language's regions (and groups, if any), and how to read regions from Wiktionary's tags. See [`languages/vi.ts`](languages/vi.ts) (dialect areas and a pronoun table) and [`languages/en.ts`](languages/en.ts) (countries with region groups, and trimming a very large language). Spanish was built this way too (countries and region groups; `languages/es.ts` in the git history). Set `placeNames: {}` so its place names translate ("Japan" ↔ its name for Japan). Set `skipFormOf` for heavily inflected languages; for very large ones, see the trimming options in [`scripts/language-config.ts`](scripts/language-config.ts) (`dropTechnical`, `formOfPos`, `keepFormOf`, `maxSensesPerEntry`, `shardLength`, …).
315
+ 1. Add `languages/<code>.ts`: the Kaikki language name, the language's regions (and groups, if any), and how to read regions from Wiktionary's tags. See [`languages/vi.ts`](languages/vi.ts) (dialect areas and a pronoun table) and [`languages/en.ts`](languages/en.ts) (countries with region groups, and trimming a very large language). [`languages/es.ts`](languages/es.ts) shows countries with region groups (Latin America, Río de la Plata) and `skipFormOf`. Set `placeNames: {}` so its place names translate ("Japan" ↔ its name for Japan). Set `skipFormOf` for heavily inflected languages; for very large ones, see the trimming options in [`scripts/language-config.ts`](scripts/language-config.ts) (`dropTechnical`, `formOfPos`, `keepFormOf`, `maxSensesPerEntry`, `shardLength`, …).
315
316
  2. Copy a data package (`packages/vi`) to `packages/<code>` and update its `package.json` and `README.md`.
316
317
  3. Add the language's code to `translationLangs` in `languages/en.ts` (so English keeps its translation tables for it) and rebuild English.
317
318
  4. `npm run build:data -- <code>`, check the results, add a few real-data tests and evaluation cases, and publish.
@@ -330,4 +331,4 @@ npm publish -w which-dialect-vi
330
331
  npm publish -w which-dialect
331
332
  ```
332
333
 
333
- The API loads data versions matching `DATA_VERSION` in `packages/core/src/index.ts` (currently `0.1`). Data-only updates can publish new 0.1.x data versions without touching the API; bump `DATA_VERSION` when the data format changes.
334
+ The API loads data versions matching `DATA_VERSION` in `packages/core/src/index.ts` (currently `0.2`; `0.1` up to which-dialect 0.5.6). Data-only updates can publish new 0.2.x data versions without touching the API; bump `DATA_VERSION` when the data format changes.
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@ export * from './pos.ts';
5
5
  * Data packages this version of the API reads by default, as a jsDelivr version range.
6
6
  * Bump it when the data format changes.
7
7
  */
8
- export declare const DATA_VERSION = "0.1";
8
+ export declare const DATA_VERSION = "0.2";
9
9
  /** Labels left out of search results unless `exclude` is passed. */
10
10
  export declare const DEFAULT_EXCLUDED_LABELS: string[];
11
11
  /** Loads one data file by its path inside the data folder, e.g. "meta.json" or "en/no.json". */
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ export * from "./pos.js";
6
6
  * Data packages this version of the API reads by default, as a jsDelivr version range.
7
7
  * Bump it when the data format changes.
8
8
  */
9
- export const DATA_VERSION = '0.1';
9
+ export const DATA_VERSION = '0.2';
10
10
  /** Labels left out of search results unless `exclude` is passed. */
11
11
  export const DEFAULT_EXCLUDED_LABELS = [
12
12
  'vulgar', 'offensive', 'derogatory', 'archaic', 'obsolete', 'dated', 'historical', 'rare', 'abbreviation',
package/dist/translate.js CHANGED
@@ -445,8 +445,9 @@ export function createTranslator(options = {}) {
445
445
  return hits;
446
446
  }
447
447
  async function englishMeanings(sense, from, bridge) {
448
+ // A meaning's gloss is its own (last) definition: a nested sense's first is the heading it shares.
448
449
  if (from === 'en')
449
- return [{ word: sense.lemma, pos: sense.pos, gloss: sense.glosses[0] ?? '', translations: sense.translations }];
450
+ return [{ word: sense.lemma, pos: sense.pos, gloss: sense.glosses.at(-1) ?? '', translations: sense.translations }];
450
451
  const found = [];
451
452
  for (const term of bridge.slice(0, 3)) {
452
453
  for (const entry of await dict('en').lookup(term)) {
@@ -454,8 +455,9 @@ export function createTranslator(options = {}) {
454
455
  const own = s.translations?.[from];
455
456
  if (!own?.some((t) => t.word === sense.lemma || t.word === sense.word))
456
457
  continue;
457
- if (!found.some((f) => f.word === entry.word && f.gloss === s.glosses[0])) {
458
- found.push({ word: entry.word, pos: entry.pos, gloss: s.glosses[0], translations: s.translations });
458
+ const gloss = s.glosses.at(-1) ?? '';
459
+ if (!found.some((f) => f.word === entry.word && f.gloss === gloss)) {
460
+ found.push({ word: entry.word, pos: entry.pos, gloss, translations: s.translations });
459
461
  }
460
462
  }
461
463
  }
@@ -522,6 +524,13 @@ export function createTranslator(options = {}) {
522
524
  ...(entry.frequency !== undefined ? { frequency: entry.frequency } : {}),
523
525
  };
524
526
  }
527
+ // A hand-picked phrase the dictionary doesn't list: used everywhere, or where its tags say.
528
+ function phraseHit(word, sense, places, meta) {
529
+ return {
530
+ word, pos: sense.pos, gloss: sense.glosses.at(-1) ?? '', regions: places ? [...places] : meta.regions,
531
+ regionTagged: Boolean(places), labels: [], senseIndex: 0, senses: 1, primary: true, phrase: true,
532
+ };
533
+ }
525
534
  // The source word's uses in a pronoun table, with the definition each came from.
526
535
  function pronounUses(rows, word) {
527
536
  const w = word.trim();
@@ -722,7 +731,8 @@ export function createTranslator(options = {}) {
722
731
  // Hand-picked words for this meaning go first, in the listed order: a speaker's judgment of the
723
732
  // natural word where the ranking puts another first. With a target region, picks tagged for it
724
733
  // lead, and picks tagged only for other regions (or words not used there) are left to the
725
- // ranking. The ranked words follow. (A pronoun-table row, below, still comes before them.)
734
+ // ranking. The ranked words follow. (A pronoun-table row, below, still comes before them.) A pick
735
+ // that isn't a headword is a phrase ("bữa tối"), given as is. Words a row excludes are dropped.
726
736
  const rows = pickRows.length
727
737
  ? await englishMeanings(sense, from, bridge).then((ms) => pickRows.filter((r) => ms.some((m) => m.word === r.word && m.pos === r.pos && m.gloss === r.gloss)))
728
738
  : [];
@@ -737,13 +747,17 @@ export function createTranslator(options = {}) {
737
747
  continue;
738
748
  if (to === from && t.word === sense.lemma && !places?.size)
739
749
  continue;
740
- const hit = scored.get(t.word) ?? (await tableHit(t.word, to, posList, toWanted, exclude, bridge));
750
+ const hit = scored.get(t.word) ??
751
+ (await tableHit(t.word, to, posList, toWanted, exclude, bridge)) ??
752
+ (t.word.includes(' ') ? phraseHit(t.word, sense, places, toMeta) : null);
741
753
  if (!hit || hit.labels.some((l) => exclude.includes(l)))
742
754
  continue;
743
755
  if (!places && !intersects(hit.regions, toWanted))
744
756
  continue;
745
757
  picked.push({ hit, tagged: Boolean(toWanted && places) });
746
758
  }
759
+ for (const w of rows.flatMap((r) => r.exclude ?? []))
760
+ scored.delete(w);
747
761
  if (picked.length) {
748
762
  const ordered = [...picked.filter((p) => p.tagged), ...picked.filter((p) => !p.tagged)];
749
763
  const top = Math.max(0, ...[...scored.values()].map((t) => t.score));
package/dist/types.d.ts CHANGED
@@ -63,6 +63,8 @@ export type Hit = {
63
63
  frequency?: number;
64
64
  /** true when the English term is the first meaning listed in the gloss (e.g. "now" in "now, today"). */
65
65
  primary: boolean;
66
+ /** true for a hand-picked phrase that isn't a dictionary headword ("bữa tối"); its details are the pick's. */
67
+ phrase?: boolean;
66
68
  };
67
69
  export type LanguageMeta = {
68
70
  /** ISO 639 code, e.g. "vi". */
@@ -213,12 +215,18 @@ export type CheckerConfig = {
213
215
  * translating between two other languages reaches it through the English meaning.
214
216
  */
215
217
  export type PickRow = {
216
- /** The English headword, part of speech and first definition of the meaning, as in the English data. */
218
+ /**
219
+ * The English headword, part of speech and definition of the meaning, as in the English data. For a
220
+ * nested sense (a heading plus its own definition: "old", "Of a living being…") it's the sense's own,
221
+ * last definition, so senses under one heading are told apart.
222
+ */
217
223
  word: string;
218
224
  pos: string;
219
225
  gloss: string;
220
- /** Best first. `tags` name regions like translation-table tags ("Southern"). */
226
+ /** Best first. `tags` name regions like translation-table tags ("Southern"). A pick needn't be a headword ("bữa tối"). */
221
227
  picks: TableTranslation[];
228
+ /** Words never given for this meaning (a speaker removed them: wrong or unnatural here). */
229
+ exclude?: string[];
222
230
  /** Put this meaning first when the caller gives no `pos` or `meaning` ("got" → "Have/has."). */
223
231
  first?: boolean;
224
232
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "which-dialect",
3
- "version": "0.5.6",
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.",
3
+ "version": "0.5.8",
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",
7
7
  "types": "./dist/index.d.ts",
@@ -26,6 +26,7 @@
26
26
  "translation",
27
27
  "language-learning",
28
28
  "wiktionary",
29
+ "spanish",
29
30
  "vietnamese"
30
31
  ],
31
32
  "license": "MIT",