Echo / src /game /Translator.js
mlmihjaz's picture
Initial commit: ECHO 3D vocab game with Node+SQLite backend, admin dashboard, Docker deploy
173e683
Raw
History Blame Contribute Delete
7.38 kB
/**
* 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;
}