@attraccess/plugin-shelly 0.1.0-nightly.1006.1 → 0.1.0-nightly.1016.1

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.
@@ -1,5 +1,7 @@
1
1
  import { r as importShared } from "./_virtual___federation_fn_import-BXODRNto.js";
2
- import { t as require_jsx_runtime } from "./jsx-runtime-B9Qmn8k7.js";
2
+ //#region \0rolldown/runtime.js
3
+ var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
4
+ //#endregion
3
5
  //#region ../../../libs/plugins-frontend-sdk/src/lib/frontend.api-client.ts
4
6
  var BASE_URL_GLOBAL = "__ATTRACCESS_API_BASE_URL__";
5
7
  /** The API origin the host is talking to. Falls back to the current origin. */
@@ -60,6 +62,48 @@ function createPluginApiClient(basePath = "") {
60
62
  };
61
63
  }
62
64
  //#endregion
65
+ //#region ../../../node_modules/.pnpm/react@19.2.8/node_modules/react/cjs/react-jsx-runtime.production.js
66
+ /**
67
+ * @license React
68
+ * react-jsx-runtime.production.js
69
+ *
70
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
71
+ *
72
+ * This source code is licensed under the MIT license found in the
73
+ * LICENSE file in the root directory of this source tree.
74
+ */
75
+ var require_react_jsx_runtime_production = /* @__PURE__ */ __commonJSMin(((exports) => {
76
+ var REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element");
77
+ function jsxProd(type, config, maybeKey) {
78
+ var key = null;
79
+ void 0 !== maybeKey && (key = "" + maybeKey);
80
+ void 0 !== config.key && (key = "" + config.key);
81
+ if ("key" in config) {
82
+ maybeKey = {};
83
+ for (var propName in config) "key" !== propName && (maybeKey[propName] = config[propName]);
84
+ } else maybeKey = config;
85
+ config = maybeKey.ref;
86
+ return {
87
+ $$typeof: REACT_ELEMENT_TYPE,
88
+ type,
89
+ key,
90
+ ref: void 0 !== config ? config : null,
91
+ props: maybeKey
92
+ };
93
+ }
94
+ exports.jsx = jsxProd;
95
+ exports.jsxs = jsxProd;
96
+ }));
97
+ //#endregion
98
+ //#region ../../../libs/plugins-frontend-sdk/src/lib/frontend.standard-drawer.tsx
99
+ var import_jsx_runtime = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
100
+ module.exports = require_react_jsx_runtime_production();
101
+ })))();
102
+ var { DrawerBackdrop: DrawerBackdrop$1, DrawerContent: DrawerContent$1, DrawerDialog: DrawerDialog$1 } = await importShared("@heroui/react");
103
+ //#endregion
104
+ //#region ../../../libs/plugins-frontend-sdk/src/lib/frontend.standard-modal.tsx
105
+ var { Modal: Modal$1, ModalBackdrop: ModalBackdrop$1, ModalContainer: ModalContainer$1, ModalDialog: ModalDialog$1 } = await importShared("@heroui/react");
106
+ //#endregion
63
107
  //#region frontend/src/api.ts
64
108
  var api = createPluginApiClient("/api/shelly");
65
109
  function listDevices() {
@@ -117,9 +161,279 @@ function discoverDevices(input = {}) {
117
161
  body: input
118
162
  });
119
163
  }
