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
+
+
+
Salesforce XLIFF export files
+
+
No Salesforce XLIFF export files selected.
+
+
+
+
Target translation (Excel or CSV)
+
+
No target translation file selected.
+
+ Apply target translation to
+
+ Select a target XLIFF file
+
+
+
+
+
+ Preview object filter
+
+ All objects
+
+
+
+
+ Create preview
+ Create ZIP
+
+
+
+
+ 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 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 = 'All objects ';
+ }
+
+ 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 = 'Select a target XLIFF file ';
+
+ 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
-
- Export the required target languages from Salesforce in XLIFF format.
- Salesforce may provide several language files in a ZIP archive. You can upload the Salesforce ZIP directly or select extracted XLIFF files.
- 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.
- Prepare one Excel or CSV target translation file and upload it under Target translation .
- Check the language matches, create the preview, and then download the translated files as a ZIP archive.
- Extract the generated ZIP and import each completed target-language XLIFF file into Salesforce separately. Salesforce accepts one translation file per import.
-
-
- 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.
-
-
-
-
- object
- field
- target
- language
-
-
-
-
- Account
- Name
- Kontoname
- de
-
-
- Account
- Name
- Nom du compte
- fr
-
-
-
-
- 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.
-
-
-
-
Salesforce XLIFF export files:
-
-
No Salesforce XLIFF export files selected.
-
-
-
-
Target translation (Excel or CSV):
-
-
No target translation file selected.
-
-
Apply target translation to:
-
- Select a target XLIFF file
-
+
-
-
- Preview object filter:
-
- All objects
-
-
-
-
- Create preview
- Create ZIP
-
-
-
-
-
-
-
-
-File
-Language
-Object
-Field
-Current target
-Result target
-
-
-
-
-
-
-
+
+
+
-
+