my-homelab-configs/apps/website/translation.js

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();