164
+ var en_default = {
165
+ devices: {
166
+ "title": "Shelly Devices",
167
+ "table": "Shelly devices",
168
+ "description": "Discovered and manually added Shelly devices.",
169
+ "discover": "Discover",
170
+ "add": "Add device",
171
+ "loadError": "Failed to load devices",
172
+ "device": "Device",
173
+ "address": "Address",
174
+ "model": "Model",
175
+ "auth": "Auth",
176
+ "firmware": "Firmware",
177
+ "actions": "Actions",
178
+ "unknown": "Unknown",
179
+ "generation": "Gen {{generation}}",
180
+ "generationPlus": "Gen {{generation}}+",
181
+ "empty": "No devices yet. Run discovery, or add your first Shelly by its IP.",
182
+ "probeError": "Probe failed: {{message}}",
183
+ "info": "View device info",
184
+ "more": "More actions",
185
+ "deviceActions": "Device actions",
186
+ "manageFirmware": "Manage firmware",
187
+ "password": "Set admin password",
188
+ "reprobe": "Re-probe device",
189
+ "delete": "Delete device",
190
+ "deleteDescription": "Remove {{name}} ({{address}}) from the registry? The device itself is not changed."
191
+ },
192
+ auth: {
193
+ "none": "No auth",
194
+ "required": "Auth required",
195
+ "unknown": "Unknown"
196
+ },
197
+ add: {
198
+ "title": "Add a device",
199
+ "description": "Enter the device's IP address. We probe GET /shelly to detect its generation and model.",
200
+ "ip": "IP address",
201
+ "name": "Name (optional)",
202
+ "placeholder": "Workshop light",
203
+ "error": "Could not add the device",
204
+ "required": "IP address is required."
205
+ },
206
+ password: {
207
+ "title": "Admin password",
208
+ "description": "Set or change the admin password of {{name}} ({{address}}).",
209
+ "current": "Current password",
210
+ "currentDescription": "Required because this device already has authentication enabled.",
211
+ "new": "New admin password",
212
+ "newDescription": "Protects the device's local web interface and API.",
213
+ "error": "Could not set password",
214
+ "required": "New password is required.",
215
+ "save": "Save password",
216
+ "show": "Show password",
217
+ "hide": "Hide password"
218
+ },
219
+ discovery: {
220
+ "title": "Discover devices",
221
+ "description": "Listens for Shelly devices announcing over mDNS, then probes every address of a subnet. Everything found is added to the registry.",
222
+ "subnet": "Subnet to scan (optional)",
223
+ "subnetDescription": "Leave empty to scan the server's own networks. Private ranges only, /22 at most. Required when Attraccess runs in a container, since its network is not your LAN.",
224
+ "error": "Discovery failed",
225
+ "running": "Probing addresses — a /24 takes a few seconds.",
226
+ "start": "Start discovery",
227
+ "found": "Found {{count}}, added {{added}}",
228
+ "empty": "No devices found",
229
+ "mdnsOnly": "no subnet (mDNS only)",
230
+ "probed": {
231
+ "one": "Probed {{count}} address in {{subnets}}.",
232
+ "many": "Probed {{count}} addresses in {{subnets}}."
233
+ },
234
+ "hint": "Nothing answered GET /shelly. If Attraccess runs in a container, enter the subnet your devices are on — the container's own network is not your LAN.",
235
+ "via": "via {{source}}",
236
+ "scan": "subnet scan",
237
+ "added": "Added",
238
+ "known": "Already known"
239
+ },
240
+ info: {
241
+ "title": "Device info",
242
+ "loadError": "Could not load device info",
243
+ "auth": "This device requires authentication. Enter its admin password to read protected info.",
244
+ "passwordPlaceholder": "device admin password",
245
+ "load": "Load info",
246
+ "notReported": "Not reported",
247
+ "on": "On",
248
+ "off": "Off",
249
+ "name": "Name",
250
+ "generation": "Generation",
251
+ "timezone": "Timezone",
252
+ "uptime": "Uptime",
253
+ "network": "Network",
254
+ "wifi": "Wi-Fi network",
255
+ "signal": "Signal",
256
+ "output": "Output",
257
+ "state": "State",
258
+ "power": "Power",
259
+ "voltage": "Voltage",
260
+ "current": "Current",
261
+ "updated": "Updated {{time}}"
262
+ },
263
+ firmware: {
264
+ "checking": "Checking…",
265
+ "unavailable": "Unavailable",
266
+ "available": "{{version}} available",
267
+ "updateAvailable": "Firmware update available: {{version}}",
268
+ "empty": "No firmware info loaded yet.",
269
+ "installed": "Installed",
270
+ "stable": "Stable channel",
271
+ "beta": "Beta channel",
272
+ "upToDate": "Up to date",
273
+ "nothingNewer": "Nothing newer",
274
+ "checked": "Checked",
275
+ "description": "Check for and install firmware updates on {{name}} ({{address}}).",
276
+ "error": "Firmware check failed",
277
+ "running": "Update running",
278
+ "installing": "Installing the {{stage}} firmware{{version}}. The device reboots during the update and is offline for a moment — this page keeps checking.",
279
+ "finished": "Update finished",
280
+ "success": "The device now runs {{version}}.",
281
+ "recheck": "Check again",
282
+ "installLabel": "Install {{stage}} firmware {{version}}",
283
+ "install": "Install {{stage}} {{version}}",
284
+ "stableStage": "stable",
285
+ "betaStage": "beta",
286
+ "timeout": "The device did not report the expected firmware version within 5 minutes. Check it and re-check the firmware manually."
287
+ },
288
+ common: {
289
+ "cancel": "Cancel",
290
+ "close": "Close",
291
+ "delete": "Delete",
292
+ "done": "Done",
293
+ "refresh": "Refresh"
294
+ }
295
+ };
296
+ var de_default = {
297
+ devices: {
298
+ "title": "Shelly-Geräte",
299
+ "table": "Shelly-Geräte",
300
+ "description": "Automatisch erkannte und manuell hinzugefügte Shelly-Geräte.",
301
+ "discover": "Suchen",
302
+ "add": "Gerät hinzufügen",
303
+ "loadError": "Geräte konnten nicht geladen werden",
304
+ "device": "Gerät",
305
+ "address": "Adresse",
306
+ "model": "Modell",
307
+ "auth": "Authentifizierung",
308
+ "firmware": "Firmware",
309
+ "actions": "Aktionen",
310
+ "unknown": "Unbekannt",
311
+ "generation": "Gen {{generation}}",
312
+ "generationPlus": "Gen {{generation}}+",
313
+ "empty": "Noch keine Geräte vorhanden. Starte die Suche oder füge dein erstes Shelly-Gerät über seine IP-Adresse hinzu.",
314
+ "probeError": "Geräteprüfung fehlgeschlagen: {{message}}",
315
+ "info": "Geräteinformationen anzeigen",
316
+ "more": "Weitere Aktionen",
317
+ "deviceActions": "Geräteaktionen",
318
+ "manageFirmware": "Firmware verwalten",
319
+ "password": "Admin-Passwort festlegen",
320
+ "reprobe": "Gerät erneut prüfen",
321
+ "delete": "Gerät löschen",
322
+ "deleteDescription": "{{name}} ({{address}}) aus der Registrierung entfernen? Das Gerät selbst wird nicht verändert."
323
+ },
324
+ auth: {
325
+ "none": "Keine Authentifizierung",
326
+ "required": "Authentifizierung erforderlich",
327
+ "unknown": "Unbekannt"
328
+ },
329
+ add: {
330
+ "title": "Gerät hinzufügen",
331
+ "description": "Gib die IP-Adresse des Geräts ein. Wir prüfen GET /shelly, um Generation und Modell zu erkennen.",
332
+ "ip": "IP-Adresse",
333
+ "name": "Name (optional)",
334
+ "placeholder": "Werkstattbeleuchtung",
335
+ "error": "Gerät konnte nicht hinzugefügt werden",
336
+ "required": "Eine IP-Adresse ist erforderlich."
337
+ },
338
+ password: {
339
+ "title": "Admin-Passwort",
340
+ "description": "Admin-Passwort von {{name}} ({{address}}) festlegen oder ändern.",
341
+ "current": "Aktuelles Passwort",
342
+ "currentDescription": "Erforderlich, da für dieses Gerät bereits die Authentifizierung aktiviert ist.",
343
+ "new": "Neues Admin-Passwort",
344
+ "newDescription": "Schützt die lokale Weboberfläche und API des Geräts.",
345
+ "error": "Passwort konnte nicht festgelegt werden",
346
+ "required": "Ein neues Passwort ist erforderlich.",
347
+ "save": "Passwort speichern",
348
+ "show": "Passwort anzeigen",
349
+ "hide": "Passwort verbergen"
350
+ },
351
+ discovery: {
352
+ "title": "Geräte suchen",
353
+ "description": "Sucht nach Shelly-Geräten, die sich über mDNS melden, und prüft anschließend jede Adresse eines Subnetzes. Gefundene Geräte werden registriert.",
354
+ "subnet": "Zu durchsuchendes Subnetz (optional)",
355
+ "subnetDescription": "Leer lassen, um die Netzwerke des Servers zu durchsuchen. Nur private Bereiche, maximal /22. Bei Attraccess in einem Container erforderlich, da dessen Netzwerk nicht dein LAN ist.",
356
+ "error": "Gerätesuche fehlgeschlagen",
357
+ "running": "Adressen werden geprüft – ein /24-Netz dauert einige Sekunden.",
358
+ "start": "Suche starten",
359
+ "found": "{{count}} gefunden, {{added}} hinzugefügt",
360
+ "empty": "Keine Geräte gefunden",
361
+ "mdnsOnly": "kein Subnetz (nur mDNS)",
362
+ "probed": {
363
+ "one": "{{count}} Adresse in {{subnets}} geprüft.",
364
+ "many": "{{count}} Adressen in {{subnets}} geprüft."
365
+ },
366
+ "hint": "Kein Gerät hat auf GET /shelly geantwortet. Wenn Attraccess in einem Container läuft, gib das Subnetz deiner Geräte an. Das Netzwerk des Containers ist nicht dein LAN.",
367
+ "via": "über {{source}}",
368
+ "scan": "Subnetzsuche",
369
+ "added": "Hinzugefügt",
370
+ "known": "Bereits bekannt"
371
+ },
372
+ info: {
373
+ "title": "Geräteinformationen",
374
+ "loadError": "Geräteinformationen konnten nicht geladen werden",
375
+ "auth": "Dieses Gerät erfordert eine Authentifizierung. Gib sein Admin-Passwort ein, um geschützte Informationen zu lesen.",
376
+ "passwordPlaceholder": "Admin-Passwort des Geräts",
377
+ "load": "Informationen laden",
378
+ "notReported": "Nicht gemeldet",
379
+ "on": "Ein",
380
+ "off": "Aus",
381
+ "name": "Name",
382
+ "generation": "Generation",
383
+ "timezone": "Zeitzone",
384
+ "uptime": "Betriebszeit",
385
+ "network": "Netzwerk",
386
+ "wifi": "WLAN-Netzwerk",
387
+ "signal": "Signal",
388
+ "output": "Ausgang",
389
+ "state": "Zustand",
390
+ "power": "Leistung",
391
+ "voltage": "Spannung",
392
+ "current": "Stromstärke",
393
+ "updated": "Aktualisiert {{time}}"
394
+ },
395
+ firmware: {
396
+ "checking": "Wird geprüft…",
397
+ "unavailable": "Nicht verfügbar",
398
+ "available": "{{version}} verfügbar",
399
+ "updateAvailable": "Firmware-Aktualisierung verfügbar: {{version}}",
400
+ "empty": "Noch keine Firmware-Informationen geladen.",
401
+ "installed": "Installiert",
402
+ "stable": "Stabiler Kanal",
403
+ "beta": "Beta-Kanal",
404
+ "upToDate": "Aktuell",
405
+ "nothingNewer": "Keine neuere Version",
406
+ "checked": "Geprüft",
407
+ "description": "Firmware-Aktualisierungen für {{name}} ({{address}}) prüfen und installieren.",
408
+ "error": "Firmware-Prüfung fehlgeschlagen",
409
+ "running": "Aktualisierung läuft",
410
+ "installing": "Firmware des Kanals {{stage}}{{version}} wird installiert. Das Gerät startet während der Aktualisierung neu und ist kurzzeitig offline. Diese Seite prüft weiterhin den Status.",
411
+ "finished": "Aktualisierung abgeschlossen",
412
+ "success": "Auf dem Gerät läuft jetzt {{version}}.",
413
+ "recheck": "Erneut prüfen",
414
+ "installLabel": "Firmware {{stage}} {{version}} installieren",
415
+ "install": "{{stage}} {{version}} installieren",
416
+ "stableStage": "stabil",
417
+ "betaStage": "Beta",
418
+ "timeout": "Das Gerät hat die erwartete Firmware-Version nicht innerhalb von 5 Minuten gemeldet. Prüfe das Gerät und die Firmware manuell erneut."
419
+ },
420
+ common: {
421
+ "cancel": "Abbrechen",
422
+ "close": "Schließen",
423
+ "delete": "Löschen",
424
+ "done": "Fertig",
425
+ "refresh": "Aktualisieren"
426
+ }
427
+ };
428
+ //#endregion
429
+ //#region frontend/src/i18n.ts
430
+ var { useTranslations } = await importShared("@attraccess/plugins-frontend-ui");
431
+ var useShellyTranslations = () => useTranslations({
432
+ en: en_default,
433
+ de: de_default
434
+ }, { escapeValues: false });
120
435
  //#endregion
121
436
  //#region frontend/src/drawer.tsx
122
- var import_jsx_runtime = require_jsx_runtime();
123
437
  var { Button: Button$6, Description, DrawerBackdrop, DrawerContent, DrawerDialog, Input, InputGroup, Label, TextField, Tooltip: Tooltip$2 } = await importShared("@heroui/react");
124
438
  var { EyeIcon: EyeIcon$1, EyeOffIcon: EyeOffIcon$1 } = await importShared("lucide-react");
125
439
  var { useState: useState$6 } = await importShared("react");
@@ -159,8 +473,9 @@ function TextFieldRow({ label, value, onChange, placeholder, required, descripti
159
473
  });
160
474
  }
