import { apiGet, apiPut, apiDelete } from '../api.js'; import { showToast } from '../app.js'; import { SOURCE_LANG, normalizeEntry, translationFor, targetLanguages, sourceLanguageLabel, translationListHeaderHTML, buildTranslationGroups, renderGroupedTranslationsHTML, flattenAllQuestionnaires, escHtml as esc, } from '../translations-helpers.js'; let transData = null; let allEntries = []; let selectedLang = ''; let saveTimers = {}; export async function translationsPage() { const app = document.getElementById('app'); app.innerHTML = `
`; try { renderShell(); await loadTranslations(); } catch (e) { document.getElementById('transPageRoot').innerHTML = `

${esc(e.message)}

`; } } function renderShell() { document.getElementById('transPageRoot').innerHTML = `
More languages
Loading…
`; } function pickDefaultTargetLang(languages) { const targets = targetLanguages(languages); if (!targets.length) return ''; if (selectedLang && targets.some(l => l.languageCode === selectedLang)) { return selectedLang; } return targets[0].languageCode; } async function ensureGermanLanguage() { try { await apiPut('translations.php', { type: 'language', languageCode: SOURCE_LANG, name: 'German' }); } catch (_) { /* already exists */ } } async function loadTranslations() { const listArea = document.getElementById('transListArea'); const langSelect = document.getElementById('transLangSelect'); const filter = document.getElementById('transFilter'); const typeFilter = document.getElementById('transTypeFilter'); if (listArea) listArea.innerHTML = '
'; saveTimers = {}; try { await ensureGermanLanguage(); const data = await apiGet('translations.php?all=1'); transData = data; const languages = transData.languages || []; const questionnaires = transData.questionnaires || []; const flat = flattenAllQuestionnaires(questionnaires, data.appStrings || []); allEntries = flat.allEntries; transData.sections = flat.sections; selectedLang = pickDefaultTargetLang(languages); if (langSelect) { const targets = targetLanguages(languages); langSelect.disabled = !targets.length; langSelect.innerHTML = targets.length ? targets.map(l => { const label = l.name ? `${l.name} (${l.languageCode})` : l.languageCode; return ``; }).join('') : ''; langSelect.onchange = () => { selectedLang = langSelect.value; renderEntryList(); }; } if (filter) filter.disabled = false; if (typeFilter) typeFilter.disabled = false; renderLanguagePanel(languages); renderEntryList(); bindFilters(); } catch (e) { if (listArea) listArea.innerHTML = `

${esc(e.message)}

`; showToast(e.message, 'error'); } } function renderLanguagePanel(languages) { const panel = document.getElementById('transLangPanel'); if (!panel) return; const others = targetLanguages(languages); panel.innerHTML = `

German source text is edited in the questionnaire editor. Add other languages here.

DE German (source) ${others.map(l => ` ${esc(l.languageCode.toUpperCase())} ${l.name ? `${esc(l.name)}` : ''} `).join('')}
`; document.getElementById('addLangBtn')?.addEventListener('click', async () => { const code = document.getElementById('newLangCode').value.trim().toLowerCase(); const name = document.getElementById('newLangName').value.trim(); if (!code) { showToast('Language code is required', 'error'); return; } if (code === SOURCE_LANG) { showToast('German (de) is always available as the source language', 'error'); return; } if (languages.some(l => l.languageCode === code)) { showToast('Language already exists', 'error'); return; } try { await apiPut('translations.php', { type: 'language', languageCode: code, name }); showToast(`Language "${code}" added`, 'success'); selectedLang = code; await loadTranslations(); } catch (e) { showToast(e.message, 'error'); } }); panel.querySelectorAll('.lang-chip-remove').forEach(btn => { btn.addEventListener('click', async () => { const lc = btn.dataset.lc; if (!confirm(`Remove "${lc}" and all its translations?`)) return; try { await apiDelete('translations.php', { type: 'language', languageCode: lc }); showToast(`Language "${lc}" removed`, 'success'); if (selectedLang === lc) selectedLang = ''; await loadTranslations(); } catch (e) { showToast(e.message, 'error'); } }); }); } function renderEntryList() { const listArea = document.getElementById('transListArea'); if (!listArea || !transData) return; const languages = transData.languages || []; const targets = targetLanguages(languages); const sections = transData.sections || []; if (!targets.length) { listArea.innerHTML = `

Add at least one language under More languages to translate into.

`; return; } if (!selectedLang) { listArea.innerHTML = `

Choose a language above.

`; return; } if (!sections.length) { listArea.innerHTML = `

No translatable content yet. Add questionnaires and questions in the editor first.

`; return; } const sourceLabel = sourceLanguageLabel(languages); const langLabel = targets.find(l => l.languageCode === selectedLang)?.name || selectedLang.toUpperCase(); const missingCount = allEntries.filter(e => !translationFor(e, selectedLang).trim()).length; const pct = allEntries.length ? Math.round(((allEntries.length - missingCount) / allEntries.length) * 100) : 0; const headerOnce = translationListHeaderHTML(langLabel, sourceLabel); const sectionsHtml = sections.map(sec => { let entries = allEntries.slice(sec.startIdx, sec.startIdx + sec.entryCount); if (sec.isApp) { entries = entries.filter(e => e.type === 'app_string'); } const groups = buildTranslationGroups(entries, selectedLang, sec.startIdx); return `

${esc(sec.name)}

${renderGroupedTranslationsHTML(groups, selectedLang, sourceLabel, true, { showColumnHeader: false, })}
`; }).join(''); listArea.innerHTML = ` ${headerOnce}
${sectionsHtml}
${missingCount ? `${missingCount} missing · ` : ''}${allEntries.length} keys ${pct}% in ${esc(selectedLang.toUpperCase())}
`; bindEntryEditing(allEntries); applyFilters(); } function bindEntryEditing(entries) { document.querySelectorAll('#transGroupedRoot .trans-cell-input').forEach(inp => { inp.addEventListener('input', () => { const idx = parseInt(inp.dataset.idx, 10); const entry = entries[idx]; if (!entry) return; const isGerman = inp.dataset.field === 'german'; if (isGerman) { entry.germanText = inp.value; if (!entry.translations || typeof entry.translations !== 'object') entry.translations = {}; entry.translations[SOURCE_LANG] = inp.value; } else { if (!entry.translations || typeof entry.translations !== 'object') entry.translations = {}; entry.translations[selectedLang] = inp.value; } if (!isGerman) { const missing = !inp.value.trim(); inp.classList.toggle('trans-input-missing', missing); const row = inp.closest('.trans-list-item'); if (row) { row.classList.toggle('trans-list-item-missing', missing); row.dataset.missing = missing ? '1' : '0'; } } const timerKey = isGerman ? `${idx}_de` : `${idx}_${selectedLang}`; clearTimeout(saveTimers[timerKey]); saveTimers[timerKey] = setTimeout(() => saveTranslation(entry, inp, isGerman), 500); }); }); } async function saveTranslation(entry, inp, isGerman = false) { try { await apiPut('translations.php', { type: entry.type, id: entry.entityId, languageCode: isGerman ? SOURCE_LANG : selectedLang, text: inp.value, }); inp.classList.add('save-flash'); setTimeout(() => inp.classList.remove('save-flash'), 400); updateProgress(); } catch (e) { showToast(e.message, 'error'); } } function updateProgress() { const missing = allEntries.filter(e => !translationFor(e, selectedLang).trim()).length; const pct = allEntries.length ? Math.round(((allEntries.length - missing) / allEntries.length) * 100) : 0; const fill = document.querySelector('.trans-progress-fill'); if (fill) fill.style.width = `${pct}%`; const label = document.querySelector('.trans-progress-label'); if (label) label.textContent = `${pct}% in ${selectedLang.toUpperCase()}`; const countEl = document.getElementById('transVisibleCount'); if (countEl && !document.getElementById('transFilter')?.value && !document.getElementById('transTypeFilter')?.value) { countEl.textContent = `${missing ? `${missing} missing · ` : ''}${allEntries.length} keys`; } } let filtersBound = false; function bindFilters() { if (filtersBound) { applyFilters(); return; } filtersBound = true; document.getElementById('transFilter')?.addEventListener('input', applyFilters); document.getElementById('transTypeFilter')?.addEventListener('change', applyFilters); } function applyFilters() { const text = (document.getElementById('transFilter')?.value || '').toLowerCase(); const type = document.getElementById('transTypeFilter')?.value || ''; const items = document.querySelectorAll('#transGroupedRoot .trans-list-item'); let visible = 0; let visibleMissing = 0; items.forEach(row => { const key = row.dataset.key?.toLowerCase() || row.querySelector('.trans-key-text')?.textContent?.toLowerCase() || ''; const rowType = row.dataset.type || ''; const german = row.querySelector('.trans-source-text')?.textContent?.toLowerCase() || row.querySelector('[data-field="german"]')?.value?.toLowerCase() || ''; const val = row.querySelector('[data-field="translation"]')?.value?.toLowerCase() || ''; const show = (!type || rowType === type) && (!text || key.includes(text) || german.includes(text) || val.includes(text)); row.classList.toggle('trans-row-hidden', !show); if (show) { visible++; if (row.dataset.missing === '1') visibleMissing++; } }); document.querySelectorAll('#transGroupedRoot .trans-group').forEach(group => { const rows = group.querySelectorAll('.trans-list-item'); const anyVisible = [...rows].some(r => !r.classList.contains('trans-row-hidden')); group.classList.toggle('trans-group-hidden', !anyVisible); }); document.querySelectorAll('#transGroupedRoot .trans-qn-block').forEach(block => { const anyVisible = [...block.querySelectorAll('.trans-list-item')].some(r => !r.classList.contains('trans-row-hidden')); block.classList.toggle('trans-qn-hidden', !anyVisible); }); const countEl = document.getElementById('transVisibleCount'); if (countEl) { if (text || type) { countEl.textContent = `Showing ${visible} (${visibleMissing} missing)`; } else { const totalMissing = [...items].filter(r => r.dataset.missing === '1').length; countEl.textContent = `${totalMissing ? `${totalMissing} missing · ` : ''}${items.length} keys`; } } }