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.
@@ -0,0 +1,227 @@
1
+ /** One meaning of a word. Regions are per sense: a word can be common in one meaning and regional in another. */
2
+ export type Sense = {
3
+ glosses: string[];
4
+ /**
5
+ * Regions where this sense is used. Senses with no region tag in the source are listed under
6
+ * every region of the language (see `regionTagged`).
7
+ */
8
+ regions: string[];
9
+ /** true when the source tagged the region(s); false when all regions were assumed because it had no tag. */
10
+ regionTagged: boolean;
11
+ /** Usage labels from the source, e.g. colloquial, literary, slang, archaic, vulgar. */
12
+ labels: string[];
13
+ /** Set when this sense is a variant of another word, e.g. Vietnamese "hông" is a Southern form of "không". */
14
+ altOf?: string;
15
+ /**
16
+ * Same-language words with this meaning, from Wiktionary: often the other dialects' words
17
+ * (Vietnamese "ngô" -> "bắp", "lợn" -> "heo", English "lift" -> "elevator").
18
+ */
19
+ synonyms?: string[];
20
+ /** Example sentences from Wiktionary, with an English translation for non-English languages. */
21
+ examples?: Example[];
22
+ /**
23
+ * English senses only: Wiktionary's translation table for this meaning, by language code: the words
24
+ * editors list as the usual translation of exactly this sense, with region/register tags when given
25
+ * (e.g. "I" → vi: tôi, tớ [informal], tui [South]).
26
+ */
27
+ translations?: Record<string, TableTranslation[]>;
28
+ };
29
+ export type TableTranslation = {
30
+ word: string;
31
+ tags?: string[];
32
+ };
33
+ export type Example = {
34
+ text: string;
35
+ translation?: string;
36
+ };
37
+ export type Entry = {
38
+ word: string;
39
+ /** Part of speech, e.g. noun, verb, adj, pron, particle. */
40
+ pos: string;
41
+ senses: Sense[];
42
+ /**
43
+ * How common the word is, as a Zipf frequency from wordfreq: log10 of uses per billion words (about 7
44
+ * for "the", 6 for everyday words, 3 for rare ones). Undefined when unknown (usually rare).
45
+ */
46
+ frequency?: number;
47
+ };
48
+ /** One way to say an English term: a word plus the sense that matched. */
49
+ export type Hit = {
50
+ word: string;
51
+ pos: string;
52
+ /** The gloss the English term came from. */
53
+ gloss: string;
54
+ regions: string[];
55
+ regionTagged: boolean;
56
+ labels: string[];
57
+ altOf?: string;
58
+ /** Position of the sense within its entry; earlier senses are usually the main meaning. */
59
+ senseIndex: number;
60
+ /** How many senses the word has in total (a fallback commonness signal when `frequency` is missing). */
61
+ senses: number;
62
+ /** How common the word is (Zipf frequency, see Entry.frequency). */
63
+ frequency?: number;
64
+ /** true when the English term is the first meaning listed in the gloss (e.g. "now" in "now, today"). */
65
+ primary: boolean;
66
+ };
67
+ export type LanguageMeta = {
68
+ /** ISO 639 code, e.g. "vi". */
69
+ lang: string;
70
+ /** English name, e.g. "Vietnamese". */
71
+ name: string;
72
+ /** The language's regions, e.g. ["Northern", "Central", "Southern"], or countries for English. */
73
+ regions: string[];
74
+ /**
75
+ * Named groups of regions that can be searched as one, e.g. English "North America" or "British Isles".
76
+ * Omitted when the language has none.
77
+ */
78
+ regionGroups?: Record<string, string[]>;
79
+ source: {
80
+ name: string;
81
+ url: string;
82
+ retrieved: string;
83
+ lastModified: string | null;
84
+ };
85
+ license: {
86
+ name: string;
87
+ url: string;
88
+ };
89
+ /** Where word frequencies come from, with the credit its license requires. */
90
+ frequencySource?: {
91
+ name: string;
92
+ url: string;
93
+ license: string;
94
+ sources: string;
95
+ };
96
+ counts: {
97
+ entries: number;
98
+ senses: number;
99
+ regionTaggedSenses: number;
100
+ englishTerms: number;
101
+ };
102
+ /** Shard file names (without .json) in words/ and en/. */
103
+ shards: {
104
+ words: string[];
105
+ en: string[];
106
+ };
107
+ /** Letters per shard key; see shardKey. Omitted means 2. */
108
+ shardLength?: number;
109
+ /** true when the language has a pronoun table (pronouns.json, see PronounRow). */
110
+ pronouns?: boolean;
111
+ };
112
+ /**
113
+ * One word in the pronoun table: how to say "I", "you", "he/she", "we", plural "you" or "they" in one
114
+ * relationship. Most come from a dictionary definition that describes the relationship ("you, a male
115
+ * who's (presumably) slightly older than me"); regular compounds the dictionary doesn't list come from a
116
+ * grammar rule ("các" + "anh" = plural "you" to older men); the rest are hand-written overrides for what
117
+ * the definitions don't say (Vietnamese "em" is never defined as "I").
118
+ */
119
+ export type PronounChoice = {
120
+ word: string;
121
+ /** The definition this choice comes from. Omitted for rules and overrides. */
122
+ gloss?: string;
123
+ /**
124
+ * Where the choice comes from: a dictionary definition, a grammar rule (a regular compound, with a
125
+ * `note` naming the rule), or a hand-written override with a `note`.
126
+ */
127
+ source: 'gloss' | 'rule' | 'override';
128
+ regions: string[];
129
+ regionTagged: boolean;
130
+ labels: string[];
131
+ /** Only when the speaker is male or female ("anh" if you're a man, "chị" if a woman). */
132
+ speaker?: 'male' | 'female';
133
+ /** The gender of the person the word refers to, when it has one ("anh ấy" he, "chị ấy" she). */
134
+ gender?: 'male' | 'female';
135
+ /** For "we": true when it includes the listener ("chúng ta"), false when it doesn't ("chúng tôi"). */
136
+ inclusive?: boolean;
137
+ note?: string;
138
+ };
139
+ /** The pronoun table's columns: I, you, he/she, we, you (plural), they. */
140
+ export declare const PRONOUN_PERSONS: readonly ["self", "addressee", "third", "selfPlural", "addresseePlural", "thirdPlural"];
141
+ export type PronounPerson = (typeof PRONOUN_PERSONS)[number];
142
+ /**
143
+ * A relationship and the words for it, e.g. Vietnamese "Your parents": you say "con", you call them
144
+ * "ba"/"má" (Southern) or "bố"/"mẹ". For "he/she" and "they" the relationship is with the person you're
145
+ * talking about.
146
+ */
147
+ export type PronounRow = {
148
+ /** Stable id, e.g. "parent", "older-male". */
149
+ id: string;
150
+ /** Who you're talking to (or about), for display: "Your parents". */
151
+ label: string;
152
+ /** How to say "I". */
153
+ self: PronounChoice[];
154
+ /** How to say "you". */
155
+ addressee: PronounChoice[];
156
+ /** How to say "he"/"she" about this person. */
157
+ third: PronounChoice[];
158
+ /** How to say "we" (you and others, talking to this person). */
159
+ selfPlural: PronounChoice[];
160
+ /** How to say "you" to several people like this. */
161
+ addresseePlural: PronounChoice[];
162
+ /** How to say "they" about several people like this. */
163
+ thirdPlural: PronounChoice[];
164
+ /** The row to use when the relationship isn't known (neutral words: Vietnamese "tôi", "bạn"). */
165
+ default?: boolean;
166
+ /** A caution for the whole row, e.g. that "tao"/"mày" are rude outside close friendships. */
167
+ warning?: string;
168
+ };
169
+ export type StoredPronounChoice = Omit<PronounChoice, 'regions' | 'regionTagged' | 'labels'> & {
170
+ regions?: string[];
171
+ labels?: string[];
172
+ };
173
+ /** Stored rows leave out empty columns. */
174
+ export type StoredPronounRow = Omit<PronounRow, PronounPerson> & Partial<Record<PronounPerson, StoredPronounChoice[]>>;
175
+ export type StoredSense = Omit<Sense, 'regions' | 'regionTagged' | 'labels'> & {
176
+ regions?: string[];
177
+ labels?: string[];
178
+ };
179
+ export type StoredEntry = Omit<Entry, 'senses'> & {
180
+ senses: StoredSense[];
181
+ };
182
+ export type StoredHit = Omit<Hit, 'regions' | 'regionTagged' | 'labels'> & {
183
+ regions?: string[];
184
+ labels?: string[];
185
+ };
186
+ /** words/<shard>.json: entries keyed by headword. */
187
+ export type WordShard = Record<string, StoredEntry[]>;
188
+ /** en/<shard>.json: hits keyed by lowercase English term. */
189
+ export type EnglishShard = Record<string, StoredHit[]>;
190
+ /** Converts a full sense or hit to its stored form. */
191
+ export declare function toStored<T extends {
192
+ regions: string[];
193
+ regionTagged: boolean;
194
+ labels: string[];
195
+ }>(item: T): Omit<T, "regions" | "regionTagged" | "labels"> & {
196
+ labels?: string[] | undefined;
197
+ regions?: string[] | undefined;
198
+ };
199
+ /** Converts a stored sense or hit back to its full form, given the language's regions. */
200
+ export declare function fromStored<T extends {
201
+ regions?: string[];
202
+ labels?: string[];
203
+ }>(item: T, allRegions: string[]): T & {
204
+ regions: string[];
205
+ regionTagged: boolean;
206
+ labels: string[];
207
+ };
208
+ /**
209
+ * Which shard file a term lives in: its first `length` letters (two by default) without diacritics (đ counts as d, ñ as n),
210
+ * so a lookup only downloads a small file. Characters outside a-z become "_", e.g. "ở" -> "o_", "3D" -> "_d".
211
+ */
212
+ export declare function shardKey(term: string, length?: number): string;
213
+ /** Normalizes an English search term the same way the index keys were built. */
214
+ export declare function normalizeEnglish(term: string): string;
215
+ /**
216
+ * The most useful line of a sense's definitions for display. Wiktionary nests some senses under a
217
+ * heading ("As a copulative verb:" → "Used to indicate that the subject and object are the same."), so
218
+ * headings ending in ":" are skipped in favor of the specific definition.
219
+ */
220
+ export declare function displayGloss(glosses: string[]): string;
221
+ /**
222
+ * Splits an English gloss into search terms, main meaning first: "now, today, this time" ->
223
+ * [["now", 0], ["today", 1], ["this time", 2]]. Parenthesized notes and final punctuation are dropped.
224
+ * When a gloss explains before a colon ("Negates the meaning of the modified verb: not"), only the
225
+ * part after it is used. Only short, plain phrases (up to 4 words) are kept. Returns [term, position].
226
+ */
227
+ export declare function glossTerms(gloss: string): [string, number][];
package/dist/types.js ADDED
@@ -0,0 +1,96 @@
1
+ // Shapes of the data. The build script (scripts/build-language.ts) writes the Stored* types to the
2
+ // data files; the API in index.ts reads them and returns the full Sense/Entry/Hit types.
3
+ /** The pronoun table's columns: I, you, he/she, we, you (plural), they. */
4
+ export const PRONOUN_PERSONS = ['self', 'addressee', 'third', 'selfPlural', 'addresseePlural', 'thirdPlural'];
5
+ /** Converts a full sense or hit to its stored form. */
6
+ export function toStored(item) {
7
+ const { regions, regionTagged, labels, ...rest } = item;
8
+ return {
9
+ ...rest,
10
+ ...(regionTagged ? { regions } : {}),
11
+ ...(labels.length ? { labels } : {}),
12
+ };
13
+ }
14
+ /** Converts a stored sense or hit back to its full form, given the language's regions. */
15
+ export function fromStored(item, allRegions) {
16
+ return {
17
+ ...item,
18
+ regions: item.regions ?? allRegions,
19
+ regionTagged: item.regions !== undefined,
20
+ labels: item.labels ?? [],
21
+ };
22
+ }
23
+ /**
24
+ * Which shard file a term lives in: its first `length` letters (two by default) without diacritics (đ counts as d, ñ as n),
25
+ * so a lookup only downloads a small file. Characters outside a-z become "_", e.g. "ở" -> "o_", "3D" -> "_d".
26
+ */
27
+ export function shardKey(term, length = 2) {
28
+ const base = term
29
+ .trim()
30
+ .toLowerCase()
31
+ .normalize('NFD')
32
+ .replace(/[\u0300-\u036f]/g, '')
33
+ .replace(/đ/g, 'd');
34
+ const letter = (c) => (c >= 'a' && c <= 'z' ? c : '_');
35
+ let key = '';
36
+ for (let i = 0; i < length; i++)
37
+ key += letter(base.charAt(i));
38
+ return key;
39
+ }
40
+ /** Normalizes an English search term the same way the index keys were built. */
41
+ export function normalizeEnglish(term) {
42
+ return term
43
+ .toLowerCase()
44
+ .replace(/[’‘]/g, "'")
45
+ .replace(/\s+/g, ' ')
46
+ .trim()
47
+ .replace(/^(to|a|an|the) (?=\S)/, '');
48
+ }
49
+ /**
50
+ * The most useful line of a sense's definitions for display. Wiktionary nests some senses under a
51
+ * heading ("As a copulative verb:" → "Used to indicate that the subject and object are the same."), so
52
+ * headings ending in ":" are skipped in favor of the specific definition.
53
+ */
54
+ export function displayGloss(glosses) {
55
+ return glosses.find((g) => !g.trim().endsWith(':')) ?? glosses[0] ?? '';
56
+ }
57
+ // Trailing words dropped to also match the bare verb: "wait for" is found by "wait" too.
58
+ const TRAILING_PARTICLE = / (for|to|at|on|with|about|of|in|into|up|out|off|over)$/;
59
+ /**
60
+ * Splits an English gloss into search terms, main meaning first: "now, today, this time" ->
61
+ * [["now", 0], ["today", 1], ["this time", 2]]. Parenthesized notes and final punctuation are dropped.
62
+ * When a gloss explains before a colon ("Negates the meaning of the modified verb: not"), only the
63
+ * part after it is used. Only short, plain phrases (up to 4 words) are kept. Returns [term, position].
64
+ */
65
+ export function glossTerms(gloss) {
66
+ // Grammar words are defined by what they do: "marks the future tense" (Vietnamese "sẽ"), "Used to
67
+ // express the future tense" (English "will"). The phrase after marks/expresses/indicates/denotes is
68
+ // their meaning, so it's a term too, placed after the gloss's own terms.
69
+ const grammar = [...gloss.matchAll(GRAMMAR_PHRASE)].map((m) => normalizeEnglish(m[1])).filter((t) => t.split(' ').length <= 3);
70
+ let cleaned = gloss.replace(/\([^)]*\)/g, ' ').replace(/[“”"]/g, '');
71
+ if (cleaned.includes(':'))
72
+ cleaned = cleaned.slice(cleaned.lastIndexOf(':') + 1);
73
+ const terms = [];
74
+ let position = 0;
75
+ // "I/me" lists two meanings, like "I; me".
76
+ for (const part of cleaned.split(/[;,/]/)) {
77
+ // "etc." is not a meaning: "walking etc" -> "walking", and a lone "etc" is skipped.
78
+ const t = normalizeEnglish(part.replace(/[.!?]+\s*$/, '').replace(/\betc\.?$/i, '').trim());
79
+ if (!t || t.split(' ').length > 4 || !/^[a-z][a-z' -]*$/.test(t))
80
+ continue;
81
+ terms.push([t, position]);
82
+ // The bare verb shares its phrase's position, so "wait" counts as the main meaning of "to wait for".
83
+ const bare = t.replace(TRAILING_PARTICLE, '');
84
+ if (bare !== t && bare)
85
+ terms.push([bare, position]);
86
+ position++;
87
+ }
88
+ // A grammar phrase is the main meaning when the gloss's first term is the grammar description itself
89
+ // ("marks the future tense" → "future tense" is what "sẽ" means); otherwise it follows the other terms.
90
+ const describesGrammar = terms.length === 0 || grammar.some((g) => terms[0][0].includes(g));
91
+ for (const t of grammar)
92
+ if (!terms.some(([x]) => x === t))
93
+ terms.push([t, describesGrammar ? 0 : position++]);
94
+ return terms;
95
+ }
96
+ const GRAMMAR_PHRASE = /\b(?:marks?|marking|express(?:es|ing)?|indicat(?:es?|ing)|denot(?:es?|ing)|signals?)\s+(?:the\s+|a\s+|an\s+)?([a-z][a-z -]{2,30}?)(?=\s*(?:[,;.(]|$|\s+(?:of|in|with|for|or|and|when|that)\b))/gi;
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "which-dialect",
3
+ "version": "0.1.0",
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
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "sideEffects": false,
18
+ "scripts": {
19
+ "build": "tsc -p tsconfig.json",
20
+ "prepublishOnly": "npm run build"
21
+ },
22
+ "keywords": [
23
+ "dictionary",
24
+ "dialect",
25
+ "regional",
26
+ "translation",
27
+ "language-learning",
28
+ "wiktionary",
29
+ "vietnamese"
30
+ ],
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/nguyen18/which-dialect.git",
35
+ "directory": "packages/core"
36
+ },
37
+ "homepage": "https://github.com/nguyen18/which-dialect#readme"
38
+ }