diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 24c00aca..d51b656c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,29 +11,23 @@ jobs: prettier: name: Formatting Check runs-on: ubuntu-latest - steps: - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 with: node-version: 24 cache: npm - - run: npm ci - run: npm run format:check build: name: Production Build runs-on: ubuntu-latest - steps: - uses: actions/checkout@v6 - - uses: actions/setup-node@v6 with: node-version: 24 cache: npm - - run: npm ci - run: npm run build diff --git a/.gitignore b/.gitignore index eff0d12d..9d370db3 100644 --- a/.gitignore +++ b/.gitignore @@ -17,9 +17,7 @@ .DS_Store .env.local -.env.development.local -.env.test.local -.env.production.local +.env.*.local npm-debug.log* yarn-debug.log* diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d9f2520..18d3a2fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ - gui: Fixed double episode content in TeddyStudio [https://github.com/toniebox-reverse-engineering/teddycloud_web/issues/296](https://github.com/toniebox-reverse-engineering/teddycloud_web/issues/296) - gui: Added formatting (internal) - gui: Fixed some display issues in TeddyStudio labels +- gui: Fixed bug deleting only marked notifications +- gui: Added ESP32-C3 UART Gateway to cc3200 Box flashing guide [https://github.com/toniebox-reverse-engineering/teddycloud_web/issues/292](https://github.com/toniebox-reverse-engineering/teddycloud_web/issues/292) ### Commits diff --git a/README.md b/README.md index 6f60c86d..9d460163 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ You'll need to allow CORS for your teddyCloud instance used for development. The ## NPM Environment file '.env' -Please place an environment file '.env.development.local' in the teddycloud_web directory. +Please place an environment file '.env.local' in the teddycloud_web directory. ```env VITE_APP_TEDDYCLOUD_API_URL=http:// @@ -503,22 +503,6 @@ This means: - Performance reflects the real-world production setup - You can verify that your build works correctly before deployment -## GitHub Actions Runtime - -The workflow opts GitHub Actions into the Node.js 24 action runtime via: - -```yaml -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true -``` - -This avoids Node.js 20 deprecation warnings for JavaScript-based actions such as: - -- `actions/checkout@v4` -- `actions/setup-node@v4` - -while keeping the project runtime pinned separately through `setup-node`. - ## Learn More You can learn more in the [Vite documentation](https://vitejs.dev/guide/). diff --git a/package-lock.json b/package-lock.json index 71ef50e8..75a6f490 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,12 +8,12 @@ "name": "teddycloud-web", "version": "0.7.0", "dependencies": { - "@ant-design/icons": "^6.2.2", + "@ant-design/icons": "^6.2.3", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@remix-run/router": "^1.23.2", "@types/jest": "^30.0.0", - "@types/node": "^25.6.2", + "@types/node": "^25.7.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@types/w3c-web-serial": "^1.0.8", @@ -99,9 +99,9 @@ } }, "node_modules/@ant-design/icons": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.2.2.tgz", - "integrity": "sha512-zlJtE7AMbG12TeYVPhtBXwNpFInNy8mjLzcIm+0BPw16/b8ODG87YJ1G37VIF5VFscdgfsf6EweAFPTobu/3iQ==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.2.3.tgz", + "integrity": "sha512-Pl3aoAtxQeKryYnt6VvDJtOxMOtA8wrRSACe/pTjOAIG3fdHrWm6Ivb4ku9tsFjYroSXBKirvuxG4QkwBXD9gg==", "license": "MIT", "dependencies": { "@ant-design/colors": "^8.0.1", @@ -167,6 +167,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -511,6 +512,7 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", + "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -553,6 +555,7 @@ "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" @@ -564,6 +567,7 @@ "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -1914,6 +1918,7 @@ "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -2080,12 +2085,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.6.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.2.tgz", - "integrity": "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==", + "version": "25.7.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.7.0.tgz", + "integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==", "license": "MIT", "dependencies": { - "undici-types": "~7.19.0" + "undici-types": "~7.21.0" } }, "node_modules/@types/prismjs": { @@ -2099,6 +2104,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2322,6 +2328,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -2676,7 +2683,8 @@ "version": "1.11.20", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/debug": { "version": "4.4.3", @@ -2759,9 +2767,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.353", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.353.tgz", - "integrity": "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==", + "version": "1.5.354", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.354.tgz", + "integrity": "sha512-JaBHwWcfIdmSAfWM5l3uwjGd431j8YEMikZ+K/2nXVuBqJKyZ0f+2h4n4JY5AyNiZmnY9qQr2RU3v9DxDmHMNg==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -3098,6 +3106,7 @@ } ], "license": "MIT", + "peer": true, "peerDependencies": { "typescript": "^5 || ^6" }, @@ -4358,9 +4367,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.38", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", - "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "version": "2.0.44", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz", + "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==", "license": "MIT" }, "node_modules/pako": { @@ -4574,6 +4583,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -4583,6 +4593,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -5251,6 +5262,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5260,9 +5272,9 @@ } }, "node_modules/undici-types": { - "version": "7.19.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", - "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.21.0.tgz", + "integrity": "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==", "license": "MIT" }, "node_modules/unified": { @@ -5430,6 +5442,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.12.tgz", "integrity": "sha512-w2dDofOWv2QB09ZITZBsvKTVAlYvPR4IAmrY/v0ir9KvLs0xybR7i48wxhM1/oyBWO34wPns+bPGw5ZrZqDpZg==", "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", diff --git a/package.json b/package.json index b2963f19..8ffb412a 100644 --- a/package.json +++ b/package.json @@ -4,12 +4,12 @@ "private": true, "homepage": "/web", "dependencies": { - "@ant-design/icons": "^6.2.2", + "@ant-design/icons": "^6.2.3", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@remix-run/router": "^1.23.2", "@types/jest": "^30.0.0", - "@types/node": "^25.6.2", + "@types/node": "^25.7.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@types/w3c-web-serial": "^1.0.8", diff --git a/public/translations/de.json b/public/translations/de.json index 9127eb87..9e8eac0c 100644 --- a/public/translations/de.json +++ b/public/translations/de.json @@ -105,11 +105,11 @@ "empty": "Kein Plugin verfügbar", "error": { "notification": { - "message": "Fehlende Beschreibung json – Element \"pluginName\" im Plugin \"{{pluginId}}\", wird übersprungen.", + "message": "Fehlende Beschreibung json - Element \"pluginName\" im Plugin \"{{pluginId}}\", wird übersprungen.", "missingPluginIndexHtml": "Fehlende index.html im Plugin \"{{pluginId}}\"!", "title": "Fehler beim Laden des Plugins" }, - "pluginNotFound": "Plugin index.html nicht gefunden – bitte überprüfe das Plugin!", + "pluginNotFound": "Plugin index.html nicht gefunden - bitte überprüfe das Plugin!", "title": "Fehler beim Laden des Plugins" }, "errorLoadingPlugin": "Fehler beim Laden der Metadaten für Plugin {{pluginname}}", @@ -191,7 +191,7 @@ }, "formattedRequest": "Formatierte Anfrage", "generate": "Generieren", - "hintSendingSupportRequest": "Kopiere den folgenden Inhalt in die Telegram-Gruppe oder in einen neuen Foren-Thread. Bitte vergiss nicht, die gesammelten Logs und – falls vorhanden - zusätzliche Screenshots oder Bildschirmaufnahmen anzuhängen.", + "hintSendingSupportRequest": "Kopiere den folgenden Inhalt in die Telegram-Gruppe oder in einen neuen Foren-Thread. Bitte vergiss nicht, die gesammelten Logs und - falls vorhanden - zusätzliche Screenshots oder Bildschirmaufnahmen anzuhängen.", "intro": "Wenn du Unterstützung anforderst, hilft es der Community, dein Problem schneller zu diagnostizieren und zu lösen, wenn du detaillierte und klare Informationen bereitstellst. Folge diesen Schritten, um sicherzustellen, dass deine Support-Anfrage alle notwendigen Details enthält.", "logs": { "label": "Relevante Log-Auszüge" @@ -911,6 +911,7 @@ "no-bv": "Norwegisch (Bouvetinsel)", "no-no": "Norwegisch (Norwegen)", "no-sj": "Norwegisch (Svalbard und Jan Mayen)", + "other": "Andere", "pl-pl": "Polnisch (Polen)", "pt-ao": "Portugiesisch (Angola)", "pt-br": "Portugiesisch (Brasilien)", @@ -943,7 +944,6 @@ "tl-ph": "Tagalog (Philippinen)", "tr-tr": "Türkisch (Türkei)", "uk-ua": "Ukrainisch (Ukraine)", - "other": "Andere", "undefined": "Andere/undefiniert", "unknownLanguageCode": "Unbekannter Sprachcode: ", "ur-pk": "Urdu (Pakistan)", @@ -1350,7 +1350,8 @@ "bootloaderInstalled": "Bootloader erfolgreich installiert", "certificates": { "alreadyAvailable": "Die Zertifikate ca.der, client.der und private.der sollten bereits hier verfügbar sein:", - "extractAgain": "Falls nicht, stelle sicher, dass du Schritt 2 korrekt ausgeführt hast. Du kannst die Dateien auch erneut mit dem cc3200-Tool von der Box extrahieren (dein PC sollte weiterhin über UART und Debug-Port mit der Toniebox verbunden sein)." + "extractAgain": "Falls nicht, stelle sicher, dass du Schritt 2 korrekt ausgeführt hast. Du kannst die Dateien auch erneut mit dem cc3200-Tool von der Box extrahieren (dein PC sollte weiterhin über UART und Debug-Port mit der Toniebox verbunden sein).", + "uploadCertificates": "Lade diese Zertifikate mit dem folgenden Button in TeddyCloud hoch." }, "certificatesDumpedCAreplacementFlashed": "Zertifikate entnommen und CA-Ersatz geflasht", "checkBoxes": "Verfügbare Boxen anzeigen", @@ -1359,15 +1360,27 @@ "connectToTonieboxConnectDebugPortText2": " verwenden oder alternativ dünne Drähte für die Verbindung nutzen.", "connectToTonieboxConnectTableExplanation": "*Pin SOP2 der Toniebox sollte mit dem VCC der Toniebox überbrückt werden.", "connectToTonieboxConnectTableIntro": "Als Nächstes verbindest du den Debug-Port der Toniebox mit dem UART wie in der folgenden Tabelle beschrieben:", - "connectToTonieboxIntro": "Du musst deinen UART mit dem Debug-Port verbinden. Beginne damit, den Debug-Port auf der unbestückten Seite der Platine (die Unterseite der Platine) zu lokalisieren. Das Layout des Debug-Ports ist im folgenden Bild dargestellt.", + "connectToTonieboxIntro": "Du musst deinen UART/ESP32-C3 mit dem Debug-Port verbinden. Beginne damit, den Debug-Port auf der unbestückten Seite der Platine (die Unterseite der Platine) zu lokalisieren. Das Layout des Debug-Ports ist im folgenden Bild dargestellt.", "connectToTonieboxLink": "Hier findest du die Verbindungsanleitung", "connectToTonieboxProceed": "Jetzt bist du bereit, weiterzumachen!", - "connectToTonieboxText": "Stelle sicher, dass der UART auf 3,3V konfiguriert ist, bevor du den UART-Programmer über USB mit deinem Computer verbindest. Die grüne LED auf der Hauptplatine der Toniebox sollte dauerhaft leuchten (ohne zu blinken). Falls dies nicht der Fall ist, sind deine Verbindungen möglicherweise locker oder falsch. Trenne den Programmer, überprüfe alle Verbindungen sorgfältig und versuche es erneut. Wenn du mit dünnen Drähten arbeitest, könntest du Heißkleber verwenden, um sie zu fixieren und Bewegungen zu verhindern. Stelle außerdem sicher, dass die Toniebox über das Ladegerät mit Strom versorgt wird.", + "connectToTonieboxText": "Die grüne LED auf der Hauptplatine der Toniebox sollte dauerhaft leuchten (ohne zu blinken). Falls dies nicht der Fall ist, sind deine Verbindungen möglicherweise locker oder falsch. Trenne den Programmer, überprüfe alle Verbindungen sorgfältig und versuche es erneut. Wenn du mit dünnen Drähten arbeitest, könntest du Heißkleber verwenden, um sie zu fixieren und Bewegungen zu verhindern. Stelle außerdem sicher, dass die Toniebox über das Ladegerät mit Strom versorgt wird.", + "connectToTonieboxTextUart": "Stelle sicher, dass der UART auf 3,3V konfiguriert ist, bevor du den UART-Programmer über USB mit deinem Computer verbindest.", "createPatch": "Erstelle altUrl.custom.305.patch", "customUrlPatch": "Erstelle einen benutzerdefinierten URL-Patch (altUrl.custom.305.json)", "customUrlPatchHint": "Bitte erstelle diesen Patch nur, wenn die vordefinierten Patches altUrl.tc.fritz.box.json und altUrl.305.json mit deiner aktuellen TeddyCloud-Installation nicht funktionieren. Wenn sie deinen Anforderungen entsprechen, kannst du mit 'Weiter' fortfahren.", + "dedicatedUart": "Dedizierter UART", "dumpCertificates": "Zertifikate für TeddyCloud entnehmen", "dumpCertificatesLink": "Hier findest du die Anleitung zum Entnehmen von Zertifikaten", + "esp32C3UartGateway": { + "intro": "Du kannst einen ESP32-C3 als UART-Gateway verwenden.", + "new": "Neu", + "prepareStep1": "Verbinde deinen ESP32-C3 mit deinem PC.", + "prepareStep2": "Öffne das folgende Webtool von g3gg0, einem Mitglied von Team Revvox:", + "prepareStep3": "Öffne den Tab „Flasher“, klicke auf „Connect & Flash“ und warte, bis der Vorgang abgeschlossen ist.", + "prepareStep4": "Öffne den Tab „Config“, klicke auf „Connect“ und übernehme die folgenden Einstellungen. Klicke anschließend auf „Send Config“ und warte erneut, bis der Vorgang abgeschlossen ist:", + "prepareStep5": "Verbinde nun deine Toniebox wie folgt mit dem vorbereiteten ESP32-C3:", + "title": "ESP32-C3 UART-Gateway" + }, "flashCAreplacement": "CA-Ersatz für TeddyCloud flashen", "flashCAreplacementIntro": "Es wird empfohlen, das Ersatz-CA-Zertifikat nach /cert/c2.der zu flashen und den Hackiebox-NG-Bootloader mit dem altCA.305-Patch zu verwenden (weitere Details in den nächsten Schritten). Dadurch kannst du nahtlos zwischen dem Originalzertifikat und deinem Ersatzzertifikat wechseln.", "flashCAreplacementOutro": "Stelle sicher, dass du den richtigen Pfad zur Datei c2.der ausgewählt hast.", @@ -1531,7 +1544,8 @@ "confirmDeleteModal": "Bestätige Löschvorgang", "connectESP32Modal": { "beware": "Achtung!", - "connectESP32Text1": "Bitte verbinde den Jumper J100 (Boot) und setze die Box zurück, um sie in den erforderlichen UART-Modus zu versetzen. Verbinde dein 3,3 V UART mit J103 (Toniebox J103 [TxD | RxD | GND] -> UART: TxD -> RxD, RxD -> TxD, GND -> GND).", + "connectESP32Text1.1": "Bitte verbinde den Jumper J100 (Boot) und setze die Box zurück, um sie in den erforderlichen UART-Modus zu versetzen.", + "connectESP32Text1.2": "Verbinde dein 3,3 V UART mit J103:", "connectESP32Text2": "Wenn du dir nicht sicher bist, in welchem Modus der ESP32 startet, überprüfe Folgendes.", "downloadMode": "Download-Modus", "downloadModeText": "LED ist aus, kein Ton, Serieller Output:", @@ -1581,7 +1595,7 @@ "ackLatestFirmware": "Ich bestätige, dass die Box mit der originalen Cloud verbunden war, die aktuelle Firmware erhalten hat und ein Tonie erfolgreich abgespielt wurde.", "ackRisk": "Ich habe diesen Hinweis gelesen, verstanden und bin mir der damit verbundenen Risiken bewusst.", "ackUARTHint": "Ich habe die korrekte Spannung (3,3 V) eingestellt und den erforderlichen Treiber in der aktuellen Version installiert.", - "alternativeTools": "Unten findest du einige alternative Tools für ein Backup. Es gibt weitere Möglichkeiten – verwende gerne das Tool, mit dem du vertraut bist.", + "alternativeTools": "Unten findest du einige alternative Tools für ein Backup. Es gibt weitere Möglichkeiten - verwende gerne das Tool, mit dem du vertraut bist.", "alternativeToolsNoLinks": "Verwende einen ESP32-Reader, mit dem du vertraut bist, oder suche nach einem anderen geeigneten ESP32-Reader.", "automatically": "Automatisch", "backupFlash": "Wichtig: Sichere deine originale, ungepatchte Firmware", @@ -1599,7 +1613,7 @@ "connectingTo": "Verbinde zu", "connectingToESP": "Verbinde zum ESP...", "connectingWriteFlash": "Verbinde zum Schreiben des Flash...", - "downloadFlashFilesHint": "Die Firmware-Images werden im Datenverzeichnis deines Servers gespeichert, falls du sie erneut flashen möchtest. Falls du das noch nicht getan hast, lade jetzt unbedingt eine Kopie der Original-Firmware herunter und speichere sie sicher! So kannst du deine Toniebox im Notfall jederzeit in den Werkszustand zurückversetzen. Sichere die Firmware an mehreren Orten – zum Beispiel auf deinem Hauptrechner oder Laptop, einem externen Laufwerk oder USB-Stick und/oder in einem vertrauenswürdigen Cloud-Dienst (z. B. Google Drive, Dropbox, OneDrive). Verlasse dich niemals nur auf die Kopie auf dem TeddyCloud-Server.", + "downloadFlashFilesHint": "Die Firmware-Images werden im Datenverzeichnis deines Servers gespeichert, falls du sie erneut flashen möchtest. Falls du das noch nicht getan hast, lade jetzt unbedingt eine Kopie der Original-Firmware herunter und speichere sie sicher! So kannst du deine Toniebox im Notfall jederzeit in den Werkszustand zurückversetzen. Sichere die Firmware an mehreren Orten - zum Beispiel auf deinem Hauptrechner oder Laptop, einem externen Laufwerk oder USB-Stick und/oder in einem vertrauenswürdigen Cloud-Dienst (z. B. Google Drive, Dropbox, OneDrive). Verlasse dich niemals nur auf die Kopie auf dem TeddyCloud-Server.", "downloadLink": "Download ungepachte Firmware", "downloadLinkPatched": "Download gepachte Firmware", "downloadStubFailed": "Download des Stubs auf den ESP32 fehlgeschlagen! Bitte überprüfe die Verbindung und versuche es erneut!", @@ -1806,19 +1820,32 @@ "title": "Neuen Custom Tonie/Tag anlegen", "track": "Track" }, - "customToniesEditorJsonEntry": "Custom-Modelle", - "imageManager": { - "title": "Library: Bilder auswählen & hochladen", - "titleSelect": "Bild auswählen", - "okText": "Übernehmen", - "sourceCustom": "Custom", - "sourceOriginal": "Original", - "noOriginalImages": "Keine Original-Bilder gefunden", - "originalImagesLoading": "Original Bilder werden geladen …", - "originalUrlSearchPlaceholder": "Suchwörter (alle müssen in der URL vorkommen, Reihenfolge egal) …", - "noOriginalSearchResults": "Keine URLs passen zur Suche", - "originalUrlColumn": "URL" + "alternativeSource": "Der Toniefigur {{originalTonie}} ist der Inhalt {{assignedContent}} zugewiesen!", + "alternativeSourceUnknown": "Der Toniefigur {{originalTonie}} ist alternativer Inhalt zugewiesen!", + "cancel": "Abbrechen", + "closeAudioPlayer": "Player schließen", + "closeAudioPlayerPopover": "Schließen stoppt die Wiedergabe!", + "confirmAudioModelMismatchModal": { + "cancel": "Abbrechen", + "confirm": "Fortfahren", + "content": "Das zugewiesene Audio hat Modell „{{audioModel}}“. Die Anzeige auf der Box kommt von der audio_id der TAF. Das leere Modell wird auf „{{audioModel}}“ zurückgesetzt. Fortfahren?", + "title": "Modell vs. Audio", + "unknownModel": "unbekannt" + }, + "confirmHideModal": { + "cancel": "Abbrechen", + "confirmHideDialog": "Möchtest du den Tonie/Tag \"{{tonieToHide}}\" wirklich aus der Toniesübersicht ausblenden? Um ihn wieder einzublenden musst du den Tonie/Tag auf eine deiner Tonieboxen platzieren.", + "hide": "Ausblenden", + "title": "Tonie/Tag ausblenden" }, + "connectionToBoxineNotAvailable": "Verbindung zu Boxine/Toniecloud nicht möglich. Bitte erlaube den Zugriff, um den Download zu ermöglichen.", + "content": { + "navigationTitle": "Content", + "showToniesOfBoxes": "Inhalte der folgenden Tonieboxen anzeigen", + "title": "Content", + "toniePicture": "Tonie Bild" + }, + "currentPath": "Aktueller Pfad: ", "customEditor": { "actions": { "browse": "Durchsuchen", @@ -1833,41 +1860,54 @@ }, "audio": { "libraryLabel": "Custom-Audio ({{library}})", - "selectFromLibrary": "Audio aus {{library}} auswählen", "notInIndex": "Nicht im Custom-Audio-Index: {{audioId}}/{{hash}}", - "placeholder": "Custom-Audio aus {{library}} wählen" + "placeholder": "Custom-Audio aus {{library}} wählen", + "selectFromLibrary": "Audio aus {{library}} auswählen" }, "batch": { "description": "Änderungen gelten für alle ausgewählten Modelle.", "title": "Batch-Bearbeitung aktiv" }, "coinHint": { - "description": "Optional – nur für Metadaten (Titel, Tracks). Custom Coins funktionieren auch ohne Modell. Nur Custom-Audio – offizielle IDs überschreiben echte Tonies. Verknüpft keine NFC-Tags mit Audio.", + "description": "Optional - nur für Metadaten (Titel, Tracks). Custom Tags funktionieren auch ohne Modell. Nur Custom-Audio - offizielle IDs überschreiben echte Tonies. Verknüpft keine NFC-Tags mit Audio.", "title": "Hinweis: Audio-Zuordnung" }, "columns": { "actions": "Aktionen", "image": "Bild" }, - "errors": { - "emptyResult": "Mindestens ein Modell muss erhalten bleiben.", - "invalidSelectedIndex": "Ungültiger ausgewählter Index", - "releaseNumeric": "Release muss numerisch sein", - "duplicateModel": "Duplikat: {{model}}", - "duplicateAudioHash": "Doppelte audio_id+hash-Kombination: {{audioId}}", - "modelExistsInBase": "Modell existiert in base tonies.json: {{models}}", - "invalidLanguageCode": "Ungültiger Sprachcode. Bitte verwende einen Code wie {{example}}." + "deleteConfirm": { + "abort": "Abbrechen", + "confirm": "Löschen", + "deleted": "Modell \"{{model}}\" wurde gelöscht.", + "description": "\"{{model}}\" wirklich löschen? Dies kann nicht rückgängig gemacht werden.", + "title": "Modell löschen" }, "duplicateTitleSuffix": "Kopie", "editModalTitle": "Modell {{current}} von {{total}} bearbeiten", "editModalTitleCreate": "Neues Modell", "editModalTitleEdit": "Modell bearbeiten", + "emptyState": "Keine Custom-Modelle", + "emptyStateHint": "tonies.custom.json ist leer. Füge dein erstes Modell hinzu.", + "errors": { + "duplicateAudioHash": "Doppelte audio_id+hash-Kombination: {{audioId}}", + "duplicateModel": "Duplikat: {{model}}", + "emptyResult": "Mindestens ein Modell muss erhalten bleiben.", + "invalidLanguageCode": "Ungültiger Sprachcode. Bitte verwende einen Code wie {{example}}.", + "invalidSelectedIndex": "Ungültiger ausgewählter Index", + "modelExistsInBase": "Modell existiert in base tonies.json: {{models}}", + "releaseNumeric": "Release muss numerisch sein" + }, "filterFieldsPlaceholder": "In Feldern suchen", "filterPlaceholder": "Modelle filtern...", + "modelSavedSuccessful": "Modell {{title}} wurde erfolgreich gespeichert", "modelsTitle": "Modelle", "noChangesBody": "Es gibt aktuell nichts zu speichern.", "noChangesTitle": "Keine Änderungen erkannt", "optionalColumns": "Zusatzspalten", + "pagination": { + "modelsPerPage": "Modelle/Seite" + }, "picHint": "Bild aus custom_img oder von Original-Tonies wählen", "preflight": { "confirm": "Jetzt speichern", @@ -1878,9 +1918,16 @@ "upserts": "Aktualisierungen/Neuanlagen: {{count}}" }, "previewTitle": "Bildvorschau", - "saveSuccessWithCount": "tonies.custom.json gespeichert ({{count}} Einträge). Sicherung und Neuladen wurden ausgelöst.", + "renameConfirm": { + "abort": "Abbrechen", + "confirm": "Bestätigen", + "description": "Modell-ID wird von \"{{from}}\" zu \"{{to}}\" geändert. Verknüpfte Tonies werden automatisch aktualisiert.", + "skipUpdate": "Modellzuordnung von verknüpften Tonies entfernen (Audio bleibt)", + "title": "Modell umbenennen" + }, + "renameFailed": "Umbenennung fehlgeschlagen", "savedState": "Alle Änderungen gespeichert", - "modelSavedSuccessful": "Modell {{title}} wurde erfolgreich gespeichert", + "saveSuccessWithCount": "tonies.custom.json gespeichert ({{count}} Einträge). Sicherung und Neuladen wurden ausgelöst.", "sections": { "audio": "Audio-Zuordnung", "audioAssignment": "Audio-Zuordnung", @@ -1899,74 +1946,35 @@ "unsaved": "{{count}} ungespeicherte Änderung(en)", "validation": { "title": "Bitte zuerst diese Probleme beheben" - }, - "renameFailed": "Umbenennung fehlgeschlagen", - "renameConfirm": { - "title": "Modell umbenennen", - "description": "Modell-ID wird von \"{{from}}\" zu \"{{to}}\" geändert. Verknüpfte Tonies werden automatisch aktualisiert.", - "skipUpdate": "Modellzuordnung von verknüpften Tonies entfernen (Audio bleibt)", - "confirm": "Bestätigen", - "abort": "Abbrechen" - }, - "deleteConfirm": { - "title": "Modell löschen", - "description": "\"{{model}}\" wirklich löschen? Dies kann nicht rückgängig gemacht werden.", - "deleted": "Modell \"{{model}}\" wurde gelöscht.", - "confirm": "Löschen", - "abort": "Abbrechen" - }, - "emptyState": "Keine Custom-Modelle", - "emptyStateHint": "tonies.custom.json ist leer. Füge dein erstes Modell hinzu.", - "pagination": { - "modelsPerPage": "Modelle/Seite" } }, + "customImages": { + "navigationTitle": "Bilder", + "title": "Eigene Bilder verwalten" + }, "customModelPicker": { - "title": "Custom-Modell auswählen", - "searchPlaceholder": "Modell, Serie suchen...", "addNew": "Neu hinzufügen", "loading": "Laden...", - "noModels": "Keine Custom-Modelle gefunden" - }, - "alternativeSource": "Der Toniefigur {{originalTonie}} ist der Inhalt {{assignedContent}} zugewiesen!", - "alternativeSourceUnknown": "Der Toniefigur {{originalTonie}} ist alternativer Inhalt zugewiesen!", - "cancel": "Abbrechen", - "closeAudioPlayer": "Player schließen", - "closeAudioPlayerPopover": "Schließen stoppt die Wiedergabe!", - "confirmHideModal": { - "cancel": "Abbrechen", - "confirmHideDialog": "Möchtest du den Tonie/Tag \"{{tonieToHide}}\" wirklich aus der Toniesübersicht ausblenden? Um ihn wieder einzublenden musst du den Tonie/Tag auf eine deiner Tonieboxen platzieren.", - "hide": "Ausblenden", - "title": "Tonie/Tag ausblenden" - }, - "confirmAudioModelMismatchModal": { - "cancel": "Abbrechen", - "confirm": "Fortfahren", - "title": "Modell vs. Audio", - "content": "Das zugewiesene Audio hat Modell „{{audioModel}}“. Die Anzeige auf der Box kommt von der audio_id der TAF. Das leere Modell wird auf „{{audioModel}}“ zurückgesetzt. Fortfahren?", - "unknownModel": "unbekannt" - }, - "connectionToBoxineNotAvailable": "Verbindung zu Boxine/Toniecloud nicht möglich. Bitte erlaube den Zugriff, um den Download zu ermöglichen.", - "content": { - "navigationTitle": "Content", - "showToniesOfBoxes": "Inhalte der folgenden Tonieboxen anzeigen", - "title": "Content", - "toniePicture": "Tonie Bild" + "noModels": "Keine Custom-Modelle gefunden", + "searchPlaceholder": "Modell, Serie suchen...", + "title": "Custom-Modell auswählen" }, - "currentPath": "Aktueller Pfad: ", + "customToniesEditorJsonEntry": "Custom-Modelle", "editModal": { + "audioInfoHeading": "Audio-Info", + "createNewModelTooltip": "Neues Modell erstellen", + "editModelTooltip": "Modell bearbeiten", "editSelectedCustomModel": "Modell weiter bearbeiten", "invalidModelError": "Das Modell \"{{model}}\" ist ungültig. Bitte ein vorhandenes Modell aus der Suche auswählen.", + "model": "Modell", + "modelInfoAudioPath": "Audio-Pfad", + "modelInfoCategory": "Kategorie", + "modelInfoEpisode": "Episode", "modelInfoHeading": "Modell-Info", - "audioInfoHeading": "Audio-Info", + "modelInfoLanguage": "Sprache", "modelInfoModel": "Modell", "modelInfoSeries": "Serie", - "modelInfoEpisode": "Episode", "modelInfoTitle": "Titel", - "modelInfoLanguage": "Sprache", - "modelInfoCategory": "Kategorie", - "modelInfoAudioPath": "Audio-Pfad", - "model": "Modell", "modelReadOnlyHint": "Original-Tonie: Modell kann nicht geändert werden. Du kannst nur eigenes Audio zuweisen.", "placeholderSearchForAModel": "Nach einem Modell suchen", "placeholderSearchForARadioStream": "Suche nach einem Radiostream", @@ -1975,9 +1983,7 @@ "setAudioFromModelUnavailableInLibrary": "Audio aus dem Modell ist noch nicht in der Bibliothek verfügbar.", "setModelFromAudio": "Modell aus Audio setzen", "source": "Quelle", - "createNewModelTooltip": "Neues Modell erstellen", - "editModelTooltip": "Modell bearbeiten", - "title": "Tonie/Coin bearbeiten" + "title": "Tonie/Tag bearbeiten" }, "encoder": { "browserEncodingInProgress": "Audio-Dateien werden im Browser encodiert...", @@ -2104,6 +2110,18 @@ }, "title": "Hilfe" }, + "imageManager": { + "noOriginalImages": "Keine Original-Bilder gefunden", + "noOriginalSearchResults": "Keine URLs passen zur Suche", + "okText": "Übernehmen", + "originalImagesLoading": "Original Bilder werden geladen …", + "originalUrlColumn": "URL", + "originalUrlSearchPlaceholder": "Suchwörter (alle müssen in der URL vorkommen, Reihenfolge egal) …", + "sourceCustom": "Custom", + "sourceOriginal": "Original", + "title": "Library: Bilder auswählen & hochladen", + "titleSelect": "Bild auswählen" + }, "infoModal": { "download": "Download TAF als *.ogg", "exists": "Existiert:", @@ -2127,10 +2145,6 @@ "navigationTitle": "Library", "title": "Library" }, - "customImages": { - "navigationTitle": "Bilder", - "title": "Eigene Bilder verwalten" - }, "libraryAudio": { "navigationTitle": "Audio" }, @@ -2259,6 +2273,21 @@ "adaptLabelsHint": "Du kannst die Label-Texte und Bilder anpassen, indem du auf den Bearbeiten-Button jeder Labelgruppe klickst.", "addedCustomImageHint": "Benutzerdefiniertes Bild wurde hinzugefügt.", "addedHint": "\"{{title}}\" wurde hinzugefügt.", + "bulkAdd": { + "button": "Mehrere Tonies hinzufügen", + "modal": { + "confirm": "{{count}} hinzufügen", + "itemsUnit": "Tonies", + "itemUnit": "Tonie", + "leftTitle": "tonies.json", + "notFound": "Keine passenden Tonies", + "rightTitle": "Zum Hinzufügen vorgemerkt", + "searchHint": "Bitte mindestens 2 Zeichen eingeben, um tonies.json zu durchsuchen.", + "searchPlaceholder": "Suchen", + "searchPrompt": "tonies.json durchsuchen…", + "title": "Mehrere Tonies zum Druckbogen hinzufügen" + } + }, "cancel": "Abbrechen", "clear": "Blatt leeren", "clearSettings": "Standardwerte", @@ -2267,16 +2296,18 @@ "currentLabel": "Aktuelles Label", "custom": "Benutzerdefiniert", "customImage": "Eigene Bilder", - "customImageHint": "Du kannst temporär eigene Bilder hinzufügen – sie werden NICHT in die Library übernommen und nur für dieses Druckblatt verwendet.", + "customImageHint": "Du kannst temporär eigene Bilder hinzufügen - sie werden NICHT in die Library übernommen und nur für dieses Druckblatt verwendet.", "customImageTitle": "Gib einen Titel ein, der gedruckt werden soll", "customImageUpload": "Temporäres Bild hinzufügen (ohne Library)", "diameter": "Innendurchmesser", "empty": "Leer - Bitte wähle zuerst Tonies oder Tags aus oder füge ein eigenes Bild hinzu!", "episodes": "Episoden", + "fontFamily": "Schriftart", + "fontFamilyPlaceholder": "Schriftart wählen", + "imageBottomLeft": "Bildabstand unten/links", "imagePosition": "Bildausrichtung", "imageScale": "Bildskalierung", - "imageBottomLeft": "Bildabstand unten/links", - "intro": "Durchsuche alle Tonies und Tags - sowohl offizielle als auch deine eigenen - um dein druckbares Traveltonies (Münze) Blatt zu erstellen.", + "intro": "Durchsuche alle Tonies und Tags - sowohl offizielle als auch deine eigenen - um dein druckbares Traveltonies Blatt zu erstellen.", "labelBackgroundColor": "Hintergrund", "labelEditTitle": "Label-Elemente bearbeiten", "labelImage": "Labelbild", @@ -2301,7 +2332,7 @@ "preview": "Vorschau Label", "printMode": "Druckmodus", "printModeImageAndText": "Bild + Text", - "printModeImageAndTextTooltip": "Beide Seiten deiner Traveltonie-Münze drucken: Bild auf der einen Seite, Titel und Episodeninfos auf der anderen", + "printModeImageAndTextTooltip": "Beide Seiten deiner Traveltonie drucken: Bild auf der einen Seite, Titel und Episodeninfos auf der anderen", "printModeOnlyImage": "Bild", "printModeOnlyImageTooltip": "Nur eine Seite mit dem Bild der Tags drucken", "printModeOnlyText": "Text", @@ -2328,24 +2359,7 @@ "square": "Eckig", "textFontSize": "Textgröße", "title": "TeddyStudio", - "trackTitles": "Titel (jeweils eine Zeile)", - "fontFamily": "Schriftart", - "fontFamilyPlaceholder": "Schriftart wählen", - "bulkAdd": { - "button": "Mehrere Tonies hinzufügen", - "modal": { - "title": "Mehrere Tonies zum Druckbogen hinzufügen", - "leftTitle": "tonies.json", - "rightTitle": "Zum Hinzufügen vorgemerkt", - "searchPlaceholder": "Suchen", - "searchPrompt": "tonies.json durchsuchen…", - "searchHint": "Bitte mindestens 2 Zeichen eingeben, um tonies.json zu durchsuchen.", - "itemUnit": "Tonie", - "itemsUnit": "Tonies", - "notFound": "Keine passenden Tonies", - "confirm": "{{count}} hinzufügen" - } - } + "trackTitles": "Titel (jeweils eine Zeile)" }, "title": "Tonies", "tonies": { diff --git a/public/translations/en.json b/public/translations/en.json index 3d027e7e..b4ae6782 100644 --- a/public/translations/en.json +++ b/public/translations/en.json @@ -911,6 +911,7 @@ "no-bv": "Norwegian (Bouvet Island)", "no-no": "Norwegian (Norway)", "no-sj": "Norwegian (Svalbard and Jan Mayen)", + "other": "Other", "pl-pl": "Polish (Poland)", "pt-ao": "Portuguese (Angola)", "pt-br": "Portuguese (Brazil)", @@ -943,7 +944,6 @@ "tl-ph": "Tagalog (Philippines)", "tr-tr": "Turkish (Turkey)", "uk-ua": "Ukrainian (Ukraine)", - "other": "Other", "undefined": "Other/undefined", "unknownLanguageCode": "Unknown language code: ", "ur-pk": "Urdu (Pakistan)", @@ -1350,7 +1350,8 @@ "bootloaderInstalled": "Bootloader successfully installed", "certificates": { "alreadyAvailable": "The certificates ca.der, client.der and private.der should be already available in:", - "extractAgain": "If not, ensure you have done step 2 correctly. You can also extract the files from the box using the cc3200 tool once more (your PC should still be connected via UART and debug port to the Toniebox)" + "extractAgain": "If not, ensure you have done step 2 correctly. You can also extract the files from the box using the cc3200 tool once more (your PC should still be connected via UART/ESP32-C3 and debug port to the Toniebox)", + "uploadCertificates": "Upload those certificates to TeddyCloud using the following button." }, "certificatesDumpedCAreplacementFlashed": "Certificates dumped and CA replacement flashed", "checkBoxes": "Show available boxes", @@ -1359,15 +1360,27 @@ "connectToTonieboxConnectDebugPortText2": " or alternatively use thin wires for the connection.", "connectToTonieboxConnectTableExplanation": "*Pin SOP2 of the Toniebox should be bridged with the VCC of the Toniebox.", "connectToTonieboxConnectTableIntro": "Next, connect the Toniebox debug port to the UART as described in the following table:", - "connectToTonieboxIntro": "You need to connect your UART to the Debug Port. Start by locating the debug port on the unpopulated side of the PCB, which is on the bottom. The layout of the debug port is shown in the following image.", + "connectToTonieboxIntro": "You need to connect your UART/ESP32-C3 to the Debug Port. Start by locating the debug port on the unpopulated side of the PCB, which is on the bottom. The layout of the debug port is shown in the following image.", "connectToTonieboxLink": "Find connection instructions here", "connectToTonieboxProceed": "You are now ready to proceed!", - "connectToTonieboxText": "Ensure that the UART is configured to 3.3V before connecting the UART programmer to your computer via USB. The green LED on the Toniebox mainboard should stay steadily lit (no flashing or blinking). If it does not, your connections might be loose or incorrect. Disconnect the programmer, double-check all connections, and try again. If you are working with thin wires, consider applying hot glue to secure them in place and prevent any movement. Additionally, ensure that the Toniebox is powered using its charger.", + "connectToTonieboxText": "The green LED on the Toniebox mainboard should stay steadily lit (no flashing or blinking). If it does not, your connections might be loose or incorrect. Disconnect the programmer, double-check all connections, and try again. If you are working with thin wires, consider applying hot glue to secure them in place and prevent any movement. Additionally, ensure that the Toniebox is powered using its charger.", + "connectToTonieboxTextUart": "Ensure that the UART is configured to 3.3V before connecting the UART programmer to your computer via USB.", "createPatch": "Create altUrl.custom.305.patch", "customUrlPatch": "Create a custom URL Patch (altUrl.custom.305.json)", "customUrlPatchHint": "Please create this patch only if the predefined altUrl.tc.fritz.box.json and altUrl.305.json patches are not functioning with your current TeddyCloud setup. If they do meet your needs you can proceed by clicking 'Next'.", + "dedicatedUart": "Dedicated UART", "dumpCertificates": "Dump certificates for TeddyCloud", "dumpCertificatesLink": "Find instructions for dumping certificates here", + "esp32C3UartGateway": { + "intro": "You can use an ESP32-C3 as a UART gateway.", + "new": "New", + "prepareStep1": "Connect your ESP32-C3 to your PC.", + "prepareStep2": "Open the following web tool from g3gg0, a member of Team Revvox:", + "prepareStep3": "Open the \"Flasher\" tab, click \"Connect & Flash\", and wait until the process is finished.", + "prepareStep4": "Open the \"Config\" tab, click \"Connect\", apply the following settings, then click \"Send Config\" and wait until the process is finished again:", + "prepareStep5": "Now connect your Toniebox to the prepared ESP32-C3 as follows:", + "title": "ESP32-C3 UART Gateway" + }, "flashCAreplacement": "Flash CA replacement for TeddyCloud", "flashCAreplacementIntro": "It is recommended to flash the replacement CA to /cert/c2.der and use the Hackiebox-NG bootloader with the altCA.305 patch (more details provided in the next steps). This setup enables you to switch seamlessly between the original certificate and your replacement certificate.", "flashCAreplacementOutro": "Ensure that you have selected the correct path to the c2.der file.", @@ -1531,7 +1544,8 @@ "confirmDeleteModal": "Confirm delete", "connectESP32Modal": { "beware": "Beware!", - "connectESP32Text1": "Please connect the jumper J100 (Boot) and reset the box to put it into the required UARTmode. Connect your 3.3V UART to J103 (Toniebox J103 [TxD | RxD | GND] -> UART: TxD -> RxD, RxD -> TxD, GND -> GND).", + "connectESP32Text1.1": "Please connect the jumper J100 (Boot) and reset the box to put it into the required UARTmode.", + "connectESP32Text1.2": "Connect your 3.3V UART to J103:", "connectESP32Text2": "If you`re unsure in which mode the ESP32 is starting, check the following.", "downloadMode": "Download mode", "downloadModeText": "LED is off, no sound, Serial output:", @@ -1806,19 +1820,32 @@ "title": "Create new Custom Tonie/Tag", "track": "Track" }, - "customToniesEditorJsonEntry": "Custom models", - "imageManager": { - "title": "Library: Select & upload images", - "titleSelect": "Select image", - "okText": "Apply", - "sourceCustom": "Custom", - "sourceOriginal": "Original", - "noOriginalImages": "No original images found", - "originalImagesLoading": "Loading original images …", - "originalUrlSearchPlaceholder": "Words to find in URL (all must match, order ignored) …", - "noOriginalSearchResults": "No URLs match your search", - "originalUrlColumn": "URL" + "alternativeSource": "This Tonie {{originalTonie}} is assigned the content {{assignedContent}}!", + "alternativeSourceUnknown": "This Tonie {{originalTonie}} is assigned alternative content!", + "cancel": "Cancel", + "closeAudioPlayer": "Close player", + "closeAudioPlayerPopover": "Close stops playback!", + "confirmAudioModelMismatchModal": { + "cancel": "Cancel", + "confirm": "Continue", + "content": "The assigned audio has model \"{{audioModel}}\". The display on the box comes from the audio_id of the TAF. The empty model will be reset to \"{{audioModel}}\". Continue?", + "title": "Model vs. Audio", + "unknownModel": "unknown" + }, + "confirmHideModal": { + "cancel": "Cancel", + "confirmHideDialog": "Do you really want to hide the Tonie/Tag \"{{tonieToHide}}\" in the Tonies overview? To make it visible again, you need to place the Tonie/Tag on one of your Tonieboxes.", + "hide": "Hide", + "title": "Hide Tonie/Tag" }, + "connectionToBoxineNotAvailable": "Unable to connect to Boxine/Toniecloud. Please enable access to allow downloading.", + "content": { + "navigationTitle": "Content", + "showToniesOfBoxes": "Content of the following Tonieboxes", + "title": "Content", + "toniePicture": "Tonie Picture" + }, + "currentPath": "Current Path: ", "customEditor": { "actions": { "browse": "Browse", @@ -1833,41 +1860,54 @@ }, "audio": { "libraryLabel": "Custom audio ({{library}})", - "selectFromLibrary": "Select audio from {{library}}", "notInIndex": "Not in custom audio index: {{audioId}}/{{hash}}", - "placeholder": "Select custom audio from {{library}}" + "placeholder": "Select custom audio from {{library}}", + "selectFromLibrary": "Select audio from {{library}}" }, "batch": { "description": "Changes apply to all selected models.", "title": "Batch editing active" }, "coinHint": { - "description": "Optional – only for metadata (title, tracks). Custom coins work without a model. Use only custom audio – official IDs overwrite real Tonies. Does not link NFC tags to audio.", + "description": "Optional - only for metadata (title, tracks). Custom tags work without a model. Use only custom audio - official IDs overwrite real Tonies. Does not link NFC tags to audio.", "title": "Hint: Audio assignment" }, "columns": { "actions": "Actions", "image": "Image" }, - "errors": { - "emptyResult": "At least one model must remain.", - "invalidSelectedIndex": "Invalid selected index", - "releaseNumeric": "Release must be numeric", - "duplicateModel": "Duplicate: {{model}}", - "duplicateAudioHash": "Duplicate audio_id+hash combination: {{audioId}}", - "modelExistsInBase": "Model exists in base tonies.json: {{models}}", - "invalidLanguageCode": "Invalid language code. Please use a code like {{example}}." + "deleteConfirm": { + "abort": "Abort", + "confirm": "Delete", + "deleted": "Model \"{{model}}\" has been deleted.", + "description": "Really delete \"{{model}}\"? This cannot be undone.", + "title": "Delete model" }, "duplicateTitleSuffix": "Copy", "editModalTitle": "Edit model {{current}} of {{total}}", "editModalTitleCreate": "New model", "editModalTitleEdit": "Edit model", + "emptyState": "No custom models", + "emptyStateHint": "tonies.custom.json is empty. Add your first model.", + "errors": { + "duplicateAudioHash": "Duplicate audio_id+hash combination: {{audioId}}", + "duplicateModel": "Duplicate: {{model}}", + "emptyResult": "At least one model must remain.", + "invalidLanguageCode": "Invalid language code. Please use a code like {{example}}.", + "invalidSelectedIndex": "Invalid selected index", + "modelExistsInBase": "Model exists in base tonies.json: {{models}}", + "releaseNumeric": "Release must be numeric" + }, "filterFieldsPlaceholder": "Search in fields", "filterPlaceholder": "Filter models...", + "modelSavedSuccessful": "Model {{title}} was successfully saved", "modelsTitle": "Models", "noChangesBody": "There is nothing to save at the moment.", "noChangesTitle": "No changes detected", "optionalColumns": "Optional columns", + "pagination": { + "modelsPerPage": "models/page" + }, "picHint": "Select image from custom_img or from original tonies", "preflight": { "confirm": "Save now", @@ -1878,9 +1918,16 @@ "upserts": "Updates/new entries: {{count}}" }, "previewTitle": "Image preview", - "saveSuccessWithCount": "tonies.custom.json saved ({{count}} entries). Backup and reload triggered.", + "renameConfirm": { + "abort": "Abort", + "confirm": "Confirm", + "description": "Model ID will change from \"{{from}}\" to \"{{to}}\". Linked Tonies will be updated automatically.", + "skipUpdate": "Remove model assignment from linked Tonies (audio remains)", + "title": "Rename model" + }, + "renameFailed": "Rename failed", "savedState": "All changes saved", - "modelSavedSuccessful": "Model {{title}} was successfully saved", + "saveSuccessWithCount": "tonies.custom.json saved ({{count}} entries). Backup and reload triggered.", "sections": { "audio": "Audio assignment", "audioAssignment": "Audio assignment", @@ -1899,74 +1946,35 @@ "unsaved": "{{count}} unsaved change(s)", "validation": { "title": "Please fix these issues first" - }, - "renameFailed": "Rename failed", - "renameConfirm": { - "title": "Rename model", - "description": "Model ID will change from \"{{from}}\" to \"{{to}}\". Linked Tonies will be updated automatically.", - "skipUpdate": "Remove model assignment from linked Tonies (audio remains)", - "confirm": "Confirm", - "abort": "Abort" - }, - "deleteConfirm": { - "title": "Delete model", - "description": "Really delete \"{{model}}\"? This cannot be undone.", - "deleted": "Model \"{{model}}\" has been deleted.", - "confirm": "Delete", - "abort": "Abort" - }, - "emptyState": "No custom models", - "emptyStateHint": "tonies.custom.json is empty. Add your first model.", - "pagination": { - "modelsPerPage": "models/page" } }, + "customImages": { + "navigationTitle": "Images", + "title": "Manage custom images" + }, "customModelPicker": { - "title": "Select custom model", - "searchPlaceholder": "Search model, series...", "addNew": "Add new", "loading": "Loading...", - "noModels": "No custom models found" - }, - "alternativeSource": "This Tonie {{originalTonie}} is assigned the content {{assignedContent}}!", - "alternativeSourceUnknown": "This Tonie {{originalTonie}} is assigned alternative content!", - "cancel": "Cancel", - "closeAudioPlayer": "Close player", - "closeAudioPlayerPopover": "Close stops playback!", - "confirmHideModal": { - "cancel": "Cancel", - "confirmHideDialog": "Do you really want to hide the Tonie/Tag \"{{tonieToHide}}\" in the Tonies overview? To make it visible again, you need to place the Tonie/Tag on one of your Tonieboxes.", - "hide": "Hide", - "title": "Hide Tonie/Tag" - }, - "confirmAudioModelMismatchModal": { - "cancel": "Cancel", - "confirm": "Continue", - "title": "Model vs. Audio", - "content": "The assigned audio has model \"{{audioModel}}\". The display on the box comes from the audio_id of the TAF. The empty model will be reset to \"{{audioModel}}\". Continue?", - "unknownModel": "unknown" - }, - "connectionToBoxineNotAvailable": "Unable to connect to Boxine/Toniecloud. Please enable access to allow downloading.", - "content": { - "navigationTitle": "Content", - "showToniesOfBoxes": "Content of the following Tonieboxes", - "title": "Content", - "toniePicture": "Tonie Picture" + "noModels": "No custom models found", + "searchPlaceholder": "Search model, series...", + "title": "Select custom model" }, - "currentPath": "Current Path: ", + "customToniesEditorJsonEntry": "Custom models", "editModal": { + "audioInfoHeading": "Audio Info", + "createNewModelTooltip": "Create new model", + "editModelTooltip": "Edit model", "editSelectedCustomModel": "Continue editing model", "invalidModelError": "Model \"{{model}}\" is invalid. Please select an existing model from search.", + "model": "Model", + "modelInfoAudioPath": "Audio Path", + "modelInfoCategory": "Category", + "modelInfoEpisode": "Episode", "modelInfoHeading": "Model Info", - "audioInfoHeading": "Audio Info", + "modelInfoLanguage": "Language", "modelInfoModel": "Model", "modelInfoSeries": "Series", - "modelInfoEpisode": "Episode", "modelInfoTitle": "Title", - "modelInfoLanguage": "Language", - "modelInfoCategory": "Category", - "modelInfoAudioPath": "Audio Path", - "model": "Model", "modelReadOnlyHint": "Original tonie: Model cannot be changed. You can only assign custom audio.", "placeholderSearchForAModel": "Search for a model", "placeholderSearchForARadioStream": "Search for a radio stream", @@ -1975,9 +1983,7 @@ "setAudioFromModelUnavailableInLibrary": "Model audio is not yet available in the library.", "setModelFromAudio": "Set model from audio", "source": "Source", - "createNewModelTooltip": "Create new model", - "editModelTooltip": "Edit model", - "title": "Edit Tonie/Coin" + "title": "Edit Tonie/Tag" }, "encoder": { "browserEncodingInProgress": "Encoding audio files in browser...", @@ -2104,6 +2110,18 @@ }, "title": "Help" }, + "imageManager": { + "noOriginalImages": "No original images found", + "noOriginalSearchResults": "No URLs match your search", + "okText": "Apply", + "originalImagesLoading": "Loading original images …", + "originalUrlColumn": "URL", + "originalUrlSearchPlaceholder": "Words to find in URL (all must match, order ignored) …", + "sourceCustom": "Custom", + "sourceOriginal": "Original", + "title": "Library: Select & upload images", + "titleSelect": "Select image" + }, "infoModal": { "download": "Download TAF as *.ogg", "exists": "Exists:", @@ -2127,10 +2145,6 @@ "navigationTitle": "Library", "title": "Library" }, - "customImages": { - "navigationTitle": "Images", - "title": "Manage custom images" - }, "libraryAudio": { "navigationTitle": "Audio" }, @@ -2259,6 +2273,21 @@ "adaptLabelsHint": "You can adapt the labels texts and images by clicking the edit button on each labelgroup.", "addedCustomImageHint": "Custom image has been added.", "addedHint": "\"{{title}}\" has been added.", + "bulkAdd": { + "button": "Bulk add tonies", + "modal": { + "confirm": "Add {{count}} to sheet", + "itemsUnit": "tonies", + "itemUnit": "tonie", + "leftTitle": "tonies.json", + "notFound": "No matching tonies", + "rightTitle": "Queued to add", + "searchHint": "Type at least 2 characters to search tonies.json.", + "searchPlaceholder": "Search", + "searchPrompt": "Search tonies.json…", + "title": "Bulk add tonies to print sheet" + } + }, "cancel": "Cancel", "clear": "Clear sheet", "clearSettings": "Restore defaults", @@ -2267,16 +2296,18 @@ "currentLabel": "Current label", "custom": "Custom", "customImage": "Custom Images", - "customImageHint": "You can add images temporarily – they will NOT be added to the library and are only used for this print sheet.", + "customImageHint": "You can add images temporarily - they will NOT be added to the library and are only used for this print sheet.", "customImageTitle": "Enter a title to be printed", "customImageUpload": "Add temporary image (without Library)", "diameter": "Inner diameter", "empty": "Empty - Please select Tonies or Tags or add custom image first!", "episodes": "Episodes", + "fontFamily": "Label font", + "fontFamilyPlaceholder": "Choose a font", + "imageBottomLeft": "Image margin bottom/left", "imagePosition": "Image position", "imageScale": "Image scale", - "imageBottomLeft": "Image margin bottom/left", - "intro": "Search all Tonies and tags - both official and your custom ones - to create your printable Traveltonies (coin) sheet.", + "intro": "Search all Tonies and tags - both official and your custom ones - to create your printable Traveltonies sheet.", "labelBackgroundColor": "Background", "labelEditTitle": "Edit Label elements", "labelImage": "Label image", @@ -2301,7 +2332,7 @@ "preview": "Preview Label", "printMode": "Print mode", "printModeImageAndText": "Image + Text", - "printModeImageAndTextTooltip": "Print both sides of your Traveltonie coin with the tags image for one side and the title and episode information for the other", + "printModeImageAndTextTooltip": "Print both sides of your Traveltonie with the tags image for one side and the title and episode information for the other", "printModeOnlyImage": "Image", "printModeOnlyImageTooltip": "Print only one side with the tags image", "printModeOnlyText": "Text", @@ -2328,24 +2359,7 @@ "square": "Square", "textFontSize": "Text size", "title": "TeddyStudio", - "trackTitles": "Track titles (one per line)", - "fontFamily": "Label font", - "fontFamilyPlaceholder": "Choose a font", - "bulkAdd": { - "button": "Bulk add tonies", - "modal": { - "title": "Bulk add tonies to print sheet", - "leftTitle": "tonies.json", - "rightTitle": "Queued to add", - "searchPlaceholder": "Search", - "searchPrompt": "Search tonies.json…", - "searchHint": "Type at least 2 characters to search tonies.json.", - "itemUnit": "tonie", - "itemsUnit": "tonies", - "notFound": "No matching tonies", - "confirm": "Add {{count}} to sheet" - } - } + "trackTitles": "Track titles (one per line)" }, "title": "Tonies", "tonies": { diff --git a/public/translations/es.json b/public/translations/es.json index ae8ba676..0438a77b 100644 --- a/public/translations/es.json +++ b/public/translations/es.json @@ -358,7 +358,7 @@ "help": { "actionItems": { "deleteFileFolder": { - "text": "Elimina el archivo o carpeta (solo si la carpeta está vacía). Nota: Si este archivo está asignado a un Tonie/Etiqueta, la referencia no se actualizará.", + "text": "Elimina el archivo o carpeta (solo si la carpeta está vacía). Nota: Si este archivo está asignado a un Tonie/Tag, la referencia no se actualizará.", "title": "Eliminar archivo/carpeta" }, "duplicateTAPFile": { @@ -382,7 +382,7 @@ "title": "Migrar archivo TAF a la raíz de la biblioteca" }, "moveFile": { - "text": "Mueve el archivo a una nueva ubicación. Nota: Si este archivo está asignado a un Tonie/Etiqueta, la referencia no se actualizará.", + "text": "Mueve el archivo a una nueva ubicación. Nota: Si este archivo está asignado a un Tonie/Tag, la referencia no se actualizará.", "title": "Mover archivo" }, "playAudioFile": { @@ -390,21 +390,21 @@ "title": "Reproducir archivo de audio" }, "renameFile": { - "text": "Renombra el archivo. Nota: Si este archivo está asignado a un Tonie/Etiqueta, la referencia no se actualizará automáticamente.", + "text": "Renombra el archivo. Nota: Si este archivo está asignado a un Tonie/Tag, la referencia no se actualizará automáticamente.", "title": "Renombrar archivo" } }, "actionItemsMulti": { "deleteSelectedFiles": { - "text": "Elimina los archivos o carpetas seleccionados (solo si están vacíos). Nota: Las referencias a archivos eliminados en cualquier Tonie/Etiqueta no se actualizarán.", + "text": "Elimina los archivos o carpetas seleccionados (solo si están vacíos). Nota: Las referencias a archivos eliminados en cualquier Tonie/Tag no se actualizarán.", "title": "Eliminar archivos/carpetas seleccionados" }, "encodeFilesToTAF": { - "text": "Agrega archivos compatibles a una cola de codificación. Puedes seleccionar varios archivos para codificarlos en un archivo TAF, que luego puede asignarse a un Tonie/Etiqueta.", + "text": "Agrega archivos compatibles a una cola de codificación. Puedes seleccionar varios archivos para codificarlos en un archivo TAF, que luego puede asignarse a un Tonie/Tag.", "title": "Codificar archivos a TAF" }, "moveSelectedFiles": { - "text": "Mueve los archivos seleccionados a una nueva ubicación. Nota: Las referencias a estos archivos en cualquier Tonie/Etiqueta no se actualizarán. Las carpetas seleccionadas se omitirán.", + "text": "Mueve los archivos seleccionados a una nueva ubicación. Nota: Las referencias a estos archivos en cualquier Tonie/Tag no se actualizarán. Las carpetas seleccionadas se omitirán.", "title": "Mover archivos seleccionados" } }, @@ -430,7 +430,7 @@ "title": "Abrir visor de TAP/JSON" }, "viewTonieInfoModal": { - "text": "Haz clic en la imagen de un Tonie/Etiqueta en la fila para abrir el modal de TonieInfo. Esto muestra información detallada, incluida una imagen más grande y lista de pistas (si está disponible).", + "text": "Haz clic en la imagen de un Tonie/Tag en la fila para abrir el modal de TonieInfo. Esto muestra información detallada, incluida una imagen más grande y lista de pistas (si está disponible).", "title": "Ver modal de TonieInfo" } }, @@ -911,6 +911,7 @@ "no-bv": "Noruego (Isla Bouvet)", "no-no": "Noruego (Noruega)", "no-sj": "Noruego (Svalbard y Jan Mayen)", + "other": "Otro", "pl-pl": "Polaco (Polonia)", "pt-ao": "Portugués (Angola)", "pt-br": "Portugués (Brasil)", @@ -943,7 +944,6 @@ "tl-ph": "Tagalo (Filipinas)", "tr-tr": "Turco (Turquía)", "uk-ua": "Ucraniano (Ucrania)", - "other": "Otro", "undefined": "Otro/no definido", "unknownLanguageCode": "Código de idioma desconocido: ", "ur-pk": "Urdu (Pakistán)", @@ -1350,7 +1350,8 @@ "bootloaderInstalled": "Bootloader instalado con éxito", "certificates": { "alreadyAvailable": "Los certificados ca.der, client.der y private.der deberían estar ya disponibles en:", - "extractAgain": "Si no, asegúrate de haber realizado correctamente el paso 2. También puedes extraer los archivos de la caja nuevamente utilizando la herramienta cc3200 (tu PC debería seguir conectado a la Toniebox a través del puerto UART y el puerto de depuración)." + "extractAgain": "Si no, asegúrate de haber realizado correctamente el paso 2. También puedes extraer los archivos de la caja nuevamente utilizando la herramienta cc3200 (tu PC debería seguir conectado a la Toniebox a través del puerto UART/ESP32-C3 y el puerto de depuración).", + "uploadCertificates": "Sube estos certificados a TeddyCloud usando el siguiente botón." }, "certificatesDumpedCAreplacementFlashed": "Certificados extraídos y reemplazo CA flasheado", "checkBoxes": "Mostrar cajas disponibles", @@ -1359,15 +1360,27 @@ "connectToTonieboxConnectDebugPortText2": " o alternativamente usar cables delgados para la conexión.", "connectToTonieboxConnectTableExplanation": "*El pin SOP2 de la Toniebox debe estar puenteado con el VCC de la Toniebox.", "connectToTonieboxConnectTableIntro": "A continuación, conecta el puerto de depuración de la Toniebox con el UART como se describe en la siguiente tabla:", - "connectToTonieboxIntro": "Necesitas conectar tu UART al puerto de depuración. Empieza localizando el puerto de depuración en la parte inferior de la PCB, en el lado no ensamblado. El diseño del puerto de depuración se muestra en la siguiente imagen.", + "connectToTonieboxIntro": "Necesitas conectar tu UART/ESP32-C3 al puerto de depuración. Empieza localizando el puerto de depuración en la parte inferior de la PCB, en el lado no ensamblado. El diseño del puerto de depuración se muestra en la siguiente imagen.", "connectToTonieboxLink": "Encuentra las instrucciones de conexión aquí", "connectToTonieboxProceed": "¡Ahora estás listo para continuar!", - "connectToTonieboxText": "Asegúrate de que el UART esté configurado a 3.3V antes de conectar el programador UART a tu computadora mediante USB. El LED verde en la placa principal de la Toniebox debe permanecer encendido de forma continua (sin parpadear). Si no lo hace, es posible que las conexiones estén flojas o incorrectas. Desconecta el programador, verifica todas las conexiones y vuelve a intentarlo. Si estás trabajando con cables finos, considera usar pegamento caliente para asegurarlos y evitar que se muevan. Además, asegúrate de que la Toniebox esté alimentada mediante su cargador.", + "connectToTonieboxText": "El LED verde en la placa principal de la Toniebox debe permanecer encendido de forma continua (sin parpadear). Si no lo hace, es posible que las conexiones estén flojas o incorrectas. Desconecta el programador, verifica todas las conexiones y vuelve a intentarlo. Si estás trabajando con cables finos, considera usar pegamento caliente para asegurarlos y evitar que se muevan. Además, asegúrate de que la Toniebox esté alimentada mediante su cargador.", + "connectToTonieboxTextUart": "Asegúrate de que el UART esté configurado a 3.3V antes de conectar el programador UART a tu computadora mediante USB.", "createPatch": "Crear altUrl.custom.305.patch", "customUrlPatch": "Crear un parche de URL personalizado (altUrl.custom.305.json)", "customUrlPatchHint": "Por favor, crea este parche solo si los parches predefinidos altUrl.tc.fritz.box.json y altUrl.305.json no funcionan con tu configuración actual de TeddyCloud. Si satisfacen tus necesidades, puedes continuar haciendo clic en 'Siguiente'.", + "dedicatedUart": "UART dedicado", "dumpCertificates": "Extraer certificados para TeddyCloud", "dumpCertificatesLink": "Encuentra las instrucciones para extraer certificados aquí", + "esp32C3UartGateway": { + "intro": "Puedes usar un ESP32-C3 como gateway UART.", + "new": "Nuevo", + "prepareStep1": "Conecta tu ESP32-C3 a tu PC.", + "prepareStep2": "Abre la siguiente herramienta web de g3gg0, miembro de Team Revvox:", + "prepareStep3": "Abre la pestaña \"Flasher\", haz clic en \"Connect & Flash\" y espera hasta que el proceso termine.", + "prepareStep4": "Abre la pestaña \"Config\", haz clic en \"Connect\" y aplica la siguiente configuración. Luego haz clic en \"Send Config\" y espera nuevamente hasta que termine:", + "prepareStep5": "Ahora conecta tu Toniebox al ESP32-C3 preparado de la siguiente manera:", + "title": "Gateway UART ESP32-C3" + }, "flashCAreplacement": "Flashear reemplazo CA para TeddyCloud", "flashCAreplacementIntro": "Se recomienda flashear el certificado CA de reemplazo en /cert/c2.der y usar el bootloader Hackiebox-NG con el parche altCA.305 (más detalles en los siguientes pasos). Esto te permitirá cambiar fácilmente entre el certificado original y tu certificado de reemplazo.", "flashCAreplacementOutro": "Asegúrate de haber seleccionado la ruta correcta al archivo c2.der.", @@ -1531,7 +1544,8 @@ "confirmDeleteModal": "Confirmar eliminación", "connectESP32Modal": { "beware": "¡Cuidado!", - "connectESP32Text1": "Por favor, conecta el jumper J100 (Boot) y reinicia la caja para ponerla en el modo UART requerido. Conecta tu UART de 3.3V a J103 (Toniebox J103 [TxD | RxD | GND] -> UART: TxD -> RxD, RxD -> TxD, GND -> GND).", + "connectESP32Text1.1": "Por favor, conecta el jumper J100 (Boot) y reinicia la caja para ponerla en el modo UART requerido.", + "connectESP32Text1.2": "Conecta tu UART de 3.3V a J103:", "connectESP32Text2": "Si no estás seguro de en qué modo está iniciando el ESP32, verifica lo siguiente.", "downloadMode": "Modo de descarga", "downloadModeText": "LED apagado, sin sonido, Salida serial:", @@ -1803,22 +1817,35 @@ "seriesRequired": "La serie debe ser completada.", "successfullyCreated": "¡Modelo creado con éxito!", "successfullyCreatedDetails": "¡Modelo {{series}} [{{model}}] creado con éxito!", - "title": "Crear nuevo Tonie/Etiqueta personalizado", + "title": "Crear nuevo Tonie/Tag personalizado", "track": "Pista" }, - "customToniesEditorJsonEntry": "Modelos personalizados", - "imageManager": { - "title": "Biblioteca: Seleccionar y subir imágenes", - "titleSelect": "Seleccionar imagen", - "okText": "Aplicar", - "sourceCustom": "Personalizado", - "sourceOriginal": "Original", - "noOriginalImages": "No se encontraron imágenes originales", - "originalImagesLoading": "Cargando imágenes originales …", - "originalUrlSearchPlaceholder": "Palabras en la URL (deben aparecer todas, el orden no importa) …", - "noOriginalSearchResults": "Ninguna URL coincide con la búsqueda", - "originalUrlColumn": "URL" + "alternativeSource": "Este Tonie {{originalTonie}} tiene asignado el contenido {{assignedContent}}!", + "alternativeSourceUnknown": "¡Este Tonie {{originalTonie}} tiene asignado contenido alternativo!", + "cancel": "Cancelar", + "closeAudioPlayer": "Cerrar reproductor", + "closeAudioPlayerPopover": "¡Cerrar detiene la reproducción!", + "confirmAudioModelMismatchModal": { + "cancel": "Cancelar", + "confirm": "Continuar", + "content": "El audio asignado tiene el modelo \"{{audioModel}}\". La visualización en la caja proviene del audio_id del TAF. El modelo vacío se restablecerá a \"{{audioModel}}\". ¿Continuar?", + "title": "Modelo vs. Audio", + "unknownModel": "desconocido" + }, + "confirmHideModal": { + "cancel": "Cancelar", + "confirmHideDialog": "¿Realmente quieres ocultar el Tonie/Tag \"{{tonieToHide}}\" en la vista general de Tonies? Para volver a hacerlo visible, debes colocar el Tonie/Tag en una de tus Tonieboxes.", + "hide": "Ocultar", + "title": "Ocultar Tonie/Tag" + }, + "connectionToBoxineNotAvailable": "No se puede conectar a Boxine/Toniecloud. Por favor, habilita el acceso para permitir la descarga.", + "content": { + "navigationTitle": "Contenido", + "showToniesOfBoxes": "Contenido de las siguientes Tonieboxes", + "title": "Contenido", + "toniePicture": "Imagen de Tonie" }, + "currentPath": "Ruta actual: ", "customEditor": { "actions": { "browse": "Explorar", @@ -1833,9 +1860,9 @@ }, "audio": { "libraryLabel": "Audio personalizado ({{library}})", - "selectFromLibrary": "Seleccionar audio de {{library}}", "notInIndex": "No en el índice de audio personalizado: {{audioId}}/{{hash}}", - "placeholder": "Seleccionar audio personalizado de {{library}}" + "placeholder": "Seleccionar audio personalizado de {{library}}", + "selectFromLibrary": "Seleccionar audio de {{library}}" }, "batch": { "description": "Los cambios se aplican a todos los modelos seleccionados.", @@ -1849,25 +1876,38 @@ "actions": "Acciones", "image": "Imagen" }, - "errors": { - "emptyResult": "Debe permanecer al menos un modelo.", - "invalidSelectedIndex": "Índice seleccionado no válido", - "releaseNumeric": "Release debe ser numérico", - "duplicateModel": "Duplicado: {{model}}", - "duplicateAudioHash": "Combinación audio_id+hash duplicada: {{audioId}}", - "modelExistsInBase": "El modelo existe en base tonies.json: {{models}}", - "invalidLanguageCode": "Código de idioma no válido. Usa un código como {{example}}." + "deleteConfirm": { + "abort": "Cancelar", + "confirm": "Eliminar", + "deleted": "El modelo \"{{model}}\" ha sido eliminado.", + "description": "¿Realmente eliminar \"{{model}}\"? Esto no se puede deshacer.", + "title": "Eliminar modelo" }, "duplicateTitleSuffix": "Copia", "editModalTitle": "Editar modelo {{current}} de {{total}}", "editModalTitleCreate": "Nuevo modelo", "editModalTitleEdit": "Editar modelo", + "emptyState": "Sin modelos personalizados", + "emptyStateHint": "tonies.custom.json está vacío. Añade tu primer modelo.", + "errors": { + "duplicateAudioHash": "Combinación audio_id+hash duplicada: {{audioId}}", + "duplicateModel": "Duplicado: {{model}}", + "emptyResult": "Debe permanecer al menos un modelo.", + "invalidLanguageCode": "Código de idioma no válido. Usa un código como {{example}}.", + "invalidSelectedIndex": "Índice seleccionado no válido", + "modelExistsInBase": "El modelo existe en base tonies.json: {{models}}", + "releaseNumeric": "Release debe ser numérico" + }, "filterFieldsPlaceholder": "Buscar en campos", "filterPlaceholder": "Filtrar modelos...", + "modelSavedSuccessful": "Modelo {{title}} guardado correctamente", "modelsTitle": "Modelos", "noChangesBody": "No hay nada que guardar en este momento.", "noChangesTitle": "No se detectaron cambios", "optionalColumns": "Columnas opcionales", + "pagination": { + "modelsPerPage": "modelos/página" + }, "picHint": "Seleccionar imagen de custom_img o de tonies originales", "preflight": { "confirm": "Guardar ahora", @@ -1878,9 +1918,16 @@ "upserts": "Actualizaciones/nuevas entradas: {{count}}" }, "previewTitle": "Vista previa de imagen", - "saveSuccessWithCount": "tonies.custom.json guardado ({{count}} entradas). Copia de seguridad y recarga activadas.", + "renameConfirm": { + "abort": "Cancelar", + "confirm": "Confirmar", + "description": "La ID del modelo cambiará de \"{{from}}\" a \"{{to}}\". Los Tonies vinculados se actualizarán automáticamente.", + "skipUpdate": "Quitar asignación de modelo de Tonies vinculados (el audio permanece)", + "title": "Renombrar modelo" + }, + "renameFailed": "Error al renombrar", "savedState": "Todos los cambios guardados", - "modelSavedSuccessful": "Modelo {{title}} guardado correctamente", + "saveSuccessWithCount": "tonies.custom.json guardado ({{count}} entradas). Copia de seguridad y recarga activadas.", "sections": { "audio": "Asignación de audio", "audioAssignment": "Asignación de audio", @@ -1899,74 +1946,35 @@ "unsaved": "{{count}} cambio(s) sin guardar", "validation": { "title": "Por favor corrige estos problemas primero" - }, - "renameFailed": "Error al renombrar", - "renameConfirm": { - "title": "Renombrar modelo", - "description": "La ID del modelo cambiará de \"{{from}}\" a \"{{to}}\". Los Tonies vinculados se actualizarán automáticamente.", - "skipUpdate": "Quitar asignación de modelo de Tonies vinculados (el audio permanece)", - "confirm": "Confirmar", - "abort": "Cancelar" - }, - "deleteConfirm": { - "title": "Eliminar modelo", - "description": "¿Realmente eliminar \"{{model}}\"? Esto no se puede deshacer.", - "deleted": "El modelo \"{{model}}\" ha sido eliminado.", - "confirm": "Eliminar", - "abort": "Cancelar" - }, - "emptyState": "Sin modelos personalizados", - "emptyStateHint": "tonies.custom.json está vacío. Añade tu primer modelo.", - "pagination": { - "modelsPerPage": "modelos/página" } }, + "customImages": { + "navigationTitle": "Imágenes", + "title": "Gestionar imágenes personalizadas" + }, "customModelPicker": { - "title": "Seleccionar modelo personalizado", - "searchPlaceholder": "Buscar modelo, serie...", "addNew": "Añadir nuevo", "loading": "Cargando...", - "noModels": "No se encontraron modelos personalizados" - }, - "alternativeSource": "Este Tonie {{originalTonie}} tiene asignado el contenido {{assignedContent}}!", - "alternativeSourceUnknown": "¡Este Tonie {{originalTonie}} tiene asignado contenido alternativo!", - "cancel": "Cancelar", - "closeAudioPlayer": "Cerrar reproductor", - "closeAudioPlayerPopover": "¡Cerrar detiene la reproducción!", - "confirmHideModal": { - "cancel": "Cancelar", - "confirmHideDialog": "¿Realmente quieres ocultar el Tonie/Etiqueta \"{{tonieToHide}}\" en la vista general de Tonies? Para volver a hacerlo visible, debes colocar el Tonie/Etiqueta en una de tus Tonieboxes.", - "hide": "Ocultar", - "title": "Ocultar Tonie/Etiqueta" - }, - "confirmAudioModelMismatchModal": { - "cancel": "Cancelar", - "confirm": "Continuar", - "title": "Modelo vs. Audio", - "content": "El audio asignado tiene el modelo \"{{audioModel}}\". La visualización en la caja proviene del audio_id del TAF. El modelo vacío se restablecerá a \"{{audioModel}}\". ¿Continuar?", - "unknownModel": "desconocido" - }, - "connectionToBoxineNotAvailable": "No se puede conectar a Boxine/Toniecloud. Por favor, habilita el acceso para permitir la descarga.", - "content": { - "navigationTitle": "Contenido", - "showToniesOfBoxes": "Contenido de las siguientes Tonieboxes", - "title": "Contenido", - "toniePicture": "Imagen de Tonie" + "noModels": "No se encontraron modelos personalizados", + "searchPlaceholder": "Buscar modelo, serie...", + "title": "Seleccionar modelo personalizado" }, - "currentPath": "Ruta actual: ", + "customToniesEditorJsonEntry": "Modelos personalizados", "editModal": { + "audioInfoHeading": "Info de audio", + "createNewModelTooltip": "Crear nuevo modelo", + "editModelTooltip": "Editar modelo", "editSelectedCustomModel": "Continuar editando modelo", "invalidModelError": "El modelo \"{{model}}\" no es válido. Selecciona un modelo existente desde la búsqueda.", + "model": "Modelo", + "modelInfoAudioPath": "Ruta de audio", + "modelInfoCategory": "Categoría", + "modelInfoEpisode": "Episodio", "modelInfoHeading": "Info del modelo", - "audioInfoHeading": "Info de audio", + "modelInfoLanguage": "Idioma", "modelInfoModel": "Modelo", "modelInfoSeries": "Serie", - "modelInfoEpisode": "Episodio", "modelInfoTitle": "Título", - "modelInfoLanguage": "Idioma", - "modelInfoCategory": "Categoría", - "modelInfoAudioPath": "Ruta de audio", - "model": "Modelo", "modelReadOnlyHint": "Tonie original: el modelo no se puede cambiar. Solo puedes asignar audio personalizado.", "placeholderSearchForAModel": "Buscar un modelo", "placeholderSearchForARadioStream": "Buscar una transmisión de radio", @@ -1975,8 +1983,6 @@ "setAudioFromModelUnavailableInLibrary": "El audio del modelo aún no está disponible en la biblioteca.", "setModelFromAudio": "Establecer modelo desde el audio", "source": "Fuente", - "createNewModelTooltip": "Crear nuevo modelo", - "editModelTooltip": "Editar modelo", "title": "Editar Tonie/Coin" }, "encoder": { @@ -2029,12 +2035,12 @@ "editIcon": { "createNewModel": { "description": "Abre una ventana para introducir los detalles del nuevo modelo. Esta función aún está en desarrollo.", - "text": "Crea un modelo personalizado para tu Tonie/Etiqueta.", + "text": "Crea un modelo personalizado para tu Tonie/Tag.", "title": "Crear nuevo modelo (en desarrollo)" }, "model": { "description": "Busca y asigna un modelo. La imagen se mostrará en la vista general.", - "text": "Representa el modelo visual asignado al Tonie/Etiqueta.", + "text": "Representa el modelo visual asignado al Tonie/Tag.", "title": "Modelo" }, "radioStreamSearch": { @@ -2048,7 +2054,7 @@ "title": "Fuente" }, "text": "Abre la ventana de edición con las siguientes opciones:", - "title": "Editar Tonie/Etiqueta" + "title": "Editar Tonie/Tag" }, "infoIcon": { "download": { @@ -2058,19 +2064,19 @@ }, "exists": { "description": "El estado 'existe' se muestra como 'sí' si el archivo fuente asignado está presente.", - "text": "Muestra si la fuente asignada del Tonie/Etiqueta existe en el sistema.", + "text": "Muestra si la fuente asignada del Tonie/Tag existe en el sistema.", "title": "Existe" }, "hide": { - "description": "Usa esta opción para ocultar un Tonie/Etiqueta. Para volver a mostrarlo, colócalo en una de tus Tonieboxes.", - "text": "Elimina el Tonie/Etiqueta de la vista general.", - "title": "Ocultar Tonie/Etiqueta" + "description": "Usa esta opción para ocultar un Tonie/Tag. Para volver a mostrarlo, colócalo en una de tus Tonieboxes.", + "text": "Elimina el Tonie/Tag de la vista general.", + "title": "Ocultar Tonie/Tag" }, - "text": "Abre una ventana con los detalles clave del Tonie/Etiqueta, incluyendo título, imagen, UID, lista de pistas y varios indicadores de estado:", - "title": "Información de Tonie/Etiqueta", + "text": "Abre una ventana con los detalles clave del Tonie/Tag, incluyendo título, imagen, UID, lista de pistas y varios indicadores de estado:", + "title": "Información de Tonie/Tag", "valid": { - "description": "Un Tonie/Etiqueta se considera válido si su fuente asignada es un archivo TAF válido.", - "text": "Muestra si la fuente asignada del Tonie/Etiqueta es válida.", + "description": "Un Tonie/Tag se considera válido si su fuente asignada es un archivo TAF válido.", + "text": "Muestra si la fuente asignada del Tonie/Tag es válida.", "title": "Válido" } }, @@ -2079,19 +2085,19 @@ "title": "Estado en vivo" }, "playIcon": { - "text": "Reproduce el contenido asignado al Tonie/Etiqueta. Si el icono aparece en gris claro, no hay contenido asignado.", + "text": "Reproduce el contenido asignado al Tonie/Tag. Si el icono aparece en gris claro, no hay contenido asignado.", "title": "Reproducir contenido" } }, "blueTopBorder": { - "text": "Indica que este Tonie/Etiqueta fue el último reproducido en al menos una de tus Tonieboxes.", + "text": "Indica que este Tonie/Tag fue el último reproducido en al menos una de tus Tonieboxes.", "title": "Borde superior azul" }, "closeButton": "Cerrar", "modelImage": { "bigImage": { - "description": "Esta imagen refleja el modelo seleccionado asignado al Tonie/Etiqueta. Para cambiarlo, debes asignar otro modelo.", - "text": "Representa el aspecto visual del Tonie/Etiqueta.", + "description": "Esta imagen refleja el modelo seleccionado asignado al Tonie/Tag. Para cambiarlo, debes asignar otro modelo.", + "text": "Representa el aspecto visual del Tonie/Tag.", "title": "Imagen grande" }, "smallImage": { @@ -2099,11 +2105,23 @@ "text": "Muestra la fuente real si es diferente del modelo asignado.", "title": "Imagen pequeña" }, - "text": "Muestra la representación visual del Tonie/Etiqueta si es reconocido. Los Tonies/Etiquetas desconocidos mostrarán un signo de interrogación. Si tienes activada la opción 'Frontend' → 'Split content / model', pueden aparecer dos imágenes:", - "title": "Imagen de modelo del Tonie/Etiqueta" + "text": "Muestra la representación visual del Tonie/Tag si es reconocido. Los Tonies/Etiquetas desconocidos mostrarán un signo de interrogación. Si tienes activada la opción 'Frontend' → 'Split content / model', pueden aparecer dos imágenes:", + "title": "Imagen de modelo del Tonie/Tag" }, "title": "Ayuda" }, + "imageManager": { + "noOriginalImages": "No se encontraron imágenes originales", + "noOriginalSearchResults": "Ninguna URL coincide con la búsqueda", + "okText": "Aplicar", + "originalImagesLoading": "Cargando imágenes originales …", + "originalUrlColumn": "URL", + "originalUrlSearchPlaceholder": "Palabras en la URL (deben aparecer todas, el orden no importa) …", + "sourceCustom": "Personalizado", + "sourceOriginal": "Original", + "title": "Biblioteca: Seleccionar y subir imágenes", + "titleSelect": "Seleccionar imagen" + }, "infoModal": { "download": "Descargar TAF como *.ogg", "exists": "Existe:", @@ -2114,7 +2132,7 @@ "yes": "Sí" }, "informationModal": { - "hideTag": "Ocultar Tonie/Etiqueta", + "hideTag": "Ocultar Tonie/Tag", "ok": "OK", "unknownModel": "Modelo desconocido" }, @@ -2127,10 +2145,6 @@ "navigationTitle": "Biblioteca", "title": "Biblioteca" }, - "customImages": { - "navigationTitle": "Imágenes", - "title": "Gestionar imágenes personalizadas" - }, "libraryAudio": { "navigationTitle": "Audio" }, @@ -2159,10 +2173,10 @@ "filterLoadedDetails": "¡El filtro {{name}} se cargó correctamente!", "filterSaved": "Filtro guardado", "filterSavedDetails": "¡El filtro {{name}} se guardó correctamente!", - "hideTonieFailed": "¡No se pudo ocultar el Tonie/Etiqueta!", - "hideTonieFailedDetails": "No se pudo ocultar Tonie/Etiqueta [{{ruid}}]: ", - "hideTonieSuccessful": "¡Tonie/Etiqueta ocultado con éxito!", - "hideTonieSuccessfulDetails": "Tonie/Etiqueta [{{ruid}}] se ha marcado como oculto con éxito", + "hideTonieFailed": "¡No se pudo ocultar el Tonie/Tag!", + "hideTonieFailedDetails": "No se pudo ocultar Tonie/Tag [{{ruid}}]: ", + "hideTonieSuccessful": "¡Tonie/Tag ocultado con éxito!", + "hideTonieSuccessfulDetails": "Tonie/Tag [{{ruid}}] se ha marcado como oculto con éxito", "liveDisabled": "En vivo deshabilitado.", "liveDisabledDetails": "Indicador en vivo deshabilitado para \"{{model}}\" [{{ruid}}]", "liveEnabled": "En vivo habilitado.", @@ -2249,7 +2263,7 @@ "noTracks": "No hay pistas disponibles", "openInTonieAudioPlayer": "Abrir en Audio Player", "openStandalone": "Abrir el reproductor en modo independiente", - "selectTonieToPlay": "Selecciona un Tonie/etiqueta para reproducir", + "selectTonieToPlay": "Selecciona un Tonie/Tag para reproducir", "shrink": "Minimizar el reproductor de audio", "standaloneTitle": "Reproductor de Audio", "title": "Reproductor de Audio", @@ -2259,6 +2273,21 @@ "adaptLabelsHint": "Puedes ajustar los textos y las imágenes de las etiquetas haciendo clic en el botón de editar de cada grupo de etiquetas.", "addedCustomImageHint": "La imagen personalizada ha sido añadida.", "addedHint": "«{{title}}» ha sido añadido.", + "bulkAdd": { + "button": "Añadir varios tonies", + "modal": { + "confirm": "Añadir {{count}} a la hoja", + "itemsUnit": "tonies", + "itemUnit": "tonie", + "leftTitle": "tonies.json", + "notFound": "Ningún tonie coincide", + "rightTitle": "En cola para añadir", + "searchHint": "Escribe al menos 2 caracteres para buscar en tonies.json.", + "searchPlaceholder": "Buscar", + "searchPrompt": "Buscar en tonies.json…", + "title": "Añadir varios tonies a la hoja de impresión" + } + }, "cancel": "Cancelar", "clear": "Limpiar hoja", "clearSettings": "Restaurar", @@ -2273,9 +2302,11 @@ "diameter": "Diámetro interior", "empty": "Vacío - ¡Primero selecciona Tonies o Tags o añade una imagen propia!", "episodes": "Episodios", + "fontFamily": "Fuente de etiqueta", + "fontFamilyPlaceholder": "Elegir una fuente", + "imageBottomLeft": "Margen inferior/izquierdo de la imagen", "imagePosition": "Posición de la imagen", "imageScale": "Escala de imagen", - "imageBottomLeft": "Margen inferior/izquierdo de la imagen", "intro": "Busca todos los Tonies y etiquetas - tanto los oficiales como los personalizados - para crear tu hoja imprimible de Traveltonies (moneda).", "labelBackgroundColor": "Fondo", "labelEditTitle": "Editar elementos de la etiqueta", @@ -2328,24 +2359,7 @@ "square": "Cuadrada", "textFontSize": "Tamaño de texto", "title": "TeddyStudio", - "trackTitles": "Títulos (uno por línea)", - "fontFamily": "Fuente de etiqueta", - "fontFamilyPlaceholder": "Elegir una fuente", - "bulkAdd": { - "button": "Añadir varios tonies", - "modal": { - "title": "Añadir varios tonies a la hoja de impresión", - "leftTitle": "tonies.json", - "rightTitle": "En cola para añadir", - "searchPlaceholder": "Buscar", - "searchPrompt": "Buscar en tonies.json…", - "searchHint": "Escribe al menos 2 caracteres para buscar en tonies.json.", - "itemUnit": "tonie", - "itemsUnit": "tonies", - "notFound": "Ningún tonie coincide", - "confirm": "Añadir {{count}} a la hoja" - } - } + "trackTitles": "Títulos (uno por línea)" }, "title": "Tonies", "tonies": { diff --git a/public/translations/fr.json b/public/translations/fr.json index 7fdcd777..6b5cd79a 100644 --- a/public/translations/fr.json +++ b/public/translations/fr.json @@ -530,7 +530,7 @@ "uploadedFileFailed": "Échec du téléchargement du fichier!", "uploadFailed": "Certains fichiers n'ont pas pu être téléversés. Veuillez réessayer.", "uploadFailedForFile": "Échec du téléversement de \"{{file}}\".", - "uploadHint": "Cliquez ou glissez les fichiers dans la zone d’envoi / sur la liste de fichiers pour les téléverser", + "uploadHint": "Cliquez ou glissez les fichiers dans la zone d'envoi / sur la liste de fichiers pour les téléverser", "uploading": "Téléversement en cours...", "uploadInProgress": "Téléversement de \"{{file}}\"...", "uploadSuccessful": "Fichiers téléversés avec succès.", @@ -911,6 +911,7 @@ "no-bv": "Norvégien (Île Bouvet)", "no-no": "Norvégien (Norvège)", "no-sj": "Norvégien (Svalbard et Jan Mayen)", + "other": "Autre", "pl-pl": "Polonais (Pologne)", "pt-ao": "Portugais (Angola)", "pt-br": "Portugais (Brésil)", @@ -943,7 +944,6 @@ "tl-ph": "Tagalog (Philippines)", "tr-tr": "Turc (Turquie)", "uk-ua": "Ukrainien (Ukraine)", - "other": "Autre", "undefined": "Autre/indéfini", "unknownLanguageCode": "Code de langue inconnu : ", "ur-pk": "Ourdou (Pakistan)", @@ -1227,7 +1227,7 @@ "proceedToFlash1": "Accédez à la page de flashage pour la version sélectionnée :", "proceedToFlashLinkText": "Flashage de la boîte", "tb2": "Concernant la Toniebox 2", - "tb2Details": "Une seule version de la dernière Toniebox 2 est connue, donc l’identification est simple. Si votre box est une Toniebox 2, vous pouvez aller directement aux instructions de flash pour la Toniebox 2.", + "tb2Details": "Une seule version de la dernière Toniebox 2 est connue, donc l'identification est simple. Si votre box est une Toniebox 2, vous pouvez aller directement aux instructions de flash pour la Toniebox 2.", "tb2text": "Cette version est unique et a TB-MAIN_2.0.P imprimé sur le pcb comme montré dans l'image suivante.", "tiInstruction": "Pour connaître la version spécifique, tu dois ouvrir ta Toniebox et utiliser la puce principale de la carte PCB pour l'identifier.", "title": "Identifie ta version de Toniebox 1", @@ -1350,7 +1350,8 @@ "bootloaderInstalled": "Bootloader installé avec succès", "certificates": { "alreadyAvailable": "Les certificats ca.der, client.der et private.der devraient déjà être disponibles ici :", - "extractAgain": "Si ce n'est pas le cas, assure-toi d'avoir correctement suivi l'étape 2. Tu peux également extraire à nouveau les fichiers de la box en utilisant l'outil cc3200 (ton PC devrait toujours être connecté à la Toniebox via le port UART et le port de débogage)." + "extractAgain": "Si ce n'est pas le cas, assure-toi d'avoir correctement suivi l'étape 2. Tu peux également extraire à nouveau les fichiers de la box en utilisant l'outil cc3200 (ton PC devrait toujours être connecté à la Toniebox via le port UART/ESP32-C3 et le port de débogage).", + "uploadCertificates": "Téléversez ces certificats dans TeddyCloud à l'aide du bouton suivant." }, "certificatesDumpedCAreplacementFlashed": "Certificats extraits et remplacement CA flashé", "checkBoxes": "Afficher les boîtes disponibles", @@ -1359,15 +1360,27 @@ "connectToTonieboxConnectDebugPortText2": " ou, alternativement, des fils fins pour la connexion.", "connectToTonieboxConnectTableExplanation": "*La broche SOP2 de la Toniebox doit être pontée avec le VCC de la Toniebox.", "connectToTonieboxConnectTableIntro": "Ensuite, connecte le port de débogage de la Toniebox au UART comme décrit dans le tableau suivant :", - "connectToTonieboxIntro": "Tu dois connecter ton UART au port de débogage. Commence par localiser le port de débogage sur la face non assemblée de la carte PCB, située en bas. Le schéma du port de débogage est montré dans l'image suivante.", + "connectToTonieboxIntro": "Tu dois connecter ton UART/ESP32-C3 au port de débogage. Commence par localiser le port de débogage sur la face non assemblée de la carte PCB, située en bas. Le schéma du port de débogage est montré dans l'image suivante.", "connectToTonieboxLink": "Trouve les instructions de connexion ici", "connectToTonieboxProceed": "Tu es maintenant prêt à continuer !", - "connectToTonieboxText": "Assure-toi que l'UART est configuré à 3,3V avant de connecter le programmeur UART à ton ordinateur via USB. La LED verte de la carte principale de la Toniebox doit rester allumée en continu (sans clignoter). Si ce n'est pas le cas, tes connexions pourraient être mal fixées ou incorrectes. Déconnecte le programmeur, vérifie toutes les connexions et essaie à nouveau. Si tu travailles avec des fils fins, pense à utiliser de la colle chaude pour les fixer et éviter qu'ils ne bougent. De plus, assure-toi que la Toniebox est alimentée via son chargeur.", + "connectToTonieboxText": "La LED verte de la carte principale de la Toniebox doit rester allumée en continu (sans clignoter). Si ce n'est pas le cas, tes connexions pourraient être mal fixées ou incorrectes. Déconnecte le programmeur, vérifie toutes les connexions et essaie à nouveau. Si tu travailles avec des fils fins, pense à utiliser de la colle chaude pour les fixer et éviter qu'ils ne bougent. De plus, assure-toi que la Toniebox est alimentée via son chargeur.", + "connectToTonieboxTextUart": "Assure-toi que l'UART est configuré à 3,3V avant de connecter le programmeur UART à ton ordinateur via USB.", "createPatch": "Créer altUrl.custom.305.patch", "customUrlPatch": "Créer un patch URL personnalisé (altUrl.custom.305.json)", "customUrlPatchHint": "Veuillez créer ce patch uniquement si les patches prédéfinis altUrl.tc.fritz.box.json et altUrl.305.json ne fonctionnent pas avec votre configuration actuelle de TeddyCloud. S'ils répondent à vos besoins, vous pouvez continuer en cliquant sur 'Suivant'.", + "dedicatedUart": "UART dédié", "dumpCertificates": "Extraire les certificats pour TeddyCloud", "dumpCertificatesLink": "Trouve les instructions pour extraire les certificats ici", + "esp32C3UartGateway": { + "intro": "Vous pouvez utiliser un ESP32-C3 comme passerelle UART.", + "new": "Nouveau", + "prepareStep1": "Connectez votre ESP32-C3 à votre PC.", + "prepareStep2": "Ouvrez l'outil web suivant de g3gg0, membre de Team Revvox :", + "prepareStep3": "Ouvrez l'onglet « Flasher », cliquez sur « Connect & Flash » et attendez la fin du processus.", + "prepareStep4": "Ouvrez l'onglet « Config », cliquez sur « Connect » et appliquez les paramètres suivants. Cliquez ensuite sur « Send Config » et attendez de nouveau la fin du processus :", + "prepareStep5": "Connectez maintenant votre Toniebox à l'ESP32-C3 préparé comme suit :", + "title": "Passerelle UART ESP32-C3" + }, "flashCAreplacement": "Flasher le remplacement CA pour TeddyCloud", "flashCAreplacementIntro": "Il est recommandé de flasher le certificat CA de remplacement dans /cert/c2.der et d'utiliser le bootloader Hackiebox-NG avec le patch altCA.305 (plus de détails dans les étapes suivantes). Cela te permettra de basculer facilement entre le certificat original et ton certificat de remplacement.", "flashCAreplacementOutro": "Assure-toi d'avoir sélectionné le bon chemin vers le fichier c2.der.", @@ -1531,7 +1544,8 @@ "confirmDeleteModal": "Confirmer la suppression", "connectESP32Modal": { "beware": "Attention !", - "connectESP32Text1": "Veuillez connecter le jumper J100 (Boot) et réinitialiser la boîte pour la mettre en mode UART requis. Connectez votre UART de 3,3V à J103 (Toniebox J103 [TxD | RxD | GND] -> UART: TxD -> RxD, RxD -> TxD, GND -> GND)", + "connectESP32Text1.1": "Veuillez connecter le jumper J100 (Boot) et réinitialiser la boîte pour la mettre en mode UART requis.", + "connectESP32Text1.2": "Connectez votre UART de 3,3V à J103:", "connectESP32Text2": "Si vous n'êtes pas sûr du mode dans lequel démarre l'ESP32, vérifiez ce qui suit.", "downloadMode": "Mode de téléchargement", "downloadModeText": "LED éteinte, pas de son, Sortie série :", @@ -1806,19 +1820,32 @@ "title": "Créer un nouveau Tonie/Tag personnalisé", "track": "Piste" }, - "customToniesEditorJsonEntry": "Modèles personnalisés", - "imageManager": { - "title": "Bibliothèque : Sélectionner et télécharger des images", - "titleSelect": "Sélectionner une image", - "okText": "Appliquer", - "sourceCustom": "Personnalisé", - "sourceOriginal": "Original", - "noOriginalImages": "Aucune image originale trouvée", - "originalImagesLoading": "Chargement des images originales …", - "originalUrlSearchPlaceholder": "Mots à trouver dans l’URL (tous requis, ordre libre) …", - "noOriginalSearchResults": "Aucune URL ne correspond à la recherche", - "originalUrlColumn": "URL" + "alternativeSource": "Cette Tonie {{originalTonie}} est assignée à un contenu {{assignedContent}} !", + "alternativeSourceUnknown": "Cette Tonie {{originalTonie}} est assignée à un contenu alternatif !", + "cancel": "Annuler", + "closeAudioPlayer": "Fermer le lecteur audio", + "closeAudioPlayerPopover": "Fermer arrête la lecture!", + "confirmAudioModelMismatchModal": { + "cancel": "Annuler", + "confirm": "Continuer", + "content": "L'audio assigné a le modèle \"{{audioModel}}\". L'affichage sur la boîte provient de l'audio_id du TAF. Le modèle vide sera réinitialisé à \"{{audioModel}}\". Continuer?", + "title": "Modèle vs. Audio", + "unknownModel": "inconnu" }, + "confirmHideModal": { + "cancel": "Annuler", + "confirmHideDialog": "Voulez-vous vraiment masquer le Tonie/Tag \"{{tonieToHide}}\" dans la vue d'ensemble des Tonies? Pour le rendre visible à nouveau, vous devez placer le Tonie/Tag sur l'une de vos Tonieboxes.", + "hide": "Masquer", + "title": "Masquer le Tonie/Tag" + }, + "connectionToBoxineNotAvailable": "Impossible de se connecter à Boxine/Toniecloud. Veuillez activer l'accès pour permettre le téléchargement.", + "content": { + "navigationTitle": "Contenu", + "showToniesOfBoxes": "Contenu des Tonieboxes suivants", + "title": "Contenu", + "toniePicture": "Image du Tonie" + }, + "currentPath": "Chemin actuel : ", "customEditor": { "actions": { "browse": "Parcourir", @@ -1833,9 +1860,9 @@ }, "audio": { "libraryLabel": "Audio personnalisé ({{library}})", - "selectFromLibrary": "Sélectionner l'audio dans {{library}}", "notInIndex": "Pas dans l'index audio personnalisé : {{audioId}}/{{hash}}", - "placeholder": "Sélectionner l'audio personnalisé dans {{library}}" + "placeholder": "Sélectionner l'audio personnalisé dans {{library}}", + "selectFromLibrary": "Sélectionner l'audio dans {{library}}" }, "batch": { "description": "Les modifications s'appliquent à tous les modèles sélectionnés.", @@ -1849,25 +1876,38 @@ "actions": "Actions", "image": "Image" }, - "errors": { - "emptyResult": "Au moins un modèle doit rester.", - "invalidSelectedIndex": "Index sélectionné invalide", - "releaseNumeric": "Release doit être numérique", - "duplicateModel": "Doublon : {{model}}", - "duplicateAudioHash": "Combinaison audio_id+hash en double : {{audioId}}", - "modelExistsInBase": "Le modèle existe dans base tonies.json : {{models}}", - "invalidLanguageCode": "Code de langue invalide. Veuillez utiliser un code comme {{example}}." + "deleteConfirm": { + "abort": "Annuler", + "confirm": "Supprimer", + "deleted": "Le modèle \"{{model}}\" a été supprimé.", + "description": "Voulez-vous vraiment supprimer \"{{model}}\" ? Cette action est irréversible.", + "title": "Supprimer le modèle" }, "duplicateTitleSuffix": "Copie", "editModalTitle": "Modifier le modèle {{current}} sur {{total}}", "editModalTitleCreate": "Nouveau modèle", "editModalTitleEdit": "Modifier le modèle", + "emptyState": "Aucun modèle personnalisé", + "emptyStateHint": "tonies.custom.json est vide. Ajoutez votre premier modèle.", + "errors": { + "duplicateAudioHash": "Combinaison audio_id+hash en double : {{audioId}}", + "duplicateModel": "Doublon : {{model}}", + "emptyResult": "Au moins un modèle doit rester.", + "invalidLanguageCode": "Code de langue invalide. Veuillez utiliser un code comme {{example}}.", + "invalidSelectedIndex": "Index sélectionné invalide", + "modelExistsInBase": "Le modèle existe dans base tonies.json : {{models}}", + "releaseNumeric": "Release doit être numérique" + }, "filterFieldsPlaceholder": "Rechercher dans les champs", "filterPlaceholder": "Filtrer les modèles...", + "modelSavedSuccessful": "Modèle {{title}} enregistré avec succès", "modelsTitle": "Modèles", "noChangesBody": "Il n'y a rien à enregistrer pour le moment.", "noChangesTitle": "Aucune modification détectée", "optionalColumns": "Colonnes optionnelles", + "pagination": { + "modelsPerPage": "modèles/page" + }, "picHint": "Sélectionner une image depuis custom_img ou les tonies originaux", "preflight": { "confirm": "Enregistrer maintenant", @@ -1878,9 +1918,16 @@ "upserts": "Mises à jour/nouvelles entrées : {{count}}" }, "previewTitle": "Aperçu de l'image", - "saveSuccessWithCount": "tonies.custom.json enregistré ({{count}} entrées). Sauvegarde et rechargement déclenchés.", + "renameConfirm": { + "abort": "Annuler", + "confirm": "Confirmer", + "description": "L'ID du modèle passera de \"{{from}}\" à \"{{to}}\". Les Tonies liés seront mis à jour automatiquement.", + "skipUpdate": "Retirer l'assignation du modèle des Tonies liés (l'audio reste)", + "title": "Renommer le modèle" + }, + "renameFailed": "Échec du renommage", "savedState": "Toutes les modifications enregistrées", - "modelSavedSuccessful": "Modèle {{title}} enregistré avec succès", + "saveSuccessWithCount": "tonies.custom.json enregistré ({{count}} entrées). Sauvegarde et rechargement déclenchés.", "sections": { "audio": "Assignation audio", "audioAssignment": "Assignation audio", @@ -1899,74 +1946,35 @@ "unsaved": "{{count}} modification(s) non enregistrée(s)", "validation": { "title": "Veuillez d'abord corriger ces problèmes" - }, - "renameFailed": "Échec du renommage", - "renameConfirm": { - "title": "Renommer le modèle", - "description": "L'ID du modèle passera de \"{{from}}\" à \"{{to}}\". Les Tonies liés seront mis à jour automatiquement.", - "skipUpdate": "Retirer l'assignation du modèle des Tonies liés (l'audio reste)", - "confirm": "Confirmer", - "abort": "Annuler" - }, - "deleteConfirm": { - "title": "Supprimer le modèle", - "description": "Voulez-vous vraiment supprimer \"{{model}}\" ? Cette action est irréversible.", - "deleted": "Le modèle \"{{model}}\" a été supprimé.", - "confirm": "Supprimer", - "abort": "Annuler" - }, - "emptyState": "Aucun modèle personnalisé", - "emptyStateHint": "tonies.custom.json est vide. Ajoutez votre premier modèle.", - "pagination": { - "modelsPerPage": "modèles/page" } }, + "customImages": { + "navigationTitle": "Images", + "title": "Gérer les images personnalisées" + }, "customModelPicker": { - "title": "Sélectionner un modèle personnalisé", - "searchPlaceholder": "Rechercher modèle, série...", "addNew": "Ajouter", "loading": "Chargement...", - "noModels": "Aucun modèle personnalisé trouvé" - }, - "alternativeSource": "Cette Tonie {{originalTonie}} est assignée à un contenu {{assignedContent}} !", - "alternativeSourceUnknown": "Cette Tonie {{originalTonie}} est assignée à un contenu alternatif !", - "cancel": "Annuler", - "closeAudioPlayer": "Fermer le lecteur audio", - "closeAudioPlayerPopover": "Fermer arrête la lecture!", - "confirmHideModal": { - "cancel": "Annuler", - "confirmHideDialog": "Voulez-vous vraiment masquer le Tonie/Tag \"{{tonieToHide}}\" dans la vue d'ensemble des Tonies? Pour le rendre visible à nouveau, vous devez placer le Tonie/Tag sur l'une de vos Tonieboxes.", - "hide": "Masquer", - "title": "Masquer le Tonie/Tag" - }, - "confirmAudioModelMismatchModal": { - "cancel": "Annuler", - "confirm": "Continuer", - "title": "Modèle vs. Audio", - "content": "L'audio assigné a le modèle \"{{audioModel}}\". L'affichage sur la boîte provient de l'audio_id du TAF. Le modèle vide sera réinitialisé à \"{{audioModel}}\". Continuer?", - "unknownModel": "inconnu" - }, - "connectionToBoxineNotAvailable": "Impossible de se connecter à Boxine/Toniecloud. Veuillez activer l'accès pour permettre le téléchargement.", - "content": { - "navigationTitle": "Contenu", - "showToniesOfBoxes": "Contenu des Tonieboxes suivants", - "title": "Contenu", - "toniePicture": "Image du Tonie" + "noModels": "Aucun modèle personnalisé trouvé", + "searchPlaceholder": "Rechercher modèle, série...", + "title": "Sélectionner un modèle personnalisé" }, - "currentPath": "Chemin actuel : ", + "customToniesEditorJsonEntry": "Modèles personnalisés", "editModal": { + "audioInfoHeading": "Infos audio", + "createNewModelTooltip": "Créer un nouveau modèle", + "editModelTooltip": "Modifier le modèle", "editSelectedCustomModel": "Continuer l'édition du modèle", "invalidModelError": "Le modèle \"{{model}}\" est invalide. Veuillez sélectionner un modèle existant dans la recherche.", + "model": "Modèle", + "modelInfoAudioPath": "Chemin audio", + "modelInfoCategory": "Catégorie", + "modelInfoEpisode": "Épisode", "modelInfoHeading": "Infos du modèle", - "audioInfoHeading": "Infos audio", + "modelInfoLanguage": "Langue", "modelInfoModel": "Modèle", "modelInfoSeries": "Série", - "modelInfoEpisode": "Épisode", "modelInfoTitle": "Titre", - "modelInfoLanguage": "Langue", - "modelInfoCategory": "Catégorie", - "modelInfoAudioPath": "Chemin audio", - "model": "Modèle", "modelReadOnlyHint": "Tonie original : le modèle ne peut pas être modifié. Vous pouvez uniquement assigner un audio personnalisé.", "placeholderSearchForAModel": "Rechercher un modèle", "placeholderSearchForARadioStream": "Rechercher un flux radio", @@ -1975,9 +1983,7 @@ "setAudioFromModelUnavailableInLibrary": "L'audio du modèle n'est pas encore disponible dans la bibliothèque.", "setModelFromAudio": "Définir le modèle depuis l'audio", "source": "Source", - "createNewModelTooltip": "Créer un nouveau modèle", - "editModelTooltip": "Modifier le modèle", - "title": "Modifier Tonie/Coin" + "title": "Modifier Tonie/Tag" }, "encoder": { "browserEncodingInProgress": "Encodage des fichiers audio dans le navigateur...", @@ -2029,12 +2035,12 @@ "editIcon": { "createNewModel": { "description": "Ouvre une fenêtre pour saisir les détails du nouveau modèle. Cette fonction est encore en développement.", - "text": "Crée un modèle personnalisé pour ton Tonie/Étiquette.", + "text": "Crée un modèle personnalisé pour ton Tonie/Tag.", "title": "Créer un nouveau modèle (en cours)" }, "model": { "description": "Recherche et assigne un modèle. L'image sera affichée dans l'aperçu.", - "text": "Affiche le modèle visuel assigné au Tonie/Étiquette.", + "text": "Affiche le modèle visuel assigné au Tonie/Tag.", "title": "Modèle" }, "radioStreamSearch": { @@ -2048,7 +2054,7 @@ "title": "Source" }, "text": "Ouvre la fenêtre de modification avec les options suivantes :", - "title": "Modifier Tonie/Étiquette" + "title": "Modifier Tonie/Tag" }, "infoIcon": { "download": { @@ -2058,19 +2064,19 @@ }, "exists": { "description": "Le statut 'existe' est défini sur 'oui' si le fichier source assigné est présent.", - "text": "Indique si la source assignée au Tonie/Étiquette existe dans le système.", + "text": "Indique si la source assignée au Tonie/Tag existe dans le système.", "title": "Existe" }, "hide": { - "description": "Utilise cette option pour masquer un Tonie/Étiquette. Pour le faire réapparaître, place-le sur une de tes Tonieboxes.", - "text": "Retire le Tonie/Étiquette de l'aperçu.", - "title": "Masquer le Tonie/Étiquette" + "description": "Utilise cette option pour masquer un Tonie/Tag. Pour le faire réapparaître, place-le sur une de tes Tonieboxes.", + "text": "Retire le Tonie/Tag de l'aperçu.", + "title": "Masquer le Tonie/Tag" }, - "text": "Ouvre une fenêtre avec les détails principaux du Tonie/Étiquette, y compris le titre, l'image, l'UID, la liste des pistes et divers indicateurs d'état :", - "title": "Informations Tonie/Étiquette", + "text": "Ouvre une fenêtre avec les détails principaux du Tonie/Tag, y compris le titre, l'image, l'UID, la liste des pistes et divers indicateurs d'état :", + "title": "Informations Tonie/Tag", "valid": { - "description": "Un Tonie/Étiquette est considéré comme valide si sa source est un fichier TAF valide.", - "text": "Indique si la source assignée au Tonie/Étiquette est valide.", + "description": "Un Tonie/Tag est considéré comme valide si sa source est un fichier TAF valide.", + "text": "Indique si la source assignée au Tonie/Tag est valide.", "title": "Valide" } }, @@ -2079,19 +2085,19 @@ "title": "Statut en direct" }, "playIcon": { - "text": "Joue le contenu assigné au Tonie/Étiquette. Si l'icône est grise claire, aucun contenu n'est assigné.", + "text": "Joue le contenu assigné au Tonie/Tag. Si l'icône est grise claire, aucun contenu n'est assigné.", "title": "Lire le contenu" } }, "blueTopBorder": { - "text": "Indique que ce Tonie/Étiquette a été le dernier joué sur au moins une de tes Tonieboxes.", + "text": "Indique que ce Tonie/Tag a été le dernier joué sur au moins une de tes Tonieboxes.", "title": "Bord supérieur bleu" }, "closeButton": "Fermer", "modelImage": { "bigImage": { - "description": "Cette image reflète le modèle sélectionné assigné au Tonie/Étiquette. Pour le modifier, tu dois assigner un autre modèle.", - "text": "Représente l'apparence visuelle du Tonie/Étiquette.", + "description": "Cette image reflète le modèle sélectionné assigné au Tonie/Tag. Pour le modifier, tu dois assigner un autre modèle.", + "text": "Représente l'apparence visuelle du Tonie/Tag.", "title": "Grande image" }, "smallImage": { @@ -2099,11 +2105,23 @@ "text": "Indique la source réelle si elle diffère du modèle assigné.", "title": "Petite image" }, - "text": "Affiche la représentation visuelle du Tonie/Étiquette s'il est reconnu. Les Tonies/Étiquettes inconnus affichent un point d'interrogation. Si tu actives le paramètre 'Frontend' → 'Split content / model', deux images peuvent apparaître :", - "title": "Image du modèle Tonie/Étiquette" + "text": "Affiche la représentation visuelle du Tonie/Tag s'il est reconnu. Les Tonies/Étiquettes inconnus affichent un point d'interrogation. Si tu actives le paramètre 'Frontend' → 'Split content / model', deux images peuvent apparaître :", + "title": "Image du modèle Tonie/Tag" }, "title": "Aide" }, + "imageManager": { + "noOriginalImages": "Aucune image originale trouvée", + "noOriginalSearchResults": "Aucune URL ne correspond à la recherche", + "okText": "Appliquer", + "originalImagesLoading": "Chargement des images originales …", + "originalUrlColumn": "URL", + "originalUrlSearchPlaceholder": "Mots à trouver dans l'URL (tous requis, ordre libre) …", + "sourceCustom": "Personnalisé", + "sourceOriginal": "Original", + "title": "Bibliothèque : Sélectionner et télécharger des images", + "titleSelect": "Sélectionner une image" + }, "infoModal": { "download": "Télécharger TAF en *.ogg", "exists": "Existe:", @@ -2127,10 +2145,6 @@ "navigationTitle": "Bibliothèque", "title": "Bibliothèque" }, - "customImages": { - "navigationTitle": "Images", - "title": "Gérer les images personnalisées" - }, "libraryAudio": { "navigationTitle": "Audio" }, @@ -2249,7 +2263,7 @@ "noTracks": "Aucune piste disponible", "openInTonieAudioPlayer": "Ouvrir dans le lecteur audio", "openStandalone": "Ouvrir le lecteur en mode autonome", - "selectTonieToPlay": "Sélectionne un Tonie/étiquette à lire", + "selectTonieToPlay": "Sélectionne un Tonie/Tag à lire", "shrink": "Réduire le lecteur audio", "standaloneTitle": "Lecteur Audio", "title": "Lecteur Audio", @@ -2259,6 +2273,21 @@ "adaptLabelsHint": "Tu peux adapter les textes et images des étiquettes en cliquant sur le bouton d'édition de chaque groupe d'étiquettes.", "addedCustomImageHint": "L'image personnalisée a été ajoutée.", "addedHint": "« {{title}} » a été ajouté.", + "bulkAdd": { + "button": "Ajouter plusieurs tonies", + "modal": { + "confirm": "Ajouter {{count}} à la feuille", + "itemsUnit": "tonies", + "itemUnit": "tonie", + "leftTitle": "tonies.json", + "notFound": "Aucun tonie correspondant", + "rightTitle": "À ajouter", + "searchHint": "Tapez au moins 2 caractères pour rechercher dans tonies.json.", + "searchPlaceholder": "Rechercher", + "searchPrompt": "Rechercher dans tonies.json…", + "title": "Ajouter plusieurs tonies à la feuille d'impression" + } + }, "cancel": "Annuler", "clear": "Effacer la feuille", "clearSettings": "Restaurer", @@ -2273,9 +2302,11 @@ "diameter": "Diamètre intérieur", "empty": "Vide - Sélectionne d'abord des Tonies ou Tags ou ajoute une image personnalisée !", "episodes": "Épisodes", + "fontFamily": "Police de l'étiquette", + "fontFamilyPlaceholder": "Choisir une police", + "imageBottomLeft": "Marge de l'image en bas / à gauche", "imagePosition": "Position de l'image", "imageScale": "Échelle de l'image", - "imageBottomLeft": "Marge de l'image en bas / à gauche", "intro": "Recherche tous les Tonies et Tags, officiels et tes propres, pour créer ta feuille imprimable de Traveltonies (pièce).", "labelBackgroundColor": "Fond", "labelEditTitle": "Modifier les éléments de l'étiquette", @@ -2328,24 +2359,7 @@ "square": "Carrée", "textFontSize": "Taille du texte", "title": "TeddyStudio", - "trackTitles": "Titres (un par ligne)", - "fontFamily": "Police de l'étiquette", - "fontFamilyPlaceholder": "Choisir une police", - "bulkAdd": { - "button": "Ajouter plusieurs tonies", - "modal": { - "title": "Ajouter plusieurs tonies à la feuille d'impression", - "leftTitle": "tonies.json", - "rightTitle": "À ajouter", - "searchPlaceholder": "Rechercher", - "searchPrompt": "Rechercher dans tonies.json…", - "searchHint": "Tapez au moins 2 caractères pour rechercher dans tonies.json.", - "itemUnit": "tonie", - "itemsUnit": "tonies", - "notFound": "Aucun tonie correspondant", - "confirm": "Ajouter {{count}} à la feuille" - } - } + "trackTitles": "Titres (un par ligne)" }, "title": "Tonies", "tonies": { diff --git a/src/assets/boxSetup/cc3200.jpg b/src/assets/boxSetup/cc3200.jpg index 94347eaf..04bed711 100644 Binary files a/src/assets/boxSetup/cc3200.jpg and b/src/assets/boxSetup/cc3200.jpg differ diff --git a/src/assets/boxSetup/cc3235.jpg b/src/assets/boxSetup/cc3235.jpg index c840e256..4c826c9b 100644 Binary files a/src/assets/boxSetup/cc3235.jpg and b/src/assets/boxSetup/cc3235.jpg differ diff --git a/src/assets/boxSetup/esp32.jpg b/src/assets/boxSetup/esp32.jpg index a2efcea9..1f8836d8 100644 Binary files a/src/assets/boxSetup/esp32.jpg and b/src/assets/boxSetup/esp32.jpg differ diff --git a/src/assets/boxSetup/esp32_j100.png b/src/assets/boxSetup/esp32_j100.png index 74d1c927..4b707282 100644 Binary files a/src/assets/boxSetup/esp32_j100.png and b/src/assets/boxSetup/esp32_j100.png differ diff --git a/src/assets/boxSetup/tb-esp32-uart.png b/src/assets/boxSetup/tb-esp32-uart.png index 948e44d6..eca5d376 100644 Binary files a/src/assets/boxSetup/tb-esp32-uart.png and b/src/assets/boxSetup/tb-esp32-uart.png differ diff --git a/src/components/settings/guisettings/GuiLocalStorageSettings.tsx b/src/components/settings/guisettings/GuiLocalStorageSettings.tsx index 3a06ce76..9ca99d6e 100644 --- a/src/components/settings/guisettings/GuiLocalStorageSettings.tsx +++ b/src/components/settings/guisettings/GuiLocalStorageSettings.tsx @@ -4,7 +4,6 @@ import { UploadOutlined } from "@ant-design/icons"; import { useTranslation } from "react-i18next"; import { useGuiLocalSettings } from "./hooks/useGUILocalSettings"; -const { Panel } = Collapse; const { Title, Text } = Typography; export const GuiLocalStorageSettings = () => { diff --git a/src/components/settings/guisettings/hooks/useGUILocalSettings.ts b/src/components/settings/guisettings/hooks/useGUILocalSettings.ts index 050b9d82..75bc5fc6 100644 --- a/src/components/settings/guisettings/hooks/useGUILocalSettings.ts +++ b/src/components/settings/guisettings/hooks/useGUILocalSettings.ts @@ -4,14 +4,66 @@ import { TeddyCloudApi } from "../../../../api"; import { defaultAPIConfig } from "../../../../config/defaultApiConfig"; import { useTeddyCloud } from "../../../../provider/TeddyCloudProvider"; import { NotificationTypeEnum } from "../../../../types/teddyCloudNotificationTypes"; +import { generateUUID } from "../../../../utils/ids/generateUUID"; type LocalSettings = Record; const api = new TeddyCloudApi(defaultAPIConfig()); +const NOTIFICATIONS_STORAGE_KEY = "notifications"; +const MAX_STORED_NOTIFICATIONS = 500; + +const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null && !Array.isArray(value); + +const parseMaybeJson = (value: unknown): unknown => { + if (typeof value !== "string") return value; + + try { + return JSON.parse(value); + } catch { + return value; + } +}; + +const normalizeNotificationDate = (value: unknown): string => { + const parsedDate = + value instanceof Date + ? new Date(value.getTime()) + : typeof value === "string" || typeof value === "number" + ? new Date(value) + : new Date(); + + return Number.isNaN(parsedDate.getTime()) ? new Date().toISOString() : parsedDate.toISOString(); +}; + +const normalizeStoredNotifications = (value: unknown): unknown[] => { + const parsedValue = parseMaybeJson(value); + if (!Array.isArray(parsedValue)) return []; + + const usedUuids = new Set(); + + return parsedValue.slice(0, MAX_STORED_NOTIFICATIONS).map((notification) => { + const raw = isRecord(notification) ? notification : {}; + const rawUuid = typeof raw.uuid === "string" ? raw.uuid : ""; + const uuid = rawUuid && !usedUuids.has(rawUuid) ? rawUuid : generateUUID(); + usedUuids.add(uuid); + + return { + uuid, + date: normalizeNotificationDate(raw.date), + type: typeof raw.type === "string" ? raw.type : NotificationTypeEnum.Info, + title: typeof raw.title === "string" ? raw.title : "", + description: typeof raw.description === "string" ? raw.description : "", + context: typeof raw.context === "string" ? raw.context : "", + flagConfirmed: Boolean(raw.flagConfirmed), + }; + }); +}; + export const useGuiLocalSettings = () => { const { t } = useTranslation(); - const { addNotification } = useTeddyCloud(); + const { addNotification, reloadNotifications } = useTeddyCloud(); const [localSettings, setLocalSettings] = useState({}); @@ -101,17 +153,23 @@ export const useGuiLocalSettings = () => { throw new Error("Invalid Teddycloud JSON file"); } - const data = importedData as Record; + const data = { ...(importedData as Record) }; delete data.teddycloudExport; Object.entries(data).forEach(([key, value]) => { - if (typeof value === "string") { - localStorage.setItem(key, value); + const valueToStore = + key === NOTIFICATIONS_STORAGE_KEY + ? normalizeStoredNotifications(value) + : value; + + if (typeof valueToStore === "string") { + localStorage.setItem(key, valueToStore); } else { - localStorage.setItem(key, JSON.stringify(value)); + localStorage.setItem(key, JSON.stringify(valueToStore)); } }); + reloadNotifications(); loadLocalSettings(); addNotification( @@ -119,6 +177,8 @@ export const useGuiLocalSettings = () => { t("settings.guiSettings.jsonLoaded"), t("settings.guiSettings.jsonLoadedDetails"), t("settings.title"), + true, + false, ); } catch (err) { console.error(err); @@ -130,7 +190,7 @@ export const useGuiLocalSettings = () => { ); } }, - [addNotification, loadLocalSettings, t], + [addNotification, loadLocalSettings, reloadNotifications, t], ); const settingKeys = useMemo(() => Object.keys(localSettings), [localSettings]); diff --git a/src/components/settings/notificationlist/NotificationsList.tsx b/src/components/settings/notificationlist/NotificationsList.tsx index 46641102..59db9120 100644 --- a/src/components/settings/notificationlist/NotificationsList.tsx +++ b/src/components/settings/notificationlist/NotificationsList.tsx @@ -1,4 +1,5 @@ -import React, { useMemo, useState } from "react"; +import React, { useState } from "react"; +import dayjs from "dayjs"; import { Table, Select, @@ -23,11 +24,10 @@ import { NotificationRecord, NotificationType } from "../../../types/teddyCloudN import { useNotificationsList } from "./hooks/useNotificationsList"; import { canHover } from "../../../utils/browser/browserUtils"; -const { Option } = Select; const { Paragraph } = Typography; const { useToken } = theme; -type NotificationRow = NotificationRecord & { _rowId: string }; +type NotificationRow = NotificationRecord; const NotificationsList: React.FC = () => { const { t } = useTranslation(); @@ -64,14 +64,7 @@ const NotificationsList: React.FC = () => { warning: , }; - const tableData: NotificationRow[] = useMemo( - () => - filteredNotifications.map((n, index) => ({ - ...n, - _rowId: `${n.uuid}-${index}`, - })), - [filteredNotifications], - ); + const tableData: NotificationRow[] = filteredNotifications; const columns: ColumnsType = [ { @@ -201,54 +194,58 @@ const NotificationsList: React.FC = () => { > + options={[ + { value: "success", label: t("settings.notifications.success") }, + { value: "info", label: t("settings.notifications.info") }, + { value: "warning", label: t("settings.notifications.warning") }, + { value: "error", label: t("settings.notifications.error") }, + ]} + /> + + options={uniqueContexts.map((context) => ({ + value: context, + label: context, + }))} + /> + + options={[ + { value: "Confirmed", label: t("settings.notifications.confirmed") }, + { + value: "Unconfirmed", + label: t("settings.notifications.unconfirmed"), + }, + ]} + /> { if (dates && dates[0] && dates[1]) { - setDateRange([dates[0].toDate(), dates[1].toDate()]); + setDateRange([ + dates[0].startOf("day").toDate(), + dates[1].endOf("day").toDate(), + ]); } else { setDateRange([null, null]); } }} value={ dateRange[0] && dateRange[1] - ? [dateRange[0] as any, dateRange[1] as any] + ? [dayjs(dateRange[0]), dayjs(dateRange[1])] : undefined } placeholder={[ @@ -303,9 +300,18 @@ const NotificationsList: React.FC = () => { style={{ width: "100%" }} /> - + {selectedRowKeys.length > 0 ? ( - + @@ -329,7 +335,7 @@ const NotificationsList: React.FC = () => { rowSelection={rowSelection} dataSource={tableData} columns={columns} - rowKey="_rowId" + rowKey="uuid" pagination={{ current: currentPage, pageSize, @@ -383,16 +389,24 @@ const NotificationsList: React.FC = () => { {t("settings.notifications.colDate")}: {" "} - {record.date - .toLocaleString("en-US", { - year: "numeric", - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - hour12: false, - }) - .replace(",", "")} + {(() => { + const d = + record.date instanceof Date + ? record.date + : new Date(record.date); + if (Number.isNaN(d.getTime())) return ""; + + return d + .toLocaleString("en-US", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + hour12: false, + }) + .replace(",", ""); + })()} ) : null} diff --git a/src/components/settings/notificationlist/hooks/useNotificationsList.ts b/src/components/settings/notificationlist/hooks/useNotificationsList.ts index 0e276d44..11bdb6d1 100644 --- a/src/components/settings/notificationlist/hooks/useNotificationsList.ts +++ b/src/components/settings/notificationlist/hooks/useNotificationsList.ts @@ -81,8 +81,7 @@ export const useNotificationsList = () => { const confirmSelectedNotifications = () => { selectedRowKeys.forEach((key) => { - const uuid = String(key); - confirmNotification(uuid); + confirmNotification(String(key)); }); setSelectedRowKeys([]); }; diff --git a/src/components/tonieboxes/boxsetup/cc3200/CC3200FlashingGuide.tsx b/src/components/tonieboxes/boxsetup/cc3200/CC3200FlashingGuide.tsx index 2d1874e2..cd0202b5 100644 --- a/src/components/tonieboxes/boxsetup/cc3200/CC3200FlashingGuide.tsx +++ b/src/components/tonieboxes/boxsetup/cc3200/CC3200FlashingGuide.tsx @@ -22,6 +22,8 @@ import { scrollToTop } from "../../../../utils/browser/browserUtils"; const { Paragraph } = Typography; +export type HwTool = "uart" | "esp32c3"; + export const CC3200BoxFlashingGuide: React.FC = () => { const { t } = useTranslation(); const scrollToTopAnchor = useRef(null); @@ -30,6 +32,8 @@ export const CC3200BoxFlashingGuide: React.FC = () => { const [hostname, setHostname] = useState(""); const [warningTextHostname, setWarningTextHostname] = useState(""); + const [hwTool, setHwTool] = useState("uart"); + const [isOpenAvailableBoxesModal, setIsOpenAvailableBoxesModal] = useState(false); useEffect(() => { @@ -99,11 +103,11 @@ export const CC3200BoxFlashingGuide: React.FC = () => { const renderStepContent = () => { switch (currentStep) { case 0: - return ; + return ; case 1: - return ; + return ; case 2: - return ; + return ; case 3: return ( { +interface Step0PreparationsProps { + hwTool: HwTool; + onHwToolChange: (tool: HwTool) => void; +} + +export const Step0Preparations: React.FC = ({ hwTool, onHwToolChange }) => { const { t } = useTranslation(); + const activeKey = hwTool === "uart" ? "uartHW" : "esp32c3HW"; + + const handleTabChange = (newKey: string) => { + if (newKey.startsWith("uart")) { + onHwToolChange("uart"); + } else { + onHwToolChange("esp32c3"); + } + }; + const debugPortUARTData = [ { key: "1", toniebox1: "GND", toniebox2: "", uart: "GND" }, { key: "2", toniebox1: "TX", toniebox2: "", uart: "RX" }, @@ -42,6 +58,128 @@ export const Step0Preparations: React.FC = () => { ); }; + const ESP32C3ConfigSettingsData = [ + { key: "0", setting: "Baud Rate", value: "921600" }, + { key: "1", setting: "TX GPIO Pin", value: "GPIO 20" }, + { key: "2", setting: "RX GPIO Pin", value: "GPIO 21" }, + { key: "3", setting: "Reset GPIO Pin", value: "GPIO 10" }, + { key: "4", setting: "Control GPIO Pin", value: "GPIO 9" }, + { key: "5", setting: "LED GPIO Pin", value: "GPIO 8" }, + ]; + + const ESP32C3ConfigSettingsTable = () => { + const columns = [ + { title: "Setting", dataIndex: "setting", key: "setting" }, + { title: "Value", dataIndex: "value", key: "value" }, + ]; + return ( + + ); + }; + + const debugPortESP32C3UartGatewayData = [ + { key: "1", toniebox1: "GND", toniebox2: "", esp32: "GND" }, + { key: "2", toniebox1: "TX", toniebox2: "", esp32: "GPIO 21 (RX)" }, + { key: "3", toniebox1: "RX", toniebox2: "", esp32: "GPIO 20 (TX)" }, + { key: "4", toniebox1: "RST", toniebox2: "", esp32: "GPIO 10 (DTR)" }, + { key: "5", toniebox1: "VCC", toniebox2: "SOP2*", esp32: "" }, + { key: "6", toniebox1: "SOP2", toniebox2: "VCC*", esp32: "" }, + ]; + + const TonieboxESP32C3UartGatewayTable = () => { + const columns = [ + { title: "Toniebox", dataIndex: "toniebox1", key: "toniebox1" }, + { title: "Toniebox", dataIndex: "toniebox2", key: "toniebox2" }, + { title: "ESP32-C3", dataIndex: "esp32", key: "esp32" }, + ]; + return ( +
+ ); + }; + + const dedicatedUartTab = ( + <> + + + {t("tonieboxes.cc3200BoxFlashing.connectToTonieboxConnectTableIntro")} + + + + + + {t("tonieboxes.cc3200BoxFlashing.connectToTonieboxConnectTableExplanation")} + + + ); + + const esp32C3UartGatewayTab = ( + <> + {t("tonieboxes.cc3200BoxFlashing.esp32C3UartGateway.intro")} +
    +
  • {t("tonieboxes.cc3200BoxFlashing.esp32C3UartGateway.prepareStep1")}
  • +
  • + {t("tonieboxes.cc3200BoxFlashing.esp32C3UartGateway.prepareStep2")}{" "} + + Flashing ESP32-C3 {} + +
  • +
  • {t("tonieboxes.cc3200BoxFlashing.esp32C3UartGateway.prepareStep3")}
  • +
  • + + {t("tonieboxes.cc3200BoxFlashing.esp32C3UartGateway.prepareStep4")} + + + + +
  • + +
  • + + {t("tonieboxes.cc3200BoxFlashing.esp32C3UartGateway.prepareStep5")} + +
  • + + + +
+ + ); + + const uartTabs: TabsProps["items"] = [ + { + key: "uartHW", + label: t("tonieboxes.cc3200BoxFlashing.dedicatedUart"), + children: dedicatedUartTab, + }, + { + key: "esp32c3HW", + label: + t("tonieboxes.cc3200BoxFlashing.esp32C3UartGateway.new") + + "! " + + t("tonieboxes.cc3200BoxFlashing.esp32C3UartGateway.title"), + children: esp32C3UartGatewayTab, + }, + ]; + return ( <>

{t("tonieboxes.boxFlashingCommon.preparations")}

@@ -85,14 +223,12 @@ export const Step0Preparations: React.FC = () => { alt={t("tonieboxes.cc3235BoxFlashing.flashCollapse.cc3235flash")} /> - - - {t("tonieboxes.cc3200BoxFlashing.connectToTonieboxConnectTableIntro")} - - - - {t("tonieboxes.cc3200BoxFlashing.connectToTonieboxConnectTableExplanation")} - + origin - 20, align: "center" }} + /> @@ -139,7 +275,12 @@ export const Step0Preparations: React.FC = () => { - {t("tonieboxes.cc3200BoxFlashing.connectToTonieboxText")} + + {hwTool === "uart" + ? t("tonieboxes.cc3200BoxFlashing.connectToTonieboxTextUart") + " " + : ""} + {t("tonieboxes.cc3200BoxFlashing.connectToTonieboxText")} + {t("tonieboxes.cc3200BoxFlashing.connectToTonieboxProceed")} ); diff --git a/src/components/tonieboxes/boxsetup/cc3200/steps/Step1Bootloader.tsx b/src/components/tonieboxes/boxsetup/cc3200/steps/Step1Bootloader.tsx index f56054be..18d9f392 100644 --- a/src/components/tonieboxes/boxsetup/cc3200/steps/Step1Bootloader.tsx +++ b/src/components/tonieboxes/boxsetup/cc3200/steps/Step1Bootloader.tsx @@ -6,10 +6,15 @@ import { Link } from "react-router-dom"; import cc3200cfwUpdate from "../../../../../assets/boxSetup/cc3200_installCfwFlashUpload.png"; import CodeSnippet from "../../../../common/elements/CodeSnippet"; import { ExportOutlined } from "@ant-design/icons"; +import { HwTool } from "../CC3200FlashingGuide"; const { Paragraph } = Typography; -export const Step1Bootloader: React.FC = () => { +interface Step1PreparationsProps { + hwTool: HwTool; +} + +export const Step1Bootloader: React.FC = ({ hwTool }) => { const { t } = useTranslation(); const importantTBFilesData = [ @@ -104,7 +109,7 @@ export const Step1Bootloader: React.FC = () => { )} @@ -120,7 +125,7 @@ export const Step1Bootloader: React.FC = () => { {t( @@ -227,24 +232,29 @@ export const Step1Bootloader: React.FC = () => { - - {t("tonieboxes.cc3200BoxFlashing.installingBootloader.resetCommand")} - + {hwTool === "uart" && ( + + {t("tonieboxes.cc3200BoxFlashing.installingBootloader.resetCommand")} + + )} {t("tonieboxes.cc3200BoxFlashing.installingBootloader.inCaseText")} - + diff --git a/src/components/tonieboxes/boxsetup/cc3200/steps/Step2Certificates.tsx b/src/components/tonieboxes/boxsetup/cc3200/steps/Step2Certificates.tsx index 8553693b..dde40cc3 100644 --- a/src/components/tonieboxes/boxsetup/cc3200/steps/Step2Certificates.tsx +++ b/src/components/tonieboxes/boxsetup/cc3200/steps/Step2Certificates.tsx @@ -5,10 +5,15 @@ import CodeSnippet from "../../../../common/elements/CodeSnippet"; import { useState } from "react"; import { CertificatesModal } from "../../../common/modals/CertificatesModal"; import { CertificateIntro } from "../../common/elements/CertificateIntro"; +import { HwTool } from "../CC3200FlashingGuide"; const { Paragraph } = Typography; -export const Step2Certificates: React.FC = () => { +interface Step2CertificatesProps { + hwTool: HwTool; +} + +export const Step2Certificates: React.FC = ({ hwTool }) => { const { t } = useTranslation(); const [certModalOpen, setCertModalOpen] = useState(false); @@ -27,13 +32,13 @@ export const Step2Certificates: React.FC = () => { - {t("tonieboxes.cc3200BoxFlashing.certificates.extractAgain")} + {t("tonieboxes.cc3200BoxFlashing.certificates.uploadCertificates")}
+ diff --git a/src/components/tonies/common/modals/selectImage/OriginalImagesPanel.tsx b/src/components/tonies/common/modals/selectImage/OriginalImagesPanel.tsx index 24c42b43..efbb35b4 100644 --- a/src/components/tonies/common/modals/selectImage/OriginalImagesPanel.tsx +++ b/src/components/tonies/common/modals/selectImage/OriginalImagesPanel.tsx @@ -6,12 +6,12 @@ import { useTranslation } from "react-i18next"; import ThumbnailCell from "../../elements/ThumbnailCell"; import { toImageSrc } from "../../utils/imagePathUtils"; import { nextSelectionForMode } from "./selectionUtils"; +import { renderSelectImageSelectionCell } from "../../../../../utils/formatting/renderSelectImageTableLayout"; import { + SELECT_IMAGE_THUMB_COL_WIDTH, SELECT_IMAGE_CELL_GAP_HALF, SELECT_IMAGE_CHECKBOX_COL_WIDTH, - SELECT_IMAGE_THUMB_COL_WIDTH, - renderSelectImageSelectionCell, -} from "../../../../../constants/selectImageTableLayout"; +} from "../../../../../constants/selectImageTableLayoutSizes"; type OriginalTableRow = { url: string }; diff --git a/src/components/tonies/filebrowser/SelectFileFileBrowser.tsx b/src/components/tonies/filebrowser/SelectFileFileBrowser.tsx index 305db04d..3445bc79 100644 --- a/src/components/tonies/filebrowser/SelectFileFileBrowser.tsx +++ b/src/components/tonies/filebrowser/SelectFileFileBrowser.tsx @@ -15,10 +15,8 @@ import { NotificationTypeEnum } from "../../../types/teddyCloudNotificationTypes import { useFileBrowserCore } from "./hooks/useFileBrowserCore"; import { createColumns } from "./helper/Columns"; import { useAudioContext } from "../../../provider/AudioProvider"; -import { - SELECT_IMAGE_CHECKBOX_COL_WIDTH, - renderSelectImageSelectionCell, -} from "../../../constants/selectImageTableLayout"; +import { renderSelectImageSelectionCell } from "../../../utils/formatting/renderSelectImageTableLayout"; +import { SELECT_IMAGE_CHECKBOX_COL_WIDTH } from "../../../constants/selectImageTableLayoutSizes"; const { useToken } = theme; diff --git a/src/components/tonies/filebrowser/helper/Columns.tsx b/src/components/tonies/filebrowser/helper/Columns.tsx index 5e24042f..1e3f76d7 100644 --- a/src/components/tonies/filebrowser/helper/Columns.tsx +++ b/src/components/tonies/filebrowser/helper/Columns.tsx @@ -25,10 +25,9 @@ import { toImageSrc } from "../../common/utils/imagePathUtils"; import ThumbnailCell from "../../common/elements/ThumbnailCell"; import { toModelKey } from "../../utils/modelKey"; import { - SELECT_IMAGE_CELL_GAP, - SELECT_IMAGE_CELL_GAP_HALF, SELECT_IMAGE_THUMB_COL_WIDTH, -} from "../../../../constants/selectImageTableLayout"; + SELECT_IMAGE_CELL_GAP_HALF, +} from "../../../../constants/selectImageTableLayoutSizes"; const { useToken } = theme; diff --git a/src/components/tonies/teddystudio/styles/print.css b/src/components/tonies/teddystudio/styles/print.css index 29091323..9b314238 100644 --- a/src/components/tonies/teddystudio/styles/print.css +++ b/src/components/tonies/teddystudio/styles/print.css @@ -85,9 +85,10 @@ .labelElement .labelContentBleed { position: absolute; - height: 100%; - width: 100%; - justify-content: center; + top: calc(-1 * var(--paper-label-image-bleed, 0mm)); + left: calc(-1 * var(--paper-label-image-bleed, 0mm)); + right: calc(-1 * var(--paper-label-image-bleed, 0mm)); + bottom: calc(-1 * var(--paper-label-image-bleed, 0mm)); z-index: 3; pointer-events: none; overflow: hidden; diff --git a/src/constants/keys.ts b/src/constants/keys.ts new file mode 100644 index 00000000..3d9d47a0 --- /dev/null +++ b/src/constants/keys.ts @@ -0,0 +1 @@ +export const NOTIFICATIONS_STORAGE_KEY = "notifications"; diff --git a/src/constants/numbers.ts b/src/constants/numbers.ts index 00c5b77f..75a0c34d 100644 --- a/src/constants/numbers.ts +++ b/src/constants/numbers.ts @@ -1,4 +1,5 @@ export const MAX_FILES = 99; +export const MAX_STORED_NOTIFICATIONS = 500; export const DEFAULT_UPLOAD_TIMEOUT_MS = 120000; /** Debounce for search/filter inputs (file browser table filter, image manager original URLs). */ diff --git a/src/constants/selectImageTableLayoutSizes.ts b/src/constants/selectImageTableLayoutSizes.ts new file mode 100644 index 00000000..94ae03bd --- /dev/null +++ b/src/constants/selectImageTableLayoutSizes.ts @@ -0,0 +1,12 @@ +/** Width of the thumbnail-only column (matches pre-refactor Columns picture width). */ +export const SELECT_IMAGE_THUMB_COL_WIDTH = 56; + +/** Ant Design selection column width for multi-select in image pickers. */ +export const SELECT_IMAGE_CHECKBOX_COL_WIDTH = 44; + +/** + * Target horizontal gap between checkbox↔thumbnail and thumbnail↔text. + * Adjacent cells use half each so the sum equals this value. + */ +export const SELECT_IMAGE_CELL_GAP = 8; +export const SELECT_IMAGE_CELL_GAP_HALF = SELECT_IMAGE_CELL_GAP / 2; diff --git a/src/hooks/useTeddyCloudVersion.ts b/src/hooks/useTeddyCloudVersion.ts index acb25ce7..cc861cde 100644 --- a/src/hooks/useTeddyCloudVersion.ts +++ b/src/hooks/useTeddyCloudVersion.ts @@ -54,7 +54,9 @@ export const useTeddyCloudVersion = () => { .then((versionInfo) => { if (develop) { const latestDevelopSHA = versionInfo.sha; - setNewVersionAvailable(commitGitSha !== latestDevelopSHA); + setNewVersionAvailable( + !commitGitSha.includes("unknown") && commitGitSha !== latestDevelopSHA, + ); setLatestDevelopSHA(latestDevelopSHA); } else { const tagName = versionInfo.tag_name; diff --git a/src/provider/TeddyCloudProvider.tsx b/src/provider/TeddyCloudProvider.tsx index e842dd6f..7bd4fac0 100644 --- a/src/provider/TeddyCloudProvider.tsx +++ b/src/provider/TeddyCloudProvider.tsx @@ -28,6 +28,73 @@ import { generateUUID } from "../utils/ids/generateUUID"; const api = new TeddyCloudApi(defaultAPIConfig()); +const NOTIFICATIONS_STORAGE_KEY = "notifications"; +const MAX_STORED_NOTIFICATIONS = 500; + +const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null && !Array.isArray(value); + +const normalizeNotificationDate = (value: unknown): Date => { + const parsedDate = + value instanceof Date + ? new Date(value.getTime()) + : typeof value === "string" || typeof value === "number" + ? new Date(value) + : new Date(); + + return Number.isNaN(parsedDate.getTime()) ? new Date() : parsedDate; +}; + +const normalizeStoredNotifications = (value: unknown): NotificationRecord[] => { + if (!Array.isArray(value)) return []; + + const usedUuids = new Set(); + + return value.slice(0, MAX_STORED_NOTIFICATIONS).map((notification) => { + const raw = isRecord(notification) ? notification : {}; + const rawUuid = typeof raw.uuid === "string" ? raw.uuid : ""; + const uuid = rawUuid && !usedUuids.has(rawUuid) ? rawUuid : generateUUID(); + usedUuids.add(uuid); + + return { + uuid, + date: normalizeNotificationDate(raw.date), + type: (typeof raw.type === "string" + ? raw.type + : NotificationTypeEnum.Info) as NotificationType, + title: typeof raw.title === "string" ? raw.title : "", + description: typeof raw.description === "string" ? raw.description : "", + context: typeof raw.context === "string" ? raw.context : "", + flagConfirmed: Boolean(raw.flagConfirmed), + }; + }); +}; + +const persistNotifications = (notificationsToPersist: NotificationRecord[]) => { + if (typeof window === "undefined") return; + + localStorage.setItem( + NOTIFICATIONS_STORAGE_KEY, + JSON.stringify(notificationsToPersist.slice(0, MAX_STORED_NOTIFICATIONS)), + ); +}; + +const readStoredNotifications = (): NotificationRecord[] => { + if (typeof window === "undefined") return []; + + try { + const stored = localStorage.getItem(NOTIFICATIONS_STORAGE_KEY); + if (!stored) return []; + + const parsed = normalizeStoredNotifications(JSON.parse(stored)); + persistNotifications(parsed); + return parsed; + } catch (e) { + console.error("Failed to load notifications", e); + return []; + } +}; + // ===================================== // Helpers // ===================================== @@ -68,6 +135,7 @@ interface TeddyCloudContextType { unconfirmedCount: number; clearAllNotifications: () => void; removeNotifications: (uuid: string[]) => void; + reloadNotifications: () => void; navOpen: boolean; setNavOpen: (show: boolean) => void; @@ -101,6 +169,7 @@ const TeddyCloudContext = createContext({ unconfirmedCount: 0, clearAllNotifications: () => {}, removeNotifications: () => {}, + reloadNotifications: () => {}, navOpen: false, setNavOpen: () => {}, subNavOpen: false, @@ -142,47 +211,19 @@ export function TeddyCloudProvider({ children }: TeddyCloudProviderProps) { const { boxModelImages, loading: boxModelImagesLoading } = useBoxModelImages(); // ===================================== - // Notifications: lokal aus Storage laden + // Notification Handling // ===================================== const loadStoredNotifications = useCallback(() => { - try { - const stored = localStorage.getItem("notifications"); - if (!stored) return; - - const parsed: (Omit & { date: string })[] = - JSON.parse(stored); - const chunk = 200; - let idx = 0; - - const processChunk = () => { - const slice: NotificationRecord[] = parsed.slice(idx, idx + chunk).map((n) => ({ - ...n, - date: new Date(n.date), - })); - - setNotifications((prev) => [...prev, ...slice]); - - idx += chunk; - if (idx < parsed.length) { - scheduleTask(processChunk); - } - }; - - scheduleTask(processChunk); - } catch (e) { - console.error("Failed to load notifications", e); - } + const storedNotifications = readStoredNotifications(); + setNotifications(storedNotifications); + return storedNotifications; }, []); useEffect(() => { - scheduleTask(loadStoredNotifications); + loadStoredNotifications(); }, [loadStoredNotifications]); - // ===================================== - // Notification Handling - // ===================================== - const addNotification = useCallback( ( type: NotificationType, @@ -205,11 +246,8 @@ export function TeddyCloudProvider({ children }: TeddyCloudProviderProps) { }; setNotifications((prev) => { - const updated = [newNotification, ...prev]; - if (updated.length > 500) { - updated.splice(500, updated.length - 500); - } - localStorage.setItem("notifications", JSON.stringify(updated)); + const updated = [newNotification, ...prev].slice(0, MAX_STORED_NOTIFICATIONS); + persistNotifications(updated); return updated; }); } @@ -251,7 +289,7 @@ export function TeddyCloudProvider({ children }: TeddyCloudProviderProps) { const confirmNotification = useCallback((uuid: string) => { setNotifications((prev) => { const updated = prev.map((n) => (n.uuid === uuid ? { ...n, flagConfirmed: true } : n)); - localStorage.setItem("notifications", JSON.stringify(updated)); + persistNotifications(updated); return updated; }); }, []); @@ -259,16 +297,18 @@ export function TeddyCloudProvider({ children }: TeddyCloudProviderProps) { const removeNotifications = useCallback((uuids: string[]) => { setNotifications((prev) => { const updated = prev.filter((n) => !uuids.includes(n.uuid)); - localStorage.setItem("notifications", JSON.stringify(updated)); + persistNotifications(updated); return updated; }); }, []); const clearAllNotifications = useCallback(() => { setNotifications([]); - localStorage.removeItem("notifications"); + localStorage.removeItem(NOTIFICATIONS_STORAGE_KEY); }, []); + const reloadNotifications = loadStoredNotifications; + const unconfirmedCount = useMemo( () => notifications.filter((n) => !n.flagConfirmed).length, [notifications], @@ -398,6 +438,7 @@ export function TeddyCloudProvider({ children }: TeddyCloudProviderProps) { unconfirmedCount, clearAllNotifications, removeNotifications, + reloadNotifications, navOpen, setNavOpen, subNavOpen, @@ -423,6 +464,7 @@ export function TeddyCloudProvider({ children }: TeddyCloudProviderProps) { unconfirmedCount, clearAllNotifications, removeNotifications, + reloadNotifications, navOpen, subNavOpen, currentTCSection, diff --git a/src/constants/selectImageTableLayout.tsx b/src/utils/formatting/renderSelectImageTableLayout.tsx similarity index 56% rename from src/constants/selectImageTableLayout.tsx rename to src/utils/formatting/renderSelectImageTableLayout.tsx index b645d79e..309629b9 100644 --- a/src/constants/selectImageTableLayout.tsx +++ b/src/utils/formatting/renderSelectImageTableLayout.tsx @@ -1,18 +1,5 @@ import React from "react"; - -/** Width of the thumbnail-only column (matches pre-refactor Columns picture width). */ -export const SELECT_IMAGE_THUMB_COL_WIDTH = 56; - -/** Ant Design selection column width for multi-select in image pickers. */ -export const SELECT_IMAGE_CHECKBOX_COL_WIDTH = 44; - -/** - * Target horizontal gap between checkbox↔thumbnail and thumbnail↔text. - * Adjacent cells use half each so the sum equals this value. - */ -export const SELECT_IMAGE_CELL_GAP = 8; - -export const SELECT_IMAGE_CELL_GAP_HALF = SELECT_IMAGE_CELL_GAP / 2; +import { SELECT_IMAGE_CELL_GAP_HALF } from "../../constants/selectImageTableLayoutSizes"; /** Multi-select: selection cell padding + thumb cell padding = SELECT_IMAGE_CELL_GAP. */ export function renderSelectImageSelectionCell(originNode: React.ReactNode): React.ReactNode {