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

This commit is contained in:
2026-06-25 00:17:31 +02:00
parent 46111d8988
commit ac2d95fc56
140 changed files with 1168 additions and 1984 deletions

View File

@@ -1,993 +0,0 @@
<!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>

View File

@@ -69,10 +69,10 @@ return [
'options' => [], 'options' => [],
'assets' => [ 'assets' => [
'styles' => [ 'styles' => [
['href' => '/assets/apps/user-self-management/app.css'], ['href' => '/system-app-assets/index.php?app=user-self-management&path=assets/app.css'],
], ],
'scripts' => [ 'scripts' => [
['src' => '/assets/apps/user-self-management/app.js'], ['src' => '/system-app-assets/index.php?app=user-self-management&path=assets/app.js'],
], ],
], ],
], ],
@@ -116,10 +116,10 @@ return [
], ],
'assets' => [ 'assets' => [
'styles' => [ 'styles' => [
['href' => '/assets/apps/user-management/app.css'], ['href' => '/system-app-assets/index.php?app=user-management&path=assets/app.css'],
], ],
'scripts' => [ 'scripts' => [
['src' => '/assets/apps/user-management/app.js'], ['src' => '/system-app-assets/index.php?app=user-management&path=assets/app.js'],
], ],
], ],
], ],
@@ -148,10 +148,10 @@ return [
], ],
'assets' => [ 'assets' => [
'styles' => [ 'styles' => [
['href' => '/assets/apps/cron-management/app.css'], ['href' => '/system-app-assets/index.php?app=cron-management&path=assets/app.css'],
], ],
'scripts' => [ 'scripts' => [
['src' => '/assets/apps/cron-management/app.js'], ['src' => '/system-app-assets/index.php?app=cron-management&path=assets/app.js'],
], ],
], ],
], ],

View File

@@ -1,6 +1,6 @@
# Module # Module
Klassische Module bleiben in diesem Projekt unter `modules/<modul>/`. Installierbare und fachliche Apps liegen in diesem Projekt unter `custom/apps/<app>/`.
Die neue Desktop-Shell ist nur die UI-Schicht. Modul-Businesslogik wird nicht in den Desktop-Core verschoben. Die neue Desktop-Shell ist nur die UI-Schicht. Modul-Businesslogik wird nicht in den Desktop-Core verschoben.
@@ -15,13 +15,13 @@ Ein Desktop-Modul kann zusaetzlich diese Projektdateien besitzen:
- `docs/README.md` fuer modulspezifische Hinweise, API und Sonderregeln - `docs/README.md` fuer modulspezifische Hinweise, API und Sonderregeln
- `module.json` fuer Setup, Desktop-Freigabe, Widget-Funktionen und Cron-Endpunkte - `module.json` fuer Setup, Desktop-Freigabe, Widget-Funktionen und Cron-Endpunkte
Module-Assets werden in diesem Projekt nicht direkt aus `public/assets/apps/...` dupliziert, sondern ueber den generischen Endpoint `/module-assets/index.php` aus dem jeweiligen Modul ausgeliefert. Custom-App-Assets werden nicht direkt aus `public/` geladen, sondern ueber den generischen Endpoint `/module-assets/index.php` aus dem jeweiligen App-Ordner ausgeliefert.
## Trennung zwischen Systemtools und Modulen ## Trennung zwischen Systemtools und Modulen
- `Systemtools` sind globale Verwaltungs- und Setup-Apps und nicht installierbar - `Systemtools` sind globale Verwaltungs- und Setup-Apps und nicht installierbar
- aktuelle Beispiele: `User Management`, `User Self Management`, `Cron Tool` - aktuelle Beispiele: `User Management`, `User Self Management`, `Cron Tool`
- `Module` sind installierbare Fachanwendungen - `Custom Apps` sind installierbare Fachanwendungen
- aktuelle Beispiele: `Mining-Checker`, `Waehrungs-Checker`, `Boersenchecker`, `Pi-hole` - aktuelle Beispiele: `Mining-Checker`, `Waehrungs-Checker`, `Boersenchecker`, `Pi-hole`
Diese Trennung wird ueber App-Metadaten gesteuert: Diese Trennung wird ueber App-Metadaten gesteuert:

View File

@@ -4,7 +4,7 @@ Importiertes Altmodul fuer Depotverwaltung, Aktienstammdaten, Kursverlauf, Alpha
## Einbindung ## Einbindung
- Desktop-App ueber `modules/boersenchecker/desktop.php` - Desktop-App ueber `custom/apps/boersenchecker/desktop.php`
- oeffentlicher App-Einstieg ueber `public/apps/boersenchecker/index.php` - oeffentlicher App-Einstieg ueber `public/apps/boersenchecker/index.php`
- API-Endpunkt fuer Chartdaten ueber `public/api/boersenchecker/index.php?path=v1/chart-data` - API-Endpunkt fuer Chartdaten ueber `public/api/boersenchecker/index.php?path=v1/chart-data`

View File

@@ -4,7 +4,7 @@ Das Modul `fx-rates` stellt gespeicherte Waehrungskurse, Historie, Umrechnung un
## Kernpunkte ## Kernpunkte
- Desktop-App unter `modules/fx-rates/desktop.php` - Desktop-App unter `custom/apps/fx-rates/desktop.php`
- API unter `/api/fx-rates/...` - API unter `/api/fx-rates/...`
- Widget fuer den rechten Desktop-Infobereich ueber `config/widgets.php` - Widget fuer den rechten Desktop-Infobereich ueber `config/widgets.php`
- externer Provider-Token wird aus dem alten Bestand als Default uebernommen - externer Provider-Token wird aus dem alten Bestand als Default uebernommen

View File

@@ -4,7 +4,7 @@
"description": "Erfassung, OCR-Auswertung und Analyse von DOGE-Mining-Messwerten als eingebettetes Modul.", "description": "Erfassung, OCR-Auswertung und Analyse von DOGE-Mining-Messwerten als eingebettetes Modul.",
"actions": [ "actions": [
{ "label": "Nexus Übersicht", "href": "/modules" }, { "label": "Nexus Übersicht", "href": "/modules" },
{ "label": "Setup", "href": "/modules/setup/mining-checker", "variant": "secondary" } { "label": "Setup", "href": "/apps/mining-checker?view=setup", "variant": "secondary" }
], ],
"sections": [ "sections": [
{ "key": "overview", "label": "Übersicht" }, { "key": "overview", "label": "Übersicht" },

View File

@@ -7,7 +7,7 @@ Das Modul erfasst DOGE-Mining-Messpunkte, analysiert OCR-Vorschlaege aus Screens
## Ordnerstruktur ## Ordnerstruktur
```text ```text
modules/mining-checker/ custom/apps/mining-checker/
|-- api/ |-- api/
|-- assets/ |-- assets/
| |-- css/ | |-- css/

Some files were not shown because too many files have changed in this diff Show More