/** * Translator — Sinhala (si) + Tamil (ta) with quality validation. * Primary: Google Translate unofficial endpoint * Fallback: MyMemory * * Validation rejects: wrong-script responses, Bamini-encoded gibberish, * paragraph-length descriptions, English echoes. */ const CACHE_PREFIX = 'echo_t3_'; // bumped to ignore old contextual mistakes const SCRIPT = { si: /[඀-෿]/, ta: /[஀-௿]/, }; /** * Curated translations for the trickiest short / function words. * These ALWAYS win over the translation APIs (which translate "at" as "time", * "of" as "of belonging", etc., because there's no sentence context). */ const FUNCTION_WORDS = { a: { si: 'එක', ta: 'ஒரு' }, i: { si: 'මම', ta: 'நான்' }, an: { si: 'එක', ta: 'ஒரு' }, the: { si: 'එම', ta: 'அந்த' }, at: { si: 'හි', ta: 'இல்' }, in: { si: 'තුළ', ta: 'உள்ளே' }, on: { si: 'මත', ta: 'மீது' }, of: { si: 'හි', ta: 'இன்' }, to: { si: 'කරා', ta: 'க்கு' }, by: { si: 'විසින්', ta: 'மூலம்' }, for: { si: 'සඳහා', ta: 'க்காக' }, from: { si: 'වෙතින්', ta: 'இருந்து' }, up: { si: 'ඉහළ', ta: 'மேலே' }, us: { si: 'අපට', ta: 'எங்களை' }, we: { si: 'අපි', ta: 'நாம்' }, he: { si: 'ඔහු', ta: 'அவன்' }, she: { si: 'ඇය', ta: 'அவள்' }, it: { si: 'එය', ta: 'அது' }, me: { si: 'මට', ta: 'என்னை' }, my: { si: 'මගේ', ta: 'என்' }, no: { si: 'නැහැ', ta: 'இல்லை' }, or: { si: 'හෝ', ta: 'அல்லது' }, so: { si: 'එසේ', ta: 'அப்படி' }, if: { si: 'නම්', ta: 'என்றால்' }, is: { si: 'වේ', ta: 'உள்ளது' }, am: { si: 'වෙමි', ta: 'இருக்கிறேன்' }, are: { si: 'වෙති', ta: 'உள்ளனர்' }, was: { si: 'විය', ta: 'இருந்தது' }, be: { si: 'වෙන්න', ta: 'இரு' }, do: { si: 'කරන්න', ta: 'செய்' }, go: { si: 'යන්න', ta: 'போ' }, and: { si: 'සහ', ta: 'மற்றும்' }, but: { si: 'නමුත්', ta: 'ஆனால்' }, not: { si: 'නැත', ta: 'இல்லை' }, yes: { si: 'ඔව්', ta: 'ஆம்' }, you: { si: 'ඔබ', ta: 'நீ' }, who: { si: 'කවුද', ta: 'யார்' }, why: { si: 'ඇයි', ta: 'ஏன்' }, how: { si: 'කෙසේද', ta: 'எப்படி' }, what: { si: 'මොකක්ද', ta: 'என்ன' }, when: { si: 'කවදද', ta: 'எப்போது' }, where:{ si: 'කොහෙද', ta: 'எங்கே' }, this: { si: 'මේ', ta: 'இந்த' }, that: { si: 'එම', ta: 'அந்த' }, these:{ si: 'මේවා', ta: 'இவை' }, those:{ si: 'ඒවා', ta: 'அவை' }, with: { si: 'සමඟ', ta: 'உடன்' }, out: { si: 'පිටත', ta: 'வெளியே' }, off: { si: 'ඉවත්', ta: 'விலகி' }, one: { si: 'එක', ta: 'ஒன்று' }, two: { si: 'දෙක', ta: 'இரண்டு' }, ten: { si: 'දහය', ta: 'பத்து' }, yes: { si: 'ඔව්', ta: 'ஆம்' }, ok: { si: 'හරි', ta: 'சரி' }, hi: { si: 'හායි', ta: 'வணக்கம்' }, oh: { si: 'ඕ', ta: 'ஓ' }, as: { si: 'ලෙස', ta: 'ஆக' }, all: { si: 'සියල්ල', ta: 'அனைத்தும்' }, any: { si: 'ඕනෑම', ta: 'எதேனும்' }, can: { si: 'හැකි', ta: 'முடியும்' }, will: { si: 'කරයි', ta: 'செய்யும்' }, may: { si: 'පුළුවන්', ta: 'இருக்கலாம்' }, her: { si: 'ඇගේ', ta: 'அவளுடைய' }, him: { si: 'ඔහුට', ta: 'அவனை' }, his: { si: 'ඔහුගේ', ta: 'அவனது' }, our: { si: 'අපේ', ta: 'எங்கள்' }, your: { si: 'ඔබේ', ta: 'உங்கள்' }, its: { si: 'එහි', ta: 'அதன்' }, }; export function validateTranslation(text, lang, sourceWord) { if (!text) return false; const t = String(text).trim(); if (!t) return false; if (t.toLowerCase() === sourceWord.toLowerCase()) return false; if (t.length > Math.max(40, sourceWord.length * 6)) return false; const re = SCRIPT[lang]; if (!re) return true; const scriptChars = (t.match(new RegExp(re.source, 'g')) || []).length; const latinChars = (t.match(/[a-zA-Z]/g) || []).length; if (scriptChars < 1) return false; if (latinChars > scriptChars) return false; const garbage = (t.match(/[;>\/\\\[\]{}|`~]/g) || []).length; if (garbage > 1 && scriptChars < 3) return false; return true; } async function tryGoogle(word, lang) { try { const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl=${lang}&dt=t&q=${encodeURIComponent(word)}`; const r = await fetch(url); if (!r.ok) return null; const j = await r.json(); if (j?.[0]?.[0]?.[0]) { const t = String(j[0][0][0]).trim(); if (validateTranslation(t, lang, word)) return t; } } catch (e) {} return null; } async function tryMyMemory(word, lang) { try { const url = `https://api.mymemory.translated.net/get?q=${encodeURIComponent(word)}&langpair=en|${lang}`; const r = await fetch(url); if (!r.ok) return null; const j = await r.json(); if (j?.responseData?.translatedText) { const t = String(j.responseData.translatedText).trim(); if (validateTranslation(t, lang, word)) return t; } } catch (e) {} return null; } async function translateOne(word, lang) { return (await tryGoogle(word, lang)) || (await tryMyMemory(word, lang)); } export async function getMeaning(word) { // 1. Curated function-word dictionary ALWAYS wins (context-dependent words) if (FUNCTION_WORDS[word]) { return { si: FUNCTION_WORDS[word].si, ta: FUNCTION_WORDS[word].ta }; } // 2. Validated cache try { const cached = JSON.parse(localStorage.getItem(CACHE_PREFIX + word) || 'null'); if (cached) { const siOK = cached.si === '—' || validateTranslation(cached.si, 'si', word); const taOK = cached.ta === '—' || validateTranslation(cached.ta, 'ta', word); if (siOK && taOK) return cached; } } catch (e) {} // 3. Live API const [si, ta] = await Promise.all([translateOne(word, 'si'), translateOne(word, 'ta')]); const out = { si: si || '—', ta: ta || '—' }; try { localStorage.setItem(CACHE_PREFIX + word, JSON.stringify(out)); } catch (e) {} return out; }