asdasd
This commit is contained in:
916
custom/apps/salesforce-translation-import/assets/js/app.js
Normal file
916
custom/apps/salesforce-translation-import/assets/js/app.js
Normal 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('&', '&')
|
||||
.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 || {});
|
||||
};
|
||||
}());
|
||||
Reference in New Issue
Block a user