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 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
@@ -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
- function renderShell(startPath, configuration, pluginVersion, csrfToken) {
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 || "Samba files",
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
- function boot(){window.saltcornSambaMountFM && window.saltcornSambaMountFM(${JSON.stringify(
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
- if(window.saltcornSambaMountFM){boot();return;}
208
- var s=document.createElement('script');
209
- s.src='/plugins/public/saltcorn-samba@${pluginVersion}/samba-filemanager.js';
210
- s.onload=boot;
211
- document.head.appendChild(s);
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
+ };