sdfsdf
This commit is contained in:
285
modules/salesforce-translation-import/assets/css/app.css
Normal file
285
modules/salesforce-translation-import/assets/css/app.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
832
modules/salesforce-translation-import/assets/js/app.js
Normal file
832
modules/salesforce-translation-import/assets/js/app.js
Normal 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('&', '&')
|
||||||
|
.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>')
|
||||||
|
.replaceAll('"', '"')
|
||||||
|
.replaceAll("'", ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildApp(host, options) {
|
||||||
|
host.innerHTML = template;
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
translationsByLanguage: new Map(),
|
||||||
|
sourceFiles: [],
|
||||||
|
sourceLoadPromise: Promise.resolve(),
|
||||||
|
previewChanges: [],
|
||||||
|
targetFileName: '',
|
||||||
|
targetRowCount: 0,
|
||||||
|
targetHasLanguageColumn: false,
|
||||||
|
targetStats: createEmptyTargetStats(),
|
||||||
|
selectedGenericSourceId: '',
|
||||||
|
sourceSequence: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
const xmlInput = host.querySelector('#salesforce-translation-import-xml-files');
|
||||||
|
const targetInput = host.querySelector('#salesforce-translation-import-target-file');
|
||||||
|
const genericSourceSelect = host.querySelector('#salesforce-translation-import-generic-source-select');
|
||||||
|
const analyzeBtn = host.querySelector('#salesforce-translation-import-analyze-btn');
|
||||||
|
const exportBtn = host.querySelector('#salesforce-translation-import-export-btn');
|
||||||
|
const sourceStatus = host.querySelector('#salesforce-translation-import-source-status');
|
||||||
|
const targetStatus = host.querySelector('#salesforce-translation-import-target-status');
|
||||||
|
const statsNode = host.querySelector('#salesforce-translation-import-stats');
|
||||||
|
const previewBody = host.querySelector('#salesforce-translation-import-preview-body');
|
||||||
|
const objectSelect = host.querySelector('#salesforce-translation-import-object-select');
|
||||||
|
const genericSourceSelection = host.querySelector('#salesforce-translation-import-generic-source-selection');
|
||||||
|
|
||||||
|
function appendMetric(container, value, label, className) {
|
||||||
|
const metric = document.createElement('div');
|
||||||
|
metric.className = className || 'salesforce-translation-import__muted';
|
||||||
|
metric.textContent = value + ' ' + label;
|
||||||
|
container.appendChild(metric);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetObjectSelect() {
|
||||||
|
objectSelect.innerHTML = '<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 || {});
|
||||||
|
};
|
||||||
|
}());
|
||||||
@@ -15,11 +15,25 @@ return [
|
|||||||
'icon' => 'SF',
|
'icon' => 'SF',
|
||||||
'entry_route' => '/apps/salesforce-translation-import',
|
'entry_route' => '/apps/salesforce-translation-import',
|
||||||
'window_mode' => 'window',
|
'window_mode' => 'window',
|
||||||
'content_mode' => 'iframe',
|
'content_mode' => 'native-module',
|
||||||
'default_width' => 1180,
|
'default_width' => 1180,
|
||||||
'default_height' => 820,
|
'default_height' => 820,
|
||||||
'supports_widget' => false,
|
'supports_widget' => false,
|
||||||
'supports_tray' => false,
|
'supports_tray' => false,
|
||||||
'module_name' => 'integration',
|
'module_name' => 'integration',
|
||||||
'summary' => (string) ($manifest['description'] ?? 'Import-Werkzeug fuer Salesforce-XLIFF-Dateien und Zieluebersetzungen aus Excel oder CSV.'),
|
'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'))],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ Desktop-App fuer den Import von Salesforce-XLIFF-Dateien mit Zieluebersetzungen
|
|||||||
|
|
||||||
- Desktop-App ueber `modules/salesforce-translation-import/desktop.php`
|
- Desktop-App ueber `modules/salesforce-translation-import/desktop.php`
|
||||||
- oeffentlicher App-Einstieg ueber `public/apps/salesforce-translation-import/index.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
|
## Hinweis
|
||||||
|
|
||||||
- die bestehende HTML-/JS-Logik aus `Customimport/salesforcelanguageimport.html` wurde als App-Seite uebernommen
|
- die bestehende HTML-/JS-Logik aus `Customimport/salesforcelanguageimport.html` wurde in modul-eigene Assets ueberfuehrt
|
||||||
- vorhandene Inline-CSS bleibt weitgehend erhalten, weil die App in einer isolierten Fensterseite laeuft und dadurch das globale Desktop-UI nicht direkt stoert
|
- die App nutzt weiterhin nur clientseitige Verarbeitung fuer Excel-, CSV-, ZIP- und XLIFF-Dateien
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user