161
475
  function PasswordFieldRow({ label, value, onChange, description, required, dataCy, autoComplete }) {
476
+ const { t } = useShellyTranslations();
162
477
  const [visible, setVisible] = useState$6(false);
163
- const toggleLabel = visible ? "Hide password" : "Show password";
478
+ const toggleLabel = t(visible ? "password.hide" : "password.show");
164
479
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TextField, {
165
480
  value,
166
481
  onChange,
@@ -209,6 +524,7 @@ var { Button: Button$5, DrawerBody: DrawerBody$4, DrawerHeader: DrawerHeader$4,
209
524
  var { PlusIcon: PlusIcon$1, XIcon: XIcon$4 } = await importShared("lucide-react");
210
525
  var { useCallback: useCallback$5, useRef: useRef$2, useState: useState$5 } = await importShared("react");
211
526
  function AddDeviceDrawer({ isOpen, onOpenChange, onAdded }) {
527
+ const { t, tMessage } = useShellyTranslations();
212
528
  const [ipAddress, setIpAddress] = useState$5("");
213
529
  const [name, setName] = useState$5("");
214
530
  const [submitting, setSubmitting] = useState$5(false);
@@ -219,7 +535,7 @@ function AddDeviceDrawer({ isOpen, onOpenChange, onAdded }) {
219
535
  if (inFlight.current) return;
220
536
  const ip = ipAddress.trim();
221
537
  if (!ip) {
222
- setError("IP address is required.");
538
+ setError({ key: "add.required" });
223
539
  return;
224
540
  }
225
541
  inFlight.current = true;
@@ -255,19 +571,15 @@ function AddDeviceDrawer({ isOpen, onOpenChange, onAdded }) {
255
571
  className: "sh:flex sh:flex-col sh:gap-1",
256
572
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", {
257
573
  className: "sh:text-lg sh:font-semibold",
258
- children: "Add a device"
259
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
574
+ children: t("add.title")
575
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
260
576
  className: "sh:text-sm sh:text-muted",
261
- children: [
262
- "Enter the device's IP address. We probe ",
263
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: "GET /shelly" }),
264
- " to detect its generation and model."
265
- ]
577
+ children: t("add.description")
266
578
  })]
267
579
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$5, {
268
580
  isIconOnly: true,
269
581
  variant: "ghost",
270
- "aria-label": "Close",
582
+ "aria-label": t("common.close"),
271
583
  onPress: close,
272
584
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(XIcon$4, { size: 16 })
273
585
  })]
@@ -276,7 +588,7 @@ function AddDeviceDrawer({ isOpen, onOpenChange, onAdded }) {
276
588
  className: "sh:flex sh:flex-col sh:gap-4",
277
589
  children: [
278
590
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TextFieldRow, {
279
- label: "IP address",
591
+ label: t("add.ip"),
280
592
  value: ipAddress,
281
593
  onChange: setIpAddress,
282
594
  placeholder: "192.168.1.42",
@@ -284,31 +596,35 @@ function AddDeviceDrawer({ isOpen, onOpenChange, onAdded }) {
284
596
  dataCy: "shelly-add-ip"
285
597
  }),
286
598
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TextFieldRow, {
287
- label: "Name (optional)",
599
+ label: t("add.name"),
288
600
  value: name,
289
601
  onChange: setName,
290
- placeholder: "Workshop light",
602
+ placeholder: t("add.placeholder"),
291
603
  dataCy: "shelly-add-name"
292
604
  }),
293
605
  error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatusAlert, {
294
606
  status: "danger",
295
- title: "Could not add the device",
607
+ title: t("add.error"),
296
608
  dataCy: "shelly-add-error",
297
- children: error
609
+ children: tMessage(error)
298
610
  }),
299
611
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
300
612
  className: "sh:flex sh:justify-end sh:gap-2 sh:pt-2",
301
613
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$5, {
302
614
  variant: "secondary",
303
615
  onPress: close,
304
- children: "Cancel"
616
+ children: t("common.cancel")
305
617
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$5, {
306
618
  variant: "primary",
307
619
  type: "submit",
308
620
  isPending: submitting,
309
621
  onPress: submit,
310
622
  "data-cy": "shelly-add-submit",
311
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PlusIcon$1, { className: "sh:h-4 sh:w-4" }), " Add device"]
623
+ children: [
624
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PlusIcon$1, { className: "sh:h-4 sh:w-4" }),
625
+ " ",
626
+ t("devices.add")
627
+ ]
312
628
  })]
313
629
  }),
314
630
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", {
@@ -325,6 +641,7 @@ var { Button: Button$4, DrawerBody: DrawerBody$3, DrawerFooter: DrawerFooter$2,
325
641
  var { KeyRoundIcon: KeyRoundIcon$1, XIcon: XIcon$3 } = await importShared("lucide-react");
326
642
  var { useCallback: useCallback$4, useEffect: useEffect$3, useState: useState$4 } = await importShared("react");
327
643
  function AdminPasswordDrawer({ device, onOpenChange, onSaved }) {
644
+ const { t, tMessage } = useShellyTranslations();
328
645
  const [currentPassword, setCurrentPassword] = useState$4("");
329
646
  const [password, setPassword] = useState$4("");
330
647
  const [submitting, setSubmitting] = useState$4(false);
@@ -341,7 +658,7 @@ function AdminPasswordDrawer({ device, onOpenChange, onSaved }) {
341
658
  if (!device) return;
342
659
  const nextPassword = password.trim();
343
660
  if (!nextPassword) {
344
- setError("New password is required.");
661
+ setError({ key: "password.required" });
345
662
  return;
346
663
  }
347
664
  setSubmitting(true);
@@ -381,22 +698,19 @@ function AdminPasswordDrawer({ device, onOpenChange, onSaved }) {
381
698
  className: "sh:flex sh:items-center sh:gap-2",
382
699
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(KeyRoundIcon$1, { className: "sh:h-5 sh:w-5 sh:shrink-0 sh:text-accent-soft-foreground" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", {
383
700
  className: "sh:text-lg sh:font-semibold",
384
- children: "Admin password"
701
+ children: t("password.title")
385
702
  })]
386
- }), device && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
703
+ }), device && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
387
704
  className: "sh:text-sm sh:text-muted",
388
- children: [
389
- "Set or change the admin password of ",
390
- device.name,
391
- " (",
392
- device.ipAddress,
393
- ")."
394
- ]
705
+ children: t("password.description", {
706
+ name: device.name,
707
+ address: device.ipAddress
708
+ })
395
709
  })]
396
710
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$4, {
397
711
  isIconOnly: true,
398
712
  variant: "ghost",
399
- "aria-label": "Close",
713
+ "aria-label": t("common.close"),
400
714
  onPress: close,
401
715
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(XIcon$3, { size: 16 })
402
716
  })]
@@ -406,27 +720,27 @@ function AdminPasswordDrawer({ device, onOpenChange, onSaved }) {
406
720
  className: "sh:flex sh:flex-col sh:gap-4",
407
721
  children: [
408
722
  device?.authState === "required" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PasswordFieldRow, {
409
- label: "Current password",
723
+ label: t("password.current"),
410
724
  value: currentPassword,
411
725
  onChange: setCurrentPassword,
412
- description: "Required because this device already has authentication enabled.",
726
+ description: t("password.currentDescription"),
413
727
  autoComplete: "current-password",
414
728
  dataCy: "shelly-auth-current-password"
415
729
  }),
416
730
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PasswordFieldRow, {
417
- label: "New admin password",
731
+ label: t("password.new"),
418
732
  value: password,
419
733
  onChange: setPassword,
420
- description: "Protects the device's local web interface and API.",
734
+ description: t("password.newDescription"),
421
735
  required: true,
422
736
  autoComplete: "new-password",
423
737
  dataCy: "shelly-auth-password"
424
738
  }),
425
739
  error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatusAlert, {
426
740
  status: "danger",
427
- title: "Could not set password",
741
+ title: t("password.error"),
428
742
  dataCy: "shelly-auth-error",
429
- children: error
743
+ children: tMessage(error)
430
744
  }),
431
745
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", {
432
746
  type: "submit",
@@ -437,13 +751,17 @@ function AdminPasswordDrawer({ device, onOpenChange, onSaved }) {
437
751
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DrawerFooter$2, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$4, {
438
752
  variant: "secondary",
439
753
  onPress: close,
440
- children: "Cancel"
754
+ children: t("common.cancel")
441
755
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$4, {
442
756
  variant: "primary",
443
757
  isPending: submitting,
444
758
  onPress: () => void submit(),
445
759
  "data-cy": "shelly-auth-submit",
446
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(KeyRoundIcon$1, { className: "sh:h-4 sh:w-4" }), " Save password"]
760
+ children: [
761
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(KeyRoundIcon$1, { className: "sh:h-4 sh:w-4" }),
762
+ " ",
763
+ t("password.save")
764
+ ]
447
765
  })] })
