ysds
This commit is contained in:
@@ -56,6 +56,7 @@ jobs:
|
||||
--exclude=".ci_config_deploy/***" \
|
||||
--exclude="data/***" \
|
||||
--exclude="docs/***" \
|
||||
--exclude="Customimport/***" \
|
||||
--exclude="temp/***" \
|
||||
--exclude="CopyToKeycloak/***" \
|
||||
--exclude="Umsetzungsanweisung/***" \
|
||||
@@ -140,6 +141,7 @@ jobs:
|
||||
--exclude="data/***" \
|
||||
--exclude="docs/***" \
|
||||
--exclude="temp/***" \
|
||||
--exclude="Customimport/***" \
|
||||
--exclude="CopyToKeycloak/***" \
|
||||
--exclude="Umsetzungsanweisung/***" \
|
||||
--exclude="config/staging/***" \
|
||||
|
||||
993
Customimport/salesforcelanguageimport.html
Normal file
993
Customimport/salesforcelanguageimport.html
Normal file
@@ -0,0 +1,993 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Salesforce XLIFF Translation Tool</title>
|
||||
|
||||
<script src="https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js"></script>
|
||||
|
||||
<style>
|
||||
body{
|
||||
font-family:Segoe UI,Arial,sans-serif;
|
||||
margin:20px;
|
||||
color:#1f2937;
|
||||
line-height:1.45;
|
||||
}
|
||||
|
||||
h1{
|
||||
margin:0 0 20px;
|
||||
}
|
||||
|
||||
h2{
|
||||
margin:0 0 10px;
|
||||
font-size:18px;
|
||||
}
|
||||
|
||||
.help{
|
||||
max-width:1000px;
|
||||
margin-bottom:24px;
|
||||
padding:14px 18px;
|
||||
border-left:4px solid #2563eb;
|
||||
background:#f8fafc;
|
||||
}
|
||||
|
||||
.help ol{
|
||||
margin:8px 0 12px;
|
||||
padding-left:22px;
|
||||
}
|
||||
|
||||
.help p{
|
||||
margin:8px 0;
|
||||
}
|
||||
|
||||
.help-table{
|
||||
width:auto;
|
||||
min-width:520px;
|
||||
margin:10px 0;
|
||||
}
|
||||
|
||||
.section{
|
||||
margin-bottom:20px;
|
||||
}
|
||||
|
||||
label{
|
||||
display:inline-block;
|
||||
margin-bottom:5px;
|
||||
font-weight:600;
|
||||
}
|
||||
|
||||
input,
|
||||
select{
|
||||
max-width:100%;
|
||||
}
|
||||
|
||||
button{
|
||||
padding:10px 18px;
|
||||
margin-right:8px;
|
||||
cursor:pointer;
|
||||
}
|
||||
|
||||
button:disabled{
|
||||
cursor:not-allowed;
|
||||
opacity:.55;
|
||||
}
|
||||
|
||||
table{
|
||||
border-collapse:collapse;
|
||||
width:100%;
|
||||
margin-top:15px;
|
||||
}
|
||||
|
||||
th,td{
|
||||
border:1px solid #cbd5e1;
|
||||
padding:7px;
|
||||
text-align:left;
|
||||
vertical-align:top;
|
||||
}
|
||||
|
||||
th{
|
||||
background:#f1f5f9;
|
||||
}
|
||||
|
||||
.file-status{
|
||||
max-width:1000px;
|
||||
margin-top:8px;
|
||||
font-size:14px;
|
||||
}
|
||||
|
||||
.status-row{
|
||||
display:flex;
|
||||
flex-wrap:wrap;
|
||||
gap:8px;
|
||||
align-items:center;
|
||||
padding:5px 0;
|
||||
border-bottom:1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.file-name{
|
||||
font-weight:600;
|
||||
overflow-wrap:anywhere;
|
||||
}
|
||||
|
||||
.language-code{
|
||||
padding:1px 6px;
|
||||
border:1px solid #94a3b8;
|
||||
border-radius:4px;
|
||||
font-family:Consolas,monospace;
|
||||
}
|
||||
|
||||
.match-ok{
|
||||
color:#166534;
|
||||
font-weight:600;
|
||||
}
|
||||
|
||||
.match-missing{
|
||||
color:#b91c1c;
|
||||
font-weight:600;
|
||||
}
|
||||
|
||||
.muted{
|
||||
color:#64748b;
|
||||
}
|
||||
|
||||
#preview{
|
||||
max-height:420px;
|
||||
overflow:auto;
|
||||
border:1px solid #cbd5e1;
|
||||
}
|
||||
|
||||
#preview table{
|
||||
margin-top:0;
|
||||
}
|
||||
|
||||
#stats{
|
||||
margin:15px 0;
|
||||
font-weight:600;
|
||||
}
|
||||
|
||||
@media (max-width:700px){
|
||||
body{
|
||||
margin:12px;
|
||||
}
|
||||
|
||||
.help-table{
|
||||
min-width:0;
|
||||
width:100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Salesforce XLIFF Translation Tool</h1>
|
||||
|
||||
<section class="help" aria-labelledby="helpTitle">
|
||||
<h2 id="helpTitle">How to use this tool</h2>
|
||||
<ol>
|
||||
<li>Export the required target languages from Salesforce in XLIFF format.</li>
|
||||
<li>Salesforce may provide several language files in a ZIP archive. You can upload the Salesforce ZIP directly or select extracted XLIFF files.</li>
|
||||
<li>Under <strong>Salesforce XLIFF export files</strong>, select the ZIP or the individual XLIFF files for the languages you want to translate. The tool automatically reads all XLIFF files from a ZIP and detects each file's <code>target-language</code> value.</li>
|
||||
<li>Prepare one Excel or CSV target translation file and upload it under <strong>Target translation</strong>.</li>
|
||||
<li>Check the language matches, create the preview, and then download the translated files as a ZIP archive.</li>
|
||||
<li>Extract the generated ZIP and import each completed target-language XLIFF file into Salesforce separately. Salesforce accepts one translation file per import.</li>
|
||||
</ol>
|
||||
|
||||
<p><strong>Required columns:</strong> Header names are case-insensitive. The English headers <code>object</code>, <code>field</code>, and <code>target</code> are recommended. The existing German aliases <code>Objekt</code> and <code>Feld</code> also remain supported.</p>
|
||||
|
||||
<table class="help-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>
|
||||
|
||||
<p>The optional <code>language</code> column lets one target translation file contain several languages. Its values must match the XLIFF <code>target-language</code> values, for example <code>de</code>, <code>fr</code>, or <code>de-DE</code>. If the column is omitted and several Salesforce XLIFF files are loaded, select the one target XLIFF file to which the translation data belongs.</p>
|
||||
<p><strong>Important:</strong> Each matched target language is exported as a separate XLIFF file inside the generated ZIP. A language file is omitted when no matching translation can be applied. The result contains only changed translation units with a non-empty <code>target</code> value. Rows with an empty <code>target</code>, rows where <code>field</code> and <code>target</code> are identical, and translations identical to the current XLIFF source or target are omitted.</p>
|
||||
</section>
|
||||
|
||||
<div class="section">
|
||||
<label for="xmlFiles">Salesforce XLIFF export files:</label><br>
|
||||
<input type="file"
|
||||
id="xmlFiles"
|
||||
multiple
|
||||
accept=".zip,.xml,.xlf,.xliff">
|
||||
<div id="sourceStatus" class="file-status muted">No Salesforce XLIFF export files selected.</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<label for="targetFile">Target translation (Excel or CSV):</label><br>
|
||||
<input type="file"
|
||||
id="targetFile"
|
||||
accept=".xlsx,.xls,.csv">
|
||||
<div id="targetStatus" class="file-status muted">No target translation file selected.</div>
|
||||
<div id="genericSourceSelection" class="file-status" hidden>
|
||||
<label for="genericSourceSelect">Apply target translation to:</label><br>
|
||||
<select id="genericSourceSelect">
|
||||
<option value="">Select a target XLIFF file</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<label for="objectSelect">Preview object filter:</label><br>
|
||||
<select id="objectSelect">
|
||||
<option value="">All objects</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<button id="analyzeBtn">Create preview</button>
|
||||
<button id="exportBtn">Create ZIP</button>
|
||||
</div>
|
||||
|
||||
<div id="stats"></div>
|
||||
|
||||
<div id="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="previewBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const translationsByLanguage = new Map();
|
||||
let sourceFiles = [];
|
||||
let sourceLoadPromise = Promise.resolve();
|
||||
let previewChanges = [];
|
||||
let targetFileName = "";
|
||||
let targetRowCount = 0;
|
||||
let targetHasLanguageColumn = false;
|
||||
let targetStats = createEmptyTargetStats();
|
||||
let selectedGenericSourceId = "";
|
||||
let sourceSequence = 0;
|
||||
|
||||
const xmlInput = document.getElementById("xmlFiles");
|
||||
const targetInput = document.getElementById("targetFile");
|
||||
|
||||
xmlInput.addEventListener("change", ()=>{
|
||||
sourceLoadPromise = loadSourceFiles();
|
||||
});
|
||||
|
||||
targetInput.addEventListener("change", loadTargetTranslations);
|
||||
document.getElementById("genericSourceSelect").addEventListener("change", event=>{
|
||||
selectedGenericSourceId = event.target.value;
|
||||
renderFileStatus();
|
||||
});
|
||||
document.getElementById("analyzeBtn").addEventListener("click", analyze);
|
||||
document.getElementById("exportBtn").addEventListener("click", exportZip);
|
||||
|
||||
function normalizeHeader(value){
|
||||
return String(value || "").trim().toLowerCase();
|
||||
}
|
||||
|
||||
function createEmptyTargetStats(){
|
||||
return {
|
||||
totalRows:0,
|
||||
withoutTarget:0,
|
||||
sameFieldAndTarget:0,
|
||||
incomplete:0,
|
||||
duplicates:0,
|
||||
usableUniqueRows:0
|
||||
};
|
||||
}
|
||||
|
||||
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)
|
||||
};
|
||||
}
|
||||
|
||||
async function loadSourceFiles(){
|
||||
sourceFiles = [];
|
||||
selectedGenericSourceId = "";
|
||||
sourceSequence = 0;
|
||||
previewChanges = [];
|
||||
renderPreview();
|
||||
document.getElementById("stats").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 loadSourceZip(file){
|
||||
try{
|
||||
const archive = await 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){
|
||||
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){
|
||||
sourceFiles.push({
|
||||
name:file.name,
|
||||
error:"The ZIP could not be read: " + error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function addSourceFile(name, text, displayName = name){
|
||||
try{
|
||||
const xml = parseXml(text);
|
||||
const language = getTargetLanguage(xml);
|
||||
|
||||
sourceFiles.push({
|
||||
id:"source-" + (++sourceSequence),
|
||||
name,
|
||||
displayName,
|
||||
text,
|
||||
language:language.normalized,
|
||||
languageLabel:language.raw || "Not specified",
|
||||
unitCount:xml.getElementsByTagName("trans-unit").length
|
||||
});
|
||||
}catch(error){
|
||||
sourceFiles.push({
|
||||
name,
|
||||
displayName,
|
||||
error:error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function findHeaderIndex(headers, aliases){
|
||||
return headers.findIndex(header=>aliases.includes(header));
|
||||
}
|
||||
|
||||
async function loadTargetTranslations(event){
|
||||
const file = event.target.files[0];
|
||||
|
||||
translationsByLanguage.clear();
|
||||
targetFileName = "";
|
||||
targetRowCount = 0;
|
||||
targetHasLanguageColumn = false;
|
||||
targetStats = createEmptyTargetStats();
|
||||
resetObjectSelect();
|
||||
|
||||
if(!file){
|
||||
renderFileStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
try{
|
||||
const workbook = XLSX.read(await file.arrayBuffer());
|
||||
const sheet = workbook.Sheets[workbook.SheetNames[0]];
|
||||
const data = 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."
|
||||
);
|
||||
}
|
||||
|
||||
targetHasLanguageColumn = languageIndex >= 0;
|
||||
const objects = new Set();
|
||||
const targetRows = data.slice(1).filter(row=>
|
||||
row.some(value=>String(value ?? "").trim() !== "")
|
||||
);
|
||||
|
||||
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 = targetHasLanguageColumn
|
||||
? normalizeLanguage(row[languageIndex])
|
||||
: "";
|
||||
|
||||
if(
|
||||
!objectName ||
|
||||
!fieldName ||
|
||||
(targetHasLanguageColumn && !language)
|
||||
){
|
||||
targetStats.incomplete++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!targetValue){
|
||||
targetStats.withoutTarget++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if(fieldName === targetValue){
|
||||
targetStats.sameFieldAndTarget++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!translationsByLanguage.has(language)){
|
||||
translationsByLanguage.set(language, new Map());
|
||||
}
|
||||
|
||||
const key = objectName + "|" + fieldName;
|
||||
const languageTranslations = translationsByLanguage.get(language);
|
||||
|
||||
if(languageTranslations.has(key)){
|
||||
targetStats.duplicates++;
|
||||
}
|
||||
|
||||
languageTranslations.set(key, targetValue);
|
||||
|
||||
objects.add(objectName);
|
||||
}
|
||||
|
||||
targetRowCount = [...translationsByLanguage.values()]
|
||||
.reduce((total, entries)=>total + entries.size, 0);
|
||||
targetStats.usableUniqueRows = targetRowCount;
|
||||
|
||||
targetFileName = file.name;
|
||||
populateObjectSelect(objects);
|
||||
renderFileStatus();
|
||||
}catch(error){
|
||||
translationsByLanguage.clear();
|
||||
targetInput.value = "";
|
||||
renderFileStatus();
|
||||
alert(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function resetObjectSelect(){
|
||||
const select = document.getElementById("objectSelect");
|
||||
select.innerHTML = '<option value="">All objects</option>';
|
||||
}
|
||||
|
||||
function populateObjectSelect(objects){
|
||||
resetObjectSelect();
|
||||
const select = document.getElementById("objectSelect");
|
||||
|
||||
[...objects].sort().forEach(objectName=>{
|
||||
const option = document.createElement("option");
|
||||
option.value = objectName;
|
||||
option.textContent = objectName;
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
function getTranslationsForSource(source){
|
||||
if(source.error){
|
||||
return null;
|
||||
}
|
||||
|
||||
if(source.language && translationsByLanguage.has(source.language)){
|
||||
return {
|
||||
translations:translationsByLanguage.get(source.language),
|
||||
label:source.language,
|
||||
languageKey:source.language
|
||||
};
|
||||
}
|
||||
|
||||
if(
|
||||
!targetHasLanguageColumn &&
|
||||
(
|
||||
sourceFiles.filter(entry=>!entry.error).length === 1 ||
|
||||
source.id === selectedGenericSourceId
|
||||
) &&
|
||||
translationsByLanguage.has("")
|
||||
){
|
||||
return {
|
||||
translations:translationsByLanguage.get(""),
|
||||
label:"single-source mode",
|
||||
languageKey:""
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function appendStatusRow(container, source){
|
||||
const row = document.createElement("div");
|
||||
row.className = "status-row";
|
||||
|
||||
const fileName = document.createElement("span");
|
||||
fileName.className = "file-name";
|
||||
fileName.textContent = source.displayName || source.name;
|
||||
row.appendChild(fileName);
|
||||
|
||||
if(source.error){
|
||||
const error = document.createElement("span");
|
||||
error.className = "match-missing";
|
||||
error.textContent = "Invalid XLIFF: " + source.error;
|
||||
row.appendChild(error);
|
||||
container.appendChild(row);
|
||||
return;
|
||||
}
|
||||
|
||||
const language = document.createElement("span");
|
||||
language.className = "language-code";
|
||||
language.textContent = source.languageLabel;
|
||||
row.appendChild(language);
|
||||
|
||||
const unitCount = document.createElement("span");
|
||||
unitCount.className = "muted";
|
||||
unitCount.textContent = source.unitCount + " translation units";
|
||||
row.appendChild(unitCount);
|
||||
|
||||
const match = getTranslationsForSource(source);
|
||||
const matchText = document.createElement("span");
|
||||
|
||||
if(match){
|
||||
matchText.className = "match-ok";
|
||||
matchText.textContent = "Matched: " + match.label;
|
||||
}else if(
|
||||
targetFileName &&
|
||||
!targetHasLanguageColumn &&
|
||||
sourceFiles.filter(entry=>!entry.error).length > 1
|
||||
){
|
||||
matchText.className = "muted";
|
||||
matchText.textContent = "Not selected for this target translation";
|
||||
}else if(targetFileName){
|
||||
matchText.className = "match-missing";
|
||||
matchText.textContent = "No matching target translations";
|
||||
}else{
|
||||
matchText.className = "muted";
|
||||
matchText.textContent = "Waiting for target translation";
|
||||
}
|
||||
|
||||
row.appendChild(matchText);
|
||||
container.appendChild(row);
|
||||
}
|
||||
|
||||
function renderFileStatus(){
|
||||
const sourceStatus = document.getElementById("sourceStatus");
|
||||
const targetStatus = document.getElementById("targetStatus");
|
||||
|
||||
renderGenericSourceSelection();
|
||||
|
||||
sourceStatus.innerHTML = "";
|
||||
|
||||
if(sourceFiles.length === 0){
|
||||
sourceStatus.textContent = "No Salesforce XLIFF export files selected.";
|
||||
sourceStatus.className = "file-status muted";
|
||||
}else{
|
||||
sourceStatus.className = "file-status";
|
||||
sourceFiles.forEach(source=>appendStatusRow(sourceStatus, source));
|
||||
}
|
||||
|
||||
targetStatus.innerHTML = "";
|
||||
|
||||
if(!targetFileName){
|
||||
targetStatus.textContent = "No target translation file selected.";
|
||||
targetStatus.className = "file-status muted";
|
||||
return;
|
||||
}
|
||||
|
||||
targetStatus.className = "file-status";
|
||||
|
||||
const summary = document.createElement("div");
|
||||
summary.className = "status-row";
|
||||
|
||||
const fileName = document.createElement("span");
|
||||
fileName.className = "file-name";
|
||||
fileName.textContent = targetFileName;
|
||||
summary.appendChild(fileName);
|
||||
|
||||
const details = document.createElement("span");
|
||||
const languages = [...translationsByLanguage.keys()]
|
||||
.filter(Boolean)
|
||||
.sort();
|
||||
|
||||
details.textContent = targetHasLanguageColumn
|
||||
? "Languages: " + (languages.join(", ") || "none")
|
||||
: "No language column (single-source mode)";
|
||||
|
||||
summary.appendChild(details);
|
||||
targetStatus.appendChild(summary);
|
||||
|
||||
appendMetric(targetStatus, targetStats.totalRows, "rows in target translation file");
|
||||
appendMetric(targetStatus, targetStats.withoutTarget, "rows without a target value");
|
||||
appendMetric(targetStatus, targetStats.sameFieldAndTarget, "rows where field and target are identical");
|
||||
appendMetric(targetStatus, targetStats.incomplete, "incomplete rows");
|
||||
appendMetric(targetStatus, targetStats.duplicates, "duplicate object/field rows");
|
||||
appendMetric(targetStatus, targetStats.usableUniqueRows, "usable unique target rows", "match-ok");
|
||||
}
|
||||
|
||||
function appendMetric(container, value, label, className = "muted"){
|
||||
const metric = document.createElement("div");
|
||||
metric.className = className;
|
||||
metric.textContent = value + " " + label;
|
||||
container.appendChild(metric);
|
||||
}
|
||||
|
||||
function renderGenericSourceSelection(){
|
||||
const wrapper = document.getElementById("genericSourceSelection");
|
||||
const select = document.getElementById("genericSourceSelect");
|
||||
const validSources = sourceFiles.filter(entry=>!entry.error);
|
||||
const selectionRequired =
|
||||
Boolean(targetFileName) &&
|
||||
!targetHasLanguageColumn &&
|
||||
validSources.length > 1;
|
||||
|
||||
wrapper.hidden = !selectionRequired;
|
||||
|
||||
if(!selectionRequired){
|
||||
selectedGenericSourceId = validSources.length === 1
|
||||
? validSources[0].id
|
||||
: "";
|
||||
return;
|
||||
}
|
||||
|
||||
const previousSelection = selectedGenericSourceId;
|
||||
select.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 + "]";
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
if(validSources.some(source=>source.id === previousSelection)){
|
||||
select.value = previousSelection;
|
||||
}else{
|
||||
selectedGenericSourceId = "";
|
||||
select.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
function genericSourceSelectionIsMissing(){
|
||||
return Boolean(targetFileName) &&
|
||||
!targetHasLanguageColumn &&
|
||||
sourceFiles.filter(entry=>!entry.error).length > 1 &&
|
||||
!selectedGenericSourceId;
|
||||
}
|
||||
|
||||
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()
|
||||
};
|
||||
}
|
||||
|
||||
async function analyze(){
|
||||
await sourceLoadPromise;
|
||||
|
||||
if(sourceFiles.filter(entry=>!entry.error).length === 0){
|
||||
alert("Select at least one valid Salesforce XLIFF source file.");
|
||||
return;
|
||||
}
|
||||
|
||||
if(translationsByLanguage.size === 0){
|
||||
alert("Select a valid target translation file.");
|
||||
return;
|
||||
}
|
||||
|
||||
if(genericSourceSelectionIsMissing()){
|
||||
alert("Select the target XLIFF file for this target translation.");
|
||||
return;
|
||||
}
|
||||
|
||||
const matchedSources = sourceFiles.filter(getTranslationsForSource);
|
||||
|
||||
if(matchedSources.length === 0){
|
||||
alert("No Salesforce XLIFF source language matches the target translation data.");
|
||||
return;
|
||||
}
|
||||
|
||||
previewChanges = [];
|
||||
const selectedObject = document.getElementById("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();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
previewChanges.push({
|
||||
file:source.name,
|
||||
language:source.languageLabel,
|
||||
object:details.objectName,
|
||||
field:details.fieldName,
|
||||
oldValue,
|
||||
newValue
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const rowsNotInSource = [...allTargetRows]
|
||||
.filter(rowToken=>!rowsFoundInSource.has(rowToken))
|
||||
.length;
|
||||
const translationsToApply = new Set([
|
||||
...rowsToCreate,
|
||||
...rowsToReplace
|
||||
]).size;
|
||||
|
||||
renderPreview();
|
||||
document.getElementById("stats").innerHTML =
|
||||
"<strong>Salesforce source comparison</strong><br>" +
|
||||
rowsFoundInSource.size + " target rows found in matched source files<br>" +
|
||||
rowsNotInSource + " target rows not present in matched source files<br>" +
|
||||
rowsSameAsSource.size + " target rows identical to the XLIFF source value<br>" +
|
||||
rowsSameAsTarget.size + " target rows identical to the current XLIFF target value<br>" +
|
||||
translationsToApply + " translations to apply<br>" +
|
||||
rowsToReplace.size + " existing targets to replace<br>" +
|
||||
rowsToCreate.size + " new targets to create";
|
||||
}
|
||||
|
||||
function renderPreview(){
|
||||
const body = document.getElementById("previewBody");
|
||||
body.innerHTML = "";
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
body.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
function removeUnit(unit){
|
||||
const whitespaceBefore = unit.previousSibling;
|
||||
|
||||
if(
|
||||
whitespaceBefore &&
|
||||
whitespaceBefore.nodeType === 3 &&
|
||||
/^\s*$/.test(whitespaceBefore.nodeValue)
|
||||
){
|
||||
whitespaceBefore.remove();
|
||||
}
|
||||
|
||||
unit.remove();
|
||||
}
|
||||
|
||||
async function exportZip(){
|
||||
await sourceLoadPromise;
|
||||
|
||||
if(sourceFiles.filter(entry=>!entry.error).length === 0){
|
||||
alert("Select at least one valid Salesforce XLIFF source file.");
|
||||
return;
|
||||
}
|
||||
|
||||
if(translationsByLanguage.size === 0){
|
||||
alert("Select a valid target translation file.");
|
||||
return;
|
||||
}
|
||||
|
||||
if(genericSourceSelectionIsMissing()){
|
||||
alert("Select the target XLIFF file for this target translation.");
|
||||
return;
|
||||
}
|
||||
|
||||
const matchedSources = sourceFiles.filter(getTranslationsForSource);
|
||||
|
||||
if(matchedSources.length === 0){
|
||||
alert("No Salesforce XLIFF source language matches the target translation data.");
|
||||
return;
|
||||
}
|
||||
|
||||
const zip = new 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;
|
||||
}
|
||||
|
||||
const output = new XMLSerializer().serializeToString(xml);
|
||||
zip.file(source.name, output);
|
||||
exportedFileCount++;
|
||||
}
|
||||
|
||||
if(exportedFileCount === 0){
|
||||
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();
|
||||
|
||||
setTimeout(()=>URL.revokeObjectURL(downloadUrl), 1000);
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -51,6 +51,7 @@ Aktuell wichtig:
|
||||
|
||||
- der Mining-Checker ist das erste als echtes Desktop-Modul angebundene Modul unter `modules/mining-checker/`
|
||||
- der Waehrungs-Checker ist als zweites echtes Desktop-Modul unter `modules/fx-rates/` angebunden
|
||||
- der Salesforce Translation Import ist als weitere Desktop-App unter `modules/salesforce-translation-import/` angebunden
|
||||
- Modul-Assets koennen ueber einen gemeinsamen Auslieferungsweg aus dem Modul selbst geladen werden
|
||||
- die API bleibt vorerst auf demselben Host und wird nicht auf `api.desktop.kusche.berlin` ausgelagert
|
||||
- das gemeinsame Admin-Debug-Widget neben der Uhr ist der Standard fuer Live-Debugging in Desktop und Native-Apps
|
||||
|
||||
25
modules/salesforce-translation-import/desktop.php
Normal file
25
modules/salesforce-translation-import/desktop.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$manifest = [];
|
||||
$manifestRaw = is_file(__DIR__ . '/module.json') ? file_get_contents(__DIR__ . '/module.json') : false;
|
||||
if ($manifestRaw !== false) {
|
||||
$decoded = json_decode($manifestRaw, true);
|
||||
$manifest = is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
|
||||
return [
|
||||
'app_id' => 'salesforce-translation-import',
|
||||
'title' => (string) ($manifest['title'] ?? 'Salesforce Translation Import'),
|
||||
'icon' => 'SF',
|
||||
'entry_route' => '/apps/salesforce-translation-import',
|
||||
'window_mode' => 'window',
|
||||
'content_mode' => 'iframe',
|
||||
'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.'),
|
||||
];
|
||||
14
modules/salesforce-translation-import/docs/README.md
Normal file
14
modules/salesforce-translation-import/docs/README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Salesforce Translation Import
|
||||
|
||||
Desktop-App fuer den Import von Salesforce-XLIFF-Dateien mit Zieluebersetzungen aus Excel oder CSV.
|
||||
|
||||
## Einbindung
|
||||
|
||||
- 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
|
||||
|
||||
## 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
|
||||
12
modules/salesforce-translation-import/module.json
Normal file
12
modules/salesforce-translation-import/module.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "salesforce-translation-import",
|
||||
"title": "Salesforce Translation Import",
|
||||
"version": "0.1.0",
|
||||
"description": "Import-Werkzeug fuer Salesforce-XLIFF-Dateien und Zieluebersetzungen aus Excel oder CSV.",
|
||||
"enabled_by_default": true,
|
||||
"auth": {
|
||||
"required": true,
|
||||
"users": [],
|
||||
"groups": []
|
||||
}
|
||||
}
|
||||
1008
modules/salesforce-translation-import/pages/index.php
Normal file
1008
modules/salesforce-translation-import/pages/index.php
Normal file
File diff suppressed because it is too large
Load Diff
5
public/apps/salesforce-translation-import/index.php
Normal file
5
public/apps/salesforce-translation-import/index.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require dirname(__DIR__, 3) . '/modules/salesforce-translation-import/pages/index.php';
|
||||
Reference in New Issue
Block a user