917 lines
39 KiB
JavaScript
917 lines
39 KiB
JavaScript
(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('&', '&')
|
|
.replaceAll('<', '<')
|
|
.replaceAll('>', '>')
|
|
.replaceAll('"', '"')
|
|
.replaceAll("'", ''');
|
|
}
|
|
|
|
function buildApp(host, options) {
|
|
host.innerHTML = template;
|
|
|
|
const state = {
|
|
translationsByLanguage: new Map(),
|
|
sourceFiles: [],
|
|
sourceLoadPromise: Promise.resolve(),
|
|
previewChanges: [],
|
|
targetFileName: '',
|
|
targetRowCount: 0,
|
|
targetHasLanguageColumn: false,
|
|
targetStats: createEmptyTargetStats(),
|
|
selectedGenericSourceId: '',
|
|
sourceSequence: 0
|
|
};
|
|
|
|
const xmlInput = host.querySelector('#salesforce-translation-import-xml-files');
|
|
const targetInput = host.querySelector('#salesforce-translation-import-target-file');
|
|
const genericSourceSelect = host.querySelector('#salesforce-translation-import-generic-source-select');
|
|
const analyzeBtn = host.querySelector('#salesforce-translation-import-analyze-btn');
|
|
const exportBtn = host.querySelector('#salesforce-translation-import-export-btn');
|
|
const extractBtn = host.querySelector('#salesforce-translation-import-extract-btn');
|
|
const sourceStatus = host.querySelector('#salesforce-translation-import-source-status');
|
|
const targetStatus = host.querySelector('#salesforce-translation-import-target-status');
|
|
const statsNode = host.querySelector('#salesforce-translation-import-stats');
|
|
const previewBody = host.querySelector('#salesforce-translation-import-preview-body');
|
|
const objectSelect = host.querySelector('#salesforce-translation-import-object-select');
|
|
const genericSourceSelection = host.querySelector('#salesforce-translation-import-generic-source-selection');
|
|
|
|
function appendMetric(container, value, label, className) {
|
|
const metric = document.createElement('div');
|
|
metric.className = className || 'salesforce-translation-import__muted';
|
|
metric.textContent = value + ' ' + label;
|
|
container.appendChild(metric);
|
|
}
|
|
|
|
function resetObjectSelect() {
|
|
objectSelect.innerHTML = '<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 || {});
|
|
};
|
|
}());
|