448
766
  ]
449
767
  });
@@ -453,10 +771,6 @@ function AdminPasswordDrawer({ device, onOpenChange, onSaved }) {
453
771
  var { Button: Button$3, Card, DrawerBody: DrawerBody$2, DrawerFooter: DrawerFooter$1, DrawerHeader: DrawerHeader$2, Form: Form$1, Skeleton } = await importShared("@heroui/react");
454
772
  var { EyeIcon, EyeOffIcon, InfoIcon: InfoIcon$1, RefreshCwIcon: RefreshCwIcon$2, XIcon: XIcon$2 } = await importShared("lucide-react");
455
773
  var { useCallback: useCallback$3, useEffect: useEffect$2, useState: useState$3 } = await importShared("react");
456
- function generationLabel$1(generation) {
457
- if (generation === null) return "Unknown";
458
- return generation === 1 ? "Gen 1" : `Gen ${generation}+`;
459
- }
460
774
  function isRecord(value) {
461
775
  return typeof value === "object" && value !== null && !Array.isArray(value);
462
776
  }
@@ -472,14 +786,14 @@ function firstValue(source, paths) {
472
786
  if (value !== void 0 && value !== null && value !== "") return value;
473
787
  }
474
788
  }
475
- function formatValue(value, suffix = "") {
476
- if (value === void 0 || value === null || value === "") return "Not reported";
477
- if (typeof value === "boolean") return value ? "On" : "Off";
478
- if (typeof value === "number") return `${Number.isInteger(value) ? value : value.toFixed(1)}${suffix}`;
789
+ function formatInfoValue(value, t, language, suffix = "") {
790
+ if (value === void 0 || value === null || value === "") return t("info.notReported");
791
+ if (typeof value === "boolean") return t(value ? "info.on" : "info.off");
792
+ if (typeof value === "number") return `${value.toLocaleString(language, { maximumFractionDigits: 1 })}${suffix}`;
479
793
  return String(value);
480
794
  }
481
- function formatUptime(seconds) {
482
- if (typeof seconds !== "number") return formatValue(seconds);
795
+ function formatUptime(seconds, t, language) {
796
+ if (typeof seconds !== "number") return formatInfoValue(seconds, t, language);
483
797
  const days = Math.floor(seconds / 86400);
484
798
  const hours = Math.floor(seconds % 86400 / 3600);
485
799
  const minutes = Math.floor(seconds % 3600 / 60);
@@ -488,6 +802,7 @@ function formatUptime(seconds) {
488
802
  return `${minutes}m`;
489
803
  }
490
804
  function AuthProtectedForm({ authState, currentPassword, onChange, loading, onLoad }) {
805
+ const { t } = useShellyTranslations();
491
806
  const [visible, setVisible] = useState$3(false);
492
807
  if (authState !== "required") return null;
493
808
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
@@ -501,21 +816,21 @@ function AuthProtectedForm({ authState, currentPassword, onChange, loading, onLo
501
816
  children: [
502
817
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
503
818
  className: "sh:text-sm",
504
- children: "This device requires authentication. Enter its admin password to read protected info."
819
+ children: t("info.auth")
505
820
  }),
506
821
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
507
822
  className: "sh:relative",
508
823
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(TextFieldRow, {
509
- label: "Admin password",
824
+ label: t("password.title"),
510
825
  value: currentPassword,
511
826
  onChange,
512
- placeholder: "device admin password",
827
+ placeholder: t("info.passwordPlaceholder"),
513
828
  dataCy: "shelly-info-current-password"
514
829
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$3, {
515
830
  isIconOnly: true,
516
831
  variant: "ghost",
517
832
  size: "sm",
518
- "aria-label": visible ? "Hide password" : "Show password",
833
+ "aria-label": t(visible ? "password.hide" : "password.show"),
519
834
  className: "sh:absolute sh:right-1 sh:top-6",
520
835
  onPress: () => setVisible((v) => !v),
521
836
  children: visible ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EyeOffIcon, { className: "sh:h-4 sh:w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EyeIcon, { className: "sh:h-4 sh:w-4" })
@@ -529,7 +844,7 @@ function AuthProtectedForm({ authState, currentPassword, onChange, loading, onLo
529
844
  isPending: loading,
530
845
  onPress: onLoad,
531
846
  "data-cy": "shelly-info-unlock",
532
- children: "Load info"
847
+ children: t("info.load")
533
848
  })
534
849
  }),
535
850
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", {
@@ -541,6 +856,8 @@ function AuthProtectedForm({ authState, currentPassword, onChange, loading, onLo
541
856
  });
542
857
  }
543
858
  function DeviceInfoCards({ info }) {
859
+ const { t, language } = useShellyTranslations();
860
+ const formatValue = (value, suffix = "") => formatInfoValue(value, t, language, suffix);
544
861
  const status = info.status;
545
862
  const config = info.config;
546
863
  const output = firstValue(status, [
@@ -557,10 +874,10 @@ function DeviceInfoCards({ info }) {
557
874
  const current = firstValue(status, ["switch:0.current", "meters.0.current"]);
558
875
  const cards = [
559
876
  {
560
- title: "Device",
877
+ title: t("devices.device"),
561
878
  rows: [
562
879
  {
563
- label: "Name",
880
+ label: t("info.name"),
564
881
  value: formatValue(firstValue(config, [
565
882
  "sys.device.name",
566
883
  "name",
@@ -568,24 +885,24 @@ function DeviceInfoCards({ info }) {
568
885
  ]))
569
886
  },
570
887
  {
571
- label: "Generation",
572
- value: generationLabel$1(info.generation)
888
+ label: t("info.generation"),
889
+ value: info.generation === null ? t("devices.unknown") : t(info.generation === 1 ? "devices.generation" : "devices.generationPlus", { generation: info.generation })
573
890
  },
574
891
  {
575
- label: "Timezone",
892
+ label: t("info.timezone"),
576
893
  value: formatValue(firstValue(config, ["sys.location.tz", "timezone"]))
577
894
  },
578
895
  {
579
- label: "Uptime",
580
- value: formatUptime(firstValue(status, ["sys.uptime", "uptime"]))
896
+ label: t("info.uptime"),
897
+ value: formatUptime(firstValue(status, ["sys.uptime", "uptime"]), t, language)
581
898
  }
582
899
  ]
583
900
  },
584
901
  {
585
- title: "Network",
902
+ title: t("info.network"),
586
903
  rows: [
587
904
  {
588
- label: "IP address",
905
+ label: t("add.ip"),
589
906
  value: formatValue(firstValue(status, [
590
907
  "wifi.sta_ip",
591
908
  "wifi_sta.ip",
@@ -593,7 +910,7 @@ function DeviceInfoCards({ info }) {
593
910
  ]))
594
911
  },
595
912
  {
596
- label: "Wi-Fi network",
913
+ label: t("info.wifi"),
597
914
  value: formatValue(firstValue(status, [
598
915
  "wifi.ssid",
599
916
  "wifi_sta.ssid",
@@ -601,28 +918,28 @@ function DeviceInfoCards({ info }) {
601
918
  ]))
602
919
  },
603
920
  {
604
- label: "Signal",
921
+ label: t("info.signal"),
605
922
  value: formatValue(firstValue(status, ["wifi.rssi", "wifi_sta.rssi"]), " dBm")
606
923
  }
607
924
  ]
608
925
  },
609
926
  {
610
- title: "Output",
927
+ title: t("info.output"),
611
928
  rows: [
612
929
  {
613
- label: "State",
930
+ label: t("info.state"),
614
931
  value: formatValue(output)
615
932
  },
616
933
  {
617
- label: "Power",
934
+ label: t("info.power"),
618
935
  value: formatValue(power, " W")
619
936
  },
620
937
  {
621
- label: "Voltage",
938
+ label: t("info.voltage"),
622
939
  value: formatValue(voltage, " V")
623
940
  },
624
941
  {
625
- label: "Current",
942
+ label: t("info.current"),
626
943
  value: formatValue(current, " A")
627
944
  }
628
945
  ]
@@ -653,6 +970,7 @@ function DeviceInfoCards({ info }) {
653
970
  });
654
971
  }
655
972
  function DeviceInfoDrawer({ device, onOpenChange }) {
973
+ const { t, language } = useShellyTranslations();
656
974
  const [info, setInfo] = useState$3(null);
657
975
  const [currentPassword, setCurrentPassword] = useState$3("");
658
976
  const [loading, setLoading] = useState$3(false);
@@ -692,7 +1010,7 @@ function DeviceInfoDrawer({ device, onOpenChange }) {
692
1010
  className: "sh:flex sh:items-center sh:gap-2",
693
1011
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(InfoIcon$1, { className: "sh:h-5 sh:w-5 sh:shrink-0 sh:text-accent-soft-foreground" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", {
694
1012
  className: "sh:text-lg sh:font-semibold",
695
- children: device?.name ?? "Device info"
1013
+ children: device?.name ?? t("info.title")
696
1014
  })]
697
1015
  }), device && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
698
1016
  className: "sh:text-sm sh:text-muted",
@@ -701,7 +1019,7 @@ function DeviceInfoDrawer({ device, onOpenChange }) {
701
1019
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$3, {
702
1020
  isIconOnly: true,
703
1021
  variant: "ghost",
704
- "aria-label": "Close",
1022
+ "aria-label": t("common.close"),
705
1023
  onPress: close,
706
1024
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(XIcon$2, { size: 16 })
707
1025
  })]
@@ -718,7 +1036,7 @@ function DeviceInfoDrawer({ device, onOpenChange }) {
718
1036
  }),
719
1037
  error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatusAlert, {
720
1038
  status: "danger",
721
- title: "Could not load device info",
1039
+ title: t("info.loadError"),
722
1040
  children: error
723
1041
  }),
724
1042
  loading && !info ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
@@ -736,13 +1054,17 @@ function DeviceInfoDrawer({ device, onOpenChange }) {
736
1054
  className: "sh:flex sh:w-full sh:items-center sh:justify-between sh:gap-3",
737
1055
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
738
1056
  className: "sh:text-xs sh:text-default-500",
739
- children: info ? `Updated ${new Date(info.fetchedAt).toLocaleTimeString()}` : ""
1057
+ children: info ? t("info.updated", { time: new Date(info.fetchedAt).toLocaleTimeString(language) }) : ""
740
1058
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$3, {
741
1059
  variant: "secondary",
742
1060
  onPress: () => void load(),
743
1061
  isPending: loading,
744
1062
  "data-cy": "shelly-info-refresh",
745
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(RefreshCwIcon$2, { className: "sh:h-4 sh:w-4" }), " Refresh"]
1063
+ children: [
1064
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RefreshCwIcon$2, { className: "sh:h-4 sh:w-4" }),
1065
+ " ",
1066
+ t("common.refresh")
1067
+ ]
746
1068
  })]
747
1069
  }) })
748
1070
  ]
@@ -754,32 +1076,27 @@ var { Button: Button$2, Chip: Chip$1, DrawerBody: DrawerBody$1, DrawerHeader: Dr
754
1076
  var { SearchIcon: SearchIcon$1, XIcon: XIcon$1 } = await importShared("lucide-react");
755
1077
  var { useCallback: useCallback$2, useRef: useRef$1, useState: useState$2 } = await importShared("react");
756
1078
  function ResultSummary({ result }) {
1079
+ const { t } = useShellyTranslations();
757
1080
  const added = result.devices.filter((device) => device.isNew).length;
758
- const scanned = result.subnets.length > 0 ? result.subnets.join(", ") : "no subnet (mDNS only)";
1081
+ const scanned = result.subnets.length > 0 ? result.subnets.join(", ") : t("discovery.mdnsOnly");
759
1082
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
760
1083
  className: "sh:flex sh:flex-col sh:gap-3",
761
1084
  "data-cy": "shelly-discover-result",
762
1085
  children: [
763
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(StatusAlert, {
1086
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatusAlert, {
764
1087
  status: result.devices.length > 0 ? "success" : "warning",
765
- title: result.devices.length > 0 ? `Found ${result.devices.length}, added ${added}` : "No devices found",
766
- children: [
767
- "Probed ",
768
- result.probed,
769
- " address",
770
- result.probed === 1 ? "" : "es",
771
- " in ",
772
- scanned,
773
- "."
774
- ]
1088
+ title: result.devices.length > 0 ? t("discovery.found", {
1089
+ count: result.devices.length,
1090
+ added
1091
+ }) : t("discovery.empty"),
1092
+ children: t("discovery.probed", {
1093
+ count: result.probed,
1094
+ subnets: scanned
1095
+ })
775
1096
  }),
776
- result.devices.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
1097
+ result.devices.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
777
1098
  className: "sh:text-sm sh:text-muted",
778
- children: [
779
- "Nothing answered ",
780
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: "GET /shelly" }),
781
- ". If Attraccess runs in a container, enter the subnet your devices are on — the container's own network is not your LAN."
782
- ]
1099
+ children: t("discovery.hint")
783
1100
  }),
784
1101
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", {
785
1102
  className: "sh:flex sh:flex-col sh:gap-2",
@@ -795,16 +1112,17 @@ function ResultSummary({ result }) {
795
1112
  className: "sh:text-xs sh:text-muted",
796
1113
  children: [
797
1114
  device.ipAddress,
798
- " · Gen ",
799
- device.generation,
800
- " · via ",
801
- device.source === "mdns" ? "mDNS" : "subnet scan"
1115
+ " · ",
1116
+ t("devices.generation", { generation: device.generation }),
1117
+ " ·",
1118
+ " ",
1119
+ t("discovery.via", { source: device.source === "mdns" ? "mDNS" : t("discovery.scan") })
802
1120
  ]
803
1121
  })]
