From af4886191f614c3a7d0792f2118319b508eb5d32 Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Mon, 22 Jun 2026 22:12:43 +0200 Subject: [PATCH] sdfsdf --- .../assets/css/app.css | 285 +++++ .../assets/js/app.js | 832 ++++++++++++++ .../salesforce-translation-import/desktop.php | 16 +- .../docs/README.md | 6 +- .../pages/index.php | 1005 +---------------- 5 files changed, 1155 insertions(+), 989 deletions(-) create mode 100644 modules/salesforce-translation-import/assets/css/app.css create mode 100644 modules/salesforce-translation-import/assets/js/app.js diff --git a/modules/salesforce-translation-import/assets/css/app.css b/modules/salesforce-translation-import/assets/css/app.css new file mode 100644 index 00000000..9c120fbe --- /dev/null +++ b/modules/salesforce-translation-import/assets/css/app.css @@ -0,0 +1,285 @@ +.salesforce-translation-import, +.salesforce-translation-import-page { + min-height: 100%; + background: + radial-gradient(circle at top left, rgba(96, 165, 250, 0.10), transparent 24%), + linear-gradient(180deg, #f8fafc 0%, #eef2f7 100%); + color: #0f172a; + font-family: "IBM Plex Sans", "Segoe UI", sans-serif; +} + +.salesforce-translation-import-page { + min-height: 100vh; +} + +.salesforce-translation-import { + min-height: 100%; + padding: 24px; +} + +.salesforce-translation-import *, +.salesforce-translation-import *::before, +.salesforce-translation-import *::after { + box-sizing: border-box; +} + +.salesforce-translation-import__shell { + display: flex; + flex-direction: column; + gap: 20px; +} + +.salesforce-translation-import__hero, +.salesforce-translation-import__card { + border: 1px solid rgba(148, 163, 184, 0.24); + border-radius: 28px; + background: rgba(255, 255, 255, 0.92); + box-shadow: 0 24px 60px rgba(15, 23, 42, 0.10); +} + +.salesforce-translation-import__hero { + padding: 24px 28px; +} + +.salesforce-translation-import__eyebrow { + margin: 0 0 12px; + color: #4338ca; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.18em; + text-transform: uppercase; +} + +.salesforce-translation-import__title { + margin: 0; + font-size: clamp(2rem, 4vw, 2.8rem); + line-height: 1.05; +} + +.salesforce-translation-import__lede { + max-width: 70rem; + margin: 14px 0 0; + color: #475569; + font-size: 1rem; + line-height: 1.6; +} + +.salesforce-translation-import__grid { + display: grid; + gap: 20px; + grid-template-columns: minmax(0, 1.2fr) minmax(320px, 0.8fr); +} + +.salesforce-translation-import__main, +.salesforce-translation-import__side { + display: flex; + flex-direction: column; + gap: 20px; + min-width: 0; +} + +.salesforce-translation-import__card { + padding: 22px 24px; +} + +.salesforce-translation-import__card h2 { + margin: 0 0 14px; + font-size: 1.55rem; + line-height: 1.15; +} + +.salesforce-translation-import__card p { + margin: 0 0 12px; + color: #475569; + line-height: 1.55; +} + +.salesforce-translation-import__card ol { + margin: 0; + padding-left: 20px; + color: #334155; + line-height: 1.6; +} + +.salesforce-translation-import__card li + li { + margin-top: 8px; +} + +.salesforce-translation-import__sample { + overflow-x: auto; + margin-top: 14px; + border: 1px solid rgba(148, 163, 184, 0.24); + border-radius: 18px; +} + +.salesforce-translation-import__field { + display: flex; + flex-direction: column; + gap: 8px; +} + +.salesforce-translation-import__field + .salesforce-translation-import__field { + margin-top: 18px; +} + +.salesforce-translation-import__label { + font-weight: 700; + color: #1e293b; +} + +.salesforce-translation-import__input, +.salesforce-translation-import__select { + width: 100%; + min-height: 48px; + padding: 12px 14px; + border: 1px solid rgba(148, 163, 184, 0.45); + border-radius: 16px; + background: #ffffff; + color: #0f172a; + font: inherit; +} + +.salesforce-translation-import__actions { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 20px; +} + +.salesforce-translation-import__button { + min-height: 48px; + padding: 11px 18px; + border: 1px solid rgba(99, 102, 241, 0.22); + border-radius: 999px; + background: #ffffff; + color: #111827; + font: inherit; + font-weight: 700; + cursor: pointer; +} + +.salesforce-translation-import__button:hover { + border-color: rgba(79, 70, 229, 0.4); +} + +.salesforce-translation-import__button:disabled { + opacity: 0.55; + cursor: not-allowed; +} + +.salesforce-translation-import__button--primary { + border-color: transparent; + background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%); + color: #ffffff; +} + +.salesforce-translation-import__status { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 10px; +} + +.salesforce-translation-import__status.is-muted { + color: #64748b; +} + +.salesforce-translation-import__status-row { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; + padding: 6px 0; + border-bottom: 1px solid rgba(226, 232, 240, 0.9); +} + +.salesforce-translation-import__status-row:last-child { + border-bottom: 0; +} + +.salesforce-translation-import__file-name { + font-weight: 700; + overflow-wrap: anywhere; +} + +.salesforce-translation-import__language-code { + padding: 2px 8px; + border: 1px solid rgba(148, 163, 184, 0.65); + border-radius: 999px; + font-family: "IBM Plex Mono", "SFMono-Regular", monospace; + font-size: 0.86rem; +} + +.salesforce-translation-import__match-ok { + color: #166534; + font-weight: 700; +} + +.salesforce-translation-import__match-missing { + color: #b91c1c; + font-weight: 700; +} + +.salesforce-translation-import__muted { + color: #64748b; +} + +.salesforce-translation-import__stats { + color: #334155; + line-height: 1.65; +} + +.salesforce-translation-import__preview { + overflow: auto; + border: 1px solid rgba(148, 163, 184, 0.24); + border-radius: 18px; + background: #ffffff; +} + +.salesforce-translation-import table { + width: 100%; + border-collapse: collapse; +} + +.salesforce-translation-import th, +.salesforce-translation-import td { + padding: 12px 14px; + border-bottom: 1px solid rgba(226, 232, 240, 0.9); + vertical-align: top; + text-align: left; +} + +.salesforce-translation-import thead th { + position: sticky; + top: 0; + background: #f8fafc; + color: #334155; + z-index: 1; +} + +.salesforce-translation-import tbody tr:nth-child(even) { + background: rgba(248, 250, 252, 0.75); +} + +.salesforce-translation-import__preview-empty { + padding: 20px 14px; + color: #64748b; +} + +@media (max-width: 1100px) { + .salesforce-translation-import__grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 720px) { + .salesforce-translation-import { + padding: 16px; + } + + .salesforce-translation-import__hero, + .salesforce-translation-import__card { + padding: 18px; + border-radius: 22px; + } +} diff --git a/modules/salesforce-translation-import/assets/js/app.js b/modules/salesforce-translation-import/assets/js/app.js new file mode 100644 index 00000000..69f8ea2b --- /dev/null +++ b/modules/salesforce-translation-import/assets/js/app.js @@ -0,0 +1,832 @@ +(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

+
+
+ + + + + + + + + + + + + + + + +
FileLanguageObjectFieldCurrent targetResult 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 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 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 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); + } + + 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); + + 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 || {}); + }; +}()); diff --git a/modules/salesforce-translation-import/desktop.php b/modules/salesforce-translation-import/desktop.php index e4e4b837..c13ea572 100644 --- a/modules/salesforce-translation-import/desktop.php +++ b/modules/salesforce-translation-import/desktop.php @@ -15,11 +15,25 @@ return [ 'icon' => 'SF', 'entry_route' => '/apps/salesforce-translation-import', 'window_mode' => 'window', - 'content_mode' => 'iframe', + 'content_mode' => 'native-module', 'default_width' => 1180, 'default_height' => 820, 'supports_widget' => false, 'supports_tray' => false, 'module_name' => 'integration', 'summary' => (string) ($manifest['description'] ?? 'Import-Werkzeug fuer Salesforce-XLIFF-Dateien und Zieluebersetzungen aus Excel oder CSV.'), + 'native_module' => [ + 'initializer' => 'initSalesforceTranslationImportApp', + 'options' => [], + 'assets' => [ + 'styles' => [ + ['href' => '/module-assets/index.php?module=salesforce-translation-import&path=assets/css/app.css&v=' . rawurlencode((string) (is_file(__DIR__ . '/assets/css/app.css') ? filemtime(__DIR__ . '/assets/css/app.css') : '0'))], + ], + 'scripts' => [ + ['src' => 'https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js'], + ['src' => 'https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js'], + ['src' => '/module-assets/index.php?module=salesforce-translation-import&path=assets/js/app.js&v=' . rawurlencode((string) (is_file(__DIR__ . '/assets/js/app.js') ? filemtime(__DIR__ . '/assets/js/app.js') : '0'))], + ], + ], + ], ]; diff --git a/modules/salesforce-translation-import/docs/README.md b/modules/salesforce-translation-import/docs/README.md index 78f077af..9bea346a 100644 --- a/modules/salesforce-translation-import/docs/README.md +++ b/modules/salesforce-translation-import/docs/README.md @@ -6,9 +6,9 @@ Desktop-App fuer den Import von Salesforce-XLIFF-Dateien mit Zieluebersetzungen - Desktop-App ueber `modules/salesforce-translation-import/desktop.php` - oeffentlicher App-Einstieg ueber `public/apps/salesforce-translation-import/index.php` -- Darstellung als isolierte Fensterseite per `iframe`, damit die Import-Logik und die Formularstruktur stabil bleiben +- Darstellung als `native-module` mit modul-eigenem `app.js` und `app.css` ## Hinweis -- die bestehende HTML-/JS-Logik aus `Customimport/salesforcelanguageimport.html` wurde als App-Seite uebernommen -- vorhandene Inline-CSS bleibt weitgehend erhalten, weil die App in einer isolierten Fensterseite laeuft und dadurch das globale Desktop-UI nicht direkt stoert +- die bestehende HTML-/JS-Logik aus `Customimport/salesforcelanguageimport.html` wurde in modul-eigene Assets ueberfuehrt +- die App nutzt weiterhin nur clientseitige Verarbeitung fuer Excel-, CSV-, ZIP- und XLIFF-Dateien diff --git a/modules/salesforce-translation-import/pages/index.php b/modules/salesforce-translation-import/pages/index.php index a0861eec..144a8fdb 100644 --- a/modules/salesforce-translation-import/pages/index.php +++ b/modules/salesforce-translation-import/pages/index.php @@ -11,998 +11,33 @@ session_start(); $projectRoot = dirname(__DIR__, 3); ModuleHttp::requireDesktopAccess($projectRoot); +$assetVersion = static function (string $relativePath): string { + $fullPath = dirname(__DIR__) . '/' . ltrim($relativePath, '/'); + $mtime = is_file($fullPath) ? filemtime($fullPath) : false; + + return $mtime === false ? '0' : (string) $mtime; +}; ?> - - -Salesforce Translation Import - - - - - + + + Salesforce Translation Import + - -

