From 13a0c26f67439b0c9697dd126992cca7bf08ddeb Mon Sep 17 00:00:00 2001 From: Dan Ditomaso Date: Mon, 26 May 2025 22:45:47 -0400 Subject: [PATCH] feat: added language switcher, updated some translations --- deno.lock | 6 + src/components/BatteryStatus.tsx | 28 +-- src/components/DeviceInfoPanel.tsx | 237 ++++++++++++++++++ src/components/LanguageSwitcher.tsx | 127 ++++++---- .../PageComponents/Config/Display.tsx | 2 +- .../PageComponents/Config/Position.tsx | 2 +- .../ModuleConfig/ExternalNotification.tsx | 2 +- src/components/Sidebar.tsx | 114 ++------- src/components/ThemeSwitcher.tsx | 73 ++++-- src/components/UI/Input.tsx | 4 +- .../generic/Filter/FilterControl.tsx | 54 ++-- src/components/types.ts | 4 + src/i18n/config.ts | 11 +- src/i18n/locales/en/commandPalette.json | 3 + src/i18n/locales/en/common.json | 3 +- src/i18n/locales/en/deviceConfig.json | 4 +- src/i18n/locales/en/ui.json | 148 ++++++----- src/pages/Dashboard/index.tsx | 4 +- vite.config.ts | 31 ++- 19 files changed, 554 insertions(+), 303 deletions(-) create mode 100644 src/components/DeviceInfoPanel.tsx create mode 100644 src/components/types.ts diff --git a/deno.lock b/deno.lock index 44abcf4a..20bbe03b 100644 --- a/deno.lock +++ b/deno.lock @@ -1,6 +1,7 @@ { "version": "5", "specifiers": { + "jsr:@std/path@*": "1.0.6", "npm:@bufbuild/protobuf@^2.2.5": "2.2.5", "npm:@jsr/meshtastic__core@2.6.2": "2.6.2", "npm:@jsr/meshtastic__js@2.6.0-0": "2.6.0-0", @@ -79,6 +80,11 @@ "npm:zod@^3.24.3": "3.24.3", "npm:zustand@5.0.4": "5.0.4_@types+react@19.1.2_immer@10.1.1_react@19.1.0" }, + "jsr": { + "@std/path@1.0.6": { + "integrity": "ab2c55f902b380cf28e0eec501b4906e4c1960d13f00e11cfbcd21de15f18fed" + } + }, "npm": { "@adobe/css-tools@4.4.2": { "integrity": "sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==" diff --git a/src/components/BatteryStatus.tsx b/src/components/BatteryStatus.tsx index 19d6f379..56a0bc4b 100644 --- a/src/components/BatteryStatus.tsx +++ b/src/components/BatteryStatus.tsx @@ -7,15 +7,7 @@ import { } from "lucide-react"; import { useTranslation } from "react-i18next"; import { Subtle } from "@components/UI/Typography/Subtle.tsx"; - -interface DeviceMetrics { - batteryLevel?: number | null; - voltage?: number | null; -} - -interface BatteryStatusProps { - deviceMetrics?: DeviceMetrics | null; -} +import { DeviceMetrics } from "./types.ts"; interface BatteryStateConfig { condition: (level: number) => boolean; @@ -24,6 +16,10 @@ interface BatteryStateConfig { text: (level: number) => string; } +interface BatteryStatusProps { + deviceMetrics?: DeviceMetrics | null; +} + const getBatteryStates = ( t: (key: string, options?: object) => string, ): BatteryStateConfig[] => { @@ -73,7 +69,7 @@ const BatteryStatus: React.FC = ({ deviceMetrics }) => { const { t } = useTranslation(); const batteryStates = getBatteryStates(t); - const { batteryLevel, voltage } = deviceMetrics; + const { batteryLevel } = deviceMetrics; const currentState = getBatteryState(batteryLevel, batteryStates) ?? batteryStates[batteryStates.length - 1]; @@ -81,19 +77,13 @@ const BatteryStatus: React.FC = ({ deviceMetrics }) => { const iconClassName = currentState.className; const statusText = currentState.text(batteryLevel); - const voltageTitle = `${voltage?.toPrecision(3) ?? t("unknown.shortName")} ${ - t("unit.volts") - }`; - return (
- - {statusText} - + {statusText}
); }; diff --git a/src/components/DeviceInfoPanel.tsx b/src/components/DeviceInfoPanel.tsx new file mode 100644 index 00000000..6e9306f6 --- /dev/null +++ b/src/components/DeviceInfoPanel.tsx @@ -0,0 +1,237 @@ +import { cn } from "@core/utils/cn.ts"; +import { + CpuIcon, + Languages, + LucideIcon, + Palette, + PenLine, + Search as SearchIcon, + ZapIcon, +} from "lucide-react"; +import BatteryStatus from "./BatteryStatus.tsx"; +import { Subtle } from "./UI/Typography/Subtle.tsx"; +import { Avatar } from "./UI/Avatar.tsx"; +import { DeviceMetrics } from "./types.ts"; +import { Button } from "./UI/Button.tsx"; +import React from "react"; +import { useTranslation } from "react-i18next"; +import ThemeSwitcher from "./ThemeSwitcher.tsx"; +import LanguageSwitcher from "./LanguageSwitcher.tsx"; +import { Code } from "./UI/Typography/Code.tsx"; + +interface DeviceInfoPanelProps { + isCollapsed: boolean; + deviceMetrics: DeviceMetrics; + firmwareVersion: string; + user: { + shortName: string; + longName: string; + }; + setDialogOpen: () => void; + setCommandPaletteOpen: () => void; + disableHover?: boolean; +} + +interface InfoDisplayItem { + id: string; + label: string; + icon?: LucideIcon; + customComponent?: React.ReactNode; + value?: string | number | null; +} + +interface ActionButtonConfig { + id: string; + label: string; + icon: LucideIcon; + onClick?: () => void; + render?: () => React.ReactNode; +} + +export const DeviceInfoPanel = ({ + deviceMetrics, + firmwareVersion, + user, + isCollapsed, + setDialogOpen, + setCommandPaletteOpen, + disableHover = false, +}: DeviceInfoPanelProps) => { + const { t } = useTranslation(); + const { batteryLevel, voltage } = deviceMetrics; + + const deviceInfoItems: InfoDisplayItem[] = [ + { + id: "battery", + label: t("batteryStatus.title"), + customComponent: , + value: batteryLevel !== undefined ? `${batteryLevel}%` : "N/A", + }, + { + id: "voltage", + label: t("batteryVoltage.title"), + icon: ZapIcon, + value: voltage !== undefined + ? `${voltage?.toPrecision(3)} V` + : t("unknown.notAvailable", "N/A"), + }, + { + id: "firmware", + label: t("sidebar.deviceInfo.firmware.title"), + icon: CpuIcon, + value: firmwareVersion ?? t("unknown.notAvailable", "N/A"), + }, + ]; + + const actionButtons: ActionButtonConfig[] = [ + { + id: "changeName", + label: t("sidebar.deviceInfo.deviceName.changeName"), + icon: PenLine, + onClick: setDialogOpen, + }, + { + id: "commandMenu", + label: t("page.title", { ns: "commandPalette" }), + icon: SearchIcon, + onClick: setCommandPaletteOpen, + }, + { + id: "theme", + label: t("theme.changeTheme"), + icon: Palette, + render: () => , + }, + { + id: "language", + label: t("language.changeLanguage"), + icon: Languages, + render: () => , + }, + ]; + + return ( +
+
+
+ + {!isCollapsed && ( +

+ {user.longName} +

+ )} +
+ + {!isCollapsed && ( +
+ )} + +
+ {deviceInfoItems.map((item) => { + const IconComponent = item.icon; + return ( +
+ {IconComponent && ( + + )} + {item.customComponent} + {item.id !== "battery" && ( + + {item.label}: {item.value} + + )} +
+ ); + })} +
+ + {!isCollapsed && ( +
+ )} + +
+ {actionButtons.map((buttonItem) => { + const Icon = buttonItem.icon; + if (buttonItem.render) { + return buttonItem.render(); + } + return ( + + ); + })} + {/* {import.meta.env.COMMIT_HASH} */} +
+
+
+ ); +}; diff --git a/src/components/LanguageSwitcher.tsx b/src/components/LanguageSwitcher.tsx index 0ef52534..03db9d9c 100644 --- a/src/components/LanguageSwitcher.tsx +++ b/src/components/LanguageSwitcher.tsx @@ -1,60 +1,91 @@ -import clsx from "clsx"; +import { Check, Languages } from "lucide-react"; import { useTranslation } from "react-i18next"; +import { LangCode, supportedLanguages } from "../i18n/config.ts"; import useLang from "@core/hooks/useLang.ts"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "./UI/DropdownMenu.tsx"; +import { Subtle } from "./UI/Typography/Subtle.tsx"; +import { cn } from "@core/utils/cn.ts"; import { Button } from "./UI/Button.tsx"; -import { supportedLanguages } from "../i18n/config.ts"; -import { DropdownMenu, DropdownMenuContent } from "./UI/DropdownMenu.tsx"; -import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; -function LanguageSwitcher() { - const { t, i18n } = useTranslation(); - const { set } = useLang(); +interface LanguageSwitcherProps { + disableHover?: boolean; +} + +export default function LanguageSwitcher( + { disableHover = false }: LanguageSwitcherProps, +) { + const { i18n } = useTranslation("ui"); + const { set: setLanguage } = useLang(); + + const currentLanguage = + supportedLanguages.find((lang) => lang.code === i18n.language) || + supportedLanguages[0]; + + const handleLanguageChange = async (languageCode: LangCode) => { + await setLanguage(languageCode, true); + }; return ( - - asdlkfj - - {supportedLanguages?.map((lang) => ( -
  • + + -
  • + {`${i18n.t("language.changeLanguage")}:`} + + + {currentLanguage.code.toUpperCase()} + + + + + {supportedLanguages.map((language) => ( + handleLanguageChange(language.code as LangCode)} + className="flex items-center justify-between cursor-pointer" + > +
    + {language.flag} + {language.name} +
    + {i18n.language === language.code && ( + + )} +
    ))}
    ); } - -export default LanguageSwitcher; diff --git a/src/components/PageComponents/Config/Display.tsx b/src/components/PageComponents/Config/Display.tsx index 1ec0fa28..478aec32 100644 --- a/src/components/PageComponents/Config/Display.tsx +++ b/src/components/PageComponents/Config/Display.tsx @@ -98,7 +98,7 @@ export const Display = () => { type: "select", name: "displaymode", label: t("display.displayMode.label"), - description: t("display.displayMode.descriptio"), + description: t("display.displayMode.description"), properties: { enumValue: Protobuf.Config.Config_DisplayConfig_DisplayMode, formatEnumName: true, diff --git a/src/components/PageComponents/Config/Position.tsx b/src/components/PageComponents/Config/Position.tsx index add2bb77..78841477 100644 --- a/src/components/PageComponents/Config/Position.tsx +++ b/src/components/PageComponents/Config/Position.tsx @@ -76,7 +76,7 @@ export const Position = () => { activeFlags?.includes(name as FlagName) ?? false, onValueChange: onPositonFlagChange, label: t("position.positionFlags.label"), - placeholder: t("position.positionFlags.placeholder"), + placeholder: t("position.flags.placeholder"), description: t("position.positionFlags.description"), properties: { enumValue: getAllFlags(), diff --git a/src/components/PageComponents/ModuleConfig/ExternalNotification.tsx b/src/components/PageComponents/ModuleConfig/ExternalNotification.tsx index 9f9fdaa2..470ab5ea 100644 --- a/src/components/PageComponents/ModuleConfig/ExternalNotification.tsx +++ b/src/components/PageComponents/ModuleConfig/ExternalNotification.tsx @@ -47,7 +47,7 @@ export const ExternalNotification = () => { }, ], properties: { - suffix: t("unit.ms", { ns: "common" }), + suffix: t("unit.millisecond.suffix"), }, }, { diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 1d288e22..fe4aeba9 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -4,7 +4,6 @@ import { Subtle } from "@components/UI/Typography/Subtle.tsx"; import { useDevice } from "@core/stores/deviceStore.ts"; import type { Page } from "@core/stores/deviceStore.ts"; import { Spinner } from "@components/UI/Spinner.tsx"; -import { Avatar } from "@components/UI/Avatar.tsx"; import { CircleChevronLeft, @@ -13,19 +12,15 @@ import { type LucideIcon, MapIcon, MessageSquareIcon, - PenLine, - SearchIcon, SettingsIcon, UsersIcon, - ZapIcon, } from "lucide-react"; import { cn } from "@core/utils/cn.ts"; import { useSidebar } from "@core/stores/sidebarStore.tsx"; -import ThemeSwitcher from "@components/ThemeSwitcher.tsx"; import { useAppStore } from "@core/stores/appStore.ts"; -import BatteryStatus from "@components/BatteryStatus.tsx"; import { SidebarButton } from "@components/UI/Sidebar/SidebarButton.tsx"; import { useTranslation } from "react-i18next"; +import { DeviceInfoPanel } from "./DeviceInfoPanel.tsx"; export interface SidebarProps { children?: React.ReactNode; @@ -210,98 +205,21 @@ export const Sidebar = ({ children }: SidebarProps) => { ) : ( - <> -
    - -

    - {myNode.user?.longName} -

    -
    - -
    -
    - -
    -
    - - - {t("sidebar.deviceInfo.volts", { - voltage: myNode.deviceMetrics?.voltage?.toPrecision(3) ?? - t("unknown.shortName"), - })} - -
    -
    - - - {t("sidebar.deviceInfo.firmwareVersion", { - version: myMetadata?.firmwareVersion ?? - t("unknown.shortName"), - })} - -
    -
    -
    - - - -
    - + setCommandPaletteOpen(true)} + setDialogOpen={() => setDialogOpen("deviceName", true)} + user={{ + longName: myNode?.user?.longName ?? t("unknown.longName"), + shortName: myNode?.user?.shortName ?? t("unknown.shortName"), + }} + firmwareVersion={myMetadata?.firmwareVersion ?? + t("unknown.firmwareVersion")} + deviceMetrics={{ + batteryLevel: myNode.deviceMetrics?.batteryLevel, + voltage: myNode.deviceMetrics?.voltage, + }} + /> )} diff --git a/src/components/ThemeSwitcher.tsx b/src/components/ThemeSwitcher.tsx index 1814546d..ab147ce2 100644 --- a/src/components/ThemeSwitcher.tsx +++ b/src/components/ThemeSwitcher.tsx @@ -1,22 +1,35 @@ -import { useTheme } from "../core/hooks/useTheme.ts"; -import { cn } from "../core/utils/cn.ts"; +import { useTheme } from "@core/hooks/useTheme.ts"; +import { cn } from "@core/utils/cn.ts"; import { Monitor, Moon, Sun } from "lucide-react"; import { useTranslation } from "react-i18next"; +import { Subtle } from "./UI/Typography/Subtle.tsx"; +import { Button } from "./UI/Button.tsx"; type ThemePreference = "light" | "dark" | "system"; -export default function ThemeSwitcher({ - className = "", -}: { +interface ThemeSwitcherProps { className?: string; -}) { + disableHover?: boolean; +} + +export default function ThemeSwitcher({ + className: passedClassName = "", + disableHover = false, +}: ThemeSwitcherProps) { const { preference, setPreference } = useTheme(); const { t } = useTranslation("ui"); + const iconBaseClass = + "size-4 flex-shrink-0 text-gray-500 dark:text-gray-400 transition-colors duration-150"; + const iconHoverClass = !disableHover + ? "group-hover:text-gray-700 dark:group-hover:text-gray-200" + : ""; + const combinedIconClass = cn(iconBaseClass, iconHoverClass); + const themeIcons = { - light: , - dark: , - system: , + light: , + dark: , + system: , }; const toggleTheme = () => { @@ -35,22 +48,46 @@ export default function ThemeSwitcher({ const currentDisplayPreference = preferenceDisplayMap[preference]; return ( - + + {t("theme.changeTheme")} + + ); } diff --git a/src/components/UI/Input.tsx b/src/components/UI/Input.tsx index 603465cb..81a072e7 100644 --- a/src/components/UI/Input.tsx +++ b/src/components/UI/Input.tsx @@ -82,8 +82,8 @@ const Input = React.forwardRef( ref.current.focus(); } }, - ariaLabel: t("filters.clearInput.label"), - tooltip: t("filters.clearInput.label"), + ariaLabel: t("clearInput.label"), + tooltip: t("clearInput.label"), condition: !!showClearButton && !!value, }, { diff --git a/src/components/generic/Filter/FilterControl.tsx b/src/components/generic/Filter/FilterControl.tsx index 451d9aec..87ff3723 100644 --- a/src/components/generic/Filter/FilterControl.tsx +++ b/src/components/generic/Filter/FilterControl.tsx @@ -62,8 +62,8 @@ function HopsLabelContent({ hopsAway, t }: HopsLabelProps) { return ( <> - {t("filters.hops.text", { - value: startHops === 0 ? t("filters.hops.direct") : startHops, + {t("hops.text", { + value: startHops === 0 ? t("hops.direct") : startHops, })} {startHops !== endHops ? ` — ${endHops}` : ""} @@ -82,11 +82,11 @@ function LastHeardLabelContent( const [start, end] = lastHeardRange; return ( <> - {t("filters.lastHeard.labelText", { value: "" })} + {t("lastHeard.labelText", { value: "" })}
    {start === 0 ? ( - t("filters.lastHeard.nowLabel") + t("lastHeard.nowLabel") ) : ( <> @@ -123,7 +123,7 @@ function BatteryLevelLabelContent( return ( <> - {t("filters.batteryLevel.labelText", { + {t("batteryLevel.labelText", { value: formatBatteryValue(start), })} {start !== end && typeof end !== "undefined" && ( @@ -231,7 +231,7 @@ export function FilterControl({ : "", parameters?.popoverTriggerClassName, )} - aria-label={t("filters.filter.label")} + aria-label={t("filter.label")} > {parameters?.triggerIcon ?? } @@ -246,14 +246,14 @@ export function FilterControl({
    - + {(parameters?.showTextSearch ?? true) && (
    )}
    - + - + - + - {t("filters.resetFilters.label")} + {t("button.reset")} {children &&
    {children}
    } diff --git a/src/components/types.ts b/src/components/types.ts new file mode 100644 index 00000000..2fcf8d14 --- /dev/null +++ b/src/components/types.ts @@ -0,0 +1,4 @@ +export type DeviceMetrics = { + batteryLevel?: number | null; + voltage?: number | null; +}; diff --git a/src/i18n/config.ts b/src/i18n/config.ts index 4e5702e1..f530a4fe 100644 --- a/src/i18n/config.ts +++ b/src/i18n/config.ts @@ -3,14 +3,15 @@ import { initReactI18next } from "react-i18next"; import Backend from "i18next-http-backend"; import LanguageDetector from "i18next-browser-languagedetector"; -export type Lang = { code: string; name: string }; +export type Lang = { code: string; name: string; flag: string }; export type LangCode = Lang["code"]; export const supportedLanguages: Lang[] = [ - // { code: "de", name: "Deutsch" }, - { code: "en", name: "English" }, - // { code: "es", name: "Español" }, - // { code: "fr", name: "Français" }, + // { code: "de", name: "Deutsch", flag: "🇩🇪" }, + { code: "en", name: "English", flag: "🇺🇸" }, + // { code: "es", name: "Español", flag: "🇪🇸" }, + // { code: "fr", name: "Français", flag: "🇫🇷" }, + // { code: "zh", name: "中文", flag: "🇨🇳" }, ]; i18next diff --git a/src/i18n/locales/en/commandPalette.json b/src/i18n/locales/en/commandPalette.json index 56f87a24..7b82e97b 100644 --- a/src/i18n/locales/en/commandPalette.json +++ b/src/i18n/locales/en/commandPalette.json @@ -1,5 +1,8 @@ { "emptyState": "No results found.", + "page": { + "title": "Command Palette" + }, "pinGroup": { "label": "Pin command group" }, diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index dc44bf70..b54b8f88 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -55,7 +55,7 @@ }, "second": { "one": "Second", "plural": "Seconds" }, "snr": "SNR", - "volts": "Volts", + "volt": { "one": "Volt", "plural": "Volts", "suffix": "V" }, "record": { "one": "Records", "plural": "Records" } }, "security": { @@ -64,6 +64,7 @@ "unknown": { "longName": "Unknown", "shortName": "UNK", + "notAvailable": "N/A", "num": "??" }, "nodeUnknownPrefix": "!", diff --git a/src/i18n/locales/en/deviceConfig.json b/src/i18n/locales/en/deviceConfig.json index 4a5ad8fb..0fc9314e 100644 --- a/src/i18n/locales/en/deviceConfig.json +++ b/src/i18n/locales/en/deviceConfig.json @@ -78,7 +78,7 @@ "display": { "description": "Settings for the device display", "title": "Display Settings", - "boldHeading": { + "headingBold": { "description": "Bolden the heading text", "label": "Bold Heading" }, @@ -320,12 +320,12 @@ "description": "GPS module TX pin override", "label": "Transmit Pin" }, - "fieldPlaceholder_selectPositionFlags": "Select position flags...", "intervalsSettings": { "description": "How often to send position updates", "label": "Intervals" }, "flags": { + "placeholder": "Select position flags...", "altitude": "Altitude", "altitudeGeoidalSeparation": "Altitude Geoidal Separation", "altitudeMsl": "Altitude is Mean Sea Level", diff --git a/src/i18n/locales/en/ui.json b/src/i18n/locales/en/ui.json index 546509c3..17bf2ca5 100644 --- a/src/i18n/locales/en/ui.json +++ b/src/i18n/locales/en/ui.json @@ -22,10 +22,17 @@ }, "deviceInfo": { "volts": "{{voltage}} volts", - "firmwareVersion": "v{{version}}", - "button": { - "editDeviceName": "Edit device name" - } + "firmware": { + "title": "Firmware", + "version": "v{{version}}", + "buildDate": "Build date: {{date}}" + }, + "deviceName": { + "title": "Device Name", + "changeName": "Change Device Name", + "placeholder": "Enter device name" + }, + "editDeviceName": "Edit device name" } }, "batteryStatus": { @@ -75,76 +82,79 @@ "label": "Show password" } }, - "filters": { - "general": { - "label": "General" - }, - "hardware": { - "label": "Hardware" - }, - "metrics": { - "label": "Metrics" - }, - "role": { - "label": "Role" - }, - "filter": { - "label": "Filter" - }, - "clearInput": { - "label": "Clear input" - }, - "resetFilters": { - "label": "Reset Filters" - }, - "nodeName": { - "label": "Node name/number", - "placeholder": "Meshtastic 1234" - }, - "airtimeUtilization": { - "label": "Airtime Utilization (%)" - }, - "batteryLevel": { - "label": "Battery level (%)", - "labelText": "Battery level (%): {{value}}" - }, - "batteryVoltage": { - "label": "Battery voltage (V)" - }, - "channelUtilization": { - "label": "Channel Utilization (%)" - }, - "hops": { - "direct": "Direct", - "label": "Number of hops", - "text": "Number of hops: {{value}}" - }, - "lastHeard": { - "label": "Last heard", - "labelText": "Last heard: {{value}}", - "nowLabel": "Now" - }, - "snr": { - "label": "SNR (db)" - }, - "favorites": { - "label": "Favorites" - }, - "hide": { - "label": "Hide" - }, - "showOnly": { - "label": "Show Only" - }, - "viaMqtt": { - "label": "Connected via MQTT" - } + "general": { + "label": "General" + }, + "hardware": { + "label": "Hardware" + }, + "metrics": { + "label": "Metrics" + }, + "role": { + "label": "Role" + }, + "filter": { + "label": "Filter" + }, + "clearInput": { + "label": "Clear input" + }, + "resetFilters": { + "label": "Reset Filters" + }, + "nodeName": { + "label": "Node name/number", + "placeholder": "Meshtastic 1234" + }, + "airtimeUtilization": { + "label": "Airtime Utilization (%)" + }, + "batteryLevel": { + "label": "Battery level (%)", + "labelText": "Battery level (%): {{value}}" + }, + "batteryVoltage": { + "label": "Battery voltage (V)", + "title": "Voltage" + }, + "channelUtilization": { + "label": "Channel Utilization (%)" + }, + "hops": { + "direct": "Direct", + "label": "Number of hops", + "text": "Number of hops: {{value}}" + }, + "lastHeard": { + "label": "Last heard", + "labelText": "Last heard: {{value}}", + "nowLabel": "Now" + }, + "snr": { + "label": "SNR (db)" + }, + "favorites": { + "label": "Favorites" + }, + "hide": { + "label": "Hide" + }, + "showOnly": { + "label": "Show Only" + }, + "viaMqtt": { + "label": "Connected via MQTT" + }, + "language": { + "label": "Language", + "changeLanguage": "Change Language" }, "theme": { "dark": "Dark", "light": "Light", "system": "Automatic", - "changeTheme": "Change current theme" + "changeTheme": "Change Color Scheme" }, "footer": { "text": "Powered by <0>▲ Vercel | Meshtastic® is a registered trademark of Meshtastic LLC. | <1>Legal Information" diff --git a/src/pages/Dashboard/index.tsx b/src/pages/Dashboard/index.tsx index 86cd8bab..69cfa2e2 100644 --- a/src/pages/Dashboard/index.tsx +++ b/src/pages/Dashboard/index.tsx @@ -14,6 +14,7 @@ import { } from "lucide-react"; import { useMemo } from "react"; import { useTranslation } from "react-i18next"; +import LanguageSwitcher from "@components/LanguageSwitcher.tsx"; export const Dashboard = () => { const { t } = useTranslation("dashboard"); @@ -24,7 +25,7 @@ export const Dashboard = () => { return ( <> -
    +
    @@ -34,6 +35,7 @@ export const Dashboard = () => { {t("dashboard.description")}
    +
    diff --git a/vite.config.ts b/vite.config.ts index a49feff5..8bdd7876 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,15 +1,26 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import { VitePWA } from "vite-plugin-pwa"; -import { execSync } from "node:child_process"; -import process from "node:process"; -import path from "node:path"; +import { resolve } from "jsr:@std/path/resolve"; let hash = ""; try { - hash = execSync("git rev-parse --short HEAD", { encoding: "utf8" }).trim(); + const command = new Deno.Command("git", { + args: ["rev-parse", "--short", "HEAD"], + stdout: "piped", + stderr: "piped", + }); + const { code, stdout, stderr } = await command.output(); + + if (code === 0) { + hash = new TextDecoder().decode(stdout).trim(); + } else { + const errorOutput = new TextDecoder().decode(stderr); + console.error("Error getting git hash:", errorOutput); + hash = "DEV"; + } } catch (error) { - console.error("Error getting git hash:", error); + console.error("Failed to execute git command:", error); hash = "DEV"; } @@ -37,11 +48,11 @@ export default defineConfig({ }, resolve: { alias: { - "@app": path.resolve(process.cwd(), "./src"), - "@pages": path.resolve(process.cwd(), "./src/pages"), - "@components": path.resolve(process.cwd(), "./src/components"), - "@core": path.resolve(process.cwd(), "./src/core"), - "@layouts": path.resolve(process.cwd(), "./src/layouts"), + "@app": resolve(Deno.cwd(), "./src"), + "@pages": resolve(Deno.cwd(), "./src/pages"), + "@components": resolve(Deno.cwd(), "./src/components"), + "@core": resolve(Deno.cwd(), "./src/core"), + "@layouts": resolve(Deno.cwd(), "./src/layouts"), }, }, server: {