804
1122
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Chip$1, {
805
1123
  variant: "soft",
806
1124
  color: device.isNew ? "success" : "default",
807
- children: device.isNew ? "Added" : "Already known"
1125
+ children: t(device.isNew ? "discovery.added" : "discovery.known")
808
1126
  })]
809
1127
  }, device.deviceId))
810
1128
  })
@@ -812,6 +1130,7 @@ function ResultSummary({ result }) {
812
1130
  });
813
1131
  }
814
1132
  function DiscoverDrawer({ isOpen, onOpenChange, onDiscovered }) {
1133
+ const { t } = useShellyTranslations();
815
1134
  const [cidr, setCidr] = useState$2("");
816
1135
  const [running, setRunning] = useState$2(false);
817
1136
  const [error, setError] = useState$2(null);
@@ -844,15 +1163,15 @@ function DiscoverDrawer({ isOpen, onOpenChange, onDiscovered }) {
844
1163
  className: "sh:flex sh:flex-col sh:gap-1",
845
1164
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", {
846
1165
  className: "sh:text-lg sh:font-semibold",
847
- children: "Discover devices"
1166
+ children: t("discovery.title")
848
1167
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
849
1168
  className: "sh:text-sm sh:text-muted",
850
- children: "Listens for Shelly devices announcing over mDNS, then probes every address of a subnet. Everything found is added to the registry."
1169
+ children: t("discovery.description")
851
1170
  })]
852
1171
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$2, {
853
1172
  isIconOnly: true,
854
1173
  variant: "ghost",
855
- "aria-label": "Close",
1174
+ "aria-label": t("common.close"),
856
1175
  onPress: close,
857
1176
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(XIcon$1, { size: 16 })
858
1177
  })]
@@ -861,16 +1180,16 @@ function DiscoverDrawer({ isOpen, onOpenChange, onDiscovered }) {
861
1180
  className: "sh:flex sh:flex-col sh:gap-4",
862
1181
  children: [
863
1182
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TextFieldRow, {
864
- label: "Subnet to scan (optional)",
1183
+ label: t("discovery.subnet"),
865
1184
  value: cidr,
866
1185
  onChange: setCidr,
867
1186
  placeholder: "192.168.1.0/24",
868
- description: "Leave empty to scan the server's own networks. Private ranges only, /22 at most. Required when Attraccess runs in a container, since its network is not your LAN.",
1187
+ description: t("discovery.subnetDescription"),
869
1188
  dataCy: "shelly-discover-cidr"
870
1189
  }),
871
1190
  error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatusAlert, {
872
1191
  status: "danger",
873
- title: "Discovery failed",
1192
+ title: t("discovery.error"),
874
1193
  dataCy: "shelly-discover-error",
875
1194
  children: error
876
1195
  }),
@@ -879,7 +1198,7 @@ function DiscoverDrawer({ isOpen, onOpenChange, onDiscovered }) {
879
1198
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner$2, {
880
1199
  color: "accent",
881
1200
  size: "sm"
882
- }), "Probing addresses — a /24 takes a few seconds."]
1201
+ }), t("discovery.running")]
883
1202
  }),
884
1203
  result && !running && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ResultSummary, { result }),
885
1204
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
@@ -887,14 +1206,18 @@ function DiscoverDrawer({ isOpen, onOpenChange, onDiscovered }) {
887
1206
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$2, {
888
1207
  variant: "secondary",
889
1208
  onPress: close,
890
- children: result ? "Done" : "Cancel"
1209
+ children: t(result ? "common.done" : "common.cancel")
891
1210
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$2, {
892
1211
  variant: "primary",
893
1212
  type: "submit",
894
1213
  isPending: running,
895
1214
  onPress: submit,
896
1215
  "data-cy": "shelly-discover-submit",
897
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SearchIcon$1, { className: "sh:h-4 sh:w-4" }), " Start discovery"]
1216
+ children: [
1217
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SearchIcon$1, { className: "sh:h-4 sh:w-4" }),
1218
+ " ",
1219
+ t("discovery.start")
1220
+ ]
898
1221
  })]
