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

This commit is contained in:
2026-06-22 22:12:43 +02:00
parent 881fbd0651
commit af4886191f
5 changed files with 1155 additions and 989 deletions

View File

@@ -0,0 +1,285 @@
.salesforce-translation-import,
.salesforce-translation-import-page {
min-height: 100%;
background:
radial-gradient(circle at top left, rgba(96, 165, 250, 0.10), transparent 24%),
linear-gradient(180deg, #f8fafc 0%, #eef2f7 100%);
color: #0f172a;
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
}
.salesforce-translation-import-page {
min-height: 100vh;
}
.salesforce-translation-import {
min-height: 100%;
padding: 24px;
}
.salesforce-translation-import *,
.salesforce-translation-import *::before,
.salesforce-translation-import *::after {
box-sizing: border-box;
}
.salesforce-translation-import__shell {
display: flex;
flex-direction: column;
gap: 20px;
}
.salesforce-translation-import__hero,
.salesforce-translation-import__card {
border: 1px solid rgba(148, 163, 184, 0.24);
border-radius: 28px;
background: rgba(255, 255, 255, 0.92);
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.10);
}
.salesforce-translation-import__hero {
padding: 24px 28px;
}
.salesforce-translation-import__eyebrow {
margin: 0 0 12px;
color: #4338ca;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.18em;
text-transform: uppercase;
}
.salesforce-translation-import__title {
margin: 0;
font-size: clamp(2rem, 4vw, 2.8rem);
line-height: 1.05;
}
.salesforce-translation-import__lede {
max-width: 70rem;
margin: 14px 0 0;
color: #475569;
font-size: 1rem;
line-height: 1.6;
}
.salesforce-translation-import__grid {
display: grid;
gap: 20px;
grid-template-columns: minmax(0, 1.2fr) minmax(320px, 0.8fr);
}
.salesforce-translation-import__main,
.salesforce-translation-import__side {
display: flex;
flex-direction: column;
gap: 20px;
min-width: 0;
}
.salesforce-translation-import__card {
padding: 22px 24px;
}
.salesforce-translation-import__card h2 {
margin: 0 0 14px;
font-size: 1.55rem;
line-height: 1.15;
}
.salesforce-translation-import__card p {
margin: 0 0 12px;
color: #475569;
line-height: 1.55;
}
.salesforce-translation-import__card ol {
margin: 0;
padding-left: 20px;
color: #334155;
line-height: 1.6;
}
.salesforce-translation-import__card li + li {
margin-top: 8px;
}
.salesforce-translation-import__sample {
overflow-x: auto;
margin-top: 14px;
border: 1px solid rgba(148, 163, 184, 0.24);
border-radius: 18px;
}
.salesforce-translation-import__field {
display: flex;
flex-direction: column;
gap: 8px;
}
.salesforce-translation-import__field + .salesforce-translation-import__field {
margin-top: 18px;
}
.salesforce-translation-import__label {
font-weight: 700;
color: #1e293b;
}
.salesforce-translation-import__input,
.salesforce-translation-import__select {
width: 100%;
min-height: 48px;
padding: 12px 14px;
border: 1px solid rgba(148, 163, 184, 0.45);
border-radius: 16px;
background: #ffffff;
color: #0f172a;
font: inherit;
}
.salesforce-translation-import__actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 20px;
}
.salesforce-translation-import__button {
min-height: 48px;
padding: 11px 18px;
border: 1px solid rgba(99, 102, 241, 0.22);
border-radius: 999px;
background: #ffffff;
color: #111827;
font: inherit;
font-weight: 700;
cursor: pointer;
}
.salesforce-translation-import__button:hover {
border-color: rgba(79, 70, 229, 0.4);
}
.salesforce-translation-import__button:disabled {
opacity: 0.55;
cursor: not-allowed;
}
.salesforce-translation-import__button--primary {
border-color: transparent;
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
color: #ffffff;
}
.salesforce-translation-import__status {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 10px;
}
.salesforce-translation-import__status.is-muted {
color: #64748b;
}
.salesforce-translation-import__status-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
padding: 6px 0;
border-bottom: 1px solid rgba(226, 232, 240, 0.9);
}
.salesforce-translation-import__status-row:last-child {
border-bottom: 0;
}
.salesforce-translation-import__file-name {
font-weight: 700;
overflow-wrap: anywhere;
}
.salesforce-translation-import__language-code {
padding: 2px 8px;
border: 1px solid rgba(148, 163, 184, 0.65);
border-radius: 999px;
font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
font-size: 0.86rem;
}
.salesforce-translation-import__match-ok {
color: #166534;
font-weight: 700;
}
.salesforce-translation-import__match-missing {
color: #b91c1c;
font-weight: 700;
}
.salesforce-translation-import__muted {
color: #64748b;
}
.salesforce-translation-import__stats {
color: #334155;
line-height: 1.65;
}
.salesforce-translation-import__preview {
overflow: auto;
border: 1px solid rgba(148, 163, 184, 0.24);
border-radius: 18px;
background: #ffffff;
}
.salesforce-translation-import table {
width: 100%;
border-collapse: collapse;
}
.salesforce-translation-import th,
.salesforce-translation-import td {
padding: 12px 14px;
border-bottom: 1px solid rgba(226, 232, 240, 0.9);
vertical-align: top;
text-align: left;
}
.salesforce-translation-import thead th {
position: sticky;
top: 0;
background: #f8fafc;
color: #334155;
z-index: 1;
}
.salesforce-translation-import tbody tr:nth-child(even) {
background: rgba(248, 250, 252, 0.75);
}
.salesforce-translation-import__preview-empty {
padding: 20px 14px;
color: #64748b;
}
@media (max-width: 1100px) {
.salesforce-translation-import__grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 720px) {
.salesforce-translation-import {
padding: 16px;
}
.salesforce-translation-import__hero,
.salesforce-translation-import__card {
padding: 18px;
border-radius: 22px;
}
}

