This commit is contained in:
@ -9,7 +9,7 @@ import {
|
||||
buildTranslationGroups,
|
||||
sourceLanguageLabel,
|
||||
translationListHeaderHTML,
|
||||
renderGroupedTranslationsHTML,
|
||||
renderCollapsibleGroupedTranslationsHTML,
|
||||
escHtml,
|
||||
} from '../translations-helpers.js';
|
||||
import {
|
||||
@ -1726,12 +1726,9 @@ function renderEditorTransList(qnEntries, editable) {
|
||||
const pct = qnEntries.length ? Math.round(((qnEntries.length - missingCount) / qnEntries.length) * 100) : 0;
|
||||
|
||||
const qnGroups = buildTranslationGroups(qnEntries, editorTransLang, 0);
|
||||
const qnName = transData.questionnaire?.name || questionnaire?.name || 'Questionnaire';
|
||||
const bodyHtml = `
|
||||
<div class="trans-qn-block" data-qn="${escHtml(questionnaire.questionnaireID)}">
|
||||
<h2 class="trans-qn-title">${escHtml(qnName)}</h2>
|
||||
${renderGroupedTranslationsHTML(qnGroups, editorTransLang, sourceLabel, editable, { showColumnHeader: false })}
|
||||
</div>`;
|
||||
const bodyHtml = renderCollapsibleGroupedTranslationsHTML(qnGroups, editorTransLang, sourceLabel, editable, {
|
||||
openGroupsWithMissing: true,
|
||||
});
|
||||
|
||||
return `
|
||||
<div class="trans-toolbar">
|
||||
@ -1743,8 +1740,12 @@ function renderEditorTransList(qnEntries, editable) {
|
||||
<option value="answer_option">Answer options</option>
|
||||
</select>
|
||||
</div>
|
||||
${translationListHeaderHTML(langLabel, sourceLabel)}
|
||||
<div id="transGroupedRoot">${bodyHtml}</div>
|
||||
<div class="trans-sheet">
|
||||
${translationListHeaderHTML(langLabel, sourceLabel)}
|
||||
<div id="transGroupedRoot" class="trans-sheet-body">
|
||||
<div class="trans-scope-block">${bodyHtml}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="trans-stats">
|
||||
<span id="transStats">${missingCount ? `${missingCount} missing · ` : ''}${qnEntries.length} keys</span>
|
||||
<span class="trans-stats-progress">
|
||||
@ -1826,11 +1827,11 @@ function bindEditorTransFiltering(entries) {
|
||||
if (row.dataset.missing === '1') visibleMissing++;
|
||||
}
|
||||
});
|
||||
root.querySelectorAll('.trans-group').forEach(group => {
|
||||
root.querySelectorAll('.trans-group-details').forEach(group => {
|
||||
const anyVisible = [...group.querySelectorAll('.trans-list-item')].some(r => !r.classList.contains('trans-row-hidden'));
|
||||
group.classList.toggle('trans-group-hidden', !anyVisible);
|
||||
});
|
||||
root.querySelectorAll('.trans-qn-block').forEach(block => {
|
||||
root.querySelectorAll('.trans-scope-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);
|
||||
});
|
||||
|
||||
@ -8,15 +8,25 @@ import {
|
||||
sourceLanguageLabel,
|
||||
translationListHeaderHTML,
|
||||
buildTranslationGroups,
|
||||
renderGroupedTranslationsHTML,
|
||||
renderCollapsibleGroupedTranslationsHTML,
|
||||
flattenAllQuestionnaires,
|
||||
countMissing,
|
||||
sectionEntries,
|
||||
escHtml as esc,
|
||||
} from '../translations-helpers.js';
|
||||
|
||||
const STORAGE_LANG = 'qdb_trans_lang';
|
||||
const STORAGE_SCOPE = 'qdb_trans_scope';
|
||||
const STORAGE_SHOW = 'qdb_trans_show_mode';
|
||||
|
||||
let transData = null;
|
||||
let allEntries = [];
|
||||
let selectedLang = '';
|
||||
let selectedScope = '';
|
||||
/** @type {'missing' | 'all' | 'complete'} */
|
||||
let showMode = 'missing';
|
||||
let saveTimers = {};
|
||||
let savesInFlight = 0;
|
||||
|
||||
export async function translationsPage() {
|
||||
const app = document.getElementById('app');
|
||||
@ -61,7 +71,21 @@ function renderShell() {
|
||||
</details>
|
||||
|
||||
<div class="trans-toolbar">
|
||||
<input type="search" id="transFilter" class="trans-search-input" placeholder="Search…" disabled>
|
||||
<label class="trans-control trans-toolbar-field">
|
||||
<span>Content</span>
|
||||
<select id="transScopeSelect" class="trans-select" disabled>
|
||||
<option value="">Loading…</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="trans-control trans-toolbar-field">
|
||||
<span>Show</span>
|
||||
<select id="transShowMode" class="trans-select trans-type-select" disabled>
|
||||
<option value="missing">Missing only</option>
|
||||
<option value="all">All</option>
|
||||
<option value="complete">Complete only</option>
|
||||
</select>
|
||||
</label>
|
||||
<input type="search" id="transFilter" class="trans-search-input" placeholder="Search in scope…" disabled>
|
||||
<select id="transTypeFilter" class="trans-select trans-type-select" disabled>
|
||||
<option value="">All types</option>
|
||||
<option value="app_string">App strings</option>
|
||||
@ -79,12 +103,75 @@ function renderShell() {
|
||||
function pickDefaultTargetLang(languages) {
|
||||
const targets = targetLanguages(languages);
|
||||
if (!targets.length) return '';
|
||||
const stored = localStorage.getItem(STORAGE_LANG);
|
||||
if (stored && targets.some(l => l.languageCode === stored)) {
|
||||
return stored;
|
||||
}
|
||||
if (selectedLang && targets.some(l => l.languageCode === selectedLang)) {
|
||||
return selectedLang;
|
||||
}
|
||||
return targets[0].languageCode;
|
||||
}
|
||||
|
||||
function pickDefaultShowMode() {
|
||||
const stored = localStorage.getItem(STORAGE_SHOW);
|
||||
if (stored === 'missing' || stored === 'all' || stored === 'complete') {
|
||||
return stored;
|
||||
}
|
||||
return 'missing';
|
||||
}
|
||||
|
||||
function pickDefaultScope(sections, entries, lang) {
|
||||
const stored = localStorage.getItem(STORAGE_SCOPE);
|
||||
if (stored === '__all__' || sections.some(s => s.questionnaireID === stored)) {
|
||||
return stored;
|
||||
}
|
||||
for (const sec of sections) {
|
||||
const slice = sectionEntries(entries, sec);
|
||||
if (countMissing(slice, lang) > 0) {
|
||||
return sec.questionnaireID;
|
||||
}
|
||||
}
|
||||
return sections[0]?.questionnaireID || '__app__';
|
||||
}
|
||||
|
||||
function scopeLabel(sec, entries, lang) {
|
||||
const slice = sectionEntries(entries, sec);
|
||||
const missing = countMissing(slice, lang);
|
||||
const suffix = missing ? ` (${missing} missing)` : ' (complete)';
|
||||
return `${sec.name}${suffix}`;
|
||||
}
|
||||
|
||||
function scopedSections() {
|
||||
const sections = transData?.sections || [];
|
||||
if (!selectedScope || selectedScope === '__all__') {
|
||||
return sections;
|
||||
}
|
||||
return sections.filter(s => s.questionnaireID === selectedScope);
|
||||
}
|
||||
|
||||
function entriesInScope() {
|
||||
const sections = scopedSections();
|
||||
const out = [];
|
||||
for (const sec of sections) {
|
||||
out.push(...sectionEntries(allEntries, sec));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function setSaveStatus(state) {
|
||||
const el = document.getElementById('transSaveStatus');
|
||||
if (!el) return;
|
||||
el.dataset.state = state;
|
||||
if (state === 'saving') {
|
||||
el.textContent = 'Saving…';
|
||||
} else if (state === 'error') {
|
||||
el.textContent = 'Save failed';
|
||||
} else {
|
||||
el.textContent = 'All changes saved';
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureGermanLanguage() {
|
||||
try {
|
||||
await apiPut('translations.php', { type: 'language', languageCode: SOURCE_LANG, name: 'German' });
|
||||
@ -112,6 +199,8 @@ async function loadTranslations() {
|
||||
transData.sections = flat.sections;
|
||||
|
||||
selectedLang = pickDefaultTargetLang(languages);
|
||||
showMode = pickDefaultShowMode();
|
||||
selectedScope = pickDefaultScope(flat.sections, allEntries, selectedLang);
|
||||
|
||||
if (langSelect) {
|
||||
const targets = targetLanguages(languages);
|
||||
@ -124,16 +213,40 @@ async function loadTranslations() {
|
||||
: '<option value="">Add a language below</option>';
|
||||
langSelect.onchange = () => {
|
||||
selectedLang = langSelect.value;
|
||||
localStorage.setItem(STORAGE_LANG, selectedLang);
|
||||
populateScopeSelect();
|
||||
renderEntryList();
|
||||
};
|
||||
}
|
||||
|
||||
const scopeSelect = document.getElementById('transScopeSelect');
|
||||
const showSelect = document.getElementById('transShowMode');
|
||||
if (scopeSelect) {
|
||||
scopeSelect.disabled = false;
|
||||
populateScopeSelect();
|
||||
scopeSelect.onchange = () => {
|
||||
selectedScope = scopeSelect.value;
|
||||
localStorage.setItem(STORAGE_SCOPE, selectedScope);
|
||||
renderEntryList();
|
||||
};
|
||||
}
|
||||
if (showSelect) {
|
||||
showSelect.disabled = false;
|
||||
showSelect.value = showMode;
|
||||
showSelect.onchange = () => {
|
||||
showMode = showSelect.value;
|
||||
localStorage.setItem(STORAGE_SHOW, showMode);
|
||||
applyFilters();
|
||||
};
|
||||
}
|
||||
|
||||
if (filter) filter.disabled = false;
|
||||
if (typeFilter) typeFilter.disabled = false;
|
||||
|
||||
renderLanguagePanel(languages);
|
||||
renderEntryList();
|
||||
bindFilters();
|
||||
setSaveStatus('saved');
|
||||
} catch (e) {
|
||||
if (listArea) listArea.innerHTML = `<p class="error-text">${esc(e.message)}</p>`;
|
||||
showToast(e.message, 'error');
|
||||
@ -208,13 +321,31 @@ function renderLanguagePanel(languages) {
|
||||
});
|
||||
}
|
||||
|
||||
function populateScopeSelect() {
|
||||
const scopeSelect = document.getElementById('transScopeSelect');
|
||||
if (!scopeSelect || !transData) return;
|
||||
|
||||
const sections = transData.sections || [];
|
||||
const options = sections.map(sec =>
|
||||
`<option value="${esc(sec.questionnaireID)}"${sec.questionnaireID === selectedScope ? ' selected' : ''}>${esc(scopeLabel(sec, allEntries, selectedLang))}</option>`
|
||||
);
|
||||
options.push(`<option value="__all__"${selectedScope === '__all__' ? ' selected' : ''}>All content</option>`);
|
||||
|
||||
scopeSelect.innerHTML = options.join('');
|
||||
if (!selectedScope || (selectedScope !== '__all__' && !sections.some(s => s.questionnaireID === selectedScope))) {
|
||||
selectedScope = pickDefaultScope(sections, allEntries, selectedLang);
|
||||
scopeSelect.value = selectedScope;
|
||||
localStorage.setItem(STORAGE_SCOPE, selectedScope);
|
||||
}
|
||||
}
|
||||
|
||||
function renderEntryList() {
|
||||
const listArea = document.getElementById('transListArea');
|
||||
if (!listArea || !transData) return;
|
||||
|
||||
const languages = transData.languages || [];
|
||||
const targets = targetLanguages(languages);
|
||||
const sections = transData.sections || [];
|
||||
const sections = scopedSections();
|
||||
|
||||
if (!targets.length) {
|
||||
listArea.innerHTML = `
|
||||
@ -227,7 +358,7 @@ function renderEntryList() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sections.length) {
|
||||
if (!transData.sections?.length) {
|
||||
listArea.innerHTML = `
|
||||
<p class="text-muted trans-hint">No translatable content yet. Add questionnaires and questions in the editor first.</p>`;
|
||||
return;
|
||||
@ -235,44 +366,97 @@ function renderEntryList() {
|
||||
|
||||
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 scopeEntries = entriesInScope();
|
||||
const missingCount = countMissing(scopeEntries, selectedLang);
|
||||
const pct = scopeEntries.length
|
||||
? Math.round(((scopeEntries.length - missingCount) / scopeEntries.length) * 100)
|
||||
: 0;
|
||||
|
||||
const headerOnce = translationListHeaderHTML(langLabel, sourceLabel);
|
||||
const useQnDetails = selectedScope === '__all__';
|
||||
|
||||
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 entries = sectionEntries(allEntries, sec);
|
||||
const groups = buildTranslationGroups(entries, selectedLang, sec.startIdx);
|
||||
return `
|
||||
<div class="trans-qn-block" data-qn="${esc(sec.questionnaireID)}">
|
||||
<h2 class="trans-qn-title">${esc(sec.name)}</h2>
|
||||
${renderGroupedTranslationsHTML(groups, selectedLang, sourceLabel, true, {
|
||||
showColumnHeader: false,
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
const secMissing = countMissing(entries, selectedLang);
|
||||
const inner = renderCollapsibleGroupedTranslationsHTML(groups, selectedLang, sourceLabel, true, {
|
||||
openGroupsWithMissing: selectedScope !== '__all__',
|
||||
});
|
||||
|
||||
if (useQnDetails) {
|
||||
const open = secMissing > 0 && sections.filter(s =>
|
||||
countMissing(sectionEntries(allEntries, s), selectedLang) > 0
|
||||
)[0]?.questionnaireID === sec.questionnaireID;
|
||||
const summary = secMissing
|
||||
? `${sec.name} · ${secMissing} missing`
|
||||
: `${sec.name} · complete`;
|
||||
return `
|
||||
<details class="trans-qn-section"${open ? ' open' : ''} data-qn="${esc(sec.questionnaireID)}">
|
||||
<summary class="trans-qn-summary">${esc(summary)}</summary>
|
||||
${inner}
|
||||
</details>`;
|
||||
}
|
||||
|
||||
return `<div class="trans-scope-block" data-qn="${esc(sec.questionnaireID)}">${inner}</div>`;
|
||||
}).join('');
|
||||
|
||||
listArea.innerHTML = `
|
||||
${headerOnce}
|
||||
<div id="transGroupedRoot">${sectionsHtml}</div>
|
||||
<div class="trans-stats">
|
||||
<span id="transVisibleCount">${missingCount ? `${missingCount} missing · ` : ''}${allEntries.length} keys</span>
|
||||
<span class="trans-stats-progress">
|
||||
<span class="trans-progress-bar"><span class="trans-progress-fill" style="width:${pct}%"></span></span>
|
||||
<span class="trans-progress-label">${pct}% in ${esc(selectedLang.toUpperCase())}</span>
|
||||
</span>
|
||||
<div class="trans-top-bar">
|
||||
<div class="trans-top-stats">
|
||||
<span id="transVisibleCount">${missingCount ? `${missingCount} missing · ` : ''}${scopeEntries.length} in scope</span>
|
||||
<span class="trans-stats-progress">
|
||||
<span class="trans-progress-bar"><span class="trans-progress-fill" style="width:${pct}%"></span></span>
|
||||
<span class="trans-progress-label">${pct}% in ${esc(selectedLang.toUpperCase())}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="trans-top-actions">
|
||||
<span id="transSaveStatus" class="trans-save-status" data-state="saved">All changes saved</span>
|
||||
<button type="button" class="btn btn-sm" id="transJumpMissingBtn">Jump to next missing</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="trans-sheet">
|
||||
${headerOnce}
|
||||
<div id="transGroupedRoot" class="trans-sheet-body${useQnDetails ? ' trans-sheet-body-stack' : ''}">${sectionsHtml}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('transJumpMissingBtn')?.addEventListener('click', jumpToNextMissing);
|
||||
|
||||
bindEntryEditing(allEntries);
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function openAncestors(el) {
|
||||
let node = el;
|
||||
while (node) {
|
||||
if (node.tagName === 'DETAILS') node.open = true;
|
||||
node = node.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
function jumpToNextMissing() {
|
||||
const rows = [...document.querySelectorAll('#transGroupedRoot .trans-list-item')]
|
||||
.filter(r => !r.classList.contains('trans-row-hidden') && r.dataset.missing === '1');
|
||||
if (!rows.length) {
|
||||
showToast(showMode === 'missing'
|
||||
? 'All translations in this scope are complete'
|
||||
: 'No missing translations in view', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
const activeRow = document.activeElement?.closest?.('.trans-list-item');
|
||||
let start = 0;
|
||||
if (activeRow) {
|
||||
const idx = rows.indexOf(activeRow);
|
||||
if (idx >= 0) start = idx + 1;
|
||||
}
|
||||
const target = rows[start] || rows[0];
|
||||
openAncestors(target);
|
||||
const inp = target.querySelector('[data-field="translation"]');
|
||||
inp?.focus({ preventScroll: false });
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
|
||||
function bindEntryEditing(entries) {
|
||||
document.querySelectorAll('#transGroupedRoot .trans-cell-input').forEach(inp => {
|
||||
inp.addEventListener('input', () => {
|
||||
@ -303,11 +487,14 @@ function bindEntryEditing(entries) {
|
||||
const timerKey = isGerman ? `${idx}_de` : `${idx}_${selectedLang}`;
|
||||
clearTimeout(saveTimers[timerKey]);
|
||||
saveTimers[timerKey] = setTimeout(() => saveTranslation(entry, inp, isGerman), 500);
|
||||
setSaveStatus('saving');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function saveTranslation(entry, inp, isGerman = false) {
|
||||
savesInFlight++;
|
||||
setSaveStatus('saving');
|
||||
try {
|
||||
await apiPut('translations.php', {
|
||||
type: entry.type,
|
||||
@ -319,23 +506,48 @@ async function saveTranslation(entry, inp, isGerman = false) {
|
||||
setTimeout(() => inp.classList.remove('save-flash'), 400);
|
||||
updateProgress();
|
||||
} catch (e) {
|
||||
setSaveStatus('error');
|
||||
showToast(e.message, 'error');
|
||||
} finally {
|
||||
savesInFlight = Math.max(0, savesInFlight - 1);
|
||||
if (savesInFlight === 0) {
|
||||
setSaveStatus('saved');
|
||||
populateScopeSelect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 scopeEntries = entriesInScope();
|
||||
const missing = countMissing(scopeEntries, selectedLang);
|
||||
const pct = scopeEntries.length
|
||||
? Math.round(((scopeEntries.length - missing) / scopeEntries.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`;
|
||||
if (!document.getElementById('transFilter')?.value && !document.getElementById('transTypeFilter')?.value) {
|
||||
updateVisibleCountSummary();
|
||||
}
|
||||
}
|
||||
|
||||
function updateVisibleCountSummary(visible = null, visibleMissing = null) {
|
||||
const countEl = document.getElementById('transVisibleCount');
|
||||
if (!countEl) return;
|
||||
const scopeEntries = entriesInScope();
|
||||
const text = document.getElementById('transFilter')?.value || '';
|
||||
const type = document.getElementById('transTypeFilter')?.value || '';
|
||||
if (text || type || showMode !== 'missing') {
|
||||
if (visible != null) {
|
||||
countEl.textContent = `Showing ${visible}${visibleMissing ? ` (${visibleMissing} missing)` : ''}`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const missing = countMissing(scopeEntries, selectedLang);
|
||||
countEl.textContent = `${missing ? `${missing} missing · ` : ''}${scopeEntries.length} in scope`;
|
||||
}
|
||||
|
||||
let filtersBound = false;
|
||||
|
||||
function bindFilters() {
|
||||
@ -364,8 +576,11 @@ function applyFilters() {
|
||||
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) &&
|
||||
const isMissing = row.dataset.missing === '1';
|
||||
let show = (!type || rowType === type) &&
|
||||
(!text || key.includes(text) || label.includes(text) || german.includes(text) || val.includes(text));
|
||||
if (showMode === 'missing' && !isMissing) show = false;
|
||||
if (showMode === 'complete' && isMissing) show = false;
|
||||
row.classList.toggle('trans-row-hidden', !show);
|
||||
if (show) {
|
||||
visible++;
|
||||
@ -373,26 +588,33 @@ function applyFilters() {
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('#transGroupedRoot .trans-group').forEach(group => {
|
||||
document.querySelectorAll('#transGroupedRoot .trans-group-details').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 => {
|
||||
document.querySelectorAll('#transGroupedRoot .trans-scope-block, #transGroupedRoot .trans-qn-section').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`;
|
||||
const emptyEl = document.getElementById('transEmptyHint');
|
||||
if (emptyEl) emptyEl.remove();
|
||||
if (visible === 0 && items.length > 0) {
|
||||
const root = document.getElementById('transGroupedRoot');
|
||||
if (root) {
|
||||
const hint = document.createElement('p');
|
||||
hint.id = 'transEmptyHint';
|
||||
hint.className = 'text-muted trans-hint trans-empty-hint';
|
||||
hint.textContent = showMode === 'missing'
|
||||
? 'All translations in this scope are complete for the selected language.'
|
||||
: 'No rows match the current filters.';
|
||||
root.insertAdjacentElement('afterend', hint);
|
||||
}
|
||||
}
|
||||
|
||||
updateVisibleCountSummary(visible, visibleMissing);
|
||||
}
|
||||
|
||||
function bindTranslationBundleActions() {
|
||||
|
||||
Reference in New Issue
Block a user