marathi-transliterate 0.0.0-stage → 1.1.1

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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ketan Katore
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.
package/README.md CHANGED
@@ -1,3 +1,174 @@
1
- # Temporary Holding Version
1
+ # marathi-transliterate
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > **Zero-dependency, offline-first Plain JavaScript phonetic transliteration library for Marathi (Devanagari script).**
4
+ > Converts colloquial English typing to authentic Marathi (e.g., `sakhar` → `साखर`, `kanda` → `कांदा`, `batata` → `बटाटा`) with single-step backspace undo and interactive input attachment.
5
+
6
+ [![npm version](https://img.shields.io/npm/v/marathi-transliterate.svg)](https://www.npmjs.com/package/marathi-transliterate)
7
+ [![license](https://img.shields.io/npm/l/marathi-transliterate.svg)](https://github.com/Ketan-K/marathi-transliterate/blob/main/LICENSE)
8
+
9
+ ---
10
+
11
+ ## Why marathi-transliterate?
12
+
13
+ Most existing Sanskrit/IAST transliteration libraries convert `sakhar` to **सखर** or `batata` to **बतत** because they require scholarly transliteration (`saakhar`, `bataataa`).
14
+
15
+ In everyday reality, people in Maharashtra type phonetically:
16
+ - `sakhar` → **साखर**
17
+ - `kanda` → **कांदा**
18
+ - `batata` → **बटाटा**
19
+ - `mith` → **मीठ**
20
+ - `tel` → **तेल**
21
+ - `gahu` → **गहू**
22
+ - `tandul` → **तांदूळ**
23
+ - `pohe` → **पोहे**
24
+ - `rava` → **रवा**
25
+ - `besan` → **बेसन**
26
+ - `toor dal` → **तूर डाळ**
27
+ - `chaha` → **चहा**
28
+ - `dnyaneshwar` → **ज्ञानेश्वर**
29
+ - `patil` → **पाटील**
30
+
31
+ `marathi-transliterate` is built specifically for modern Marathi phonetic typing:
32
+ - **Zero dependencies**: Pure JavaScript, ~15 KB.
33
+ - **Offline first**: Works 100% offline without requiring internet or API calls.
34
+ - **Framework agnostic**: Works in Plain HTML/JS, Node.js, React, Vue, Svelte, Angular, Electron.
35
+ - **Interactive DOM integration**: Attach directly to any `<input>` or `<textarea>` with Space-to-convert, Backspace-to-undo, and `Ctrl+M` toggle.
36
+
37
+ ---
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ npm install marathi-transliterate
43
+ ```
44
+
45
+ ---
46
+
47
+ ## Quick Start
48
+
49
+ ### 1. Plain JavaScript / Node.js
50
+
51
+ ```javascript
52
+ import { transliterate, transliterateWord } from 'marathi-transliterate';
53
+
54
+ // Single word transliteration
55
+ transliterateWord('sakhar'); // → 'साखर'
56
+ transliterateWord('kanda'); // → 'कांदा'
57
+ transliterateWord('batata'); // → 'बटाटा'
58
+ transliterateWord('namaskar'); // → 'नमस्कार'
59
+
60
+ // Full sentence transliteration (numbers and symbols preserved)
61
+ transliterate('sakhar 1 kilo ani kanda 2 kg');
62
+ // → 'साखर 1 किलो आणि कांदा 2 कि.ग्रॅ.'
63
+ ```
64
+
65
+ ---
66
+
67
+ ### 2. Attach to any HTML `<input>` or `<textarea>`
68
+
69
+ Easily enable Marathi phonetic typing on any input element:
70
+
71
+ ```html
72
+ <input type="text" id="product-name" placeholder="उदा. sakhar टाइप करा..." />
73
+
74
+ <script type="module">
75
+ import { attachDevanagari } from 'marathi-transliterate';
76
+
77
+ const inputEl = document.getElementById('product-name');
78
+
79
+ // Attach Devanagari typing
80
+ const handler = attachDevanagari(inputEl, {
81
+ defaultEnabled: true,
82
+ enableShortcut: true, // Ctrl+M or Cmd+M toggles Marathi/English
83
+ onToggle: (enabled) => console.log('Marathi typing:', enabled)
84
+ });
85
+
86
+ // To clean up listeners when unmounting:
87
+ // handler.detach();
88
+ </script>
89
+ ```
90
+
91
+ #### Interactive Smart Suggesting:
92
+ As you type (e.g. `sakhar`), a floating suggestion popup appears with ranked candidates:
93
+ - **`Space` or `Enter`**: Automatically selects the top recommended candidate (`साखर`).
94
+ - **`1`, `2`, `3`, `4`, `5`**: Press any digit key to select that candidate immediately.
95
+ - **`Arrow Down / Up`**: Navigate through candidate suggestions.
96
+ - **`Esc`**: Dismisses the suggestion popup without converting.
97
+ - **`Backspace`**: Undoes the transliteration back to English.
98
+ - **`Ctrl+M` (or `Cmd+M`)**: Toggles Marathi and English typing mode on the fly.
99
+
100
+ ---
101
+
102
+ ### 3. Usage in React
103
+
104
+ Use a standard React `ref` or `useEffect`:
105
+
106
+ ```jsx
107
+ import React, { useRef, useEffect, useState } from 'react';
108
+ import { attachDevanagari } from 'marathi-transliterate';
109
+
110
+ export function MarathiInput() {
111
+ const inputRef = useRef(null);
112
+ const [isMarathi, setIsMarathi] = useState(true);
113
+
114
+ useEffect(() => {
115
+ if (!inputRef.current) return;
116
+
117
+ const handler = attachDevanagari(inputRef.current, {
118
+ defaultEnabled: true,
119
+ onToggle: (enabled) => setIsMarathi(enabled)
120
+ });
121
+
122
+ return () => handler.detach();
123
+ }, []);
124
+
125
+ return (
126
+ <div style={{ position: 'relative' }}>
127
+ <input ref={inputRef} placeholder="वस्तूचे नाव टाइप करा..." />
128
+ <span style={{ fontSize: '12px', color: isMarathi ? 'green' : 'gray' }}>
129
+ {isMarathi ? 'मराठी (Ctrl+M)' : 'English (Ctrl+M)'}
130
+ </span>
131
+ </div>
132
+ );
133
+ }
134
+ ```
135
+
136
+ ---
137
+
138
+ ## API Reference
139
+
140
+ ### `transliterateWord(word: string): string`
141
+ Converts a single English word into Marathi Devanagari script.
142
+
143
+ ### `transliterate(text: string): string`
144
+ Converts full text, sentences, or paragraphs into Marathi while preserving numbers, whitespace, and punctuation.
145
+
146
+ ### `getSuggestions(word: string): string[]`
147
+ Returns candidate phonetic variations for a given word.
148
+
149
+ ### `attachDevanagari(element, options): Handler`
150
+ Attaches keyboard listeners to an `<input>` or `<textarea>` element.
151
+
152
+ | Option | Type | Default | Description |
153
+ |---|---|---|---|
154
+ | `defaultEnabled` | `boolean` | `true` | Whether Marathi typing starts enabled |
155
+ | `enableShortcut` | `boolean` | `true` | Enables `Ctrl+M` / `Cmd+M` toggle |
156
+ | `triggerKeys` | `string[]` | `[' ', 'Enter', ',', '.']` | Keys that trigger transliteration |
157
+ | `onToggle` | `Function` | `undefined` | Callback `(isEnabled) => void` |
158
+ | `onTransliterate` | `Function` | `undefined` | Callback `({ original, transliterated }) => void` |
159
+
160
+ **Returns:**
161
+ ```typescript
162
+ {
163
+ detach: () => void;
164
+ setEnabled: (val: boolean) => void;
165
+ isEnabled: () => boolean;
166
+ toggle: () => boolean;
167
+ }
168
+ ```
169
+
170
+ ---
171
+
172
+ ## License
173
+
174
+ MIT © Ketan Katore
package/package.json CHANGED
@@ -1,6 +1,42 @@
1
1
  {
2
2
  "name": "marathi-transliterate",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.1.1",
4
+ "description": "Zero-dependency, offline-first Marathi / Devanagari phonetic transliteration library. Converts English typing to Marathi (e.g. sakhar -> साखर) seamlessly in plain JS, Node.js, and frontend frameworks.",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "module": "src/index.js",
8
+ "exports": {
9
+ ".": "./src/index.js",
10
+ "./dom": "./src/dom.js",
11
+ "./dictionary": "./src/dictionary.js"
12
+ },
13
+ "scripts": {
14
+ "test": "node tests/transliterate.test.js"
15
+ },
16
+ "keywords": [
17
+ "marathi",
18
+ "transliterate",
19
+ "transliteration",
20
+ "devanagari",
21
+ "indic",
22
+ "marathi-typing",
23
+ "input-tools",
24
+ "ime",
25
+ "offline"
26
+ ],
27
+ "author": "Ketan Katore",
28
+ "license": "MIT",
29
+ "files": [
30
+ "src",
31
+ "README.md",
32
+ "LICENSE"
33
+ ],
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/Ketan-K/marathi-transliterate.git"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/Ketan-K/marathi-transliterate/issues"
40
+ },
41
+ "homepage": "https://github.com/Ketan-K/marathi-transliterate#readme"
42
+ }
package/src/cache.js ADDED
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Cache and asynchronous online enrichment for Marathi transliteration.
3
+ */
4
+
5
+ const MEMORY_CACHE = new Map();
6
+ const LOCAL_STORAGE_KEY = 'marathi_transliterate_cache_v1';
7
+
8
+ // Load initial persistent cache if available
9
+ function initStorage() {
10
+ if (typeof window !== 'undefined' && window.localStorage) {
11
+ try {
12
+ const stored = localStorage.getItem(LOCAL_STORAGE_KEY);
13
+ if (stored) {
14
+ const parsed = JSON.parse(stored);
15
+ for (const [k, v] of Object.entries(parsed)) {
16
+ MEMORY_CACHE.set(k.toLowerCase(), v);
17
+ }
18
+ }
19
+ } catch {
20
+ // Ignore storage errors
21
+ }
22
+ }
23
+ }
24
+
25
+ initStorage();
26
+
27
+ export function getCachedWord(word) {
28
+ if (!word) return null;
29
+ return MEMORY_CACHE.get(word.toLowerCase().trim()) || null;
30
+ }
31
+
32
+ export function setCachedWord(word, transliteratedList) {
33
+ if (!word || !transliteratedList) return;
34
+ const key = word.toLowerCase().trim();
35
+ const list = Array.isArray(transliteratedList) ? transliteratedList : [transliteratedList];
36
+ MEMORY_CACHE.set(key, list);
37
+
38
+ if (typeof window !== 'undefined' && window.localStorage) {
39
+ try {
40
+ const obj = {};
41
+ MEMORY_CACHE.forEach((v, k) => {
42
+ obj[k] = v;
43
+ });
44
+ localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(obj));
45
+ } catch {
46
+ // Ignore quota/storage errors
47
+ }
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Optionally fetch online candidates from Google Input Tools API (mr-t-i0-und)
53
+ * and cache the results for offline use.
54
+ * @param {string} word
55
+ * @param {number} numOptions
56
+ * @returns {Promise<string[]>}
57
+ */
58
+ export async function fetchOnlineMarathi(word, numOptions = 5) {
59
+ if (!word || !word.trim()) return [];
60
+ const clean = word.toLowerCase().trim();
61
+
62
+ // If already in cache
63
+ const cached = getCachedWord(clean);
64
+ if (cached && cached.length > 0) {
65
+ return cached;
66
+ }
67
+
68
+ // If in browser/electron/node with fetch
69
+ if (typeof fetch !== 'undefined') {
70
+ try {
71
+ const url = `https://inputtools.google.com/request?text=${encodeURIComponent(clean)}&itc=mr-t-i0-und&num=${numOptions}`;
72
+ const res = await fetch(url);
73
+ if (res.ok) {
74
+ const data = await res.json();
75
+ if (data && data[0] === 'SUCCESS' && data[1] && data[1][0] && data[1][0][1]) {
76
+ const suggestions = data[1][0][1];
77
+ setCachedWord(clean, suggestions);
78
+ return suggestions;
79
+ }
80
+ }
81
+ } catch {
82
+ // Network failure / offline: fail gracefully
83
+ }
84
+ }
85
+
86
+ return [];
87
+ }
@@ -0,0 +1,384 @@
1
+ /**
2
+ * Curated high-accuracy Marathi vocabulary for everyday usage,
3
+ * Kirana/grocery items, customer names, units, and shop terms.
4
+ */
5
+ export const MARATHI_DICTIONARY = {
6
+ // --- Grocery & Kirana Staples ---
7
+ 'sakhar': 'साखर',
8
+ 'saakhar': 'साखर',
9
+ 'sugar': 'साखर',
10
+ 'kanda': 'कांदा',
11
+ 'kaanda': 'कांदा',
12
+ 'onion': 'कांदा',
13
+ 'batata': 'बटाटा',
14
+ 'batate': 'बटाटे',
15
+ 'potato': 'बटाटा',
16
+ 'mith': 'मीठ',
17
+ 'meeth': 'मीठ',
18
+ 'namak': 'मीठ',
19
+ 'salt': 'मीठ',
20
+ 'tel': 'तेल',
21
+ 'godetel': 'गोडेतेल',
22
+ 'gahu': 'गहू',
23
+ 'gehu': 'गहू',
24
+ 'wheat': 'गहू',
25
+ 'tandul': 'तांदूळ',
26
+ 'taandul': 'तांदूळ',
27
+ 'bhat': 'भात',
28
+ 'rice': 'तांदूळ',
29
+ 'kolam': 'कोलम',
30
+ 'basmati': 'बासमती',
31
+ 'jowar': 'ज्वारी',
32
+ 'jwari': 'ज्वारी',
33
+ 'bajra': 'बाजरी',
34
+ 'bajri': 'बाजरी',
35
+ 'nachani': 'नाचणी',
36
+ 'ragi': 'नाचणी',
37
+ 'makka': 'मक्का',
38
+ 'pohe': 'पोहे',
39
+ 'poha': 'पोहे',
40
+ 'rava': 'रवा',
41
+ 'suji': 'सुजी',
42
+ 'sooji': 'सुजी',
43
+ 'maida': 'मैदा',
44
+ 'besan': 'बेसन',
45
+ 'pith': 'पीठ',
46
+ 'atta': 'आटा',
47
+ 'chakki': 'चक्की',
48
+ 'chaha': 'चहा',
49
+ 'chai': 'चहा',
50
+ 'tea': 'चहा',
51
+ 'coffee': 'कॉफी',
52
+ 'toor': 'तूर',
53
+ 'tur': 'तूर',
54
+ 'toordal': 'तूर डाळ',
55
+ 'moong': 'मूग',
56
+ 'mung': 'मूग',
57
+ 'udid': 'उडीद',
58
+ 'urad': 'उडीद',
59
+ 'chana': 'चना',
60
+ 'harbhara': 'हरभरा',
61
+ 'masoor': 'मसूर',
62
+ 'masur': 'मसूर',
63
+ 'matki': 'मटकी',
64
+ 'watana': 'वाटाणा',
65
+ 'vatana': 'वाटाणा',
66
+ 'chole': 'छोले',
67
+ 'rajma': 'राजमा',
68
+ 'chavli': 'चवळी',
69
+ 'kulith': 'कुळीथ',
70
+ 'shengdane': 'शेंगदाणे',
71
+ 'dane': 'दाणे',
72
+ 'sheng': 'शेंगदाणे',
73
+ 'khobare': 'खोबरे',
74
+ 'khobra': 'खोबरे',
75
+ 'coconut': 'खोबरे',
76
+ 'toop': 'तूप',
77
+ 'tup': 'तूप',
78
+ 'ghee': 'तूप',
79
+ 'gul': 'गूळ',
80
+ 'gud': 'गूळ',
81
+ 'jaggery': 'गूळ',
82
+ 'kakvi': 'काकवी',
83
+ 'honey': 'मध',
84
+ 'madh': 'मध',
85
+
86
+ // --- Spices & Masala ---
87
+ 'masala': 'मसाला',
88
+ 'mirchi': 'मिरची',
89
+ 'mirch': 'मिरची',
90
+ 'halad': 'हळद',
91
+ 'haldi': 'हळद',
92
+ 'lasun': 'लसूण',
93
+ 'lahsun': 'लसूण',
94
+ 'garlic': 'लसूण',
95
+ 'aale': 'आले',
96
+ 'adrak': 'आले',
97
+ 'ginger': 'आले',
98
+ 'kothimbir': 'कोथिंबीर',
99
+ 'dhana': 'धना',
100
+ 'dhane': 'धने',
101
+ 'dhania': 'धने',
102
+ 'jira': 'जिरे',
103
+ 'jeera': 'जिरे',
104
+ 'jire': 'जिरे',
105
+ 'mohari': 'मोहरी',
106
+ 'rai': 'राई',
107
+ 'mustard': 'मोहरी',
108
+ 'hing': 'हिंग',
109
+ 'dalchini': 'दालचिनी',
110
+ 'lavang': 'लवंग',
111
+ 'miri': 'मिरी',
112
+ 'tamalpatra': 'तमालपत्र',
113
+ 'tejpatta': 'तेजपत्ता',
114
+ 'jayfal': 'जायफळ',
115
+ 'elaichi': 'वेलची',
116
+ 'velchi': 'वेलची',
117
+ 'kesar': 'केसर',
118
+ 'til': 'तीळ',
119
+ 'khaskhas': 'खसखस',
120
+ 'methi': 'मेथी',
121
+ 'badishep': 'बडीशेप',
122
+ 'saunf': 'बडीशेप',
123
+ 'chinch': 'चिंच',
124
+ 'kokam': 'कोकम',
125
+ 'amchur': 'आमचूर',
126
+
127
+ // --- Dry Fruits ---
128
+ 'kaju': 'काजू',
129
+ 'cashew': 'काजू',
130
+ 'badam': 'बदाम',
131
+ 'almond': 'बदाम',
132
+ 'pista': 'पिस्ता',
133
+ 'manuka': 'मनुका',
134
+ 'kismis': 'किसमिस',
135
+ 'kharik': 'खारीक',
136
+ 'khajur': 'खजूर',
137
+ 'akhrot': 'अक्रोड',
138
+ 'makhana': 'मखाना',
139
+ 'charoli': 'चारोळी',
140
+ 'anjeer': 'अंजीर',
141
+
142
+ // --- Dairy & Cold Storage ---
143
+ 'dudh': 'दूध',
144
+ 'milk': 'दूध',
145
+ 'dahi': 'दही',
146
+ 'curd': 'दही',
147
+ 'taak': 'ताक',
148
+ 'paneer': 'पनीर',
149
+ 'loni': 'लोणी',
150
+ 'maska': 'मस्का',
151
+ 'butter': 'बटर',
152
+ 'cheese': 'चीज',
153
+ 'shrikhand': 'श्रीखंड',
154
+ 'amrakhand': 'आम्रखंड',
155
+
156
+ // --- Bakery, Snacks & Packaged Foods ---
157
+ 'biscuit': 'बिस्कीट',
158
+ 'biscuits': 'बिस्कीट',
159
+ 'parle': 'पारले',
160
+ 'toast': 'टोस्ट',
161
+ 'khari': 'खारी',
162
+ 'shev': 'शेव',
163
+ 'bhujia': 'भुजिया',
164
+ 'farsan': 'फरसाण',
165
+ 'chivda': 'चिवडा',
166
+ 'papad': 'पापड',
167
+ 'lonche': 'लोणचे',
168
+ 'achar': 'लोणचे',
169
+ 'ketchup': 'केचप',
170
+ 'sauce': 'सॉस',
171
+ 'maggi': 'मॅगी',
172
+ 'noodles': 'नुडल्स',
173
+ 'sevai': 'शेवया',
174
+ 'pasta': 'पास्ता',
175
+ 'kurdaya': 'कुरडया',
176
+ 'kurdai': 'कुरडई',
177
+ 'wafers': 'वेफर्स',
178
+ 'chips': 'चिप्स',
179
+
180
+ // --- Pooja & Religious ---
181
+ 'agarbatti': 'अगरबत्ती',
182
+ 'incense': 'अगरबत्ती',
183
+ 'dhoop': 'धूप',
184
+ 'kapoor': 'कापूर',
185
+ 'karpur': 'कापूर',
186
+ 'supari': 'सुपारी',
187
+ 'gulal': 'गुलाल',
188
+ 'kumkum': 'कुंकू',
189
+ 'shendur': 'शेंदूर',
190
+ 'bukka': 'बुक्का',
191
+ 'abir': 'अबीर',
192
+ 'ashtagandha': 'अष्टगंध',
193
+ 'gandh': 'गंध',
194
+ 'attar': 'अत्तर',
195
+ 'janve': 'जानवे',
196
+ 'phulvat': 'फुलवात',
197
+ 'samai': 'समई',
198
+
199
+ // --- Cleaning & Household ---
200
+ 'saban': 'साबण',
201
+ 'soap': 'साबण',
202
+ 'shampoo': 'शॅम्पू',
203
+ 'detergent': 'डिटर्जंट',
204
+ 'colgate': 'कोलगेट',
205
+ 'toothpaste': 'टूथपेस्ट',
206
+ 'surf': 'सर्फ',
207
+ 'rin': 'रिन',
208
+ 'vim': 'विम',
209
+ 'wheel': 'व्हील',
210
+ 'ariel': 'एरियल',
211
+ 'tide': 'टाइड',
212
+ 'harpic': 'हार्पिक',
213
+ 'lizol': 'लायझॉल',
214
+ 'phenyl': 'फिनाईल',
215
+ 'jhadu': 'झाडू',
216
+ 'brush': 'ब्रश',
217
+
218
+ // --- Units of Measure ---
219
+ 'kilo': 'किलो',
220
+ 'kg': 'कि.ग्रॅ.',
221
+ 'gram': 'ग्रॅम',
222
+ 'gm': 'ग्रॅम',
223
+ 'liter': 'लिटर',
224
+ 'litre': 'लिटर',
225
+ 'ltr': 'लिटर',
226
+ 'mili': 'मिली',
227
+ 'ml': 'मिली',
228
+ 'packet': 'पॅकेट',
229
+ 'pkt': 'पॅकेट',
230
+ 'nag': 'नग',
231
+ 'piece': 'नग',
232
+ 'dozen': 'डझन',
233
+ 'box': 'बॉक्स',
234
+ 'poti': 'पोती',
235
+ 'bori': 'बोरी',
236
+ 'bag': 'बॅग',
237
+
238
+ // --- Customer Names & Surnames ---
239
+ 'ketan': 'केतन',
240
+ 'rahul': 'राहुल',
241
+ 'amit': 'अमित',
242
+ 'sachin': 'सचिन',
243
+ 'suresh': 'सुरेश',
244
+ 'ramesh': 'रमेश',
245
+ 'mahesh': 'महेश',
246
+ 'ganesh': 'गणेश',
247
+ 'santosh': 'संतोष',
248
+ 'vijay': 'विजय',
249
+ 'ajay': 'अजय',
250
+ 'sandeep': 'संदीप',
251
+ 'pradip': 'प्रदीप',
252
+ 'pravin': 'प्रवीण',
253
+ 'nitin': 'नितीन',
254
+ 'anil': 'अनिल',
255
+ 'sunil': 'सुनील',
256
+ 'vikas': 'विकास',
257
+ 'vishal': 'विशाल',
258
+ 'amol': 'अमोल',
259
+ 'swapnil': 'स्वप्निल',
260
+ 'rohit': 'रोहित',
261
+ 'akshay': 'अक्षय',
262
+ 'chetan': 'चेतन',
263
+ 'deepak': 'दीपक',
264
+ 'sagar': 'सागर',
265
+ 'datta': 'दत्त',
266
+ 'dnyaneshwar': 'ज्ञानेश्वर',
267
+ 'shivaji': 'शिवाजी',
268
+ 'sambhaji': 'संभाजी',
269
+ 'tukaram': 'तुकाराम',
270
+ 'eknath': 'एकनाथ',
271
+ 'namdev': 'नामदेव',
272
+ 'ram': 'राम',
273
+ 'krishna': 'कृष्ण',
274
+ 'laxman': 'लक्ष्मण',
275
+ 'bharat': 'भरत',
276
+ 'shankar': 'शंकर',
277
+ 'mahadev': 'महादेव',
278
+ 'hanumant': 'हनुमंत',
279
+ 'maruti': 'मारुती',
280
+ 'priya': 'प्रिया',
281
+ 'pooja': 'पूजा',
282
+ 'sneha': 'स्नेहा',
283
+ 'neha': 'नेहा',
284
+ 'anita': 'अनिता',
285
+ 'sunita': 'सुनिता',
286
+ 'rekha': 'रेखा',
287
+ 'kavita': 'कविता',
288
+ 'savita': 'सविता',
289
+ 'sangita': 'संगीता',
290
+ 'manisha': 'मनीषा',
291
+ 'swati': 'स्वाती',
292
+ 'shital': 'शीतल',
293
+ 'jyoti': 'ज्योती',
294
+
295
+ // Surnames
296
+ 'patil': 'पाटील',
297
+ 'deshmukh': 'देशमुख',
298
+ 'pawar': 'पवार',
299
+ 'jadhav': 'जाधव',
300
+ 'shinde': 'शिंदे',
301
+ 'kadam': 'कदम',
302
+ 'chavan': 'चव्हाण',
303
+ 'chavhan': 'चव्हाण',
304
+ 'more': 'मोरे',
305
+ 'gaikwad': 'गायकवाड',
306
+ 'kale': 'काळे',
307
+ 'bhosale': 'भोसले',
308
+ 'jagtap': 'जगतप',
309
+ 'sawant': 'सावंत',
310
+ 'sharma': 'शर्मा',
311
+ 'varma': 'वर्मा',
312
+ 'gupta': 'गुप्ता',
313
+ 'jain': 'जैन',
314
+ 'koli': 'कोळी',
315
+ 'mali': 'माळी',
316
+ 'wagh': 'वाघ',
317
+ 'gore': 'गोरे',
318
+ 'kharat': 'खरात',
319
+ 'gharge': 'घारगे',
320
+ 'joshi': 'जोशी',
321
+ 'kulkarni': 'कुलकर्णी',
322
+ 'salunkhe': 'साळुंखे',
323
+ 'raut': 'राऊत',
324
+ 'thorat': 'थोरात',
325
+
326
+ // --- Common Shop, Business & Billing Phrases ---
327
+ 'namaskar': 'नमस्कार',
328
+ 'namaste': 'नमस्ते',
329
+ 'swagat': 'स्वागत',
330
+ 'dhanyavad': 'धन्यवाद',
331
+ 'abhar': 'आभार',
332
+ 'shree': 'श्री',
333
+ 'shri': 'श्री',
334
+ 'om': 'ॐ',
335
+ 'jai': 'जय',
336
+ 'gurudev': 'गुरुदेव',
337
+ 'kirana': 'किराणा',
338
+ 'dukan': 'दुकान',
339
+ 'store': 'स्टोअर',
340
+ 'market': 'मार्केट',
341
+ 'supermarket': 'सुपर मार्केट',
342
+ 'general': 'जनरल',
343
+ 'billing': 'बिलिंग',
344
+ 'bill': 'बिल',
345
+ 'grahak': 'ग्राहक',
346
+ 'customer': 'ग्राहक',
347
+ 'rokh': 'रोख',
348
+ 'cash': 'रोख',
349
+ 'udhari': 'उधारी',
350
+ 'jama': 'जमा',
351
+ 'baki': 'बाकी',
352
+ 'sut': 'सूट',
353
+ 'discount': 'सूट',
354
+ 'ekun': 'एकूण',
355
+ 'total': 'एकूण',
356
+ 'payment': 'पेमेंट',
357
+ 'online': 'ऑनलाइन',
358
+ 'upi': 'युपीआय',
359
+
360
+ // --- Common Connecting Words & Pronouns ---
361
+ 'ani': 'आणि',
362
+ 'aani': 'आणि',
363
+ 'and': 'आणि',
364
+ 'pan': 'पण',
365
+ 'madhe': 'मध्ये',
366
+ 'var': 'वर',
367
+ 'khali': 'खाली',
368
+ 'dya': 'द्या',
369
+ 'ghya': 'घ्या',
370
+ 'pahije': 'पाहिजे',
371
+ 'ahe': 'आहे',
372
+ 'nahi': 'नाही',
373
+ 'kiti': 'किती',
374
+ 'kay': 'काय',
375
+ 'kuthe': 'कुठे',
376
+ 'kasa': 'कसा',
377
+ 'kashi': 'कशी',
378
+ 'kashe': 'कसे',
379
+ 'he': 'हे',
380
+ 'te': 'ते',
381
+ 'ha': 'हा',
382
+ 'hi': 'ही'
383
+ };
384
+
package/src/dom.js ADDED
@@ -0,0 +1,274 @@
1
+ import { transliterateWord, getSuggestions } from './index.js';
2
+
3
+ export function attachDevanagari(target, options = {}) {
4
+ const el = typeof target === 'string' ? document.querySelector(target) : target;
5
+ if (!el) {
6
+ console.warn('[marathi-transliterate] attachDevanagari: element not found', target);
7
+ return { detach: () => {}, setEnabled: () => {}, isEnabled: () => false, toggle: () => false };
8
+ }
9
+
10
+ const {
11
+ defaultEnabled = true,
12
+ showSuggestions = true,
13
+ enableShortcut = true,
14
+ maxSuggestions = 5,
15
+ onToggle,
16
+ onTransliterate
17
+ } = options;
18
+
19
+ let enabled = defaultEnabled;
20
+ let lastConversion = null;
21
+ let activeWord = '';
22
+ let activeWordStart = -1;
23
+ let candidates = [];
24
+ let selectedIndex = 0;
25
+ let popupEl = null;
26
+
27
+ function createPopup() {
28
+ if (typeof document === 'undefined') return;
29
+ popupEl = document.createElement('div');
30
+ popupEl.className = 'devanagari-suggestion-popup';
31
+ Object.assign(popupEl.style, {
32
+ position: 'fixed',
33
+ zIndex: '999999',
34
+ display: 'none',
35
+ flexDirection: 'column',
36
+ background: 'rgba(23, 23, 23, 0.95)',
37
+ color: '#f8fafc',
38
+ backdropFilter: 'blur(12px)',
39
+ WebkitBackdropFilter: 'blur(12px)',
40
+ border: '1px solid rgba(255, 255, 255, 0.15)',
41
+ borderRadius: '8px',
42
+ padding: '4px',
43
+ boxShadow: '0 12px 30px -4px rgba(0, 0, 0, 0.5), 0 6px 12px -4px rgba(0, 0, 0, 0.3)',
44
+ fontFamily: 'system-ui, -apple-system, sans-serif',
45
+ fontSize: '13px',
46
+ minWidth: '160px',
47
+ maxWidth: '300px',
48
+ userSelect: 'none',
49
+ pointerEvents: 'auto'
50
+ });
51
+ document.body.appendChild(popupEl);
52
+ }
53
+
54
+ if (showSuggestions) createPopup();
55
+
56
+ function positionPopup() {
57
+ if (!popupEl || !el) return;
58
+ const rect = el.getBoundingClientRect();
59
+ const popupHeight = popupEl.offsetHeight || 120;
60
+ const spaceBelow = window.innerHeight - rect.bottom;
61
+ let top = rect.bottom + 4;
62
+ if (spaceBelow < popupHeight && rect.top > popupHeight) {
63
+ top = rect.top - popupHeight - 4;
64
+ }
65
+ popupEl.style.top = Math.max(8, top) + 'px';
66
+ popupEl.style.left = Math.max(8, Math.min(rect.left, window.innerWidth - 320)) + 'px';
67
+ }
68
+
69
+ function hidePopup() {
70
+ if (!popupEl) return;
71
+ popupEl.style.display = 'none';
72
+ candidates = [];
73
+ selectedIndex = 0;
74
+ activeWord = '';
75
+ activeWordStart = -1;
76
+ }
77
+
78
+ function renderPopup() {
79
+ if (!popupEl || candidates.length === 0) {
80
+ hidePopup();
81
+ return;
82
+ }
83
+ popupEl.innerHTML = '';
84
+ candidates.forEach((cand, idx) => {
85
+ const item = document.createElement('div');
86
+ item.className = 'devanagari-suggestion-item';
87
+ const isSelected = idx === selectedIndex;
88
+ Object.assign(item.style, {
89
+ display: 'flex',
90
+ alignItems: 'center',
91
+ justifyContent: 'space-between',
92
+ padding: '6px 10px',
93
+ borderRadius: '5px',
94
+ cursor: 'pointer',
95
+ background: isSelected ? 'rgba(59, 130, 246, 0.95)' : 'transparent',
96
+ color: isSelected ? '#ffffff' : '#f1f5f9',
97
+ fontWeight: isSelected ? '700' : '500',
98
+ transition: 'background 0.1s ease'
99
+ });
100
+ const label = document.createElement('span');
101
+ label.textContent = cand;
102
+ label.style.overflow = 'hidden';
103
+ label.style.textOverflow = 'ellipsis';
104
+ label.style.whiteSpace = 'nowrap';
105
+ const keyBadge = document.createElement('span');
106
+ keyBadge.textContent = String(idx + 1);
107
+ Object.assign(keyBadge.style, {
108
+ fontSize: '10px',
109
+ fontWeight: '700',
110
+ opacity: isSelected ? '1' : '0.6',
111
+ marginLeft: '8px',
112
+ background: isSelected ? 'rgba(255, 255, 255, 0.3)' : 'rgba(255, 255, 255, 0.12)',
113
+ padding: '1px 5px',
114
+ borderRadius: '3px'
115
+ });
116
+ item.appendChild(label);
117
+ item.appendChild(keyBadge);
118
+ item.addEventListener('mousedown', (e) => {
119
+ e.preventDefault();
120
+ selectCandidate(idx, ' ');
121
+ });
122
+ popupEl.appendChild(item);
123
+ });
124
+ popupEl.style.display = 'flex';
125
+ positionPopup();
126
+ }
127
+
128
+ function setNativeValue(input, newValue) {
129
+ const isInput = input instanceof HTMLInputElement;
130
+ const proto = isInput ? HTMLInputElement.prototype : HTMLTextAreaElement.prototype;
131
+ const desc = Object.getOwnPropertyDescriptor(proto, 'value');
132
+ if (desc && desc.set) {
133
+ desc.set.call(input, newValue);
134
+ } else {
135
+ input.value = newValue;
136
+ }
137
+ input.dispatchEvent(new Event('input', { bubbles: true }));
138
+ input.dispatchEvent(new Event('change', { bubbles: true }));
139
+ }
140
+
141
+ function selectCandidate(index, delimiter = ' ') {
142
+ if (index < 0 || index >= candidates.length) return;
143
+ const chosen = candidates[index];
144
+ const currentValue = el.value;
145
+ const before = currentValue.slice(0, activeWordStart);
146
+ const after = currentValue.slice(el.selectionStart);
147
+ const newValue = before + chosen + delimiter + after;
148
+ setNativeValue(el, newValue);
149
+ const newCursor = activeWordStart + chosen.length + delimiter.length;
150
+ setTimeout(() => { el.setSelectionRange(newCursor, newCursor); }, 0);
151
+ lastConversion = {
152
+ original: activeWord,
153
+ converted: chosen,
154
+ startPos: activeWordStart,
155
+ endPos: activeWordStart + chosen.length
156
+ };
157
+ if (onTransliterate) {
158
+ onTransliterate({ original: activeWord, transliterated: chosen });
159
+ }
160
+ hidePopup();
161
+ }
162
+
163
+ function setEnabled(val) {
164
+ enabled = Boolean(val);
165
+ if (!enabled) hidePopup();
166
+ if (onToggle) onToggle(enabled);
167
+ }
168
+
169
+ function isEnabled() { return enabled; }
170
+
171
+ function toggle() { setEnabled(!enabled); return enabled; }
172
+
173
+ function handleInput() {
174
+ if (!enabled || !showSuggestions) return;
175
+ const cursorPos = el.selectionStart;
176
+ const textBeforeCursor = el.value.slice(0, cursorPos);
177
+ const match = textBeforeCursor.match(/([a-zA-Z]+)$/);
178
+ if (match) {
179
+ activeWord = match[1];
180
+ activeWordStart = cursorPos - activeWord.length;
181
+ candidates = getSuggestions(activeWord, { maxSuggestions, includeOriginal: true });
182
+ selectedIndex = 0;
183
+ renderPopup();
184
+ } else {
185
+ hidePopup();
186
+ }
187
+ }
188
+
189
+ function handleKeyDown(e) {
190
+ const isCmdOrCtrl = e.metaKey || e.ctrlKey;
191
+ if (enableShortcut && isCmdOrCtrl && e.key.toLowerCase() === 'm') {
192
+ e.preventDefault();
193
+ toggle();
194
+ return;
195
+ }
196
+ if (!enabled) return;
197
+
198
+ if (popupEl && popupEl.style.display !== 'none' && candidates.length > 0) {
199
+ if (e.key === 'ArrowDown') {
200
+ e.preventDefault();
201
+ selectedIndex = (selectedIndex + 1) % candidates.length;
202
+ renderPopup();
203
+ return;
204
+ }
205
+ if (e.key === 'ArrowUp') {
206
+ e.preventDefault();
207
+ selectedIndex = (selectedIndex - 1 + candidates.length) % candidates.length;
208
+ renderPopup();
209
+ return;
210
+ }
211
+ if (e.key === 'Escape') {
212
+ e.preventDefault();
213
+ hidePopup();
214
+ return;
215
+ }
216
+ if (e.key === 'Tab' || e.key === 'Enter') {
217
+ e.preventDefault();
218
+ selectCandidate(selectedIndex, e.key === 'Enter' ? '' : ' ');
219
+ return;
220
+ }
221
+ if (e.key === ' ') {
222
+ e.preventDefault();
223
+ selectCandidate(selectedIndex, ' ');
224
+ return;
225
+ }
226
+ const num = parseInt(e.key, 10);
227
+ if (!isNaN(num) && num >= 1 && num <= candidates.length) {
228
+ e.preventDefault();
229
+ selectCandidate(num - 1, ' ');
230
+ return;
231
+ }
232
+ }
233
+
234
+ const cursorPos = el.selectionStart;
235
+ if (e.key === 'Backspace' && lastConversion) {
236
+ if (cursorPos === lastConversion.endPos || cursorPos === lastConversion.endPos + 1) {
237
+ e.preventDefault();
238
+ const currentValue = el.value;
239
+ const before = currentValue.slice(0, lastConversion.startPos);
240
+ const after = currentValue.slice(cursorPos);
241
+ const restored = before + lastConversion.original + after;
242
+ setNativeValue(el, restored);
243
+ const newCursor = lastConversion.startPos + lastConversion.original.length;
244
+ setTimeout(() => { el.setSelectionRange(newCursor, newCursor); }, 0);
245
+ lastConversion = null;
246
+ hidePopup();
247
+ return;
248
+ }
249
+ }
250
+ if (e.key !== 'Backspace') {
251
+ lastConversion = null;
252
+ }
253
+ }
254
+
255
+ function handleBlur() {
256
+ setTimeout(() => { hidePopup(); }, 200);
257
+ }
258
+
259
+ el.addEventListener('keydown', handleKeyDown);
260
+ el.addEventListener('input', handleInput);
261
+ el.addEventListener('blur', handleBlur);
262
+
263
+ function detach() {
264
+ el.removeEventListener('keydown', handleKeyDown);
265
+ el.removeEventListener('input', handleInput);
266
+ el.removeEventListener('blur', handleBlur);
267
+ if (popupEl && popupEl.parentNode) {
268
+ popupEl.parentNode.removeChild(popupEl);
269
+ }
270
+ popupEl = null;
271
+ }
272
+
273
+ return { detach, setEnabled, isEnabled, toggle };
274
+ }
package/src/index.js ADDED
@@ -0,0 +1,125 @@
1
+ /**
2
+ * marathi-transliterate
3
+ * Zero-dependency, offline-first Plain JavaScript phonetic transliteration library
4
+ * for Marathi (Devanagari) script with live smart suggestions.
5
+ */
6
+
7
+ import { MARATHI_DICTIONARY } from './dictionary.js';
8
+ import { phoneticTransliterateWord } from './phonetics.js';
9
+ import { getCachedWord, setCachedWord, fetchOnlineMarathi } from './cache.js';
10
+ import { attachDevanagari } from './dom.js';
11
+
12
+ export { MARATHI_DICTIONARY, fetchOnlineMarathi, attachDevanagari };
13
+
14
+ /**
15
+ * Transliterates a single English word into Marathi (Devanagari).
16
+ * @param {string} word - Romanized word e.g. sakhar, kanda, atata * @returns {string} - Marathi Devanagari script e.g. साखर, कांदा, बटाटा */
17
+ export function transliterateWord(word) {
18
+ if (!word) return '';
19
+ const clean = word.toLowerCase().trim();
20
+
21
+ // 1. Check curated high-accuracy dictionary first
22
+ if (MARATHI_DICTIONARY[clean]) {
23
+ return MARATHI_DICTIONARY[clean];
24
+ }
25
+
26
+ // 2. Check local/memory cache
27
+ const cached = getCachedWord(clean);
28
+ if (cached && cached.length > 0) {
29
+ return cached[0];
30
+ }
31
+
32
+ // 3. Algorithmic rule-based phonetic engine
33
+ const phonetic = phoneticTransliterateWord(word);
34
+
35
+ // Background fetch to enrich cache if online (non-blocking)
36
+ if (typeof fetch !== 'undefined') {
37
+ fetchOnlineMarathi(clean).catch(() => {});
38
+ }
39
+
40
+ return phonetic;
41
+ }
42
+
43
+ /**
44
+ * Transliterates a full sentence, paragraph or mixed text into Marathi.
45
+ * Preserves numbers, punctuation, spaces, and formatting.
46
+ * @param {string} text - Input text
47
+ * @returns {string} - Transliterated text
48
+ */
49
+ export function transliterate(text) {
50
+ if (!text) return '';
51
+
52
+ return text.replace(/[a-zA-Z]+/g, match => {
53
+ return transliterateWord(match);
54
+ });
55
+ }
56
+
57
+ /**
58
+ * Alias for transliterate (drop-in compatibility)
59
+ */
60
+ export const toDevanagari = transliterate;
61
+
62
+ /**
63
+ * Returns ranked candidate spelling suggestions for a word.
64
+ * @param {string} word
65
+ * @param {Object} [options]
66
+ * @param {number} [options.maxSuggestions=5]
67
+ * @param {boolean} [options.includeOriginal=true]
68
+ * @returns {string[]}
69
+ */
70
+ export function getSuggestions(word, options = {}) {
71
+ if (!word || !word.trim()) return [];
72
+ const clean = word.toLowerCase().trim();
73
+ const max = options.maxSuggestions || 5;
74
+ const includeOriginal = options.includeOriginal !== false;
75
+ const suggestions = [];
76
+
77
+ const add = (candidate) => {
78
+ if (candidate && !suggestions.includes(candidate)) {
79
+ suggestions.push(candidate);
80
+ }
81
+ };
82
+
83
+ // 1. Exact match from curated dictionary
84
+ if (MARATHI_DICTIONARY[clean]) {
85
+ add(MARATHI_DICTIONARY[clean]);
86
+ }
87
+
88
+ // 2. Check local/memory cache
89
+ const cached = getCachedWord(clean);
90
+ if (cached) {
91
+ for (const c of cached) add(c);
92
+ }
93
+
94
+ // 3. Phonetic algorithm conversion
95
+ const phonetic = phoneticTransliterateWord(word);
96
+ if (phonetic) add(phonetic);
97
+
98
+ // 4. Prefix matches from dictionary (if clean.length >= 3)
99
+ if (clean.length >= 3 && suggestions.length < max) {
100
+ for (const [key, val] of Object.entries(MARATHI_DICTIONARY)) {
101
+ if (key.startsWith(clean) && key !== clean) {
102
+ add(val);
103
+ if (suggestions.length >= max) break;
104
+ }
105
+ }
106
+ }
107
+
108
+ // 5. Add raw English word at the end so user can easily keep original
109
+ if (includeOriginal && !suggestions.includes(word)) {
110
+ suggestions.push(word);
111
+ }
112
+
113
+ return suggestions.slice(0, max);
114
+ }
115
+
116
+ // Default export
117
+ export default {
118
+ transliterate,
119
+ transliterateWord,
120
+ toDevanagari,
121
+ getSuggestions,
122
+ attachDevanagari,
123
+ MARATHI_DICTIONARY,
124
+ fetchOnlineMarathi
125
+ };
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Algorithmic phonetic transliteration from Roman (English) script
3
+ * to Marathi (Devanagari) script.
4
+ */
5
+
6
+ const CONSONANTS = [
7
+ // Multi-character special combinations
8
+ { en: 'dny', mr: 'ज्ञ' },
9
+ { en: 'jn', mr: 'ज्ञ' },
10
+ { en: 'gy', mr: 'ज्ञ' },
11
+ { en: 'ksh', mr: 'क्ष' },
12
+ { en: 'shr', mr: 'श्र' },
13
+ { en: 'chh', mr: 'छ' },
14
+ { en: 'ch', mr: 'च' },
15
+ { en: 'kh', mr: 'ख' },
16
+ { en: 'gh', mr: 'घ' },
17
+ { en: 'jh', mr: 'झ' },
18
+ { en: 'zh', mr: 'झ' },
19
+ { en: 'th', mr: 'थ' },
20
+ { en: 'dh', mr: 'ध' },
21
+ { en: 'ph', mr: 'फ' },
22
+ { en: 'bh', mr: 'भ' },
23
+ { en: 'shh', mr: 'ष' },
24
+ { en: 'sh', mr: 'श' },
25
+ { en: 'ng', mr: 'ङ' },
26
+
27
+ // Single consonants
28
+ { en: 'k', mr: 'क' },
29
+ { en: 'g', mr: 'ग' },
30
+ { en: 'j', mr: 'ज' },
31
+ { en: 't', mr: 'त' },
32
+ { en: 'd', mr: 'द' },
33
+ { en: 'n', mr: 'न' },
34
+ { en: 'p', mr: 'प' },
35
+ { en: 'f', mr: 'फ' },
36
+ { en: 'b', mr: 'ब' },
37
+ { en: 'm', mr: 'म' },
38
+ { en: 'y', mr: 'य' },
39
+ { en: 'r', mr: 'र' },
40
+ { en: 'l', mr: 'ल' },
41
+ { en: 'L', mr: 'ळ' },
42
+ { en: 'v', mr: 'व' },
43
+ { en: 'w', mr: 'व' },
44
+ { en: 's', mr: 'स' },
45
+ { en: 'h', mr: 'ह' },
46
+ { en: 'x', mr: 'क्ष' },
47
+ { en: 'z', mr: 'झ' },
48
+ { en: 'T', mr: 'ट' },
49
+ { en: 'D', mr: 'ड' },
50
+ { en: 'N', mr: 'ण' }
51
+ ];
52
+
53
+ const INDEPENDENT_VOWELS = [
54
+ { en: 'aa', mr: 'आ' },
55
+ { en: 'A', mr: 'आ' },
56
+ { en: 'a', mr: 'अ' },
57
+ { en: 'ee', mr: 'ई' },
58
+ { en: 'ii', mr: 'ई' },
59
+ { en: 'i', mr: 'इ' },
60
+ { en: 'oo', mr: 'ऊ' },
61
+ { en: 'uu', mr: 'ऊ' },
62
+ { en: 'u', mr: 'उ' },
63
+ { en: 'ai', mr: 'ऐ' },
64
+ { en: 'ay', mr: 'ऐ' },
65
+ { en: 'au', mr: 'औ' },
66
+ { en: 'ou', mr: 'औ' },
67
+ { en: 'ow', mr: 'औ' },
68
+ { en: 'e', mr: 'ए' },
69
+ { en: 'o', mr: 'ओ' },
70
+ { en: 'am', mr: 'अं' },
71
+ { en: 'an', mr: 'अं' },
72
+ { en: 'ri', mr: 'ऋ' },
73
+ { en: 'ru', mr: 'ऋ' }
74
+ ];
75
+
76
+ const MATRAS = [
77
+ { en: 'aa', mr: 'ा' },
78
+ { en: 'A', mr: 'ा' },
79
+ { en: 'a', mr: '' }, // Inherent vowel: cancels halant, no visible matra
80
+ { en: 'ee', mr: 'ी' },
81
+ { en: 'ii', mr: 'ी' },
82
+ { en: 'i', mr: 'ि' },
83
+ { en: 'oo', mr: 'ू' },
84
+ { en: 'uu', mr: 'ू' },
85
+ { en: 'u', mr: 'ु' },
86
+ { en: 'ai', mr: 'ै' },
87
+ { en: 'ay', mr: 'ै' },
88
+ { en: 'au', mr: 'ौ' },
89
+ { en: 'ou', mr: 'ौ' },
90
+ { en: 'ow', mr: 'ौ' },
91
+ { en: 'e', mr: 'े' },
92
+ { en: 'o', mr: 'ो' },
93
+ { en: 'am', mr: 'ं' },
94
+ { en: 'an', mr: 'ं' },
95
+ { en: 'ri', mr: 'ृ' },
96
+ { en: 'ru', mr: 'ृ' }
97
+ ];
98
+
99
+ /**
100
+ * Phonetically transliterates a single Roman word to Devanagari.
101
+ * @param {string} text - Single English word (e.g. "shantanu", "ramesh")
102
+ * @returns {string} - Devanagari representation
103
+ */
104
+ export function phoneticTransliterateWord(text) {
105
+ if (!text) return '';
106
+ const clean = text.trim();
107
+ if (!clean) return '';
108
+
109
+ let res = '';
110
+ let i = 0;
111
+ const n = clean.length;
112
+
113
+ while (i < n) {
114
+ // Non-alphabetical character: preserve as is
115
+ if (!/[a-zA-Z]/.test(clean[i])) {
116
+ res += clean[i];
117
+ i++;
118
+ continue;
119
+ }
120
+
121
+ // Check independent vowel at the beginning of a word or after non-alpha
122
+ let matchedVowel = false;
123
+ if (i === 0 || !/[a-zA-Z]/.test(clean[i - 1])) {
124
+ for (const v of INDEPENDENT_VOWELS) {
125
+ if (clean.substring(i, i + v.en.length).toLowerCase() === v.en.toLowerCase()) {
126
+ res += v.mr;
127
+ i += v.en.length;
128
+ matchedVowel = true;
129
+ break;
130
+ }
131
+ }
132
+ if (matchedVowel) continue;
133
+ }
134
+
135
+ // Match consonant
136
+ let matchedConsonant = false;
137
+ for (const c of CONSONANTS) {
138
+ const sub = clean.substring(i, i + c.en.length);
139
+ const isCaseSensitive = c.en === 'L' || c.en === 'T' || c.en === 'D' || c.en === 'N';
140
+ const isMatch = isCaseSensitive ? sub === c.en : sub.toLowerCase() === c.en.toLowerCase();
141
+
142
+ if (isMatch) {
143
+ res += c.mr;
144
+ i += c.en.length;
145
+ matchedConsonant = true;
146
+
147
+ // Check for attached vowel matra
148
+ let matchedMatra = false;
149
+ for (const m of MATRAS) {
150
+ const nextSub = clean.substring(i, i + m.en.length);
151
+ const mCaseSensitive = m.en === 'A';
152
+ const mMatch = mCaseSensitive ? nextSub === m.en : nextSub.toLowerCase() === m.en.toLowerCase();
153
+
154
+ if (mMatch) {
155
+ res += m.mr;
156
+ i += m.en.length;
157
+ matchedMatra = true;
158
+ break;
159
+ }
160
+ }
161
+
162
+ // If no vowel followed and next character is also a consonant, apply halant (virama ्)
163
+ // In Marathi/Hindi phonetics, word-final consonants do NOT get halant
164
+ if (!matchedMatra) {
165
+ if (i < n && /[a-zA-Z]/.test(clean[i])) {
166
+ res += '्';
167
+ }
168
+ }
169
+
170
+ break;
171
+ }
172
+ }
173
+
174
+ if (!matchedConsonant) {
175
+ // Fallback
176
+ res += clean[i];
177
+ i++;
178
+ }
179
+ }
180
+
181
+ return res;
182
+ }