Browse Source

Refactor: updated how translations are scoped, updated all references

pull/627/head
Dan Ditomaso 1 year ago
parent
commit
f64449f505
  1. 18
      .github/workflows/crowdin-upload-sources.yml
  2. 14
      crowdin.yml
  3. 16
      src/components/BatteryStatus.tsx
  4. 58
      src/components/CommandPalette/index.tsx
  5. 10
      src/components/Dialog/DeleteMessagesDialog/DeleteMessagesDialog.tsx
  6. 24
      src/components/Dialog/DeviceNameDialog.tsx
  7. 20
      src/components/Dialog/ImportDialog.tsx
  8. 21
      src/components/Dialog/LocationResponseDialog.tsx
  9. 18
      src/components/Dialog/NewDeviceDialog.tsx
  10. 73
      src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx
  11. 51
      src/components/Dialog/PKIBackupDialog.tsx
  12. 14
      src/components/Dialog/PkiRegenerateDialog.tsx
  13. 19
      src/components/Dialog/QRDialog.tsx
  14. 8
      src/components/Dialog/RebootDialog.tsx
  15. 15
      src/components/Dialog/RebootOTADialog.tsx
  16. 20
      src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx
  17. 8
      src/components/Dialog/RemoveNodeDialog.tsx
  18. 10
      src/components/Dialog/ShutdownDialog.tsx
  19. 11
      src/components/Dialog/TracerouteResponseDialog.tsx
  20. 20
      src/components/Dialog/UnsafeRolesDialog/UnsafeRolesDialog.tsx
  21. 2
      src/components/Dialog/UnsafeRolesDialog/useUnsafeRolesDialog.ts
  22. 26
      src/components/Form/FormMultiSelect.tsx
  23. 4
      src/components/KeyBackupReminder.tsx
  24. 60
      src/components/LanguageSwitcher.tsx
  25. 103
      src/components/PageComponents/Channel.tsx
  26. 238
      src/components/PageComponents/Config/Bluetooth.tsx
  27. 52
      src/components/PageComponents/Config/Device/index.tsx
  28. 58
      src/components/PageComponents/Config/Display.tsx
  29. 88
      src/components/PageComponents/Config/LoRa.tsx
  30. 70
      src/components/PageComponents/Config/Network/index.tsx
  31. 72
      src/components/PageComponents/Config/Position.tsx
  32. 64
      src/components/PageComponents/Config/Power.tsx
  33. 168
      src/components/PageComponents/Config/Security/Security.tsx
  34. 4
      src/components/PageComponents/Connect/BLE.tsx
  35. 38
      src/components/PageComponents/Connect/HTTP.tsx
  36. 12
      src/components/PageComponents/Connect/Serial.tsx
  37. 49
      src/components/PageComponents/Map/NodeDetail.tsx
  38. 4
      src/components/PageComponents/Messages/ChannelChat.tsx
  39. 16
      src/components/PageComponents/Messages/TraceRoute.tsx
  40. 26
      src/components/PageComponents/ModuleConfig/AmbientLighting.tsx
  41. 36
      src/components/PageComponents/ModuleConfig/Audio.tsx
  42. 53
      src/components/PageComponents/ModuleConfig/CannedMessage.tsx
  43. 50
      src/components/PageComponents/ModuleConfig/DetectionSensor.tsx
  44. 77
      src/components/PageComponents/ModuleConfig/ExternalNotification.tsx
  45. 148
      src/components/PageComponents/ModuleConfig/MQTT.tsx
  46. 19
      src/components/PageComponents/ModuleConfig/NeighborInfo.tsx
  47. 27
      src/components/PageComponents/ModuleConfig/Paxcounter.tsx
  48. 22
      src/components/PageComponents/ModuleConfig/RangeTest.tsx
  49. 46
      src/components/PageComponents/ModuleConfig/Serial.tsx
  50. 29
      src/components/PageComponents/ModuleConfig/StoreForward.tsx
  51. 54
      src/components/PageComponents/ModuleConfig/Telemetry.tsx
  52. 66
      src/components/Sidebar.tsx
  53. 10
      src/components/ThemeSwitcher.tsx
  54. 2
      src/components/UI/Avatar.tsx
  55. 2
      src/components/UI/Footer.tsx
  56. 22
      src/components/UI/Input.tsx
  57. 335
      src/components/generic/Filter/FilterControl.tsx
  58. 92
      src/core/hooks/useLang.ts
  59. 22
      src/core/hooks/usePositionFlags.ts
  60. 28
      src/i18n.ts
  61. 51
      src/i18n/config.ts
  62. 607
      src/i18n/locales/en.json
  63. 69
      src/i18n/locales/en/channels.json
  64. 47
      src/i18n/locales/en/commandPalette.json
  65. 72
      src/i18n/locales/en/common.json
  66. 12
      src/i18n/locales/en/dashboard.json
  67. 442
      src/i18n/locales/en/deviceConfig.json
  68. 160
      src/i18n/locales/en/dialog.json
  69. 37
      src/i18n/locales/en/messages.json
  70. 448
      src/i18n/locales/en/moduleConfig.json
  71. 51
      src/i18n/locales/en/nodes.json
  72. 152
      src/i18n/locales/en/ui.json
  73. 2
      src/index.tsx
  74. 18
      src/pages/Channels.tsx
  75. 26
      src/pages/Config/DeviceConfig.tsx
  76. 28
      src/pages/Config/ModuleConfig.tsx
  77. 33
      src/pages/Config/index.tsx
  78. 23
      src/pages/Dashboard/index.tsx
  79. 50
      src/pages/Messages.tsx
  80. 40
      src/pages/Nodes.tsx

18
.github/workflows/crowdin-upload-sources.yml

@ -1,8 +1,11 @@
name: Crowdin Upload Sources Action
on:
push: # Watch source en.json for changes on main
paths: [ '/src/i18n/locales/en.json' ]
push:
# Monitor all .json files within the /src/i18n/locales/en/ directory.
# This ensures the workflow triggers if any the English namespace files are modified on the main branch.
paths:
- '/src/i18n/locales/en/**/*.json'
branches: [ main ]
workflow_dispatch: # Allow manual triggering
@ -18,11 +21,12 @@ jobs:
uses: crowdin/github-action@v2
with:
base_url: 'https://meshtastic.crowdin.com/api/v2'
config: 'crowdin.yml'
upload_sources: true
upload_translations: false
download_translations: false
crowdin_branch_name: 'main'
config: 'crowdin.yml'
upload_sources: true
upload_translations: false
download_translations: false
crowdin_branch_name: 'main'
env:
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}

14
crowdin.yml

@ -1,10 +1,10 @@
"project_id_env": "CROWDIN_PROJECT_ID"
"api_token_env": "CROWDIN_PERSONAL_TOKEN"
"base_path": "."
"base_url": "https://meshtastic.crowdin.com/api/v2"
project_id_env: CROWDIN_PROJECT_ID
api_token_env: CROWDIN_PERSONAL_TOKEN
base_path: "."
base_url: "https://meshtastic.crowdin.com/api/v2"
"preserve_hierarchy": true
preserve_hierarchy: true
files:
- source: /src/i18n/locales/en.json
translation: /src/i18n/locales/%two_letters_code%.json
- source: "/src/i18n/locales/en/**/*.json"
translation: "/src/i18n/locales/%locale%/%original_file_name%"

16
src/components/BatteryStatus.tsx

