/** German (de) is the canonical source language for questionnaire content. */ export const SOURCE_LANG = 'de'; export function normalizeTranslations(raw) { if (!raw) return {}; if (Array.isArray(raw)) { const map = {}; raw.forEach(row => { if (row && row.languageCode) map[row.languageCode] = row.text ?? ''; }); return map; } return typeof raw === 'object' ? { ...raw } : {}; } export function normalizeEntry(entry) { const translations = normalizeTranslations(entry.translations); const germanText = (entry.germanText || translations[SOURCE_LANG] || entry.key || '').trim() || entry.key || ''; return { ...entry, translations, germanText }; } export function germanFor(entry) { return entry.germanText || normalizeTranslations(entry.translations)[SOURCE_LANG] || entry.key || ''; } const HEX_ID_RE = /^[a-f0-9]{32}$/i; /** Key column label (short id or readable preview — not the app lookup key). */ export function entryDisplayKey(entry) { const dk = entry.displayKey || ''; if (dk && !HEX_ID_RE.test(dk)) return dk; if (entry.type === 'question' || entry.type === 'answer_option') { const key = (entry.key || '').trim(); if (key) { return key.length > 56 ? `${key.slice(0, 56)}…` : key; } } if (entry.type === 'question' || entry.type === 'answer_option') { const id = entry.entityId || ''; const sep = id.lastIndexOf('__'); if (sep >= 0) return id.slice(sep + 2); } return dk || entry.key || ''; } export function translationFor(entry, lang) { return normalizeTranslations(entry.translations)[lang] || ''; } export function targetLanguages(languages) { return (languages || []).filter(l => l.languageCode !== SOURCE_LANG); } /** Sort rows within a group: missing translations first, then questionnaire order */ export function sortGroupItems(items, targetLang) { return [...items].sort((a, b) => { const aMissing = !translationFor(a.entry, targetLang).trim(); const bMissing = !translationFor(b.entry, targetLang).trim(); if (aMissing !== bMissing) return aMissing ? -1 : 1; const orderA = a.entry.sortOrder ?? 0; const orderB = b.entry.sortOrder ?? 0; if (orderA !== orderB) return orderA - orderB; const typeCmp = (a.entry.type === 'question' ? 0 : 1) - (b.entry.type === 'question' ? 0 : 1); if (typeCmp !== 0) return typeCmp; return germanFor(a.entry).localeCompare(germanFor(b.entry), 'de'); }); } /** * Groups for one questionnaire: UI strings, then questions + options (interleaved). * @param {number} indexOffset - global index for save handlers */ export function buildTranslationGroups(entries, targetLang, indexOffset = 0) { const items = entries.map((entry, i) => ({ entry, idx: indexOffset + i })); 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 (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(qStrings, targetLang), }); } if (content.length) { groups.push({ id: 'content', title: 'Questions', items: sortGroupItems(content, targetLang), }); } return groups; } /** Render grouped sections (optional questionnaire wrapper title). */ export function renderGroupedTranslationsHTML(groups, targetLang, sourceLabel, editable, options = {}) { const { showColumnHeader = true, questionnaireTitle = null } = options; if (!groups.length) return ''; const header = showColumnHeader ? translationListHeaderHTML( targetLang, sourceLabel ) : ''; const qnHeader = questionnaireTitle ? `