asdasd
All checks were successful
Deploy / deploy-staging (push) Successful in 25s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-06-25 00:17:31 +02:00
parent 46111d8988
commit ac2d95fc56
140 changed files with 1168 additions and 1984 deletions

View File

@@ -0,0 +1,916 @@
(function () {
'use strict';
const template = `
<div class="salesforce-translation-import window-app-shell">
<div class="salesforce-translation-import__shell">
<section class="salesforce-translation-import__hero">
<p class="salesforce-translation-import__eyebrow">Integration</p>
<h1 class="salesforce-translation-import__title">Salesforce Translation Import</h1>
<p class="salesforce-translation-import__lede">Clientseitiges Werkzeug fuer Salesforce-XLIFF-Dateien und Zieluebersetzungen aus Excel oder CSV. Der Export erzeugt pro Sprache eine separate XLIFF-Datei im ZIP.</p>
</section>
<div class="salesforce-translation-import__grid">
<div class="salesforce-translation-import__main">
<section class="salesforce-translation-import__card">
<h2>Import</h2>
<div class="salesforce-translation-import__field">
<label class="salesforce-translation-import__label" for="salesforce-translation-import-xml-files">Salesforce XLIFF export files</label>
<input class="salesforce-translation-import__input" type="file" id="salesforce-translation-import-xml-files" multiple accept=".zip,.xml,.xlf,.xliff">
<div id="salesforce-translation-import-source-status" class="salesforce-translation-import__status salesforce-translation-import__status is-muted">No Salesforce XLIFF export files selected.</div>
</div>
<div class="salesforce-translation-import__field">
<label class="salesforce-translation-import__label" for="salesforce-translation-import-target-file">Target translation (Excel or CSV)</label>
<input class="salesforce-translation-import__input" type="file" id="salesforce-translation-import-target-file" accept=".xlsx,.xls,.csv">
<div id="salesforce-translation-import-target-status" class="salesforce-translation-import__status salesforce-translation-import__status is-muted">No target translation file selected.</div>
<div id="salesforce-translation-import-generic-source-selection" class="salesforce-translation-import__field" hidden>
<label class="salesforce-translation-import__label" for="salesforce-translation-import-generic-source-select">Apply target translation to</label>
<select class="salesforce-translation-import__select" id="salesforce-translation-import-generic-source-select">
<option value="">Select a target XLIFF file</option>
</select>
</div>
</div>
<div class="salesforce-translation-import__field">
<label class="salesforce-translation-import__label" for="salesforce-translation-import-object-select">Preview object filter</label>
<select class="salesforce-translation-import__select" id="salesforce-translation-import-object-select">
<option value="">All objects</option>
</select>
</div>
<div class="salesforce-translation-import__actions">
<button class="salesforce-translation-import__button salesforce-translation-import__button--primary" type="button" id="salesforce-translation-import-analyze-btn">Create preview</button>
<button class="salesforce-translation-import__button" type="button" id="salesforce-translation-import-export-btn">Create ZIP</button>
<button class="salesforce-translation-import__button" type="button" id="salesforce-translation-import-extract-btn">Extract CSV</button>
</div>
</section>
<section class="salesforce-translation-import__card">
<h2>Preview</h2>
<div id="salesforce-translation-import-stats" class="salesforce-translation-import__stats"></div>
<div class="salesforce-translation-import__preview">
<table>
<thead>
<tr>
<th>File</th>
<th>Language</th>
<th>Object</th>
<th>Field</th>
<th>Current target</th>
<th>Result target</th>
</tr>
</thead>
<tbody id="salesforce-translation-import-preview-body">
<tr>
<td colspan="6" class="salesforce-translation-import__preview-empty">Noch keine Vorschau erstellt.</td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
<aside class="salesforce-translation-import__side">
<section class="salesforce-translation-import__card">
<h2>Vorgehen</h2>
<ol>
<li>Salesforce-Zielsprachen als XLIFF exportieren. ZIP-Dateien koennen direkt hochgeladen werden.</li>
<li>Eine Excel- oder CSV-Datei mit den Spalten <code>object</code>, <code>field</code> und <code>target</code> vorbereiten.</li>
<li>Optional kann die Spalte <code>language</code> mehrere Zielsprachen in einer Datei abbilden.</li>
<li>Vorschau erzeugen, Matches pruefen und danach das Ergebnis als ZIP herunterladen.</li>
<li>Die exportierten XLIFF-Dateien einzeln wieder in Salesforce importieren.</li>
</ol>
</section>
<section class="salesforce-translation-import__card">
<h2>Beispielstruktur</h2>
<p>Header werden gross-/kleinschreibungsunabhaengig ausgewertet. Die Aliasnamen <code>Objekt</code> und <code>Feld</code> bleiben ebenfalls gueltig.</p>
<div class="salesforce-translation-import__sample">
<table>
<thead>
<tr>
<th>object</th>
<th>field</th>
<th>target</th>
<th>language</th>
</tr>
</thead>
<tbody>
<tr>
<td>Account</td>
<td>Name</td>
<td>Kontoname</td>
<td>de</td>
</tr>
<tr>
<td>Account</td>
<td>Name</td>
<td>Nom du compte</td>
<td>fr</td>
</tr>
</tbody>
</table>
</div>
</section>
</aside>
</div>
</div>
</div>
`;
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('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
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 = '<option value="">All objects</option>';
}
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 = '<option value="">Select a target XLIFF file</option>';
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 = '<td colspan="6" class="salesforce-translation-import__preview-empty">Noch keine Vorschau erstellt.</td>';
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 =
'<strong>Salesforce source comparison</strong><br>' +
escapeHtml(rowsFoundInSource.size) + ' target rows found in matched source files<br>' +
escapeHtml(rowsNotInSource) + ' target rows not present in matched source files<br>' +
escapeHtml(rowsSameAsSource.size) + ' target rows identical to the XLIFF source value<br>' +
escapeHtml(rowsSameAsTarget.size) + ' target rows identical to the current XLIFF target value<br>' +
escapeHtml(translationsToApply) + ' translations to apply<br>' +
escapeHtml(rowsToReplace.size) + ' existing targets to replace<br>' +
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 = '<div class="salesforce-translation-import"><div class="salesforce-translation-import__card"><h2>Abhaengigkeiten fehlen</h2><p>Die benoetigten Bibliotheken fuer Excel- und ZIP-Verarbeitung konnten nicht geladen werden.</p></div></div>';
return;
}
buildApp(host, options || {});
};
}());