diff --git a/public/translations/de.json b/public/translations/de.json index da413d78..311cbf17 100644 --- a/public/translations/de.json +++ b/public/translations/de.json @@ -34,7 +34,22 @@ "navigationTitle": "Forum" }, "navigationTitle": "Community", - "title": "Community" + "title": "Community", + "translations": { + "navigationTitle": "Übersetzungen", + "title": "Übersetzungen", + "language": "Sprache", + "missingKeys": "Fehlende Schlüssel", + "noMissingKeys": "Keine fehlenden Schlüssel.", + "extraKeys": "Zusätzliche Schlüssel", + "noExtraKeys": "Keine zusätzlichen Schlüssel.", + "missingExtraKeysHeadline": "Fehlende / Zusätzliche Übersetzungsschlüssel", + "noDiscrepancies": "Alles ist in Ordnung.", + "missing": "FEHLT", + "key": "Schlüssel", + "allTranslationStrings": "Alle Übersetzungsstrings", + "hint": "Wenn du eine schlechte Übersetzung siehst, komm bitte über Telegram auf uns zu. Französisch und Spanisch werden derzeit vollständig von ChatGPT übersetzt." + } }, "confirmDialog": { "contentHintTitle": "Denk dran" @@ -271,7 +286,6 @@ "tonieMeeting": { "navigationTitle": "Tonie Meeting" }, - "yourCustomTonies": "Deine Custom Tonies", "yourTonies": "Deine Tonies" }, "inputValidator": { diff --git a/public/translations/en.json b/public/translations/en.json index b7d059f1..a3efe3f0 100644 --- a/public/translations/en.json +++ b/public/translations/en.json @@ -34,7 +34,22 @@ "navigationTitle": "Forum" }, "navigationTitle": "Community", - "title": "Community" + "title": "Community", + "translations": { + "navigationTitle": "Translations", + "title": "Translations", + "language": "Language", + "missingKeys": "Missing keys", + "noMissingKeys": "No missing keys.", + "extraKeys": "Extra keys", + "noExtraKeys": "No extra keys.", + "missingExtraKeysHeadline": "Missing / Extra Translation keys", + "noDiscrepancies": "Everything is fine.", + "missing": "MISSING", + "key": "Key", + "allTranslationStrings": "All Translation strings", + "hint": "If you see any bad translation, please come back to us on Telegram. French and Spanish are currently completely translated by ChatGPT." + } }, "confirmDialog": { "contentHintTitle": "Keep in mind" @@ -351,6 +366,7 @@ "toniesJsonReloadFailed": "Failed to reload Tonies(.custom).json!", "toniesJsonReloadInProgress": "Loading Tonies(.custom).json...", "toniesJsonReloadSuccessful": "Tonies(.custom).json reloaded successfully!", + "warning": "Attention! Save button + Settings level", "warningHint": "Text inputs currently have to be saved explicitly until a better solution is implemented. To do this, click on the save symbol at the end of the field. If you are missing settings, increase the 'Settings level'." }, diff --git a/public/translations/es.json b/public/translations/es.json index f23401fd..b0c8fb95 100644 --- a/public/translations/es.json +++ b/public/translations/es.json @@ -34,7 +34,22 @@ "navigationTitle": "Foro" }, "navigationTitle": "Comunidad", - "title": "Comunidad" + "title": "Comunidad", + "translations": { + "navigationTitle": "Traducciones", + "title": "Traducciones", + "language": "Idioma", + "missingKeys": "Claves faltantes", + "noMissingKeys": "No hay claves faltantes.", + "extraKeys": "Claves adicionales", + "noExtraKeys": "No hay claves adicionales.", + "missingExtraKeysHeadline": "Claves de traducción faltantes / adicionales", + "noDiscrepancies": "Todo está bien.", + "missing": "FALTANTE", + "key": "Clave", + "allTranslationStrings": "Todas las cadenas de traducción", + "hint": "Si ves alguna mala traducción, por favor vuelve a contactarnos por Telegram. El francés y el español están actualmente completamente traducidos por ChatGPT." + } }, "confirmDialog": { "contentHintTitle": "Ten en cuenta" @@ -351,6 +366,7 @@ "toniesJsonReloadFailed": "¡Error al recargar Tonies(.custom).json!", "toniesJsonReloadInProgress": "Cargando Tonies(.custom).json...", "toniesJsonReloadSuccessful": "¡Tonies(.custom).json recargado con éxito!", + "warning": "¡Atención! Botón Guardar + Nivel de configuración", "warningHint": "Actualmente, los campos de texto deben guardarse explícitamente hasta que se implemente una mejor solución. Para hacerlo, haz clic en el símbolo de guardar al final del campo. Si faltan configuraciones, aumenta el 'Nivel de configuración'." }, diff --git a/public/translations/fr.json b/public/translations/fr.json index 4472359d..73c81374 100644 --- a/public/translations/fr.json +++ b/public/translations/fr.json @@ -34,7 +34,22 @@ "navigationTitle": "Forum" }, "navigationTitle": "Communauté", - "title": "Communauté" + "title": "Communauté", + "translations": { + "navigationTitle": "Traductions", + "title": "Traductions", + "language": "Langue", + "missingKeys": "Clés manquantes", + "noMissingKeys": "Aucune clé manquante.", + "extraKeys": "Clés supplémentaires", + "noExtraKeys": "Aucune clé supplémentaire.", + "missingExtraKeysHeadline": "Clés de traduction manquantes / supplémentaires", + "noDiscrepancies": "Tout va bien.", + "missing": "MANQUANT", + "key": "Clé", + "allTranslationStrings": "Toutes les chaînes de traduction", + "hint": "Si tu vois une mauvaise traduction, reviens vers nous sur Telegram. Le français et l'espagnol sont actuellement complètement traduits par ChatGPT." + } }, "confirmDialog": { "contentHintTitle": "Garde à l'esprit" @@ -471,7 +486,6 @@ "readingFlash": "Lecture du flash...", "readyToProceed": ", prêt à continuer.", "retrievingMac": "Récupération de l'adresse MAC...", - "title": "Flashage de la boîte ESP32", "titleESP32FirmwareFlashed": "ESP32 flashé", "titleFlashESP32": "Flasher l'ESP32", "titlePatchFlash": "Patch Flash", @@ -638,10 +652,10 @@ "liveEnabled": "En direct activé!", "setToEmptyValue": "vide", "setTonieToModelFailed": "Impossible de changer le modèle de Tonie!", - "setTonieToModelSuccess": "Le modèle a été changé pour: ", - "setTonieToRadioStreamSuccess": "Le flux radio a été changé pour: ", - "setTracklistFailed": "Impossible de modifier la liste de lecture: ", - "successfulTonieUpdate": "Tonie mis à jour avec succès!" + "setTonieToModelSuccessful": "Le modèle a été changé pour: ", + "setTonieToSourceFailed": "Impossible de changer la source du tonie !", + "setTonieToSourceSuccessful": "Source du tonie définie sur {{selectedSource}} !", + "sourceSetSuccessful": "Source définie sur {{selectedFile}} !" }, "navigationTitle": "Tonies", "selectFileModal": { diff --git a/src/App.tsx b/src/App.tsx index d781a3eb..17b975ce 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -27,12 +27,17 @@ import { ChangelogPage } from "./pages/community/ChangelogPage"; import { useState, useEffect } from "react"; import { ConfigProvider, theme } from "antd"; import { SunOutlined, MoonOutlined, BulbOutlined } from "@ant-design/icons"; -import { ESP32BoxFlashing } from "./pages/tonieboxes/ESP32BoxFlashing"; -import { TonieMeetingPage } from "./pages/home/TonieMeeting"; +import { ESP32BoxFlashingPage } from "./pages/tonieboxes/boxsetup/esp32/ESP32BoxFlashingPage"; +import { TonieMeetingPage } from "./pages/home/TonieMeetingPage"; import { FAQPage } from "./pages/community/FAQPage"; import { FeaturesPage } from "./pages/home/FeaturesPage"; -import { CC3235BoxFlashingPage } from "./pages/tonieboxes/CC3235BoxFlashing"; -import { CC3200BoxFlashingPage } from "./pages/tonieboxes/CC3200BoxFlashing"; +import { CC3235BoxFlashingPage } from "./pages/tonieboxes/boxsetup/cc3235/CC3235BoxFlashingPage"; +import { CC3200BoxFlashingPage } from "./pages/tonieboxes/boxsetup/cc3200/CC3200BoxFlashingPage"; +import { BoxSetupPage } from "./pages/tonieboxes/boxsetup/BoxSetupPage"; +import { IdentifyBoxVersionPage } from "./pages/tonieboxes/boxsetup/IdentifyBoxVersionPage"; +import { ESP32LegacyPage } from "./pages/tonieboxes/boxsetup/esp32/ESP32LegacyPage"; +import { CC3200AltUrlPatchPage } from "./pages/tonieboxes/boxsetup/cc3200/CC3200AltUrlPatchPage"; +import { TranslationsPage } from "./pages/community/TranslationsPage"; function detectColorScheme() { const prefersDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches; @@ -124,9 +129,28 @@ function App() { } /> } /> } /> - } /> - } /> - } /> + } /> + } + /> + } + /> + } /> + } + /> + } + /> + } + /> } /> } /> } /> @@ -137,6 +161,7 @@ function App() { path="/community/contribution/tonies-json" element={} /> + } /> } /> } /> } /> diff --git a/src/components/community/CommunitySubNav.tsx b/src/components/community/CommunitySubNav.tsx index 86a33204..c677d295 100644 --- a/src/components/community/CommunitySubNav.tsx +++ b/src/components/community/CommunitySubNav.tsx @@ -7,14 +7,44 @@ import { BranchesOutlined, FileTextOutlined, QuestionCircleOutlined, + GlobalOutlined, } from "@ant-design/icons"; -import React from "react"; +import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import { StyledSubMenu } from "../StyledComponents"; export const CommunitySubNav = () => { const { t } = useTranslation(); + const [openKeys, setOpenKeys] = useState([]); + + // Function to add new open key without removing existing ones + const updateOpenKeys = (pathname: string) => { + const newKeys: string[] = []; + if (pathname.includes("/contribution")) { + newKeys.push("contribution"); + } + setOpenKeys((prevKeys) => Array.from(new Set([...prevKeys, ...newKeys]))); + }; + + // Update open keys and selected keys when location changes + useEffect(() => { + updateOpenKeys(location.pathname); + }, [location.pathname]); + + const onOpenChange = (keys: string[]) => { + // Check which keys are currently opened or closed + const latestOpenKey = keys.find((key) => !openKeys.includes(key)); // New key being opened + const latestCloseKey = openKeys.find((key) => !keys.includes(key)); // Key being closed + + if (latestOpenKey) { + // Opening new key, merge it with previously open keys + setOpenKeys((prevKeys) => [...prevKeys, latestOpenKey]); + } else if (latestCloseKey) { + // Closing a key, filter it out from the open keys + setOpenKeys((prevKeys) => prevKeys.filter((key) => key !== latestCloseKey)); + } + }; const subnav: MenuProps["items"] = [ { @@ -49,6 +79,16 @@ export const CommunitySubNav = () => { icon: React.createElement(FileTextOutlined), title: t("community.contribution.toniesJson.navigationTitle"), }, + { + key: "translation", + label: ( + + {t("community.translations.navigationTitle")} + + ), + icon: React.createElement(GlobalOutlined), + title: t("community.translations.navigationTitle"), + }, ], }, { @@ -75,5 +115,14 @@ export const CommunitySubNav = () => { }, ]; - return ; + return ( + + ); }; diff --git a/src/components/settings/SettingsSubNav.tsx b/src/components/settings/SettingsSubNav.tsx index c3c89d88..8796899f 100644 --- a/src/components/settings/SettingsSubNav.tsx +++ b/src/components/settings/SettingsSubNav.tsx @@ -7,7 +7,7 @@ import { SyncOutlined, HistoryOutlined, } from "@ant-design/icons"; -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import { StyledSubMenu } from "../StyledComponents"; diff --git a/src/components/tonieboxes/TonieboxesSubNav.tsx b/src/components/tonieboxes/TonieboxesSubNav.tsx index 96405a5e..fa8a6d44 100644 --- a/src/components/tonieboxes/TonieboxesSubNav.tsx +++ b/src/components/tonieboxes/TonieboxesSubNav.tsx @@ -1,13 +1,48 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { Link } from "react-router-dom"; +import { Link, useLocation } from "react-router-dom"; import { MenuProps } from "antd"; import { StyledSubMenu } from "../StyledComponents"; import { TonieboxIcon } from "../../utils/tonieboxIcon"; -import { DeliveredProcedureOutlined } from "@ant-design/icons"; +import { DeliveredProcedureOutlined, SearchOutlined } from "@ant-design/icons"; export const TonieboxesSubNav = () => { const { t } = useTranslation(); + const location = useLocation(); + + const [openKeys, setOpenKeys] = useState([]); + + const updateOpenKeys = (pathname: string) => { + const newKeys: string[] = []; + + if (pathname.includes("/tonieboxes/boxsetup")) { + newKeys.push("boxsetup"); + if (pathname.includes("/tonieboxes/boxsetup/esp32")) { + newKeys.push("esp32"); + } else if (pathname.includes("/tonieboxes/boxsetup/cc3200")) { + newKeys.push("cc3200"); + } else if (pathname.includes("/tonieboxes/boxsetup/cc3235")) { + newKeys.push("cc3235"); + } + } + + setOpenKeys((prevKeys) => Array.from(new Set([...prevKeys, ...newKeys]))); + }; + + useEffect(() => { + updateOpenKeys(location.pathname); + }, [location.pathname]); + + const onOpenChange = (keys: string[]) => { + const latestOpenKey = keys.find((key) => !openKeys.includes(key)); // New key being opened + const latestCloseKey = openKeys.find((key) => !keys.includes(key)); // Key being closed + + if (latestOpenKey) { + setOpenKeys((prevKeys) => [...prevKeys, latestOpenKey]); + } else if (latestCloseKey) { + setOpenKeys((prevKeys) => prevKeys.filter((key) => key !== latestCloseKey)); + } + }; const subnav: MenuProps["items"] = [ { @@ -20,25 +55,122 @@ export const TonieboxesSubNav = () => { icon: React.createElement(TonieboxIcon), title: t("tonieboxes.navigationTitle"), }, + /* + { + key: "boxsetup", + label: ( + + {t("tonieboxes.boxsetup.navigationTitle")} + + ), + icon: React.createElement(DeliveredProcedureOutlined), + title: t("tonieboxes.boxsetup.navigationTitle"), + children: [ + { + key: "identifyboxversion", + label: ( + + {t("tonieboxes.boxsetup.identifyBoxVersion.navigationTitle")} + + ), + icon: React.createElement(SearchOutlined), + title: t("tonieboxes.boxsetup.identifyBoxVersion.navigationTitle"), + }, + { + key: "esp32", + label: ( + + {t("tonieboxes.boxsetup.esp32.boxflashing.navigationTitle")} + + ), + icon: React.createElement(DeliveredProcedureOutlined), + title: t("tonieboxes.boxsetup.esp32.boxflashing.navigationTitle"), + children: [ + { + key: "esp32legacy", + label: ( + + {t("tonieboxes.boxsetup.esp32.legacy.navigationTitle")} + + ), + icon: React.createElement(SearchOutlined), + title: t("tonieboxes.boxsetup.esp32.legacy.navigationTitle"), + }, + ], + }, + { + key: "cc3200", + label: ( + + {t("tonieboxes.boxsetup.cc3200.boxflashing.navigationTitle")} + + ), + icon: React.createElement(DeliveredProcedureOutlined), + title: t("tonieboxes.boxsetup.cc3200.boxflashing.navigationTitle"), + children: [ + { + key: "cc3200altUrlPatch", + label: ( + + {t("tonieboxes.boxsetup.cc3200.alturlpatch.navigationTitle")} + + ), + icon: React.createElement(SearchOutlined), + title: t("tonieboxes.boxsetup.cc3200.alturlpatch.navigationTitle"), + }, + ], + }, + { + key: "cc3235", + label: ( + + {t("tonieboxes.boxsetup.cc3235.boxflashing.navigationTitle")} + + ), + icon: React.createElement(DeliveredProcedureOutlined), + title: t("tonieboxes.boxsetup.cc3235.boxflashing.navigationTitle"), + }, + ], + }, + */ { key: "esp32boxflashing", - label: {t("tonieboxes.esp32BoxFlashing.navigationTitle")}, + label: ( + {t("tonieboxes.esp32BoxFlashing.navigationTitle")} + ), icon: React.createElement(DeliveredProcedureOutlined), title: t("tonieboxes.esp32BoxFlashing.navigationTitle"), }, { key: "cc3200boxflashing", - label: {t("tonieboxes.cc3200BoxFlashing.navigationTitle")}, + label: ( + + {t("tonieboxes.cc3200BoxFlashing.navigationTitle")} + + ), icon: React.createElement(DeliveredProcedureOutlined), title: t("tonieboxes.cc3200BoxFlashing.navigationTitle"), }, { key: "cc3235boxflashing", - label: {t("tonieboxes.cc3235BoxFlashing.navigationTitle")}, + label: ( + + {t("tonieboxes.cc3235BoxFlashing.navigationTitle")} + + ), icon: React.createElement(DeliveredProcedureOutlined), title: t("tonieboxes.cc3235BoxFlashing.navigationTitle"), }, ]; - return ; + return ( + + ); }; diff --git a/src/pages/community/TranslationsPage.tsx b/src/pages/community/TranslationsPage.tsx new file mode 100644 index 00000000..79592989 --- /dev/null +++ b/src/pages/community/TranslationsPage.tsx @@ -0,0 +1,44 @@ +import { useTranslation } from "react-i18next"; +import { Typography } from "antd"; + +import BreadcrumbWrapper, { + HiddenDesktop, + StyledContent, + StyledLayout, + StyledSider, +} from "../../components/StyledComponents"; +import TranslationComparison from "../../utils/translationComparison"; +import TranslationTable from "../../utils/translationTable"; +import { CommunitySubNav } from "../../components/community/CommunitySubNav"; + +const { Paragraph } = Typography; + +export const TranslationsPage = () => { + const { t } = useTranslation(); + + return ( + <> + + + + + + + + + +

{t("community.translations.title")}

+ {t("community.translations.hint")} + + +
+
+ + ); +}; diff --git a/src/pages/home/TonieMeeting.tsx b/src/pages/home/TonieMeetingPage.tsx similarity index 100% rename from src/pages/home/TonieMeeting.tsx rename to src/pages/home/TonieMeetingPage.tsx diff --git a/src/pages/tonieboxes/boxsetup/BoxSetupPage.tsx b/src/pages/tonieboxes/boxsetup/BoxSetupPage.tsx new file mode 100644 index 00000000..8923ab82 --- /dev/null +++ b/src/pages/tonieboxes/boxsetup/BoxSetupPage.tsx @@ -0,0 +1,40 @@ +import { useTranslation } from "react-i18next"; +import { Alert, Typography } from "antd"; + +import BreadcrumbWrapper, { + HiddenDesktop, + StyledContent, + StyledLayout, + StyledSider, +} from "../../../components/StyledComponents"; +import { TonieboxesSubNav } from "../../../components/tonieboxes/TonieboxesSubNav"; +import { Link } from "react-router-dom"; + +const { Paragraph } = Typography; + +export const BoxSetupPage = () => { + const { t } = useTranslation(); + + return ( + <> + + + + + + + + + +

{t(`tonieboxes.BoxFlashing.title`)}

+
+
+ + ); +}; diff --git a/src/pages/tonieboxes/boxsetup/IdentifyBoxVersionPage.tsx b/src/pages/tonieboxes/boxsetup/IdentifyBoxVersionPage.tsx new file mode 100644 index 00000000..ce686985 --- /dev/null +++ b/src/pages/tonieboxes/boxsetup/IdentifyBoxVersionPage.tsx @@ -0,0 +1,40 @@ +import { useTranslation } from "react-i18next"; +import { Alert, Typography } from "antd"; + +import BreadcrumbWrapper, { + HiddenDesktop, + StyledContent, + StyledLayout, + StyledSider, +} from "../../../components/StyledComponents"; +import { TonieboxesSubNav } from "../../../components/tonieboxes/TonieboxesSubNav"; +import { Link } from "react-router-dom"; + +const { Paragraph } = Typography; + +export const IdentifyBoxVersionPage = () => { + const { t } = useTranslation(); + + return ( + <> + + + + + + + + + +

{t(`tonieboxes.BoxFlashing.title`)}

+
+
+ + ); +}; diff --git a/src/pages/tonieboxes/boxsetup/cc3200/CC3200AltUrlPatchPage.tsx b/src/pages/tonieboxes/boxsetup/cc3200/CC3200AltUrlPatchPage.tsx new file mode 100644 index 00000000..2907d87e --- /dev/null +++ b/src/pages/tonieboxes/boxsetup/cc3200/CC3200AltUrlPatchPage.tsx @@ -0,0 +1,71 @@ +import { useTranslation } from "react-i18next"; +import { Alert, Typography } from "antd"; + +import BreadcrumbWrapper, { + HiddenDesktop, + StyledContent, + StyledLayout, + StyledSider, +} from "../../../../components/StyledComponents"; +import { TonieboxesSubNav } from "../../../../components/tonieboxes/TonieboxesSubNav"; +import { Link } from "react-router-dom"; + +const { Paragraph } = Typography; + +export const CC3200AltUrlPatchPage = () => { + const { t } = useTranslation(); + + return ( + <> + + + + + + + + + +

{t(`tonieboxes.cc3200BoxFlashing.title`)}

+ + + {t(`tonieboxes.cc3200BoxFlashing.hint`)} +
    +
  • + + {t(`tonieboxes.cc3200BoxFlashing.linkGeneral`)} + +
  • +
+
+ + + {t(`tonieboxes.cc3200BoxFlashing.hint2`)} +
    +
  • + + {t(`tonieboxes.cc3200BoxFlashing.linkSpecific`)} + +
  • +
+
+
+
+ + ); +}; diff --git a/src/pages/tonieboxes/CC3200BoxFlashing.tsx b/src/pages/tonieboxes/boxsetup/cc3200/CC3200BoxFlashingPage.tsx similarity index 94% rename from src/pages/tonieboxes/CC3200BoxFlashing.tsx rename to src/pages/tonieboxes/boxsetup/cc3200/CC3200BoxFlashingPage.tsx index 87ad41f0..8e588e44 100644 --- a/src/pages/tonieboxes/CC3200BoxFlashing.tsx +++ b/src/pages/tonieboxes/boxsetup/cc3200/CC3200BoxFlashingPage.tsx @@ -6,8 +6,8 @@ import BreadcrumbWrapper, { StyledContent, StyledLayout, StyledSider, -} from "../../components/StyledComponents"; -import { TonieboxesSubNav } from "../../components/tonieboxes/TonieboxesSubNav"; +} from "../../../../components/StyledComponents"; +import { TonieboxesSubNav } from "../../../../components/tonieboxes/TonieboxesSubNav"; import { Link } from "react-router-dom"; const { Paragraph } = Typography; diff --git a/src/pages/tonieboxes/CC3235BoxFlashing.tsx b/src/pages/tonieboxes/boxsetup/cc3235/CC3235BoxFlashingPage.tsx similarity index 94% rename from src/pages/tonieboxes/CC3235BoxFlashing.tsx rename to src/pages/tonieboxes/boxsetup/cc3235/CC3235BoxFlashingPage.tsx index b645b346..0b4942c5 100644 --- a/src/pages/tonieboxes/CC3235BoxFlashing.tsx +++ b/src/pages/tonieboxes/boxsetup/cc3235/CC3235BoxFlashingPage.tsx @@ -6,8 +6,8 @@ import BreadcrumbWrapper, { StyledContent, StyledLayout, StyledSider, -} from "../../components/StyledComponents"; -import { TonieboxesSubNav } from "../../components/tonieboxes/TonieboxesSubNav"; +} from "../../../../components/StyledComponents"; +import { TonieboxesSubNav } from "../../../../components/tonieboxes/TonieboxesSubNav"; import { Link } from "react-router-dom"; const { Paragraph } = Typography; diff --git a/src/pages/tonieboxes/ESP32BoxFlashing.tsx b/src/pages/tonieboxes/boxsetup/esp32/ESP32BoxFlashingPage.tsx similarity index 99% rename from src/pages/tonieboxes/ESP32BoxFlashing.tsx rename to src/pages/tonieboxes/boxsetup/esp32/ESP32BoxFlashingPage.tsx index 84270418..ae63207a 100644 --- a/src/pages/tonieboxes/ESP32BoxFlashing.tsx +++ b/src/pages/tonieboxes/boxsetup/esp32/ESP32BoxFlashingPage.tsx @@ -1,20 +1,20 @@ import { useEffect, useState, useRef } from "react"; import { JSX } from "react/jsx-runtime"; import { ESPLoader, Transport } from "esptool-js"; -import i18n from "../../i18n"; +import i18n from "../../../../i18n"; import { useTranslation } from "react-i18next"; import { Alert, Button, Col, Collapse, Divider, Form, Input, message, Progress, Row, Steps, Typography } from "antd"; -import { TeddyCloudApi } from "../../api"; -import { defaultAPIConfig } from "../../config/defaultApiConfig"; +import { TeddyCloudApi } from "../../../../api"; +import { defaultAPIConfig } from "../../../../config/defaultApiConfig"; import BreadcrumbWrapper, { HiddenDesktop, StyledContent, StyledLayout, StyledSider, -} from "../../components/StyledComponents"; -import { TonieboxesSubNav } from "../../components/tonieboxes/TonieboxesSubNav"; -import ConfirmationDialog from "../../components/utils/ConfirmationDialog"; -import DotAnimation from "../../utils/dotAnimation"; +} from "../../../../components/StyledComponents"; +import { TonieboxesSubNav } from "../../../../components/tonieboxes/TonieboxesSubNav"; +import ConfirmationDialog from "../../../../components/utils/ConfirmationDialog"; +import DotAnimation from "../../../../utils/dotAnimation"; import { CodeOutlined, DownloadOutlined, @@ -25,7 +25,7 @@ import { SyncOutlined, UploadOutlined, } from "@ant-design/icons"; -import { isWebSerialSupported } from "../../utils/checkWebSerialSupport"; +import { isWebSerialSupported } from "../../../../utils/checkWebSerialSupport"; import { Link } from "react-router-dom"; const api = new TeddyCloudApi(defaultAPIConfig()); @@ -64,7 +64,7 @@ interface ESP32Flasher { const { Paragraph, Text } = Typography; const { Step } = Steps; -export const ESP32BoxFlashing = () => { +export const ESP32BoxFlashingPage = () => { const { t } = useTranslation(); const currentLanguage = i18n.language; diff --git a/src/pages/tonieboxes/boxsetup/esp32/ESP32LegacyPage.tsx b/src/pages/tonieboxes/boxsetup/esp32/ESP32LegacyPage.tsx new file mode 100644 index 00000000..341c9052 --- /dev/null +++ b/src/pages/tonieboxes/boxsetup/esp32/ESP32LegacyPage.tsx @@ -0,0 +1,40 @@ +import { useTranslation } from "react-i18next"; +import { Alert, Typography } from "antd"; + +import BreadcrumbWrapper, { + HiddenDesktop, + StyledContent, + StyledLayout, + StyledSider, +} from "../../../../components/StyledComponents"; +import { TonieboxesSubNav } from "../../../../components/tonieboxes/TonieboxesSubNav"; +import { Link } from "react-router-dom"; + +const { Paragraph } = Typography; + +export const ESP32LegacyPage = () => { + const { t } = useTranslation(); + + return ( + <> + + + + + + + + + +

{t(`tonieboxes.BoxFlashing.title`)}

+
+
+ + ); +}; diff --git a/src/utils/translationComparison.tsx b/src/utils/translationComparison.tsx new file mode 100644 index 00000000..071d1917 --- /dev/null +++ b/src/utils/translationComparison.tsx @@ -0,0 +1,193 @@ +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; + +interface TranslationEntry { + question: string; + answer: string; +} + +interface Translations { + [key: string]: string | Translations | TranslationEntry[]; // Updated to allow for TranslationEntry arrays +} + +const findMissingKeys = (baseObj: Translations, otherObj: Translations, parentKey = ""): string[] => { + const missingKeys: string[] = []; + + Object.keys(baseObj).forEach((key) => { + const fullKey = parentKey ? `${parentKey}.${key}` : key; + + if (Array.isArray(baseObj[key])) { + // Compare each item in the array + (baseObj[key] as TranslationEntry[]).forEach((item, index) => { + const questionKey = `${fullKey}[${index}].question`; + const answerKey = `${fullKey}[${index}].answer`; + + if (!otherObj[key] || !Array.isArray(otherObj[key]) || !otherObj[key][index]) { + missingKeys.push(questionKey, answerKey); + } + }); + } else if (typeof baseObj[key] === "object" && baseObj[key] !== null) { + if (!(key in otherObj) || typeof otherObj[key] !== "object") { + missingKeys.push(...collectAllKeys(baseObj[key] as Translations, fullKey)); + } else { + missingKeys.push( + ...findMissingKeys(baseObj[key] as Translations, otherObj[key] as Translations, fullKey) + ); + } + } else if (!(key in otherObj)) { + missingKeys.push(fullKey); + } + }); + + return missingKeys; +}; + +const collectAllKeys = (obj: Translations, parentKey = ""): string[] => { + const keys: string[] = []; + + Object.keys(obj).forEach((key) => { + const fullKey = parentKey ? `${parentKey}.${key}` : key; + + if (Array.isArray(obj[key])) { + // Handle array of TranslationEntry + (obj[key] as TranslationEntry[]).forEach((item, index) => { + keys.push(`${fullKey}[${index}].question`, `${fullKey}[${index}].answer`); + }); + } else if (typeof obj[key] === "object" && obj[key] !== null) { + keys.push(...collectAllKeys(obj[key] as Translations, fullKey)); + } else { + keys.push(fullKey); + } + }); + + return keys; +}; + +const findExtraKeys = (baseObj: Translations, otherObj: Translations, parentKey = ""): string[] => { + const extraKeys: string[] = []; + + Object.keys(otherObj).forEach((key) => { + const fullKey = parentKey ? `${parentKey}.${key}` : key; + + if (Array.isArray(otherObj[key])) { + // Check for extra items in the array + (otherObj[key] as TranslationEntry[]).forEach((_, index) => { + const questionKey = `${fullKey}[${index}].question`; + const answerKey = `${fullKey}[${index}].answer`; + + if (!baseObj[key] || !Array.isArray(baseObj[key]) || !baseObj[key][index]) { + extraKeys.push(questionKey, answerKey); + } + }); + } else if (typeof otherObj[key] === "object" && otherObj[key] !== null) { + if (!baseObj[key] || typeof baseObj[key] !== "object") { + extraKeys.push(fullKey); + } else { + extraKeys.push(...findExtraKeys(baseObj[key] as Translations, otherObj[key] as Translations, fullKey)); + } + } else if (!(key in baseObj)) { + extraKeys.push(fullKey); + } + }); + + return extraKeys; +}; + +const TranslationComparison: React.FC = () => { + const { t } = useTranslation(); + const [translations, setTranslations] = useState>({}); + const [missingKeys, setMissingKeys] = useState>({}); + const [extraKeys, setExtraKeys] = useState>({}); + const [loading, setLoading] = useState(true); + + const languages: string[] = ["en", "fr", "de", "es"]; + const baseLang: string = "en"; + + useEffect(() => { + const fetchTranslations = async () => { + const fetchedTranslations: Record = {}; + + for (let lang of languages) { + const response = await fetch( + import.meta.env.MODE === "production" + ? import.meta.env.VITE_APP_TEDDYCLOUD_API_URL + `/web/translations/${lang}.json` + : `/web/translations/${lang}.json` + ); + const data: Translations = await response.json(); + fetchedTranslations[lang] = data; + } + setTranslations(fetchedTranslations); + setLoading(false); + }; + + fetchTranslations(); + }, []); + + useEffect(() => { + if (!loading && translations[baseLang]) { + const baseTranslations = translations[baseLang]; + const missing: Record = {}; + const extra: Record = {}; + languages.forEach((lang) => { + if (lang !== baseLang) { + const otherTranslations = translations[lang]; + missing[lang] = findMissingKeys(baseTranslations, otherTranslations); + extra[lang] = findExtraKeys(baseTranslations, otherTranslations); + } + }); + + setMissingKeys(missing); + setExtraKeys(extra); + } + }, [loading, translations]); + + if (loading) return

Loading translations...

; + + return ( + <> +

{t("community.translations.missingExtraKeysHeadline")}

+ {languages.every( + (lang) => lang === baseLang || (missingKeys[lang]?.length === 0 && extraKeys[lang]?.length === 0) + ) ? ( +

{t("community.translations.noDiscrepancies")}

+ ) : ( +
+ {languages.map( + (lang) => + lang !== baseLang && ( +
+

+ {t("community.translations.language")}: {lang.toUpperCase()} +

+ +

{t("community.translations.missingKeys")}

+ {missingKeys[lang]?.length > 0 ? ( +
    + {missingKeys[lang].map((key, index) => ( +
  • {key}
  • + ))} +
+ ) : ( +

{t("community.translations.noMissingKeys")}

+ )} + +

{t("community.translations.extraKeys")}:

+ {extraKeys[lang]?.length > 0 ? ( +
    + {extraKeys[lang].map((key, index) => ( +
  • {key}
  • + ))} +
+ ) : ( +

{t("community.translations.noExtraKeys")}

+ )} +
+ ) + )} +
+ )} + + ); +}; + +export default TranslationComparison; diff --git a/src/utils/translationTable.tsx b/src/utils/translationTable.tsx new file mode 100644 index 00000000..d132ef8e --- /dev/null +++ b/src/utils/translationTable.tsx @@ -0,0 +1,132 @@ +import React, { useEffect, useState } from "react"; +import { Table } from "antd"; +import { useTranslation } from "react-i18next"; + +interface TranslationEntry { + question: string; + answer: string; +} + +interface Translations { + [key: string]: string | Translations | TranslationEntry[]; +} + +const collectAllKeys = (obj: Translations, parentKey = ""): string[] => { + const keys: string[] = []; + + Object.keys(obj).forEach((key) => { + const fullKey = parentKey ? `${parentKey}.${key}` : key; + + if (Array.isArray(obj[key])) { + (obj[key] as TranslationEntry[]).forEach((_, index) => { + // Construct unique keys for each question and answer in the FAQ + keys.push(`${fullKey}[${index}].question`, `${fullKey}[${index}].answer`); + }); + } else if (typeof obj[key] === "object" && obj[key] !== null) { + keys.push(...collectAllKeys(obj[key] as Translations, fullKey)); + } else { + keys.push(fullKey); + } + }); + + return keys; +}; + +const getValueFromKey = (obj: Translations, keyPath: string): string | undefined => { + const keys = keyPath.split("."); + let value: any = obj; + + for (let key of keys) { + // Check if the key has an array index, e.g., 'faq[0]' + if (key.includes("[")) { + const [arrayKey, indexPart] = key.split("["); // Split 'faq[0]' into 'faq' and '0]' + const index = parseInt(indexPart.replace("]", ""), 10); // Extract the index number + + value = value[arrayKey]; + if (Array.isArray(value)) { + value = value[index]; + } else { + return undefined; + } + } else { + value = value ? value[key] : undefined; + } + + if (value === undefined) { + return undefined; + } + } + + return typeof value === "string" ? value : undefined; +}; + +const TranslationTable: React.FC = () => { + const { t } = useTranslation(); + const [translations, setTranslations] = useState>({}); + const [loading, setLoading] = useState(true); + + const languages: string[] = ["en", "fr", "de", "es"]; + const baseLang: string = "en"; + + useEffect(() => { + const fetchTranslations = async () => { + const fetchedTranslations: Record = {}; + for (let lang of languages) { + const response = await fetch( + import.meta.env.MODE === "production" + ? import.meta.env.VITE_APP_TEDDYCLOUD_API_URL + `/web/translations/${lang}.json` + : `/web/translations/${lang}.json` + ); + const data: Translations = await response.json(); + fetchedTranslations[lang] = data; + } + setTranslations(fetchedTranslations); + setLoading(false); + }; + fetchTranslations(); + }, []); + + if (loading) return

Loading translations...

; + + const allKeys = collectAllKeys(translations[baseLang]); + + const columns = [ + { + title: t("community.translations.key"), + dataIndex: "key", + key: "key", + width: "20%", + render: (text: string) =>
{text}
, + }, + ...languages.map((lang) => ({ + title: lang.toUpperCase(), + dataIndex: lang, + key: lang, + })), + ]; + + // Prepare table data by combining translations from all languages + const dataSource = allKeys.map((key) => { + const row: any = { key }; + languages.forEach((lang) => { + const value = getValueFromKey(translations[lang], key); + row[lang] = value || t("community.translations.missing"); // Show "MISSING" if the translation is not found + }); + return row; + }); + + return ( + <> +

{t("community.translations.allTranslationStrings")}

+ + + ); +}; + +export default TranslationTable;