|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
|
const translateButton = document.getElementById('translate-button');
|
|
|
const clearButton = document.getElementById('clear-button');
|
|
|
const copyButton = document.getElementById('copy-button');
|
|
|
const downloadButton = document.getElementById('download-button');
|
|
|
const shareButton = document.getElementById('share-button');
|
|
|
const textToTranslate = document.getElementById('text-to-translate');
|
|
|
const sourceLanguage = document.getElementById('source-language');
|
|
|
const outputDiv = document.getElementById('output');
|
|
|
const batchToggle = document.getElementById('batch-toggle');
|
|
|
const langDetectHint = document.getElementById('lang-detect-hint');
|
|
|
const themeToggle = document.getElementById('theme-toggle');
|
|
|
const processDataButton = document.getElementById('process-data-button');
|
|
|
const processDataStatus = document.getElementById('process-data-status');
|
|
|
|
|
|
|
|
|
let detectTimer = null;
|
|
|
const detectDelay = 250;
|
|
|
|
|
|
|
|
|
try {
|
|
|
const langRes = await fetch('/languages');
|
|
|
const langData = await langRes.json();
|
|
|
const langs = (langData && langData.supported_languages) || ['nepali', 'sinhala'];
|
|
|
sourceLanguage.innerHTML = '';
|
|
|
langs.forEach(l => {
|
|
|
const opt = document.createElement('option');
|
|
|
opt.value = l;
|
|
|
opt.textContent = l.charAt(0).toUpperCase() + l.slice(1);
|
|
|
sourceLanguage.appendChild(opt);
|
|
|
});
|
|
|
} catch (e) {
|
|
|
|
|
|
sourceLanguage.innerHTML = '<option value="nepali">Nepali</option><option value="sinhala">Sinhala</option>';
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
(function() {
|
|
|
const savedTheme = localStorage.getItem('theme');
|
|
|
if (!savedTheme) {
|
|
|
document.documentElement.setAttribute('data-theme', 'gradient');
|
|
|
}
|
|
|
})();
|
|
|
|
|
|
themeToggle.addEventListener('click', () => {
|
|
|
const html = document.documentElement;
|
|
|
const isDark = html.getAttribute('data-theme') === 'dark';
|
|
|
html.setAttribute('data-theme', isDark ? 'light' : 'dark');
|
|
|
themeToggle.textContent = isDark ? 'Light mode' : 'Dark mode';
|
|
|
localStorage.setItem('anuvaad_theme', isDark ? 'light' : 'dark');
|
|
|
});
|
|
|
const savedTheme = localStorage.getItem('anuvaad_theme');
|
|
|
if (savedTheme) {
|
|
|
document.documentElement.setAttribute('data-theme', savedTheme);
|
|
|
themeToggle.textContent = savedTheme === 'dark' ? 'Dark mode' : 'Light mode';
|
|
|
}
|
|
|
|
|
|
function setLoading(isLoading) {
|
|
|
translateButton.disabled = isLoading;
|
|
|
translateButton.textContent = isLoading ? 'Translating…' : 'Translate';
|
|
|
outputDiv.setAttribute('aria-busy', String(isLoading));
|
|
|
}
|
|
|
|
|
|
|
|
|
textToTranslate.addEventListener('input', () => {
|
|
|
clearTimeout(detectTimer);
|
|
|
detectTimer = setTimeout(async () => {
|
|
|
const sample = (textToTranslate.value || '').slice(0, 200);
|
|
|
let detected = '';
|
|
|
|
|
|
try {
|
|
|
const res = await fetch('/detect', {
|
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json', 'accept': 'application/json' },
|
|
|
body: JSON.stringify({ text: sample })
|
|
|
});
|
|
|
if (res.ok) {
|
|
|
const data = await res.json();
|
|
|
detected = data.detected_language || '';
|
|
|
}
|
|
|
} catch (e) {
|
|
|
|
|
|
}
|
|
|
if (!detected) {
|
|
|
const hasDevanagari = /[\u0900-\u097F]/.test(sample);
|
|
|
const hasSinhala = /[\u0D80-\u0DFF]/.test(sample);
|
|
|
if (hasDevanagari) detected = 'nepali';
|
|
|
else if (hasSinhala) detected = 'sinhala';
|
|
|
}
|
|
|
if (detected) {
|
|
|
sourceLanguage.value = detected;
|
|
|
langDetectHint.textContent = `Detected: ${detected}`;
|
|
|
} else {
|
|
|
langDetectHint.textContent = '';
|
|
|
}
|
|
|
}, detectDelay);
|
|
|
});
|
|
|
|
|
|
translateButton.addEventListener('click', async () => {
|
|
|
const text = (textToTranslate.value || '').trim();
|
|
|
const lang = sourceLanguage.value;
|
|
|
const isBatch = batchToggle && batchToggle.checked;
|
|
|
const borrowedFixEl = document.getElementById('borrowed-toggle');
|
|
|
const borrowedFix = borrowedFixEl ? borrowedFixEl.checked : true;
|
|
|
|
|
|
outputDiv.innerHTML = '';
|
|
|
|
|
|
if (!text) {
|
|
|
outputDiv.innerText = 'Please enter some text to translate.';
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
setLoading(true);
|
|
|
try {
|
|
|
let response;
|
|
|
if (isBatch) {
|
|
|
const texts = text.split('\n').map(t => t.trim()).filter(Boolean);
|
|
|
if (texts.length === 0) {
|
|
|
outputDiv.innerText = 'Please provide at least one non-empty line for batch translation.';
|
|
|
setLoading(false);
|
|
|
return;
|
|
|
}
|
|
|
response = await fetch('/batch-translate', {
|
|
|
method: 'POST',
|
|
|
headers: {
|
|
|
'Content-Type': 'application/json',
|
|
|
'accept': 'application/json'
|
|
|
},
|
|
|
body: JSON.stringify({ texts, source_language: lang, borrowed_fix: borrowedFix })
|
|
|
});
|
|
|
} else {
|
|
|
response = await fetch('/translate', {
|
|
|
method: 'POST',
|
|
|
headers: {
|
|
|
'Content-Type': 'application/json',
|
|
|
'accept': 'application/json'
|
|
|
},
|
|
|
body: JSON.stringify({ text, source_language: lang, borrowed_fix: borrowedFix })
|
|
|
});
|
|
|
}
|
|
|
|
|
|
if (!response.ok) {
|
|
|
const errorData = await response.json().catch(() => ({}));
|
|
|
throw new Error(errorData.detail || 'An error occurred while translating.');
|
|
|
}
|
|
|
|
|
|
const data = await response.json();
|
|
|
if (isBatch) {
|
|
|
const results = data.translated_texts || [];
|
|
|
const table = document.createElement('table');
|
|
|
table.className = 'result-table';
|
|
|
const thead = document.createElement('thead');
|
|
|
thead.innerHTML = '<tr><th>#</th><th>Source</th><th>Translation</th></tr>';
|
|
|
table.appendChild(thead);
|
|
|
const tbody = document.createElement('tbody');
|
|
|
const sources = text.split('\n').map(t => t.trim()).filter(Boolean);
|
|
|
results.forEach((t, idx) => {
|
|
|
const tr = document.createElement('tr');
|
|
|
const tdIdx = document.createElement('td'); tdIdx.textContent = String(idx + 1);
|
|
|
const tdSrc = document.createElement('td'); tdSrc.textContent = sources[idx] || '';
|
|
|
const tdDst = document.createElement('td'); tdDst.textContent = t;
|
|
|
tr.appendChild(tdIdx); tr.appendChild(tdSrc); tr.appendChild(tdDst);
|
|
|
tbody.appendChild(tr);
|
|
|
});
|
|
|
table.appendChild(tbody);
|
|
|
outputDiv.appendChild(table);
|
|
|
downloadButton.dataset.csv = toCSV(sources, results);
|
|
|
} else {
|
|
|
outputDiv.innerText = data.translated_text || data.translation || '';
|
|
|
downloadButton.dataset.csv = toCSV([text], [outputDiv.innerText]);
|
|
|
}
|
|
|
} catch (error) {
|
|
|
outputDiv.innerText = `Error: ${error.message}`;
|
|
|
} finally {
|
|
|
setLoading(false);
|
|
|
}
|
|
|
});
|
|
|
|
|
|
|
|
|
textToTranslate.addEventListener('keydown', (e) => {
|
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
|
|
translateButton.click();
|
|
|
}
|
|
|
});
|
|
|
|
|
|
|
|
|
clearButton.addEventListener('click', () => {
|
|
|
textToTranslate.value = '';
|
|
|
outputDiv.innerHTML = '';
|
|
|
downloadButton.removeAttribute('data-csv');
|
|
|
});
|
|
|
|
|
|
|
|
|
if (processDataButton) {
|
|
|
const datasetControl = processDataButton.closest('.control');
|
|
|
if (datasetControl) datasetControl.hidden = true;
|
|
|
const datasetLabel = document.querySelector('label[for="process-data-button"]');
|
|
|
if (datasetLabel) datasetLabel.hidden = true;
|
|
|
if (processDataStatus) processDataStatus.hidden = true;
|
|
|
}
|
|
|
|
|
|
|
|
|
const borrowedFixEl = document.getElementById('borrowed-toggle');
|
|
|
if (borrowedFixEl) {
|
|
|
const borrowedControl = borrowedFixEl.closest('.control');
|
|
|
|
|
|
if (borrowedControl) borrowedControl.hidden = true;
|
|
|
const borrowedLabel = borrowedFixEl.closest('label');
|
|
|
if (borrowedLabel) {
|
|
|
borrowedLabel.hidden = true;
|
|
|
|
|
|
borrowedLabel.childNodes.forEach(node => {
|
|
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
|
node.textContent = '';
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
const borrowedHint = borrowedControl ? borrowedControl.querySelector('small.hint') : null;
|
|
|
if (borrowedHint) {
|
|
|
borrowedHint.hidden = true;
|
|
|
borrowedHint.textContent = '';
|
|
|
}
|
|
|
|
|
|
borrowedFixEl.remove();
|
|
|
}
|
|
|
|
|
|
|
|
|
async function triggerProcessData() {
|
|
|
if (!processDataStatus) return;
|
|
|
try {
|
|
|
const res = await fetch('/process-data', { method: 'POST' });
|
|
|
if (!res.ok) {
|
|
|
const err = await res.json().catch(() => ({}));
|
|
|
throw new Error(err.detail || 'Failed to process dataset');
|
|
|
}
|
|
|
const data = await res.json();
|
|
|
|
|
|
processDataStatus.textContent = `Processed: ${data.processed_files} files, ${data.total_lines} lines`;
|
|
|
} catch (e) {
|
|
|
processDataStatus.textContent = `Error: ${e.message}`;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
triggerProcessData();
|
|
|
|
|
|
|
|
|
if (processDataButton) {
|
|
|
processDataButton.addEventListener('click', async () => {
|
|
|
|
|
|
processDataStatus.textContent = 'Processing dataset…';
|
|
|
try {
|
|
|
const res = await fetch('/process-data', { method: 'POST' });
|
|
|
if (!res.ok) {
|
|
|
const err = await res.json().catch(() => ({}));
|
|
|
throw new Error(err.detail || 'Failed to process dataset');
|
|
|
}
|
|
|
const data = await res.json();
|
|
|
processDataStatus.textContent = `Processed: ${data.processed_files} files, ${data.total_lines} lines`;
|
|
|
} catch (e) {
|
|
|
processDataStatus.textContent = `Error: ${e.message}`;
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
|
|
|
copyButton.addEventListener('click', async () => {
|
|
|
const text = outputDiv.innerText || '';
|
|
|
if (!text) return;
|
|
|
try {
|
|
|
await navigator.clipboard.writeText(text);
|
|
|
} catch (e) {
|
|
|
|
|
|
const ta = document.createElement('textarea');
|
|
|
ta.value = text;
|
|
|
document.body.appendChild(ta);
|
|
|
ta.select();
|
|
|
document.execCommand('copy');
|
|
|
document.body.removeChild(ta);
|
|
|
}
|
|
|
});
|
|
|
|
|
|
|
|
|
downloadButton.addEventListener('click', () => {
|
|
|
const csv = downloadButton.dataset.csv;
|
|
|
if (!csv) return;
|
|
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
const a = document.createElement('a');
|
|
|
a.href = url;
|
|
|
a.download = 'translations.csv';
|
|
|
a.click();
|
|
|
URL.revokeObjectURL(url);
|
|
|
});
|
|
|
|
|
|
|
|
|
shareButton.addEventListener('click', async () => {
|
|
|
const text = outputDiv.innerText || '';
|
|
|
if (!text) return;
|
|
|
try {
|
|
|
await navigator.share({ text });
|
|
|
} catch (e) {
|
|
|
|
|
|
}
|
|
|
});
|
|
|
|
|
|
function toCSV(sources, results) {
|
|
|
const rows = sources.map((s, i) => [s, results[i] || '']);
|
|
|
const csvRows = rows.map(r => r.map(v => '"' + String(v).replaceAll('"', '""') + '"').join(','));
|
|
|
return 'source,translation\n' + csvRows.join('\n');
|
|
|
}
|
|
|
|
|
|
|
|
|
const themeSelect = document.getElementById('theme-select');
|
|
|
if (themeSelect) {
|
|
|
const saved = localStorage.getItem('theme');
|
|
|
const initial = saved || 'gradient';
|
|
|
document.documentElement.setAttribute('data-theme', initial);
|
|
|
themeSelect.value = initial;
|
|
|
themeSelect.addEventListener('change', (e) => {
|
|
|
const v = e.target.value;
|
|
|
document.documentElement.setAttribute('data-theme', v);
|
|
|
localStorage.setItem('theme', v);
|
|
|
});
|
|
|
}
|
|
|
|
|
|
const themeToggleEl = document.getElementById('theme-toggle');
|
|
|
if (themeToggleEl) {
|
|
|
themeToggleEl.addEventListener('click', () => {
|
|
|
const html = document.documentElement;
|
|
|
const isDark = html.getAttribute('data-theme') === 'dark';
|
|
|
html.setAttribute('data-theme', isDark ? 'light' : 'dark');
|
|
|
localStorage.setItem('theme', isDark ? 'light' : 'dark');
|
|
|
});
|
|
|
}
|
|
|
});
|
|
|
|