(function () { 'use strict'; const template = `

Integration

Salesforce Translation Import

Clientseitiges Werkzeug fuer Salesforce-XLIFF-Dateien und Zieluebersetzungen aus Excel oder CSV. Der Export erzeugt pro Sprache eine separate XLIFF-Datei im ZIP.

Import

No Salesforce XLIFF export files selected.
No target translation file selected.

Preview

File Language Object Field Current target Result target
Noch keine Vorschau erstellt.
`; function createEmptyTargetStats() { return { totalRows: 0, withoutTarget: 0, sameFieldAndTarget: 0, incomplete: 0, duplicates: 0, usableUniqueRows: 0 }; } function normalizeHeader(value) { return String(value || '').trim().toLowerCase(); } function normalizeLanguage(value) { return String(value || '').trim().replaceAll('_', '-').toLowerCase(); } function parseXml(xmlText) { const xml = new DOMParser().parseFromString(xmlText, 'text/xml'); const parserError = xml.getElementsByTagName('parsererror')[0]; if (parserError) { throw new Error(parserError.textContent || 'Invalid XML file'); } return xml; } function getTargetLanguage(xml) { const fileNode = xml.getElementsByTagName('file')[0]; const rawLanguage = fileNode ? fileNode.getAttribute('target-language') || '' : ''; return { raw: rawLanguage.trim(), normalized: normalizeLanguage(rawLanguage) }; } function findHeaderIndex(headers, aliases) { return headers.findIndex((header) => aliases.includes(header)); } function getUnitDetails(unit) { const id = unit.getAttribute('id'); if (!id) { return null; } const parts = id.split('.'); if (parts.length < 3) { return null; } return { objectName: parts[1].trim(), fieldName: parts.slice(2).join('.').trim() }; } function removeUnit(unit) { const whitespaceBefore = unit.previousSibling; if (whitespaceBefore && whitespaceBefore.nodeType === 3 && /^\s*$/.test(whitespaceBefore.nodeValue)) { whitespaceBefore.remove(); } unit.remove(); } function csvEscape(value) { const text = String(value ?? ''); return '"' + text.replaceAll('"', '""') + '"'; } function escapeHtml(value) { return String(value) .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } function buildApp(host, options) { host.innerHTML = template; const state = { translationsByLanguage: new Map(), sourceFiles: [], sourceLoadPromise: Promise.resolve(), previewChanges: [], targetFileName: '', targetRowCount: 0, targetHasLanguageColumn: false, targetStats: createEmptyTargetStats(), selectedGenericSourceId: '', sourceSequence: 0 }; const xmlInput = host.querySelector('#salesforce-translation-import-xml-files'); const targetInput = host.querySelector('#salesforce-translation-import-target-file'); const genericSourceSelect = host.querySelector('#salesforce-translation-import-generic-source-select'); const analyzeBtn = host.querySelector('#salesforce-translation-import-analyze-btn'); const exportBtn = host.querySelector('#salesforce-translation-import-export-btn'); const extractBtn = host.querySelector('#salesforce-translation-import-extract-btn'); const sourceStatus = host.querySelector('#salesforce-translation-import-source-status'); const targetStatus = host.querySelector('#salesforce-translation-import-target-status'); const statsNode = host.querySelector('#salesforce-translation-import-stats'); const previewBody = host.querySelector('#salesforce-translation-import-preview-body'); const objectSelect = host.querySelector('#salesforce-translation-import-object-select'); const genericSourceSelection = host.querySelector('#salesforce-translation-import-generic-source-selection'); function appendMetric(container, value, label, className) { const metric = document.createElement('div'); metric.className = className || 'salesforce-translation-import__muted'; metric.textContent = value + ' ' + label; container.appendChild(metric); } function resetObjectSelect() { objectSelect.innerHTML = ''; } function populateObjectSelect(objects) { resetObjectSelect(); [...objects].sort().forEach((objectName) => { const option = document.createElement('option'); option.value = objectName; option.textContent = objectName; objectSelect.appendChild(option); }); } function getTranslationsForSource(source) { if (source.error) { return null; } if (source.language && state.translationsByLanguage.has(source.language)) { return { translations: state.translationsByLanguage.get(source.language), label: source.language, languageKey: source.language }; } if ( !state.targetHasLanguageColumn && (state.sourceFiles.filter((entry) => !entry.error).length === 1 || source.id === state.selectedGenericSourceId) && state.translationsByLanguage.has('') ) { return { translations: state.translationsByLanguage.get(''), label: 'single-source mode', languageKey: '' }; } return null; } function appendStatusRow(container, source) { const row = document.createElement('div'); row.className = 'salesforce-translation-import__status-row'; const fileName = document.createElement('span'); fileName.className = 'salesforce-translation-import__file-name'; fileName.textContent = source.displayName || source.name; row.appendChild(fileName); if (source.error) { const error = document.createElement('span'); error.className = 'salesforce-translation-import__match-missing'; error.textContent = 'Invalid XLIFF: ' + source.error; row.appendChild(error); container.appendChild(row); return; } const language = document.createElement('span'); language.className = 'salesforce-translation-import__language-code'; language.textContent = source.languageLabel; row.appendChild(language); const unitCount = document.createElement('span'); unitCount.className = 'salesforce-translation-import__muted'; unitCount.textContent = source.unitCount + ' translation units'; row.appendChild(unitCount); const match = getTranslationsForSource(source); const matchText = document.createElement('span'); if (match) { matchText.className = 'salesforce-translation-import__match-ok'; matchText.textContent = 'Matched: ' + match.label; } else if (state.targetFileName && !state.targetHasLanguageColumn && state.sourceFiles.filter((entry) => !entry.error).length > 1) { matchText.className = 'salesforce-translation-import__muted'; matchText.textContent = 'Not selected for this target translation'; } else if (state.targetFileName) { matchText.className = 'salesforce-translation-import__match-missing'; matchText.textContent = 'No matching target translations'; } else { matchText.className = 'salesforce-translation-import__muted'; matchText.textContent = 'Waiting for target translation'; } row.appendChild(matchText); container.appendChild(row); } function renderGenericSourceSelection() { const validSources = state.sourceFiles.filter((entry) => !entry.error); const selectionRequired = Boolean(state.targetFileName) && !state.targetHasLanguageColumn && validSources.length > 1; genericSourceSelection.hidden = !selectionRequired; if (!selectionRequired) { state.selectedGenericSourceId = validSources.length === 1 ? validSources[0].id : ''; return; } const previousSelection = state.selectedGenericSourceId; genericSourceSelect.innerHTML = ''; validSources.forEach((source) => { const option = document.createElement('option'); option.value = source.id; option.textContent = (source.displayName || source.name) + ' [' + source.languageLabel + ']'; genericSourceSelect.appendChild(option); }); if (validSources.some((source) => source.id === previousSelection)) { genericSourceSelect.value = previousSelection; } else { state.selectedGenericSourceId = ''; genericSourceSelect.value = ''; } } function renderFileStatus() { renderGenericSourceSelection(); sourceStatus.innerHTML = ''; if (state.sourceFiles.length === 0) { sourceStatus.textContent = 'No Salesforce XLIFF export files selected.'; sourceStatus.className = 'salesforce-translation-import__status salesforce-translation-import__status is-muted'; } else { sourceStatus.className = 'salesforce-translation-import__status'; state.sourceFiles.forEach((source) => appendStatusRow(sourceStatus, source)); } targetStatus.innerHTML = ''; if (!state.targetFileName) { targetStatus.textContent = 'No target translation file selected.'; targetStatus.className = 'salesforce-translation-import__status salesforce-translation-import__status is-muted'; return; } targetStatus.className = 'salesforce-translation-import__status'; const summary = document.createElement('div'); summary.className = 'salesforce-translation-import__status-row'; const fileName = document.createElement('span'); fileName.className = 'salesforce-translation-import__file-name'; fileName.textContent = state.targetFileName; summary.appendChild(fileName); const details = document.createElement('span'); const languages = [...state.translationsByLanguage.keys()].filter(Boolean).sort(); details.textContent = state.targetHasLanguageColumn ? 'Languages: ' + (languages.join(', ') || 'none') : 'No language column (single-source mode)'; summary.appendChild(details); targetStatus.appendChild(summary); appendMetric(targetStatus, state.targetStats.totalRows, 'rows in target translation file'); appendMetric(targetStatus, state.targetStats.withoutTarget, 'rows without a target value'); appendMetric(targetStatus, state.targetStats.sameFieldAndTarget, 'rows where field and target are identical'); appendMetric(targetStatus, state.targetStats.incomplete, 'incomplete rows'); appendMetric(targetStatus, state.targetStats.duplicates, 'duplicate object/field rows'); appendMetric(targetStatus, state.targetStats.usableUniqueRows, 'usable unique target rows', 'salesforce-translation-import__match-ok'); } function renderPreview() { previewBody.innerHTML = ''; if (state.previewChanges.length === 0) { const emptyRow = document.createElement('tr'); emptyRow.innerHTML = 'Noch keine Vorschau erstellt.'; previewBody.appendChild(emptyRow); return; } state.previewChanges.forEach((change) => { const tr = document.createElement('tr'); [ change.file, change.language, change.object, change.field, change.oldValue, change.newValue ].forEach((value) => { const td = document.createElement('td'); td.textContent = value; tr.appendChild(td); }); previewBody.appendChild(tr); }); } function collectExtractionRows() { const rows = []; state.sourceFiles .filter((source) => !source.error) .forEach((source) => { const xml = parseXml(source.text); const units = Array.from(xml.getElementsByTagName('trans-unit')); units.forEach((unit) => { const details = getUnitDetails(unit); if (!details) { return; } const sourceNode = unit.getElementsByTagName('source')[0]; const targetNode = unit.getElementsByTagName('target')[0]; rows.push({ object: details.objectName, field: details.fieldName, target: targetNode ? targetNode.textContent ?? '' : '', language: source.languageLabel, source: sourceNode ? sourceNode.textContent ?? '' : '', file: source.name }); }); }); return rows; } function downloadCsv(filename, rows) { const header = ['object', 'field', 'target', 'language', 'source', 'file']; const lines = [ header.map(csvEscape).join(','), ...rows.map((row) => [ row.object, row.field, row.target, row.language, row.source, row.file ].map(csvEscape).join(',')) ]; const blob = new Blob(["\uFEFF" + lines.join('\n')], { type: 'text/csv;charset=utf-8' }); const link = document.createElement('a'); const downloadUrl = URL.createObjectURL(blob); link.href = downloadUrl; link.download = filename; link.click(); window.setTimeout(() => URL.revokeObjectURL(downloadUrl), 1000); } function genericSourceSelectionIsMissing() { return Boolean(state.targetFileName) && !state.targetHasLanguageColumn && state.sourceFiles.filter((entry) => !entry.error).length > 1 && !state.selectedGenericSourceId; } async function addSourceFile(name, text, displayName) { try { const xml = parseXml(text); const language = getTargetLanguage(xml); state.sourceFiles.push({ id: 'source-' + (++state.sourceSequence), name: name, displayName: displayName || name, text: text, language: language.normalized, languageLabel: language.raw || 'Not specified', unitCount: xml.getElementsByTagName('trans-unit').length }); } catch (error) { state.sourceFiles.push({ name: name, displayName: displayName || name, error: error.message }); } } async function loadSourceZip(file) { try { const archive = await window.JSZip.loadAsync(await file.arrayBuffer()); const entries = Object.values(archive.files).filter((entry) => !entry.dir && /\.(xml|xlf|xliff)$/i.test(entry.name)); if (entries.length === 0) { state.sourceFiles.push({ name: file.name, error: 'The ZIP does not contain any XML or XLIFF files.' }); return; } for (const entry of entries) { await addSourceFile(entry.name, await entry.async('string'), file.name + ' / ' + entry.name); } } catch (error) { state.sourceFiles.push({ name: file.name, error: 'The ZIP could not be read: ' + error.message }); } } async function loadSourceFiles() { state.sourceFiles = []; state.selectedGenericSourceId = ''; state.sourceSequence = 0; state.previewChanges = []; renderPreview(); statsNode.textContent = ''; for (const file of xmlInput.files) { if (file.name.toLowerCase().endsWith('.zip')) { await loadSourceZip(file); } else { await addSourceFile(file.name, await file.text()); } } renderFileStatus(); } async function loadTargetTranslations(event) { const file = event.target.files[0]; state.translationsByLanguage.clear(); state.targetFileName = ''; state.targetRowCount = 0; state.targetHasLanguageColumn = false; state.targetStats = createEmptyTargetStats(); resetObjectSelect(); if (!file) { renderFileStatus(); return; } try { const workbook = window.XLSX.read(await file.arrayBuffer()); const sheet = workbook.Sheets[workbook.SheetNames[0]]; const data = window.XLSX.utils.sheet_to_json(sheet, { header: 1, defval: '' }); if (data.length === 0) { throw new Error('The target translation file is empty.'); } const headers = data[0].map(normalizeHeader); const objectIndex = findHeaderIndex(headers, ['object', 'objekt']); const fieldIndex = findHeaderIndex(headers, ['field', 'feld']); const targetIndex = findHeaderIndex(headers, ['target']); const languageIndex = findHeaderIndex(headers, ['language', 'sprache']); if (objectIndex < 0 || fieldIndex < 0 || targetIndex < 0) { throw new Error('Required columns are missing. Use object, field, and target.'); } state.targetHasLanguageColumn = languageIndex >= 0; const objects = new Set(); const targetRows = data.slice(1).filter((row) => row.some((value) => String(value ?? '').trim() !== '')); state.targetStats.totalRows = targetRows.length; for (const row of targetRows) { const objectName = String(row[objectIndex] ?? '').trim(); const fieldName = String(row[fieldIndex] ?? '').trim(); const targetValue = String(row[targetIndex] ?? '').trim(); const language = state.targetHasLanguageColumn ? normalizeLanguage(row[languageIndex]) : ''; if (!objectName || !fieldName || (state.targetHasLanguageColumn && !language)) { state.targetStats.incomplete++; continue; } if (!targetValue) { state.targetStats.withoutTarget++; continue; } if (fieldName === targetValue) { state.targetStats.sameFieldAndTarget++; continue; } if (!state.translationsByLanguage.has(language)) { state.translationsByLanguage.set(language, new Map()); } const key = objectName + '|' + fieldName; const languageTranslations = state.translationsByLanguage.get(language); if (languageTranslations.has(key)) { state.targetStats.duplicates++; } languageTranslations.set(key, targetValue); objects.add(objectName); } state.targetRowCount = [...state.translationsByLanguage.values()].reduce((total, entries) => total + entries.size, 0); state.targetStats.usableUniqueRows = state.targetRowCount; state.targetFileName = file.name; populateObjectSelect(objects); renderFileStatus(); } catch (error) { state.translationsByLanguage.clear(); targetInput.value = ''; renderFileStatus(); window.alert(error.message); } } async function analyze() { await state.sourceLoadPromise; if (state.sourceFiles.filter((entry) => !entry.error).length === 0) { window.alert('Select at least one valid Salesforce XLIFF source file.'); return; } if (state.translationsByLanguage.size === 0) { window.alert('Select a valid target translation file.'); return; } if (genericSourceSelectionIsMissing()) { window.alert('Select the target XLIFF file for this target translation.'); return; } const matchedSources = state.sourceFiles.filter(getTranslationsForSource); if (matchedSources.length === 0) { window.alert('No Salesforce XLIFF source language matches the target translation data.'); return; } state.previewChanges = []; const selectedObject = objectSelect.value; const allTargetRows = new Set(); const rowsFoundInSource = new Set(); const rowsSameAsSource = new Set(); const rowsSameAsTarget = new Set(); const rowsToCreate = new Set(); const rowsToReplace = new Set(); state.translationsByLanguage.forEach((entries, language) => { entries.forEach((value, key) => { allTargetRows.add(language + '|' + key); }); }); for (const source of matchedSources) { const xml = parseXml(source.text); const match = getTranslationsForSource(source); const units = xml.getElementsByTagName('trans-unit'); for (const unit of units) { const details = getUnitDetails(unit); if (!details) { continue; } const key = details.objectName + '|' + details.fieldName; if (!match.translations.has(key)) { continue; } const rowToken = match.languageKey + '|' + key; rowsFoundInSource.add(rowToken); const targetNode = unit.getElementsByTagName('target')[0]; const sourceNode = unit.getElementsByTagName('source')[0]; const oldValue = targetNode ? targetNode.textContent : ''; const sourceValue = sourceNode ? sourceNode.textContent : ''; const newValue = match.translations.get(key); if (sourceValue.trim() === newValue) { rowsSameAsSource.add(rowToken); continue; } if (targetNode && oldValue.trim() === newValue) { rowsSameAsTarget.add(rowToken); continue; } if (targetNode) { rowsToReplace.add(rowToken); } else { rowsToCreate.add(rowToken); } if (selectedObject && details.objectName !== selectedObject) { continue; } state.previewChanges.push({ file: source.name, language: source.languageLabel, object: details.objectName, field: details.fieldName, oldValue: oldValue, newValue: newValue }); } } const rowsNotInSource = [...allTargetRows].filter((rowToken) => !rowsFoundInSource.has(rowToken)).length; const translationsToApply = new Set([...rowsToCreate, ...rowsToReplace]).size; renderPreview(); statsNode.innerHTML = 'Salesforce source comparison
' + escapeHtml(rowsFoundInSource.size) + ' target rows found in matched source files
' + escapeHtml(rowsNotInSource) + ' target rows not present in matched source files
' + escapeHtml(rowsSameAsSource.size) + ' target rows identical to the XLIFF source value
' + escapeHtml(rowsSameAsTarget.size) + ' target rows identical to the current XLIFF target value
' + escapeHtml(translationsToApply) + ' translations to apply
' + escapeHtml(rowsToReplace.size) + ' existing targets to replace
' + escapeHtml(rowsToCreate.size) + ' new targets to create'; } async function exportZip() { await state.sourceLoadPromise; if (state.sourceFiles.filter((entry) => !entry.error).length === 0) { window.alert('Select at least one valid Salesforce XLIFF source file.'); return; } if (state.translationsByLanguage.size === 0) { window.alert('Select a valid target translation file.'); return; } if (genericSourceSelectionIsMissing()) { window.alert('Select the target XLIFF file for this target translation.'); return; } const matchedSources = state.sourceFiles.filter(getTranslationsForSource); if (matchedSources.length === 0) { window.alert('No Salesforce XLIFF source language matches the target translation data.'); return; } const zip = new window.JSZip(); let exportedFileCount = 0; for (const source of matchedSources) { const xml = parseXml(source.text); const match = getTranslationsForSource(source); const units = Array.from(xml.getElementsByTagName('trans-unit')); let appliedTranslationCount = 0; for (const unit of units) { const details = getUnitDetails(unit); const key = details ? details.objectName + '|' + details.fieldName : ''; if (!details || !match.translations.has(key)) { removeUnit(unit); continue; } const newValue = match.translations.get(key); let targetNode = unit.getElementsByTagName('target')[0]; const sourceNode = unit.getElementsByTagName('source')[0]; const sourceValue = sourceNode ? sourceNode.textContent : ''; if (sourceValue.trim() === newValue || (targetNode && targetNode.textContent.trim() === newValue)) { removeUnit(unit); continue; } if (!targetNode) { targetNode = xml.createElement('target'); unit.appendChild(targetNode); } targetNode.textContent = newValue; appliedTranslationCount++; } if (appliedTranslationCount === 0) { continue; } zip.file(source.name, new XMLSerializer().serializeToString(xml)); exportedFileCount++; } if (exportedFileCount === 0) { window.alert('No matching translations were found for the selected Salesforce XLIFF export files.'); return; } const blob = await zip.generateAsync({ type: 'blob' }); const link = document.createElement('a'); const downloadUrl = URL.createObjectURL(blob); link.href = downloadUrl; link.download = 'salesforce_translated_xliff.zip'; link.click(); window.setTimeout(() => URL.revokeObjectURL(downloadUrl), 1000); } async function extractCsv() { await state.sourceLoadPromise; if (state.sourceFiles.filter((entry) => !entry.error).length === 0) { window.alert('Select at least one valid Salesforce XLIFF source file.'); return; } const rows = collectExtractionRows(); if (rows.length === 0) { window.alert('No translatable Salesforce rows were found in the selected XLIFF files.'); return; } downloadCsv('salesforce_translation_template.csv', rows); } xmlInput.addEventListener('change', () => { state.sourceLoadPromise = loadSourceFiles(); }); targetInput.addEventListener('change', loadTargetTranslations); genericSourceSelect.addEventListener('change', (event) => { state.selectedGenericSourceId = event.target.value; renderFileStatus(); }); analyzeBtn.addEventListener('click', analyze); exportBtn.addEventListener('click', exportZip); extractBtn.addEventListener('click', extractCsv); renderFileStatus(); renderPreview(); if (options && options.standalone) { document.body.classList.add('salesforce-translation-import-page'); } } window.initSalesforceTranslationImportApp = function initSalesforceTranslationImportApp(host, options) { if (!(host instanceof HTMLElement)) { return; } if (!window.XLSX || !window.JSZip) { host.innerHTML = '

Abhaengigkeiten fehlen

Die benoetigten Bibliotheken fuer Excel- und ZIP-Verarbeitung konnten nicht geladen werden.

'; return; } buildApp(host, options || {}); }; }());