899
1222
  }),
900
1223
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", {
@@ -912,10 +1235,6 @@ var { ArrowUpCircleIcon, CpuIcon: CpuIcon$1, DownloadIcon, RefreshCwIcon: Refres
912
1235
  var { useCallback: useCallback$1, useEffect: useEffect$1, useRef, useState: useState$1 } = await importShared("react");
913
1236
  var POLL_INTERVAL_MS = 5e3;
914
1237
  var UPDATE_TIMEOUT_MS = 3e5;
915
- var STAGE_LABEL = {
916
- stable: "stable",
917
- beta: "beta"
918
- };
919
1238
  /**
920
1239
  * Gen1 reports versions as `20230913-114150/v1.14.0` — only the tail is useful
921
1240
  * at a glance, the full string stays in the tooltip.
@@ -926,25 +1245,26 @@ function shortVersion(version) {
926
1245
  }
927
1246
  /** Table cell summarising a device's firmware state from the bulk overview. */
928
1247
  function FirmwareCell({ entry }) {
1248
+ const { t } = useShellyTranslations();
929
1249
  if (!entry) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
930
1250
  className: "sh:text-sm sh:text-default-400",
931
- children: "Checking…"
1251
+ children: t("firmware.checking")
932
1252
  });
933
1253
  if (entry.error || !entry.status) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
934
1254
  className: "sh:block sh:max-w-40 sh:truncate sh:text-sm sh:text-default-400",
935
1255
  title: entry.error ?? void 0,
936
- children: "Unavailable"
1256
+ children: t("firmware.unavailable")
937
1257
  });
938
1258
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
939
1259
  className: "sh:flex sh:flex-col sh:gap-1",
940
1260
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
941
1261
  className: "sh:block sh:max-w-40 sh:truncate sh:text-sm sh:text-default-700",
942
1262
  title: entry.status.currentVersion ?? void 0,
943
- children: entry.status.currentVersion ? shortVersion(entry.status.currentVersion) : "Unknown"
944
- }), entry.status.hasUpdate && entry.status.available.stable && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
1263
+ children: entry.status.currentVersion ? shortVersion(entry.status.currentVersion) : t("devices.unknown")
1264
+ }), entry.status.hasUpdate && entry.status.available.stable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
945
1265
  className: "sh:max-w-40 sh:truncate sh:text-xs sh:text-warning-600",
946
1266
  title: entry.status.available.stable,
947
- children: [shortVersion(entry.status.available.stable), " available"]
1267
+ children: t("firmware.available", { version: shortVersion(entry.status.available.stable) })
948
1268
  })]
949
1269
  });
950
1270
  }
@@ -955,9 +1275,10 @@ function FirmwareCell({ entry }) {
955
1275
  * table's visible width on tablets.
956
1276
  */
957
1277
  function UpdateAvailableIndicator({ entry }) {
1278
+ const { t } = useShellyTranslations();
958
1279
  const version = entry?.status?.hasUpdate ? entry.status.available.stable : null;
959
1280
  if (!version) return null;
960
- const label = `Firmware update available: ${shortVersion(version)}`;
1281
+ const label = t("firmware.updateAvailable", { version: shortVersion(version) });
961
1282
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Tooltip$1, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Tooltip$1.Trigger, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
962
1283
  variant: "ghost",
963
1284
  size: "sm",
@@ -982,34 +1303,36 @@ function VersionRow({ label, value }) {
982
1303
  });
983
1304
  }
984
1305
  function FirmwareDetails({ status }) {
1306
+ const { t, language } = useShellyTranslations();
985
1307
  if (!status) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
986
1308
  className: "sh:rounded-xl sh:border sh:border-dashed sh:border-default-300 sh:p-4 sh:text-sm sh:text-default-500",
987
- children: "No firmware info loaded yet."
1309
+ children: t("firmware.empty")
988
1310
  });
989
1311
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", {
990
1312
  className: "sh:rounded-xl sh:border sh:border-default-200 sh:bg-surface sh:p-4",
991
1313
  "data-cy": "shelly-firmware-details",
992
1314
  children: [
993
1315
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(VersionRow, {
994
- label: "Installed",
995
- value: status.currentVersion ?? "Unknown"
1316
+ label: t("firmware.installed"),
1317
+ value: status.currentVersion ?? t("devices.unknown")
996
1318
  }),
997
1319
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(VersionRow, {
998
- label: "Stable channel",
999
- value: status.available.stable ?? "Up to date"
1320
+ label: t("firmware.stable"),
1321
+ value: status.available.stable ?? t("firmware.upToDate")
1000
1322
  }),
1001
1323
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(VersionRow, {
1002
- label: "Beta channel",
1003
- value: status.available.beta ?? "Nothing newer"
1324
+ label: t("firmware.beta"),
1325
+ value: status.available.beta ?? t("firmware.nothingNewer")
1004
1326
  }),
1005
1327
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(VersionRow, {
1006
- label: "Checked",
1007
- value: new Date(status.fetchedAt).toLocaleString()
1328
+ label: t("firmware.checked"),
1329
+ value: new Date(status.fetchedAt).toLocaleString(language)
1008
1330
  })
1009
1331
  ]
1010
1332
  });
1011
1333
  }
1012
1334
  function FirmwareDrawer({ device, onOpenChange, onUpdated }) {
1335
+ const { t, tMessage } = useShellyTranslations();
1013
1336
  const [status, setStatus] = useState$1(null);
1014
1337
  const [currentPassword, setCurrentPassword] = useState$1("");
1015
1338
  const [loading, setLoading] = useState$1(false);
@@ -1070,7 +1393,7 @@ function FirmwareDrawer({ device, onOpenChange, onUpdated }) {
1070
1393
  if (cancelled) return;
1071
1394
  if (Date.now() > deadline.current) {
1072
1395
  setInstalling(null);
1073
- setError("The device did not report the expected firmware version within 5 minutes. Check it and re-check the firmware manually.");
1396
+ setError({ key: "firmware.timeout" });
1074
1397
  return;
1075
1398
  }
1076
1399
  try {
@@ -1107,22 +1430,19 @@ function FirmwareDrawer({ device, onOpenChange, onUpdated }) {
1107
1430
  className: "sh:flex sh:items-center sh:gap-2",
1108
1431
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CpuIcon$1, { className: "sh:h-5 sh:w-5 sh:shrink-0 sh:text-accent-soft-foreground" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", {
1109
1432
  className: "sh:text-lg sh:font-semibold",
1110
- children: "Firmware"
1433
+ children: t("devices.firmware")
1111
1434
  })]
1112
- }), device && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
1435
+ }), device && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
1113
1436
  className: "sh:text-sm sh:text-muted",
1114
- children: [
1115
- "Check for and install firmware updates on ",
1116
- device.name,
1117
- " (",
1118
- device.ipAddress,
1119
- ")."
1120
- ]
1437
+ children: t("firmware.description", {
1438
+ name: device.name,
1439
+ address: device.ipAddress
1440
+ })
1121
1441
  })]
1122
1442
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
1123
1443
  isIconOnly: true,
1124
1444
  variant: "ghost",
1125
- "aria-label": "Close",
1445
+ "aria-label": t("common.close"),
1126
1446
  onPress: close,
1127
1447
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(XIcon, { size: 16 })
1128
1448
  })]
@@ -1131,47 +1451,39 @@ function FirmwareDrawer({ device, onOpenChange, onUpdated }) {
1131
1451
  className: "sh:flex sh:flex-col sh:gap-4",
1132
1452
  children: [
1133
1453
  device?.authState === "required" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PasswordFieldRow, {
1134
- label: "Current password",
1454
+ label: t("password.current"),
1135
1455
  value: currentPassword,
1136
1456
  onChange: setCurrentPassword,
1137
- description: "Required because this device already has authentication enabled.",
1457
+ description: t("password.currentDescription"),
1138
1458
  autoComplete: "current-password",
1139
1459
  dataCy: "shelly-firmware-current-password"
1140
1460
  }),
1141
1461
  error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatusAlert, {
1142
1462
  status: "danger",
1143
- title: "Firmware check failed",
1463
+ title: t("firmware.error"),
1144
1464
  dataCy: "shelly-firmware-error",
1145
- children: error
1465
+ children: tMessage(error)
1146
1466
  }),
