added app translations for better maintainability
This commit is contained in:
@ -1085,6 +1085,7 @@ th.sort-desc::after { content: ' \2193'; opacity: 1; }
|
||||
}
|
||||
.badge-q { background: #dbeafe; color: #1e40af; }
|
||||
.badge-opt { background: #e2e8f0; color: #475569; }
|
||||
.badge-app { background: #d1fae5; color: #065f46; }
|
||||
.badge-str { background: #ede9fe; color: #5b21b6; }
|
||||
|
||||
.cell-trans { position: relative; }
|
||||
|
||||
@ -961,10 +961,10 @@ function renderTranslationsTab() {
|
||||
if (!container || !transData) return;
|
||||
const editable = canEdit();
|
||||
const languages = transData.languages || [];
|
||||
const entries = transData.entries || [];
|
||||
const qnEntries = transData.entries || [];
|
||||
const targets = targetLanguages(languages);
|
||||
|
||||
if (!entries.length) {
|
||||
if (!qnEntries.length) {
|
||||
container.innerHTML = `
|
||||
${editable ? languageManagerHTML(languages) : ''}
|
||||
<p style="color:var(--text-secondary);margin-top:12px">No translatable content yet. Add questions with German text first.</p>
|
||||
@ -992,7 +992,7 @@ function renderTranslationsTab() {
|
||||
container.innerHTML = `
|
||||
${editable ? languageManagerHTML(languages) : ''}
|
||||
${langSelectHtml}
|
||||
${targets.length && editorTransLang ? renderEditorTransList(entries, editable) : ''}
|
||||
${targets.length && editorTransLang ? renderEditorTransList(qnEntries, editable) : ''}
|
||||
`;
|
||||
|
||||
if (editable) bindLanguageManager(languages);
|
||||
@ -1003,18 +1003,25 @@ function renderTranslationsTab() {
|
||||
});
|
||||
|
||||
if (targets.length && editorTransLang) {
|
||||
bindEditorTransEditing(entries, editable);
|
||||
bindEditorTransFiltering(entries);
|
||||
bindEditorTransEditing(qnEntries, editable);
|
||||
bindEditorTransFiltering(qnEntries);
|
||||
}
|
||||
}
|
||||
|
||||
function renderEditorTransList(entries, editable) {
|
||||
function renderEditorTransList(qnEntries, editable) {
|
||||
const targets = targetLanguages(transData.languages || []);
|
||||
const langLabel = targets.find(l => l.languageCode === editorTransLang)?.name || editorTransLang.toUpperCase();
|
||||
const sourceLabel = sourceLanguageLabel(transData.languages || []);
|
||||
const groups = buildTranslationGroups(entries, editorTransLang, 0);
|
||||
const missingCount = entries.filter(e => !translationFor(e, editorTransLang).trim()).length;
|
||||
const pct = entries.length ? Math.round(((entries.length - missingCount) / entries.length) * 100) : 0;
|
||||
const missingCount = qnEntries.filter(e => !translationFor(e, editorTransLang).trim()).length;
|
||||
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>`;
|
||||
|
||||
return `
|
||||
<div class="trans-toolbar">
|
||||
@ -1027,11 +1034,9 @@ function renderEditorTransList(entries, editable) {
|
||||
</select>
|
||||
</div>
|
||||
${translationListHeaderHTML(langLabel, sourceLabel)}
|
||||
<div id="transGroupedRoot">
|
||||
${renderGroupedTranslationsHTML(groups, editorTransLang, sourceLabel, editable, { showColumnHeader: false })}
|
||||
</div>
|
||||
<div id="transGroupedRoot">${bodyHtml}</div>
|
||||
<div class="trans-stats">
|
||||
<span id="transStats">${missingCount ? `${missingCount} missing · ` : ''}${entries.length} keys</span>
|
||||
<span id="transStats">${missingCount ? `${missingCount} missing · ` : ''}${qnEntries.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 ${escHtml(editorTransLang.toUpperCase())}</span>
|
||||
@ -1114,6 +1119,10 @@ function bindEditorTransFiltering(entries) {
|
||||
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 => {
|
||||
const anyVisible = [...block.querySelectorAll('.trans-list-item')].some(r => !r.classList.contains('trans-row-hidden'));
|
||||
block.classList.toggle('trans-qn-hidden', !anyVisible);
|
||||
});
|
||||
const stats = document.getElementById('transStats');
|
||||
if (stats) {
|
||||
stats.textContent = text || type
|
||||
|
||||
@ -57,7 +57,8 @@ function renderShell() {
|
||||
<input type="search" id="transFilter" class="trans-search-input" placeholder="Search…" disabled>
|
||||
<select id="transTypeFilter" class="trans-select trans-type-select" disabled>
|
||||
<option value="">All types</option>
|
||||
<option value="string">UI strings</option>
|
||||
<option value="app_string">App strings</option>
|
||||
<option value="string">Questionnaire UI strings</option>
|
||||
<option value="question">Questions</option>
|
||||
<option value="answer_option">Answer options</option>
|
||||
</select>
|
||||
@ -99,7 +100,7 @@ async function loadTranslations() {
|
||||
const languages = transData.languages || [];
|
||||
const questionnaires = transData.questionnaires || [];
|
||||
|
||||
const flat = flattenAllQuestionnaires(questionnaires);
|
||||
const flat = flattenAllQuestionnaires(questionnaires, data.appStrings || []);
|
||||
allEntries = flat.allEntries;
|
||||
transData.sections = flat.sections;
|
||||
|
||||
@ -234,7 +235,10 @@ function renderEntryList() {
|
||||
const headerOnce = translationListHeaderHTML(langLabel, sourceLabel);
|
||||
|
||||
const sectionsHtml = sections.map(sec => {
|
||||
const entries = allEntries.slice(sec.startIdx, sec.startIdx + sec.entryCount);
|
||||
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 `
|
||||
<div class="trans-qn-block" data-qn="${esc(sec.questionnaireID)}">
|
||||
|
||||
@ -57,15 +57,25 @@ export function sortGroupItems(items, targetLang) {
|
||||
*/
|
||||
export function buildTranslationGroups(entries, targetLang, indexOffset = 0) {
|
||||
const items = entries.map((entry, i) => ({ entry, idx: indexOffset + i }));
|
||||
const strings = items.filter(({ entry }) => entry.type === 'string');
|
||||
const content = items.filter(({ entry }) => entry.type !== 'string');
|
||||
const appStrings = items.filter(({ entry }) => entry.type === 'app_string');
|
||||
const qStrings = items.filter(({ entry }) => entry.type === 'string');
|
||||
const content = items.filter(({ entry }) =>
|
||||
entry.type !== 'string' && entry.type !== 'app_string'
|
||||
);
|
||||
const groups = [];
|
||||
|
||||
if (strings.length) {
|
||||
if (appStrings.length) {
|
||||
groups.push({
|
||||
id: 'app_strings',
|
||||
title: 'App strings',
|
||||
items: sortGroupItems(appStrings, targetLang),
|
||||
});
|
||||
}
|
||||
if (qStrings.length) {
|
||||
groups.push({
|
||||
id: 'strings',
|
||||
title: 'UI strings',
|
||||
items: sortGroupItems(strings, targetLang),
|
||||
items: sortGroupItems(qStrings, targetLang),
|
||||
});
|
||||
}
|
||||
if (content.length) {
|
||||
@ -107,11 +117,29 @@ export function renderGroupedTranslationsHTML(groups, targetLang, sourceLabel, e
|
||||
return `<div class="${wrapClass}">${qnHeader}${header}${body}</div>`;
|
||||
}
|
||||
|
||||
/** Flatten questionnaires from ?all=1 into one entries array + section metadata */
|
||||
export function flattenAllQuestionnaires(questionnaires) {
|
||||
/** Only global app UI rows (never questionnaire questions/options). */
|
||||
export function filterGlobalAppEntries(entries) {
|
||||
return (entries || []).filter(e => e && e.type === 'app_string');
|
||||
}
|
||||
|
||||
/** Flatten app strings + questionnaires from ?all=1 */
|
||||
export function flattenAllQuestionnaires(questionnaires, appStrings = []) {
|
||||
const allEntries = [];
|
||||
const sections = [];
|
||||
|
||||
const appNorm = filterGlobalAppEntries((appStrings || []).map(normalizeEntry));
|
||||
if (appNorm.length) {
|
||||
const startIdx = 0;
|
||||
appNorm.forEach(e => allEntries.push(e));
|
||||
sections.push({
|
||||
questionnaireID: '__app__',
|
||||
name: 'App UI',
|
||||
startIdx,
|
||||
entryCount: appNorm.length,
|
||||
isApp: true,
|
||||
});
|
||||
}
|
||||
|
||||
for (const qn of questionnaires) {
|
||||
const normalized = (qn.entries || []).map(normalizeEntry);
|
||||
if (!normalized.length) continue;
|
||||
@ -122,6 +150,7 @@ export function flattenAllQuestionnaires(questionnaires) {
|
||||
name: qn.name || 'Questionnaire',
|
||||
startIdx,
|
||||
entryCount: normalized.length,
|
||||
isApp: false,
|
||||
});
|
||||
}
|
||||
|
||||
@ -129,8 +158,8 @@ export function flattenAllQuestionnaires(questionnaires) {
|
||||
}
|
||||
|
||||
export function typeBadge(type) {
|
||||
const map = { question: 'Q', answer_option: 'Opt', string: 'Str' };
|
||||
const cls = { question: 'badge-q', answer_option: 'badge-opt', string: 'badge-str' };
|
||||
const map = { question: 'Q', answer_option: 'Opt', string: 'Str', app_string: 'App' };
|
||||
const cls = { question: 'badge-q', answer_option: 'badge-opt', string: 'badge-str', app_string: 'badge-app' };
|
||||
return `<span class="trans-type-badge ${cls[type] || ''}">${map[type] || type}</span>`;
|
||||
}
|
||||
|
||||
@ -165,7 +194,7 @@ export function translationRowHTML(entry, idx, { targetLang, editable = true })
|
||||
const missing = !val.trim();
|
||||
const disabled = editable ? '' : 'disabled';
|
||||
|
||||
const sourceCell = entry.type === 'string' && editable
|
||||
const sourceCell = editable
|
||||
? `<input type="text" class="trans-cell-input" data-idx="${idx}" data-field="german"
|
||||
value="${escHtml(german)}" placeholder="German text…" autocomplete="off">`
|
||||
: `<span class="trans-source-text" title="${escHtml(german)}">${escHtml(german)}</span>`;
|
||||
|
||||
Reference in New Issue
Block a user