155 lines
4.9 KiB
JavaScript
155 lines
4.9 KiB
JavaScript
|
|
const LANG_NAMES = {
|
|
en: 'English', es: 'Spanish', hu: 'Hungarian',
|
|
ro: 'Romanian', hi: 'Hindi', fr: 'French',
|
|
de: 'German', pt: 'Portuguese', it: 'Italian',
|
|
zh: 'Chinese', ja: 'Japanese', ko: 'Korean',
|
|
ar: 'Arabic', ru: 'Russian', pl: 'Polish',
|
|
tr: 'Turkish', sv: 'Swedish', nl: 'Dutch',
|
|
fi: 'Finnish', cs: 'Czech', sk: 'Slovak',
|
|
nah: 'Nahuatl',
|
|
};
|
|
|
|
const urlLang = new URLSearchParams(window.location.search).get('lang');
|
|
const browserLang = (urlLang || navigator.language).slice(0, 2).toLowerCase();
|
|
const langName = LANG_NAMES[browserLang] || browserLang.toUpperCase();
|
|
|
|
const badge = document.getElementById('translation-badge');
|
|
const prompt = document.getElementById('translate-prompt');
|
|
const btn = document.getElementById('translate-btn');
|
|
const actionSpan = document.getElementById('translate-action');
|
|
const detectedSpan = document.getElementById('detected-lang-name');
|
|
let lastTranslationModel = 'Ollama';
|
|
|
|
function showBadge(msg) {
|
|
if (badge) {
|
|
badge.textContent = msg;
|
|
badge.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
function hidePrompt() {
|
|
if (prompt) prompt.style.display = 'none';
|
|
}
|
|
|
|
async function translateBatch(texts, targetLang) {
|
|
const name = LANG_NAMES[targetLang] || targetLang;
|
|
const response = await fetch(TRANSLATE_URL, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
targetLang,
|
|
targetName: name,
|
|
texts
|
|
}),
|
|
signal: AbortSignal.timeout(120000)
|
|
});
|
|
|
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
const data = await response.json();
|
|
const result = data.translations;
|
|
if (data.model) lastTranslationModel = `Ollama / ${data.model}`;
|
|
|
|
const resultCount = Array.isArray(result) ? result.length : 0;
|
|
if (!Array.isArray(result) || resultCount !== texts.length) {
|
|
throw new Error(`Unexpected response: got ${resultCount}, expected ${texts.length}`);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
async function saveLang(lang, translations) {
|
|
try {
|
|
const res = await fetch(SAVE_URL, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ lang, translations })
|
|
});
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
console.log(`Saved runtime translation ${lang}.json`);
|
|
} else {
|
|
console.warn('Save failed:', data.error);
|
|
}
|
|
} catch (e) {
|
|
console.warn('Could not save lang file:', e.message);
|
|
}
|
|
}
|
|
|
|
async function collectPageTranslations(doc, targetLang) {
|
|
const elements = [...doc.querySelectorAll('[data-translate]')];
|
|
if (!elements.length) return {};
|
|
|
|
const texts = elements.map(el => el.getAttribute('data-en') || el.textContent.trim());
|
|
const translated = await translateBatch(texts, targetLang);
|
|
|
|
const result = {};
|
|
elements.forEach((el, i) => {
|
|
const key = el.getAttribute('data-key') || el.getAttribute('data-en');
|
|
result[key] = translated[i];
|
|
});
|
|
return result;
|
|
}
|
|
|
|
async function doTranslation() {
|
|
hidePrompt();
|
|
if (btn) btn.disabled = true;
|
|
|
|
const elements = [...document.querySelectorAll('[data-translate]')];
|
|
if (!elements.length) return;
|
|
|
|
showBadge('Translating page...');
|
|
elements.forEach(el => el.style.opacity = '0.4');
|
|
|
|
const allTranslations = {};
|
|
|
|
try {
|
|
const texts = elements.map(el => el.getAttribute('data-en') || el.textContent.trim());
|
|
const translated = await translateBatch(texts, browserLang);
|
|
|
|
elements.forEach((el, i) => {
|
|
el.textContent = translated[i];
|
|
const key = el.getAttribute('data-key') || el.getAttribute('data-en');
|
|
allTranslations[key] = translated[i];
|
|
});
|
|
} catch (e) {
|
|
console.warn('Page translation failed:', e.message);
|
|
elements.forEach(el => el.style.opacity = '1');
|
|
showBadge('Translation failed — showing original');
|
|
return;
|
|
}
|
|
|
|
elements.forEach(el => el.style.opacity = '1');
|
|
|
|
if (typeof OTHER_PAGES !== 'undefined' && OTHER_PAGES.length > 0) {
|
|
for (const url of OTHER_PAGES) {
|
|
showBadge(`Translating ${url}...`);
|
|
try {
|
|
const res = await fetch(url);
|
|
const html = await res.text();
|
|
const parser = new DOMParser();
|
|
const doc = parser.parseFromString(html, 'text/html');
|
|
const pageTranslations = await collectPageTranslations(doc, browserLang);
|
|
Object.assign(allTranslations, pageTranslations);
|
|
} catch (e) {
|
|
console.warn(`Could not translate ${url}:`, e.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
await saveLang(browserLang, allTranslations);
|
|
showBadge(`Translated by ${lastTranslationModel}`);
|
|
}
|
|
|
|
|
|
function initTranslation() {
|
|
if (STATIC_LANGS.includes(browserLang)) return;
|
|
|
|
if (browserLang === 'nah') return;
|
|
if (actionSpan) actionSpan.textContent = 'Translate to';
|
|
if (detectedSpan) detectedSpan.textContent = langName;
|
|
if (prompt) prompt.style.display = 'block';
|
|
if (btn) btn.addEventListener('click', doTranslation);
|
|
}
|
|
|
|
initTranslation();
|