@ -32,25 +32,25 @@ const getBatteryStates = (
condition: (level) => level > 100,
Icon: PlugZapIcon,
className: "text-gray-500",
text: () => t("common_batteryStatus_pluggedIn"),
text: () => t("batteryStatus.pluggedIn"),
},
{
condition: (level) => level > 80,
Icon: BatteryFullIcon,
className: "text-green-500",
text: (level) => t("common_batteryStatus_charging", { level }),
text: (level) => t("batteryStatus.charging", { level }),
},
{
condition: (level) => level > 20,
Icon: BatteryMediumIcon,
className: "text-yellow-500",
text: (level) => t("common_batteryStatus_charging", { level }),
text: (level) => t("batteryStatus.charging", { level }),
},
{
condition: () => true,
Icon: BatteryLowIcon,
className: "text-red-500",
text: (level) => t("common_batteryStatus_charging", { level }),
text: (level) => t("batteryStatus.charging", { level }),
},
];
};
@ -81,9 +81,9 @@ const BatteryStatus: React.FC<BatteryStatusProps> = ({ deviceMetrics }) => {
const iconClassName = currentState.className;
const statusText = currentState.text(batteryLevel);
const voltageTitle = `${
voltage?.toPrecision(3) ?? t("common_unknown_short")
} ${t("common_unit_volts")}`;
const voltageTitle = `${voltage?.toPrecision(3) ?? t("unknown.shortName")} ${
t("unit.volts")
}`;
return (
<div
@ -91,7 +91,7 @@ const BatteryStatus: React.FC<BatteryStatusProps> = ({ deviceMetrics }) => {
title={voltageTitle}
>
<BatteryIcon size={22} className={iconClassName} />
<Subtle aria-label={t("common_batteryStatus_title")}>
<Subtle aria-label={t("batteryStatus.title")}>
{statusText}
</Subtle>
</div>

58
src/components/CommandPalette/index.tsx

@ -66,29 +66,29 @@ export const CommandPalette = () => {
const { pinnedItems, togglePinnedItem } = usePinnedItems({
storageName: "pinnedCommandMenuGroups",
});
const { t } = useTranslation();
const { t } = useTranslation("commandPalette");
const groups: Group[] = [
{
label: t("command_palette_goto_label"),
label: t("goto.label"),
icon: LinkIcon,
commands: [
{
label: t("command_palette_goto_command_messages"),
label: t("goto.command.messages"),
icon: MessageSquareIcon,
action() {
setActivePage("messages");
},
},
{
label: t("command_palette_goto_command_map"),
label: t("goto.command.map"),
icon: MapIcon,
action() {
setActivePage("map");
},
},
{
label: t("command_palette_goto_command_config"),
label: t("goto.command.config"),
icon: SettingsIcon,
action() {
setActivePage("config");
@ -96,14 +96,14 @@ export const CommandPalette = () => {
tags: ["settings"],
},
{
label: t("command_palette_goto_command_channels"),
label: t("goto.command.channels"),
icon: LayersIcon,
action() {
setActivePage("channels");
},
},
{
label: t("command_palette_goto_command_nodes"),
label: t("goto.command.nodes"),
icon: UsersIcon,
action() {
setActivePage("nodes");
@ -112,19 +112,19 @@ export const CommandPalette = () => {
],
},
{
label: t("command_palette_manage_label"),
label: t("manage.label"),
icon: SmartphoneIcon,
commands: [
{
label: t("command_palette_manage_command_switch_node"),
label: t("manage.command.switchNode"),
icon: ArrowLeftRightIcon,
subItems: getDevices().map((device) => ({
label: getNode(device.hardware.myNodeNum)?.user?.longName ??
t("common_unknown_short"), // Or a more specific key for node name
t("unknown.shortName"),
icon: (
<Avatar
text={getNode(device.hardware.myNodeNum)?.user?.shortName ??
t("common_unknown_short")} // Or a more specific key
t("unknown.shortName")}
/>
),
action() {
@ -133,7 +133,7 @@ export const CommandPalette = () => {
})),
},
{
label: t("command_palette_manage_command_connect_new_node"),
label: t("manage.command.connectNewNode"),
icon: PlusIcon,
action() {
setConnectDialogOpen(true);
@ -142,22 +142,22 @@ export const CommandPalette = () => {
],
},
{
label: t("command_palette_contextual_label"),
label: t("contextual.label"),
icon: BoxSelectIcon,
commands: [
{
label: t("command_palette_contextual_command_qr_code"),
label: t("contextual.command.qrCode"),
icon: QrCodeIcon,
subItems: [
{
label: t("command_palette_contextual_command_qr_generator"),
label: t("contextual.command.qrGenerator"),
icon: <QrCodeIcon size={16} />,
action() {
setDialogOpen("QR", true);
},
},
{
label: t("command_palette_contextual_command_qr_import"),
label: t("contextual.command.qrImport"),
icon: <QrCodeIcon size={16} />,
action() {
setDialogOpen("import", true);
@ -166,42 +166,42 @@ export const CommandPalette = () => {
],
},
{
label: t("command_palette_contextual_command_schedule_shutdown"),
label: t("contextual.command.scheduleShutdown"),
icon: PowerIcon,
action() {
setDialogOpen("shutdown", true);
},
},
{
label: t("command_palette_contextual_command_schedule_reboot"),
label: t("contextual.command.scheduleReboot"),
icon: RefreshCwIcon,
action() {
setDialogOpen("reboot", true);
},
},
{
label: t("command_palette_contextual_command_reboot_to_ota_mode"),
label: t("contextual.command.rebootToOtaMode"),
icon: RefreshCwIcon,
action() {
setDialogOpen("rebootOTA", true);
},
},
{
label: t("command_palette_contextual_command_reset_nodes"),
label: t("contextual.command.resetNodeDb"),
icon: TrashIcon,
action() {
connection?.resetNodes();
},
},
{
label: t("command_palette_contextual_command_factory_reset_device"),
label: t("contextual.command.factoryResetDevice"),
icon: FactoryIcon,
action() {
connection?.factoryResetDevice();
},
},
{
label: t("command_palette_contextual_command_factory_reset_config"),
label: t("contextual.command.factoryResetConfig"),
icon: FactoryIcon,
action() {
connection?.factoryResetConfig();
@ -210,18 +210,18 @@ export const CommandPalette = () => {
],
},
{
label: t("command_palette_debug_label"),
label: t("debug.label"),
icon: BugIcon,
commands: [
{
label: t("command_palette_debug_command_reconfigure"),
label: t("debug.command.reconfigure"),
icon: RefreshCwIcon,
action() {
void connection?.configure();
},
},
{
label: t("command_palette_debug_command_clear_all_stored_messages"),
label: t("debug.command.clearAllStoredMessages"),
icon: EraserIcon,
action() {
setDialogOpen("deleteMessages", true);
@ -254,9 +254,9 @@ export const CommandPalette = () => {
open={commandPaletteOpen}
onOpenChange={setCommandPaletteOpen}
>
<CommandInput placeholder={t("command_palette_input_placeholder")} />
<CommandInput placeholder={t("search.commandPalette")} />
<CommandList>
<CommandEmpty>{t("command_palette_empty_message")}</CommandEmpty>
<CommandEmpty>{t("emptyState")}</CommandEmpty>
{sortedGroups.map((group) => (
<CommandGroup
key={group.label}
@ -270,8 +270,8 @@ export const CommandPalette = () => {
"transition-all duration-300 scale-100 cursor-pointer p-2 focus:*:data-label:opacity-100",
)}
aria-description={pinnedItems.includes(group.label)
? t("command_palette_unpin_group_aria_label")
: t("command_palette_pin_group_aria_label")}
? t("unpinGroup.label")
: t("pinGroup.label")}
>
<span
data-label

10
src/components/Dialog/DeleteMessagesDialog/DeleteMessagesDialog.tsx

@ -21,7 +21,7 @@ export const DeleteMessagesDialog = ({
open,
onOpenChange,
}: DeleteMessagesDialogProps) => {
const { t } = useTranslation();
const { t } = useTranslation("dialog");
const { deleteAllMessages } = useMessageStore();
const handleCloseDialog = () => {
onOpenChange(false);
@ -34,10 +34,10 @@ export const DeleteMessagesDialog = ({
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<AlertTriangleIcon className="h-5 w-5 text-warning" />
{t("dialog_deleteMessages_title")}
{t("deleteMessages.title")}
</DialogTitle>
<DialogDescription>
{t("dialog_deleteMessages_description")}
{t("deleteMessages.description")}
</DialogDescription>
</DialogHeader>
<DialogFooter className="mt-4">
@ -46,7 +46,7 @@ export const DeleteMessagesDialog = ({
onClick={handleCloseDialog}
name="dismiss"
>
{t("dialog_button_dismiss")}
{t("button.dismiss")}
</Button>
<Button
variant="destructive"
@ -56,7 +56,7 @@ export const DeleteMessagesDialog = ({
}}
name="clearMessages"
>
{t("dialog_button_clearMessages")}
{t("button.clearMessages")}
</Button>
</DialogFooter>
</DialogContent>

24
src/components/Dialog/DeviceNameDialog.tsx

@ -10,12 +10,12 @@ import {
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog.tsx";
import { Label } from "@components/UI/Label.tsx";
import { Protobuf } from "@meshtastic/core";
import { useForm } from "react-hook-form";
import { GenericInput } from "@components/Form/FormInput.tsx";
import { useTranslation } from "react-i18next";
import { validateMaxByteLength } from "@core/utils/string.ts";
import { Label } from "../UI/Label.tsx";
export interface User {
longName: string;
@ -33,13 +33,13 @@ export const DeviceNameDialog = ({
open,
onOpenChange,
}: DeviceNameDialogProps) => {
const { t } = useTranslation();
const { t } = useTranslation("dialog");
const { hardware, getNode, connection } = useDevice();
const myNode = getNode(hardware.myNodeNum);
const defaultValues = {
longName: myNode?.user?.longName ?? t("common_unknown_long"),
shortName: myNode?.user?.shortName ?? t("common_unknown_short"),
longName: myNode?.user?.longName ?? t("unknown.longName"),
shortName: myNode?.user?.shortName ?? t("unknown.shortName"),
};
const { getValues, setValue, reset, control, handleSubmit } = useForm<User>({
@ -76,21 +76,21 @@ export const DeviceNameDialog = ({
<DialogContent>
<DialogClose />
<DialogHeader>
<DialogTitle>{t("dialog_deviceName_title")}</DialogTitle>
<DialogTitle>{t("deviceName.title")}</DialogTitle>
<DialogDescription>
{t("dialog_deviceName_description")}
{t("deviceName.description")}
</DialogDescription>
</DialogHeader>
<form onSubmit={onSubmit} className="flex flex-col gap-4">
<div>
<Label htmlFor="longName">
{t("dialog_deviceName_label_longName")}
{t("deviceName.longName")}
</Label>
<GenericInput
control={control}
field={{
name: "longName",
label: "Long Name",
label: t("deviceName.longName"),
type: "text",
properties: {
className: "text-slate-900 dark:text-slate-200",
@ -105,13 +105,13 @@ export const DeviceNameDialog = ({
</div>
<div>
<Label htmlFor="shortName">
{t("dialog_deviceName_label_shortName")}
{t("deviceName.shortName")}
</Label>
<GenericInput
control={control}
field={{
name: "shortName",
label: "Short Name",
label: t("deviceName.shortName"),
type: "text",
properties: {
fieldLength: {
@ -131,9 +131,9 @@ export const DeviceNameDialog = ({
name="reset"
onClick={handleReset}
>
{t("dialog_button_reset")}
{t("button.reset")}
</Button>
<Button type="submit" name="save">{t("common_button_save")}</Button>
<Button type="submit" name="save">{t("button.save")}</Button>
</DialogFooter>
</form>
</DialogContent>

20
src/components/Dialog/ImportDialog.tsx

@ -29,7 +29,7 @@ export const ImportDialog = ({
open,
onOpenChange,
}: ImportDialogProps) => {
const { t } = useTranslation();
const { t } = useTranslation("dialog");
const [importDialogInput, setImportDialogInput] = useState<string>("");
const [channelSet, setChannelSet] = useState<Protobuf.AppOnly.ChannelSet>();
const [validUrl, setValidUrl] = useState<boolean>(false);
@ -46,7 +46,7 @@ export const ImportDialog = ({
channelsUrl.pathname !== "/e/") ||
!channelsUrl.hash
) {
throw t("dialog_import_error_invalidUrl");
throw t("import.error.invalidUrl");
}
const encodedChannelConfig = channelsUrl.hash.substring(1);
@ -101,13 +101,13 @@ export const ImportDialog = ({
<DialogContent>
<DialogClose />
<DialogHeader>
<DialogTitle>{t("dialog_import_title")}</DialogTitle>
<DialogTitle>{t("import.title")}</DialogTitle>
<DialogDescription>
{t("dialog_import_description")}
{t("import.description")}
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-3">
<Label>{t("dialog_import_label_channelSetUrl")}</Label>
<Label>{t("import.channelSetUrl")}</Label>
<Input
value={importDialogInput}
suffix={validUrl ? "✅" : "❌"}
@ -119,7 +119,7 @@ export const ImportDialog = ({
<div className="flex flex-col gap-3">
<div className="flex w-full gap-2">
<div className="w-36">
<Label>{t("dialog_import_label_usePreset")}</Label>
<Label>{t("import.usePreset")}</Label>
<Switch
disabled
checked={channelSet?.loraConfig?.usePreset ?? true}
@ -146,7 +146,7 @@ export const ImportDialog = ({
}
<span className="text-md block font-medium text-text-primary">
{t("dialog_import_label_channels")}
{t("import.channels")}
</span>
<div className="flex w-40 flex-col gap-1">
{channelSet?.settings.map((channel) => (
@ -154,9 +154,7 @@ export const ImportDialog = ({
<Label>
{channel.name.length
? channel.name
: `${
t("dialog_import_label_channelPrefix")
}${channel.id}`}
: `${t("import.channelPrefix")}${channel.id}`}
</Label>
<Checkbox key={channel.id} />
</div>
@ -167,7 +165,7 @@ export const ImportDialog = ({
</div>
<DialogFooter>
<Button onClick={apply} disabled={!validUrl} name="apply">
{t("dialog_button_apply")}
{t("button.apply")}
</Button>
</DialogFooter>
</DialogContent>

21
src/components/Dialog/LocationResponseDialog.tsx

@ -1,4 +1,4 @@
import { useDevice } from "../../core/stores/deviceStore.ts";
import { useDevice } from "@core/stores/deviceStore.ts";
import {
Dialog,
DialogClose,
@ -22,16 +22,16 @@ export const LocationResponseDialog = ({
open,
onOpenChange,
}: LocationResponseDialogProps) => {
const { t } = useTranslation();
const { t } = useTranslation("dialog");
const { getNode } = useDevice();
const from = getNode(location?.from ?? 0);
const longName = from?.user?.longName ??
(from ? `!${numberToHexUnpadded(from?.num)}` : t("common_unknown_short"));
(from ? `!${numberToHexUnpadded(from?.num)}` : t("unknown.shortName"));
const shortName = from?.user?.shortName ??
(from
? `${numberToHexUnpadded(from?.num).substring(0, 4)}`
: t("common_unknown_short"));
: t("unknown.shortName"));
return (
<Dialog open={open} onOpenChange={onOpenChange}>
@ -39,15 +39,16 @@ export const LocationResponseDialog = ({
<DialogClose />
<DialogHeader>
<DialogTitle>
{t("dialog_locationResponse_titlePrefix")}
{longName} ({shortName})
{t("locationResponse.title", {
identifier: `${longName} (${shortName})`,
})}
</DialogTitle>
</DialogHeader>
<DialogDescription>
<div className="ml-5 flex">
<span className="ml-4 border-l-2 border-l-backgroundPrimary pl-2 text-textPrimary">
<p>
{t("dialog_locationResponse_label_coordinates")}
{t("locationResponse.coordinates")}
<a
className="text-blue-500 dark:text-blue-400"
href={`https://www.openstreetmap.org/?mlat=${
@ -61,9 +62,11 @@ export const LocationResponseDialog = ({
</a>
</p>
<p>
{t("dialog_locationResponse_label_altitude")}
{t("locationResponse.altitude")}
{location?.data.altitude}
{t("dialog_locationResponse_unit_meter")}
{location?.data.altitde < 1
? t("unit.meter.one")
: t("unit.meter.plural")}
</p>
</span>
</div>

18
src/components/Dialog/NewDeviceDialog.tsx

@ -53,7 +53,7 @@ const links: { [key: string]: string } = {
};
const ErrorMessage = ({ missingFeatures }: FeatureErrorProps) => {
const { i18n } = useTranslation();
const { i18n } = useTranslation("dialog");
const listFormatter = useMemo(
() =>
@ -95,7 +95,7 @@ const ErrorMessage = ({ missingFeatures }: FeatureErrorProps) => {
<p className="text-sm text-white">
{browserFeatures.length > 0 && (
<Trans
i18nKey="dialog_newDeviceDialog_errorMessage_requiresFeatures"
i18nKey="newDeviceDialog.validation.requiresFeatures"
components={{
"0": <>{featureNodes}</>,
}}
@ -105,8 +105,8 @@ const ErrorMessage = ({ missingFeatures }: FeatureErrorProps) => {
{needsSecureContext && (
<Trans
i18nKey={browserFeatures.length > 0
? "dialog_newDeviceDialog_errorMessage_additionallyRequiresSecureContext"
: "dialog_newDeviceDialog_errorMessage_requiresSecureContext"}
? "newDeviceDialog.validation.additionallyRequiresSecureContext"
: "newDeviceDialog.validation.requiresSecureContext"}
components={{
"0": (
<Link
@ -128,23 +128,23 @@ export const NewDeviceDialog = ({
open,
onOpenChange,
}: NewDeviceProps) => {
const { t } = useTranslation();
const { t } = useTranslation("dialog");
const { unsupported } = useBrowserFeatureDetection();
const tabs: TabManifest[] = [
{
label: t("dialog_newDeviceDialog_tabLabelHttp"),
label: t("newDeviceDialog.tabHttp"),
element: HTTP,
isDisabled: false,
},
{
label: t("dialog_newDeviceDialog_tabLabelBluetooth"),
label: t("newDeviceDialog.tabBluetooth"),
element: BLE,
isDisabled: unsupported.includes("Web Bluetooth") ||
unsupported.includes("Secure Context"),
},
{
label: t("dialog_newDeviceDialog_tabLabelSerial"),
label: t("newDeviceDialog.tabSerial"),
element: Serial,
isDisabled: unsupported.includes("Web Serial") ||
unsupported.includes("Secure Context"),
@ -156,7 +156,7 @@ export const NewDeviceDialog = ({
<DialogContent aria-describedby={undefined}>
<DialogClose />
<DialogHeader>
<DialogTitle>{t("dialog_newDeviceDialog_title")}</DialogTitle>
<DialogTitle>{t("newDeviceDialog.title")}</DialogTitle>
</DialogHeader>
<Tabs defaultValue="HTTP">
<TabsList>

73
src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx

@ -61,8 +61,9 @@ export const NodeDetailsDialog = ({
open,
onOpenChange,
}: NodeDetailsDialogProps) => {
const { t } = useTranslation();
const { setDialogOpen, connection, setActivePage } = useDevice();
const { t } = useTranslation("dialog");
const { setDialogOpen, connection, setActivePage, getNodeError } =
useDevice();
const { setNodeNumToBeRemoved } = useAppStore();
const { setChatType, setActiveChat } = useMessageStore();
@ -92,11 +93,11 @@ export const NodeDetailsDialog = ({
if (!node) return;
toast({
title: t("toast_requestingPosition"),
title: t("toast.requestingPosition.title", { ns: "ui" }),
});
connection?.requestPosition(node.num).then(() =>
toast({
title: t("toast_positionRequestSent"),
title: t("toast.positionRequestSent.title", { ns: "ui" }),
})
);
onOpenChange(false);
@ -106,11 +107,11 @@ export const NodeDetailsDialog = ({
if (!node) return;
toast({
title: t("toast_sendingTraceroute"),
title: t("toast.sendingTraceroute.title", { ns: "ui" }),
});
connection?.traceRoute(node.num).then(() =>
toast({
title: t("toast_tracerouteSent"),
title: t("toast.tracerouteSent.title", { ns: "ui" }),
})
);
onOpenChange(false);
@ -141,25 +142,25 @@ export const NodeDetailsDialog = ({
const deviceMetricsMap = [
{
key: "airUtilTx",
label: t("dialog_nodeDetails_label_airTxUtilization"),
label: t("nodeDetails.airTxUtilization"),
value: node.deviceMetrics?.airUtilTx,
format: (val: number) => `${val.toFixed(2)}%`,
},
{
key: "channelUtilization",
label: t("dialog_nodeDetails_label_channelUtilization"),
label: t("nodeDetails.channelUtilization"),
value: node.deviceMetrics?.channelUtilization,
format: (val: number) => `${val.toFixed(2)}%`,
},
{
key: "batteryLevel",
label: t("dialog_nodeDetails_label_batteryLevel"),
label: t("nodeDetails.batteryLevel"),
value: node.deviceMetrics?.batteryLevel,
format: (val: number) => `${val.toFixed(2)}%`,
},
{
key: "voltage",
label: t("dialog_nodeDetails_label_voltage"),
label: t("nodeDetails.voltage"),
value: node.deviceMetrics?.voltage,
format: (val: number) => `${val.toFixed(2)}V`,
},
@ -171,9 +172,11 @@ export const NodeDetailsDialog = ({
<DialogClose />
<DialogHeader>
<DialogTitle>
{t("dialog_nodeDetails_titlePrefix")}
{node.user?.longName ?? t("common_unknown_short")} (
{node.user?.shortName ?? t("common_unknown_short")})
{t("nodeDetails.title", {
identifier: `${node.user?.longName ?? t("unknown.shortName")} (${
node.user?.shortName ?? t("unknown.shortName")
})`,
})}
</DialogTitle>
</DialogHeader>
<DialogFooter>
@ -185,7 +188,7 @@ export const NodeDetailsDialog = ({
onClick={handleDirectMessage}
>
<MessageSquareIcon className="mr-2" />
{t("dialog_nodeDetails_button_message")}
{t("nodeDetails.message")}
</Button>
<Button
className="mr-1"
@ -193,7 +196,7 @@ export const NodeDetailsDialog = ({
onClick={handleTraceroute}
>
<WaypointsIcon className="mr-2" />
{t("dialog_nodeDetails_button_traceRoute")}
{t("nodeDetails.traceRoute")}
</Button>
<Button className="mr-1" onClick={handleToggleFavorite}>
<StarIcon
@ -221,8 +224,8 @@ export const NodeDetailsDialog = ({
</TooltipTrigger>
<TooltipContent className="bg-slate-800 dark:bg-slate-600 text-white px-4 py-1 rounded text-xs">
{isIgnoredState
? t("dialog_nodeDetails_tooltip_unignoreNode")
: t("dialog_nodeDetails_tooltip_ignoreNode")}
? t("nodeDetails.unignoreNode")
: t("nodeDetails.ignoreNode")}
<TooltipArrow className="fill-slate-800 dark:fill-slate-600" />
</TooltipContent>
</Tooltip>
@ -240,7 +243,7 @@ export const NodeDetailsDialog = ({
</Button>
</TooltipTrigger>
<TooltipContent className="bg-slate-800 dark:bg-slate-600 text-white px-4 py-1 rounded text-xs">
{t("dialog_nodeDetails_tooltip_removeNode")}
{t("nodeDetails.removeNode")}
<TooltipArrow className="fill-slate-800 dark:fill-slate-600" />
</TooltipContent>
</Tooltip>
@ -253,29 +256,29 @@ export const NodeDetailsDialog = ({
<div className="flex flex-row space-x-2">
<div className="w-full bg-slate-100 text-slate-900 dark:text-slate-100 dark:bg-slate-800 p-3 rounded-lg">
<p className="text-lg font-semibold">
{t("dialog_nodeDetails_label_details")}
{t("nodeDetails.details")}
</p>
<p>{t("dialog_nodeDetails_label_nodeNumber")}{node.num}</p>
<p>{t("nodeDetails.nodeNumber")}{node.num}</p>
<p>
{t("dialog_nodeDetails_label_nodeHexPrefix")}
{t("nodeDetails.nodeHexPrefix")}
{numberToHexUnpadded(node.num)}
</p>
<p>
{t("dialog_nodeDetails_label_role")}
{t("nodeDetails.role")}
{Protobuf.Config.Config_DeviceConfig_Role[
node.user?.role ?? 0
].replace(/_/g, " ")}
</p>
<p>
{t("dialog_nodeDetails_label_lastHeard")}
{t("nodeDetails.lastHeard")}
{node.lastHeard === 0
? t("nodes_table_lastHeardStatus_never")
? t("nodesTable.lastHeardStatus.never", { ns: "nodes" })
: <TimeAgo timestamp={node.lastHeard * 1000} />}
</p>
<p>
{t("dialog_nodeDetails_label_hardware")}
{t("nodeDetails.hardware")}
{(Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0] ??
t("common_unknown_short"))
t("unknown.shortName"))
.replace(/_/g, " ")}
</p>
</div>
@ -290,7 +293,7 @@ export const NodeDetailsDialog = ({
<div>
<div className="text-slate-900 dark:text-slate-100 bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
<p className="text-lg font-semibold">
{t("dialog_nodeDetails_label_position")}
{t("nodeDetails.position")}
</p>
{node.position
@ -299,7 +302,7 @@ export const NodeDetailsDialog = ({
{node.position.latitudeI &&
node.position.longitudeI && (
<p>
{t("dialog_locationResponse_label_coordinates")}
{t("locationResponse.coordinates")}
<a
className="text-blue-500 dark:text-blue-400"
href={`https://www.openstreetmap.org/?mlat=${
@ -315,28 +318,28 @@ export const NodeDetailsDialog = ({
)}
{node.position.altitude && (
<p>
{t("dialog_locationResponse_label_altitude")}
{t("locationResponse.altitude")}
{node.position.altitude}
{t("dialog_locationResponse_unit_meter")}
{t("unit.meter")}
</p>
)}
</>
)
: <p>{t("common_unknown_short")}</p>}
: <p>{t("unknown.shortName")}</p>}
<Button
onClick={handleRequestPosition}
name="requestPosition"
className="mt-2"
>
<MapPinnedIcon className="mr-2" />
{t("dialog_nodeDetails_button_requestPosition")}
{t("nodeDetails.requestPosition")}
</Button>
</div>
{node.deviceMetrics && (
<div className="text-slate-900 dark:text-slate-100 bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
<p className="text-lg font-semibold text-slate-900 dark:text-slate-100">
{t("dialog_nodeDetails_label_deviceMetrics")}
{t("nodeDetails.deviceMetrics")}
</p>
{deviceMetricsMap.map(
(metric) =>
@ -348,7 +351,7 @@ export const NodeDetailsDialog = ({
)}
{node.deviceMetrics.uptimeSeconds && (
<p>
{t("dialog_nodeDetails_label_uptime")}
{t("nodeDetails.uptime")}
<Uptime seconds={node.deviceMetrics.uptimeSeconds} />
</p>
)}
@ -361,7 +364,7 @@ export const NodeDetailsDialog = ({
<AccordionItem className="AccordionItem" value="item-1">
<AccordionTrigger>
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
{t("dialog_nodeDetails_label_allRawMetrics")}
{t("nodeDetails.allRawMetrics")}
</p>
</AccordionTrigger>
<AccordionContent className="overflow-x-scroll">

51
src/components/Dialog/PKIBackupDialog.tsx

@ -23,8 +23,8 @@ export const PkiBackupDialog = ({
open,
onOpenChange,
}: PkiBackupDialogProps) => {
const { t } = useTranslation();
const { config, setDialogOpen } = useDevice();
const { t } = useTranslation("dialog");
const { config, setDialogOpen, getMyNode } = useDevice();
const privateKey = config.security?.privateKey;
const publicKey = config.security?.publicKey;
@ -48,7 +48,12 @@ export const PkiBackupDialog = ({
printWindow.document.write(`
<html>
<head>
<title>${t("dialog_pkiBackup_print_header")}</title>
<title>${
t("pkiBackup.header", {
shortName: getMyNode()?.user?.shortName ?? t("unknown.shortName"),
longName: getMyNode()?.user?.longName ?? t("unknown.longName"),
})
}</title>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
h1 { font-size: 18px; }
@ -56,14 +61,18 @@ export const PkiBackupDialog = ({
</style>
</head>
<body>
<h1>${t("dialog_pkiBackup_print_header")}</h1>
<br>
<h2>${t("dialog_pkiBackup_print_label_publicKey")}</h2>
<h1>${
t("pkiBackup.header", {
shortName: getMyNode()?.user?.shortName ?? t("unknown.shortName"),
longName: getMyNode()?.user?.longName ?? t("unknown.longName"),
})
}</h1>
<h3>${t("pkiBackup.secureBackup")}</h3>
<h3>${t("pkiBackup.publicKey")}</h3>
<p>${decodeKeyData(publicKey)}</p>
<h2>${t("dialog_pkiBackup_print_label_privateKey")}</h2>
<h3>${t("pkiBackup.privateKey")}</h3>
<p>${decodeKeyData(privateKey)}</p>
<br>
<p>${t("dialog_pkiBackup_print_footer")}</p>
<p>${t("pkiBackup.footer")}</p>
</body>
</html>
`);
@ -80,12 +89,12 @@ export const PkiBackupDialog = ({
const decodedPublicKey = decodeKeyData(publicKey);
const formattedContent = [
`${t("dialog_pkiBackup_print_header")}\n\n`,
`${t("dialog_pkiBackup_print_label_privateKey")}\n`,
`${t("pkiBackup.header")}\n\n`,
`${t("pkiBackup.privateKey")}\n`,
decodedPrivateKey,
`\n\n${t("dialog_pkiBackup_print_label_publicKey")}\n`,
`\n\n${t("pkiBackup.publicKey")}\n`,
decodedPublicKey,
`\n\n${t("dialog_pkiBackup_print_footer")}`,
`\n\n${t("pkiBackup.footer")}`,
].join("");
const blob = new Blob([formattedContent], { type: "text/plain" });
@ -93,7 +102,11 @@ export const PkiBackupDialog = ({
const link = document.createElement("a");
link.href = url;
link.download = "meshtastic_keys.txt";
link.download = t("pkiBackup.fileName", {
shortName: getMyNode()?.user?.shortName ?? t("unknown.shortName"),
longName: getMyNode()?.user?.longName ?? t("unknown.longName"),
});
link.style.display = "none";
document.body.appendChild(link);
link.click();
@ -107,13 +120,13 @@ export const PkiBackupDialog = ({
<DialogContent>
<DialogClose />
<DialogHeader>
<DialogTitle>{t("dialog_pkiBackup_title")}</DialogTitle>
<DialogTitle>{t("pkiBackup.title")}</DialogTitle>
<DialogDescription>
{t("dialog_pkiBackup_description_secureBackup")}
{t("pkiBackup.secureBackup")}
</DialogDescription>
<DialogDescription>
<span className="font-bold break-before-auto">
{t("dialog_pkiBackup_description_loseKeysWarning")}
{t("pkiBackup.loseKeysWarning")}
</span>
</DialogDescription>
</DialogHeader>
@ -125,11 +138,11 @@ export const PkiBackupDialog = ({
className=""
>
<DownloadIcon size={20} className="mr-2" />
{t("dialog_button_download")}
{t("button.download")}
</Button>
<Button variant="default" onClick={() => renderPrintWindow()}>
<PrinterIcon size={20} className="mr-2" />
{t("dialog_button_print")}
{t("button.print")}
</Button>
</DialogFooter>
</DialogContent>

14
src/components/Dialog/PkiRegenerateDialog.tsx

@ -23,20 +23,20 @@ export interface PkiRegenerateDialogProps {
export const PkiRegenerateDialog = ({
text = {
title: "", // Default will be set by useTranslation
description: "", // Default will be set by useTranslation
button: "", // Default will be set by useTranslation
title: "",
description: "",
button: "",
},
open,
onOpenChange,
onSubmit,
}: PkiRegenerateDialogProps) => {
const { t } = useTranslation();
const { t } = useTranslation("dialog");
const dialogText = {
title: text.title || t("dialog_pkiRegenerate_title_keyPair"),
title: text.title || t("pkiRegenerate.title"),
description: text.description ||
t("dialog_pkiRegenerate_description_keyPair"),
button: text.button || t("dialog_pkiRegenerateDialog_buttonRegenerate"),
t("pkiRegenerate.description"),
button: text.button || t("button.regenerate"),
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>

19
src/components/Dialog/QRDialog.tsx

@ -31,7 +31,7 @@ export const QRDialog = ({
loraConfig,
channels,
}: QRDialogProps) => {
const { t } = useTranslation();
const { t } = useTranslation("dialog");
const [selectedChannels, setSelectedChannels] = useState<number[]>([0]);
const [qrCodeUrl, setQrCodeUrl] = useState<string>("");
const [qrCodeAdd, setQrCodeAdd] = useState<boolean>();
@ -67,9 +67,9 @@ export const QRDialog = ({
<DialogContent>
<DialogClose />
<DialogHeader>
<DialogTitle>{t("dialog_qr_title")}</DialogTitle>
<DialogTitle>{t("qr.title")}</DialogTitle>
<DialogDescription>
{t("dialog_qr_description")}
{t("qr.description")}
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
@ -81,9 +81,12 @@ export const QRDialog = ({
{channel.settings?.name.length
? channel.settings.name
: channel.role === Protobuf.Channel.Channel_Role.PRIMARY
? t("channel_name_primary")
? t("page.broadcastLabel", { ns: "channels" })
: `${
t("dialog_import_label_channelPrefix")
t("page.channelIndex", {
ns: "channels",
index: channel.index,
})
}${channel.index}`}
</Label>
<Checkbox
@ -120,7 +123,7 @@ export const QRDialog = ({
name="addChannels"
onClick={() => setQrCodeAdd(true)}
>
{t("dialog_qr_button_addChannels")}
{t("qr.addChannels")}
</button>
<button
type="button"
@ -132,12 +135,12 @@ export const QRDialog = ({
name="replaceChannels"
onClick={() => setQrCodeAdd(false)}
>
{t("dialog_qr_button_replaceChannels")}
{t("qr.replaceChannels")}
</button>
</div>
</div>
<DialogFooter>
<Label>{t("dialog_qr_label_sharableUrl")}</Label>
<Label>{t("qr.sharableUrl")}</Label>
<Input
value={qrCodeUrl}
disabled

8
src/components/Dialog/RebootDialog.tsx

@ -22,7 +22,7 @@ export const RebootDialog = ({
open,
onOpenChange,
}: RebootDialogProps) => {
const { t } = useTranslation();
const { t } = useTranslation("dialog");
const { connection } = useDevice();
const [time, setTime] = useState<number>(5);
@ -33,10 +33,10 @@ export const RebootDialog = ({
<DialogClose />
<DialogHeader>
<DialogTitle>
{t("command_palette_contextual_command_schedule_reboot")}
{t("reboot.title")}
</DialogTitle>
<DialogDescription>
{t("dialog_reboot_description")}
{t("reboot.description")}
</DialogDescription>
</DialogHeader>
<div className="flex gap-2 p-4">
@ -60,7 +60,7 @@ export const RebootDialog = ({
}}
>
<RefreshCwIcon className="mr-2" size={16} />
{t("dialog_button_now")}
{t("button.now")}
</Button>
</div>
</DialogContent>

15
src/components/Dialog/RebootOTADialog.tsx

@ -23,7 +23,7 @@ const DEFAULT_REBOOT_DELAY = 5; // seconds
export const RebootOTADialog = (
{ open, onOpenChange }: RebootOTADialogProps,
) => {
const { t } = useTranslation();
const { t } = useTranslation("dialog");
const { connection } = useDevice();
const [time, setTime] = useState<number>(DEFAULT_REBOOT_DELAY);
const [isScheduled, setIsScheduled] = useState(false);
@ -52,7 +52,6 @@ export const RebootOTADialog = (
await new Promise<void>((resolve) => {
setTimeout(() => {
console.log("Rebooting...");
resolve();
}, delay * 1000);
}).finally(() => {
@ -76,10 +75,10 @@ export const RebootOTADialog = (
<DialogClose />
<DialogHeader>
<DialogTitle>
{t("command_palette_contextual_command_reboot_to_ota_mode")}
{t("rebootOta.title")}
</DialogTitle>
<DialogDescription>
{t("dialog_rebootOta_description")}
{t("rebootOta.description")}
</DialogDescription>
</DialogHeader>
@ -91,7 +90,7 @@ export const RebootOTADialog = (
className="dark:text-slate-900 appearance-none"
value={inputValue}
onChange={handleSetTime}
placeholder={t("dialog_rebootOta_placeholder_enterDelay")}
placeholder={t("rebootOta.enterDelay")}
/>
<Button
onClick={() => handleRebootWithTimeout()}
@ -99,9 +98,7 @@ export const RebootOTADialog = (
className="w-9/12"
>
<ClockIcon className="mr-2" size={18} />
{isScheduled
? t("dialog_rebootOta_status_scheduled")
: t("command_palette_contextual_command_schedule_reboot")}
{isScheduled ? t("rebootOta.scheduled") : t("rebootOta.title")}
</Button>
</div>
@ -111,7 +108,7 @@ export const RebootOTADialog = (
onClick={() => handleInstantReboot()}
>
<RefreshCwIcon className="mr-2" size={16} />
{t("dialog_button_rebootOtaNow")}
{t("button.rebootOtaNow")}
</Button>
</DialogContent>
</Dialog>

20
src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx

@ -20,7 +20,7 @@ export interface RefreshKeysDialogProps {
export const RefreshKeysDialog = (
{ open, onOpenChange }: RefreshKeysDialogProps,
) => {
const { t } = useTranslation();
const { t } = useTranslation("dialog");
const { activeChat } = useMessageStore();
const { nodeErrors, getNode } = useDevice();
const { handleCloseDialog, handleNodeRemove } = useRefreshKeysDialog();
@ -34,13 +34,13 @@ export const RefreshKeysDialog = (
const nodeWithError = getNode(nodeErrorNum.node);
const text = {
title: `${t("dialog_refreshKeys_titlePrefix")}${
nodeWithError?.user?.longName ?? ""
}`,
description: `${t("dialog_refreshKeys_description_unableToSendDmPrefix")}${
title: t("refreshKeys.title", {
identifier: nodeWithError?.user?.longName ?? "",
}),
description: `${t("refreshKeys.description.unableToSendDmPrefix")}${
nodeWithError?.user?.longName ?? ""
} (${nodeWithError?.user?.shortName ?? ""})${
t("dialog_refreshKeys_description_keyMismatchReasonSuffix")
t("refreshKeys.description.keyMismatchReasonSuffix")
}`,
};
@ -66,10 +66,10 @@ export const RefreshKeysDialog = (
<div className="flex flex-col gap-2">
<div>
<p className="font-bold mb-0.5">
{t("dialog_refreshKeys_label_acceptNewKeys")}
{t("refreshKeys.label.acceptNewKeys")}
</p>
<p>
{t("dialog_refreshKeys_description_acceptNewKeys")}
{t("refreshKeys.description.acceptNewKeys")}
</p>
</div>
<Button
@ -77,14 +77,14 @@ export const RefreshKeysDialog = (
name="requestNewKeys"
onClick={handleNodeRemove}
>
{t("dialog_button_requestNewKeys")}
{t("button.requestNewKeys")}
</Button>
<Button
variant="outline"
name="dismiss"
onClick={handleCloseDialog}
>
{t("dialog_button_dismiss")}
{t("button.dismiss")}
</Button>
</div>
</li>

8
src/components/Dialog/RemoveNodeDialog.tsx

@ -22,7 +22,7 @@ export const RemoveNodeDialog = ({
open,
onOpenChange,
}: RemoveNodeDialogProps) => {
const { t } = useTranslation();
const { t } = useTranslation("dialog");
const { connection, getNode, removeNode } = useDevice();
const { nodeNumToBeRemoved } = useAppStore();
@ -37,9 +37,9 @@ export const RemoveNodeDialog = ({
<DialogContent>
<DialogClose />
<DialogHeader>
<DialogTitle>{t("dialog_removeNode_title")}</DialogTitle>
<DialogTitle>{t("removeNode.title")}</DialogTitle>
<DialogDescription>
{t("dialog_removeNode_description")}
{t("removeNode.description")}
</DialogDescription>
</DialogHeader>
<div className="gap-4">
@ -53,7 +53,7 @@ export const RemoveNodeDialog = ({
name="remove"
onClick={() => onSubmit()}
>
{t("dialog_button_remove")}
{t("button.remove")}
</Button>
</DialogFooter>
</DialogContent>

10
src/components/Dialog/ShutdownDialog.tsx

@ -22,7 +22,7 @@ export const ShutdownDialog = ({
open,
onOpenChange,
}: ShutdownDialogProps) => {
const { t } = useTranslation();
const { t } = useTranslation("dialog");
const { connection } = useDevice();
const [time, setTime] = useState<number>(5);
@ -33,10 +33,10 @@ export const ShutdownDialog = ({
<DialogClose />
<DialogHeader>
<DialogTitle>
{t("command_palette_contextual_command_schedule_shutdown")}
{t("shutdown.title")}
</DialogTitle>
<DialogDescription>
{t("dialog_shutdown_description")}
{t("shutdown.description")}
</DialogDescription>
</DialogHeader>
@ -45,7 +45,7 @@ export const ShutdownDialog = ({
type="number"
value={time}
onChange={(e) => setTime(Number.parseInt(e.target.value))}
suffix={t("dialog_shutdown_suffix_minutes")}
suffix={t("unit.minute.plural")}
/>
<Button
className="w-24"
@ -63,7 +63,7 @@ export const ShutdownDialog = ({
}}
>
<PowerIcon className="mr-2" size={16} />
{t("dialog_button_now")}
{t("button.now")}
</Button>
</div>
</DialogContent>

11
src/components/Dialog/TracerouteResponseDialog.tsx

@ -24,7 +24,7 @@ export const TracerouteResponseDialog = ({
open,
onOpenChange,
}: TracerouteResponseDialogProps) => {
const { t } = useTranslation();
const { t } = useTranslation("dialog");
const { getNode } = useDevice();
const route: number[] = traceroute?.data.route ?? [];
const routeBack: number[] = traceroute?.data.routeBack ?? [];
@ -32,11 +32,11 @@ export const TracerouteResponseDialog = ({
const snrBack = (traceroute?.data.snrBack ?? []).map((snr) => snr / 4);
const from = getNode(traceroute?.from ?? 0);
const longName = from?.user?.longName ??
(from ? `!${numberToHexUnpadded(from?.num)}` : t("common_unknown_short"));
(from ? `!${numberToHexUnpadded(from?.num)}` : t("unknown.shortName"));
const shortName = from?.user?.shortName ??
(from
? `${numberToHexUnpadded(from?.num).substring(0, 4)}`
: t("common_unknown_short"));
: t("unknown.shortName"));
const to = getNode(traceroute?.to ?? 0);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
@ -44,8 +44,9 @@ export const TracerouteResponseDialog = ({
<DialogClose />
<DialogHeader>
<DialogTitle>
{t("dialog_tracerouteResponse_titlePrefix")}
{longName} ({shortName})
{t("tracerouteResponse.title", {
identifier: `${longName} (${shortName})`,
})}
</DialogTitle>
</DialogHeader>
<DialogDescription>

20
src/components/Dialog/UnsafeRolesDialog/UnsafeRolesDialog.tsx

@ -23,7 +23,7 @@ export interface RouterRoleDialogProps {
export const UnsafeRolesDialog = (
{ open, onOpenChange }: RouterRoleDialogProps,
) => {
const { t } = useTranslation();
const { t } = useTranslation("dialog");
const [confirmState, setConfirmState] = useState(false);
const { setDialogOpen } = useDevice();
@ -43,18 +43,18 @@ export const UnsafeRolesDialog = (
<DialogContent className="max-w-8 flex flex-col">
<DialogClose onClick={() => handleCloseDialog("dismiss")} />
<DialogHeader>
<DialogTitle>{t("dialog_unsafeRoles_title")}</DialogTitle>
<DialogTitle>{t("unsafeRoles.title")}</DialogTitle>
</DialogHeader>
<DialogDescription className="text-md">
{t("dialog_unsafeRoles_description_preamble")}
{t("unsafeRoles.preamble")}
<Link href={deviceRoleLink} className="">
{t("dialog_unsafeRoles_link_deviceRoleDocumentation")}
{t("unsafeRoles.deviceRoleDocumentation")}
</Link>
{t("dialog_unsafeRoles_description_conjunction")}
{t("unsafeRoles.conjunction")}
<Link href={choosingTheRightDeviceRoleLink}>
{t("dialog_unsafeRoles_link_choosingRightDeviceRole")}
{t("unsafeRoles.choosingRightDeviceRole")}
</Link>
{t("dialog_unsafeRoles_description_postamble")}
{t("unsafeRoles.postamble")}
</DialogDescription>
<div className="flex items-center gap-2">
<Checkbox
@ -63,7 +63,7 @@ export const UnsafeRolesDialog = (
onChange={() => setConfirmState(!confirmState)}
name="confirmUnderstanding"
>
{t("dialog_unsafeRoles_checkbox_confirmUnderstanding")}
{t("unsafeRoles.confirmUnderstanding")}
</Checkbox>
</div>
<DialogFooter className="mt-6">
@ -72,7 +72,7 @@ export const UnsafeRolesDialog = (
name="dismiss"
onClick={() => handleCloseDialog("dismiss")}
>
{t("dialog_button_dismiss")}
{t("button.dismiss")}
</Button>
<Button
variant="default"
@ -80,7 +80,7 @@ export const UnsafeRolesDialog = (
disabled={!confirmState}
onClick={() => handleCloseDialog("confirm")}
>
{t("dialog_button_confirm")}
{t("button.confirm")}
</Button>
</DialogFooter>
</DialogContent>

2
src/components/Dialog/UnsafeRolesDialog/useUnsafeRolesDialog.ts

@ -2,7 +2,7 @@ import { useCallback } from "react";
import { eventBus } from "@core/utils/eventBus.ts";
import { useDevice } from "@core/stores/deviceStore.ts";
export const UNSAFE_ROLES = ["ROUTER", "REPEATER"];
export const UNSAFE_ROLES = ["ROUTER", "ROUTER_LATE", "REPEATER"];
export type UnsafeRole = typeof UNSAFE_ROLES[number];
export const useUnsafeRolesDialog = () => {

26
src/components/Form/FormMultiSelect.tsx

@ -24,7 +24,7 @@ export interface MultiSelectFieldProps<T> extends BaseFormBuilderProps<T> {
export function MultiSelectInput<T extends FieldValues>({
field,
}: GenericFormElementProps<T, MultiSelectFieldProps<T>>) {
const { t } = useTranslation();
const { t } = useTranslation("deviceConfig");
const { enumValue, ...remainingProperties } = field.properties;
const isNewConfigStructure =
@ -49,17 +49,19 @@ export function MultiSelectInput<T extends FieldValues>({
return (
<MultiSelect {...remainingProperties}>
{optionsToRender.map((option) => (
<MultiSelectItem
key={option.key}
name={option.key}
value={option.value.toString()}
checked={field.isChecked(option.key)}
onCheckedChange={() => field.onValueChange(option.key)}
>
{option.display}
</MultiSelectItem>
))}
{optionsToRender.map((option) => {
return (
<MultiSelectItem
key={option.key}
name={option.key}
value={option.value.toString()}
checked={field.isChecked(option.key)}
onCheckedChange={() => field.onValueChange(option.key)}
>
{option.display}
</MultiSelectItem>
);
})}
</MultiSelect>
);
}

4
src/components/KeyBackupReminder.tsx

@ -4,10 +4,10 @@ import { useTranslation } from "react-i18next";
export const KeyBackupReminder = () => {
const { setDialogOpen } = useDevice();
const { t } = useTranslation();
const { t } = useTranslation("dialog");
useBackupReminder({
message: t("dialog_pkiBackup_message"),
message: t("pkiBackup.description"),
onAccept: () => setDialogOpen("pkiBackup", true),
enabled: true,
});

60
src/components/LanguageSwitcher.tsx

@ -0,0 +1,60 @@
import clsx from "clsx";
import { useTranslation } from "react-i18next";
import useLang from "@core/hooks/useLang.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();
return (
<DropdownMenu
i18nIsDynamicList={true}
data-testid="language-list"
>
<DropdownMenuTrigger>asdlkfj</DropdownMenuTrigger>
<DropdownMenuContent className="flex border border-b-2 border-b-gray-700 flex-col w-full items-stretch gap-1">
{supportedLanguages?.map((lang) => (
<li
key={lang.code}
className="list-none w-full"
data-testid={`language-item-${lang.code}`}
>
<Button
role="presentation"
className={clsx([
"flex flex-row justify-between items-center w-full",
"rounded-lg px-3 py-1 transition duration-75 cursor-default",
])}
onClick={() => set(lang.code)}
data-testid={`language-button-${lang.code}`}
data-selected={i18n.language === lang.code}
>
<div
className="flex flex-row items-center m-1 gap-3 p-1 overflow-hidden"
data-testid={`language-name-${lang.code}`}
>
{lang.name}
</div>
<div
className={clsx([
"pr-4 ml-2 flex items-center font-medium text-xs",
"text-iron dark:text-bombay",
])}
data-testid={`language-selected-indicator-${lang.code}`}
>
{i18n.language === lang.code &&
t("selected", { ns: "glossary" })}
</div>
</Button>
</li>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}
export default LanguageSwitcher;

103
src/components/PageComponents/Channel.tsx

@ -16,7 +16,7 @@ export interface SettingsPanelProps {
export const Channel = ({ channel }: SettingsPanelProps) => {
const { config, connection, addChannel } = useDevice();
const { t } = useTranslation();
const { t } = useTranslation(["channels", "ui", "dialog"]);
const { toast } = useToast();
const [pass, setPass] = useState<string>(
@ -44,7 +44,8 @@ export const Channel = ({ channel }: SettingsPanelProps) => {
});
connection?.setChannel(channel).then(() => {
toast({
title: t("channel_form_toast_saved", {
title: t("toast.savedChannel", {
ns: "ui",
channelName: channel.settings?.name,
}),
});
@ -72,7 +73,7 @@ export const Channel = ({ channel }: SettingsPanelProps) => {
const validatePass = (input: string, count: number) => {
if (input.length % 4 !== 0 || toByteArray(input).length !== count) {
setValidationText(
t("channel_form_validation_psk_invalid", { bits: count * 8 }),
t("validation.pskInvalid", { bits: count * 8 }),
);
} else {
setValidationText(undefined);
@ -116,21 +117,21 @@ export const Channel = ({ channel }: SettingsPanelProps) => {
}}
fieldGroups={[
{
label: t("channel_form_field_group_settings_label"),
description: t("channel_form_field_group_settings_description"),
label: t("settings.label"),
description: t("settings.description"),
fields: [
{
type: "select",
name: "role",
label: t("channel_form_field_role_label"),
label: t("role.label"),
disabled: channel.index === 0,
description: t("channel_form_field_role_description"),
description: t("role.description"),
properties: {
enumValue: channel.index === 0
? { [t("channel_form_field_role_option_primary")]: 1 }
? { [t("role.options.primary")]: 1 }
: {
[t("channel_form_field_role_option_disabled")]: 0,
[t("channel_form_field_role_option_secondary")]: 2,
[t("role.options.disabled")]: 0,
[t("role.options.secondary")]: 2,
},
},
},
@ -138,15 +139,15 @@ export const Channel = ({ channel }: SettingsPanelProps) => {
type: "passwordGenerator",
name: "settings.psk",
id: "channel-psk",
label: t("channel_form_field_psk_label"),
description: t("channel_form_field_psk_description"),
label: t("psk.label"),
description: t("psk.description"),
validationText: validationText,
devicePSKBitCount: bitCount ?? 0,
inputChange: inputChangeEvent,
selectChange: selectChangeEvent,
actionButtons: [
{
text: t("channel_form_field_psk_generate_button"),
text: t("psk.generate"),
variant: "success",
onClick: preSharedClickEvent,
},
@ -161,104 +162,98 @@ export const Channel = ({ channel }: SettingsPanelProps) => {
{
type: "text",
name: "settings.name",
label: t("channel_form_field_name_label"),
description: t("channel_form_field_name_description"),
label: t("name.label"),
description: t("name.description"),
},
{
type: "toggle",
name: "settings.uplinkEnabled",
label: t("channel_form_field_uplink_enabled_label"),
description: t("channel_form_field_uplink_enabled_description"),
label: t("uplinkEnabled.label"),
description: t("uplinkEnabled.description"),
},
{
type: "toggle",
name: "settings.downlinkEnabled",
label: t("channel_form_field_downlink_enabled_label"),
description: t(
"channel_form_field_downlink_enabled_description",
),
label: t("downlinkEnabled.label"),
description: t("downlinkEnabled.description"),
},
{
type: "select",
name: "settings.moduleSettings.positionPrecision",
label: t("channel_form_field_position_precision_label"),
description: t(
"channel_form_field_position_precision_description",
),
label: t("positionPrecision.label"),
description: t("positionPrecision.description"),
properties: {
enumValue: config.display?.units === 0
? {
[t("channel_form_field_position_precision_option_none")]:
0,
[t("positionPrecision.options.none")]: 0,
[
t("channel_form_field_position_precision_option_metric_km23")
t("positionPrecision.options.metric_km23")
]: 10,
[
t("channel_form_field_position_precision_option_metric_km12")
t("positionPrecision.options.metric_km12")
]: 11,
[
t("channel_form_field_position_precision_option_metric_km5_8")
t("positionPrecision.options.metric_km5_8")
]: 12,
[
t("channel_form_field_position_precision_option_metric_km2_9")
t("positionPrecision.options.metric_km2_9")
]: 13,
[
t("channel_form_field_position_precision_option_metric_km1_5")
t("positionPrecision.options.metric_km1_5")
]: 14,
[
t("channel_form_field_position_precision_option_metric_m700")
t("positionPrecision.options.metric_m700")
]: 15,
[
t("channel_form_field_position_precision_option_metric_m350")
t("positionPrecision.options.metric_m350")
]: 16,
[
t("channel_form_field_position_precision_option_metric_m200")
t("positionPrecision.options.metric_m200")
]: 17,
[
t("channel_form_field_position_precision_option_metric_m90")
t("positionPrecision.options.metric_m90")
]: 18,
[
t("channel_form_field_position_precision_option_metric_m50")
t("positionPrecision.options.metric_m50")
]: 19,
[
t("channel_form_field_position_precision_option_precise")
t("positionPrecision.options.precise")
]: 32,
}
: {
[t("channel_form_field_position_precision_option_none")]:
0,
[t("positionPrecision.options.none")]: 0,
[
t("channel_form_field_position_precision_option_imperial_mi15")
t("positionPrecision.options.imperial_mi15")
]: 10,
[
t("channel_form_field_position_precision_option_imperial_mi7_3")
t("positionPrecision.options.imperial_mi7_3")
]: 11,
[
t("channel_form_field_position_precision_option_imperial_mi3_6")
t("positionPrecision.options.imperial_mi3_6")
]: 12,
[
t("channel_form_field_position_precision_option_imperial_mi1_8")
t("positionPrecision.options.imperial_mi1_8")
]: 13,
[
t("channel_form_field_position_precision_option_imperial_mi0_9")
t("positionPrecision.options.imperial_mi0_9")
]: 14,
[
t("channel_form_field_position_precision_option_imperial_mi0_5")
t("positionPrecision.options.imperial_mi0_5")
]: 15,
[
t("channel_form_field_position_precision_option_imperial_mi0_2")
t("positionPrecision.options.imperial_mi0_2")
]: 16,
[
t("channel_form_field_position_precision_option_imperial_ft600")
t("positionPrecision.options.imperial_ft600")
]: 17,
[
t("channel_form_field_position_precision_option_imperial_ft300")
t("positionPrecision.options.imperial_ft300")
]: 18,
[
t("channel_form_field_position_precision_option_imperial_ft150")
t("positionPrecision.options.imperial_ft150")
]: 19,
[
t("channel_form_field_position_precision_option_precise")
t("positionPrecision.options.precise")
]: 32,
},
},
@ -269,9 +264,9 @@ export const Channel = ({ channel }: SettingsPanelProps) => {
/>
<PkiRegenerateDialog
text={{
button: t("dialog_pkiRegenerateDialog_buttonRegenerate"),
title: t("dialog_pkiRegenerateDialog_title"),
description: t("dialog_pkiRegenerateDialog_description"),
button: t("pkiRegenerateDialog.regenerate", { ns: "dialog" }),
title: t("pkiRegenerateDialog.title", { ns: "dialog" }),
description: t("pkiRegenerateDialog.description", { ns: "dialog" }),
}}
open={preSharedDialogOpen}
onOpenChange={() => setPreSharedDialogOpen(false)}

238
src/components/PageComponents/Config/Bluetooth.tsx

@ -8,133 +8,131 @@ import { useState } from "react";
import { useTranslation } from "react-i18next";
export const Bluetooth = () => {
const { config, setWorkingConfig } = useDevice();
const {
hasErrors,
getErrorMessage,
hasFieldError,
addError,
removeError,
clearErrors,
} = useAppStore();
const { t } = useTranslation();
const { config, setWorkingConfig } = useDevice();
const {
hasErrors,
getErrorMessage,
hasFieldError,
addError,
removeError,
clearErrors,
} = useAppStore();
const { t } = useTranslation("deviceConfig");
const [bluetoothPin, setBluetoothPin] = useState(
config?.bluetooth?.fixedPin.toString() ?? "",
);
const [bluetoothPin, setBluetoothPin] = useState(
config?.bluetooth?.fixedPin.toString() ?? "",
);
const validateBluetoothPin = (pin: string) => {
// if empty show error they need a pin set
if (pin === "") {
return addError("fixedPin", t("config_bluetooth_validation_pinRequired"));
}
const validateBluetoothPin = (pin: string) => {
// if empty show error they need a pin set
if (pin === "") {
return addError("fixedPin", t("bluetooth.validation.pinRequired"));
}
// clear any existing errors
clearErrors();
// clear any existing errors
clearErrors();
// if it starts with 0 show error
if (pin[0] === "0") {
return addError(
"fixedPin",
t("config_bluetooth_validation_pinCannotStartWithZero"),
);
}
// if it's not 6 digits show error
if (pin.length < 6) {
return addError(
"fixedPin",
t("config_bluetooth_validation_pinMustBeSixDigits"),
);
}
// if it starts with 0 show error
if (pin[0] === "0") {
return addError(
"fixedPin",
t("bluetooth.validation.pinCannotStartWithZero"),
);
}
// if it's not 6 digits show error
if (pin.length < 6) {
return addError("fixedPin", t("bluetooth.validation.pinMustBeSixDigits"));
}
removeError("fixedPin");
};
removeError("fixedPin");
};
const bluetoothPinChangeEvent = (e: React.ChangeEvent<HTMLInputElement>) => {
const numericValue = e.target.value.replace(/\D/g, "").slice(0, 6);
setBluetoothPin(numericValue);
validateBluetoothPin(numericValue);
};
const bluetoothPinChangeEvent = (e: React.ChangeEvent<HTMLInputElement>) => {
const numericValue = e.target.value.replace(/\D/g, "").slice(0, 6);
setBluetoothPin(numericValue);
validateBluetoothPin(numericValue);
};
const onSubmit = (data: BluetoothValidation) => {
if (hasErrors()) {
return;
}
const onSubmit = (data: BluetoothValidation) => {
if (hasErrors()) {
return;
}
setWorkingConfig(
create(Protobuf.Config.ConfigSchema, {
payloadVariant: {
case: "bluetooth",
value: data,
},
}),
);
};
setWorkingConfig(
create(Protobuf.Config.ConfigSchema, {
payloadVariant: {
case: "bluetooth",
value: data,
},
}),
);
};
return (
<DynamicForm<BluetoothValidation>
onSubmit={onSubmit}
defaultValues={config.bluetooth}
fieldGroups={[
{
label: t("config_bluetooth_groupLabel_bluetoothSettings"),
description: t("config_bluetooth_groupDescription_bluetoothSettings"),
notes: t("config_bluetooth_groupNotes_bluetoothWifiNote"),
fields: [
{
type: "toggle",
name: "enabled",
label: t("config_bluetooth_fieldLabel_enabled"),
description: t("config_bluetooth_fieldDescription_enabled"),
},
{
type: "select",
name: "mode",
label: t("config_bluetooth_fieldLabel_pairingMode"),
description: t("config_bluetooth_fieldDescription_pairingMode"),
selectChange: (e) => {
if (e !== "1") {
setBluetoothPin("");
removeError("fixedPin");
}
},
disabledBy: [
{
fieldName: "enabled",
},
],
properties: {
enumValue: Protobuf.Config.Config_BluetoothConfig_PairingMode,
formatEnumName: true,
},
},
{
type: "number",
name: "fixedPin",
label: t("config_bluetooth_fieldLabel_pin"),
description: t("config_bluetooth_fieldDescription_pin"),
validationText: hasFieldError("fixedPin")
? getErrorMessage("fixedPin")
: "",
inputChange: bluetoothPinChangeEvent,
disabledBy: [
{
fieldName: "mode",
selector: Protobuf.Config.Config_BluetoothConfig_PairingMode
.FIXED_PIN,
invert: true,
},
{
fieldName: "enabled",
},
],
properties: {
value: bluetoothPin,
},
},
],
},
]}
/>
);
return (
<DynamicForm<BluetoothValidation>
onSubmit={onSubmit}
defaultValues={config.bluetooth}
fieldGroups={[
{
label: t("bluetooth.title"),
description: t("bluetooth.description"),
notes: t("bluetooth.note"),
fields: [
{
type: "toggle",
name: "enabled",
label: t("bluetooth.enabled.label"),
description: t("bluetooth.enabled.description"),
},
{
type: "select",
name: "mode",
label: t("bluetooth.pairingMode.label"),
description: t("bluetooth.pairingMode.description"),
selectChange: (e) => {
if (e !== "1") {
setBluetoothPin("");
removeError("fixedPin");
}
},
disabledBy: [
{
fieldName: "enabled",
},
],
properties: {
enumValue: Protobuf.Config.Config_BluetoothConfig_PairingMode,
formatEnumName: true,
},
},
{
type: "number",
name: "fixedPin",
label: t("bluetooth.pin.label"),
description: t("bluetooth.pin.description"),
validationText: hasFieldError("fixedPin")
? getErrorMessage("fixedPin")
: "",
inputChange: bluetoothPinChangeEvent,
disabledBy: [
{
fieldName: "mode",
selector:
Protobuf.Config.Config_BluetoothConfig_PairingMode
.FIXED_PIN,
invert: true,
},
{
fieldName: "enabled",
},
],
properties: {
value: bluetoothPin,
},
},
],
},
]}
/>
);
};

52
src/components/PageComponents/Config/Device/index.tsx

@ -8,7 +8,7 @@ import { useTranslation } from "react-i18next";
export const Device = () => {
const { config, setWorkingConfig } = useDevice();
const { t } = useTranslation();
const { t } = useTranslation("deviceConfig");
const { validateRoleSelection } = useUnsafeRolesDialog();
const onSubmit = (data: DeviceValidation) => {
@ -27,14 +27,14 @@ export const Device = () => {
defaultValues={config.device}
fieldGroups={[
{
label: t("config_device_groupLabel_deviceSettings"),
description: t("config_device_groupDescription_deviceSettings"),
label: t("device.title"),
description: t("device.description"),
fields: [
{
type: "select",
name: "role",
label: t("config_device_fieldLabel_role"),
description: t("config_device_fieldDescription_role"),
label: t("device.role.label"),
description: t("device.role.description"),
validate: validateRoleSelection,
properties: {
enumValue: Protobuf.Config.Config_DeviceConfig_Role,
@ -44,20 +44,20 @@ export const Device = () => {
{
type: "number",
name: "buttonGpio",
label: t("config_device_fieldLabel_buttonPin"),
description: t("config_device_fieldDescription_buttonPin"),
label: t("device.buttonPin.label"),
description: t("device.buttonPin.description"),
},
{
type: "number",
name: "buzzerGpio",
label: t("config_device_fieldLabel_buzzerPin"),
description: t("config_device_fieldDescription_buzzerPin"),
label: t("device.buzzerPin.label"),
description: t("device.buzzerPin.description"),
},
{
type: "select",
name: "rebroadcastMode",
label: t("config_device_fieldLabel_rebroadcastMode"),
description: t("config_device_fieldDescription_rebroadcastMode"),
label: t("device.rebroadcastMode.label"),
description: t("device.rebroadcastMode.description"),
properties: {
enumValue: Protobuf.Config.Config_DeviceConfig_RebroadcastMode,
formatEnumName: true,
@ -66,35 +66,29 @@ export const Device = () => {
{
type: "number",
name: "nodeInfoBroadcastSecs",
label: t("config_device_fieldLabel_nodeInfoBroadcastInterval"),
description: t(
"config_device_fieldDescription_nodeInfoBroadcastInterval",
),
label: t("device.nodeInfoBroadcastInterval.label"),
description: t("device.nodeInfoBroadcastInterval.description"),
properties: {
suffix: t("common_unit_seconds"),
suffix: t("unit.second.plural"),
},
},
{
type: "toggle",
name: "doubleTapAsButtonPress",
label: t("config_device_fieldLabel_doubleTapAsButtonPress"),
description: t(
"config_device_fieldDescription_doubleTapAsButtonPress",
),
label: t("device.doubleTapAsButtonPress.label"),
description: t("device.doubleTapAsButtonPress.description"),
},
{
type: "toggle",
name: "disableTripleClick",
label: t("config_device_fieldLabel_disableTripleClick"),
description: t(
"config_device_fieldDescription_disableTripleClick",
),
label: t("device.disableTripleClick.label"),
description: t("device.disableTripleClick.description"),
},
{
type: "text",
name: "tzdef",
label: t("config_device_fieldLabel_posixTimezone"),
description: t("config_device_fieldDescription_posixTimezone"),
label: t("device.posixTimezone.label"),
description: t("device.posixTimezone.description"),
properties: {
fieldLength: {
max: 64,
@ -106,10 +100,8 @@ export const Device = () => {
{
type: "toggle",
name: "ledHeartbeatDisabled",
label: t("config_device_fieldLabel_ledHeartbeatDisabled"),
description: t(
"config_device_fieldDescription_ledHeartbeatDisabled",
),
label: t("device.ledHeartbeatDisabled.label"),
description: t("device.ledHeartbeatDisabled.description"),
},
],
},

58
src/components/PageComponents/Config/Display.tsx

@ -7,7 +7,7 @@ import { useTranslation } from "react-i18next";
export const Display = () => {
const { config, setWorkingConfig } = useDevice();
const { t } = useTranslation();
const { t } = useTranslation("deviceConfig");
const onSubmit = (data: DisplayValidation) => {
setWorkingConfig(
@ -26,23 +26,23 @@ export const Display = () => {
defaultValues={config.display}
fieldGroups={[
{
label: t("config_display_groupLabel_displaySettings"),
description: t("config_display_groupDescription_displaySettings"),
label: t("display.title"),
description: t("display.description"),
fields: [
{
type: "number",
name: "screenOnSecs",
label: t("config_display_fieldLabel_screenTimeout"),
description: t("config_display_fieldDescription_screenTimeout"),
label: t("display.screenTimeout.label"),
description: t("display.screenTimeout.description"),
properties: {
suffix: t("common_unit_seconds"),
suffix: t("unit.second.plural"),
},
},
{
type: "select",
name: "gpsFormat",
label: t("config_display_fieldLabel_gpsDisplayUnits"),
description: t("config_display_fieldDescription_gpsDisplayUnits"),
label: t("display.gpsDisplayUnits.label"),
description: t("display.gpsDisplayUnits.description"),
properties: {
enumValue:
Protobuf.Config.Config_DisplayConfig_GpsCoordinateFormat,
@ -51,35 +51,35 @@ export const Display = () => {
{
type: "number",
name: "autoScreenCarouselSecs",
label: t("config_display_fieldLabel_carouselDelay"),
description: t("config_display_fieldDescription_carouselDelay"),
label: t("display.carouselDelay.label"),
description: t("display.carouselDelay.description"),
properties: {
suffix: t("common_unit_seconds"),
suffix: t("unit.second.plural"),
},
},
{
type: "toggle",
name: "compassNorthTop",
label: t("config_display_fieldLabel_compassNorthTop"),
description: t("config_display_fieldDescription_compassNorthTop"),
label: t("display.compassNorthTop.label"),
description: t("display.compassNorthTop.description"),
},
{
type: "toggle",
name: "use12hClock",
label: t("config_display_fieldLabel_twelveHourClock"),
description: t("config_display_fieldDescription_twelveHourClock"),
label: t("display.twelveHourClock.label"),
description: t("display.twelveHourClock.description"),
},
{
type: "toggle",
name: "flipScreen",
label: t("config_display_fieldLabel_flipScreen"),
description: t("config_display_fieldDescription_flipScreen"),
label: t("display.flipScreen.label"),
description: t("display.flipScreen.description"),
},
{
type: "select",
name: "units",
label: t("config_display_fieldLabel_displayUnits"),
description: t("config_display_fieldDescription_displayUnits"),
label: t("display.displayUnits.label"),
description: t("display.displayUnits.description"),
properties: {
enumValue: Protobuf.Config.Config_DisplayConfig_DisplayUnits,
formatEnumName: true,
@ -88,17 +88,17 @@ export const Display = () => {
{
type: "select",
name: "oled",
label: t("config_display_fieldLabel_oledType"),
description: t("config_display_fieldDescription_oledType"),
label: t("display.oledType.label"),
description: t("display.oledType.description"),
properties: {
enumValue: Protobuf.Config.Config_DisplayConfig_OledType,
enumValue: Protobuf.Config.Config_Displayjonfig_OledType,
},
},
{
type: "select",
name: "displaymode",
label: t("config_display_fieldLabel_displayMode"),
description: t("config_display_fieldDescription_displayMode"),
label: t("display.displayMode.label"),
description: t("display.displayMode.descriptio"),
properties: {
enumValue: Protobuf.Config.Config_DisplayConfig_DisplayMode,
formatEnumName: true,
@ -107,16 +107,14 @@ export const Display = () => {
{
type: "toggle",
name: "headingBold",
label: t("config_display_fieldLabel_boldHeading"),
description: t("config_display_fieldDescription_boldHeading"),
label: t("display.headingBold.label"),
description: t("display.headingBold.description"),
},
{
type: "toggle",
name: "wakeOnTapOrMotion",
label: t("config_display_fieldLabel_wakeOnTapOrMotion"),
description: t(
"config_display_fieldDescription_wakeOnTapOrMotion",
),
label: t("display.wakeOnTapOrMotion.label"),
description: t("display.wakeOnTapOrMotion.description"),
},
],
},

88
src/components/PageComponents/Config/LoRa.tsx

@ -7,7 +7,7 @@ import { useTranslation } from "react-i18next";
export const LoRa = () => {
const { config, setWorkingConfig } = useDevice();
const { t } = useTranslation();
const { t } = useTranslation("deviceConfig");
const onSubmit = (data: LoRaValidation) => {
setWorkingConfig(
@ -26,14 +26,14 @@ export const LoRa = () => {
defaultValues={config.lora}
fieldGroups={[
{
label: t("config_lora_groupLabel_meshSettings"),
description: t("config_lora_groupDescription_meshSettings"),
label: t("lora.title"),
description: t("lora.description"),
fields: [
{
type: "select",
name: "region",
label: t("config_lora_fieldLabel_region"),
description: t("config_lora_fieldDescription_region"),
label: t("lora.region.label"),
description: t("lora.region.description"),
properties: {
enumValue: Protobuf.Config.Config_LoRaConfig_RegionCode,
},
@ -41,8 +41,8 @@ export const LoRa = () => {
{
type: "select",
name: "hopLimit",
label: t("config_lora_fieldLabel_hopLimit"),
description: t("config_lora_fieldDescription_hopLimit"),
label: t("lora.hopLimit.label"),
description: t("lora.hopLimit.description"),
properties: {
enumValue: { 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7 },
},
@ -50,38 +50,38 @@ export const LoRa = () => {
{
type: "number",
name: "channelNum",
label: t("config_lora_fieldLabel_frequencySlot"),
description: t("config_lora_fieldDescription_frequencySlot"),
label: t("lora.frequencySlot.label"),
description: t("lora.frequencySlot.description"),
},
{
type: "toggle",
name: "ignoreMqtt",
label: t("config_lora_fieldLabel_ignoreMqtt"),
description: t("config_lora_fieldDescription_ignoreMqtt"),
label: t("lora.ignoreMqtt.label"),
description: t("lora.ignoreMqtt.description"),
},
{
type: "toggle",
name: "configOkToMqtt",
label: t("config_lora_fieldLabel_okToMqtt"),
description: t("config_lora_fieldDescription_okToMqtt"),
label: t("lora.okToMqtt.label"),
description: t("lora.okToMqtt.description"),
},
],
},
{
label: t("config_lora_groupLabel_waveformSettings"),
description: t("config_lora_groupDescription_waveformSettings"),
label: t("lora.waveformSettings.label"),
description: t("lora.waveformSettings.description"),
fields: [
{
type: "toggle",
name: "usePreset",
label: t("config_lora_fieldLabel_usePreset"),
description: t("config_lora_fieldDescription_usePreset"),
label: t("lora.usePreset.label"),
description: t("lora.usePreset.description"),
},
{
type: "select",
name: "modemPreset",
label: t("config_lora_fieldLabel_modemPreset"),
description: t("config_lora_fieldDescription_modemPreset"),
label: t("lora.modemPreset.label"),
description: t("lora.modemPreset.description"),
disabledBy: [
{
fieldName: "usePreset",
@ -95,8 +95,8 @@ export const LoRa = () => {
{
type: "number",
name: "bandwidth",
label: t("config_lora_fieldLabel_bandwidth"),
description: t("config_lora_fieldDescription_bandwidth"),
label: t("lora.bandwidth.label"),
description: t("lora.bandwidth.description"),
disabledBy: [
{
fieldName: "usePreset",
@ -104,14 +104,14 @@ export const LoRa = () => {
},
],
properties: {
suffix: t("common_unit_megahertz"),
suffix: t("unit.megahertz"),
},
},
{
type: "number",
name: "spreadFactor",
label: t("config_lora_fieldLabel_spreadingFactor"),
description: t("config_lora_fieldDescription_spreadingFactor"),
label: t("lora.spreadingFactor.label"),
description: t("lora.spreadingFactor.description"),
disabledBy: [
{
@ -120,14 +120,14 @@ export const LoRa = () => {
},
],
properties: {
suffix: t("common_unit_cps"),
suffix: t("unit.cps"),
},
},
{
type: "number",
name: "codingRate",
label: t("config_lora_fieldLabel_codingRate"),
description: t("config_lora_fieldDescription_codingRate"),
label: t("lora.codingRate.label"),
description: t("lora.codingRate.description"),
disabledBy: [
{
fieldName: "usePreset",
@ -138,52 +138,52 @@ export const LoRa = () => {
],
},
{
label: t("config_lora_groupLabel_radioSettings"),
description: t("config_lora_groupDescription_radioSettings"),
label: t("lora.radioSettings.label"),
description: t("lora.radioSettings.description"),
fields: [
{
type: "toggle",
name: "txEnabled",
label: t("config_lora_fieldLabel_transmitEnabled"),
description: t("config_lora_fieldDescription_transmitEnabled"),
label: t("lora.transmitEnabled.label"),
description: t("lora.transmitEnabled.description"),
},
{
type: "number",
name: "txPower",
label: t("config_lora_fieldLabel_transmitPower"),
description: t("config_lora_fieldDescription_transmitPower"),
label: t("lora.transmitPower.label"),
description: t("lora.transmitPower.description"),
properties: {
suffix: t("common_unit_dbm"),
suffix: t("unit.dbm"),
},
},
{
type: "toggle",
name: "overrideDutyCycle",
label: t("config_lora_fieldLabel_overrideDutyCycle"),
description: t("config_lora_fieldDescription_overrideDutyCycle"),
label: t("lora.overrideDutyCycle.label"),
description: t("lora.overrideDutyCycle.description"),
},
{
type: "number",
name: "frequencyOffset",
label: t("config_lora_fieldLabel_frequencyOffset"),
description: t("config_lora_fieldDescription_frequencyOffset"),
label: t("lora.frequencyOffset.label"),
description: t("lora.frequencyOffset.description"),
properties: {
suffix: t("common_unit_hertz"),
suffix: t("unit.hertz"),
},
},
{
type: "toggle",
name: "sx126xRxBoostedGain",
label: t("config_lora_fieldLabel_boostedRxGain"),
description: t("config_lora_fieldDescription_boostedRxGain"),
label: t("lora.boostedRxGain.label"),
description: t("lora.boostedRxGain.description"),
},
{
type: "number",
name: "overrideFrequency",
label: t("config_lora_fieldLabel_overrideFrequency"),
description: t("config_lora_fieldDescription_overrideFrequency"),
label: t("lora.overrideFrequency.label"),
description: t("lora.overrideFrequency.description"),
properties: {
suffix: t("common_unit_megahertz"),
suffix: t("unit.megahertz"),
step: 0.001,
},
},

70
src/components/PageComponents/Config/Network/index.tsx

@ -15,7 +15,7 @@ import { useTranslation } from "react-i18next";
export const Network = () => {
const { config, setWorkingConfig } = useDevice();
const { t } = useTranslation();
const { t } = useTranslation("deviceConfig");
const onSubmit = (data: NetworkValidation) => {
const result = validateSchema(NetworkValidationSchema, data);
@ -65,21 +65,21 @@ export const Network = () => {
}}
fieldGroups={[
{
label: t("config_network_groupLabel_wifiConfig"),
description: t("config_network_groupDescription_wifiConfig"),
notes: t("config_network_groupNotes_wifiBluetoothNote"),
label: t("network.title"),
description: t("network.description"),
notes: t("network.note"),
fields: [
{
type: "toggle",
name: "wifiEnabled",
label: t("config_network_fieldLabel_wifiEnabled"),
description: t("config_network_fieldDescription_wifiEnabled"),
label: t("network.wifiEnabled.label"),
description: t("network.wifiEnabled.description"),
},
{
type: "text",
name: "wifiSsid",
label: t("config_network_fieldLabel_ssid"),
description: t("config_network_fieldDescription_ssid"),
label: t("network.ssid.label"),
description: t("network.ssid.label"),
disabledBy: [
{
fieldName: "wifiEnabled",
@ -89,8 +89,8 @@ export const Network = () => {
{
type: "password",
name: "wifiPsk",
label: t("config_network_fieldLabel_psk"),
description: t("config_network_fieldDescription_psk"),
label: t("network.psk.label"),
description: t("network.psk.description"),
disabledBy: [
{
fieldName: "wifiEnabled",
@ -100,26 +100,26 @@ export const Network = () => {
],
},
{
label: t("config_network_groupLabel_ethernetConfig"),
description: t("config_network_groupDescription_ethernetConfig"),
label: t("network.ethernetConfigSettings.label"),
description: t("network.ethernetConfigSettings.description"),
fields: [
{
type: "toggle",
name: "ethEnabled",
label: t("config_network_fieldLabel_ethernetEnabled"),
description: t("config_network_fieldDescription_ethernetEnabled"),
label: t("network.ethernetEnabled.label"),
description: t("network.ethernetEnabled.description"),
},
],
},
{
label: t("config_network_groupLabel_ipConfig"),
description: t("config_network_groupDescription_ipConfig"),
label: t("network.ipConfigSettings.label"),
description: t("network.ipConfigSettings.description"),
fields: [
{
type: "select",
name: "addressMode",
label: t("config_network_fieldLabel_addressMode"),
description: t("config_network_fieldDescription_addressMode"),
label: t("network.addressMode.label"),
description: t("network.addressMode.description"),
properties: {
enumValue: Protobuf.Config.Config_NetworkConfig_AddressMode,
},
@ -127,8 +127,8 @@ export const Network = () => {
{
type: "text",
name: "ipv4Config.ip",
label: t("config_network_fieldLabel_ip"),
description: t("config_network_fieldDescription_ip"),
label: t("network.ip.label"),
description: t("network.ip.description"),
disabledBy: [
{
fieldName: "addressMode",
@ -140,8 +140,8 @@ export const Network = () => {
{
type: "text",
name: "ipv4Config.gateway",
label: t("config_network_fieldLabel_gateway"),
description: t("config_network_fieldDescription_gateway"),
label: t("network.gateway.label"),
description: t("network.gateway.description"),
disabledBy: [
{
fieldName: "addressMode",
@ -153,8 +153,8 @@ export const Network = () => {
{
type: "text",
name: "ipv4Config.subnet",
label: t("config_network_fieldLabel_subnet"),
description: t("config_network_fieldDescription_subnet"),
label: t("network.subnet.label"),
description: t("network.subnet.description"),
disabledBy: [
{
fieldName: "addressMode",
@ -166,8 +166,8 @@ export const Network = () => {
{
type: "text",
name: "ipv4Config.dns",
label: t("config_network_fieldLabel_dns"),
description: t("config_network_fieldDescription_dns"),
label: t("network.dns.label"),
description: t("network.dns.description"),
disabledBy: [
{
fieldName: "addressMode",
@ -179,13 +179,13 @@ export const Network = () => {
],
},
{
label: t("config_network_groupLabel_udpConfig"),
description: t("config_network_groupDescription_udpConfig"),
label: t("network.udpConfigSettings.label"),
description: t("network.udpConfigSettings.description"),
fields: [
{
type: "select",
name: "enabledProtocols",
label: t("config_network_fieldLabel_meshViaUdp"),
label: t("network.meshViaUdp.label"),
properties: {
enumValue: Protobuf.Config.Config_NetworkConfig_ProtocolFlags,
formatEnumName: true,
@ -194,24 +194,24 @@ export const Network = () => {
],
},
{
label: t("config_network_groupLabel_ntpConfig"),
description: t("config_network_groupDescription_ntpConfig"),
label: t("network.ntpConfigSettings.label"),
description: t("network.ntpConfigSettings.description"),
fields: [
{
type: "text",
name: "ntpServer",
label: t("config_network_fieldLabel_ntpServer"),
label: t("network.ntpServer.label"),
},
],
},
{
label: t("config_network_groupLabel_rsyslogConfig"),
description: t("config_network_groupDescription_rsyslogConfig"),
label: t("network.rsyslogConfigSettings.label"),
description: t("network.rsyslogConfigSettings.description"),
fields: [
{
type: "text",
name: "rsyslogServer",
label: t("config_network_fieldLabel_rsyslogServer"),
label: t("network.rsyslogServer.label"),
},
],
},

72
src/components/PageComponents/Config/Position.tsx

@ -15,7 +15,7 @@ export const Position = () => {
const { flagsValue, activeFlags, toggleFlag, getAllFlags } = usePositionFlags(
config?.position?.positionFlags ?? 0,
);
const { t } = useTranslation();
const { t } = useTranslation("deviceConfig");
const onSubmit = (data: PositionValidation) => {
return setWorkingConfig(
@ -44,22 +44,20 @@ export const Position = () => {
defaultValues={config.position}
fieldGroups={[
{
label: t("config_position_groupLabel_positionSettings"),
description: t("config_position_groupDescription_positionSettings"),
label: t("position.title"),
description: t("position.description"),
fields: [
{
type: "toggle",
name: "positionBroadcastSmartEnabled",
label: t("config_position_fieldLabel_smartPositionEnabled"),
description: t(
"config_position_fieldDescription_smartPositionEnabled",
),
label: t("position.smartPositionEnabled.label"),
description: t("position.smartPositionEnabled.description"),
},
{
type: "select",
name: "gpsMode",
label: t("config_position_fieldLabel_gpsMode"),
description: t("config_position_fieldDescription_gpsMode"),
label: t("position.gpsMode.label"),
description: t("position.gpsMode.description"),
properties: {
enumValue: Protobuf.Config.Config_PositionConfig_GpsMode,
},
@ -67,8 +65,8 @@ export const Position = () => {
{
type: "toggle",
name: "fixedPosition",
label: t("config_position_fieldLabel_fixedPosition"),
description: t("config_position_fieldDescription_fixedPosition"),
label: t("position.fixedPosition.label"),
description: t("position.fixedPosition.description"),
},
{
type: "multiSelect",
@ -77,11 +75,9 @@ export const Position = () => {
isChecked: (name: string) =>
activeFlags?.includes(name as FlagName) ?? false,
onValueChange: onPositonFlagChange,
label: t("config_position_fieldLabel_positionFlags"),
placeholder: t(
"config_position_fieldPlaceholder_selectPositionFlags",
),
description: t("config_position_fieldDescription_positionFlags"),
label: t("position.positionFlags.label"),
placeholder: t("position.positionFlags.placeholder"),
description: t("position.positionFlags.description"),
properties: {
enumValue: getAllFlags(),
},
@ -89,56 +85,50 @@ export const Position = () => {
{
type: "number",
name: "rxGpio",
label: t("config_position_fieldLabel_receivePin"),
description: t("config_position_fieldDescription_receivePin"),
label: t("position.receivePin.label"),
description: t("position.receivePin.description"),
},
{
type: "number",
name: "txGpio",
label: t("config_position_fieldLabel_transmitPin"),
description: t("config_position_fieldDescription_transmitPin"),
label: t("position.transmitPin.label"),
description: t("position.transmitPin.description"),
},
{
type: "number",
name: "gpsEnGpio",
label: t("config_position_fieldLabel_enablePin"),
description: t("config_position_fieldDescription_enablePin"),
label: t("position.enablePin.label"),
description: t("position.enablePin.description"),
},
],
},
{
label: t("config_position_groupLabel_intervals"),
description: t("config_position_groupDescription_intervals"),
label: t("position.intervalsSettings.label"),
description: t("position.intervalsSettings.description"),
fields: [
{
type: "number",
name: "positionBroadcastSecs",
label: t("config_position_fieldLabel_broadcastInterval"),
description: t(
"config_position_fieldDescription_broadcastInterval",
),
label: t("position.broadcastInterval.label"),
description: t("position.broadcastInterval.description"),
properties: {
suffix: t("common_unit_seconds"),
suffix: t("unit.second.plural"),
},
},
{
type: "number",
name: "gpsUpdateInterval",
label: t("config_position_fieldLabel_gpsUpdateInterval"),
description: t(
"config_position_fieldDescription_gpsUpdateInterval",
),
label: t("position.gpsUpdateInterval.label"),
description: t("position.gpsUpdateInterval.description"),
properties: {
suffix: t("common_unit_seconds"),
suffix: t("unit.second.plural"),
},
},
{
type: "number",
name: "broadcastSmartMinimumDistance",
label: t("config_position_fieldLabel_smartPositionMinDistance"),
description: t(
"config_position_fieldDescription_smartPositionMinDistance",
),
label: t("position.smartPositionMinDistance.label"),
description: t("position.smartPositionMinDistance.description"),
disabledBy: [
{
fieldName: "positionBroadcastSmartEnabled",
@ -148,10 +138,8 @@ export const Position = () => {
{
type: "number",
name: "broadcastSmartMinimumIntervalSecs",
label: t("config_position_fieldLabel_smartPositionMinInterval"),
description: t(
"config_position_fieldDescription_smartPositionMinInterval",
),
label: t("position.smartPositionMinInterval.label"),
description: t("position.smartPositionMinInterval.description"),
disabledBy: [
{
fieldName: "positionBroadcastSmartEnabled",

64
src/components/PageComponents/Config/Power.tsx

@ -7,7 +7,7 @@ import { useTranslation } from "react-i18next";
export const Power = () => {
const { config, setWorkingConfig } = useDevice();
const { t } = useTranslation();
const { t } = useTranslation("deviceConfig");
const onSubmit = (data: PowerValidation) => {
setWorkingConfig(
@ -26,35 +26,29 @@ export const Power = () => {
defaultValues={config.power}
fieldGroups={[
{
label: t("config_power_groupLabel_powerConfig"),
description: t("config_power_groupDescription_powerConfig"),
label: t("power.powerConfigSettings.label"),
description: t("power.powerConfigSettings.description"),
fields: [
{
type: "toggle",
name: "isPowerSaving",
label: t("config_power_fieldLabel_powerSavingEnabled"),
description: t(
"config_power_fieldDescription_powerSavingEnabled",
),
label: t("power.powerSavingEnabled.label"),
description: t("power.powerSavingEnabled.description"),
},
{
type: "number",
name: "onBatteryShutdownAfterSecs",
label: t("config_power_fieldLabel_shutdownOnBatteryDelay"),
description: t(
"config_power_fieldDescription_shutdownOnBatteryDelay",
),
label: t("power.shutdownOnBatteryDelay.label"),
description: t("power.shutdownOnBatteryDelay.description"),
properties: {
suffix: t("common_unit_seconds"),
suffix: t("unit.second.plural"),
},
},
{
type: "number",
name: "adcMultiplierOverride",
label: t("config_power_fieldLabel_adcMultiplierOverride"),
description: t(
"config_power_fieldDescription_adcMultiplierOverride",
),
label: t("power.adcMultiplierOverride.label"),
description: t("power.adcMultiplierOverride.description"),
properties: {
step: 0.0001,
},
@ -62,55 +56,49 @@ export const Power = () => {
{
type: "number",
name: "waitBluetoothSecs",
label: t("config_power_fieldLabel_noConnectionBluetoothDisabled"),
description: t(
"config_power_fieldDescription_noConnectionBluetoothDisabled",
),
label: t("power.noConnectionBluetoothDisabled.label"),
description: t("power.noConnectionBluetoothDisabled.description"),
properties: {
suffix: t("common_unit_seconds"),
suffix: t("unit.second.plural"),
},
},
{
type: "number",
name: "deviceBatteryInaAddress",
label: t("config_power_fieldLabel_ina219Address"),
description: t("config_power_fieldDescription_ina219Address"),
label: t("power.ina219Address.label"),
description: t("power.ina219Address.description"),
},
],
},
{
label: t("config_power_groupLabel_sleepSettings"),
description: t("config_power_groupDescription_sleepSettings"),
label: t("power.sleepSettings.label"),
description: t("power.sleepSettings.description"),
fields: [
{
type: "number",
name: "sdsSecs",
label: t("config_power_fieldLabel_superDeepSleepDuration"),
description: t(
"config_power_fieldDescription_superDeepSleepDuration",
),
label: t("power.superDeepSleepDuration.label"),
description: t("power.superDeepSleepDuration.description"),
properties: {
suffix: t("common_unit_seconds"),
suffix: t("unit.second.plural"),
},
},
{
type: "number",
name: "lsSecs",
label: t("config_power_fieldLabel_lightSleepDuration"),
description: t(
"config_power_fieldDescription_lightSleepDuration",
),
label: t("power.lightSleepDuration.label"),
description: t("power.lightSleepDuration.description"),
properties: {
suffix: t("common_unit_seconds"),
suffix: t("unit.second.plural"),
},
},
{
type: "number",
name: "minWakeSecs",
label: t("config_power_fieldLabel_minimumWakeTime"),
description: t("config_power_fieldDescription_minimumWakeTime"),
label: t("power.minimumWakeTime.label"),
description: t("power.minimumWakeTime.description"),
properties: {
suffix: t("common_unit_seconds"),
suffix: t("unit.second.plural"),
},
},
],

168
src/components/PageComponents/Config/Security/Security.tsx

@ -22,7 +22,7 @@ export const Security = () => {
removeError,
clearErrors,
} = useAppStore();
const { t } = useTranslation();
const { t } = useTranslation("deviceConfig");
const [state, dispatch] = useReducer(securityReducer, {
privateKey: fromByteArray(config.security?.privateKey ?? new Uint8Array(0)),
@ -52,23 +52,18 @@ export const Security = () => {
try {
removeError(fieldNameKey);
if (fieldName === "privateKey" && input === "") {
addError(
fieldNameKey,
t("config_security_validation_privateKeyRequired"),
);
addError(fieldNameKey, t("security.validation.privateKeyRequired"));
return;
}
if (fieldName === "adminKey" && input === "") {
if (
state.isManaged && state.adminKey
.map((v, i) => i === fieldIndex ? input : v)
state.isManaged &&
state.adminKey
.map((v, i) => (i === fieldIndex ? input : v))
.every((s) => s === "")
) {
addError(
"adminKey0",
t("config_security_validation_adminKeyRequiredWhenManaged"),
);
addError("adminKey0", t("security."));
}
return;
@ -78,8 +73,8 @@ export const Security = () => {
addError(
fieldNameKey,
fieldName === "privateKey"
? t("config_security_validation_privateKeyMustBe256BitPsk")
: t("config_security_validation_adminKeyMustBe256BitPsk"),
? t("security.validation.privateKeyMustBe256BitPsk")
: t("security.validation.adminKeyMustBe256BitPsk"),
);
return;
}
@ -88,7 +83,7 @@ export const Security = () => {
if (decoded.length !== count) {
addError(
fieldNameKey,
t("config_security_validation_enterValid256BitPsk", {
t("security.validation.enterValid256BitPsk", {
bits: count * 8,
}),
);
@ -99,15 +94,13 @@ export const Security = () => {
addError(
fieldNameKey,
fieldName === "privateKey"
? t("config_security_validation_invalidPrivateKeyFormat")
: t("config_security_validation_invalidAdminKeyFormat"),
? t("security.validation.invalidPrivateKeyFormat")
: t("security.validation.invalidAdminKeyFormat"),
);
}
};
function setSecurityPayload(
overrides: SecurityConfigInit,
) {
function setSecurityPayload(overrides: SecurityConfigInit) {
const base: SecurityConfigInit = {
isManaged: state.isManaged,
adminChannelEnabled: state.adminChannelEnabled,
@ -214,13 +207,12 @@ export const Security = () => {
) => {
dispatch({ type: "SET_TOGGLE", field, payload: next });
if (
field === "isManaged" && state.adminKey.every((s) => s === "")
) {
if (next) { // If enabling 'managed' and no admin keys are set
if (field === "isManaged" && state.adminKey.every((s) => s === "")) {
if (next) {
// If enabling 'managed' and no admin keys are set
addError(
"adminKey0",
t("config_security_validation_adminKeyRequiredWhenManaged"),
t("security.validation.adminKeyRequiredWhenManaged"),
);
} else {
removeError("adminKey0");
@ -262,20 +254,22 @@ export const Security = () => {
}}
fieldGroups={[
{
label: t("config_security_groupLabel_securitySettings"),
description: t("config_security_groupDescription_securitySettings"),
label: t("security.title"),
description: t("security.description"),
fields: [
{
type: "passwordGenerator",
id: "pskInput",
name: "privateKey",
label: t("config_security_fieldLabel_privateKey"),
description: t("config_security_fieldDescription_privateKey"),
bits: [{
text: t("config_security_option_256bit"),
value: "32",
key: "bit256",
}],
label: t("security.privateKey.label"),
description: t("security.privateKey.description"),
bits: [
{
text: t("security.256bit"),
value: "32",
key: "bit256",
},
],
validationText: hasFieldError("privateKey")
? getErrorMessage("privateKey")
: "",
@ -285,7 +279,7 @@ export const Security = () => {
hide: !state.privateKeyVisible,
actionButtons: [
{
text: t("common_button_generate"),
text: t("button.generate"),
onClick: () =>
dispatch({
type: "SHOW_PRIVATE_KEY_DIALOG",
@ -294,7 +288,7 @@ export const Security = () => {
variant: "success",
},
{
text: t("config_security_button_backupKey"),
text: t("button.backupKey"),
onClick: () => setDialogOpen("pkiBackup", true),
variant: "subtle",
},
@ -308,9 +302,9 @@ export const Security = () => {
{
type: "text",
name: "publicKey",
label: t("config_security_fieldLabel_publicKey"),
label: t("security.publicKey.label"),
disabled: true,
description: t("config_security_fieldDescription_publicKey"),
description: t("security.publicKey.description"),
properties: {
value: state.publicKey,
showCopyButton: true,
@ -319,27 +313,27 @@ export const Security = () => {
],
},
{
label: t("config_security_groupLabel_adminSettings"),
description: t("config_security_groupDescription_adminSettings"),
label: t("security.adminSettings.label"),
description: t("security.adminSettings.description"),
fields: [
{
type: "passwordGenerator",
name: "adminKey.0",
id: "adminKey0Input",
label: t("config_security_fieldLabel_primaryAdminKey"),
description: t(
"config_security_fieldDescription_primaryAdminKey",
),
label: t("security.primaryAdminKey.label"),
description: t("security.primaryAdminKey.description"),
validationText: hasFieldError("adminKey0")
? getErrorMessage("adminKey0")
: "",
inputChange: (e) => adminKeyInputChangeEvent(e, 0),
selectChange: () => {},
bits: [{
text: t("config_security_option_256bit"),
value: "32",
key: "bit256",
}],
bits: [
{
text: t("security.256bit"),
value: "32",
key: "bit256",
},
],
devicePSKBitCount: state.privateKeyBitCount,
hide: !state.adminKeyVisible[0],
actionButtons: [],
@ -356,20 +350,20 @@ export const Security = () => {
type: "passwordGenerator",
name: "adminKey.1",
id: "adminKey1Input",
label: t("config_security_fieldLabel_secondaryAdminKey"),
description: t(
"config_security_fieldDescription_secondaryAdminKey",
),
label: t("security.secondaryAdminKey.label"),
description: t("security.secondaryAdminKey.description"),
validationText: hasFieldError("adminKey1")
? getErrorMessage("adminKey1")
: "",
inputChange: (e) => adminKeyInputChangeEvent(e, 1),
selectChange: () => {},
bits: [{
text: t("config_security_option_256bit"),
value: "32",
key: "bit256",
}],
bits: [
{
text: t("security.256bit"),
value: "32",
key: "bit256",
},
],
devicePSKBitCount: state.privateKeyBitCount,
hide: !state.adminKeyVisible[1],
actionButtons: [],
@ -386,20 +380,20 @@ export const Security = () => {
type: "passwordGenerator",
name: "adminKey.2",
id: "adminKey2Input",
label: t("config_security_fieldLabel_tertiaryAdminKey"),
description: t(
"config_security_fieldDescription_tertiaryAdminKey",
),
label: t("security.tertiaryAdminKey.label"),
description: t("security.tertiaryAdminKey.description"),
validationText: hasFieldError("adminKey2")
? getErrorMessage("adminKey2")
: "",
inputChange: (e) => adminKeyInputChangeEvent(e, 2),
selectChange: () => {},
bits: [{
text: t("config_security_option_256bit"),
value: "32",
key: "bit256",
}],
bits: [
{
text: t("security.256bit"),
value: "32",
key: "bit256",
},
],
devicePSKBitCount: state.privateKeyBitCount,
hide: !state.adminKeyVisible[2],
actionButtons: [],
@ -415,26 +409,22 @@ export const Security = () => {
{
type: "toggle",
name: "isManaged",
label: t("config_security_fieldLabel_managed"),
description: t("config_security_fieldDescription_managed"),
label: t("security.managed.label"),
description: t("security.managed.description"),
inputChange: (e: boolean) => onToggleChange("isManaged", e),
properties: {
checked: state.isManaged,
},
disabled: (
(hasFieldError("adminKey0") ||
hasFieldError("adminKey1") ||
hasFieldError("adminKey2")) &&
!state.adminKey.every((s) => s === "")
),
disabled: (hasFieldError("adminKey0") ||
hasFieldError("adminKey1") ||
hasFieldError("adminKey2")) &&
!state.adminKey.every((s) => s === ""),
},
{
type: "toggle",
name: "adminChannelEnabled",
label: t("config_security_fieldLabel_allowLegacyAdmin"),
description: t(
"config_security_fieldDescription_adminChannelEnabled",
),
label: t("security.adminChannelEnabled.label"),
description: t("security.adminChannelEnabled.description"),
inputChange: (e: boolean) =>
onToggleChange("adminChannelEnabled", e),
properties: {
@ -444,16 +434,14 @@ export const Security = () => {
],
},
{
label: t("config_security_groupLabel_loggingSettings"),
description: t("config_security_groupDescription_loggingSettings"),
label: t("security.loggingSettings.label"),
description: t("security.loggingSettings.description"),
fields: [
{
type: "toggle",
name: "debugLogApiEnabled",
label: t("config_security_fieldLabel_enableDebugLogApi"),
description: t(
"config_security_fieldDescription_enableDebugLogApi",
),
label: t("security.enableDebugLogApi.label"),
description: t("security.enableDebugLogApi.description"),
inputChange: (e: boolean) =>
onToggleChange("debugLogApiEnabled", e),
properties: {
@ -463,10 +451,8 @@ export const Security = () => {
{
type: "toggle",
name: "serialEnabled",
label: t("config_security_fieldLabel_serialOutputEnabled"),
description: t(
"config_security_fieldDescription_serialOutputEnabled",
),
label: t("security.serialOutputEnabled.label"),
description: t("security.serialOutputEnabled.description"),
inputChange: (e: boolean) => onToggleChange("serialEnabled", e),
properties: {
checked: state.serialEnabled,
@ -478,9 +464,9 @@ export const Security = () => {
/>
<PkiRegenerateDialog
text={{
button: t("dialog_pkiRegenerateDialog_buttonRegenerate"),
title: t("dialog_pkiRegenerate_title_keyPair"),
description: t("dialog_pkiRegenerate_description_keyPair"),
button: t("button.regenerate"),
title: t("pkiRegenerate.title"),
description: t("pkiRegenerate.description"),
}}
open={state.privateKeyDialogOpen}
onOpenChange={() =>

4
src/components/PageComponents/Connect/BLE.tsx

@ -62,7 +62,7 @@ export const BLE = (
))}
{bleDevices.length === 0 && (
<Mono className="m-auto select-none">
{t("bluetoothConnection_noDevicesPaired")}
{t("newDeviceDialog.bluetoothConnection.noDevicesPaired")}
</Mono>
)}
</div>
@ -86,7 +86,7 @@ export const BLE = (
});
}}
>
<span>{t("bluetoothConnection_newDeviceButton")}</span>
<span>{t("newDeviceDialog.bluetoothConnection.newDeviceButton")}</span>
</Button>
</fieldset>
);

38
src/components/PageComponents/Connect/HTTP.tsx

@ -24,7 +24,7 @@ interface FormData {
export const HTTP = (
{ closeDialog }: TabElementProps,
) => {
const { t } = useTranslation();
const { t } = useTranslation("dialog");
const [connectionInProgress, setConnectionInProgress] = useState(false);
const isURLHTTPS = location.protocol === "https:";
@ -81,12 +81,12 @@ export const HTTP = (
disabled={connectionInProgress}
>
<div>
<Label>{t("httpConnection_ipAddressLabel")}</Label>
<Label>{t("newDeviceDialog.httpConnection.label")}</Label>
<Input
prefix={tlsValue
? `${t("httpConnection_https")}://`
: `${t("httpConnection_http")}://`}
placeholder={t("httpConnection_field_ipAddress_placeholder")}
? `${t("newDeviceDialog.https")}://`
: `${t("newDeviceDialog.http")}://`}
placeholder={t("newDeviceDialog.httpConnection.placeholder")}
className="text-slate-900 dark:text-slate-100"
{...register("ip")}
/>
@ -98,7 +98,7 @@ export const HTTP = (
checked={isURLHTTPS || tlsValue}
{...register("tls")}
/>
<Label>{t("httpConnection_label_useHttps")}</Label>
<Label>{t("newDeviceDialog.useHttps")}</Label>
</div>
{connectionError && (
@ -110,38 +110,38 @@ export const HTTP = (
/>
<div>
<p className="text-sm font-medium text-amber-800 dark:text-amber-800">
{t("httpConnection_connectionFailedAlert.title")}
{t("newDeviceDialog.connectionFailedAlert.title")}
</p>
<p className="text-xs mt-1 text-amber-700 dark:text-amber-700">
{t("httpConnection_connectionFailedAlert.descriptionPrefix")}
{t("newDeviceDialog.connectionFailedAlert.descriptionPrefix")}
{connectionError.secure &&
t("httpConnection_connectionFailedAlert.httpsHint")}
{t("httpConnection_connectionFailedAlert.openLinkPrefix")}
t("newDeviceDialog.connectionFailedAlert.httpsHint")}
{t("newDeviceDialog.connectionFailedAlert.openLinkPrefix")}
<Link
href={`${
connectionError.secure
? t("httpConnection_https")
: t("httpConnection_http")
? t("newDeviceDialog.https")
: t("newDeviceDialog.http")
}://${connectionError.host}`}
className="underline font-medium text-amber-800 dark:text-amber-800"
>
{`${
connectionError.secure
? t("httpConnection_https")
: t("httpConnection_http")
? t("newDeviceDialog.https")
: t("newDeviceDialog.http")
}://${connectionError.host}`}
</Link>{" "}
{t("httpConnection_connectionFailedAlert.openLinkSuffix")}
{t("newDeviceDialog.connectionFailedAlert.openLinkSuffix")}
{connectionError.secure
? t(
"httpConnection_connectionFailedAlert.acceptTlsWarningSuffix",
"newDeviceDialog.connectionFailedAlert.acceptTlsWarningSuffix",
)
: ""}.{" "}
<Link
href="https://meshtastic.org/docs/software/web-client/#http"
className="underline font-medium text-amber-800 dark:text-amber-800"
>
{t("httpConnection_connectionFailedAlert.learnMoreLink")}
{t("newDeviceDialog.connectionFailedAlert.learnMoreLink")}
</Link>
</p>
</div>
@ -155,8 +155,8 @@ export const HTTP = (
>
<span>
{connectionInProgress
? t("httpConnection_button_connecting")
: t("httpConnection_button_connect")}
? t("newDeviceDialog.connecting")
: t("newDeviceDialog.connect")}
</span>
</Button>
</form>

12
src/components/PageComponents/Connect/Serial.tsx

@ -56,8 +56,8 @@ export const Serial = (
<div className="flex h-48 flex-col gap-2 overflow-y-auto">
{serialPorts.map((port, index) => {
const { usbProductId, usbVendorId } = port.getInfo();
const vendor = usbVendorId ?? t("common_unknown_short");
const product = usbProductId ?? t("common_unknown_short");
const vendor = usbVendorId ?? t("unknown.shortName");
const product = usbProductId ?? t("unknown.shortName");
return (
<Button
key={`${vendor}-${product}-${index}`}
@ -69,8 +69,8 @@ export const Serial = (
// No need to setConnectionInProgress(false) here as closeDialog() unmounts.
}}
>
{t("serialConnection_deviceIdentifier", {
index,
{t("newDeviceDialog.serialConnection.deviceIdentifier", {
index: index,
vendorId: vendor,
productId: product,
})}
@ -79,7 +79,7 @@ export const Serial = (
})}
{serialPorts.length === 0 && (
<Mono className="m-auto select-none">
{t("serialConnection_noDevicesPaired")}
{t("newDeviceDialog.serialConnection.noDevicesPaired")}
</Mono>
)}
</div>
@ -96,7 +96,7 @@ export const Serial = (
});
}}
>
<span>{t("serialConnection_newDeviceButton")}</span>
<span>{t("newDeviceDialog.serialConnection.newDeviceButton")}</span>
</Button>
</fieldset>
);

49
src/components/PageComponents/Map/NodeDetail.tsx

@ -36,17 +36,17 @@ export interface NodeDetailProps {
export const NodeDetail = ({ node }: NodeDetailProps) => {
const { setChatType, setActiveChat } = useMessageStore();
const { t } = useTranslation();
const { t } = useTranslation("nodes");
const { setActivePage } = useDevice();
const name = node.user?.longName ?? t("common_unknown_short");
const shortName = node.user?.shortName ?? t("common_unknown_short");
const name = node.user?.longName ?? t("unknown.shortName");
const shortName = node.user?.shortName ?? t("unknown.shortName");
const hwModel = node.user?.hwModel ?? 0;
const rawHardwareType = Protobuf.Mesh.HardwareModel[hwModel] as
| keyof typeof Protobuf.Mesh.HardwareModel
| undefined;
const hardwareType = rawHardwareType
? rawHardwareType === "UNSET"
? t("common_unset")
? t("unset")
: rawHardwareType.replaceAll("_", " ")
: `${hwModel}`;
function handleDirectMessage() {
@ -101,7 +101,7 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
align="center"
sideOffset={5}
>
{t("node_detail_direct_message_tooltip", {
{t("nodeDetail.directMessage.label", {
shortName,
})}
</TooltipContent>
@ -113,29 +113,22 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
fill={node.isFavorite ? "black" : "none"}
size={15}
aria-label={node.isFavorite
? t("node_detail_favorite_aria_label")
: t("node_detail_not_favorite_aria_label")}
? t("nodeDetail.favorite.label")
: t("nodeDetail.notFavorite.label")}
/>
</div>
</div>
<div>
<Heading as="h5">{name}</Heading>
{hardwareType !== t("common_unset") && <Subtle>{hardwareType}
</Subtle>}
{hardwareType !== t("unset") && <Subtle>{hardwareType}</Subtle>}
{!!node.deviceMetrics?.batteryLevel && (
<BatteryStatus deviceMetrics={node.deviceMetrics} />
)}
<div className="flex gap-2 items-center">
{node.user?.shortName && (
<div>
{t("node_detail_short_name_display_format", {
name: node.user?.shortName,
})}
</div>
)}
{node.user?.shortName && <div>"{node.user?.shortName}"</div>}
{node.user?.id && <div>{node.user?.id}</div>}
</div>
@ -148,14 +141,14 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
<div>
{node.lastHeard > 0 && (
<div>
{t("node_detail_status_heard")}{" "}
{t("nodeDetail.status.heard")}{" "}
<TimeAgo timestamp={node.lastHeard * 1000} />
</div>
)}
</div>
{node.viaMqtt && (
<div style={{ color: "#660066" }} className="font-medium">
{t("node_detail_status_mqtt")}
{t("nodeDetail.status.mqtt")}
</div>
)}
</div>
@ -168,13 +161,11 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
<div className="flex items-center grow">
<div className="border-2 border-slate-900 rounded-sm px-0.5 mr-1">
{Number.isNaN(node.hopsAway)
? t("node_detail_hops_unknown")
? t("unit.hopsAway.unknown")
: node.hopsAway}
</div>
<div>
{node.hopsAway === 1
? t("node_detail_hops_label_one")
: t("node_detail_hops_label_other")}
{node.hopsAway === 1 ? t("unit.hops.one") : t("unit.hop.plural")}
</div>
</div>
{node.position?.altitude && (
@ -186,8 +177,8 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
/>
<div>
{formatQuantity(node.position?.altitude, {
one: t("node_detail_altitude_unit_one"),
other: t("node_detail_altitude_unit_other"),
one: t("unit.meter.one"),
other: t("unit.meter.plural"),
})}
</div>
</div>
@ -197,7 +188,7 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
<div className="flex mt-2">
{!!node.deviceMetrics?.channelUtilization && (
<div className="grow">
<div>{t("node_detail_channel_util_label")}</div>
<div>{t("nodeDetail.channelUtilization")}</div>
<Mono>
{node.deviceMetrics?.channelUtilization.toPrecision(3)}%
</Mono>
@ -205,7 +196,7 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
)}
{!!node.deviceMetrics?.airUtilTx && (
<div className="grow">
<div>{t("node_detail_airtime_util_label")}</div>
<div>{t("nodeDetail.airTxUtilization")}</div>
<Mono className="text-gray-500">
{node.deviceMetrics?.airUtilTx.toPrecision(3)}%
</Mono>
@ -215,15 +206,15 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
{node.snr !== 0 && (
<div className="mt-2">
<div>{t("node_detail_snr_label")}</div>
<div>{t("unit.snr")}</div>
<Mono className="flex items-center text-xs text-gray-500">
{node.snr}
{t("common_unit_dbm")}
{t("unit.dbm")}
<Dot />
{Math.min(Math.max((node.snr + 10) * 5, 0), 100)}%
<Dot />
{(node.snr + 10) * 5}
{t("common_rawUnit")}
{t("unit.raw")}
</Mono>
</div>
)}

4
src/components/PageComponents/Messages/ChannelChat.tsx

@ -8,11 +8,11 @@ export interface ChannelChatProps {
}
const EmptyState = () => {
const { t } = useTranslation();
const { t } = useTranslation("messages");
return (
<div className="flex flex-1 flex-col place-content-center place-items-center p-8 text-slate-500 dark:text-slate-400">
<InboxIcon className="mb-2 h-8 w-8" />
<span className="text-sm">{t("messages_empty")}</span>
<span className="text-sm">{t("emptyState.text")}</span>
</div>
);
};

16
src/components/PageComponents/Messages/TraceRoute.tsx

@ -34,18 +34,18 @@ const RoutePath = (
<p className="font-semibold">{title}</p>
<p>{startNode?.user?.longName}</p>
<p>
{snr?.[0] ?? t("traceRoute_snrUnknown")}
{t("common_unit_dbm")}
{snr?.[0] ?? t("traceRoute.snrUnknown")}
{t("unit.dbm")}
</p>
{path.map((hop, i) => (
<span key={getNode(hop)?.num ?? hop}>
<p>
{getNode(hop)?.user?.longName ??
`${t("traceRoute_nodeUnknownPrefix")}${numberToHexUnpadded(hop)}`}
`${t("traceRoute.nodeUnknownPrefix")}${numberToHexUnpadded(hop)}`}
</p>
<p>
{snr?.[i + 1] ?? t("traceRoute_snrUnknown")}
{t("common_unit_dbm")}
{snr?.[i + 1] ?? t("traceRoute.snrUnknown")}
{t("unit.dbm")}
</p>
</span>
))}
@ -62,11 +62,11 @@ export const TraceRoute = ({
snrTowards,
snrBack,
}: TraceRouteProps) => {
const { t } = useTranslation();
const { t } = useTranslation("dialog");
return (
<div className="ml-5 flex">
<RoutePath
title={t("traceRoute_routeToDestinationTitle")}
title={t("traceRoute.routeToDestination")}
startNode={to}
endNode={from}
path={route}
@ -74,7 +74,7 @@ export const TraceRoute = ({
/>
{routeBack && routeBack.length > 0 && (
<RoutePath
title={t("traceRoute_routeBackTitle")}
title={t("traceRoute.routeBack")}
startNode={from}
endNode={to}
path={routeBack}

26
src/components/PageComponents/ModuleConfig/AmbientLighting.tsx

@ -3,9 +3,11 @@ import type { AmbientLightingValidation } from "@app/validation/moduleConfig/amb
import { create } from "@bufbuild/protobuf";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
export const AmbientLighting = () => {
const { moduleConfig, setWorkingModuleConfig } = useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: AmbientLightingValidation) => {
setWorkingModuleConfig(
@ -24,38 +26,38 @@ export const AmbientLighting = () => {
defaultValues={moduleConfig.ambientLighting}
fieldGroups={[
{
label: "Ambient Lighting Settings",
description: "Settings for the Ambient Lighting module",
label: t("ambientLighting.title"),
description: t("ambientLighting.description"),
fields: [
{
type: "toggle",
name: "ledState",
label: "LED State",
description: "Sets LED to on or off",
label: t("ambientLighting.ledState.label"),
description: t("ambientLighting.ledState.description"),
},
{
type: "number",
name: "current",
label: "Current",
description: "Sets the current for the LED output. Default is 10",
label: t("ambientLighting.current.label"),
description: t("ambientLighting.current.description"),
},
{
type: "number",
name: "red",
label: "Red",
description: "Sets the red LED level. Values are 0-255",
label: t("ambientLighting.red.label"),
description: t("ambientLighting.red.description"),
},
{
type: "number",
name: "green",
label: "Green",
description: "Sets the green LED level. Values are 0-255",
label: t("ambientLighting.green.label"),
description: t("ambientLighting.green.description"),
},
{
type: "number",
name: "blue",
label: "Blue",
description: "Sets the blue LED level. Values are 0-255",
label: t("ambientLighting.blue.label"),
description: t("ambientLighting.blue.description"),
},
],
},

36
src/components/PageComponents/ModuleConfig/Audio.tsx

@ -1,11 +1,13 @@
import type { AudioValidation } from "@app/validation/moduleConfig/audio.tsx";
import type { AudioValidation } from "@app/validation/moduleConfig/audio.ts";
import { create } from "@bufbuild/protobuf";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
export const Audio = () => {
const { moduleConfig, setWorkingModuleConfig } = useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: AudioValidation) => {
setWorkingModuleConfig(
@ -24,26 +26,26 @@ export const Audio = () => {
defaultValues={moduleConfig.audio}
fieldGroups={[
{
label: "Audio Settings",
description: "Settings for the Audio module",
label: t("audio.title"),
description: t("audio.description"),
fields: [
{
type: "toggle",
name: "codec2Enabled",
label: "Codec 2 Enabled",
description: "Enable Codec 2 audio encoding",
label: t("audio.codec2Enabled.label"),
description: t("audio.codec2Enabled.description"),
},
{
type: "number",
name: "pttPin",
label: "PTT Pin",
description: "GPIO pin to use for PTT",
label: t("audio.pttPin.label"),
description: t("audio.pttPin.description"),
},
{
type: "select",
name: "bitrate",
label: "Bitrate",
description: "Bitrate to use for audio encoding",
label: t("audio.bitrate.label"),
description: t("audio.bitrate.description"),
properties: {
enumValue:
Protobuf.ModuleConfig.ModuleConfig_AudioConfig_Audio_Baud,
@ -52,26 +54,26 @@ export const Audio = () => {
{
type: "number",
name: "i2sWs",
label: "i2S WS",
description: "GPIO pin to use for i2S WS",
label: t("audio.i2sWs.label"),
description: t("audio.i2sWs.description"),
},
{
type: "number",
name: "i2sSd",
label: "i2S SD",
description: "GPIO pin to use for i2S SD",
label: t("audio.i2sSd.label"),
description: t("audio.i2sSd.description"),
},
{
type: "number",
name: "i2sDin",
label: "i2S DIN",
description: "GPIO pin to use for i2S DIN",
label: t("audio.i2sDin.label"),
description: t("audio.i2sDin.description"),
},
{
type: "number",
name: "i2sSck",
label: "i2S SCK",
description: "GPIO pin to use for i2S SCK",
label: t("audio.i2sSck.label"),
description: t("audio.i2sSck.description"),
},
],
},

53
src/components/PageComponents/ModuleConfig/CannedMessage.tsx

@ -1,11 +1,13 @@
import type { CannedMessageValidation } from "@app/validation/moduleConfig/cannedMessage.tsx";
import type { CannedMessageValidation } from "@app/validation/moduleConfig/cannedMessage.ts";
import { create } from "@bufbuild/protobuf";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
export const CannedMessage = () => {
const { moduleConfig, setWorkingModuleConfig } = useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: CannedMessageValidation) => {
setWorkingModuleConfig(
@ -24,44 +26,44 @@ export const CannedMessage = () => {
defaultValues={moduleConfig.cannedMessage}
fieldGroups={[
{
label: "Canned Message Settings",
description: "Settings for the Canned Message module",
label: t("cannedMessage.title"),
description: t("cannedMessage.description"),
fields: [
{
type: "toggle",
name: "enabled",
label: "Module Enabled",
description: "Enable Canned Message",
label: t("cannedMessage.moduleEnabled.label"),
description: t("cannedMessage.moduleEnabled.description"),
},
{
type: "toggle",
name: "rotary1Enabled",
label: "Rotary Encoder #1 Enabled",
description: "Enable the rotary encoder",
label: t("cannedMessage.rotary1Enabled.label"),
description: t("cannedMessage.rotary1Enabled.description"),
},
{
type: "number",
name: "inputbrokerPinA",
label: "Encoder Pin A",
description: "GPIO Pin Value (1-39) For encoder port A",
label: t("cannedMessage.inputbrokerPinA.label"),
description: t("cannedMessage.inputbrokerPinA.description"),
},
{
type: "number",
name: "inputbrokerPinB",
label: "Encoder Pin B",
description: "GPIO Pin Value (1-39) For encoder port B",
label: t("cannedMessage.inputbrokerPinB.label"),
description: t("cannedMessage.inputbrokerPinB.description"),
},
{
type: "number",
name: "inputbrokerPinPress",
label: "Encoder Pin Press",
description: "GPIO Pin Value (1-39) For encoder Press",
label: t("cannedMessage.inputbrokerPinPress.label"),
description: t("cannedMessage.inputbrokerPinPress.description"),
},
{
type: "select",
name: "inputbrokerEventCw",
label: "Clockwise event",
description: "Select input event.",
label: t("cannedMessage.inputbrokerEventCw.label"),
description: t("cannedMessage.inputbrokerEventCw.description"),
properties: {
enumValue: Protobuf.ModuleConfig
.ModuleConfig_CannedMessageConfig_InputEventChar,
@ -70,8 +72,8 @@ export const CannedMessage = () => {
{
type: "select",
name: "inputbrokerEventCcw",
label: "Counter Clockwise event",
description: "Select input event.",
label: t("cannedMessage.inputbrokerEventCcw.label"),
description: t("cannedMessage.inputbrokerEventCcw.description"),
properties: {
enumValue: Protobuf.ModuleConfig
.ModuleConfig_CannedMessageConfig_InputEventChar,
@ -80,8 +82,8 @@ export const CannedMessage = () => {
{
type: "select",
name: "inputbrokerEventPress",
label: "Press event",
description: "Select input event",
label: t("cannedMessage.inputbrokerEventPress.label"),
description: t("cannedMessage.inputbrokerEventPress.description"),
properties: {
enumValue: Protobuf.ModuleConfig
.ModuleConfig_CannedMessageConfig_InputEventChar,
@ -90,21 +92,20 @@ export const CannedMessage = () => {
{
type: "toggle",
name: "updown1Enabled",
label: "Up Down enabled",
description: "Enable the up / down encoder",
label: t("cannedMessage.updown1Enabled.label"),
description: t("cannedMessage.updown1Enabled.description"),
},
{
type: "text",
name: "allowInputSource",
label: "Allow Input Source",
description:
"Select from: '_any', 'rotEnc1', 'upDownEnc1', 'cardkb'",
label: t("cannedMessage.allowInputSource.label"),
description: t("cannedMessage.allowInputSource.description"),
},
{
type: "toggle",
name: "sendBell",
label: "Send Bell",
description: "Sends a bell character with each message",
label: t("cannedMessage.sendBell.label"),
description: t("cannedMessage.sendBell.description"),
},
],
},

50
src/components/PageComponents/ModuleConfig/DetectionSensor.tsx

@ -1,11 +1,13 @@
import { useDevice } from "@core/stores/deviceStore.ts";
import type { DetectionSensorValidation } from "@app/validation/moduleConfig/detectionSensor.tsx";
import type { DetectionSensorValidation } from "@app/validation/moduleConfig/detectionSensor.ts";
import { create } from "@bufbuild/protobuf";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
export const DetectionSensor = () => {
const { moduleConfig, setWorkingModuleConfig } = useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: DetectionSensorValidation) => {
setWorkingModuleConfig(
@ -24,23 +26,24 @@ export const DetectionSensor = () => {
defaultValues={moduleConfig.detectionSensor}
fieldGroups={[
{
label: "Detection Sensor Settings",
description: "Settings for the Detection Sensor module",
label: t("detectionSensor.title"),
description: t("detectionSensor.description"),
fields: [
{
type: "toggle",
name: "enabled",
label: "Enabled",
description: "Enable or disable Detection Sensor Module",
label: t("detectionSensor.enabled.label"),
description: t("detectionSensor.enabled.description"),
},
{
type: "number",
name: "minimumBroadcastSecs",
label: "Minimum Broadcast Seconds",
description:
"The interval in seconds of how often we can send a message to the mesh when a state change is detected",
label: t("detectionSensor.minimumBroadcastSecs.label"),
description: t(
"detectionSensor.minimumBroadcastSecs.description",
),
properties: {
suffix: "Seconds",
suffix: t("unit.second.plural"),
},
disabledBy: [
{
@ -51,9 +54,8 @@ export const DetectionSensor = () => {
{
type: "number",
name: "stateBroadcastSecs",
label: "State Broadcast Seconds",
description:
"The interval in seconds of how often we should send a message to the mesh with the current state regardless of changes",
label: t("detectionSensor.stateBroadcastSecs.label"),
description: t("detectionSensor.stateBroadcastSecs.description"),
disabledBy: [
{
fieldName: "enabled",
@ -63,8 +65,8 @@ export const DetectionSensor = () => {
{
type: "toggle",
name: "sendBell",
label: "Send Bell",
description: "Send ASCII bell with alert message",
label: t("detectionSensor.sendBell.label"),
description: t("detectionSensor.sendBell.description"),
disabledBy: [
{
fieldName: "enabled",
@ -74,9 +76,8 @@ export const DetectionSensor = () => {
{
type: "text",
name: "name",
label: "Friendly Name",
description:
"Used to format the message sent to mesh, max 20 Characters",
label: t("detectionSensor.name.label"),
description: t("detectionSensor.name.description"),
disabledBy: [
{
fieldName: "enabled",
@ -86,8 +87,8 @@ export const DetectionSensor = () => {
{
type: "number",
name: "monitorPin",
label: "Monitor Pin",
description: "The GPIO pin to monitor for state changes",
label: t("detectionSensor.monitorPin.label"),
description: t("detectionSensor.monitorPin.description"),
disabledBy: [
{
fieldName: "enabled",
@ -97,9 +98,10 @@ export const DetectionSensor = () => {
{
type: "toggle",
name: "detectionTriggeredHigh",
label: "Detection Triggered High",
description:
"Whether or not the GPIO pin state detection is triggered on HIGH (1), otherwise LOW (0)",
label: t("detectionSensor.detectionTriggeredHigh.label"),
description: t(
"detectionSensor.detectionTriggeredHigh.description",
),
disabledBy: [
{
fieldName: "enabled",
@ -109,8 +111,8 @@ export const DetectionSensor = () => {
{
type: "toggle",
name: "usePullup",
label: "Use Pullup",
description: "Whether or not use INPUT_PULLUP mode for GPIO pin",
label: t("detectionSensor.usePullup.label"),
description: t("detectionSensor.usePullup.description"),
disabledBy: [
{
fieldName: "enabled",

77
src/components/PageComponents/ModuleConfig/ExternalNotification.tsx

@ -1,11 +1,13 @@
import type { ExternalNotificationValidation } from "@app/validation/moduleConfig/externalNotification.tsx";
import type { ExternalNotificationValidation } from "@app/validation/moduleConfig/externalNotification.ts";
import { create } from "@bufbuild/protobuf";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
export const ExternalNotification = () => {
const { moduleConfig, setWorkingModuleConfig } = useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: ExternalNotificationValidation) => {
setWorkingModuleConfig(
@ -24,20 +26,20 @@ export const ExternalNotification = () => {
defaultValues={moduleConfig.externalNotification}
fieldGroups={[
{
label: "External Notification Settings",
description: "Configure the external notification module",
label: t("externalNotification.title"),
description: t("externalNotification.description"),
fields: [
{
type: "toggle",
name: "enabled",
label: "Module Enabled",
description: "Enable External Notification",
label: t("externalNotification.enabled.label"),
description: t("externalNotification.enabled.description"),
},
{
type: "number",
name: "outputMs",
label: "Output MS",
description: "Output MS",
label: t("externalNotification.outputMs.label"),
description: t("externalNotification.outputMs.description"),
disabledBy: [
{
@ -45,14 +47,14 @@ export const ExternalNotification = () => {
},
],
properties: {
suffix: "ms",
suffix: t("unit.ms", { ns: "common" }),
},
},
{
type: "number",
name: "output",
label: "Output",
description: "Output",
label: t("externalNotification.output.label"),
description: t("externalNotification.output.description"),
disabledBy: [
{
fieldName: "enabled",
@ -62,8 +64,8 @@ export const ExternalNotification = () => {
{
type: "number",
name: "outputVibra",
label: "Output Vibrate",
description: "Output Vibrate",
label: t("externalNotification.outputVibra.label"),
description: t("externalNotification.outputVibra.description"),
disabledBy: [
{
fieldName: "enabled",
@ -73,8 +75,8 @@ export const ExternalNotification = () => {
{
type: "number",
name: "outputBuzzer",
label: "Output Buzzer",
description: "Output Buzzer",
label: t("externalNotification.outputBuzzer.label"),
description: t("externalNotification.outputBuzzer.description"),
disabledBy: [
{
fieldName: "enabled",
@ -84,8 +86,8 @@ export const ExternalNotification = () => {
{
type: "toggle",
name: "active",
label: "Active",
description: "Active",
label: t("externalNotification.active.label"),
description: t("externalNotification.active.description"),
disabledBy: [
{
fieldName: "enabled",
@ -95,8 +97,8 @@ export const ExternalNotification = () => {
{
type: "toggle",
name: "alertMessage",
label: "Alert Message",
description: "Alert Message",
label: t("externalNotification.alertMessage.label"),
description: t("externalNotification.alertMessage.description"),
disabledBy: [
{
fieldName: "enabled",
@ -106,8 +108,10 @@ export const ExternalNotification = () => {
{
type: "toggle",
name: "alertMessageVibra",
label: "Alert Message Vibrate",
description: "Alert Message Vibrate",
label: t("externalNotification.alertMessageVibra.label"),
description: t(
"externalNotification.alertMessageVibra.description",
),
disabledBy: [
{
fieldName: "enabled",
@ -117,8 +121,10 @@ export const ExternalNotification = () => {
{
type: "toggle",
name: "alertMessageBuzzer",
label: "Alert Message Buzzer",
description: "Alert Message Buzzer",
label: t("externalNotification.alertMessageBuzzer.label"),
description: t(
"externalNotification.alertMessageBuzzer.description",
),
disabledBy: [
{
fieldName: "enabled",
@ -128,9 +134,8 @@ export const ExternalNotification = () => {
{
type: "toggle",
name: "alertBell",
label: "Alert Bell",
description:
"Should an alert be triggered when receiving an incoming bell?",
label: t("externalNotification.alertBell.label"),
description: t("externalNotification.alertBell.description"),
disabledBy: [
{
fieldName: "enabled",
@ -140,8 +145,8 @@ export const ExternalNotification = () => {
{
type: "toggle",
name: "alertBellVibra",
label: "Alert Bell Vibrate",
description: "Alert Bell Vibrate",
label: t("externalNotification.alertBellVibra.label"),
description: t("externalNotification.alertBellVibra.description"),
disabledBy: [
{
fieldName: "enabled",
@ -151,8 +156,10 @@ export const ExternalNotification = () => {
{
type: "toggle",
name: "alertBellBuzzer",
label: "Alert Bell Buzzer",
description: "Alert Bell Buzzer",
label: t("externalNotification.alertBellBuzzer.label"),
description: t(
"externalNotification.alertBellBuzzer.description",
),
disabledBy: [
{
fieldName: "enabled",
@ -162,8 +169,8 @@ export const ExternalNotification = () => {
{
type: "toggle",
name: "usePwm",
label: "Use PWM",
description: "Use PWM",
label: t("externalNotification.usePwm.label"),
description: t("externalNotification.usePwm.description"),
disabledBy: [
{
fieldName: "enabled",
@ -173,8 +180,8 @@ export const ExternalNotification = () => {
{
type: "number",
name: "nagTimeout",
label: "Nag Timeout",
description: "Nag Timeout",
label: t("externalNotification.nagTimeout.label"),
description: t("externalNotification.nagTimeout.description"),
disabledBy: [
{
fieldName: "enabled",
@ -184,8 +191,8 @@ export const ExternalNotification = () => {
{
type: "toggle",
name: "useI2sAsBuzzer",
label: "Use I²S Pin as Buzzer",
description: "Designate I²S Pin as Buzzer Output",
label: t("externalNotification.useI2sAsBuzzer.label"),
description: t("externalNotification.useI2sAsBuzzer.description"),
disabledBy: [
{
fieldName: "enabled",

148
src/components/PageComponents/ModuleConfig/MQTT.tsx

@ -1,11 +1,13 @@
import { useDevice } from "@core/stores/deviceStore.ts";
import type { MqttValidation } from "@app/validation/moduleConfig/mqtt.tsx";
import type { MqttValidation } from "@app/validation/moduleConfig/mqtt.ts";
import { create } from "@bufbuild/protobuf";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
export const MQTT = () => {
const { config, moduleConfig, setWorkingModuleConfig } = useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: MqttValidation) => {
setWorkingModuleConfig(
@ -30,21 +32,20 @@ export const MQTT = () => {
defaultValues={moduleConfig.mqtt}
fieldGroups={[
{
label: "MQTT Settings",
description: "Settings for the MQTT module",
label: t("mqtt.title"),
description: t("mqtt.description"),
fields: [
{
type: "toggle",
name: "enabled",
label: "Enabled",
description: "Enable or disable MQTT",
label: t("mqtt.enabled.label"),
description: t("mqtt.enabled.description"),
},
{
type: "text",
name: "address",
label: "MQTT Server Address",
description:
"MQTT server address to use for default/custom servers",
label: t("mqtt.address.label"),
description: t("mqtt.address.description"),
disabledBy: [
{
fieldName: "enabled",
@ -54,8 +55,8 @@ export const MQTT = () => {
{
type: "text",
name: "username",
label: "MQTT Username",
description: "MQTT username to use for default/custom servers",
label: t("mqtt.username.label"),
description: t("mqtt.username.description"),
disabledBy: [
{
fieldName: "enabled",
@ -65,8 +66,8 @@ export const MQTT = () => {
{
type: "password",
name: "password",
label: "MQTT Password",
description: "MQTT password to use for default/custom servers",
label: t("mqtt.password.label"),
description: t("mqtt.password.description"),
disabledBy: [
{
fieldName: "enabled",
@ -76,9 +77,8 @@ export const MQTT = () => {
{
type: "toggle",
name: "encryptionEnabled",
label: "Encryption Enabled",
description:
"Enable or disable MQTT encryption. Note: All messages are sent to the MQTT broker unencrypted if this option is not enabled, even when your uplink channels have encryption keys set. This includes position data.",
label: t("mqtt.encryptionEnabled.label"),
description: t("mqtt.encryptionEnabled.description"),
disabledBy: [
{
fieldName: "enabled",
@ -88,8 +88,8 @@ export const MQTT = () => {
{
type: "toggle",
name: "jsonEnabled",
label: "JSON Enabled",
description: "Whether to send/consume JSON packets on MQTT",
label: t("mqtt.jsonEnabled.label"),
description: t("mqtt.jsonEnabled.description"),
disabledBy: [
{
fieldName: "enabled",
@ -99,8 +99,8 @@ export const MQTT = () => {
{
type: "toggle",
name: "tlsEnabled",
label: "TLS Enabled",
description: "Enable or disable TLS",
label: t("mqtt.tlsEnabled.label"),
description: t("mqtt.tlsEnabled.description"),
disabledBy: [
{
fieldName: "enabled",
@ -110,8 +110,8 @@ export const MQTT = () => {
{
type: "text",
name: "root",
label: "Root topic",
description: "MQTT root topic to use for default/custom servers",
label: t("mqtt.root.label"),
description: t("mqtt.root.description"),
disabledBy: [
{
fieldName: "enabled",
@ -121,9 +121,8 @@ export const MQTT = () => {
{
type: "toggle",
name: "proxyToClientEnabled",
label: "Proxy to Client Enabled",
description:
"Use the client's internet connection for MQTT (feature only active in mobile apps)",
label: t("mqtt.proxyToClientEnabled.label"),
description: t("mqtt.proxyToClientEnabled.description"),
disabledBy: [
{
fieldName: "enabled",
@ -133,8 +132,8 @@ export const MQTT = () => {
{
type: "toggle",
name: "mapReportingEnabled",
label: "Map Reporting Enabled",
description: "Enable or disable map reporting",
label: t("mqtt.mapReportingEnabled.label"),
description: t("mqtt.mapReportingEnabled.description"),
disabledBy: [
{
fieldName: "enabled",
@ -144,10 +143,12 @@ export const MQTT = () => {
{
type: "number",
name: "mapReportSettings.publishIntervalSecs",
label: "Map Report Publish Interval (s)",
description: "Interval in seconds to publish map reports",
label: t("mqtt.mapReportSettings.publishIntervalSecs.label"),
description: t(
"mqtt.mapReportSettings.publishIntervalSecs.description",
),
properties: {
suffix: "Seconds",
suffix: t("unit.second.plural"),
},
disabledBy: [
{
@ -161,34 +162,77 @@ export const MQTT = () => {
{
type: "select",
name: "mapReportSettings.positionPrecision",
label: "Approximate Location",
description:
"Position shared will be accurate within this distance",
label: t(
"mqtt.mapReportSettings.positionPrecision.label",
),
description: t(
"mqtt.mapReportSettings.positionPrecision.description",
),
properties: {
enumValue: config.display?.units === 0
? {
"Within 23 km": 10,
"Within 12 km": 11,
"Within 5.8 km": 12,
"Within 2.9 km": 13,
"Within 1.5 km": 14,
"Within 700 m": 15,
"Within 350 m": 16,
"Within 200 m": 17,
"Within 90 m": 18,
"Within 50 m": 19,
[
t("mqtt.mapReportSettings.positionPrecision.options.metric_km23")
]: 10,
[
t("mqtt.mapReportSettings.positionPrecision.options.metric_km12")
]: 11,
[
t("mqtt.mapReportSettings.positionPrecision.options.metric_km5_8")
]: 12,
[
t("mqtt.mapReportSettings.positionPrecision.options.metric_km2_9")
]: 13,
[
t("mqtt.mapReportSettings.positionPrecision.options.metric_km1_5")
]: 14,
[
t("mqtt.mapReportSettings.positionPrecision.options.metric_m700")
]: 15,
[
t("mqtt.mapReportSettings.positionPrecision.options.metric_m350")
]: 16,
[
t("mqtt.mapReportSettings.positionPrecision.options.metric_m200")
]: 17,
[
t("mqtt.mapReportSettings.positionPrecision.options.metric_m90")
]: 18,
[
t("mqtt.mapReportSettings.positionPrecision.options.metric_m50")
]: 19,
}
: {
"Within 15 miles": 10,
"Within 7.3 miles": 11,
"Within 3.6 miles": 12,
"Within 1.8 miles": 13,
"Within 0.9 miles": 14,
"Within 0.5 miles": 15,
"Within 0.2 miles": 16,
"Within 600 feet": 17,
"Within 300 feet": 18,
"Within 150 feet": 19,
[
t("mqtt.mapReportSettings.positionPrecision.options.imperial_mi15")
]: 10,
[
t("mqtt.mapReportSettings.positionPrecision.options.imperial_mi7_3")
]: 11,
[
t("mqtt.mapReportSettings.positionPrecision.options.imperial_mi3_6")
]: 12,
[
t("mqtt.mapReportSettings.positionPrecision.options.imperial_mi1_8")
]: 13,
[
t("mqtt.mapReportSettings.positionPrecision.options.imperial_mi0_9")
]: 14,
[
t("mqtt.mapReportSettings.positionPrecision.options.imperial_mi0_5")
]: 15,
[
t("mqtt.mapReportSettings.positionPrecision.options.imperial_mi0_2")
]: 16,
[
t("mqtt.mapReportSettings.positionPrecision.options.imperial_ft600")
]: 17,
[
t("mqtt.mapReportSettings.positionPrecision.options.imperial_ft300")
]: 18,
[
t("mqtt.mapReportSettings.positionPrecision.options.imperial_ft150")
]: 19,
},
},
disabledBy: [

19
src/components/PageComponents/ModuleConfig/NeighborInfo.tsx

@ -1,11 +1,13 @@
import { useDevice } from "@core/stores/deviceStore.ts";
import type { NeighborInfoValidation } from "@app/validation/moduleConfig/neighborInfo.tsx";
import type { NeighborInfoValidation } from "@app/validation/moduleConfig/neighborInfo.ts";
import { create } from "@bufbuild/protobuf";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
export const NeighborInfo = () => {
const { moduleConfig, setWorkingModuleConfig } = useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: NeighborInfoValidation) => {
setWorkingModuleConfig(
@ -24,23 +26,22 @@ export const NeighborInfo = () => {
defaultValues={moduleConfig.neighborInfo}
fieldGroups={[
{
label: "Neighbor Info Settings",
description: "Settings for the Neighbor Info module",
label: t("neighborInfo.title"),
description: t("neighborInfo.description"),
fields: [
{
type: "toggle",
name: "enabled",
label: "Enabled",
description: "Enable or disable Neighbor Info Module",
label: t("neighborInfo.enabled.label"),
description: t("neighborInfo.enabled.description"),
},
{
type: "number",
name: "updateInterval",
label: "Update Interval",
description:
"Interval in seconds of how often we should try to send our Neighbor Info to the mesh",
label: t("neighborInfo.updateInterval.label"),
description: t("neighborInfo.updateInterval.description"),
properties: {
suffix: "Seconds",
suffix: t("unit.second.plural"),
},
disabledBy: [
{

27
src/components/PageComponents/ModuleConfig/Paxcounter.tsx

@ -3,9 +3,11 @@ import { create } from "@bufbuild/protobuf";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
export const Paxcounter = () => {
const { moduleConfig, setWorkingModuleConfig } = useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: PaxcounterValidation) => {
setWorkingModuleConfig(
@ -24,23 +26,22 @@ export const Paxcounter = () => {
defaultValues={moduleConfig.paxcounter}
fieldGroups={[
{
label: "Paxcounter Settings",
description: "Settings for the Paxcounter module",
label: t("paxcounter.title"),
description: t("paxcounter.description"),
fields: [
{
type: "toggle",
name: "enabled",
label: "Module Enabled",
description: "Enable Paxcounter",
label: t("paxcounter.enabled.label"),
description: t("paxcounter.enabled.description"),
},
{
type: "number",
name: "paxcounterUpdateInterval",
label: "Update Interval (seconds)",
description:
"How long to wait between sending paxcounter packets",
label: t("paxcounter.paxcounterUpdateInterval.label"),
description: t("paxcounter.paxcounterUpdateInterval.description"),
properties: {
suffix: "Seconds",
suffix: t("unit.second.plural"),
},
disabledBy: [
{
@ -51,9 +52,8 @@ export const Paxcounter = () => {
{
type: "number",
name: "wifiThreshold",
label: "WiFi RSSI Threshold",
description:
"At what WiFi RSSI level should the counter increase. Defaults to -80.",
label: t("paxcounter.wifiThreshold.label"),
description: t("paxcounter.wifiThreshold.description"),
disabledBy: [
{
fieldName: "enabled",
@ -63,9 +63,8 @@ export const Paxcounter = () => {
{
type: "number",
name: "bleThreshold",
label: "BLE RSSI Threshold",
description:
"At what BLE RSSI level should the counter increase. Defaults to -80.",
label: t("paxcounter.bleThreshold.label"),
description: t("paxcounter.bleThreshold.description"),
disabledBy: [
{
fieldName: "enabled",

22
src/components/PageComponents/ModuleConfig/RangeTest.tsx

@ -1,11 +1,13 @@
import type { RangeTestValidation } from "@app/validation/moduleConfig/rangeTest.tsx";
import type { RangeTestValidation } from "@app/validation/moduleConfig/rangeTest.ts";
import { create } from "@bufbuild/protobuf";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
export const RangeTest = () => {
const { moduleConfig, setWorkingModuleConfig } = useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: RangeTestValidation) => {
setWorkingModuleConfig(
@ -24,22 +26,22 @@ export const RangeTest = () => {
defaultValues={moduleConfig.rangeTest}
fieldGroups={[
{
label: "Range Test Settings",
description: "Settings for the Range Test module",
label: t("rangeTest.title"),
description: t("rangeTest.description"),
fields: [
{
type: "toggle",
name: "enabled",
label: "Module Enabled",
description: "Enable Range Test",
label: t("rangeTest.enabled.label"),
description: t("rangeTest.enabled.description"),
},
{
type: "number",
name: "sender",
label: "Message Interval",
description: "How long to wait between sending test packets",
label: t("rangeTest.sender.label"),
description: t("rangeTest.sender.description"),
properties: {
suffix: "Seconds",
suffix: t("unit.second.plural"),
},
disabledBy: [
{
@ -50,8 +52,8 @@ export const RangeTest = () => {
{
type: "toggle",
name: "save",
label: "Save CSV to storage",
description: "ESP32 Only",
label: t("rangeTest.save.label"),
description: t("rangeTest.save.description"),
disabledBy: [
{
fieldName: "enabled",

46
src/components/PageComponents/ModuleConfig/Serial.tsx

@ -1,11 +1,13 @@
import type { SerialValidation } from "@app/validation/moduleConfig/serial.tsx";
import type { SerialValidation } from "@app/validation/moduleConfig/serial.ts";
import { create } from "@bufbuild/protobuf";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
export const Serial = () => {
const { moduleConfig, setWorkingModuleConfig } = useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: SerialValidation) => {
setWorkingModuleConfig(
@ -24,21 +26,20 @@ export const Serial = () => {
defaultValues={moduleConfig.serial}
fieldGroups={[
{
label: "Serial Settings",
description: "Settings for the Serial module",
label: t("serial.title"),
description: t("serial.description"),
fields: [
{
type: "toggle",
name: "enabled",
label: "Module Enabled",
description: "Enable Serial output",
label: t("serial.enabled.label"),
description: t("serial.enabled.description"),
},
{
type: "toggle",
name: "echo",
label: "Echo",
description:
"Any packets you send will be echoed back to your device",
label: t("serial.echo.label"),
description: t("serial.echo.description"),
disabledBy: [
{
fieldName: "enabled",
@ -48,8 +49,8 @@ export const Serial = () => {
{
type: "number",
name: "rxd",
label: "Receive Pin",
description: "Set the GPIO pin to the RXD pin you have set up.",
label: t("serial.rxd.label"),
description: t("serial.rxd.description"),
disabledBy: [
{
fieldName: "enabled",
@ -59,8 +60,8 @@ export const Serial = () => {
{
type: "number",
name: "txd",
label: "Transmit Pin",
description: "Set the GPIO pin to the TXD pin you have set up.",
label: t("serial.txd.label"),
description: t("serial.txd.description"),
disabledBy: [
{
fieldName: "enabled",
@ -70,8 +71,8 @@ export const Serial = () => {
{
type: "select",
name: "baud",
label: "Baud Rate",
description: "The serial baud rate",
label: t("serial.baud.label"),
description: t("serial.baud.description"),
disabledBy: [
{
@ -86,24 +87,22 @@ export const Serial = () => {
{
type: "number",
name: "timeout",
label: "Timeout",
description:
"Seconds to wait before we consider your packet as 'done'",
label: t("serial.timeout.label"),
description: t("serial.timeout.description"),
disabledBy: [
{
fieldName: "enabled",
},
],
properties: {
suffix: "Seconds",
suffix: t("unit.second.plural"),
},
},
{
type: "select",
name: "mode",
label: "Mode",
description: "Select Mode",
label: t("serial.mode.label"),
description: t("serial.mode.description"),
disabledBy: [
{
@ -119,9 +118,8 @@ export const Serial = () => {
{
type: "toggle",
name: "overrideConsoleSerialPort",
label: "Override Console Serial Port",
description:
"If you have a serial port connected to the console, this will override it.",
label: t("serial.overrideConsoleSerialPort.label"),
description: t("serial.overrideConsoleSerialPort.description"),
},
],
},

29
src/components/PageComponents/ModuleConfig/StoreForward.tsx

@ -3,9 +3,11 @@ import { create } from "@bufbuild/protobuf";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
export const StoreForward = () => {
const { moduleConfig, setWorkingModuleConfig } = useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: StoreForwardValidation) => {
setWorkingModuleConfig(
@ -24,20 +26,20 @@ export const StoreForward = () => {
defaultValues={moduleConfig.storeForward}
fieldGroups={[
{
label: "Store & Forward Settings",
description: "Settings for the Store & Forward module",
label: t("storeForward.title"),
description: t("storeForward.description"),
fields: [
{
type: "toggle",
name: "enabled",
label: "Module Enabled",
description: "Enable Store & Forward",
label: t("storeForward.enabled.label"),
description: t("storeForward.enabled.description"),
},
{
type: "toggle",
name: "heartbeat",
label: "Heartbeat Enabled",
description: "Enable Store & Forward heartbeat",
label: t("storeForward.heartbeat.label"),
description: t("storeForward.heartbeat.description"),
disabledBy: [
{
fieldName: "enabled",
@ -47,23 +49,22 @@ export const StoreForward = () => {
{
type: "number",
name: "records",
label: "Number of records",
description: "Number of records to store",
label: t("storeForward.records.label"),
description: t("storeForward.records.description"),
disabledBy: [
{
fieldName: "enabled",
},
],
properties: {
suffix: "Records",
suffix: t("unit.record.plural"),
},
},
{
type: "number",
name: "historyReturnMax",
label: "History return max",
description: "Max number of records to return",
label: t("storeForward.historyReturnMax.label"),
description: t("storeForward.historyReturnMax.description"),
disabledBy: [
{
fieldName: "enabled",
@ -73,8 +74,8 @@ export const StoreForward = () => {
{
type: "number",
name: "historyReturnWindow",
label: "History return window",
description: "Max number of records to return",
label: t("storeForward.historyReturnWindow.label"),
description: t("storeForward.historyReturnWindow.description"),
disabledBy: [
{
fieldName: "enabled",

54
src/components/PageComponents/ModuleConfig/Telemetry.tsx

@ -3,9 +3,11 @@ import { create } from "@bufbuild/protobuf";
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { Protobuf } from "@meshtastic/core";
import { useTranslation } from "react-i18next";
export const Telemetry = () => {
const { moduleConfig, setWorkingModuleConfig } = useDevice();
const { t } = useTranslation("moduleConfig");
const onSubmit = (data: TelemetryValidation) => {
setWorkingModuleConfig(
@ -24,74 +26,78 @@ export const Telemetry = () => {
defaultValues={moduleConfig.telemetry}
fieldGroups={[
{
label: "Telemetry Settings",
description: "Settings for the Telemetry module",
label: t("telemetry.title"),
description: t("telemetry.description"),
fields: [
{
type: "number",
name: "deviceUpdateInterval",
label: "Device Metrics",
description: "Device metrics update interval (seconds)",
label: t("telemetry.deviceUpdateInterval.label"),
description: t("telemetry.deviceUpdateInterval.description"),
properties: {
suffix: "Seconds",
suffix: t("unit.second.plural"),
},
},
{
type: "number",
name: "environmentUpdateInterval",
label: "Environment metrics update interval (seconds)",
description: "",
label: t("telemetry.environmentUpdateInterval.label"),
description: t("telemetry.environmentUpdateInterval.description"),
properties: {
suffix: "Seconds",
suffix: t("unit.second.plural"),
},
},
{
type: "toggle",
name: "environmentMeasurementEnabled",
label: "Module Enabled",
description: "Enable the Environment Telemetry",
label: t("telemetry.environmentMeasurementEnabled.label"),
description: t(
"telemetry.environmentMeasurementEnabled.description",
),
},
{
type: "toggle",
name: "environmentScreenEnabled",
label: "Displayed on Screen",
description: "Show the Telemetry Module on the OLED",
label: t("telemetry.environmentScreenEnabled.label"),
description: t("telemetry.environmentScreenEnabled.description"),
},
{
type: "toggle",
name: "environmentDisplayFahrenheit",
label: "Display Fahrenheit",
description: "Display temp in Fahrenheit",
label: t("telemetry.environmentDisplayFahrenheit.label"),
description: t(
"telemetry.environmentDisplayFahrenheit.description",
),
},
{
type: "toggle",
name: "airQualityEnabled",
label: "Air Quality Enabled",
description: "Enable the Air Quality Telemetry",
label: t("telemetry.airQualityEnabled.label"),
description: t("telemetry.airQualityEnabled.description"),
},
{
type: "number",
name: "airQualityInterval",
label: "Air Quality Update Interval",
description: "How often to send Air Quality data over the mesh",
label: t("telemetry.airQualityInterval.label"),
description: t("telemetry.airQualityInterval.description"),
},
{
type: "toggle",
name: "powerMeasurementEnabled",
label: "Power Measurement Enabled",
description: "Enable the Power Measurement Telemetry",
label: t("telemetry.powerMeasurementEnabled.label"),
description: t("telemetry.powerMeasurementEnabled.description"),
},
{
type: "number",
name: "powerUpdateInterval",
label: "Power Update Interval",
description: "How often to send Power data over the mesh",
label: t("telemetry.powerUpdateInterval.label"),
description: t("telemetry.powerUpdateInterval.description"),
},
{
type: "toggle",
name: "powerScreenEnabled",
label: "Power Screen Enabled",
description: "Enable the Power Telemetry Screen",
label: t("telemetry.powerScreenEnabled.label"),
description: t("telemetry.powerScreenEnabled.description"),
},
],
},

66
src/components/Sidebar.tsx

@ -1,4 +1,4 @@
import React from "react";
import React, { useEffect, useState, useTransition } from "react";
import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.tsx";
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
@ -25,7 +25,7 @@ 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 useTranslation
import { useTranslation } from "react-i18next";
export interface SidebarProps {
children?: React.ReactNode;
@ -40,10 +40,10 @@ interface NavLink {
const CollapseToggleButton = () => {
const { isCollapsed, toggleSidebar } = useSidebar();
const { t } = useTranslation();
const { t } = useTranslation("ui");
const buttonLabel = isCollapsed
? t("sidebar_collapseToggle-button_open")
: t("sidebar_collapseToggle_button_close");
? t("sidebar.collapseToggle.button.open")
: t("sidebar.collapseToggle.button.close");
return (
<button
@ -83,33 +83,47 @@ export const Sidebar = ({ children }: SidebarProps) => {
const { setCommandPaletteOpen } = useAppStore();
const myNode = getNode(hardware.myNodeNum);
const { isCollapsed } = useSidebar();
const { t } = useTranslation();
const { t } = useTranslation("ui");
const myMetadata = metadata.get(0);
const numUnread = [...unreadCounts.values()].reduce((sum, v) => sum + v, 0);
const [displayedNodeCount, setDisplayedNodeCount] = useState(() =>
Math.max(getNodesLength() - 1, 0)
);
const [_, startNodeCountTransition] = useTransition();
const currentNodeCountValue = Math.max(getNodesLength() - 1, 0);
useEffect(() => {
if (currentNodeCountValue !== displayedNodeCount) {
startNodeCountTransition(() => {
setDisplayedNodeCount(currentNodeCountValue);
});
}
}, [currentNodeCountValue, displayedNodeCount, startNodeCountTransition]);
const pages: NavLink[] = [
{
name: t("navigation_title_messages"),
name: t("navigation.messages"),
icon: MessageSquareIcon,
page: "messages",
count: numUnread ? numUnread : undefined,
},
{ name: t("navigation_title_map"), icon: MapIcon, page: "map" },
{ name: t("navigation.map"), icon: MapIcon, page: "map" },
{
name: t("navigation_title_config"),
name: t("navigation.config"),
icon: SettingsIcon,
page: "config",
},
{
name: t("navigation_title_channels"),
name: t("navigation.channels"),
icon: LayersIcon,
page: "channels",
},
{
name: `${t("navigation_title_nodes")} (${
Math.max(getNodesLength() - 1, 0)
})`,
name: `${t("navigation.nodes")} (${displayedNodeCount})`,
icon: UsersIcon,
page: "nodes",
},
@ -134,7 +148,7 @@ export const Sidebar = ({ children }: SidebarProps) => {
>
<img
src="Logo.svg"
alt={t("sidebar_app_title_alt")}
alt={t("app.logo")}
className="size-10 flex-shrink-0 rounded-xl"
/>
<h2
@ -146,11 +160,14 @@ export const Sidebar = ({ children }: SidebarProps) => {
: "opacity-100 max-w-xs visible ml-2",
)}
>
{t("common_header")}
{t("app.title")}
</h2>
</div>
<SidebarSection label={t("navigation_title")} className="mt-4 px-0">
<SidebarSection
label={t("navigation.title")}
className="mt-4 px-0"
>
{pages.map((link) => (
<SidebarButton
key={link.name}
@ -188,7 +205,7 @@ export const Sidebar = ({ children }: SidebarProps) => {
isCollapsed ? "opacity-0 invisible" : "opacity-100 visible",
)}
>
{t("common_loading")}
{t("loading")}
</Subtle>
</div>
)
@ -201,7 +218,8 @@ export const Sidebar = ({ children }: SidebarProps) => {
)}
>
<Avatar
text={myNode.user?.shortName ?? myNode.num.toString()}
text={myNode.user?.shortName ??
t("unknown.shortName")}
className={cn("flex-shrink-0 ml-2", isCollapsed && "ml-0")}
size="sm"
/>
@ -236,9 +254,9 @@ export const Sidebar = ({ children }: SidebarProps) => {
className="text-gray-500 dark:text-gray-400 w-4 flex-shrink-0"
/>
<Subtle>
{t("sidebar_deviceInfo_volts", {
{t("sidebar.deviceInfo.volts", {
voltage: myNode.deviceMetrics?.voltage?.toPrecision(3) ??
t("common_unknown_short"),
t("unknown.shortName"),
})}
</Subtle>
</div>
@ -248,9 +266,9 @@ export const Sidebar = ({ children }: SidebarProps) => {
className="text-gray-500 dark:text-gray-400 w-4 flex-shrink-0"
/>
<Subtle>
{t("sidebar_deviceInfo_firmwareVersion", {
{t("sidebar.deviceInfo.firmwareVersion", {
version: myMetadata?.firmwareVersion ??
t("common_unknown_short"),
t("unknown.shortName"),
})}
</Subtle>
</div>
@ -266,7 +284,9 @@ export const Sidebar = ({ children }: SidebarProps) => {
>
<button
type="button"
aria-label={t("sidebar_deviceInfo_button_editDeviceName")}
aria-label={t(
"sidebar.deviceInfo.button.editDeviceName",
)}
className="p-1 rounded transition-colors cursor-pointer hover:text-accent"
onClick={() => setDialogOpen("deviceName", true)}
>

10
src/components/ThemeSwitcher.tsx

@ -11,7 +11,7 @@ export default function ThemeSwitcher({
className?: string;
}) {
const { preference, setPreference } = useTheme();
const { t } = useTranslation();
const { t } = useTranslation("ui");
const themeIcons = {
light: <Sun className="size-6" />,
@ -27,9 +27,9 @@ export default function ThemeSwitcher({
};
const preferenceDisplayMap: Record<ThemePreference, string> = {
light: t("theme_preference_light"),
dark: t("theme_preference_dark"),
system: t("theme_preference_system"),
light: t("theme.light"),
dark: t("theme.dark"),
system: t("theme.system"),
};
const currentDisplayPreference = preferenceDisplayMap[preference];
@ -42,7 +42,7 @@ export default function ThemeSwitcher({
className,
)}
onClick={toggleTheme}
aria-description={t("theme_switcher_aria_change_theme")}
aria-description={t("theme.changeTheme")}
>
<span
data-label

2
src/components/UI/Avatar.tsx

@ -85,7 +85,7 @@ export const Avatar = ({
const bgColor = getColorFromText(safeText);
const isLight = ColorUtils.isLight(bgColor);
const textColor = isLight ? "#000000" : "#FFFFFF";
const initials = safeText?.slice(0, 4) ?? t("common_unknown_short");
const initials = safeText?.slice(0, 4) ?? t("unknown.shortName");
return (
<div

2
src/components/UI/Footer.tsx

@ -16,7 +16,7 @@ const Footer = ({ className, ...props }: FooterProps) => {
>
<p>
<Trans
i18nKey="footer_text"
i18nKey="footer.text"
components={[
<a
key="vercel"

22
src/components/UI/Input.tsx

@ -63,7 +63,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
) => {
const { isVisible, toggleVisibility } = usePasswordVisibilityToggle();
const { copy, isCopied } = useCopyToClipboard({ timeout: 1500 });
const { t } = useTranslation();
const { t } = useTranslation("ui");
const potentialActions: InputActionType[] = [
{
@ -82,8 +82,8 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
ref.current.focus();
}
},
ariaLabel: t("input_action_clearInput_label"),
tooltip: t("input_action_clearInput_label"),
ariaLabel: t("filters.clearInput.label"),
tooltip: t("filters.clearInput.label"),
condition: !!showClearButton && !!value,
},
{
@ -94,11 +94,11 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
toggleVisibility();
},
ariaLabel: isVisible
? t("input_action_hidePassword_label")
: t("input_action_showPassword_label"),
? t("notifications.hidePassword.label")
: t("notifications.showPassword.label"),
tooltip: isVisible
? t("input_action_hidePassword_label")
: t("input_action_showPassword_label"),
? t("notifications.hidePassword.label")
: t("notifications.showPassword.label"),
condition: !!showPasswordToggle && type === "password",
},
{
@ -111,11 +111,11 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
}
},
ariaLabel: isCopied
? t("input_action_copied_label")
: t("input_action_copyToClipboard_label"),
? t("notifications.copied.label")
: t("notifications.copyToClipboard.label"),
tooltip: isCopied
? t("input_action_copied_label")
: t("input_action_copyToClipboard_label"),
? t("notifications.copied.label")
: t("notifications.copyToClipboard.label"),
condition: !!showCopyButton,
},
];

335
src/components/generic/Filter/FilterControl.tsx

@ -1,15 +1,19 @@
import {
type ComponentProps,
ReactNode,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import { Protobuf } from "@meshtastic/core";
import { FunnelIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
import type { TFunction } from "i18next";
import { debounce } from "@core/utils/debounce.ts";
import { cn } from "@core/utils/cn.ts";
import { TimeAgo } from "@components/generic/TimeAgo.tsx";
import type { FilterState } from "@components/generic/Filter/useFilterNode.ts";
import {
Popover,
@ -18,15 +22,19 @@ import {
} from "@components/UI/Popover.tsx";
import { Input } from "@components/UI/Input.tsx";
import { Accordion } from "@components/UI/Accordion.tsx";
import { FunnelIcon } from "lucide-react";
import { TimeAgo } from "@components/generic/TimeAgo.tsx";
import {
FilterAccordionItem,
FilterMulti,
FilterSlider,
FilterToggle,
} from "@components/generic/Filter/FilterComponents.tsx";
import { useTranslation } from "react-i18next";
import type { FilterState } from "@components/generic/Filter/useFilterNode.ts";
const DEBOUNCE_DELAY_MS = 250;
const BATTERY_STATUS_PLUGGED_IN_VALUE = 101;
type PopoverContentProps = ComponentProps<typeof PopoverContent>;
@ -41,10 +49,93 @@ interface FilterControlProps {
popoverTriggerClassName?: string;
showTextSearch?: boolean;
};
children?: ReactNode;
}
interface HopsLabelProps {
hopsAway: number[];
t: TFunction<"ui", undefined>;
}
function HopsLabelContent({ hopsAway, t }: HopsLabelProps) {
const startHops = hopsAway[0];
const endHops = hopsAway[1];
return (
<>
{t("filters.hops.text", {
value: startHops === 0 ? t("filters.hops.direct") : startHops,
})}
{startHops !== endHops ? `${endHops}` : ""}
</>
);
}
interface LastHeardLabelProps {
lastHeardRange: number[];
defaultMaxLastHeard: number;
formatTS: (ts: number) => ReactNode;
t: TFunction<"ui", undefined>;
}
function LastHeardLabelContent(
{ lastHeardRange, defaultMaxLastHeard, formatTS, t }: LastHeardLabelProps,
) {
const [start, end] = lastHeardRange;
return (
<>
{t("filters.lastHeard.labelText", { value: "" })}
<br />
{start === 0
? (
t("filters.lastHeard.nowLabel")
)
: (
<>
{start === defaultMaxLastHeard && ">"}
{formatTS(start)}
</>
)}
{start !== end && (
<>
{" — "}
{end === defaultMaxLastHeard && ">"}
{formatTS(end)}
</>
)}
</>
);
}
interface BatteryLevelLabelProps {
batteryLevelRange: (number | undefined)[];
t: TFunction<"ui", undefined>;
}
function BatteryLevelLabelContent(
{ batteryLevelRange, t }: BatteryLevelLabelProps,
) {
const [start, end] = batteryLevelRange;
const formatBatteryValue = (value: number | undefined) => {
if (value === undefined) return "";
return value === BATTERY_STATUS_PLUGGED_IN_VALUE
? t("batteryStatus.pluggedIn")
: `${value}%`;
};
return (
<>
{t("filters.batteryLevel.labelText", {
value: formatBatteryValue(start),
})}
{start !== end && typeof end !== "undefined" && (
<>
{" – "}
{formatBatteryValue(end)}
</>
)}
</>
);
}
export function FilterControl({
filterState,
defaultFilterValues,
@ -53,70 +144,78 @@ export function FilterControl({
parameters,
children,
}: FilterControlProps) {
const { t } = useTranslation();
// Copy of the state that we only use for rendering sliders and their labels directly, rest is debounced
const { t } = useTranslation("ui");
const [localFilterState, setLocalFilterState] = useState(filterState);
const skipNextSync = useRef(false);
const skipNextFilterStateSync = useRef(false);
useEffect(() => {
if (skipNextSync.current) {
skipNextSync.current = false;
if (skipNextFilterStateSync.current) {
skipNextFilterStateSync.current = false;
return;
}
setLocalFilterState(filterState);
}, [filterState]);
const handleTextChange =
const handleTextChange = useCallback(
<K extends keyof FilterState>(key: K) =>
(e: React.ChangeEvent<HTMLInputElement>) => {
setFilterState((prev) => ({
...prev,
[key]: e.target.value,
}));
};
const handleRangeChange =
},
[setFilterState],
);
const debouncedSetFilterState = useCallback(
debounce(<K extends keyof FilterState>(key: K, value: number[]) => {
skipNextFilterStateSync.current = true;
setFilterState((prev) => ({
...prev,
[key]: value,
}));
}, DEBOUNCE_DELAY_MS),
[setFilterState],
);
const handleRangeChange = useCallback(
<K extends keyof FilterState>(key: K) => (value: number[]) => {
// immediate slider update
setLocalFilterState((prev) => ({
...prev,
[key]: value,
}));
debouncedSetFilterState(key, value);
},
[debouncedSetFilterState],
);
// debounced write to filterState (table/map render)
debounce(
() => {
skipNextSync.current = true;
setFilterState((prev) => ({
...prev,
[key]: value,
}));
},
250,
)();
};
const handleBoolChange = <K extends keyof FilterState>(
key: K,
value: string,
) => {
const typedValue = value === ""
? undefined
: JSON.parse(value.toLowerCase());
const handleBoolChange = useCallback(
<K extends keyof FilterState>(key: K, value: string) => {
const typedValue = value === ""
? undefined
: JSON.parse(value.toLowerCase());
setFilterState((prev) => ({
...prev,
[key]: typedValue,
}));
};
setFilterState((prev) => ({
...prev,
[key]: typedValue,
}));
},
[setFilterState],
);
const resetFilters = () => {
const resetFilters = useCallback(() => {
setFilterState(defaultFilterValues);
};
}, [defaultFilterValues, setFilterState]);
const formatTS = useCallback(
(ts: number): ReactNode => <TimeAgo timestamp={Date.now() - ts * 1000} />,
[],
);
function formatTS(ts: number): ReactNode {
return <TimeAgo timestamp={Date.now() - ts * 1000} />;
}
function formatEnumLabel(label: string): string {
return label.replace(/_/g, " ");
}
const formatEnumLabel = useCallback(
(label: string): string => label.replace(/_/g, " "),
[],
);
return (
<Popover>
@ -132,7 +231,7 @@ export function FilterControl({
: "",
parameters?.popoverTriggerClassName,
)}
aria-label={t("filter_control_button_filter_ariaLabel")}
aria-label={t("filters.filter.label")}
>
{parameters?.triggerIcon ?? <FunnelIcon />}
</button>
@ -147,188 +246,142 @@ export function FilterControl({
<form className="space-y-4">
<Accordion
type="single"
defaultValue={t("filter_control_accordion_general_label")}
defaultValue={t("filters.general.label")}
collapsible
>
<FilterAccordionItem
label={t("filter_control_accordion_general_label")}
>
<FilterAccordionItem label={t("filters.general.label")}>
{(parameters?.showTextSearch ?? true) && (
<div className="flex flex-col space-y-1 pb-2">
<label htmlFor="nodeName" className="font-medium text-sm">
{t("filter_control_input_nodeName_label")}
{t("filters.nodeName.label")}
</label>
<Input
type="text"
id="nodeName"
value={filterState.nodeName}
onChange={handleTextChange("nodeName")}
showClearButton
placeholder={t("filter_control_input_nodeName_placeholder")}
placeholder={t("filters.nodeName.placeholder")}
/>
</div>
)}
<FilterSlider
label={t("filter_control_slider_hops_label")}
label={t("filters.hops.label")}
filterKey="hopsAway"
filterState={localFilterState}
defaultFilterValues={defaultFilterValues}
onChange={handleRangeChange}
labelContent={
<>
{t("filter_control_slider_hops_labelText", {
value: localFilterState.hopsAway[0] === 0
? t("filter_control_slider_hops_directLabel")
: localFilterState.hopsAway[0],
})}
{localFilterState.hopsAway[0] !==
localFilterState.hopsAway[1]
? `${localFilterState.hopsAway[1]}`
: ""}
</>
<HopsLabelContent
hopsAway={localFilterState.hopsAway}
t={t}
/>
}
/>
<FilterSlider
label={t("filter_control_slider_lastHeard_label")}
label={t("filters.lastHeard.label")}
filterKey="lastHeard"
filterState={localFilterState}
defaultFilterValues={defaultFilterValues}
onChange={handleRangeChange}
labelContent={
<>
{t("filter_control_slider_lastHeard_labelText", {
value: "",
})}
<br />
{localFilterState.lastHeard[0] === 0
? t("filter_control_slider_lastHeard_nowLabel")
: (
<>
{localFilterState.lastHeard[0] ===
defaultFilterValues.lastHeard[1] && ">"}
{formatTS(localFilterState.lastHeard[0])}
</>
)}
{localFilterState.lastHeard[0] !==
localFilterState.lastHeard[1] && (
<>
{" — "}
{localFilterState.lastHeard[1] ===
defaultFilterValues.lastHeard[1] && ">"}
{formatTS(localFilterState.lastHeard[1])}
</>
)}
</>
<LastHeardLabelContent
lastHeardRange={localFilterState.lastHeard}
defaultMaxLastHeard={defaultFilterValues.lastHeard[1]}
formatTS={formatTS}
t={t}
/>
}
/>
<FilterToggle
label={t("filter_control_toggle_favorites_label")}
label={t("filters.favorites.label")}
filterKey="isFavorite"
alternativeLabels={[
t("filter_control_toggle_hide_label"),
t("filter_control_toggle_showOnly_label"),
t("filters.hide.label"),
t("filters.showOnly.label"),
]}
filterState={filterState}
onChange={handleBoolChange}
/>
<FilterToggle
label={t("filter_control_toggle_viaMqtt_label")}
label={t("filters.viaMqtt.label")}
filterKey="viaMqtt"
alternativeLabels={[
t("filter_control_toggle_hide_label"),
t("filter_control_toggle_showOnly_label"),
t("filters.hide.label"),
t("filters.showOnly.label"),
]}
filterState={filterState}
onChange={handleBoolChange}
/>
</FilterAccordionItem>
<FilterAccordionItem
label={t("filter_control_accordion_metrics_label")}
>
<FilterAccordionItem label={t("filters.metrics.label")}>
<FilterSlider
label={t("filter_control_slider_snr_label")}
label={t("filters.snr.label")}
filterKey="snr"
filterState={localFilterState}
defaultFilterValues={defaultFilterValues}
onChange={handleRangeChange}
/>
<FilterSlider
label={t("filter_control_slider_channelUtilization_label")}
label={t("filters.channelUtilization.label")}
filterKey="channelUtilization"
filterState={localFilterState}
defaultFilterValues={defaultFilterValues}
onChange={handleRangeChange}
/>
<FilterSlider
label={t("filter_control_slider_airtimeUtilization_label")}
label={t("filters.airtimeUtilization.label")}
filterKey="airUtilTx"
filterState={localFilterState}
defaultFilterValues={defaultFilterValues}
onChange={handleRangeChange}
/>
<FilterSlider
label={t("filter_control_slider_batteryLevel_label")}
label={t("filters.batteryLevel.label")}
filterKey="batteryLevel"
filterState={localFilterState}
defaultFilterValues={defaultFilterValues}
onChange={handleRangeChange}
labelContent={
<>
{t("filter_control_slider_batteryLevel_labelText", {
value: localFilterState.batteryLevel[0] === 101
? t("common_batteryStatus_pluggedIn")
: localFilterState.batteryLevel[0],
})}
{localFilterState.batteryLevel[0] !==
localFilterState.batteryLevel[1] && (
<>
{" – "}
{localFilterState.batteryLevel[1] === 101
? t("common_batteryStatus_pluggedIn")
: localFilterState.batteryLevel[1]}
</>
)}
</>
<BatteryLevelLabelContent
batteryLevelRange={localFilterState.batteryLevel}
t={t}
/>
}
/>
<FilterSlider
label={t("filter_control_slider_batteryVoltage_label")}
label={t("filters.batteryVoltage.label")}
filterKey="voltage"
filterState={localFilterState}
defaultFilterValues={defaultFilterValues}
onChange={handleRangeChange}
/>
</FilterAccordionItem>
<FilterAccordionItem
label={t("filter_control_accordion_role_label")}
>
<FilterAccordionItem label={t("filters.role.label")}>
<FilterMulti
filterKey="role"
filterState={filterState}
setFilterState={setFilterState}
options={Object.values(Protobuf.Config.Config_DeviceConfig_Role)
.filter(
(v): v is number => typeof v === "number",
)}
options={Object.values(
Protobuf.Config.Config_DeviceConfig_Role,
).filter((v): v is number => typeof v === "number")}
getLabel={(val) =>
formatEnumLabel(
Protobuf.Config.Config_DeviceConfig_Role[val],
)}
/>
</FilterAccordionItem>
<FilterAccordionItem
label={t("filter_control_accordion_hardware_label")}
>
<FilterAccordionItem label={t("filters.hardware.label")}>
<FilterMulti
filterKey="hwModel"
filterState={filterState}
setFilterState={setFilterState}
options={Object.values(Protobuf.Mesh.HardwareModel)
.filter(
(v): v is number => typeof v === "number",
)}
options={Object.values(Protobuf.Mesh.HardwareModel).filter(
(v): v is number => typeof v === "number",
)}
getLabel={(val) =>
formatEnumLabel(Protobuf.Mesh.HardwareModel[val])}
/>
@ -337,15 +390,11 @@ export function FilterControl({
<button
type="button"
onClick={resetFilters}
className="w-full py-1 shadow-sm hover:shadow-md bg-slate-600 dark:bg-slate-900 text-white rounded text-sm hover:text-slate-100 hover:bg-slate-700 active:bg-slate-950"
className="w-full py-1 shadow-sm hover:shadow-md bg-slate-600 dark:bg-slate-900 text-white rounded text-sm hover:text-slate-100 hover:bg-slate-700 active:bg-slate-950"
>
{t("filter_control_button_resetFilters_label")}
{t("filters.resetFilters.label")}
</button>
{children && (
<div className="mt-4 border-t pt-4">
{children}
</div>
)}
{children && <div className="mt-4 border-t pt-4">{children}</div>}
</form>
</PopoverContent>
</Popover>

92
src/core/hooks/useLang.ts

@ -0,0 +1,92 @@
import { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { LangCode } from "@app/i18n/config.ts";
import useLocalStorage from "./useLocalStorage.ts";
/**
* Hook to set the i18n language
*
* @returns The `set` function
*/
const STORAGE_KEY = "language";
type LanguageState = {
language: string;
};
function useLang() {
const { i18n } = useTranslation();
const [_, setLanguage] = useLocalStorage<LanguageState | null>(
STORAGE_KEY,
null,
);
const regionNames = useMemo(() => {
return new Intl.DisplayNames(i18n.language, {
type: "region",
fallback: "none",
style: "long",
});
}, [i18n.language]);
const collator = useMemo(() => {
return new Intl.Collator(i18n.language, {});
}, [i18n.language]);
/**
* Sets the i18n language.
*
* @param lng - The language tag to set
*/
const set = useCallback(
async (lng: LangCode, persist = true) => {
if (i18n.language === lng) {
return;
}
console.info("set language:", lng);
if (persist) {
try {
setLanguage({ language: lng });
} catch (e) {
console.warn(e);
}
await i18n.changeLanguage(lng);
}
},
[i18n],
);
/**
* Get the localized country name
*
* @param code - Two-letter country code
*/
const getCountryName = useCallback(
(code: LangCode) => {
let name = null;
try {
name = regionNames.of(code);
} catch (e) {
console.warn(e);
}
return name;
},
[regionNames],
);
/**
* Compare two strings according to the sort order of the current language
*
* @param a - The first string to compare
* @param b - The second string to compare
*/
const compare = useCallback(
(a: string, b: string) => {
return collator.compare(a, b);
},
[collator],
);
return { compare, set, getCountryName };
}
export default useLang;

22
src/core/hooks/usePositionFlags.ts

@ -1,35 +1,35 @@
import { useCallback, useMemo, useState } from "react";
export const FLAGS_CONFIG = {
UNSET: { value: 0, i18nKey: "position_flag_unset" },
ALTITUDE: { value: 1, i18nKey: "position_flag_altitude" },
ALTITUDE_MSL: { value: 2, i18nKey: "position_flag_altitude_msl" },
UNSET: { value: 0, i18nKey: "position.flags.unset" },
ALTITUDE: { value: 1, i18nKey: "position.flags.altitude" },
ALTITUDE_MSL: { value: 2, i18nKey: "position.flags.altitudeMsl" },
ALTITUDE_GEOIDAL_SEPARATION: {
value: 4,
i18nKey: "position_flag_altitude_geoidal_separation",
i18nKey: "position.flags.altitudeGeoidalSeparation",
},
DOP: {
value: 8,
i18nKey: "position_flag_dop",
i18nKey: "position.flags.dop",
},
HDOP_VDOP: {
value: 16,
i18nKey: "position_flag_hdop_vdop",
i18nKey: "position.flags.hdopVdop",
},
NUM_SATELLITES: {
value: 32,
i18nKey: "position_flag_num_satellites",
i18nKey: "position.flags.numSatellites",
},
SEQUENCE_NUMBER: {
value: 64,
i18nKey: "position_flag_sequence_number",
i18nKey: "position.flags.sequenceNumber",
},
TIMESTAMP: { value: 128, i18nKey: "position_flag_timestamp" },
TIMESTAMP: { value: 128, i18nKey: "position.flags.timestamp" },
VEHICLE_HEADING: {
value: 256,
i18nKey: "position_flag_vehicle_heading",
i18nKey: "position.flags.vehicleHeading",
},
VEHICLE_SPEED: { value: 512, i18nKey: "position_flag_vehicle_speed" },
VEHICLE_SPEED: { value: 512, i18nKey: "position.flags.vehicleSpeed" },
} as const;
export type FlagName = keyof typeof FLAGS_CONFIG;

28
src/i18n.ts

@ -1,28 +0,0 @@
import i18next from "i18next";
import { initReactI18next } from "react-i18next";
import Backend from "i18next-http-backend";
import LanguageDetector from "i18next-browser-languagedetector";
export const supportedLngs = {
en: "English",
};
i18next
.use(Backend)
.use(initReactI18next)
.use(LanguageDetector)
.init({
backend: {
// this will lazy load resources from the i8n folder
loadPath: "/src/i18n/locales/{{lng}}.json",
},
react: {
useSuspense: true,
},
fallbackLng: "en",
debug: !!import.meta.env.DEV,
supportedLngs: Object.keys(supportedLngs),
detection: {
order: ["navigator", "localStorage"],
},
});

51
src/i18n/config.ts

@ -0,0 +1,51 @@
import i18next from "i18next";
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 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" },
];
i18next
.use(Backend)
.use(initReactI18next)
.use(LanguageDetector)
.init({
backend: {
// this will lazy load resources from the i8n folder
loadPath: "/src/i18n/locales/{{lng}}/{{ns}}.json",
},
react: {
useSuspense: true,
},
detection: {
order: ["navigator", "localStorage"],
},
fallbackLng: {
"en-US": ["en"],
"en-CA": ["en-US", "en"],
"default": ["en"],
},
fallbackNS: ["common", "ui", "dialog"],
debug: import.meta.env.DEV,
supportedLngs: supportedLanguages?.map((lang) => lang.code),
ns: [
"channels",
"commandPalette",
"common",
"deviceConfig",
"configModules",
"dashboard",
"dialog",
"messages",
"nodes",
"ui",
],
});

607
src/i18n/locales/en.json

@ -1,607 +0,0 @@
{
"common_title": "Meshtastic Web",
"common_header": "Meshtastic",
"common_description": "Meshtastic Web Client",
"common_button_close": "Close",
"common_button_ok": "OK",
"common_button_cancel": "Cancel",
"common_button_delete": "Delete",
"common_button_import": "Import",
"common_button_export": "Export",
"common_button_scanQr": "Scan QR Code",
"common_button_generate": "Generate",
"common_button_save": "Save",
"common_label_primary": "Primary",
"common_loading": "Loading...",
"common_unknown_short": "UNK",
"common_unknown_long": "Unknown",
"common_unset": "UNSET",
"common_unit_volts": "Volts",
"common_unit_cps": "CPS",
"common_unit_dbm": "dBm",
"common_unit_hertz": "Hz",
"common_unit_megahertz": "MHz",
"common_unit_seconds": "Seconds",
"common_unit_raw": "raw",
"common_batteryStatus_title": "Battery",
"common_batteryStatus_pluggedIn": "Plugged in",
"common_batteryStatus_charging": "{{level}}% charging",
"navigation_title": "Navigation",
"navigation_title_messages": "Messages",
"navigation_title_map": "Map",
"navigation_title_config": "Config",
"navigation_title_radioConfig": "Radio Config",
"navigation_title_moduleConfig": "Module Config",
"navigation_title_channels": "Channels",
"navigation_title_nodes": "Nodes",
"dialog_button_apply": "Apply",
"dialog_button_clearMessages": "Clear Messages",
"dialog_button_confirm": "Confirm",
"dialog_button_dismiss": "Dismiss",
"dialog_button_download": "Download",
"dialog_button_message": "Message",
"dialog_button_now": "Now",
"dialog_button_print": "Print",
"dialog_button_rebootOtaNow": "Reboot to OTA Mode Now",
"dialog_button_remove": "Remove",
"dialog_button_requestNewKeys": "Request New Keys",
"dialog_button_requestPosition": "Request Position",
"dialog_button_reset": "Reset",
"dialog_button_traceRoute": "Trace Route",
"dialog_deleteMessages_description": "This action will clear all message history. This cannot be undone. Are you sure you want to continue?",
"dialog_deleteMessages_title": "Clear All Messages",
"dialog_deviceName_description": "The Device will restart once the config is saved.",
"dialog_deviceName_label_longName": "Long Name",
"dialog_deviceName_label_shortName": "Short Name",
"dialog_deviceName_title": "Change Device Name",
"dialog_import_description": "The current LoRa configuration will be overridden.",
"dialog_import_error_invalidUrl": "Invalid Meshtastic URL",
"dialog_import_label_channelPrefix": "Channel: ",
"dialog_import_label_channelSetUrl": "Channel Set/QR Code URL",
"dialog_import_label_channels": "Channels:",
"dialog_import_label_usePreset": "Use Preset?",
"dialog_import_title": "Import Channel Set",
"dialog_locationResponse_label_altitude": "Altitude: ",
"dialog_locationResponse_label_coordinates": "Coordinates: ",
"dialog_locationResponse_titlePrefix": "Location: ",
"dialog_locationResponse_unit_meter": "m",
"dialog_pkiRegenerateDialog_title": "Regenerate Pre-Shared Key?",
"dialog_pkiRegenerateDialog_description": "Are you sure you want to regenerate the pre-shared key?",
"dialog_pkiRegenerateDialog_buttonRegenerate": "Regenerate",
"dialog_newDeviceDialog_title": "Connect New Device",
"dialog_newDeviceDialog_tabLabelHttp": "HTTP",
"dialog_newDeviceDialog_tabLabelBluetooth": "Bluetooth",
"dialog_newDeviceDialog_tabLabelSerial": "Serial",
"dialog_newDeviceDialog_errorMessage_requiresFeatures": "This connection type requires <0></0>. Please use a supported browser, like Chrome or Edge.",
"dialog_newDeviceDialog_errorMessage_requiresSecureContext": "This application requires a <0>secure context</0>. Please connect using HTTPS or localhost.",
"dialog_newDeviceDialog_errorMessage_additionallyRequiresSecureContext": "Additionally, it requires a <0>secure context</0>. Please connect using HTTPS or localhost.",
"dialog_nodeDetails_button_message": "Message",
"dialog_nodeDetails_button_requestPosition": "Request Position",
"dialog_nodeDetails_button_traceRoute": "Trace Route",
"dialog_nodeDetails_label_airTxUtilization": "Air TX utilization",
"dialog_nodeDetails_label_allRawMetrics": "All Raw Metrics:",
"dialog_nodeDetails_label_batteryLevel": "Battery level",
"dialog_nodeDetails_label_channelUtilization": "Channel utilization",
"dialog_nodeDetails_label_details": "Details:",
"dialog_nodeDetails_label_deviceMetrics": "Device Metrics:",
"dialog_nodeDetails_label_hardware": "Hardware: ",
"dialog_nodeDetails_label_lastHeard": "Last Heard: ",
"dialog_nodeDetails_label_nodeHexPrefix": "Node Hex: !",
"dialog_nodeDetails_label_nodeNumber": "Node Number: ",
"dialog_nodeDetails_label_position": "Position:",
"dialog_nodeDetails_label_role": "Role: ",
"dialog_nodeDetails_label_uptime": "Uptime: ",
"dialog_nodeDetails_label_voltage": "Voltage",
"dialog_nodeDetails_titlePrefix": "Node Details for ",
"dialog_nodeDetails_tooltip_ignoreNode": "Ignore node",
"dialog_nodeDetails_tooltip_removeNode": "Remove node",
"dialog_nodeDetails_tooltip_unignoreNode": "Unignore node",
"dialog_pkiBackup_message": "We recommend backing up your key data regularly. Would you like to back up now?",
"dialog_pkiBackup_description_loseKeysWarning": "If you lose your keys, you will need to reset your device.",
"dialog_pkiBackup_description_secureBackup": "Its important to backup your public and private keys and store your backup securely!",
"dialog_pkiBackup_print_footer": "=== END OF KEYS ===",
"dialog_pkiBackup_print_header": "=== MESHTASTIC KEYS ===",
"dialog_pkiBackup_print_label_privateKey": "Private Key:",
"dialog_pkiBackup_print_label_publicKey": "Public Key:",
"dialog_pkiBackup_title": "Backup Keys",
"dialog_pkiRegenerate_description_keyPair": "Are you sure you want to regenerate key pair?",
"dialog_pkiRegenerate_title_keyPair": "Regenerate Key Pair",
"dialog_qr_button_addChannels": "Add Channels",
"dialog_qr_button_replaceChannels": "Replace Channels",
"dialog_qr_description": "The current LoRa configuration will also be shared.",
"dialog_qr_label_sharableUrl": "Sharable URL",
"dialog_qr_title": "Generate QR Code",
"dialog_rebootOta_button_now": "Reboot to OTA Mode Now",
"dialog_rebootOta_description": "Reboot the connected node after a delay into OTA (Over-the-Air) mode.",
"dialog_rebootOta_placeholder_enterDelay": "Enter delay (sec)",
"dialog_rebootOta_status_scheduled": "Reboot has been scheduled",
"dialog_reboot_description": "Reboot the connected node after x minutes.",
"dialog_refreshKeys_button_requestNewKeys": "Request New Keys",
"dialog_refreshKeys_description_acceptNewKeys": "This will remove the node from device and request new keys.",
"dialog_refreshKeys_description_keyMismatchReasonSuffix": ". This is due to the remote node's current public key does not match the previously stored key for this node.",
"dialog_refreshKeys_description_unableToSendDmPrefix": "Your node is unable to send a direct message to node: ",
"dialog_refreshKeys_label_acceptNewKeys": "Accept New Keys",
"dialog_refreshKeys_titlePrefix": "Keys Mismatch - ",
"dialog_removeNode_description": "Are you sure you want to remove this Node?",
"dialog_removeNode_title": "Remove Node?",
"dialog_shutdown_description": "Turn off the connected node after x minutes.",
"dialog_shutdown_suffix_minutes": "Minutes",
"dialog_tracerouteResponse_titlePrefix": "Traceroute: ",
"dialog_unsafeRoles_checkbox_confirmUnderstanding": "Yes, I know what I'm doing",
"dialog_unsafeRoles_description_conjunction": " and the blog post about ",
"dialog_unsafeRoles_description_postamble": " and understand the implications of changing the role.",
"dialog_unsafeRoles_description_preamble": "I have read the ",
"dialog_unsafeRoles_link_choosingRightDeviceRole": "Choosing The Right Device Role",
"dialog_unsafeRoles_link_deviceRoleDocumentation": "Device Role Documentation",
"dialog_unsafeRoles_title": "Are you sure?",
"dashboard_connectedDevicesSection_title": "Connected Devices",
"dashboard_connectedDevicesSection_description": "Manage your connected Meshtastic devices.",
"dashboard_connectedDevicesSection_connectionType_ble": "BLE",
"dashboard_connectedDevicesSection_connectionType_serial": "Serial",
"dashboard_connectedDevicesSection_connectionType_network": "Network",
"dashboard_connectedDevicesSection_noDevicesTitle": "No devices connected",
"dashboard_connectedDevicesSection_noDevicesDescription": "Connect a new device to get started.",
"dashboard_connectedDevicesSection_button_newConnection": "New Connection",
"sidebar_collapseToggle_button_open": "Open sidebar",
"sidebar_collapseToggle_button_close": "Close sidebar",
"sidebar_app_title_alt": "Meshtastic Logo",
"sidebar_deviceInfo_volts": "{{voltage}} volts",
"sidebar_deviceInfo_firmwareVersion": "v{{version}}",
"sidebar_deviceInfo_button_editDeviceName": "Edit device name",
"messages_empty": "No messages yet.",
"messages_selectChatPrompt": "Select a channel or node to start messaging.",
"messages_channelsSection_title": "Channels",
"messages_channelsSection_primaryChannelName": "Primary",
"messages_channelsSection_channelName": "Ch {{index}}",
"messages_nodesSection_searchPlaceholder": "Search nodes...",
"messages_title": "Messages: {{chatName}}",
"messages_title_default": "Select a Chat",
"messages_sendMessagePrompt": "Select a chat to send a message.",
"messages_actionsMenu_addReactionLabel": "Add Reaction",
"messages_actionsMenu_replyLabel": "Reply",
"message_item_fallbackName_withLastFour": "Meshtastic {{last4}}",
"message_item_status_delivered_ariaLabel": "Message delivered",
"message_item_status_delivered_displayText": "Message delivered",
"message_item_status_failed_ariaLabel": "Message delivery failed",
"message_item_status_failed_displayText": "Delivery failed",
"message_item_status_unknown_ariaLabel": "Message status unknown",
"message_item_status_unknown_displayText": "Unknown state",
"message_item_status_waiting_ariaLabel": "Sending message",
"message_item_status_waiting_displayText": "Waiting for delivery",
"channel_page_title": "Channel: {{channelName}}",
"channel_name_primary": "Primary",
"channel_name_prefix": "Ch {{index}}",
"channel_form_toast_saved": "Saved Channel: {{channelName}}",
"channel_form_validation_psk_invalid": "Please enter a valid {{bits}} bit PSK.",
"channel_form_field_group_settings_label": "Channel Settings",
"channel_form_field_group_settings_description": "Crypto, MQTT & misc settings",
"channel_form_field_role_label": "Role",
"channel_form_field_role_description": "Device telemetry is sent over PRIMARY. Only one PRIMARY allowed",
"channel_form_field_role_option_primary": "PRIMARY",
"channel_form_field_role_option_disabled": "DISABLED",
"channel_form_field_role_option_secondary": "SECONDARY",
"channel_form_field_psk_label": "Pre-Shared Key",
"channel_form_field_psk_description": "Supported PSK lengths: 256-bit, 128-bit, 8-bit, Empty (0-bit)",
"channel_form_field_psk_generate_button": "Generate",
"channel_form_field_name_label": "Name",
"channel_form_field_name_description": "A unique name for the channel <12 bytes, leave blank for default",
"channel_form_field_uplink_enabled_label": "Uplink Enabled",
"channel_form_field_uplink_enabled_description": "Send messages from the local mesh to MQTT",
"channel_form_field_downlink_enabled_label": "Downlink Enabled",
"channel_form_field_downlink_enabled_description": "Send messages from MQTT to the local mesh",
"channel_form_field_position_precision_label": "Location",
"channel_form_field_position_precision_description": "The precision of the location to share with the channel. Can be disabled.",
"channel_form_field_position_precision_option_none": "Do not share location",
"channel_form_field_position_precision_option_precise": "Precise Location",
"channel_form_field_position_precision_option_metric_km23": "Within 23 kilometers",
"channel_form_field_position_precision_option_metric_km12": "Within 12 kilometers",
"channel_form_field_position_precision_option_metric_km5_8": "Within 5.8 kilometers",
"channel_form_field_position_precision_option_metric_km2_9": "Within 2.9 kilometers",
"channel_form_field_position_precision_option_metric_km1_5": "Within 1.5 kilometers",
"channel_form_field_position_precision_option_metric_m700": "Within 700 meters",
"channel_form_field_position_precision_option_metric_m350": "Within 350 meters",
"channel_form_field_position_precision_option_metric_m200": "Within 200 meters",
"channel_form_field_position_precision_option_metric_m90": "Within 90 meters",
"channel_form_field_position_precision_option_metric_m50": "Within 50 meters",
"channel_form_field_position_precision_option_imperial_mi15": "Within 15 miles",
"channel_form_field_position_precision_option_imperial_mi7_3": "Within 7.3 miles",
"channel_form_field_position_precision_option_imperial_mi3_6": "Within 3.6 miles",
"channel_form_field_position_precision_option_imperial_mi1_8": "Within 1.8 miles",
"channel_form_field_position_precision_option_imperial_mi0_9": "Within 0.9 miles",
"channel_form_field_position_precision_option_imperial_mi0_5": "Within 0.5 miles",
"channel_form_field_position_precision_option_imperial_mi0_2": "Within 0.2 miles",
"channel_form_field_position_precision_option_imperial_ft600": "Within 600 feet",
"channel_form_field_position_precision_option_imperial_ft300": "Within 300 feet",
"channel_form_field_position_precision_option_imperial_ft150": "Within 150 feet",
"httpConnection_ipAddressLabel": "IP Address/Hostname",
"httpConnection_field_ipAddress_placeholder": "000.000.000.000 / meshtastic.local",
"httpConnection_label_useHttps": "Use HTTPS",
"httpConnection_button_connecting": "Connecting...",
"httpConnection_button_connect": "Connect",
"httpConnection_http": "http",
"httpConnection_https": "https",
"httpConnection_connectionFailedAlert_title": "Connection Failed",
"httpConnection_connectionFailedAlert_descriptionPrefix": "Could not connect to the device. ",
"httpConnection_connectionFailedAlert_httpsHint": "If using HTTPS, you may need to accept a self-signed certificate first. ",
"httpConnection_connectionFailedAlert_openLinkPrefix": "Please open ",
"httpConnection_connectionFailedAlert_openLinkSuffix": " in a new tab",
"httpConnection_connectionFailedAlert_acceptTlsWarningSuffix": ", accept any TLS warnings if prompted, then try again",
"httpConnection_connectionFailedAlert_learnMoreLink": "Learn more",
"serialConnection_noDevicesPaired": "No devices paired yet.",
"serialConnection_newDeviceButton": "New device",
"serialConnection_deviceIdentifier": "# {{index}} - {{vendorId}} - {{productId}}",
"serialConnection_connecting": "Connecting...",
"bluetoothConnection_noDevicesPaired": "No devices paired yet.",
"bluetoothConnection_newDeviceButton": "New device",
"toast_messages_pkiEncryption": "Chat is using PKI encryption.",
"toast_messages_pskEncryption": "Chat is using PSK encryption.",
"toast_positionRequestSent": "Position request sent.",
"toast_requestingPosition": "Requesting position, please wait...",
"toast_sendingTraceroute": "Sending Traceroute, please wait...",
"toast_tracerouteSent": "Traceroute sent.",
"node_detail_public_key_enabled_aria_label": "Public Key Enabled",
"node_detail_no_public_key_aria_label": "No Public Key",
"node_detail_direct_message_tooltip": "Direct Message {{shortName}}",
"node_detail_favorite_aria_label": "Favorite",
"node_detail_not_favorite_aria_label": "Not a Favorite",
"node_detail_status_heard": "Heard",
"node_detail_status_mqtt": "MQTT",
"node_detail_hops_label_one": "Hop",
"node_detail_hops_label_other": "Hops",
"node_detail_hops_unknown": "?",
"node_detail_elevation_aria_label": "Elevation",
"node_detail_altitude_unit_one": "meter",
"node_detail_altitude_unit_other": "meters",
"node_detail_channel_util_label": "Channel Util",
"node_detail_airtime_util_label": "Airtime Util",
"node_detail_snr_label": "SNR",
"node_detail_short_name_display_format": "\"{name}\"",
"traceRoute_routeToDestinationTitle": "Route to destination:",
"traceRoute_routeBackTitle": "Route back:",
"traceRoute_snrUnknown": "??",
"traceRoute_nodeUnknownPrefix": "!",
"nodes_searchPlaceholder": "Search nodes...",
"nodes_table_headings_longName": "Long Name",
"nodes_table_headings_connection": "Connection",
"nodes_table_headings_lastHeard": "Last Heard",
"nodes_table_headings_encryption": "Encryption",
"nodes_table_headings_model": "Model",
"nodes_table_headings_macAddress": "MAC Address",
"nodes_table_connectionStatus_direct": "Direct",
"nodes_table_connectionStatus_hopsAway_one": "{{count}} hop away",
"nodes_table_connectionStatus_hopsAway_other": "{{count}} hops away",
"nodes_table_lastHeardStatus_never": "Never",
"nodes_table_connectionStatus_away": "away",
"nodes_table_connectionStatus_hops_one": "hop",
"nodes_table_connectionStatus_hops_other": "hops",
"nodes_table_connectionStatus_unknown": "-",
"nodes_table_connectionStatus_viaMqtt": ", via MQTT",
"command_palette_input_placeholder": "Type a command or search...",
"command_palette_empty_message": "No results found.",
"command_palette_pin_group_aria_label": "Pin command group",
"command_palette_unpin_group_aria_label": "Unpin command group",
"command_palette_goto_label": "Goto",
"command_palette_goto_command_messages": "Messages",
"command_palette_goto_command_map": "Map",
"command_palette_goto_command_config": "Config",
"command_palette_goto_command_channels": "Channels",
"command_palette_goto_command_nodes": "Nodes",
"command_palette_manage_label": "Manage",
"command_palette_manage_command_switch_node": "Switch Node",
"command_palette_manage_command_connect_new_node": "Connect New Node",
"command_palette_contextual_label": "Contextual",
"command_palette_contextual_command_qr_code": "QR Code",
"command_palette_contextual_command_qr_generator": "Generator",
"command_palette_contextual_command_qr_import": "Import",
"command_palette_contextual_command_schedule_shutdown": "Schedule Shutdown",
"command_palette_contextual_command_schedule_reboot": "Schedule Reboot",
"command_palette_contextual_command_reboot_to_ota_mode": "Reboot To OTA Mode",
"command_palette_contextual_command_reset_nodes": "Reset Nodes",
"command_palette_contextual_command_factory_reset_device": "Factory Reset Device",
"command_palette_contextual_command_factory_reset_config": "Factory Reset Config",
"command_palette_debug_label": "Debug",
"command_palette_debug_command_reconfigure": "Reconfigure",
"command_palette_debug_command_clear_all_stored_messages": "Clear All Stored Message",
"config_page_title_configuration": "Configuration",
"config_device_tab_bluetooth": "Bluetooth",
"config_device_tab_device": "Device",
"config_device_tab_display": "Display",
"config_device_tab_lora": "LoRa",
"config_device_tab_network": "Network",
"config_device_tab_position": "Position",
"config_device_tab_power": "Power",
"config_device_tab_security": "Security",
"config_module_tab_ambientLighting": "Ambient Lighting",
"config_module_tab_audio": "Audio",
"config_module_tab_cannedMessage": "Canned",
"config_module_tab_detectionSensor": "Detection Sensor",
"config_module_tab_externalNotification": "Ext Notif",
"config_module_tab_mqtt": "MQTT",
"config_module_tab_neighborInfo": "Neighbor Info",
"config_module_tab_paxcounter": "Paxcounter",
"config_module_tab_rangeTest": "Range Test",
"config_module_tab_serial": "Serial",
"config_module_tab_storeAndForward": "S&F",
"config_module_tab_telemetry": "Telemetry",
"config_sidebar_section_modules_label": "Modules",
"config_toast_errorSaving_description": "An error occurred while saving the configuration.",
"config_toast_errorSaving_title": "Error Saving Config",
"config_toast_errorsExist_description": "Please fix the configuration errors before saving.",
"config_toast_errorsExist_title": "Config Errors Exist",
"config_toast_saving_description": "The configuration change {{case}} has been saved.",
"config_toast_saving_title": "Saving Config",
"config_bluetooth_fieldDescription_enabled": "Enable or disable Bluetooth",
"config_bluetooth_fieldDescription_pairingMode": "Pin selection behaviour.",
"config_bluetooth_fieldDescription_pin": "Pin to use when pairing",
"config_bluetooth_fieldLabel_enabled": "Enabled",
"config_bluetooth_fieldLabel_pairingMode": "Pairing mode",
"config_bluetooth_fieldLabel_pin": "Pin",
"config_bluetooth_groupDescription_bluetoothSettings": "Settings for the Bluetooth module ",
"config_bluetooth_groupLabel_bluetoothSettings": "Bluetooth Settings",
"config_bluetooth_groupNotes_bluetoothWifiNote": "Note: Some devices (ESP32) cannot use both Bluetooth and WiFi at the same time.",
"config_bluetooth_validation_pinCannotStartWithZero": "Bluetooth Pin cannot start with 0",
"config_bluetooth_validation_pinMustBeSixDigits": "Pin must be 6 digits",
"config_bluetooth_validation_pinRequired": "Bluetooth Pin is required",
"config_device_fieldDescription_buttonPin": "Button pin override",
"config_device_fieldDescription_buzzerPin": "Buzzer pin override",
"config_device_fieldDescription_disableTripleClick": "Disable triple click",
"config_device_fieldDescription_doubleTapAsButtonPress": "Treat double tap as button press",
"config_device_fieldDescription_ledHeartbeatDisabled": "Disable default blinking LED",
"config_device_fieldDescription_nodeInfoBroadcastInterval": "How often to broadcast node info",
"config_device_fieldDescription_posixTimezone": "The POSIX timezone string for the device",
"config_device_fieldDescription_rebroadcastMode": "How to handle rebroadcasting",
"config_device_fieldDescription_role": "What role the device performs on the mesh",
"config_device_fieldLabel_buttonPin": "Button Pin",
"config_device_fieldLabel_buzzerPin": "Buzzer Pin",
"config_device_fieldLabel_disableTripleClick": "Disable Triple Click",
"config_device_fieldLabel_doubleTapAsButtonPress": "Double Tap as Button Press",
"config_device_fieldLabel_ledHeartbeatDisabled": "LED Heartbeat Disabled",
"config_device_fieldLabel_nodeInfoBroadcastInterval": "Node Info Broadcast Interval",
"config_device_fieldLabel_posixTimezone": "POSIX Timezone",
"config_device_fieldLabel_rebroadcastMode": "Rebroadcast Mode",
"config_device_fieldLabel_role": "Role",
"config_device_groupDescription_deviceSettings": "Settings for the device",
"config_device_groupLabel_deviceSettings": "Device Settings",
"config_display_fieldDescription_boldHeading": "Bolden the heading text",
"config_display_fieldDescription_carouselDelay": "How fast to cycle through windows",
"config_display_fieldDescription_compassNorthTop": "Fix north to the top of compass",
"config_display_fieldDescription_displayMode": "Screen layout variant",
"config_display_fieldDescription_displayUnits": "Display metric or imperial units",
"config_display_fieldDescription_flipScreen": "Flip display 180 degrees",
"config_display_fieldDescription_gpsDisplayUnits": "Coordinate display format",
"config_display_fieldDescription_oledType": "Type of OLED screen attached to the device",
"config_display_fieldDescription_screenTimeout": "Turn off the display after this long",
"config_display_fieldDescription_twelveHourClock": "Use 12-hour clock format",
"config_display_fieldDescription_wakeOnTapOrMotion": "Wake the device on tap or motion",
"config_display_fieldLabel_boldHeading": "Bold Heading",
"config_display_fieldLabel_carouselDelay": "Carousel Delay",
"config_display_fieldLabel_compassNorthTop": "Compass North Top",
"config_display_fieldLabel_displayMode": "Display Mode",
"config_display_fieldLabel_displayUnits": "Display Units",
"config_display_fieldLabel_flipScreen": "Flip Screen",
"config_display_fieldLabel_gpsDisplayUnits": "GPS Display Units",
"config_display_fieldLabel_oledType": "OLED Type",
"config_display_fieldLabel_screenTimeout": "Screen Timeout",
"config_display_fieldLabel_twelveHourClock": "12-Hour Clock",
"config_display_fieldLabel_wakeOnTapOrMotion": "Wake on Tap or Motion",
"config_display_groupDescription_displaySettings": "Settings for the device display",
"config_display_groupLabel_displaySettings": "Display Settings",
"config_lora_fieldDescription_bandwidth": "Channel bandwidth in MHz",
"config_lora_fieldDescription_boostedRxGain": "Boosted RX gain",
"config_lora_fieldDescription_codingRate": "The denominator of the coding rate",
"config_lora_fieldDescription_frequencyOffset": "Frequency offset to correct for crystal calibration errors",
"config_lora_fieldDescription_frequencySlot": "LoRa frequency channel number",
"config_lora_fieldDescription_hopLimit": "Maximum number of hops",
"config_lora_fieldDescription_ignoreMqtt": "Don't forward MQTT messages over the mesh",
"config_lora_fieldDescription_modemPreset": "Modem preset to use",
"config_lora_fieldDescription_okToMqtt": "When set to true, this configuration indicates that the user approves the packet to be uploaded to MQTT. If set to false, remote nodes are requested not to forward packets to MQTT",
"config_lora_fieldDescription_overrideDutyCycle": "Override Duty Cycle",
"config_lora_fieldDescription_overrideFrequency": "Override frequency",
"config_lora_fieldDescription_region": "Sets the region for your node",
"config_lora_fieldDescription_spreadingFactor": "Indicates the number of chirps per symbol",
"config_lora_fieldDescription_transmitEnabled": "Enable/Disable transmit (TX) from the LoRa radio",
"config_lora_fieldDescription_transmitPower": "Max transmit power",
"config_lora_fieldDescription_usePreset": "Use one of the predefined modem presets",
"config_lora_fieldLabel_bandwidth": "Bandwidth",
"config_lora_fieldLabel_boostedRxGain": "Boosted RX Gain",
"config_lora_fieldLabel_codingRate": "Coding Rate",
"config_lora_fieldLabel_frequencyOffset": "Frequency Offset",
"config_lora_fieldLabel_frequencySlot": "Frequency Slot",
"config_lora_fieldLabel_hopLimit": "Hop Limit",
"config_lora_fieldLabel_ignoreMqtt": "Ignore MQTT",
"config_lora_fieldLabel_modemPreset": "Modem Preset",
"config_lora_fieldLabel_okToMqtt": "OK to MQTT",
"config_lora_fieldLabel_overrideDutyCycle": "Override Duty Cycle",
"config_lora_fieldLabel_overrideFrequency": "Override Frequency",
"config_lora_fieldLabel_region": "Region",
"config_lora_fieldLabel_spreadingFactor": "Spreading Factor",
"config_lora_fieldLabel_transmitEnabled": "Transmit Enabled",
"config_lora_fieldLabel_transmitPower": "Transmit Power",
"config_lora_fieldLabel_usePreset": "Use Preset",
"config_lora_groupDescription_meshSettings": "Settings for the LoRa mesh",
"config_lora_groupDescription_radioSettings": "Settings for the LoRa radio",
"config_lora_groupDescription_waveformSettings": "Settings for the LoRa waveform",
"config_lora_groupLabel_meshSettings": "Mesh Settings",
"config_lora_groupLabel_radioSettings": "Radio Settings",
"config_lora_groupLabel_waveformSettings": "Waveform Settings",
"config_network_fieldDescription_addressMode": "Address assignment selection",
"config_network_fieldDescription_dns": "DNS Server",
"config_network_fieldDescription_ethernetEnabled": "Enable or disable the Ethernet port",
"config_network_fieldDescription_gateway": "Default Gateway",
"config_network_fieldDescription_ip": "IP Address",
"config_network_fieldDescription_psk": "Network password",
"config_network_fieldDescription_ssid": "Network name",
"config_network_fieldDescription_subnet": "Subnet Mask",
"config_network_fieldDescription_wifiEnabled": "Enable or disable the WiFi radio",
"config_network_fieldLabel_addressMode": "Address Mode",
"config_network_fieldLabel_dns": "DNS",
"config_network_fieldLabel_ethernetEnabled": "Enabled",
"config_network_fieldLabel_gateway": "Gateway",
"config_network_fieldLabel_ip": "IP",
"config_network_fieldLabel_meshViaUdp": "Mesh via UDP",
"config_network_fieldLabel_ntpServer": "NTP Server",
"config_network_fieldLabel_psk": "PSK",
"config_network_fieldLabel_rsyslogServer": "Rsyslog Server",
"config_network_fieldLabel_ssid": "SSID",
"config_network_fieldLabel_subnet": "Subnet",
"config_network_fieldLabel_wifiEnabled": "Enabled",
"config_network_groupDescription_ethernetConfig": "Ethernet port configuration",
"config_network_groupDescription_ipConfig": "IP configuration",
"config_network_groupDescription_ntpConfig": "NTP configuration",
"config_network_groupDescription_rsyslogConfig": "Rsyslog configuration",
"config_network_groupDescription_udpConfig": "UDP over Mesh configuration",
"config_network_groupDescription_wifiConfig": "WiFi radio configuration",
"config_network_groupLabel_ethernetConfig": "Ethernet Config",
"config_network_groupLabel_ipConfig": "IP Config",
"config_network_groupLabel_ntpConfig": "NTP Config",
"config_network_groupLabel_rsyslogConfig": "Rsyslog Config",
"config_network_groupLabel_udpConfig": "UDP Config",
"config_network_groupLabel_wifiConfig": "WiFi Config",
"config_network_groupNotes_wifiBluetoothNote": "Note: Some devices (ESP32) cannot use both Bluetooth and WiFi at the same time.",
"config_position_fieldDescription_broadcastInterval": "How often your position is sent out over the mesh",
"config_position_fieldDescription_enablePin": "GPS module enable pin override",
"config_position_fieldDescription_fixedPosition": "Don't report GPS position, but a manually-specified one",
"config_position_fieldDescription_gpsMode": "Configure whether device GPS is Enabled, Disabled, or Not Present",
"config_position_fieldDescription_gpsUpdateInterval": "How often a GPS fix should be acquired",
"config_position_fieldDescription_positionFlags": "Optional fields to include when assembling position messages. The more fields are selected, the larger the message will be leading to longer airtime usage and a higher risk of packet loss.",
"config_position_fieldDescription_receivePin": "GPS module RX pin override",
"config_position_fieldDescription_smartPositionEnabled": "Only send position when there has been a meaningful change in location",
"config_position_fieldDescription_smartPositionMinDistance": "Minimum distance (in meters) that must be traveled before a position update is sent",
"config_position_fieldDescription_smartPositionMinInterval": "Minimum interval (in seconds) that must pass before a position update is sent",
"config_position_fieldDescription_transmitPin": "GPS module TX pin override",
"config_position_fieldLabel_broadcastInterval": "Broadcast Interval",
"config_position_fieldLabel_enablePin": "Enable Pin",
"config_position_fieldLabel_fixedPosition": "Fixed Position",
"config_position_fieldLabel_gpsMode": "GPS Mode",
"config_position_fieldLabel_gpsUpdateInterval": "GPS Update Interval",
"config_position_fieldLabel_positionFlags": "Position Flags",
"config_position_fieldLabel_receivePin": "Receive Pin",
"config_position_fieldLabel_smartPositionEnabled": "Enable Smart Position",
"config_position_fieldLabel_smartPositionMinDistance": "Smart Position Minimum Distance",
"config_position_fieldLabel_smartPositionMinInterval": "Smart Position Minimum Interval",
"config_position_fieldLabel_transmitPin": "Transmit Pin",
"config_position_fieldPlaceholder_selectPositionFlags": "Select position flags...",
"config_position_groupDescription_intervals": "How often to send position updates",
"config_position_groupDescription_positionSettings": "Settings for the position module",
"config_position_groupLabel_intervals": "Intervals",
"config_position_groupLabel_positionSettings": "Position Settings",
"config_power_fieldDescription_adcMultiplierOverride": "Used for tweaking battery voltage reading",
"config_power_fieldDescription_ina219Address": "Address of the INA219 battery monitor",
"config_power_fieldDescription_lightSleepDuration": "How long the device will be in light sleep for",
"config_power_fieldDescription_minimumWakeTime": "Minimum amount of time the device will stay awake for after receiving a packet",
"config_power_fieldDescription_noConnectionBluetoothDisabled": "If the device does not receive a Bluetooth connection, the BLE radio will be disabled after this long",
"config_power_fieldDescription_powerSavingEnabled": "Select if powered from a low-current source (i.e. solar), to minimize power consumption as much as possible.",
"config_power_fieldDescription_shutdownOnBatteryDelay": "Automatically shutdown node after this long when on battery, 0 for indefinite",
"config_power_fieldDescription_superDeepSleepDuration": "How long the device will be in super deep sleep for",
"config_power_fieldLabel_adcMultiplierOverride": "ADC Multiplier Override ratio",
"config_power_fieldLabel_ina219Address": "INA219 Address",
"config_power_fieldLabel_lightSleepDuration": "Light Sleep Duration",
"config_power_fieldLabel_minimumWakeTime": "Minimum Wake Time",
"config_power_fieldLabel_noConnectionBluetoothDisabled": "No Connection Bluetooth Disabled",
"config_power_fieldLabel_powerSavingEnabled": "Enable power saving mode",
"config_power_fieldLabel_shutdownOnBatteryDelay": "Shutdown on battery delay",
"config_power_fieldLabel_superDeepSleepDuration": "Super Deep Sleep Duration",
"config_power_groupDescription_powerConfig": "Settings for the power module",
"config_power_groupDescription_sleepSettings": "Sleep settings for the power module",
"config_power_groupLabel_powerConfig": "Power Config",
"config_power_groupLabel_sleepSettings": "Sleep Settings",
"config_security_button_backupKey": "Backup Key",
"config_security_fieldDescription_adminChannelEnabled": "Allow incoming device control over the insecure legacy admin channel",
"config_security_fieldDescription_enableDebugLogApi": "Output live debug logging over serial, view and export position-redacted device logs over Bluetooth",
"config_security_fieldDescription_managed": "If enabled, device configuration options are only able to be changed remotely by a Remote Admin node via admin messages. Do not enable this option unless at least one suitable Remote Admin node has been setup, and the public key is stored in one of the fields above.",
"config_security_fieldDescription_primaryAdminKey": "The primary public key authorized to send admin messages to this node",
"config_security_fieldDescription_privateKey": "Used to create a shared key with a remote device",
"config_security_fieldDescription_publicKey": "Sent out to other nodes on the mesh to allow them to compute a shared secret key",
"config_security_fieldDescription_secondaryAdminKey": "The secondary public key authorized to send admin messages to this node",
"config_security_fieldDescription_serialOutputEnabled": "Serial Console over the Stream API",
"config_security_fieldDescription_tertiaryAdminKey": "The tertiary public key authorized to send admin messages to this node",
"config_security_fieldLabel_adminChannelEnabled": "Allow Legacy Admin",
"config_security_fieldLabel_enableDebugLogApi": "Enable Debug Log API",
"config_security_fieldLabel_managed": "Managed",
"config_security_fieldLabel_primaryAdminKey": "Primary Admin Key",
"config_security_fieldLabel_privateKey": "Private Key",
"config_security_fieldLabel_publicKey": "Public Key",
"config_security_fieldLabel_secondaryAdminKey": "Secondary Admin Key",
"config_security_fieldLabel_serialOutputEnabled": "Serial Output Enabled",
"config_security_fieldLabel_tertiaryAdminKey": "Tertiary Admin Key",
"config_security_groupDescription_adminSettings": "Settings for Admin",
"config_security_groupDescription_loggingSettings": "Settings for Logging",
"config_security_groupDescription_securitySettings": "Settings for the Security configuration",
"config_security_groupLabel_adminSettings": "Admin Settings",
"config_security_groupLabel_loggingSettings": "Logging Settings",
"config_security_groupLabel_securitySettings": "Security Settings",
"config_security_option_256bit": "256 bit",
"config_security_validation_adminKeyMustBe256BitPsk": "Admin Key is required to be a 256 bit pre-shared key (PSK)",
"config_security_validation_adminKeyRequiredWhenManaged": "At least one admin key is requred if the node is managed.",
"config_security_validation_enterValid256BitPsk": "Please enter a valid 256 bit PSK",
"config_security_validation_invalidAdminKeyFormat": "Invalid Admin Key format",
"config_security_validation_invalidPrivateKeyFormat": "Invalid Private Key format",
"config_security_validation_privateKeyMustBe256BitPsk": "Private Key is required to be a 256 bit pre-shared key (PSK)",
"config_security_validation_privateKeyRequired": "Private Key is required",
"input_action_clearInput_label": "Clear input",
"input_action_copied_label": "Copied!",
"input_action_copyToClipboard_label": "Copy to clipboard",
"input_action_hidePassword_label": "Hide password",
"input_action_showPassword_label": "Show password",
"filter_control_accordion_general_label": "General",
"filter_control_accordion_hardware_label": "Hardware",
"filter_control_accordion_metrics_label": "Metrics",
"filter_control_accordion_role_label": "Role",
"filter_control_button_filter_ariaLabel": "Filter",
"filter_control_button_resetFilters_label": "Reset Filters",
"filter_control_input_nodeName_label": "Node name/number",
"filter_control_input_nodeName_placeholder": "Meshtastic 1234",
"filter_control_slider_airtimeUtilization_label": "Airtime Utilization (%)",
"filter_control_slider_batteryLevel_label": "Battery level (%)",
"filter_control_slider_batteryLevel_labelText": "Battery level (%): {{value}}",
"filter_control_slider_batteryVoltage_label": "Battery voltage (V)",
"filter_control_slider_channelUtilization_label": "Channel Utilization (%)",
"filter_control_slider_hops_directLabel": "Direct",
"filter_control_slider_hops_label": "Number of hops",
"filter_control_slider_hops_labelText": "Number of hops: {{value}}",
"filter_control_slider_lastHeard_label": "Last heard",
"filter_control_slider_lastHeard_labelText": "Last heard: {{value}}",
"filter_control_slider_lastHeard_nowLabel": "Now",
"filter_control_slider_snr_label": "SNR (db)",
"filter_control_toggle_favorites_label": "Favorites",
"filter_control_toggle_hide_label": "Hide",
"filter_control_toggle_showOnly_label": "Show Only",
"filter_control_toggle_viaMqtt_label": "Connected via MQTT",
"position_flag_altitude": "Altitude",
"position_flag_altitude_geoidal_separation": "Altitude Geoidal Separation",
"position_flag_altitude_msl": "Altitude is Mean Sea Level",
"position_flag_dop": "Dilution of precision (DOP) PDOP used by default",
"position_flag_hdop_vdop": "If DOP is set, use HDOP / VDOP values instead of PDOP",
"position_flag_num_satellites": "Number of satellites",
"position_flag_sequence_number": "Sequence number",
"position_flag_timestamp": "Timestamp",
"position_flag_unset": "Unset",
"position_flag_vehicle_heading": "Vehicle heading",
"position_flag_vehicle_speed": "Vehicle speed",
"theme_preference_dark": "Dark",
"theme_preference_light": "Light",
"theme_preference_system": "System",
"theme_switcher_aria_change_theme": "Change current theme",
"footer_text": "Powered by <0>▲ Vercel</0> | Meshtastic® is a registered trademark of Meshtastic LLC. | <1>Legal Information</1>"
}

69
src/i18n/locales/en/channels.json

@ -0,0 +1,69 @@
{
"page": {
"sectionLabel": "Channels",
"channelName": "Channel: {{channelName}}",
"broadcastLabel": "Primary",
"channelIndex": "Ch {{index}}"
},
"validation": {
"pskInvalid": "Please enter a valid {{bits}} bit PSK."
},
"settings": {
"label": "Channel Settings",
"description": "Crypto, MQTT & misc settings"
},
"role": {
"label": "Role",
"description": "Device telemetry is sent over PRIMARY. Only one PRIMARY allowed",
"options": {
"primary": "PRIMARY",
"disabled": "DISABLED",
"secondary": "SECONDARY"
}
},
"psk": {
"label": "Pre-Shared Key",
"description": "Supported PSK lengths: 256-bit, 128-bit, 8-bit, Empty (0-bit)",
"generate": "Generate"
},
"name": {
"label": "Name",
"description": "A unique name for the channel <12 bytes, leave blank for default"
},
"uplinkEnabled": {
"label": "Uplink Enabled",
"description": "Send messages from the local mesh to MQTT"
},
"downlinkEnabled": {
"label": "Downlink Enabled",
"description": "Send messages from MQTT to the local mesh"
},
"positionPrecision": {
"label": "Location",
"description": "The precision of the location to share with the channel. Can be disabled.",
"options": {
"none": "Do not share location",
"precise": "Precise Location",
"metric_km23": "Within 23 kilometers",
"metric_km12": "Within 12 kilometers",
"metric_km5_8": "Within 5.8 kilometers",
"metric_km2_9": "Within 2.9 kilometers",
"metric_km1_5": "Within 1.5 kilometers",
"metric_m700": "Within 700 meters",
"metric_m350": "Within 350 meters",
"metric_m200": "Within 200 meters",
"metric_m90": "Within 90 meters",
"metric_m50": "Within 50 meters",
"imperial_mi15": "Within 15 miles",
"imperial_mi7_3": "Within 7.3 miles",
"imperial_mi3_6": "Within 3.6 miles",
"imperial_mi1_8": "Within 1.8 miles",
"imperial_mi0_9": "Within 0.9 miles",
"imperial_mi0_5": "Within 0.5 miles",
"imperial_mi0_2": "Within 0.2 miles",
"imperial_ft600": "Within 600 feet",
"imperial_ft300": "Within 300 feet",
"imperial_ft150": "Within 150 feet"
}
}
}

47
src/i18n/locales/en/commandPalette.json

@ -0,0 +1,47 @@
{
"emptyState": "No results found.",
"pinGroup": {
"label": "Pin command group"
},
"unpinGroup": {
"label": "Unpin command group"
},
"goto": {
"label": "Goto",
"command": {
"messages": "Messages",
"map": "Map",
"config": "Config",
"channels": "Channels",
"nodes": "Nodes"
}
},
"manage": {
"label": "Manage",
"command": {
"switchNode": "Switch Node",
"connectNewNode": "Connect New Node"
}
},
"contextual": {
"label": "Contextual",
"command": {
"qrCode": "QR Code",
"qrGenerator": "Generator",
"qrImport": "Import",
"scheduleShutdown": "Schedule Shutdown",
"scheduleReboot": "Schedule Reboot",
"rebootToOtaMode": "Reboot To OTA Mode",
"resetNodeDb": "Reset Node DB",
"factoryResetDevice": "Factory Reset Device",
"factoryResetConfig": "Factory Reset Config"
}
},
"debug": {
"label": "Debug",
"command": {
"reconfigure": "Reconfigure",
"clearAllStoredMessages": "Clear All Stored Message"
}
}
}

72
src/i18n/locales/en/common.json

@ -0,0 +1,72 @@
{
"button": {
"apply": "Apply",
"backupKey": "Backup Key",
"cancel": "Cancel",
"clearMessages": "Clear Messages",
"close": "Close",
"confirm": "Confirm",
"delete": "Delete",
"dismiss": "Dismiss",
"download": "Download",
"export": "Export",
"generate": "Generate",
"regenerate": "Regenerate",
"import": "Import",
"message": "Message",
"now": "Now",
"ok": "OK",
"print": "Print",
"rebootOtaNow": "Reboot to OTA Mode Now",
"remove": "Remove",
"requestNewKeys": "Request New Keys",
"requestPosition": "Request Position",
"reset": "Reset",
"save": "Save",
"scanQr": "Scan QR Code",
"traceRoute": "Trace Route"
},
"app": {
"title": "Meshtastic",
"fullTitle": "Meshtastic Web Client"
},
"loading": "Loading...",
"unit": {
"cps": "CPS",
"dbm": "dBm",
"hertz": "Hz",
"hop": {
"one": "Hop",
"plural": "Hops"
},
"hopsAway": {
"one": "{{count}} hop away",
"plural": "{{count}} hops away",
"unknown": "Unknown hops away"
},
"megahertz": "MHz",
"raw": "raw",
"meter": { "one": "Meter", "plural": "Meters", "suffix": "m" },
"minute": { "one": "Minute", "plural": "Minutes" },
"millisecond": {
"one": "Millisecond",
"plural": "Milliseconds",
"suffix": "ms"
},
"second": { "one": "Second", "plural": "Seconds" },
"snr": "SNR",
"volts": "Volts",
"record": { "one": "Records", "plural": "Records" }
},
"security": {
"256bit": "256 bit"
},
"unknown": {
"longName": "Unknown",
"shortName": "UNK"
},
"snrUnknown": "??",
"nodeUnknownPrefix": "!",
"unset": "UNSET",
"fallbackName": "Meshtastic {{last4}}"
}

12
src/i18n/locales/en/dashboard.json

@ -0,0 +1,12 @@
{
"dashboard": {
"title": "Connected Devices",
"description": "Manage your connected Meshtastic devices.",
"connectionType_ble": "BLE",
"connectionType_serial": "Serial",
"connectionType_network": "Network",
"noDevicesTitle": "No devices connected",
"noDevicesDescription": "Connect a new device to get started.",
"button_newConnection": "New Connection"
}
}

442
src/i18n/locales/en/deviceConfig.json

@ -0,0 +1,442 @@
{
"page": {
"title": "Configuration",
"tabBluetooth": "Bluetooth",
"tabDevice": "Device",
"tabDisplay": "Display",
"tabLora": "LoRa",
"tabNetwork": "Network",
"tabPosition": "Position",
"tabPower": "Power",
"tabSecurity": "Security"
},
"sidebar": {
"label": "Modules"
},
"device": {
"title": "Device Settings",
"description": "Settings for the device",
"buttonPin": {
"description": "Button pin override",
"label": "Button Pin"
},
"buzzerPin": {
"description": "Buzzer pin override",
"label": "Buzzer Pin"
},
"disableTripleClick": {
"description": "Disable triple click",
"label": "Disable Triple Click"
},
"doubleTapAsButtonPress": {
"description": "Treat double tap as button press",
"label": "Double Tap as Button Press"
},
"ledHeartbeatDisabled": {
"description": "Disable default blinking LED",
"label": "LED Heartbeat Disabled"
},
"nodeInfoBroadcastInterval": {
"description": "How often to broadcast node info",
"label": "Node Info Broadcast Interval"
},
"posixTimezone": {
"description": "The POSIX timezone string for the device",
"label": "POSIX Timezone"
},
"rebroadcastMode": {
"description": "How to handle rebroadcasting",
"label": "Rebroadcast Mode"
},
"role": {
"description": "What role the device performs on the mesh",
"label": "Role"
}
},
"bluetooth": {
"title": "Bluetooth Settings",
"description": "Settings for the Bluetooth module",
"note": "Note: Some devices (ESP32) cannot use both Bluetooth and WiFi at the same time.",
"enabled": {
"description": "Enable or disable Bluetooth",
"label": "Enabled"
},
"pairingMode": {
"description": "Pin selection behaviour.",
"label": "Pairing mode"
},
"pin": {
"description": "Pin to use when pairing",
"label": "Pin"
},
"validation": {
"pinCannotStartWithZero": "Bluetooth Pin cannot start with 0",
"pinMustBeSixDigits": "Pin must be 6 digits",
"pinRequired": "Bluetooth Pin is required"
}
},
"display": {
"description": "Settings for the device display",
"title": "Display Settings",
"boldHeading": {
"description": "Bolden the heading text",
"label": "Bold Heading"
},
"carouselDelay": {
"description": "How fast to cycle through windows",
"label": "Carousel Delay"
},
"compassNorthTop": {
"description": "Fix north to the top of compass",
"label": "Compass North Top"
},
"displayMode": {
"description": "Screen layout variant",
"label": "Display Mode"
},
"displayUnits": {
"description": "Display metric or imperial units",
"label": "Display Units"
},
"flipScreen": {
"description": "Flip display 180 degrees",
"label": "Flip Screen"
},
"gpsDisplayUnits": {
"description": "Coordinate display format",
"label": "GPS Display Units"
},
"oledType": {
"description": "Type of OLED screen attached to the device",
"label": "OLED Type"
},
"screenTimeout": {
"description": "Turn off the display after this long",
"label": "Screen Timeout"
},
"twelveHourClock": {
"description": "Use 12-hour clock format",
"label": "12-Hour Clock"
},
"wakeOnTapOrMotion": {
"description": "Wake the device on tap or motion",
"label": "Wake on Tap or Motion"
}
},
"lora": {
"title": "Mesh Settings",
"description": "Settings for the LoRa mesh",
"bandwidth": {
"description": "Channel bandwidth in MHz",
"label": "Bandwidth"
},
"boostedRxGain": {
"description": "Boosted RX gain",
"label": "Boosted RX Gain"
},
"codingRate": {
"description": "The denominator of the coding rate",
"label": "Coding Rate"
},
"frequencyOffset": {
"description": "Frequency offset to correct for crystal calibration errors",
"label": "Frequency Offset"
},
"frequencySlot": {
"description": "LoRa frequency channel number",
"label": "Frequency Slot"
},
"hopLimit": {
"description": "Maximum number of hops",
"label": "Hop Limit"
},
"ignoreMqtt": {
"description": "Don't forward MQTT messages over the mesh",
"label": "Ignore MQTT"
},
"modemPreset": {
"description": "Modem preset to use",
"label": "Modem Preset"
},
"okToMqtt": {
"description": "When set to true, this configuration indicates that the user approves the packet to be uploaded to MQTT. If set to false, remote nodes are requested not to forward packets to MQTT",
"label": "OK to MQTT"
},
"overrideDutyCycle": {
"description": "Override Duty Cycle",
"label": "Override Duty Cycle"
},
"overrideFrequency": {
"description": "Override frequency",
"label": "Override Frequency"
},
"region": {
"description": "Sets the region for your node",
"label": "Region"
},
"spreadingFactor": {
"description": "Indicates the number of chirps per symbol",
"label": "Spreading Factor"
},
"transmitEnabled": {
"description": "Enable/Disable transmit (TX) from the LoRa radio",
"label": "Transmit Enabled"
},
"transmitPower": {
"description": "Max transmit power",
"label": "Transmit Power"
},
"usePreset": {
"description": "Use one of the predefined modem presets",
"label": "Use Preset"
},
"meshSettings": {
"description": "Settings for the LoRa mesh",
"label": "Mesh Settings"
},
"waveformSettings": {
"description": "Settings for the LoRa waveform",
"label": "Waveform Settings"
},
"radioSettings": {
"label": "Radio Settings",
"description": "Settings for the LoRa radio"
}
},
"network": {
"title": "WiFi Config",
"description": "WiFi radio configuration",
"note": "Note: Some devices (ESP32) cannot use both Bluetooth and WiFi at the same time.",
"addressMode": {
"description": "Address assignment selection",
"label": "Address Mode"
},
"dns": {
"description": "DNS Server",
"label": "DNS"
},
"ethernetEnabled": {
"description": "Enable or disable the Ethernet port",
"label": "Enabled"
},
"gateway": {
"description": "Default Gateway",
"label": "Gateway"
},
"ip": {
"description": "IP Address",
"label": "IP"
},
"psk": {
"description": "Network password",
"label": "PSK"
},
"ssid": {
"description": "Network name",
"label": "SSID"
},
"subnet": {
"description": "Subnet Mask",
"label": "Subnet"
},
"wifiEnabled": {
"description": "Enable or disable the WiFi radio",
"label": "Enabled"
},
"meshViaUdp": {
"label": "Mesh via UDP"
},
"ntpServer": {
"label": "NTP Server"
},
"rsyslogServer": {
"label": "Rsyslog Server"
},
"ethernetConfigSettings": {
"description": "Ethernet port configuration",
"label": "Ethernet Config"
},
"ipConfigSettings": {
"description": "IP configuration",
"label": "IP Config"
},
"ntpConfigSettings": {
"description": "NTP configuration",
"label": "NTP Config"
},
"rsyslogConfigSettings": {
"description": "Rsyslog configuration",
"label": "Rsyslog Config"
},
"udpConfigSettings": {
"description": "UDP over Mesh configuration",
"label": "UDP Config"
}
},
"position": {
"title": "Position Settings",
"description": "Settings for the position module",
"broadcastInterval": {
"description": "How often your position is sent out over the mesh",
"label": "Broadcast Interval"
},
"enablePin": {
"description": "GPS module enable pin override",
"label": "Enable Pin"
},
"fixedPosition": {
"description": "Don't report GPS position, but a manually-specified one",
"label": "Fixed Position"
},
"gpsMode": {
"description": "Configure whether device GPS is Enabled, Disabled, or Not Present",
"label": "GPS Mode"
},
"gpsUpdateInterval": {
"description": "How often a GPS fix should be acquired",
"label": "GPS Update Interval"
},
"positionFlags": {
"description": "Optional fields to include when assembling position messages. The more fields are selected, the larger the message will be leading to longer airtime usage and a higher risk of packet loss.",
"label": "Position Flags"
},
"receivePin": {
"description": "GPS module RX pin override",
"label": "Receive Pin"
},
"smartPositionEnabled": {
"description": "Only send position when there has been a meaningful change in location",
"label": "Enable Smart Position"
},
"smartPositionMinDistance": {
"description": "Minimum distance (in meters) that must be traveled before a position update is sent",
"label": "Smart Position Minimum Distance"
},
"smartPositionMinInterval": {
"description": "Minimum interval (in seconds) that must pass before a position update is sent",
"label": "Smart Position Minimum Interval"
},
"transmitPin": {
"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": {
"altitude": "Altitude",
"altitudeGeoidalSeparation": "Altitude Geoidal Separation",
"altitudeMsl": "Altitude is Mean Sea Level",
"dop": "Dilution of precision (DOP) PDOP used by default",
"hdopVdop": "If DOP is set, use HDOP / VDOP values instead of PDOP",
"numSatellites": "Number of satellites",
"sequenceNumber": "Sequence number",
"timestamp": "Timestamp",
"unset": "Unset",
"vehicleHeading": "Vehicle heading",
"vehicleSpeed": "Vehicle speed"
}
},
"power": {
"adcMultiplierOverride": {
"description": "Used for tweaking battery voltage reading",
"label": "ADC Multiplier Override ratio"
},
"ina219Address": {
"description": "Address of the INA219 battery monitor",
"label": "INA219 Address"
},
"lightSleepDuration": {
"description": "How long the device will be in light sleep for",
"label": "Light Sleep Duration"
},
"minimumWakeTime": {
"description": "Minimum amount of time the device will stay awake for after receiving a packet",
"label": "Minimum Wake Time"
},
"noConnectionBluetoothDisabled": {
"description": "If the device does not receive a Bluetooth connection, the BLE radio will be disabled after this long",
"label": "No Connection Bluetooth Disabled"
},
"powerSavingEnabled": {
"description": "Select if powered from a low-current source (i.e. solar), to minimize power consumption as much as possible.",
"label": "Enable power saving mode"
},
"shutdownOnBatteryDelay": {
"description": "Automatically shutdown node after this long when on battery, 0 for indefinite",
"label": "Shutdown on battery delay"
},
"superDeepSleepDuration": {
"description": "How long the device will be in super deep sleep for",
"label": "Super Deep Sleep Duration"
},
"powerConfigSettings": {
"description": "Settings for the power module",
"label": "Power Config"
},
"sleepSettings": {
"description": "Sleep settings for the power module",
"label": "Sleep Settings"
}
},
"security": {
"description": "Settings for the Security configuration",
"title": "Security Settings",
"button_backupKey": "Backup Key",
"adminChannelEnabled": {
"description": "Allow incoming device control over the insecure legacy admin channel",
"label": "Allow Legacy Admin"
},
"enableDebugLogApi": {
"description": "Output live debug logging over serial, view and export position-redacted device logs over Bluetooth",
"label": "Enable Debug Log API"
},
"managed": {
"description": "If enabled, device configuration options are only able to be changed remotely by a Remote Admin node via admin messages. Do not enable this option unless at least one suitable Remote Admin node has been setup, and the public key is stored in one of the fields above.",
"label": "Managed"
},
"privateKey": {
"description": "Used to create a shared key with a remote device",
"label": "Private Key"
},
"publicKey": {
"description": "Sent out to other nodes on the mesh to allow them to compute a shared secret key",
"label": "Public Key"
},
"primaryAdminKey": {
"description": "The primary public key authorized to send admin messages to this node",
"label": "Primary Admin Key"
},
"secondaryAdminKey": {
"description": "The secondary public key authorized to send admin messages to this node",
"label": "Secondary Admin Key"
},
"serialOutputEnabled": {
"description": "Serial Console over the Stream API",
"label": "Serial Output Enabled"
},
"tertiaryAdminKey": {
"description": "The tertiary public key authorized to send admin messages to this node",
"label": "Tertiary Admin Key"
},
"adminSettings": {
"description": "Settings for Admin",
"label": "Admin Settings"
},
"loggingSettings": {
"description": "Settings for Logging",
"label": "Logging Settings"
},
"validation": {
"adminKeyMustBe256BitPsk": "Admin Key is required to be a 256 bit pre-shared key (PSK)",
"adminKeyRequiredWhenManaged": "At least one admin key is requred if the node is managed.",
"enterValid256BitPsk": "Please enter a valid 256 bit PSK",
"invalidAdminKeyFormat": "Invalid Admin Key format",
"invalidPrivateKeyFormat": "Invalid Private Key format",
"privateKeyMustBe256BitPsk": "Private Key is required to be a 256 bit pre-shared key (PSK)",
"privateKeyRequired": "Private Key is required"
}
}
}

160
src/i18n/locales/en/dialog.json

@ -0,0 +1,160 @@
{
"deleteMessages": {
"description": "This action will clear all message history. This cannot be undone. Are you sure you want to continue?",
"title": "Clear All Messages"
},
"deviceName": {
"description": "The Device will restart once the config is saved.",
"longName": "Long Name",
"shortName": "Short Name",
"title": "Change Device Name"
},
"import": {
"description": "The current LoRa configuration will be overridden.",
"error": {
"invalidUrl": "Invalid Meshtastic URL"
},
"channelPrefix": "Channel: ",
"channelSetUrl": "Channel Set/QR Code URL",
"channels": "Channels:",
"usePreset": "Use Preset?",
"title": "Import Channel Set"
},
"locationResponse": {
"altitude": "Altitude: ",
"coordinates": "Coordinates: ",
"title": "Location: {{identifier}}"
},
"pkiRegenerateDialog": {
"title": "Regenerate Pre-Shared Key?",
"description": "Are you sure you want to regenerate the pre-shared key?",
"regenerate": "Regenerate"
},
"newDeviceDialog": {
"title": "Connect New Device",
"https": "https",
"http": "http",
"tabHttp": "HTTP",
"tabBluetooth": "Bluetooth",
"tabSerial": "Serial",
"useHttps": "Use HTTPS",
"connecting": "Connecting...",
"connect": "Connect",
"connectionFailedAlert": {
"title": "Connection Failed",
"descriptionPrefix": "Could not connect to the device. ",
"httpsHint": "If using HTTPS, you may need to accept a self-signed certificate first. ",
"openLinkPrefix": "Please open ",
"openLinkSuffix": " in a new tab",
"acceptTlsWarningSuffix": ", accept any TLS warnings if prompted, then try again",
"learnMoreLink": "Learn more"
},
"httpConnection": {
"label": "IP Address/Hostname",
"placeholder": "000.000.000.000 / meshtastic.local"
},
"serialConnection": {
"noDevicesPaired": "No devices paired yet.",
"newDeviceButton": "New device",
"deviceIdentifier": "# {{index}} - {{vendorId}} - {{productId}}"
},
"bluetoothConnection": {
"noDevicesPaired": "No devices paired yet.",
"newDeviceButton": "New device"
},
"validation": {
"requiresFeatures": "This connection type requires <0></0>. Please use a supported browser, like Chrome or Edge.",
"requiresSecureContext": "This application requires a <0>secure context</0>. Please connect using HTTPS or localhost.",
"additionallyRequiresSecureContext": "Additionally, it requires a <0>secure context</0>. Please connect using HTTPS or localhost."
}
},
"nodeDetails": {
"message": "Message",
"requestPosition": "Request Position",
"traceRoute": "Trace Route",
"airTxUtilization": "Air TX utilization",
"allRawMetrics": "All Raw Metrics:",
"batteryLevel": "Battery level",
"channelUtilization": "Channel utilization",
"details": "Details:",
"deviceMetrics": "Device Metrics:",
"hardware": "Hardware: ",
"lastHeard": "Last Heard: ",
"nodeHexPrefix": "Node Hex: !",
"nodeNumber": "Node Number: ",
"position": "Position:",
"role": "Role: ",
"uptime": "Uptime: ",
"voltage": "Voltage",
"title": "Node Details for {{identifier}}",
"ignoreNode": "Ignore node",
"removeNode": "Remove node",
"unignoreNode": "Unignore node"
},
"pkiBackup": {
"description": "We recommend backing up your key data regularly. Would you like to back up now?",
"loseKeysWarning": "If you lose your keys, you will need to reset your device.",
"secureBackup": "Its important to backup your public and private keys and store your backup securely!",
"footer": "=== END OF KEYS ===",
"header": "=== MESHTASTIC KEYS FOR {{longName}} ({{shortName}}) ===",
"privateKey": "Private Key:",
"publicKey": "Public Key:",
"fileName": "meshtastic_keys_{{longName}}_{{shortName}}.txt",
"title": "Backup Keys"
},
"pkiRegenerate": {
"description": "Are you sure you want to regenerate key pair?",
"title": "Regenerate Key Pair"
},
"qr": {
"addChannels": "Add Channels",
"replaceChannels": "Replace Channels",
"description": "The current LoRa configuration will also be shared.",
"sharableUrl": "Sharable URL",
"title": "Generate QR Code"
},
"rebootOta": {
"title": "Schedule Reboot",
"description": "Reboot the connected node after a delay into OTA (Over-the-Air) mode.",
"enterDelay": "Enter delay (sec)",
"scheduled": "Reboot has been scheduled"
},
"reboot": {
"title": "Schedule Reboot",
"description": "Reboot the connected node after x minutes."
},
"refreshKeys": {
"description": {
"acceptNewKeys": "This will remove the node from device and request new keys.",
"keyMismatchReasonSuffix": ". This is due to the remote node's current public key does not match the previously stored key for this node.",
"unableToSendDmPrefix": "Your node is unable to send a direct message to node: "
},
"acceptNewKeys": "Accept New Keys",
"title": "Keys Mismatch - {{identifier}}"
},
"removeNode": {
"description": "Are you sure you want to remove this Node?",
"title": "Remove Node?"
},
"shutdown": {
"title": "Schedule Shutdown",
"description": "Turn off the connected node after x minutes."
},
"traceRoute": {
"routeToDestination": "Route to destination:",
"routeBack": "Route back:"
},
"tracerouteResponse": {
"title": "Traceroute: {{identifier}}"
},
"unsafeRoles": {
"confirmUnderstanding": "Yes, I know what I'm doing",
"conjunction": " and the blog post about ",
"postamble": " and understand the implications of changing the role.",
"preamble": "I have read the ",
"choosingRightDeviceRole": "Choosing The Right Device Role",
"deviceRoleDocumentation": "Device Role Documentation",
"title": "Are you sure?"
}
}

37
src/i18n/locales/en/messages.json

@ -0,0 +1,37 @@
{
"page": {
"title": "Messages: {{chatName}}"
},
"emptyState": {
"title": "Select a Chat",
"text": "No messages yet."
},
"selectChatPrompt": {
"text": "Select a channel or node to start messaging."
},
"actionsMenu": {
"addReactionLabel": "Add Reaction",
"replyLabel": "Reply"
},
"item": {
"status": {
"delivered": {
"label": "Message delivered",
"displayText": "Message delivered"
},
"failed": {
"label": "Message delivery failed",
"displayText": "Delivery failed"
},
"unknown": {
"label": "Message status unknown",
"displayText": "Unknown state"
},
"waiting": {
"ariaLabel": "Sending message",
"displayText": "Waiting for delivery"
}
}
}
}

448
src/i18n/locales/en/moduleConfig.json

@ -0,0 +1,448 @@
{
"page": {
"tabAmbientLighting": "Ambient Lighting",
"tabAudio": "Audio",
"tabCannedMessage": "Canned",
"tabDetectionSensor": "Detection Sensor",
"tabExternalNotification": "Ext Notif",
"tabMqtt": "MQTT",
"tabNeighborInfo": "Neighbor Info",
"tabPaxcounter": "Paxcounter",
"tabRangeTest": "Range Test",
"tabSerial": "Serial",
"tabStoreAndForward": "S&F",
"tabTelemetry": "Telemetry"
},
"ambientLighting": {
"title": "Ambient Lighting Settings",
"description": "Settings for the Ambient Lighting module",
"ledState": {
"label": "LED State",
"description": "Sets LED to on or off"
},
"current": {
"label": "Current",
"description": "Sets the current for the LED output. Default is 10"
},
"red": {
"label": "Red",
"description": "Sets the red LED level. Values are 0-255"
},
"green": {
"label": "Green",
"description": "Sets the green LED level. Values are 0-255"
},
"blue": {
"label": "Blue",
"description": "Sets the blue LED level. Values are 0-255"
}
},
"audio": {
"title": "Audio Settings",
"description": "Settings for the Audio module",
"codec2Enabled": {
"label": "Codec 2 Enabled",
"description": "Enable Codec 2 audio encoding"
},
"pttPin": {
"label": "PTT Pin",
"description": "GPIO pin to use for PTT"
},
"bitrate": {
"label": "Bitrate",
"description": "Bitrate to use for audio encoding"
},
"i2sWs": {
"label": "i2S WS",
"description": "GPIO pin to use for i2S WS"
},
"i2sSd": {
"label": "i2S SD",
"description": "GPIO pin to use for i2S SD"
},
"i2sDin": {
"label": "i2S DIN",
"description": "GPIO pin to use for i2S DIN"
},
"i2sSck": {
"label": "i2S SCK",
"description": "GPIO pin to use for i2S SCK"
}
},
"cannedMessage": {
"title": "Canned Message Settings",
"description": "Settings for the Canned Message module",
"moduleEnabled": {
"label": "Module Enabled",
"description": "Enable Canned Message"
},
"rotary1Enabled": {
"label": "Rotary Encoder #1 Enabled",
"description": "Enable the rotary encoder"
},
"inputbrokerPinA": {
"label": "Encoder Pin A",
"description": "GPIO Pin Value (1-39) For encoder port A"
},
"inputbrokerPinB": {
"label": "Encoder Pin B",
"description": "GPIO Pin Value (1-39) For encoder port B"
},
"inputbrokerPinPress": {
"label": "Encoder Pin Press",
"description": "GPIO Pin Value (1-39) For encoder Press"
},
"inputbrokerEventCw": {
"label": "Clockwise event",
"description": "Select input event."
},
"inputbrokerEventCcw": {
"label": "Counter Clockwise event",
"description": "Select input event."
},
"inputbrokerEventPress": {
"label": "Press event",
"description": "Select input event"
},
"updown1Enabled": {
"label": "Up Down enabled",
"description": "Enable the up / down encoder"
},
"allowInputSource": {
"label": "Allow Input Source",
"description": "Select from: '_any', 'rotEnc1', 'upDownEnc1', 'cardkb'"
},
"sendBell": {
"label": "Send Bell",
"description": "Sends a bell character with each message"
}
},
"detectionSensor": {
"title": "Detection Sensor Settings",
"description": "Settings for the Detection Sensor module",
"enabled": {
"label": "Enabled",
"description": "Enable or disable Detection Sensor Module"
},
"minimumBroadcastSecs": {
"label": "Minimum Broadcast Seconds",
"description": "The interval in seconds of how often we can send a message to the mesh when a state change is detected"
},
"stateBroadcastSecs": {
"label": "State Broadcast Seconds",
"description": "The interval in seconds of how often we should send a message to the mesh with the current state regardless of changes"
},
"sendBell": {
"label": "Send Bell",
"description": "Send ASCII bell with alert message"
},
"name": {
"label": "Friendly Name",
"description": "Used to format the message sent to mesh, max 20 Characters"
},
"monitorPin": {
"label": "Monitor Pin",
"description": "The GPIO pin to monitor for state changes"
},
"detectionTriggeredHigh": {
"label": "Detection Triggered High",
"description": "Whether or not the GPIO pin state detection is triggered on HIGH (1), otherwise LOW (0)"
},
"usePullup": {
"label": "Use Pullup",
"description": "Whether or not use INPUT_PULLUP mode for GPIO pin"
}
},
"externalNotification": {
"title": "External Notification Settings",
"description": "Configure the external notification module",
"enabled": {
"label": "Module Enabled",
"description": "Enable External Notification"
},
"outputMs": {
"label": "Output MS",
"description": "Output MS"
},
"output": {
"label": "Output",
"description": "Output"
},
"outputVibra": {
"label": "Output Vibrate",
"description": "Output Vibrate"
},
"outputBuzzer": {
"label": "Output Buzzer",
"description": "Output Buzzer"
},
"active": {
"label": "Active",
"description": "Active"
},
"alertMessage": {
"label": "Alert Message",
"description": "Alert Message"
},
"alertMessageVibra": {
"label": "Alert Message Vibrate",
"description": "Alert Message Vibrate"
},
"alertMessageBuzzer": {
"label": "Alert Message Buzzer",
"description": "Alert Message Buzzer"
},
"alertBell": {
"label": "Alert Bell",
"description": "Should an alert be triggered when receiving an incoming bell?"
},
"alertBellVibra": {
"label": "Alert Bell Vibrate",
"description": "Alert Bell Vibrate"
},
"alertBellBuzzer": {
"label": "Alert Bell Buzzer",
"description": "Alert Bell Buzzer"
},
"usePwm": {
"label": "Use PWM",
"description": "Use PWM"
},
"nagTimeout": {
"label": "Nag Timeout",
"description": "Nag Timeout"
},
"useI2sAsBuzzer": {
"label": "Use I²S Pin as Buzzer",
"description": "Designate I²S Pin as Buzzer Output"
}
},
"mqtt": {
"title": "MQTT Settings",
"description": "Settings for the MQTT module",
"enabled": {
"label": "Enabled",
"description": "Enable or disable MQTT"
},
"address": {
"label": "MQTT Server Address",
"description": "MQTT server address to use for default/custom servers"
},
"username": {
"label": "MQTT Username",
"description": "MQTT username to use for default/custom servers"
},
"password": {
"label": "MQTT Password",
"description": "MQTT password to use for default/custom servers"
},
"encryptionEnabled": {
"label": "Encryption Enabled",
"description": "Enable or disable MQTT encryption. Note: All messages are sent to the MQTT broker unencrypted if this option is not enabled, even when your uplink channels have encryption keys set. This includes position data."
},
"jsonEnabled": {
"label": "JSON Enabled",
"description": "Whether to send/consume JSON packets on MQTT"
},
"tlsEnabled": {
"label": "TLS Enabled",
"description": "Enable or disable TLS"
},
"root": {
"label": "Root topic",
"description": "MQTT root topic to use for default/custom servers"
},
"proxyToClientEnabled": {
"label": "Proxy to Client Enabled",
"description": "Use the client's internet connection for MQTT (feature only active in mobile apps)"
},
"mapReportingEnabled": {
"label": "Map Reporting Enabled",
"description": "Enable or disable map reporting"
},
"mapReportSettings": {
"publishIntervalSecs": {
"label": "Map Report Publish Interval (s)",
"description": "Interval in seconds to publish map reports"
},
"positionPrecision": {
"label": "Approximate Location",
"description": "Position shared will be accurate within this distance",
"options": {
"metric_km23": "Within 23 km",
"metric_km12": "Within 12 km",
"metric_km5_8": "Within 5.8 km",
"metric_km2_9": "Within 2.9 km",
"metric_km1_5": "Within 1.5 km",
"metric_m700": "Within 700 m",
"metric_m350": "Within 350 m",
"metric_m200": "Within 200 m",
"metric_m90": "Within 90 m",
"metric_m50": "Within 50 m",
"imperial_mi15": "Within 15 miles",
"imperial_mi7_3": "Within 7.3 miles",
"imperial_mi3_6": "Within 3.6 miles",
"imperial_mi1_8": "Within 1.8 miles",
"imperial_mi0_9": "Within 0.9 miles",
"imperial_mi0_5": "Within 0.5 miles",
"imperial_mi0_2": "Within 0.2 miles",
"imperial_ft600": "Within 600 feet",
"imperial_ft300": "Within 300 feet",
"imperial_ft150": "Within 150 feet"
}
}
}
},
"neighborInfo": {
"title": "Neighbor Info Settings",
"description": "Settings for the Neighbor Info module",
"enabled": {
"label": "Enabled",
"description": "Enable or disable Neighbor Info Module"
},
"updateInterval": {
"label": "Update Interval",
"description": "Interval in seconds of how often we should try to send our Neighbor Info to the mesh"
}
},
"paxcounter": {
"title": "Paxcounter Settings",
"description": "Settings for the Paxcounter module",
"enabled": {
"label": "Module Enabled",
"description": "Enable Paxcounter"
},
"paxcounterUpdateInterval": {
"label": "Update Interval (seconds)",
"description": "How long to wait between sending paxcounter packets"
},
"wifiThreshold": {
"label": "WiFi RSSI Threshold",
"description": "At what WiFi RSSI level should the counter increase. Defaults to -80."
},
"bleThreshold": {
"label": "BLE RSSI Threshold",
"description": "At what BLE RSSI level should the counter increase. Defaults to -80."
}
},
"rangeTest": {
"title": "Range Test Settings",
"description": "Settings for the Range Test module",
"enabled": {
"label": "Module Enabled",
"description": "Enable Range Test"
},
"sender": {
"label": "Message Interval",
"description": "How long to wait between sending test packets"
},
"save": {
"label": "Save CSV to storage",
"description": "ESP32 Only"
}
},
"serial": {
"title": "Serial Settings",
"description": "Settings for the Serial module",
"enabled": {
"label": "Module Enabled",
"description": "Enable Serial output"
},
"echo": {
"label": "Echo",
"description": "Any packets you send will be echoed back to your device"
},
"rxd": {
"label": "Receive Pin",
"description": "Set the GPIO pin to the RXD pin you have set up."
},
"txd": {
"label": "Transmit Pin",
"description": "Set the GPIO pin to the TXD pin you have set up."
},
"baud": {
"label": "Baud Rate",
"description": "The serial baud rate"
},
"timeout": {
"label": "Timeout",
"description": "Seconds to wait before we consider your packet as 'done'"
},
"mode": {
"label": "Mode",
"description": "Select Mode"
},
"overrideConsoleSerialPort": {
"label": "Override Console Serial Port",
"description": "If you have a serial port connected to the console, this will override it."
}
},
"storeForward": {
"title": "Store & Forward Settings",
"description": "Settings for the Store & Forward module",
"enabled": {
"label": "Module Enabled",
"description": "Enable Store & Forward"
},
"heartbeat": {
"label": "Heartbeat Enabled",
"description": "Enable Store & Forward heartbeat"
},
"records": {
"label": "Number of records",
"description": "Number of records to store"
},
"historyReturnMax": {
"label": "History return max",
"description": "Max number of records to return"
},
"historyReturnWindow": {
"label": "History return window",
"description": "Max number of records to return"
}
},
"telemetry": {
"title": "Telemetry Settings",
"description": "Settings for the Telemetry module",
"deviceUpdateInterval": {
"label": "Device Metrics",
"description": "Device metrics update interval (seconds)"
},
"environmentUpdateInterval": {
"label": "Environment metrics update interval (seconds)",
"description": ""
},
"environmentMeasurementEnabled": {
"label": "Module Enabled",
"description": "Enable the Environment Telemetry"
},
"environmentScreenEnabled": {
"label": "Displayed on Screen",
"description": "Show the Telemetry Module on the OLED"
},
"environmentDisplayFahrenheit": {
"label": "Display Fahrenheit",
"description": "Display temp in Fahrenheit"
},
"airQualityEnabled": {
"label": "Air Quality Enabled",
"description": "Enable the Air Quality Telemetry"
},
"airQualityInterval": {
"label": "Air Quality Update Interval",
"description": "How often to send Air Quality data over the mesh"
},
"powerMeasurementEnabled": {
"label": "Power Measurement Enabled",
"description": "Enable the Power Measurement Telemetry"
},
"powerUpdateInterval": {
"label": "Power Update Interval",
"description": "How often to send Power data over the mesh"
},
"powerScreenEnabled": {
"label": "Power Screen Enabled",
"description": "Enable the Power Telemetry Screen"
}
}
}

51
src/i18n/locales/en/nodes.json

@ -0,0 +1,51 @@
{
"nodeDetail": {
"publicKeyEnabled": {
"label": "Public Key Enabled"
},
"noPublicKey": {
"label": "No Public Key"
},
"directMessage": {
"label": "Direct Message {{shortName}}"
},
"favorite": {
"label": "Favorite"
},
"notFavorite": {
"label": "Not a Favorite"
},
"status": {
"heard": "Heard",
"mqtt": "MQTT"
},
"elevation": {
"label": "Elevation"
},
"channelUtil": {
"label": "Channel Util"
},
"airtimeUtil": {
"label": "Airtime Util"
}
},
"nodesTable": {
"headings": {
"longName": "Long Name",
"connection": "Connection",
"lastHeard": "Last Heard",
"encryption": "Encryption",
"model": "Model",
"macAddress": "MAC Address"
},
"connectionStatus": {
"direct": "Direct",
"away": "away",
"unknown": "-",
"viaMqtt": ", via MQTT"
},
"lastHeardStatus": {
"never": "Never"
}
}
}

152
src/i18n/locales/en/ui.json

@ -0,0 +1,152 @@
{
"navigation": {
"title": "Navigation",
"messages": "Messages",
"map": "Map",
"config": "Config",
"radioConfig": "Radio Config",
"moduleConfig": "Module Config",
"channels": "Channels",
"nodes": "Nodes"
},
"app": {
"title": "Meshtastic",
"logo": "Meshtastic Logo"
},
"sidebar": {
"collapseToggle": {
"button": {
"open": "Open sidebar",
"close": "Close sidebar"
}
},
"deviceInfo": {
"volts": "{{voltage}} volts",
"firmwareVersion": "v{{version}}",
"button": {
"editDeviceName": "Edit device name"
}
}
},
"batteryStatus": {
"charging": "{{level}}% charging",
"pluggedIn": "Plugged in",
"title": "Battery"
},
"search": {
"nodes": "Search nodes...",
"channels": "Search channels...",
"commandPalette": "Search commands..."
},
"toast": {
"positionRequestSent": { "title": "Position request sent." },
"requestingPosition": { "title": "Requesting position, please wait..." },
"sendingTraceroute": { "title": "Sending Traceroute, please wait..." },
"tracerouteSent": { "title": "Traceroute sent." },
"savedChannel": { "title": "Saved Channel: {{channelName}}" },
"messages": {
"pkiEncryption": { "title": "Chat is using PKI encryption." },
"pskEncryption": { "title": "Chat is using PSK encryption." }
},
"configSaveError": {
"title": "Error Saving Config",
"description": "An error occurred while saving the configuration."
},
"validationError": {
"title": "Config Errors Exist",
"description": "Please fix the configuration errors before saving."
},
"saveSuccess": {
"title": "Saving Config",
"description": "The configuration change {{case}} has been saved."
}
},
"notifications": {
"copied": {
"label": "Copied!"
},
"copyToClipboard": {
"label": "Copy to clipboard"
},
"hidePassword": {
"label": "Hide password"
},
"showPassword": {
"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"
}
},
"theme": {
"dark": "Dark",
"light": "Light",
"system": "Automatic",
"changeTheme": "Change current theme"
},
"footer": {
"text": "Powered by <0>▲ Vercel</0> | Meshtastic® is a registered trademark of Meshtastic LLC. | <1>Legal Information</1>"
}
}

2
src/index.tsx

@ -5,7 +5,7 @@ import { StrictMode, Suspense } from "react";
import { createRoot } from "react-dom/client";
import { App } from "@app/App.tsx";
import "@app/i18n.ts";
import "./i18n/config.ts";
const container = document.getElementById("root") as HTMLElement;
const root = createRoot(container);

18
src/pages/Channels.tsx

@ -15,15 +15,16 @@ import { QrCodeIcon, UploadIcon } from "lucide-react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
export const getChannelName = (channel: Protobuf.Channel.Channel) =>
channel.settings?.name.length
export const getChannelName = (channel: Protobuf.Channel.Channel) => {
return channel.settings?.name.length
? channel.settings?.name
: channel.index === 0
? i18next.t("channel_name_primary")
: i18next.t("channel_name_prefix", { index: channel.index });
? i18next.t("page.broadcastLabel")
: i18next.t("page.channelIndex", { ns: "channels", index: channel.index });
};
const ChannelsPage = () => {
const { t } = useTranslation();
const { t } = useTranslation("channels");
const { channels, setDialogOpen } = useDevice();
const [activeChannel] = useState<Types.ChannelNumber>(
Types.ChannelNumber.Primary,
@ -35,12 +36,11 @@ const ChannelsPage = () => {
return (
<>
<PageLayout
contentClassName="overflow-auto"
leftBar={<Sidebar />}
label={currentChannel
? t("channel_page_title", {
channelName: getChannelName(currentChannel),
})
: t("common_loading")}
? getChannelName(currentChannel)
: t("loading", { ns: "common" })}
actions={[
{
key: "import",

26
src/pages/Config/DeviceConfig.tsx

@ -1,11 +1,11 @@
import { Bluetooth } from "@components/PageComponents/Config/Bluetooth.tsx";
import { Device } from "../../components/PageComponents/Config/Device/index.tsx";
import { Device } from "@components/PageComponents/Config/Device/index.tsx";
import { Display } from "@components/PageComponents/Config/Display.tsx";
import { LoRa } from "@components/PageComponents/Config/LoRa.tsx";
import { Network } from "../../components/PageComponents/Config/Network/index.tsx";
import { Network } from "@components/PageComponents/Config/Network/index.tsx";
import { Position } from "@components/PageComponents/Config/Position.tsx";
import { Power } from "@components/PageComponents/Config/Power.tsx";
import { Security } from "../../components/PageComponents/Config/Security/Security.tsx";
import { Security } from "@components/PageComponents/Config/Security/Security.tsx";
import {
Tabs,
TabsContent,
@ -15,45 +15,45 @@ import {
import { useTranslation } from "react-i18next";
export const DeviceConfig = () => {
const { t } = useTranslation();
const { t } = useTranslation("deviceConfig");
const tabs = [
{
label: t("config_device_tab_device"),
label: t("page.tabDevice"),
element: Device,
count: 0,
},
{
label: t("config_device_tab_position"),
label: t("page.tabPosition"),
element: Position,
},
{
label: t("config_device_tab_power"),
label: t("page.tabPower"),
element: Power,
},
{
label: t("config_device_tab_network"),
label: t("page.tabNetwork"),
element: Network,
},
{
label: t("config_device_tab_display"),
label: t("page.tabDisplay"),
element: Display,
},
{
label: t("config_device_tab_lora"),
label: t("page.tabLora"),
element: LoRa,
},
{
label: t("config_device_tab_bluetooth"),
label: t("page.tabBluetooth"),
element: Bluetooth,
},
{
label: t("config_device_tab_security"),
label: t("page.tabSecurity"),
element: Security,
},
];
return (
<Tabs defaultValue={t("config_device_tab_device")}>
<Tabs defaultValue={t("page.tabDevice")}>
<TabsList className="dark:bg-slate-700">
{tabs.map((tab) => (
<TabsTrigger

28
src/pages/Config/ModuleConfig.tsx

@ -19,60 +19,60 @@ import {
import { useTranslation } from "react-i18next";
export const ModuleConfig = () => {
const { t } = useTranslation();
const { t } = useTranslation("moduleConfig");
const tabs = [
{
label: t("config_module_tab_mqtt"),
label: t("page.tabMqtt"),
element: MQTT,
},
{
label: t("config_module_tab_serial"),
label: t("page.tabSerial"),
element: Serial,
},
{
label: t("config_module_tab_externalNotification"),
label: t("page.tabExternalNotification"),
element: ExternalNotification,
},
{
label: t("config_module_tab_storeAndForward"),
label: t("page.tabStoreAndForward"),
element: StoreForward,
},
{
label: t("config_module_tab_rangeTest"),
label: t("page.tabRangeTest"),
element: RangeTest,
},
{
label: t("config_module_tab_telemetry"),
label: t("page.tabTelemetry"),
element: Telemetry,
},
{
label: t("config_module_tab_cannedMessage"),
label: t("page.tabCannedMessage"),
element: CannedMessage,
},
{
label: t("config_module_tab_audio"),
label: t("page.tabAudio"),
element: Audio,
},
{
label: t("config_module_tab_neighborInfo"),
label: t("page.tabNeighborInfo"),
element: NeighborInfo,
},
{
label: t("config_module_tab_ambientLighting"),
label: t("page.tabAmbientLighting"),
element: AmbientLighting,
},
{
label: t("config_module_tab_detectionSensor"),
label: t("page.tabDetectionSensor"),
element: DetectionSensor,
},
{
label: t("config_module_tab_paxcounter"),
label: t("page.tabPaxcounter"),
element: Paxcounter,
},
];
return (
<Tabs defaultValue={t("config_module_tab_mqtt")}>
<Tabs defaultValue={t("page.tabMqtt")}>
<TabsList className="dark:bg-slate-800">
{tabs.map((tab) => (
<TabsTrigger

33
src/pages/Config/index.tsx

@ -20,13 +20,13 @@ const ConfigPage = () => {
const [isSaving, setIsSaving] = useState(false);
const { toast } = useToast();
const isError = hasErrors();
const { t } = useTranslation();
const { t } = useTranslation("deviceConfig");
const handleSave = async () => {
if (hasErrors()) {
return toast({
title: t("config_toast_errorsExist_title"),
description: t("config_toast_errorsExist_description"),
title: t("toast.validationError.title"),
description: t("toast.validationError.description"),
});
}
@ -37,8 +37,8 @@ const ConfigPage = () => {
workingConfig.map((config) =>
connection?.setConfig(config).then(() =>
toast({
title: t("config_toast_saving_title"),
description: t("config_toast_saving_description", {
title: t("toast.saveSuccess.title"),
description: t("toast.saveSuccess.description", {
case: config.payloadVariant.case,
}),
})
@ -50,8 +50,8 @@ const ConfigPage = () => {
workingModuleConfig.map((moduleConfig) =>
connection?.setModuleConfig(moduleConfig).then(() =>
toast({
title: t("config_toast_saving_title"),
description: t("config_toast_saving_description", {
title: t("toast.saveSuccess.title"),
description: t("toast.saveSuccess.description", {
case: moduleConfig.payloadVariant.case,
}),
})
@ -63,8 +63,8 @@ const ConfigPage = () => {
await connection?.commitEditSettings();
} catch (_error) {
toast({
title: t("config_toast_errorSaving_title"),
description: t("config_toast_errorSaving_description"),
title: t("toast.configSaveError.title"),
description: t("toast.configSaveError.description"),
});
} finally {
setIsSaving(false);
@ -74,15 +74,18 @@ const ConfigPage = () => {
const leftSidebar = useMemo(
() => (
<Sidebar>
<SidebarSection label={t("config_sidebar_section_modules_label")}>
<SidebarSection
label={t("sidebar.label")}
className="py-2 px-0"
>
<SidebarButton
label={t("navigation_title_radioConfig")}
label={t("navigation.radioConfig")}
active={activeConfigSection === "device"}
onClick={() => setActiveConfigSection("device")}
Icon={SettingsIcon}
/>
<SidebarButton
label={t("navigation_title_moduleConfig")}
label={t("navigation.moduleConfig")}
active={activeConfigSection === "module"}
onClick={() => setActiveConfigSection("module")}
Icon={BoxesIcon}
@ -90,7 +93,7 @@ const ConfigPage = () => {
</SidebarSection>
</Sidebar>
),
[activeConfigSection, t],
[activeConfigSection],
);
return (
@ -99,8 +102,8 @@ const ConfigPage = () => {
contentClassName="overflow-auto"
leftBar={leftSidebar}
label={activeConfigSection === "device"
? t("navigation_title_radioConfig")
: t("navigation_title_moduleConfig")}
? t("navigation.radioConfig")
: t("navigation.moduleConfig")}
actions={[
{
key: "save",

23
src/pages/Dashboard/index.tsx

@ -14,9 +14,10 @@ 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();
const { t } = useTranslation("dashboard");
const { setConnectDialogOpen, setSelectedDevice } = useAppStore();
const { getDevices } = useDeviceStore();
@ -28,10 +29,10 @@ export const Dashboard = () => {
<div className="flex items-center justify-between">
<div className="space-y-1">
<Heading as="h3">
{t("dashboard_connectedDevicesSection_title")}
{t("dashboard.title")}
</Heading>
<Subtle>
{t("dashboard_connectedDevicesSection_description")}
{t("dashboard.description")}
</Subtle>
</div>
</div>
@ -55,14 +56,15 @@ export const Dashboard = () => {
<div className="flex items-center justify-between">
<p className="truncate text-sm font-medium text-accent">
{device.getNode(device.hardware.myNodeNum)?.user
?.longName ?? t("common_unknown_short")}
?.longName ??
t("unknown.shortName")}
</p>
<div className="inline-flex w-24 justify-center gap-2 rounded-full bg-slate-100 py-1 text-xs font-semibold text-slate-900 transition-colors hover:bg-slate-700 hover:text-slate-50">
{device.connection?.connType === "ble" && (
<>
<BluetoothIcon size={16} />
{t(
"dashboard_connectedDevicesSection_connectionType_ble",
"dashboard.connectionType_ble",
)}
</>
)}
@ -70,7 +72,7 @@ export const Dashboard = () => {
<>
<UsbIcon size={16} />
{t(
"dashboard_connectedDevicesSection_connectionType_serial",
"dashboard.connectionType_serial",
)}
</>
)}
@ -78,7 +80,7 @@ export const Dashboard = () => {
<>
<NetworkIcon size={16} />
{t(
"dashboard_connectedDevicesSection_connectionType_network",
"dashboard.connectionType_network",
)}
</>
)}
@ -109,10 +111,11 @@ export const Dashboard = () => {
className="mx-auto text-text-secondary"
/>
<Heading as="h3">
{t("dashboard_connectedDevicesSection_noDevicesTitle")}
{t("dashboard.noDevicesTitle")}
</Heading>
{/* <LanguageSwitcher /> */}
<Subtle>
{t("dashboard_connectedDevicesSection_noDevicesDescription")}
{t("dashboard.noDevicesDescription")}
</Subtle>
<Button
className="gap-2"
@ -121,7 +124,7 @@ export const Dashboard = () => {
>
<PlusIcon size={16} />
{t(
"dashboard_connectedDevicesSection_button_newConnection",
"dashboard.button_newConnection",
)}
</Button>
</div>

50
src/pages/Messages.tsx

@ -46,7 +46,7 @@ export const MessagesPage = () => {
const { toast } = useToast();
const { isCollapsed } = useSidebar();
const [searchTerm, setSearchTerm] = useState<string>("");
const { t } = useTranslation();
const { t } = useTranslation(["messages", "channels", "ui"]);
const deferredSearch = useDeferredValue(searchTerm);
const filteredNodes = (): NodeInfoWithUnread[] => {
@ -165,7 +165,7 @@ export const MessagesPage = () => {
default:
return (
<div className="flex-1 flex items-center justify-center text-slate-500 p-4">
{t("messages_selectChatPrompt")}
{t("messagesPage.selectChatPrompt")}
</div>
);
}
@ -174,18 +174,20 @@ export const MessagesPage = () => {
const leftSidebar = useMemo(() => (
<Sidebar>
<SidebarSection
label={t("messages_channelsSection_title")}
label={t("navigation.channels")}
className="py-2 px-0"
>
{filteredChannels?.map((channel) => (
<SidebarButton
key={channel.index}
count={unreadCounts.get(channel.index)}
label={channel.settings?.name || (channel.index === 0
? t("messages_channelsSection_primaryChannelName")
: t("messages_channelsSection_channelName", {
index: channel.index,
}))}
label={channel.settings?.name ||
(channel.index === 0
? t("page.broadcastLabel", { ns: "channels" })
: t("page.channelLabel", {
index: channel.index,
ns: "channels",
}))}
active={activeChat === channel.index &&
chatType === MessageType.Broadcast}
onClick={() => {
@ -222,7 +224,7 @@ export const MessagesPage = () => {
<label className="p-2 block">
<Input
type="text"
placeholder={t("messages_nodesSection_searchPlaceholder")}
placeholder={t("search.nodes")}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
showClearButton={!!searchTerm}
@ -237,7 +239,8 @@ export const MessagesPage = () => {
<SidebarButton
key={node.num}
preventCollapse
label={node.user?.longName ?? t("common_unknown_short")}
label={node.user?.longName ??
t("unknown.shortName")}
count={node.unreadCount > 0 ? node.unreadCount : undefined}
active={activeChat === node.num &&
chatType === MessageType.Direct}
@ -248,7 +251,8 @@ export const MessagesPage = () => {
}}
>
<Avatar
text={node.user?.shortName ?? t("common_unknown_short")}
text={node.user?.shortName ??
t("unknown.shortName")}
className={cn(hasNodeError(node.num) && "text-red-500")}
showError={hasNodeError(node.num)}
showFavorite={node.isFavorite}
@ -271,19 +275,15 @@ export const MessagesPage = () => {
],
);
const pageTitleChatName = useMemo(() => {
if (isBroadcast && currentChannel) return getChannelName(currentChannel);
if (isDirect && otherNode) {
return otherNode.user?.longName ?? t("common_unknown_short");
}
return t("messages_title_default");
}, [isBroadcast, currentChannel, isDirect, otherNode, t]);
return (
<PageLayout
label={t("messages_title", {
chatName: pageTitleChatName,
})}
label={`Messages: ${
isBroadcast && currentChannel
? getChannelName(currentChannel)
: isDirect && otherNode
? (otherNode.user?.longName ?? t("unknown.longName"))
: t("emptyState.title")
}`}
rightBar={rightSidebar}
leftBar={leftSidebar}
actions={isDirect && otherNode
@ -297,8 +297,8 @@ export const MessagesPage = () => {
onClick() {
toast({
title: otherNode.user?.publicKey?.length
? t("toast_messages_pkiEncryption")
: t("toast_messages_pskEncryption"),
? t("toast.pkiEncryption")
: t("toast.pskEncryption"),
});
},
},
@ -319,7 +319,7 @@ export const MessagesPage = () => {
)
: (
<div className="p-4 text-center text-slate-400 italic">
{t("messages_sendMessagePrompt")}
{t("messagesPage.sendMessagePrompt")}
</div>
)}
</div>

40
src/pages/Nodes.tsx

@ -34,7 +34,7 @@ export interface DeleteNoteDialogProps {
}
const NodesPage = (): JSX.Element => {
const { t } = useTranslation();
const { t } = useTranslation("nodes");
const { getNodes, hardware, connection, hasNodeError } = useDevice();
const { nodeFilter, defaultFilterValues, isFilterDirty } = useFilterNode();
const [selectedNode, setSelectedNode] = useState<
@ -97,7 +97,7 @@ const NodesPage = (): JSX.Element => {
<div className="pl-2 pt-2 flex flex-row">
<div className="flex-1 mr-2">
<Input
placeholder={t("nodes_searchPlaceholder")}
placeholder={t("search.nodes")}
value={filterState.nodeName}
className="bg-transparent"
showClearButton={!!filterState.nodeName}
@ -131,37 +131,37 @@ const NodesPage = (): JSX.Element => {
headings={[
{ title: "", type: "blank", sortable: false },
{
title: t("nodes_table_headings_longName"),
title: t("nodesTable.headings.longName"),
type: "normal",
sortable: true,
},
{
title: t("nodes_table_headings_connection"),
title: t("nodesTable.headings.connection"),
type: "normal",
sortable: true,
},
{
title: t("nodes_table_headings_lastHeard"),
title: t("nodesTable.headings.lastHeard"),
type: "normal",
sortable: true,
},
{
title: t("nodes_table_headings_encryption"),
title: t("nodesTable.headings.encryption"),
type: "normal",
sortable: false,
},
{
title: t("node_detail_snr_label"),
title: t("unit.snr"),
type: "normal",
sortable: true,
},
{
title: t("nodes_table_headings_model"),
title: t("nodesTable.headings.model"),
type: "normal",
sortable: true,
},
{
title: t("nodes_table_headings_macAddress"),
title: t("nodesTable.headings.macAddress"),
type: "normal",
sortable: true,
},
@ -169,7 +169,7 @@ const NodesPage = (): JSX.Element => {
rows={filteredNodes.map((node) => [
<div key={node.num}>
<Avatar
text={node.user?.shortName ?? t("common_unknown_short")}
text={node.user?.shortName ?? t("unknown.shortName")}
showFavorite={node.isFavorite}
showError={hasNodeError(node.num)}
/>
@ -189,20 +189,20 @@ const NodesPage = (): JSX.Element => {
<Mono key="hops" className="w-16">
{node.hopsAway !== undefined
? node?.viaMqtt === false && node.hopsAway === 0
? t("nodes_table_connectionStatus_direct")
? t("nodesTable.connectionStatus.direct")
: `${node.hopsAway?.toString()} ${
node.hopsAway ?? 0 > 1
? t("nodes_table_connectionStatus_hops_other")
: t("nodes_table_connectionStatus_hops_one")
} ${t("nodes_table_connectionStatus_away")}`
: t("nodes_table_connectionStatus_unknown")}
? t("unit.hop.plural")
: t("unit.hops_one")
} ${t("nodesTable.connectionStatus.away")}`
: t("nodesTable.connectionStatus.unknown")}
{node?.viaMqtt === true
? t("nodes_table_connectionStatus_viaMqtt")
? t("nodesTable.connectionStatus.viaMqtt")
: ""}
</Mono>,
<Mono key="lastHeard">
{node.lastHeard === 0
? <p>{t("nodes_table_lastHeardStatus_never")}</p>
? <p>{t("nodesTable.lastHeardStatus.never")}</p>
: <TimeAgo timestamp={node.lastHeard * 1000} />}
</Mono>,
<Mono key="pki">
@ -212,13 +212,13 @@ const NodesPage = (): JSX.Element => {
</Mono>,
<Mono key="snr">
{node.snr}
{t("common_unit_dbm")}/
{t("unit.dbm")}/
{Math.min(
Math.max((node.snr + 10) * 5, 0),
100,
)}%/{/* Percentage */}
{(node.snr + 10) * 5}
{t("common_unit_raw")}
{t("unit.raw")}
</Mono>,
<Mono key="model">
{Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0]}
@ -227,7 +227,7 @@ const NodesPage = (): JSX.Element => {
{base16
.stringify(node.user?.macaddr ?? [])
.match(/.{1,2}/g)
?.join(":") ?? t("common_unknown_short")}
?.join(":") ?? t("unknown.shortName")}
</Mono>,
])}
/>

Loading…
Cancel
Save