saltcorn-samba 0.4.15 → 0.4.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +96 -0
- package/README.md +64 -0
- package/filemanager-view.js +52 -10
- package/i18n/de.json +93 -0
- package/i18n/en.json +93 -0
- package/i18n.js +155 -0
- package/index.js +34 -0
- package/package.json +4 -2
- package/pdf-view.js +8 -5
- package/public/samba-common.js +175 -0
- package/public/samba-filemanager.js +373 -381
- package/public/samba-tree.js +126 -137
- package/tree-view.js +49 -11
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,102 @@ All notable changes to `saltcorn-samba` are documented here.
|
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [0.4.16] – 2026-07-08
|
|
8
|
+
|
|
9
|
+
### Added – Internationalisierung (Deutsch + Englisch)
|
|
10
|
+
|
|
11
|
+
Alle sichtbaren Texte im File-Manager, im Tree, im PDF-Fieldview und in den
|
|
12
|
+
Dialogen (Upload, Delete, Rename, Mkdir) sind jetzt übersetzbar.
|
|
13
|
+
|
|
14
|
+
- Neue Kataloge unter `i18n/de.json` und `i18n/en.json` (~90 Schlüssel,
|
|
15
|
+
Punkt-Notation nach Bereich `ui.*`, `fm.*`, `tree.*`, `pdf.*`).
|
|
16
|
+
- Neues Server-Modul `i18n.js` – keine externe Dependency, JSON-Kataloge
|
|
17
|
+
werden gelesen und im Prozess gecacht. Placeholder-Interpolation `{name}`.
|
|
18
|
+
API: `t(key, {locale, ...params})`, `tFor(locale)`, `catalogFor(locale)`,
|
|
19
|
+
`resolveLocaleFromReq(req, explicit)`.
|
|
20
|
+
- Locale-Auflösung: expliziter Wert → `req.getLocale()` → Query `?locale=xx`
|
|
21
|
+
→ `Accept-Language`-Header → Fallback `en`. Unbekannte Locales fallen
|
|
22
|
+
automatisch auf die Sprachbasis oder `en` zurück.
|
|
23
|
+
- Neue Diagnose-Route `GET /samba-i18n.json?locale=xx` liefert den Katalog
|
|
24
|
+
als JSON (kein Auth, enthält keine Konfiguration).
|
|
25
|
+
- Client bekommt den Katalog **inline** in die View-Shell injiziert – kein
|
|
26
|
+
zusätzlicher HTTP-Roundtrip beim ersten Rendern, keine englischen Keys, die
|
|
27
|
+
kurz aufblitzen. `SambaCommon.setCatalog(…)` wird aufgerufen, bevor die
|
|
28
|
+
view-spezifische JS-Datei geladen wird.
|
|
29
|
+
- Fehlt ein Key: automatischer Fallback auf Englisch, dann auf den Key
|
|
30
|
+
selbst – nichts bricht.
|
|
31
|
+
|
|
32
|
+
### Changed – Client-JS: Gemeinsame Utilities in `public/samba-common.js`
|
|
33
|
+
|
|
34
|
+
- Neues Modul `public/samba-common.js` bündelt: `iconFor()`, `extOf()`,
|
|
35
|
+
`mediaTypeFor()`, `isViewable()`, `fmtSize()`, `fmtDate()`, `joinPath()`,
|
|
36
|
+
`parentOf()`, plus die i18n-Funktionen `t()`, `setCatalog()`, `loadCatalog()`.
|
|
37
|
+
- **`iconFor()` war bisher in `samba-filemanager.js` und `samba-tree.js`
|
|
38
|
+
identisch dupliziert** – jetzt einzige Quelle in `samba-common.js`.
|
|
39
|
+
Änderungen am Icon-Mapping (neue Extensions etc.) müssen nur noch an einer
|
|
40
|
+
Stelle gemacht werden.
|
|
41
|
+
- `samba-filemanager.js` und `samba-tree.js` konsumieren die Utilities über
|
|
42
|
+
`window.SambaCommon`. Die lokalen Kopien von `iconFor`, `extOf`,
|
|
43
|
+
`mediaTypeFor`, `isViewable`, `fmtSize`, `fmtDate`, `joinPath`, `parentOf`
|
|
44
|
+
sind entfernt.
|
|
45
|
+
- Alle sichtbaren Strings in beiden Client-JS gehen über `SambaCommon.t(...)`.
|
|
46
|
+
|
|
47
|
+
### Changed – Bootstrap der View-Shells: zweistufig
|
|
48
|
+
|
|
49
|
+
`filemanager-view.js` und `tree-view.js` laden jetzt zuerst `samba-common.js`
|
|
50
|
+
(setzt `window.SambaCommon`, wendet den inline gelieferten i18n-Katalog an)
|
|
51
|
+
und erst danach die view-spezifische JS-Datei. Sind beide bereits geladen,
|
|
52
|
+
wird nur re-mounted – kein doppeltes `<script>`-Injecten. `pdf-view.js`
|
|
53
|
+
nutzt serverseitig `tFor(resolveLocaleFromReq(req))`.
|
|
54
|
+
|
|
55
|
+
### Changed – Namens-Cleanup und Kommentare in beiden Client-JS
|
|
56
|
+
|
|
57
|
+
Goal: die Client-Dateien sollen sich wie ordentliche Node-ähnliche Module
|
|
58
|
+
lesen, nicht wie ein Minifier-Output.
|
|
59
|
+
|
|
60
|
+
- `samba-tree.js`: `h`→`element`, `fetchDir`→`fetchDirectory`,
|
|
61
|
+
`renderList`→`renderLevel`, `openDir`→`toggleDirectory`, `li`→`lineItem`
|
|
62
|
+
u.ä. Jede exportierte / interne Funktion hat einen JSDoc-Kommentar.
|
|
63
|
+
- `samba-filemanager.js`: `h`→`element`, `modal`→`openModal`, `m`→`dialog`,
|
|
64
|
+
`r`→`response`, `fd`→`formData`, `overwriteCb`→`overwriteCheckbox`,
|
|
65
|
+
`picked`→`pickedList`, `uploadBtn`→`uploadButton`. Kommentare ergänzt.
|
|
66
|
+
|
|
67
|
+
### Added – README: Sicherheits-Abschnitt „Client-/Server-Trennung“
|
|
68
|
+
|
|
69
|
+
Neuer Unterabschnitt in `## Sicherheit` erklärt explizit:
|
|
70
|
+
|
|
71
|
+
- Welche Dateien serverseitig laufen (SMB-Credentials, `base_path`,
|
|
72
|
+
`view_base_path`, Sanitizer, Routen) und welche clientseitig (reine UI).
|
|
73
|
+
- Wo genau Sicherheit durchgesetzt wird: CSRF (Saltcorn-csurf), Auth/Rolle
|
|
74
|
+
(Saltcorn + `min_role_read`/`min_role_write`), `sanitizeRelativePath`,
|
|
75
|
+
Base-Path-Enforcement, SMB-Session mit serverseitig konfiguriertem User.
|
|
76
|
+
- Wichtiger Hinweis: `view_base_path` ist **keine** Sicherheitsgrenze
|
|
77
|
+
zwischen Usern; für Mandantentrennung entweder Rollen-gated Views oder
|
|
78
|
+
eigene Plugin-Instanzen mit separaten SMB-Usern.
|
|
79
|
+
- Kurzfassung: JavaScript in `public/` ist reine Kosmetik; jeder Client (auch
|
|
80
|
+
ein manipulierter) bekommt dieselben 403/400-Antworten wie ein regulärer.
|
|
81
|
+
|
|
82
|
+
Zusätzlich neuer Abschnitt `## Internationalisierung (i18n)` mit
|
|
83
|
+
Entwickler-Doku (Kataloge ergänzen, API, Locale-Auflösung, Diagnose-Route).
|
|
84
|
+
|
|
85
|
+
### Packaging
|
|
86
|
+
|
|
87
|
+
- `package.json`: Version 0.4.15 → 0.4.16. `files` ergänzt um `i18n.js` und
|
|
88
|
+
`i18n/`. `lint`-Script prüft jetzt auch `i18n.js` und `public/samba-common.js`.
|
|
89
|
+
- Keine neuen Runtime-Dependencies. Fortlaufend: nur `smb3-client ^0.2.0`.
|
|
90
|
+
|
|
91
|
+
### Compatibility
|
|
92
|
+
|
|
93
|
+
Rückwärtskompatibel:
|
|
94
|
+
|
|
95
|
+
- Alle bestehenden Routen (`/sambadir`, `/sambafile`, `/sambalink`,
|
|
96
|
+
`/sambaupload`, `/sambadelete`, `/sambarename`, `/sambamkdir`, `/sambatest`)
|
|
97
|
+
unverändert in Signatur und Verhalten.
|
|
98
|
+
- View-Konfigurationsschema unverändert; existierende Views laufen weiter.
|
|
99
|
+
- Wer die Client-JS direkt einbindet, muss zusätzlich `samba-common.js` vor
|
|
100
|
+
`samba-filemanager.js` / `samba-tree.js` laden (das übernimmt die
|
|
101
|
+
View-Shell automatisch).
|
|
102
|
+
|
|
7
103
|
## [0.4.15] – 2026-07-07
|
|
8
104
|
|
|
9
105
|
### Fixed – CSRF-Fehlermeldung bei Upload / Ordner anlegen / Rename / Delete
|
package/README.md
CHANGED
|
@@ -304,6 +304,70 @@ führende/abschließende Punkte oder Windows-Reserved-Names enthalten.
|
|
|
304
304
|
|
|
305
305
|
Unit-Tests zum Path- und Filename-Sanitizer laufen in CI (`npm test`).
|
|
306
306
|
|
|
307
|
+
### Client-/Server-Trennung
|
|
308
|
+
|
|
309
|
+
Damit klar ist, wo Sicherheit tatsächlich durchgesetzt wird: das Plugin
|
|
310
|
+
läuft in zwei streng getrennten Welten.
|
|
311
|
+
|
|
312
|
+
- **Serverseitig (Node.js im Saltcorn-Prozess):**
|
|
313
|
+
`index.js` (HTTP-Routen), `smb-client.js` (SMB3-Verbindung, Auth, Signing,
|
|
314
|
+
Encryption), `readdir-compat.js` (Verzeichnis-Listing),
|
|
315
|
+
`filemanager-view.js` / `tree-view.js` / `pdf-view.js`
|
|
316
|
+
(configuration_workflow, run, computeStartPath, sanitizeRelativePath),
|
|
317
|
+
`i18n.js` (Übersetzungskatalog) und `tools/diag-*.js` (CLI-Diagnose).
|
|
318
|
+
**SMB-Credentials (User, Domain, Passwort), Plugin-`base_path` und pro-View
|
|
319
|
+
`view_base_path` liegen ausschließlich serverseitig in der Saltcorn-DB.**
|
|
320
|
+
Der Browser sieht davon nichts.
|
|
321
|
+
|
|
322
|
+
- **Clientseitig (Browser, unter `public/`):**
|
|
323
|
+
`samba-filemanager.js`, `samba-tree.js`, `samba-common.js` (gemeinsame
|
|
324
|
+
Utilities + i18n) und `samba.css`. Reine UI. Alle Zugriffe laufen über
|
|
325
|
+
`fetch()` mit `X-CSRF-Token`-Header.
|
|
326
|
+
|
|
327
|
+
- **Enforcement-Punkte serverseitig — in jeder Request-Bearbeitung:**
|
|
328
|
+
1. **CSRF** – Saltcorns globale csurf-Middleware. Ohne Token: 403.
|
|
329
|
+
2. **Auth/Rolle** – Saltcorn prüft Login und View/Table-Rechte vor `run(…)`.
|
|
330
|
+
Zusätzlich prüft jede Route `min_role_read` bzw. `min_role_write`.
|
|
331
|
+
3. **Pfad-Sanitizing** – `sanitizeRelativePath` lehnt `..`, absolute Pfade,
|
|
332
|
+
UNC-Präfixe, Laufwerksbuchstaben und NUL-Bytes ab.
|
|
333
|
+
4. **Base-Path-Enforcement** – der effektive Pfad ist immer relativ zum
|
|
334
|
+
serverseitig konfigurierten `base_path`; `view_base_path` stammt aus der
|
|
335
|
+
View-Konfig, nie aus dem Request.
|
|
336
|
+
5. **SMB-Session** läuft mit dem serverseitig konfigurierten SMB-User; der
|
|
337
|
+
Client kann keine anderen Credentials injizieren.
|
|
338
|
+
|
|
339
|
+
- **Wichtige Hinweise:**
|
|
340
|
+
`view_base_path` ist **keine** Sicherheitsgrenze zwischen Usern – nur
|
|
341
|
+
Bequemlichkeit. Für Mandantentrennung entweder eigene Views pro Mandant
|
|
342
|
+
(mit Rollen-Steuerung), oder mehrere Plugin-Instanzen mit unterschiedlichen
|
|
343
|
+
SMB-Usern. Produktions-Empfehlung: dedizierter SMB-Service-Account mit
|
|
344
|
+
minimalen Rechten; die Fileserver-ACL limitiert dann zusätzlich.
|
|
345
|
+
|
|
346
|
+
- **Kurzfassung:** JavaScript in `public/` ist reine Kosmetik. Alle
|
|
347
|
+
Zugriffsrechte werden serverseitig durchgesetzt — ein manipulierter Client
|
|
348
|
+
bekommt genau die gleichen 403/400-Antworten wie ein regulärer.
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## Internationalisierung (i18n)
|
|
353
|
+
|
|
354
|
+
Ab v0.4.16 sind alle sichtbaren Texte übersetzbar. Ausgeliefert werden **de**
|
|
355
|
+
und **en**.
|
|
356
|
+
|
|
357
|
+
- Kataloge liegen als JSON unter `i18n/<locale>.json`.
|
|
358
|
+
- Serverseitige Nutzung: `const { t, tFor } = require("./i18n");`
|
|
359
|
+
→ `t("fm.upload.button", { locale: "de" })` bzw.
|
|
360
|
+
`const _t = tFor("de"); _t("fm.upload.button")`.
|
|
361
|
+
- Clientseitig: `window.SambaCommon.t("fm.upload.button")`. Der Katalog wird
|
|
362
|
+
von der View-Shell inline in die Seite injiziert (kein Extra-Request).
|
|
363
|
+
- Locale-Auflösung: explizit übergebener Wert → `req.getLocale()` →
|
|
364
|
+
`?locale=xx` → `Accept-Language` → Fallback `en`.
|
|
365
|
+
- Neue Sprache ergänzen: `i18n/<locale>.json` anlegen, `availableLocales` in
|
|
366
|
+
`i18n.js` erweitern, PR aufmachen. Fehlt ein Key, greift automatisch der
|
|
367
|
+
englische Fallback — nichts bricht.
|
|
368
|
+
- Diagnose-Endpoint: `GET /samba-i18n.json?locale=de` liefert den kompletten
|
|
369
|
+
Katalog als JSON (ohne Auth, enthält keine Konfiguration).
|
|
370
|
+
|
|
307
371
|
---
|
|
308
372
|
|
|
309
373
|
## Entwicklung
|
package/filemanager-view.js
CHANGED
|
@@ -18,6 +18,7 @@ const Field = require("@saltcorn/data/models/field");
|
|
|
18
18
|
const Table = require("@saltcorn/data/models/table");
|
|
19
19
|
|
|
20
20
|
const { sanitizeRelativePath } = require("./smb-client");
|
|
21
|
+
const { catalogFor, resolveLocaleFromReq } = require("./i18n");
|
|
21
22
|
|
|
22
23
|
// ---------------------------------------------------------------------------
|
|
23
24
|
// Configuration workflow
|
|
@@ -170,8 +171,21 @@ function computeStartPath(configuration, row) {
|
|
|
170
171
|
// Shell renderer
|
|
171
172
|
// ---------------------------------------------------------------------------
|
|
172
173
|
|
|
173
|
-
|
|
174
|
+
/**
|
|
175
|
+
* Serialisiert den i18n-Katalog inline in die Seite. Alternative w\u00e4re ein
|
|
176
|
+
* zus\u00e4tzlicher HTTP-Roundtrip auf /samba-i18n.json — inline spart einen
|
|
177
|
+
* Request und rendert die Oberfl\u00e4che ohne Aufblitzen englischer Keys.
|
|
178
|
+
* `</script>` wird escaped, damit das JSON-Blob den HTML-Parser nicht t\u00f6tet.
|
|
179
|
+
*/
|
|
180
|
+
function serialiseCatalog(catalog) {
|
|
181
|
+
return JSON.stringify(catalog || {}).replace(/</g, "\\u003c");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function renderShell(startPath, configuration, pluginVersion, csrfToken, locale, catalog) {
|
|
174
185
|
const id = "samba-fm-" + Math.random().toString(36).slice(2, 10);
|
|
186
|
+
// Panel-Titel: wenn der User keinen eigenen Titel gesetzt hat, greift die
|
|
187
|
+
// \u00fcbersetzte Default-Bezeichnung aus dem i18n-Katalog.
|
|
188
|
+
const defaultTitle = (catalog && catalog["fm.title_default"]) || "Samba files";
|
|
175
189
|
const opts = {
|
|
176
190
|
startPath,
|
|
177
191
|
showHidden: !!configuration.show_hidden,
|
|
@@ -179,10 +193,16 @@ function renderShell(startPath, configuration, pluginVersion, csrfToken) {
|
|
|
179
193
|
pdfInline: configuration.pdf_inline !== false,
|
|
180
194
|
exposeSmbLink: configuration.expose_smb_link !== false,
|
|
181
195
|
pageSize: Number(configuration.page_size) || 0,
|
|
182
|
-
title: configuration.title ||
|
|
196
|
+
title: configuration.title || defaultTitle,
|
|
183
197
|
csrfToken: csrfToken || "",
|
|
198
|
+
locale: locale || "en",
|
|
184
199
|
};
|
|
185
200
|
|
|
201
|
+
// Zwei-Stufen-Bootstrap:
|
|
202
|
+
// 1. samba-common.js laden (SambaCommon global) und Katalog setzen
|
|
203
|
+
// 2. samba-filemanager.js laden und mounten
|
|
204
|
+
// Wenn beide Skripte bereits geladen sind, wird nur remounted.
|
|
205
|
+
const catalogJson = serialiseCatalog(catalog);
|
|
186
206
|
return (
|
|
187
207
|
div(
|
|
188
208
|
{ class: "samba-fm card" },
|
|
@@ -201,14 +221,23 @@ function renderShell(startPath, configuration, pluginVersion, csrfToken) {
|
|
|
201
221
|
) +
|
|
202
222
|
script(
|
|
203
223
|
`(function(){
|
|
204
|
-
|
|
224
|
+
var CATALOG=${catalogJson};
|
|
225
|
+
function applyCatalog(){ if(window.SambaCommon && window.SambaCommon.setCatalog) window.SambaCommon.setCatalog(CATALOG); }
|
|
226
|
+
function mount(){window.saltcornSambaMountFM && window.saltcornSambaMountFM(${JSON.stringify(
|
|
205
227
|
id
|
|
206
228
|
)}, ${JSON.stringify(opts)});}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
229
|
+
function loadFM(){
|
|
230
|
+
if(window.saltcornSambaMountFM){ mount(); return; }
|
|
231
|
+
var s=document.createElement('script');
|
|
232
|
+
s.src='/plugins/public/saltcorn-samba@${pluginVersion}/samba-filemanager.js';
|
|
233
|
+
s.onload=mount;
|
|
234
|
+
document.head.appendChild(s);
|
|
235
|
+
}
|
|
236
|
+
if(window.SambaCommon){ applyCatalog(); loadFM(); return; }
|
|
237
|
+
var c=document.createElement('script');
|
|
238
|
+
c.src='/plugins/public/saltcorn-samba@${pluginVersion}/samba-common.js';
|
|
239
|
+
c.onload=function(){ applyCatalog(); loadFM(); };
|
|
240
|
+
document.head.appendChild(c);
|
|
212
241
|
})();`
|
|
213
242
|
)
|
|
214
243
|
);
|
|
@@ -230,9 +259,21 @@ function extractCsrf(extra) {
|
|
|
230
259
|
return "";
|
|
231
260
|
}
|
|
232
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Liest die vom Nutzer bevorzugte Sprache aus dem Request (Saltcorn setzt
|
|
264
|
+
* das aus Login-Cookie / Accept-Language) und liefert Locale + Katalog. Bei
|
|
265
|
+
* Ausfall greift `en` als Notnagel – hardgecodete Strings werden nie leer.
|
|
266
|
+
*/
|
|
267
|
+
function resolveLocaleAndCatalog(extra) {
|
|
268
|
+
const req = extra && extra.req;
|
|
269
|
+
const locale = resolveLocaleFromReq(req);
|
|
270
|
+
return { locale, catalog: catalogFor(locale) };
|
|
271
|
+
}
|
|
272
|
+
|
|
233
273
|
async function run(table_id, viewname, configuration, state, extra) {
|
|
234
274
|
const pluginVersion = configuration.__pluginVersion || "0.3.0";
|
|
235
275
|
const csrf = extractCsrf(extra);
|
|
276
|
+
const { locale, catalog } = resolveLocaleAndCatalog(extra);
|
|
236
277
|
let row = extra && extra.row;
|
|
237
278
|
if (!row && configuration.mode === "from_field" && table_id) {
|
|
238
279
|
const table = await Table.findOne({ id: table_id });
|
|
@@ -250,12 +291,13 @@ async function run(table_id, viewname, configuration, state, extra) {
|
|
|
250
291
|
} catch (e) {
|
|
251
292
|
return div({ class: "alert alert-danger" }, "Samba file manager: " + e.message);
|
|
252
293
|
}
|
|
253
|
-
return renderShell(startPath, configuration, pluginVersion, csrf);
|
|
294
|
+
return renderShell(startPath, configuration, pluginVersion, csrf, locale, catalog);
|
|
254
295
|
}
|
|
255
296
|
|
|
256
297
|
async function runMany(table_id, viewname, configuration, state, extra) {
|
|
257
298
|
const pluginVersion = configuration.__pluginVersion || "0.3.0";
|
|
258
299
|
const csrf = extractCsrf(extra);
|
|
300
|
+
const { locale, catalog } = resolveLocaleAndCatalog(extra);
|
|
259
301
|
const rows = (extra && extra.rows) || [];
|
|
260
302
|
return rows.map((row) => {
|
|
261
303
|
let startPath = "";
|
|
@@ -265,7 +307,7 @@ async function runMany(table_id, viewname, configuration, state, extra) {
|
|
|
265
307
|
startPath = "";
|
|
266
308
|
}
|
|
267
309
|
return {
|
|
268
|
-
html: renderShell(startPath, configuration, pluginVersion, csrf),
|
|
310
|
+
html: renderShell(startPath, configuration, pluginVersion, csrf, locale, catalog),
|
|
269
311
|
row,
|
|
270
312
|
};
|
|
271
313
|
});
|
package/i18n/de.json
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$meta": {
|
|
3
|
+
"language": "Deutsch",
|
|
4
|
+
"locale": "de",
|
|
5
|
+
"note": "Übersetzungskatalog für saltcorn-samba. Punkt-Notation gruppiert nach Bereich."
|
|
6
|
+
},
|
|
7
|
+
|
|
8
|
+
"ui.loading": "Lädt …",
|
|
9
|
+
"ui.close": "Schließen",
|
|
10
|
+
"ui.cancel": "Abbrechen",
|
|
11
|
+
"ui.confirm": "OK",
|
|
12
|
+
"ui.error": "Fehler",
|
|
13
|
+
"ui.empty": "(leeres Verzeichnis)",
|
|
14
|
+
"ui.empty_short": "(leer)",
|
|
15
|
+
"ui.path": "Pfad:",
|
|
16
|
+
"ui.show_hidden": "Versteckte Dateien anzeigen",
|
|
17
|
+
"ui.up": "⬆ Übergeordnet",
|
|
18
|
+
"ui.up_title": "Ein Verzeichnis nach oben",
|
|
19
|
+
"ui.home_title": "Zur Wurzel springen",
|
|
20
|
+
"ui.refresh_title": "Aktualisieren",
|
|
21
|
+
"ui.page_of": "Seite {page} / {total}",
|
|
22
|
+
"ui.prev_page": "‹",
|
|
23
|
+
"ui.next_page": "›",
|
|
24
|
+
|
|
25
|
+
"fm.title_default": "Samba-Dateien",
|
|
26
|
+
"fm.col.filename": "Dateiname",
|
|
27
|
+
"fm.col.media_type": "MIME-Typ",
|
|
28
|
+
"fm.col.size": "Größe",
|
|
29
|
+
"fm.col.modified": "Geändert",
|
|
30
|
+
"fm.col.actions": "Aktionen",
|
|
31
|
+
"fm.counter": "{dirs} Ordner · {files} Dateien",
|
|
32
|
+
"fm.open": "Öffnen",
|
|
33
|
+
"fm.open_new_tab": "In neuem Tab öffnen",
|
|
34
|
+
"fm.view": "Anzeigen",
|
|
35
|
+
"fm.download": "Herunterladen",
|
|
36
|
+
"fm.rename": "Umbenennen",
|
|
37
|
+
"fm.rename_title": "Umbenennen",
|
|
38
|
+
"fm.delete": "Löschen",
|
|
39
|
+
"fm.delete_title": "Löschen",
|
|
40
|
+
"fm.smb_link_title": "Im Datei-Manager öffnen (Nemo/Nautilus/Explorer)",
|
|
41
|
+
"fm.upload": "⬆ Hochladen",
|
|
42
|
+
"fm.upload_title": "Dateien in dieses Verzeichnis hochladen",
|
|
43
|
+
"fm.new_folder": "+ Ordner",
|
|
44
|
+
"fm.new_folder_title": "Neuen Ordner anlegen",
|
|
45
|
+
|
|
46
|
+
"fm.confirm_delete.title": "Löschen bestätigen",
|
|
47
|
+
"fm.confirm_delete.prompt_prefix": "„",
|
|
48
|
+
"fm.confirm_delete.prompt_suffix": "\" wirklich löschen?",
|
|
49
|
+
"fm.confirm_delete.dir_hint": "Klappt nur, wenn das Verzeichnis leer ist.",
|
|
50
|
+
"fm.confirm_delete.file_hint": "Dieser Vorgang kann nicht rückgängig gemacht werden.",
|
|
51
|
+
"fm.deleted_ok": "„{name}\" gelöscht",
|
|
52
|
+
"fm.deleted_fail": "Löschen fehlgeschlagen: {msg}",
|
|
53
|
+
|
|
54
|
+
"fm.rename.title": "„{name}\" umbenennen",
|
|
55
|
+
"fm.rename.label": "Neuer Name",
|
|
56
|
+
"fm.rename.hint": "Slashes sind nicht erlaubt. Zum Verschieben stattdessen die ↗-Funktion nutzen.",
|
|
57
|
+
"fm.renamed_ok": "Umbenannt in „{name}\"",
|
|
58
|
+
"fm.rename_fail": "Umbenennen fehlgeschlagen: {msg}",
|
|
59
|
+
|
|
60
|
+
"fm.mkdir.title": "Ordner anlegen in {path}",
|
|
61
|
+
"fm.mkdir.label": "Ordnername",
|
|
62
|
+
"fm.mkdir.placeholder": "Neuer Ordnername",
|
|
63
|
+
"fm.mkdir.create": "Anlegen",
|
|
64
|
+
"fm.mkdir_ok": "Ordner „{name}\" angelegt",
|
|
65
|
+
"fm.mkdir_fail": "Ordner anlegen fehlgeschlagen: {msg}",
|
|
66
|
+
|
|
67
|
+
"fm.upload.title": "Dateien hochladen",
|
|
68
|
+
"fm.upload.dropzone": "Dateien hierher ziehen oder klicken zum Auswählen",
|
|
69
|
+
"fm.upload.into_prefix": "Ziel: ",
|
|
70
|
+
"fm.upload.limit": ". Max. {mb} MiB pro Datei.",
|
|
71
|
+
"fm.upload.or_pick": "…oder Dateien auswählen",
|
|
72
|
+
"fm.upload.overwrite": "Vorhandene Dateien überschreiben",
|
|
73
|
+
"fm.upload.button": "Hochladen",
|
|
74
|
+
"fm.upload.uploading": "Wird hochgeladen …",
|
|
75
|
+
"fm.upload.no_files": "Keine Dateien ausgewählt",
|
|
76
|
+
"fm.upload.ok": "{n} Datei(en) hochgeladen",
|
|
77
|
+
"fm.upload.partial_summary": "{ok} von {total} Dateien hochgeladen. Fehler:",
|
|
78
|
+
"fm.upload.some_failed": "Einige Uploads fehlgeschlagen",
|
|
79
|
+
"fm.upload.failed": "Upload fehlgeschlagen: {msg}",
|
|
80
|
+
"fm.upload.failed_short": "Upload fehlgeschlagen",
|
|
81
|
+
|
|
82
|
+
"tree.open_in_fm": "Im Datei-Manager öffnen",
|
|
83
|
+
"tree.open_in_fm_title": "Im Datei-Manager öffnen",
|
|
84
|
+
"tree.error_prefix": "Fehler: ",
|
|
85
|
+
"tree.samba_prefix": "Samba: ",
|
|
86
|
+
"tree.open_new_tab": "In neuem Tab öffnen",
|
|
87
|
+
|
|
88
|
+
"pdf.download": "Herunterladen",
|
|
89
|
+
"pdf.open_in_fm": "Im Datei-Manager öffnen",
|
|
90
|
+
"pdf.open_in_fm_title": "In Nemo/Nautilus/Explorer öffnen",
|
|
91
|
+
"pdf.open_new_tab": "In neuem Tab öffnen",
|
|
92
|
+
"pdf.empty": "—"
|
|
93
|
+
}
|
package/i18n/en.json
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$meta": {
|
|
3
|
+
"language": "English",
|
|
4
|
+
"locale": "en",
|
|
5
|
+
"note": "Translation catalogue for saltcorn-samba. Dot notation groups keys by area."
|
|
6
|
+
},
|
|
7
|
+
|
|
8
|
+
"ui.loading": "Loading…",
|
|
9
|
+
"ui.close": "Close",
|
|
10
|
+
"ui.cancel": "Cancel",
|
|
11
|
+
"ui.confirm": "OK",
|
|
12
|
+
"ui.error": "Error",
|
|
13
|
+
"ui.empty": "(empty directory)",
|
|
14
|
+
"ui.empty_short": "(empty)",
|
|
15
|
+
"ui.path": "Path:",
|
|
16
|
+
"ui.show_hidden": "Show hidden",
|
|
17
|
+
"ui.up": "⬆ Up",
|
|
18
|
+
"ui.up_title": "Up one directory",
|
|
19
|
+
"ui.home_title": "Go to root",
|
|
20
|
+
"ui.refresh_title": "Refresh",
|
|
21
|
+
"ui.page_of": "Page {page} / {total}",
|
|
22
|
+
"ui.prev_page": "‹",
|
|
23
|
+
"ui.next_page": "›",
|
|
24
|
+
|
|
25
|
+
"fm.title_default": "Samba files",
|
|
26
|
+
"fm.col.filename": "Filename",
|
|
27
|
+
"fm.col.media_type": "Media type",
|
|
28
|
+
"fm.col.size": "Size",
|
|
29
|
+
"fm.col.modified": "Modified",
|
|
30
|
+
"fm.col.actions": "Actions",
|
|
31
|
+
"fm.counter": "{dirs} folders · {files} files",
|
|
32
|
+
"fm.open": "Open",
|
|
33
|
+
"fm.open_new_tab": "Open in new tab",
|
|
34
|
+
"fm.view": "View",
|
|
35
|
+
"fm.download": "Download",
|
|
36
|
+
"fm.rename": "Rename",
|
|
37
|
+
"fm.rename_title": "Rename",
|
|
38
|
+
"fm.delete": "Delete",
|
|
39
|
+
"fm.delete_title": "Delete",
|
|
40
|
+
"fm.smb_link_title": "Open in file manager (Nemo/Nautilus/Explorer)",
|
|
41
|
+
"fm.upload": "⬆ Upload",
|
|
42
|
+
"fm.upload_title": "Upload files to this directory",
|
|
43
|
+
"fm.new_folder": "+ Folder",
|
|
44
|
+
"fm.new_folder_title": "Create new folder",
|
|
45
|
+
|
|
46
|
+
"fm.confirm_delete.title": "Confirm delete",
|
|
47
|
+
"fm.confirm_delete.prompt_prefix": "Delete \"",
|
|
48
|
+
"fm.confirm_delete.prompt_suffix": "\"?",
|
|
49
|
+
"fm.confirm_delete.dir_hint": "This will only succeed if the directory is empty.",
|
|
50
|
+
"fm.confirm_delete.file_hint": "This action cannot be undone.",
|
|
51
|
+
"fm.deleted_ok": "Deleted {name}",
|
|
52
|
+
"fm.deleted_fail": "Delete failed: {msg}",
|
|
53
|
+
|
|
54
|
+
"fm.rename.title": "Rename \"{name}\"",
|
|
55
|
+
"fm.rename.label": "New name",
|
|
56
|
+
"fm.rename.hint": "Slashes are not allowed. To move, use the ↗ file-manager link instead.",
|
|
57
|
+
"fm.renamed_ok": "Renamed to {name}",
|
|
58
|
+
"fm.rename_fail": "Rename failed: {msg}",
|
|
59
|
+
|
|
60
|
+
"fm.mkdir.title": "Create folder in {path}",
|
|
61
|
+
"fm.mkdir.label": "Folder name",
|
|
62
|
+
"fm.mkdir.placeholder": "New folder name",
|
|
63
|
+
"fm.mkdir.create": "Create",
|
|
64
|
+
"fm.mkdir_ok": "Created folder {name}",
|
|
65
|
+
"fm.mkdir_fail": "Create folder failed: {msg}",
|
|
66
|
+
|
|
67
|
+
"fm.upload.title": "Upload files",
|
|
68
|
+
"fm.upload.dropzone": "Drop files here or click to select",
|
|
69
|
+
"fm.upload.into_prefix": "Uploading to ",
|
|
70
|
+
"fm.upload.limit": ". Max {mb} MiB per file.",
|
|
71
|
+
"fm.upload.or_pick": "…or pick files",
|
|
72
|
+
"fm.upload.overwrite": "Overwrite existing files",
|
|
73
|
+
"fm.upload.button": "Upload",
|
|
74
|
+
"fm.upload.uploading": "Uploading…",
|
|
75
|
+
"fm.upload.no_files": "No files selected",
|
|
76
|
+
"fm.upload.ok": "Uploaded {n} file(s)",
|
|
77
|
+
"fm.upload.partial_summary": "Uploaded {ok} / {total} files. Failures:",
|
|
78
|
+
"fm.upload.some_failed": "Some uploads failed",
|
|
79
|
+
"fm.upload.failed": "Upload failed: {msg}",
|
|
80
|
+
"fm.upload.failed_short": "Upload failed",
|
|
81
|
+
|
|
82
|
+
"tree.open_in_fm": "Open in file manager",
|
|
83
|
+
"tree.open_in_fm_title": "Open in file manager",
|
|
84
|
+
"tree.error_prefix": "Error: ",
|
|
85
|
+
"tree.samba_prefix": "Samba: ",
|
|
86
|
+
"tree.open_new_tab": "Open in new tab",
|
|
87
|
+
|
|
88
|
+
"pdf.download": "Download",
|
|
89
|
+
"pdf.open_in_fm": "Open in file manager",
|
|
90
|
+
"pdf.open_in_fm_title": "Open in Nemo/Nautilus/Explorer",
|
|
91
|
+
"pdf.open_new_tab": "Open in new tab",
|
|
92
|
+
"pdf.empty": "—"
|
|
93
|
+
}
|
package/i18n.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* saltcorn-samba – Minimales i18n-Modul (Server-Seite).
|
|
3
|
+
*
|
|
4
|
+
* Design-Ziele:
|
|
5
|
+
* - Keine externe Dependency (kein i18next, keine intl-messageformat).
|
|
6
|
+
* - JSON-Kataloge unter ./i18n/<locale>.json.
|
|
7
|
+
* - Lazy load + Cache; unbekannte Locales fallen automatisch auf `en`.
|
|
8
|
+
* - Placeholder-Syntax {name} wird durch die übergebenen Werte ersetzt.
|
|
9
|
+
*
|
|
10
|
+
* Locale-Auflösung (in dieser Reihenfolge):
|
|
11
|
+
* 1. Explizit übergebener Wert (Request-Query ?locale=de).
|
|
12
|
+
* 2. `req.getLocale()` (Saltcorn setzt das aus Cookie / Accept-Language).
|
|
13
|
+
* 3. `req.headers["accept-language"]` – erste passende Locale.
|
|
14
|
+
* 4. Fallback = `en`.
|
|
15
|
+
*
|
|
16
|
+
* Die Client-Seite bekommt den passenden Katalog via Route
|
|
17
|
+
* GET /samba-i18n.json?locale=xx
|
|
18
|
+
* (siehe index.js). Dort ruft samba-common.js `SambaCommon.setCatalog(...)` auf.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
"use strict";
|
|
22
|
+
|
|
23
|
+
const fs = require("fs");
|
|
24
|
+
const path = require("path");
|
|
25
|
+
|
|
26
|
+
const CATALOG_DIR = path.join(__dirname, "i18n");
|
|
27
|
+
const DEFAULT_LOCALE = "en";
|
|
28
|
+
const catalogs = Object.create(null); // locale -> { key: str }
|
|
29
|
+
const availableLocales = new Set(["de", "en"]);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Load a catalogue from disk. Cached forever (plugin restart to reload).
|
|
33
|
+
* Missing files silently return {} — the caller falls back to the key itself.
|
|
34
|
+
*/
|
|
35
|
+
function loadCatalog(locale) {
|
|
36
|
+
if (catalogs[locale]) return catalogs[locale];
|
|
37
|
+
const file = path.join(CATALOG_DIR, locale + ".json");
|
|
38
|
+
try {
|
|
39
|
+
const raw = fs.readFileSync(file, "utf8");
|
|
40
|
+
const parsed = JSON.parse(raw);
|
|
41
|
+
delete parsed.$meta; // metadata is not a translation
|
|
42
|
+
catalogs[locale] = parsed;
|
|
43
|
+
} catch (_) {
|
|
44
|
+
catalogs[locale] = {};
|
|
45
|
+
}
|
|
46
|
+
return catalogs[locale];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve a locale string to one we actually ship a catalogue for.
|
|
51
|
+
* Accepts full IETF tags ("de-DE", "en-US") and language-only ("de", "en").
|
|
52
|
+
*/
|
|
53
|
+
function normaliseLocale(raw) {
|
|
54
|
+
if (!raw) return DEFAULT_LOCALE;
|
|
55
|
+
const lc = String(raw).toLowerCase().trim();
|
|
56
|
+
if (availableLocales.has(lc)) return lc;
|
|
57
|
+
const short = lc.split(/[-_]/)[0];
|
|
58
|
+
if (availableLocales.has(short)) return short;
|
|
59
|
+
return DEFAULT_LOCALE;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Extract the caller's preferred locale from an Express-like request object.
|
|
64
|
+
* Never throws; falls back to DEFAULT_LOCALE.
|
|
65
|
+
*/
|
|
66
|
+
function resolveLocaleFromReq(req, explicit) {
|
|
67
|
+
if (explicit) return normaliseLocale(explicit);
|
|
68
|
+
try {
|
|
69
|
+
if (req) {
|
|
70
|
+
// Saltcorn / i18next-express-middleware
|
|
71
|
+
if (typeof req.getLocale === "function") {
|
|
72
|
+
const l = req.getLocale();
|
|
73
|
+
if (l) return normaliseLocale(l);
|
|
74
|
+
}
|
|
75
|
+
// Explicit query ?locale=de
|
|
76
|
+
if (req.query && req.query.locale) {
|
|
77
|
+
return normaliseLocale(req.query.locale);
|
|
78
|
+
}
|
|
79
|
+
// Accept-Language header – take the first tag we understand.
|
|
80
|
+
const hdr = req.headers && req.headers["accept-language"];
|
|
81
|
+
if (hdr) {
|
|
82
|
+
const first = String(hdr).split(",")[0].split(";")[0];
|
|
83
|
+
return normaliseLocale(first);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} catch (_) {
|
|
87
|
+
// fall through
|
|
88
|
+
}
|
|
89
|
+
return DEFAULT_LOCALE;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Substitute {placeholder} tokens in the message. Missing values render as "".
|
|
94
|
+
*/
|
|
95
|
+
function interpolate(msg, params) {
|
|
96
|
+
if (!params) return msg;
|
|
97
|
+
return String(msg).replace(/\{(\w+)\}/g, (_, key) =>
|
|
98
|
+
Object.prototype.hasOwnProperty.call(params, key) ? String(params[key]) : ""
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Translate a single key. Falls back to English, then to the key itself so
|
|
104
|
+
* a missing translation never breaks the UI.
|
|
105
|
+
*
|
|
106
|
+
* t("fm.upload.button", { locale: "de" }) → "Hochladen"
|
|
107
|
+
* t("fm.deleted_ok", { locale: "de", name: "foo.txt" }) → "„foo.txt\" gelöscht"
|
|
108
|
+
*/
|
|
109
|
+
function t(key, params) {
|
|
110
|
+
const opts = params || {};
|
|
111
|
+
const locale = normaliseLocale(opts.locale);
|
|
112
|
+
const primary = loadCatalog(locale);
|
|
113
|
+
if (Object.prototype.hasOwnProperty.call(primary, key)) {
|
|
114
|
+
return interpolate(primary[key], opts);
|
|
115
|
+
}
|
|
116
|
+
if (locale !== DEFAULT_LOCALE) {
|
|
117
|
+
const fallback = loadCatalog(DEFAULT_LOCALE);
|
|
118
|
+
if (Object.prototype.hasOwnProperty.call(fallback, key)) {
|
|
119
|
+
return interpolate(fallback[key], opts);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return key;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Bind a locale so callers can write `const _ = tFor("de"); _("ui.close")`.
|
|
127
|
+
*/
|
|
128
|
+
function tFor(locale) {
|
|
129
|
+
const loc = normaliseLocale(locale);
|
|
130
|
+
return function boundTranslate(key, params) {
|
|
131
|
+
return t(key, Object.assign({}, params || {}, { locale: loc }));
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Return the raw catalogue for a locale, merged over the English defaults.
|
|
137
|
+
* Used to ship a single JSON blob to the browser.
|
|
138
|
+
*/
|
|
139
|
+
function catalogFor(locale) {
|
|
140
|
+
const loc = normaliseLocale(locale);
|
|
141
|
+
const en = loadCatalog(DEFAULT_LOCALE);
|
|
142
|
+
if (loc === DEFAULT_LOCALE) return Object.assign({}, en);
|
|
143
|
+
const merged = Object.assign({}, en, loadCatalog(loc));
|
|
144
|
+
return merged;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = {
|
|
148
|
+
DEFAULT_LOCALE,
|
|
149
|
+
availableLocales: Array.from(availableLocales),
|
|
150
|
+
t,
|
|
151
|
+
tFor,
|
|
152
|
+
catalogFor,
|
|
153
|
+
resolveLocaleFromReq,
|
|
154
|
+
normaliseLocale,
|
|
155
|
+
};
|