which-dialect 0.1.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/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 nguyen18
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ This license covers the code in this repository. The dictionary data in the
26
+ language data packages (packages/<lang>, published as
27
+ which-dialect-<lang>) is derived from Wiktionary and licensed
28
+ separately under CC BY-SA 4.0; see packages/<lang>/LICENSE.
package/README.md ADDED
@@ -0,0 +1,205 @@
1
+ # which-dialect
2
+
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
+
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.
6
+
7
+ ```ts
8
+ import { createTranslator } from 'which-dialect'
9
+
10
+ const tr = createTranslator()
11
+
12
+ await tr.translate('ngô', { from: 'vi', fromRegion: 'Northern', to: 'vi', toRegion: 'Southern' }) // bắp
13
+ await tr.translate('I', { from: 'en', to: 'vi', listener: 'parent' }) // con
14
+ await tr.translate('he', { from: 'en', to: 'vi', toRegion: 'Southern' }) // ảnh, nó, …
15
+ await tr.translate('truck', { from: 'en', fromRegion: 'US', to: 'en', toRegion: 'UK' }) // lorry
16
+ await tr.translate('cool', { from: 'en', to: 'vi', toRegion: 'Southern', meaning: 'awesome' }) // bá cháy, chất, khét, ngầu (not mát, "cool" the temperature)
17
+ ```
18
+
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
+
21
+ 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
+
23
+ ## Languages
24
+
25
+ | Language | Code | Data package | Regions | Entries | Region-tagged senses |
26
+ |---|---|---|---|---|---|
27
+ | Vietnamese | `vi` | [`which-dialect-vi`](packages/vi) | Northern, Central, Southern | 35,394 | 1,330 of 42,419 (3.1%) |
28
+ | 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%) |
29
+
30
+ 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.
31
+
32
+ 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).
33
+
34
+ ## Install
35
+
36
+ ```sh
37
+ npm install which-dialect
38
+ ```
39
+
40
+ Works in browsers and in Node 18+ (anywhere with `fetch`).
41
+
42
+ ## Translating
43
+
44
+ ### `createTranslator({ baseUrl?, load?, dictionary? })`
45
+
46
+ Loads each language's data on demand. The options are only needed to self-host the data: `baseUrl(lang)` returns where a language's data folder is served, and `load(lang)` returns a custom loader (e.g. reading from disk in Node).
47
+
48
+ ### `translator.translate(word, options)`
49
+
50
+ | Option | Meaning |
51
+ |---|---|
52
+ | `from`, `to` | Language codes. They can be the same, to translate between dialects of one language. |
53
+ | `fromRegion`, `toRegion` | Region or region group, e.g. `'Southern'`, `'Northern'`, `'UK'`, `'North America'`. `fromRegion` keeps only the word's senses used there; `toRegion` ranks words tagged for it first and leaves out words tagged only for other regions. |
54
+ | `pos` | Only source senses with this part of speech, e.g. `'noun'`. |
55
+ | `meaning` | A few words describing the meaning you want, e.g. `'awesome'` for *cool*. Matching senses and words rank first. |
56
+ | `register` | `'casual'`, `'neutral'` or `'polite'`: the register you want translations in, instead of the source's own. `'casual'` favors colloquial words (Southern *tui* for "I"). |
57
+ | `listener` | Who you're talking to, as a row id of the target language's pronoun table (Vietnamese: `'parent'`, `'older-male'`, `'friend'`, …; see `dictionary.pronouns()`). For "I"/"me", "you", "we"/"us" and plural "you" that relationship's words come first: *I* to a parent → *con*; *you* → Southern *ba*, *má*; *we* → *chúng con*. Without it, the table's neutral default row leads (*I* → *tôi*, *you* → *bạn*). Ignored for languages without a pronoun table. |
58
+ | `about` | Who you're talking about, as a row id like `listener`, for "he"/"him", "she"/"her" and "they"/"them": *he* about a grandparent → *ông ấy* (Southern *ổng*); *she* about a teacher → *cô*. Without it, the default row leads (*he* → *anh ấy*, Southern *ảnh*; *they* → *họ*). |
59
+ | `speaker` | `'male'` or `'female'`, for pronouns that depend on it (*anh* vs *chị* toward someone younger). |
60
+ | `exclude` | Labels to leave out (default: vulgar, offensive, derogatory, archaic, obsolete, dated, historical, rare, abbreviation). |
61
+ | `limit` | Translations per meaning (default 5). |
62
+ | `allSenses` | Return every meaning, including ones with no translation, instead of only useful ones. For letting users pick a meaning. |
63
+
64
+ It returns `TranslationGroup[]`, most relevant meaning first. Each group has:
65
+
66
+ - `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").
68
+ - `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
+ - `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
+
71
+ ### `translator.senses(word, { from, fromRegion?, pos? })`
72
+
73
+ The meanings of a word, following forms and variant spellings: useful for letting a user pick which meaning they mean before translating.
74
+
75
+ ## How translation works
76
+
77
+ Every language's data defines its words in English, so English is the bridge between any two languages. For each **meaning** of the source word:
78
+
79
+ 1. **Forms and variants go to the word they belong to:** irregular forms from the data (*said* → *say*), regular English forms by rule (*walked* → *walk*, *running* → *run*), texting spellings (*dont* → *don't*), and regional variants (*hông* → *không*, keeping its Southern region).
80
+ 2. **English terms carry the meaning:** the terms in its definition ("bắp": *corn*), or for English words the word itself and its synonyms.
81
+ 3. **The target is searched with a compatible part of speech** (`compatiblePos`): a noun for a noun, but an English auxiliary verb may be a Vietnamese particle, and an adjective may be a verb in another language.
82
+ 4. **Wiktionary's translation tables** give the usual translation of each English meaning in each language, often with region and register tags ("I" → Vietnamese *tôi*, *tớ* (informal), *tui* (South); *cool* "mildly low temperature" → *mát*). Listed words get a strong boost, more when tagged for the target region, less when tagged only for other regions. From another language, the English meaning whose table lists the source word is the same meaning.
83
+ 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
+ 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
+
86
+ 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
+
88
+ ### How accurate is it?
89
+
90
+ `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
+
92
+ | Set | Correct word in top 3 | Correct word first |
93
+ |---|---|---|
94
+ | Tuning set (used while developing) | 42 / 42 (100%) | 42 / 42 (100%) |
95
+ | Held-out set (written afterwards, not tuned against) | 17 / 17 (100%) | 16 / 17 (94%) |
96
+
97
+ 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
+
99
+ ## Looking words up
100
+
101
+ ### `createDictionary({ lang, baseUrl?, load? })`
102
+
103
+ One language's dictionary. `baseUrl` defaults to `https://cdn.jsdelivr.net/npm/which-dialect-<lang>@0.1/data`.
104
+
105
+ - `dictionary.lookup(word)`: all entries for a word, with every sense's definitions, regions, labels, synonyms and variant links (`altOf`).
106
+ - `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.
107
+ - `dictionary.meta()`: the language's name, regions, region groups, source, license, build date and counts.
108
+ - `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
+
110
+ ```js
111
+ const vi = createDictionary({ lang: 'vi' })
112
+ const [parents] = await vi.pronouns({ listener: 'parent', region: 'Southern' })
113
+ parents.self.map((c) => c.word) // ['con']
114
+ parents.addressee.map((c) => c.word) // ['ba', 'má', 'mẹ']
115
+ ```
116
+
117
+ Vietnamese rows: `general` (the default: neutral words), `friend`, `close-friend`, `older-male`, `older-female`, `younger`, `parent`, `parents-age`, `grandparents-age`, `teacher`, `partner`, `formal`. `speaker: 'male' | 'female'` resolves choices like *anh* vs *chị* toward someone younger.
118
+
119
+ ## Parts of speech
120
+
121
+ Entries and results use Wiktionary's part-of-speech codes (`noun`, `verb`, `adj`, `adv`, `pron`, `det`, `prep`, `conj`, `intj`, `particle`, `classifier`, `num`, `phrase`, `contraction`, …).
122
+
123
+ - `POS_NAMES` / `posName(code)`: readable names and one-line explanations for learners (`posName('adj')` → "Adjective"; `particle`: "A small word that adds grammar or tone rather than meaning, e.g. Vietnamese đã (past)…").
124
+ - `compatiblePos(pos)`: the parts of speech that can translate one from any language.
125
+
126
+ ## How regions work
127
+
128
+ Each language defines its regions: dialect areas for Vietnamese, countries and areas for English. Wiktionary tags some senses with a region; for example Vietnamese *lợn* "pig" is tagged Northern and *heo* Central and Southern. **Senses with no region tag are treated as used in every region**, and marked `regionTagged: false`, so you can tell a confirmed regional word from an assumed one.
129
+
130
+ - **Per sense, not per word:** Vietnamese *má* means "cheek" everywhere, but "mother" only in the South.
131
+ - **Groups:** a language can define groups of regions. Wiktionary's group tags (English "British Isles", "North America") count for every region in the group, and you can search a group as a whole.
132
+ - **Variants:** words recorded as a variant of another ("Southern Vietnam form of *không*") are found by the other word's meanings, so "not" finds *hông*, and keep their region when translated.
133
+
134
+ ## Limitations
135
+
136
+ This is a suggestion tool, not a curated translation dictionary. Check results before teaching them.
137
+
138
+ - **Few region tags.** Most senses carry no region (see the table), so they're assumed to be used everywhere, including some words that are really old-fashioned or local.
139
+ - **No word frequencies.** The source doesn't say how common a word or meaning is. Without `pos`/`meaning`, the first meaning follows the dictionary's order, which isn't always the most common one.
140
+ - **English is the bridge.** Translating between two non-English languages goes through English definitions, so nuance English doesn't mark can be lost.
141
+ - **`meaning` matches words, not ideas.** It compares your words with the dictionary's definitions (and the top translation's), so "awesome" won't find a sense defined as "Fashionable; trendy; hip." Use the definition's own wording, or let users pick from `senses()`.
142
+ - **Grammar words translate poorly.** Words defined by their function ("marks the future tense" for Vietnamese *sẽ*) aren't reached from English *will*; *the*, *is* or *gonna* may give nothing useful.
143
+
144
+ ## Word frequencies
145
+
146
+ Translations are ranked partly by how common each word is, using **[wordfreq](https://github.com/rspeer/wordfreq)** by Robyn Speer (Robyn Speer. (2022). rspeer/wordfreq: v3.0 (v3.0.2). Zenodo. https://doi.org/10.5281/zenodo.7199437), licensed CC BY-SA 4.0. Only a single frequency score is stored for each word already in these dictionaries (`frequency` on entries and results), never wordfreq's word lists.
147
+
148
+ wordfreq's data comes from, and is credited to:
149
+
150
+ - Google Books Ngrams (<http://books.google.com/ngrams>) and Google Books Syntactic Ngrams.
151
+ - The Leeds Internet Corpus, from the University of Leeds Centre for Translation Studies (<http://corpus.leeds.ac.uk/list.html>).
152
+ - Wikipedia, the free encyclopedia (<http://www.wikipedia.org>).
153
+ - ParaCrawl, a multilingual Web crawl (<https://paracrawl.eu>).
154
+ - OPUS OpenSubtitles 2018 (<http://opus.nlpl.eu/OpenSubtitles.php>), whose data originates from the OpenSubtitles project (<http://www.opensubtitles.org/>).
155
+ - SUBTLEX word lists (SUBTLEX-US, SUBTLEX-UK, SUBTLEX-CH, SUBTLEX-DE, SUBTLEX-NL) created by **Marc Brysbaert et al.**; SUBTLEX is freely available data (<http://crr.ugent.be/programs-data/subtitle-frequencies>).
156
+ - Word statistics gathered from the Twitter streaming API (no Twitter content is included).
157
+
158
+ wordfreq measures commonness **worldwide**, so a regional word can look rare even where it's the everyday word; the region bonus is kept separate from frequency for that reason.
159
+
160
+ ## License and attribution
161
+
162
+ - **Code** (this repo, and the `which-dialect` package): [MIT](LICENSE).
163
+ - **Data** (the `which-dialect-<code>` packages): derived from Wiktionary via Kaikki.org's [wiktextract](https://github.com/tatuylonen/wiktextract) extraction, modified (filtered and reshaped) by this project, with word frequencies from [wordfreq](#word-frequencies), and licensed under [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/).
164
+
165
+ If your app **shows** this data, credit it where users can see it, for example:
166
+
167
+ > Translations from [Wiktionary](https://en.wiktionary.org/), [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/), via which-dialect.
168
+
169
+ If you **redistribute modified data**, it has to stay under CC BY-SA 4.0. Using the API in your app doesn't change your app's own license. This project isn't affiliated with or endorsed by Wikimedia or Kaikki.org.
170
+
171
+ ## Development
172
+
173
+ Requires Node 22.6+ (TypeScript scripts run with `--experimental-strip-types`).
174
+
175
+ ```sh
176
+ npm install
177
+ npm run build:data -- vi # download Kaikki's file for a language (cached in .cache/) and build packages/<code>/data
178
+ npm run build:data -- vi --refresh # re-download first
179
+ 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)
181
+ npm run typecheck
182
+ npm run build # compile the API to packages/core/dist
183
+ ```
184
+
185
+ Generated data isn't committed (it would bloat git history); it's built before publishing. Downloads: Vietnamese 79 MB, English 3.3 GB. Builds take seconds (English: about 25 s and 1.4 GB of memory).
186
+
187
+ ### Adding a language
188
+
189
+ 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 `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`, …).
190
+ 2. Copy a data package (`packages/vi`) to `packages/<code>` and update its `package.json` and `README.md`.
191
+ 3. Add the language's code to `translationLangs` in `languages/en.ts` (so English keeps its translation tables for it) and rebuild English.
192
+ 4. `npm run build:data -- <code>`, check the results, add a few real-data tests and evaluation cases, and publish.
193
+
194
+ 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
+
196
+ ### Publishing
197
+
198
+ ```sh
199
+ npm run build:data -- en --refresh && npm run build:data -- vi --refresh && npm test
200
+ npm publish -w which-dialect-en
201
+ npm publish -w which-dialect-vi
202
+ npm publish -w which-dialect
203
+ ```
204
+
205
+ 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.
@@ -0,0 +1,76 @@
1
+ import { type PronounRow, type Entry, type Hit, type LanguageMeta } from './types.ts';
2
+ export * from './types.ts';
3
+ export * from './pos.ts';
4
+ /**
5
+ * Data packages this version of the API reads by default, as a jsDelivr version range.
6
+ * Bump it when the data format changes.
7
+ */
8
+ export declare const DATA_VERSION = "0.1";
9
+ /** Labels left out of search results unless `exclude` is passed. */
10
+ export declare const DEFAULT_EXCLUDED_LABELS: string[];
11
+ /** Loads one data file by its path inside the data folder, e.g. "meta.json" or "en/no.json". */
12
+ export type LoadJson = (path: string) => Promise<unknown>;
13
+ export type DictionaryOptions = {
14
+ /** ISO 639 code of a published data package, e.g. "vi" for which-dialect-vi. */
15
+ lang: string;
16
+ /**
17
+ * Where the data folder is served. Defaults to the data package on jsDelivr, so nothing has to be
18
+ * bundled: https://cdn.jsdelivr.net/npm/which-dialect-<lang>@<DATA_VERSION>/data
19
+ */
20
+ baseUrl?: string;
21
+ /** Custom loader instead of fetch, e.g. reading from disk in Node. Overrides baseUrl. */
22
+ load?: LoadJson;
23
+ };
24
+ export type SearchOptions = {
25
+ /**
26
+ * Only return words used in this region, or in any region of a group (see meta().regions and
27
+ * meta().regionGroups, e.g. Vietnamese "Southern" or English "North America"). Untagged words count as every region.
28
+ */
29
+ region?: string;
30
+ /** Only return these parts of speech (Wiktionary codes, see POS_NAMES), e.g. ['verb']. */
31
+ pos?: string[];
32
+ /** Labels to leave out. Defaults to DEFAULT_EXCLUDED_LABELS; pass [] to include everything. */
33
+ exclude?: string[];
34
+ /** Maximum number of results (default 10). */
35
+ limit?: number;
36
+ /**
37
+ * Return every matching sense of each word instead of one result per word (its best-ranked sense), so
38
+ * a caller can choose between a word's senses itself. `limit` then counts words, not results.
39
+ */
40
+ allSenses?: boolean;
41
+ };
42
+ export type PronounOptions = {
43
+ /** Only this relationship's row, by id (e.g. "parent"). Unknown ids throw, listing the valid ones. */
44
+ listener?: string;
45
+ /** Only words used in this region or region group; untagged words count as every region. */
46
+ region?: string;
47
+ /** Drop words only the other gender uses ("anh" vs "chị" toward someone younger). */
48
+ speaker?: 'male' | 'female';
49
+ /** Labels to leave out. Defaults to DEFAULT_EXCLUDED_LABELS; pass [] to include everything. */
50
+ exclude?: string[];
51
+ };
52
+ export type Dictionary = {
53
+ /** The language's name, regions, source, license and counts. */
54
+ meta(): Promise<LanguageMeta>;
55
+ /** Entries for a word in the language, e.g. lookup("má"). */
56
+ lookup(word: string): Promise<Entry[]>;
57
+ /**
58
+ * Ways to say an English word or short phrase, best first, one result per word. With a region,
59
+ * words the source tags for that region rank ahead of untagged ones.
60
+ */
61
+ searchEnglish(term: string, options?: SearchOptions): Promise<Hit[]>;
62
+ /**
63
+ * How to say "I", "you", "he/she", "we", plural "you" and "they" depending on who you're talking to
64
+ * (or about), one row per relationship, for languages whose pronouns depend on it (Vietnamese). Empty
65
+ * for languages without a pronoun table.
66
+ */
67
+ pronouns(options?: PronounOptions): Promise<PronounRow[]>;
68
+ };
69
+ /**
70
+ * The regions a region or region-group name stands for (e.g. English "North America" -> US, Canada),
71
+ * or null when no region is given. Throws for names the language doesn't have, listing the valid ones.
72
+ */
73
+ export declare function resolveRegion(meta: LanguageMeta, region: string | undefined): Set<string> | null;
74
+ export declare function dataUrl(lang: string): string;
75
+ export declare function createDictionary(options: DictionaryOptions): Dictionary;
76
+ export * from './translate.ts';
package/dist/index.js ADDED
@@ -0,0 +1,130 @@
1
+ import { PRONOUN_PERSONS, fromStored, normalizeEnglish, shardKey, } from "./types.js";
2
+ export * from "./types.js";
3
+ export * from "./pos.js";
4
+ /**
5
+ * Data packages this version of the API reads by default, as a jsDelivr version range.
6
+ * Bump it when the data format changes.
7
+ */
8
+ export const DATA_VERSION = '0.1';
9
+ /** Labels left out of search results unless `exclude` is passed. */
10
+ export const DEFAULT_EXCLUDED_LABELS = [
11
+ 'vulgar', 'offensive', 'derogatory', 'archaic', 'obsolete', 'dated', 'historical', 'rare', 'abbreviation',
12
+ ];
13
+ // Search ranks plain, everyday words first. Labels in the first set cost a little (still normal speech,
14
+ // just a particular register); the second set costs more (specialized, playful or uncommon).
15
+ const MILD_LABELS = new Set(['colloquial', 'informal', 'familiar', 'polite', 'formal', 'endearing', 'honorific']);
16
+ const LABEL_PENALTY = (h) => h.labels.reduce((sum, l) => sum + (MILD_LABELS.has(l) ? 1 : 2), 0);
17
+ /**
18
+ * The regions a region or region-group name stands for (e.g. English "North America" -> US, Canada),
19
+ * or null when no region is given. Throws for names the language doesn't have, listing the valid ones.
20
+ */
21
+ export function resolveRegion(meta, region) {
22
+ if (!region)
23
+ return null;
24
+ const members = meta.regions.includes(region) ? [region] : meta.regionGroups?.[region];
25
+ if (!members) {
26
+ const groups = Object.keys(meta.regionGroups ?? {});
27
+ throw new Error(`which-dialect: "${region}" isn't a ${meta.name} region or group ` +
28
+ `(regions: ${meta.regions.join(', ')}${groups.length ? `; groups: ${groups.join(', ')}` : ''})`);
29
+ }
30
+ return new Set(members);
31
+ }
32
+ export function dataUrl(lang) {
33
+ return `https://cdn.jsdelivr.net/npm/which-dialect-${lang}@${DATA_VERSION}/data`;
34
+ }
35
+ function fetchLoader(baseUrl) {
36
+ const base = baseUrl.replace(/\/+$/, '');
37
+ return async (path) => {
38
+ const res = await fetch(`${base}/${path}`);
39
+ if (!res.ok)
40
+ throw new Error(`which-dialect: couldn't load ${base}/${path} (${res.status})`);
41
+ return res.json();
42
+ };
43
+ }
44
+ export function createDictionary(options) {
45
+ const load = options.load ?? fetchLoader(options.baseUrl ?? dataUrl(options.lang));
46
+ // Each file is loaded at most once; a failed load is forgotten so it can be retried.
47
+ const cache = new Map();
48
+ const loadOnce = (path) => {
49
+ let pending = cache.get(path);
50
+ if (!pending) {
51
+ pending = load(path).catch((err) => {
52
+ cache.delete(path);
53
+ throw err;
54
+ });
55
+ cache.set(path, pending);
56
+ }
57
+ return pending;
58
+ };
59
+ const meta = () => loadOnce('meta.json');
60
+ // Missing shards simply mean no words start with those letters, so they aren't requested.
61
+ async function shard(kind, term) {
62
+ const m = await meta();
63
+ const key = shardKey(term, m.shardLength);
64
+ return m.shards[kind].includes(key) ? loadOnce(`${kind}/${key}.json`) : null;
65
+ }
66
+ return {
67
+ meta,
68
+ async lookup(word) {
69
+ const w = word.trim();
70
+ const [m, data] = await Promise.all([meta(), shard('words', w)]);
71
+ const stored = data?.[w] ?? data?.[w.toLowerCase()] ?? [];
72
+ return stored.map((e) => ({ ...e, senses: e.senses.map((s) => fromStored(s, m.regions)) }));
73
+ },
74
+ async searchEnglish(term, { region, pos, exclude = DEFAULT_EXCLUDED_LABELS, limit = 10, allSenses = false } = {}) {
75
+ const key = normalizeEnglish(term);
76
+ if (!key)
77
+ return [];
78
+ const m = await meta();
79
+ const wanted = resolveRegion(m, region);
80
+ const inWanted = (h) => !wanted || h.regions.some((r) => wanted.has(r));
81
+ const hits = ((await shard('en', key))?.[key] ?? []).map((h) => fromStored(h, m.regions));
82
+ const excluded = new Set(exclude);
83
+ const regional = (h) => Boolean(wanted && h.regionTagged);
84
+ // Best first: the English term is the gloss's main meaning, the word is tagged for the requested
85
+ // region, the word is plain rather than slang or literary, the word is common (has more senses),
86
+ // then the word's earlier senses.
87
+ const ranked = hits
88
+ .filter((h) => !h.labels.some((l) => excluded.has(l)))
89
+ .filter(inWanted)
90
+ .filter((h) => !pos || pos.includes(h.pos))
91
+ .sort((a, b) => Number(b.primary) - Number(a.primary) ||
92
+ Number(regional(b)) - Number(regional(a)) ||
93
+ LABEL_PENALTY(a) - LABEL_PENALTY(b) ||
94
+ // Common words first: wordfreq's frequency when the data has it, else the number of senses.
95
+ (b.frequency ?? 0) - (a.frequency ?? 0) ||
96
+ b.senses - a.senses ||
97
+ a.senseIndex - b.senseIndex);
98
+ // One result per word, its best-ranked sense (or every sense of the first `limit` words).
99
+ const seen = new Set();
100
+ const words = ranked.filter((h) => !seen.has(h.word) && seen.add(h.word)).slice(0, limit);
101
+ if (!allSenses)
102
+ return words;
103
+ const kept = new Set(words.map((h) => h.word));
104
+ return ranked.filter((h) => kept.has(h.word));
105
+ },
106
+ async pronouns({ listener, region, speaker, exclude = DEFAULT_EXCLUDED_LABELS } = {}) {
107
+ const m = await meta();
108
+ if (!m.pronouns)
109
+ return [];
110
+ const rows = await loadOnce('pronouns.json');
111
+ if (listener && !rows.some((r) => r.id === listener)) {
112
+ throw new Error(`which-dialect: unknown listener "${listener}" for ${m.name}. Use one of: ${rows.map((r) => r.id).join(', ')}`);
113
+ }
114
+ const wanted = resolveRegion(m, region);
115
+ const excluded = new Set(exclude);
116
+ const keep = (c) => (!wanted || c.regions.some((r) => wanted.has(r))) &&
117
+ (!speaker || !c.speaker || c.speaker === speaker) &&
118
+ !c.labels.some((l) => excluded.has(l));
119
+ return rows
120
+ .filter((r) => !listener || r.id === listener)
121
+ .map((r) => {
122
+ const row = { ...r };
123
+ for (const person of PRONOUN_PERSONS)
124
+ row[person] = (r[person] ?? []).map((c) => fromStored(c, m.regions)).filter(keep);
125
+ return row;
126
+ });
127
+ },
128
+ };
129
+ }
130
+ export * from "./translate.js";
package/dist/pos.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ export type PosInfo = {
2
+ name: string;
3
+ description: string;
4
+ };
5
+ export declare const POS_NAMES: Record<string, PosInfo>;
6
+ /** Readable name for a part-of-speech code, e.g. "adj" -> "Adjective". Unknown codes are returned as-is. */
7
+ export declare function posName(code: string): string;
8
+ /**
9
+ * Parts of speech that can translate a part of speech from any language. Languages don't line up one
10
+ * to one: English auxiliary verbs ("will", "can") and many adverbs are particles in Vietnamese,
11
+ * adjectives are verbs in languages without a separate adjective class, and a Vietnamese particle can
12
+ * be an adverb, conjunction or interjection elsewhere. The first entry is always the same part of speech.
13
+ */
14
+ export declare const COMPATIBLE_POS: Record<string, string[]>;
15
+ /** The parts of speech to search in another language for a part of speech (from any language). */
16
+ export declare function compatiblePos(pos: string): string[];
package/dist/pos.js ADDED
@@ -0,0 +1,70 @@
1
+ // Parts of speech. Entries use Wiktionary's codes (via Kaikki); these give readable names and
2
+ // short explanations for learners, and say which codes can translate each other across languages.
3
+ export const POS_NAMES = {
4
+ noun: { name: 'Noun', description: 'A person, place, thing or idea: "dog", "city", "happiness".' },
5
+ verb: { name: 'Verb', description: 'An action or state: "run", "think", "be".' },
6
+ adj: { name: 'Adjective', description: 'Describes a noun: "big", "happy", "cool".' },
7
+ adv: { name: 'Adverb', description: 'Describes how, when, where or how much: "quickly", "now", "very".' },
8
+ pron: { name: 'Pronoun', description: 'Stands in for a noun: "I", "you", "she", "they".' },
9
+ det: { name: 'Determiner', description: 'Points to or counts a noun: "the", "a", "this", "some".' },
10
+ article: { name: 'Article', description: 'A determiner like "the" or "a".' },
11
+ prep: { name: 'Preposition', description: 'Shows position or relation: "in", "on", "with", "for".' },
12
+ postp: { name: 'Postposition', description: 'Like a preposition, but placed after its noun.' },
13
+ conj: { name: 'Conjunction', description: 'Joins words or sentences: "and", "but", "because".' },
14
+ intj: { name: 'Interjection', description: 'An exclamation or reaction: "oh", "wow", "yeah".' },
15
+ particle: {
16
+ name: 'Particle',
17
+ description: 'A small word that adds grammar or tone rather than meaning, e.g. Vietnamese "đã" (past), "sẽ" (future), "nhé" (softener).',
18
+ },
19
+ classifier: {
20
+ name: 'Classifier',
21
+ description: 'A word used when counting or pointing at a noun, e.g. Vietnamese "con" for animals, "cái" for objects.',
22
+ },
23
+ num: { name: 'Number', description: 'A number word: "one", "twenty", "first".' },
24
+ contraction: { name: 'Contraction', description: 'Shortened words run together: "don\'t", "I\'m", "gonna".' },
25
+ phrase: { name: 'Phrase', description: 'A fixed group of words with its own meaning.' },
26
+ prep_phrase: { name: 'Prepositional phrase', description: 'A fixed phrase starting with a preposition: "at all", "in time".' },
27
+ proverb: { name: 'Proverb', description: 'A traditional saying.' },
28
+ prefix: { name: 'Prefix', description: 'A word part added to the front of a word: "un-", "re-".' },
29
+ suffix: { name: 'Suffix', description: 'A word part added to the end of a word: "-ness", "-ly".' },
30
+ combining_form: { name: 'Combining form', description: 'A word part only used inside other words.' },
31
+ name: { name: 'Proper noun', description: 'The name of a specific person, place or thing.' },
32
+ symbol: { name: 'Symbol', description: 'A sign or abbreviation that stands for something.' },
33
+ character: { name: 'Character', description: 'A single written character.' },
34
+ };
35
+ /** Readable name for a part-of-speech code, e.g. "adj" -> "Adjective". Unknown codes are returned as-is. */
36
+ export function posName(code) {
37
+ return POS_NAMES[code]?.name ?? code;
38
+ }
39
+ /**
40
+ * Parts of speech that can translate a part of speech from any language. Languages don't line up one
41
+ * to one: English auxiliary verbs ("will", "can") and many adverbs are particles in Vietnamese,
42
+ * adjectives are verbs in languages without a separate adjective class, and a Vietnamese particle can
43
+ * be an adverb, conjunction or interjection elsewhere. The first entry is always the same part of speech.
44
+ */
45
+ export const COMPATIBLE_POS = {
46
+ noun: ['noun', 'name', 'phrase'],
47
+ name: ['name', 'noun'],
48
+ verb: ['verb', 'particle', 'phrase', 'adj'],
49
+ adj: ['adj', 'verb', 'phrase'],
50
+ adv: ['adv', 'particle', 'phrase', 'prep_phrase', 'adj'],
51
+ pron: ['pron', 'noun', 'det'],
52
+ det: ['det', 'article', 'num', 'pron', 'particle', 'classifier'],
53
+ article: ['article', 'det', 'num', 'particle', 'classifier'],
54
+ prep: ['prep', 'postp', 'verb', 'adv', 'particle'],
55
+ postp: ['postp', 'prep', 'particle'],
56
+ conj: ['conj', 'particle', 'adv'],
57
+ intj: ['intj', 'particle', 'phrase'],
58
+ particle: ['particle', 'adv', 'verb', 'intj', 'conj', 'det', 'prep'],
59
+ classifier: ['classifier', 'noun', 'det'],
60
+ num: ['num', 'det'],
61
+ contraction: ['contraction', 'verb', 'phrase', 'pron', 'adv'],
62
+ phrase: ['phrase', 'verb', 'adv', 'prep_phrase', 'noun', 'intj'],
63
+ prep_phrase: ['prep_phrase', 'phrase', 'adv'],
64
+ prefix: ['prefix'],
65
+ suffix: ['suffix'],
66
+ };
67
+ /** The parts of speech to search in another language for a part of speech (from any language). */
68
+ export function compatiblePos(pos) {
69
+ return COMPATIBLE_POS[pos] ?? [pos];
70
+ }