1147
1467
  installing && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatusAlert, {
1148
1468
  status: "accent",
1149
- title: "Update running",
1469
+ title: t("firmware.running"),
1150
1470
  dataCy: "shelly-firmware-progress",
1151
1471
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
1152
1472
  className: "sh:flex sh:items-center sh:gap-2",
1153
- children: [
1154
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner$1, {
1155
- size: "sm",
1156
- color: "accent"
1157
- }),
1158
- "Installing the ",
1159
- STAGE_LABEL[installing],
1160
- " firmware",
1161
- targetVersion.current ? ` (${targetVersion.current})` : "",
1162
- ". The device reboots during the update and is offline for a moment — this page keeps checking."
1163
- ]
1473
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner$1, {
1474
+ size: "sm",
1475
+ color: "accent"
1476
+ }), t("firmware.installing", {
1477
+ stage: t(`firmware.${installing}Stage`),
1478
+ version: targetVersion.current ? ` (${targetVersion.current})` : ""
1479
+ })]
1164
1480
  })
1165
1481
  }),
1166
- installedVersion && !installing && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(StatusAlert, {
1482
+ installedVersion && !installing && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatusAlert, {
1167
1483
  status: "success",
1168
- title: "Update finished",
1484
+ title: t("firmware.finished"),
1169
1485
  dataCy: "shelly-firmware-success",
1170
- children: [
1171
- "The device now runs ",
1172
- installedVersion,
1173
- "."
1174
- ]
1486
+ children: t("firmware.success", { version: installedVersion })
1175
1487
  }),
1176
1488
  loading && !status ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
1177
1489
  className: "sh:flex sh:items-center sh:justify-center sh:p-6",
@@ -1187,7 +1499,11 @@ function FirmwareDrawer({ device, onOpenChange, onUpdated }) {
1187
1499
  isPending: loading,
1188
1500
  isDisabled: !!installing,
1189
1501
  "data-cy": "shelly-firmware-refresh",
1190
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(RefreshCwIcon$1, { className: "sh:h-4 sh:w-4" }), " Check again"]
1502
+ children: [
1503
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RefreshCwIcon$1, { className: "sh:h-4 sh:w-4" }),
1504
+ " ",
1505
+ t("firmware.recheck")
1506
+ ]
1191
1507
  }), ["stable", "beta"].map((stage) => {
1192
1508
  const version = status?.available[stage];
1193
1509
  if (!version) return null;
@@ -1197,13 +1513,17 @@ function FirmwareDrawer({ device, onOpenChange, onUpdated }) {
1197
1513
  isPending: installing === stage,
1198
1514
  isDisabled: !!installing,
1199
1515
  "data-cy": `shelly-firmware-install-${stage}`,
1200
- "aria-label": `Install ${STAGE_LABEL[stage]} firmware ${version}`,
1516
+ "aria-label": t("firmware.installLabel", {
1517
+ stage: t(`firmware.${stage}Stage`),
1518
+ version
1519
+ }),
1201
1520
  children: [
1202
1521
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DownloadIcon, { className: "sh:h-4 sh:w-4" }),
1203
- " Install ",
1204
- STAGE_LABEL[stage],
1205
1522
  " ",
1206
- shortVersion(version)
1523
+ t("firmware.install", {
1524
+ stage: t(`firmware.${stage}Stage`),
1525
+ version: shortVersion(version)
1526
+ })
1207
1527
  ]
1208
1528
  }, stage);
1209
1529
  })]
@@ -1216,97 +1536,104 @@ function FirmwareDrawer({ device, onOpenChange, onUpdated }) {
1216
1536
  var { Button, Chip, Dropdown, DropdownItem, DropdownMenu, DropdownPopover, DropdownTrigger, Modal, ModalBackdrop, ModalBody, ModalContainer, ModalDialog, ModalFooter, ModalHeader, ModalHeading, Spinner, Table, TableBody, TableCell, TableColumn, TableContent, TableHeader, TableRow, TableScrollContainer, Tooltip, useOverlayState } = await importShared("@heroui/react");
1217
1537
  var { CpuIcon, InfoIcon, KeyRoundIcon, MehIcon, MoreVerticalIcon, PlusIcon, RefreshCwIcon, SearchIcon, Trash2Icon, TriangleAlertIcon, WifiIcon: WifiIcon$1 } = await importShared("lucide-react");
1218
1538
  var { useCallback, useEffect, useState } = await importShared("react");
1219
- function generationLabel(generation) {
1220
- if (generation === null) return "Unknown";
1221
- return generation === 1 ? "Gen 1" : `Gen ${generation}+`;
1222
- }
1223
1539
  function AuthChip({ state }) {
1224
- const { color, label } = {
1225
- none: {
1226
- color: "success",
1227
- label: "No auth"
1228
- },
1229
- required: {
1230
- color: "warning",
1231
- label: "Auth required"
1232
- },
1233
- unknown: {
1234
- color: "default",
1235
- label: "Unknown"
1236
- }
1540
+ const { t } = useShellyTranslations();
1541
+ const { color } = {
1542
+ none: { color: "success" },
1543
+ required: { color: "warning" },
1544
+ unknown: { color: "default" }
1237
1545
  }[state];
1238
1546
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Chip, {
1239
1547
  variant: "soft",
1240
1548
  color,
1241
1549
  size: "sm",
1242
1550
  className: "sh:whitespace-nowrap",
1243
- children: label
1551
+ children: t(`auth.${state}`)
1244
1552
  });
1245
1553
  }
1246
1554
  function ProbeErrorIndicator({ message }) {
1555
+ const { t } = useShellyTranslations();
1247
1556
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Tooltip.Trigger, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
1248
1557
  variant: "ghost",
1249
1558
  size: "sm",
1250
1559
  isIconOnly: true,
1251
- "aria-label": `Probe failed: ${message}`,
1560
+ "aria-label": t("devices.probeError", { message }),
1252
1561
  className: "sh:h-6 sh:w-6 sh:min-w-6 sh:text-warning",
1253
1562
  "data-cy": "shelly-device-probe-error",
1254
1563
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TriangleAlertIcon, { className: "sh:h-4 sh:w-4" })
1255
- }) }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Tooltip.Content, { children: ["Probe failed: ", message] })] });
1564
+ }) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Tooltip.Content, { children: t("devices.probeError", { message }) })] });
1256
1565
  }
1257
1566
  function RowActions({ deviceId, isBusy, onInfo, onFirmware, onAuth, onReprobe, onDelete }) {
1567
+ const { t } = useShellyTranslations();
1258
1568
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1259
1569
  className: "sh:flex sh:flex-row sh:items-center sh:justify-end sh:gap-1 sh:whitespace-nowrap",
1260
1570
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Tooltip.Trigger, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
1261
1571
  variant: "ghost",
1262
1572
  size: "sm",
1263
1573
  isIconOnly: true,
1264
- "aria-label": "View device info",
1574
+ "aria-label": t("devices.info"),
1265
1575
  isDisabled: isBusy,
1266
1576
  onPress: onInfo,
1267
1577
  "data-cy": `shelly-device-info-${deviceId}`,
1268
1578
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(InfoIcon, { className: "sh:h-4 sh:w-4" })
1269
- }) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Tooltip.Content, { children: "View device info" })] }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Dropdown, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DropdownTrigger, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
1579
+ }) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Tooltip.Content, { children: t("devices.info") })] }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Dropdown, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DropdownTrigger, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
1270
1580
  variant: "ghost",
1271
1581
  size: "sm",
1272
1582
  isIconOnly: true,
1273
- "aria-label": "More actions",
1583
+ "aria-label": t("devices.more"),
1274
1584
  isPending: isBusy,
1275
1585
  "data-cy": `shelly-device-menu-${deviceId}`,
1276
1586
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MoreVerticalIcon, { className: "sh:h-4 sh:w-4" })
1277
1587
  }) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DropdownPopover, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DropdownMenu, {
1278
- "aria-label": "Device actions",
1588
+ "aria-label": t("devices.deviceActions"),
1279
1589
  children: [
1280
1590
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DropdownItem, {
1281
1591
  id: "firmware",
1282
1592
  onPress: onFirmware,
1283
1593
  "data-cy": `shelly-device-firmware-${deviceId}`,
1284
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CpuIcon, { className: "sh:mr-2 sh:inline sh:h-4 sh:w-4" }), " Manage firmware"]
1594
+ children: [
1595
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CpuIcon, { className: "sh:mr-2 sh:inline sh:h-4 sh:w-4" }),
1596
+ " ",
1597
+ t("devices.manageFirmware")
1598
+ ]
1285
1599
  }),
1286
1600
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DropdownItem, {
1287
1601
  id: "auth",
1288
1602
  onPress: onAuth,
1289
1603
  "data-cy": `shelly-device-auth-${deviceId}`,
1290
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(KeyRoundIcon, { className: "sh:mr-2 sh:inline sh:h-4 sh:w-4" }), " Set admin password"]
1604
+ children: [
1605
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(KeyRoundIcon, { className: "sh:mr-2 sh:inline sh:h-4 sh:w-4" }),
1606
+ " ",
1607
+ t("devices.password")
1608
+ ]
1291
1609
  }),