Salesforce XLIFF Translation Tool

- -
-

How to use this tool

-
    -
  1. Export the required target languages from Salesforce in XLIFF format.
  2. -
  3. Salesforce may provide several language files in a ZIP archive. You can upload the Salesforce ZIP directly or select extracted XLIFF files.
  4. -
  5. Under Salesforce XLIFF export files, select the ZIP or the individual XLIFF files for the languages you want to translate. The tool automatically reads all XLIFF files from a ZIP and detects each file's target-language value.
  6. -
  7. Prepare one Excel or CSV target translation file and upload it under Target translation.
  8. -
  9. Check the language matches, create the preview, and then download the translated files as a ZIP archive.
  10. -
  11. Extract the generated ZIP and import each completed target-language XLIFF file into Salesforce separately. Salesforce accepts one translation file per import.
  12. -
- -

Required columns: Header names are case-insensitive. The English headers object, field, and target are recommended. The existing German aliases Objekt and Feld also remain supported.

- - - - - - - - - - - - - - - - - - - - - - - - -
objectfieldtargetlanguage
AccountNameKontonamede
AccountNameNom du comptefr
- -

The optional language column lets one target translation file contain several languages. Its values must match the XLIFF target-language values, for example de, fr, or de-DE. If the column is omitted and several Salesforce XLIFF files are loaded, select the one target XLIFF file to which the translation data belongs.

-

Important: Each matched target language is exported as a separate XLIFF file inside the generated ZIP. A language file is omitted when no matching translation can be applied. The result contains only changed translation units with a non-empty target value. Rows with an empty target, rows where field and target are identical, and translations identical to the current XLIFF source or target are omitted.

-
- -
-
- -
No Salesforce XLIFF export files selected.
-
- -
-
- -
No target translation file selected.
- -
-
- -
- -
- - -
- -
- -
- - - - - - - - - - - - -
FileLanguageObjectFieldCurrent targetResult target
-
- - + + + - +