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

160 lines
5.2 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');
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 prompt = `Translate each item to ${name}.
Return ONLY a valid JSON array of translated strings in the same order, no explanations, no markdown.
Example input: ["Hello", "How are you"]
Example output: ["Bonjour", "Comment allez-vous"]
Input: ${JSON.stringify(texts)}`;
const response = await fetch(`${OLLAMA_HOST}/api/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: OLLAMA_MODEL,
prompt,
stream: false
}),
signal: AbortSignal.timeout(120000)
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
const raw = data.response.trim().replace(/```json|```/g, '').trim();
const result = JSON.parse(raw);
if (!Array.isArray(result) || result.length !== texts.length) {
throw new Error(`Unexpected response: got ${result.length}, 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 Ollama / ${OLLAMA_MODEL}`);
}
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();