993 lines
28 KiB
HTML
993 lines
28 KiB
HTML
<!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> |