1292
1610
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DropdownItem, {
1293
1611
  id: "reprobe",
1294
1612
  onPress: onReprobe,
1295
1613
  "data-cy": `shelly-device-reprobe-${deviceId}`,
1296
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(RefreshCwIcon, { className: "sh:mr-2 sh:inline sh:h-4 sh:w-4" }), " Re-probe device"]
1614
+ children: [
1615
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RefreshCwIcon, { className: "sh:mr-2 sh:inline sh:h-4 sh:w-4" }),
1616
+ " ",
1617
+ t("devices.reprobe")
1618
+ ]
1297
1619
  }),
1298
1620
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DropdownItem, {
1299
1621
  id: "delete",
1300
1622
  onPress: onDelete,
1301
1623
  className: "sh:text-danger",
1302
1624
  "data-cy": `shelly-device-delete-${deviceId}`,
1303
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Trash2Icon, { className: "sh:mr-2 sh:inline sh:h-4 sh:w-4" }), " Delete device"]
1625
+ children: [
1626
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Trash2Icon, { className: "sh:mr-2 sh:inline sh:h-4 sh:w-4" }),
1627
+ " ",
1628
+ t("devices.delete")
1629
+ ]
1304
1630
  })
1305
1631
  ]
1306
1632
  }) })] })]
1307
1633
  });
1308
1634
  }
1309
1635
  function EmptyDevices({ onAdd }) {
1636
+ const { t } = useShellyTranslations();
1310
1637
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1311
1638
  className: "sh:flex sh:flex-col sh:items-center sh:justify-center sh:gap-3 sh:px-4 sh:py-12",
1312
1639
  children: [
@@ -1316,19 +1643,24 @@ function EmptyDevices({ onAdd }) {
1316
1643
  }),
1317
1644
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
1318
1645
  className: "sh:text-sm sh:text-default-500",
1319
- children: "No devices yet. Run discovery, or add your first Shelly by its IP."
1646
+ children: t("devices.empty")
1320
1647
  }),
1321
1648
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button, {
1322
1649
  variant: "secondary",
1323
1650
  size: "sm",
1324
1651
  onPress: onAdd,
1325
1652
  "data-cy": "shelly-add-open-empty",
1326
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PlusIcon, { className: "sh:h-4 sh:w-4" }), " Add device"]
1653
+ children: [
1654
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PlusIcon, { className: "sh:h-4 sh:w-4" }),
1655
+ " ",
1656
+ t("devices.add")
1657
+ ]
1327
1658
  })
1328
1659
  ]
1329
1660
  });
1330
1661
  }
1331
1662
  function DevicesPage() {
1663
+ const { t, language } = useShellyTranslations();
1332
1664
  const [devices, setDevices] = useState([]);
1333
1665
  const [loading, setLoading] = useState(true);
1334
1666
  const [pageError, setPageError] = useState(null);
@@ -1403,10 +1735,10 @@ function DevicesPage() {
1403
1735
  className: "sh:flex sh:items-center sh:gap-3",
1404
1736
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(WifiIcon$1, { className: "sh:h-6 sh:w-6 sh:text-accent-soft-foreground" }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", {
1405
1737
  className: "sh:text-2xl sh:font-bold",
1406
- children: "Shelly Devices"
1738
+ children: t("devices.title")
1407
1739
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
1408
1740
  className: "sh:mt-1 sh:text-sm sh:text-muted",
1409
- children: "Discovered and manually added Shelly devices."
1741
+ children: t("devices.description")
1410
1742
  })] })]
1411
1743
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
1412
1744
  className: "sh:flex sh:flex-wrap sh:gap-2",
@@ -1414,18 +1746,26 @@ function DevicesPage() {
1414
1746
  variant: "secondary",
1415
1747
  onPress: discoverDrawer.open,
1416
1748
  "data-cy": "shelly-discover-open",
1417
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SearchIcon, { className: "sh:h-4 sh:w-4" }), " Discover"]
1749
+ children: [
1750
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SearchIcon, { className: "sh:h-4 sh:w-4" }),
1751
+ " ",
1752
+ t("devices.discover")
1753
+ ]
1418
1754
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button, {
1419
1755
  variant: "primary",
1420
1756
  onPress: addDrawer.open,
1421
1757
  "data-cy": "shelly-add-open",
1422
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PlusIcon, { className: "sh:h-4 sh:w-4" }), " Add device"]
1758
+ children: [
1759
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PlusIcon, { className: "sh:h-4 sh:w-4" }),
1760
+ " ",
1761
+ t("devices.add")
1762
+ ]
1423
1763
  })]
1424
1764
  })]
1425
1765
  }),
1426
1766
  pageError && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatusAlert, {
1427
1767
  status: "danger",
1428
- title: "Failed to load devices",
1768
+ title: t("devices.loadError"),
1429
1769
  children: pageError
1430
1770
  }),
1431
1771
  loading ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
@@ -1434,35 +1774,39 @@ function DevicesPage() {
1434
1774
  }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Table, {
1435
1775
  "data-cy": "shelly-device-table",
1436
1776
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableScrollContainer, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TableContent, {
1437
- "aria-label": "Shelly devices",
1777
+ "aria-label": t("devices.table"),
1438
1778
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TableHeader, { children: [
1439
1779
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableColumn, {
1440
1780
  isRowHeader: true,
1441
- children: "Device"
1781
+ children: t("devices.device")
1442
1782
  }),
1443
1783
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableColumn, {
1444
1784
  className: "sh:hidden sh:sm:table-cell sh:md:hidden sh:lg:table-cell",
1445
- children: "Address"
1785
+ children: t("devices.address")
1446
1786
  }),
1447
1787
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableColumn, {
1448
1788
  className: "sh:hidden sh:lg:table-cell",
1449
- children: "Model"
1789
+ children: t("devices.model")
1450
1790
  }),
1451
1791
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableColumn, {
1452
1792
  className: "sh:hidden sh:sm:table-cell",
1453
- children: "Auth"
1793
+ children: t("devices.auth")
1454
1794
  }),
1455
1795
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableColumn, {
1456
1796
  className: "sh:hidden sh:xl:table-cell",
1457
- children: "Firmware"
1797
+ children: t("devices.firmware")
1458
1798
  }),
1459
1799
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableColumn, {
1460
1800
  className: "sh:text-end",
1461
- children: "Actions"
1801
+ children: t("devices.actions")
1462
1802
  })
1463
1803
  ] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableBody, {
1464
1804
  items: devices,
1465
- dependencies: [firmware, rowBusyId],
1805
+ dependencies: [
1806
+ firmware,
1807
+ rowBusyId,
1808
+ language
1809
+ ],
1466
1810
  renderEmptyState: () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EmptyDevices, { onAdd: addDrawer.open }),
1467
1811
  children: (device) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TableRow, {
1468
1812
  id: device.id,
@@ -1502,7 +1846,7 @@ function DevicesPage() {
1502
1846
  variant: "soft",
1503
1847
  size: "sm",
1504
1848
  className: "sh:whitespace-nowrap",
1505
- children: generationLabel(device.generation)
1849
+ children: device.generation === null ? t("devices.unknown") : t(device.generation === 1 ? "devices.generation" : "devices.generationPlus", { generation: device.generation })
1506
1850
  })]
1507
1851
  })
1508
1852
  }),
@@ -1564,28 +1908,26 @@ function DevicesPage() {
1564
1908
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModalBackdrop, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModalContainer, {
1565
1909
  size: "sm",
1566
1910
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(ModalDialog, { children: [
1567
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModalHeader, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModalHeading, { children: "Delete device" }) }),
1568
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModalBody, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { children: [
1569
- "Remove ",
1570
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
1571
- className: "sh:font-semibold",
1572
- children: deleteTarget?.name
1573
- }),
1574
- " (",
1575
- deleteTarget?.ipAddress,
1576
- ") from the registry? The device itself is not changed."
1577
- ] }) }),
1911
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModalHeader, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModalHeading, { children: t("devices.delete") }) }),
1912
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModalBody, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: t("devices.deleteDescription", {
1913
+ name: deleteTarget?.name,
1914
+ address: deleteTarget?.ipAddress
1915
+ }) }) }),
1578
1916
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(ModalFooter, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
1579
1917
  variant: "secondary",
1580
1918
  onPress: () => setDeleteTarget(null),
1581
1919
  "data-cy": "shelly-delete-cancel",
1582
- children: "Cancel"
1920
+ children: t("common.cancel")
1583
1921
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button, {
1584
1922
  variant: "danger",
1585
1923
  onPress: confirmDelete,
1586
1924
  isPending: deleting,
1587
1925
  "data-cy": "shelly-delete-confirm",
1588
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Trash2Icon, { className: "sh:h-4 sh:w-4" }), " Delete"]
1926
+ children: [
1927
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Trash2Icon, { className: "sh:h-4 sh:w-4" }),
1928
+ " ",
1929
+ t("common.delete")
1930
+ ]
1589
1931
  })] })
1590
1932
  ] })
1591
1933
  }) })