View File

@@ -0,0 +1,832 @@
(function () {
'use strict';
const template = `
<div class="salesforce-translation-import">
<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>
</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 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 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 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);
}
xmlInput.addEventListener('change', () => {
state.sourceLoadPromise = loadSourceFiles();
});
targetInput.addEventListener('change', loadTargetTranslations);
genericSourceSelect.addEventListener('change', (event) => {
state.selectedGenericSourceId = event.target.value;
renderFileStatus();
});
analyzeBtn.addEventListener('click', analyze);
exportBtn.addEventListener('click', exportZip);
renderFileStatus();
renderPreview();
if (options && options.standalone) {
document.body.classList.add('salesforce-translation-import-page');
}
}
window.initSalesforceTranslationImportApp = function initSalesforceTranslationImportApp(host, options) {
if (!(host instanceof HTMLElement)) {
return;
}
if (!window.XLSX || !window.JSZip) {
host.innerHTML = '<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 || {});
};
}());

View File

@@ -15,11 +15,25 @@ return [
'icon' => 'SF',
'entry_route' => '/apps/salesforce-translation-import',
'window_mode' => 'window',
'content_mode' => 'iframe',
'content_mode' => 'native-module',
'default_width' => 1180,
'default_height' => 820,
'supports_widget' => false,
'supports_tray' => false,
'module_name' => 'integration',
'summary' => (string) ($manifest['description'] ?? 'Import-Werkzeug fuer Salesforce-XLIFF-Dateien und Zieluebersetzungen aus Excel oder CSV.'),
'native_module' => [
'initializer' => 'initSalesforceTranslationImportApp',
'options' => [],
'assets' => [
'styles' => [
['href' => '/module-assets/index.php?module=salesforce-translation-import&path=assets/css/app.css&v=' . rawurlencode((string) (is_file(__DIR__ . '/assets/css/app.css') ? filemtime(__DIR__ . '/assets/css/app.css') : '0'))],
],
'scripts' => [
['src' => 'https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js'],
['src' => 'https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js'],
['src' => '/module-assets/index.php?module=salesforce-translation-import&path=assets/js/app.js&v=' . rawurlencode((string) (is_file(__DIR__ . '/assets/js/app.js') ? filemtime(__DIR__ . '/assets/js/app.js') : '0'))],
],
],
],
];

View File

@@ -6,9 +6,9 @@ Desktop-App fuer den Import von Salesforce-XLIFF-Dateien mit Zieluebersetzungen
- Desktop-App ueber `modules/salesforce-translation-import/desktop.php`
- oeffentlicher App-Einstieg ueber `public/apps/salesforce-translation-import/index.php`
- Darstellung als isolierte Fensterseite per `iframe`, damit die Import-Logik und die Formularstruktur stabil bleiben
- Darstellung als `native-module` mit modul-eigenem `app.js` und `app.css`
## Hinweis
- die bestehende HTML-/JS-Logik aus `Customimport/salesforcelanguageimport.html` wurde als App-Seite uebernommen
- vorhandene Inline-CSS bleibt weitgehend erhalten, weil die App in einer isolierten Fensterseite laeuft und dadurch das globale Desktop-UI nicht direkt stoert
- die bestehende HTML-/JS-Logik aus `Customimport/salesforcelanguageimport.html` wurde in modul-eigene Assets ueberfuehrt
- die App nutzt weiterhin nur clientseitige Verarbeitung fuer Excel-, CSV-, ZIP- und XLIFF-Dateien

File diff suppressed because it is too large Load Diff