From 82090b01fa9206d57a66ccd80bd1f5a5a409095a Mon Sep 17 00:00:00 2001 From: Sebastian M Date: Fri, 7 Feb 2025 23:36:48 +0100 Subject: [PATCH] Internationalization - Config list (#1940) * Added i18n for config list * Move cells and cells into own files * Fix linter erros * Fix playwright tests * Update the github workflow to prevent double execution * Translate missing waiting * Fix linter issue --- .github/workflows/playwright.yml | 2 - Base/Settings.cs | 87 +++++ MobiFlightConnector.csproj | 1 + UI/Dialogs/ConfigWizard.cs | 4 +- UI/MainForm.cs | 9 + UI/Panels/Settings/GeneralPanel.cs | 1 + frontend/.devcontainer/devcontainer.json | 2 +- frontend/package-lock.json | 9 + frontend/package.json | 1 + frontend/public/locales/de/translation.json | 44 +++ frontend/public/locales/en/translation.json | 44 ++- frontend/public/locales/es/translation.json | 44 +++ frontend/src/App.tsx | 87 +++-- .../config-item-table-columns.tsx | 348 +++++------------- .../config-item-table/config-item-table.tsx | 13 +- .../data-table-faceted-filter.tsx | 9 +- .../config-item-table/data-table-toolbar.tsx | 14 +- .../items/ConfigItemTableActionsCell.tsx | 79 ++++ .../items/ConfigItemTableActiveCell.tsx | 33 ++ .../items/ConfigItemTableActiveHeader.tsx | 8 + .../items/ConfigItemTableNameCell.tsx | 88 +++++ .../items/ConfigItemTableStatusCell.tsx | 78 ++++ .../tables/config-item-table/items/index.ts | 5 + frontend/src/i18n.ts | 23 +- frontend/src/lib/languageDetector.ts | 11 + frontend/src/types/messages.d.ts | 5 +- frontend/src/types/settings.d.ts | 24 ++ frontend/tests/ConfigListView.spec.ts | 2 +- 28 files changed, 739 insertions(+), 336 deletions(-) create mode 100644 Base/Settings.cs create mode 100644 frontend/public/locales/de/translation.json create mode 100644 frontend/public/locales/es/translation.json create mode 100644 frontend/src/components/tables/config-item-table/items/ConfigItemTableActionsCell.tsx create mode 100644 frontend/src/components/tables/config-item-table/items/ConfigItemTableActiveCell.tsx create mode 100644 frontend/src/components/tables/config-item-table/items/ConfigItemTableActiveHeader.tsx create mode 100644 frontend/src/components/tables/config-item-table/items/ConfigItemTableNameCell.tsx create mode 100644 frontend/src/components/tables/config-item-table/items/ConfigItemTableStatusCell.tsx create mode 100644 frontend/src/components/tables/config-item-table/items/index.ts create mode 100644 frontend/src/lib/languageDetector.ts create mode 100644 frontend/src/types/settings.d.ts diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index fa7e1c2d7..392caed8c 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -1,7 +1,5 @@ name: Playwright Tests on: - push: - branches: [ mf2025/* ] pull_request: branches: [ main, mf2025/main ] jobs: diff --git a/Base/Settings.cs b/Base/Settings.cs new file mode 100644 index 000000000..0a89aadd6 --- /dev/null +++ b/Base/Settings.cs @@ -0,0 +1,87 @@ +using System.Linq; + +namespace MobiFlight.Base +{ + public class Settings + { + public bool ArcazeSupportEnabled { get; set; } + public bool AutoRetrigger { get; set; } + public bool AutoRun { get; set; } + public bool AutoLoadLinkedConfig { get; set; } + public bool BetaUpdates { get; set; } + public bool CommunityFeedback { get; set; } + public bool EnableJoystickSupport { get; set; } + public bool EnableMidiSupport { get; set; } + public string ExcludedJoysticks { get; set; } + public string ExcludedMidiBoards { get; set; } + public bool FwAutoUpdateCheck { get; set; } + public bool HubHopAutoCheck { get; set; } + public string IgnoredComPortsList { get; set; } + public string Language { get; set; } + public bool LogEnabled { get; set; } + public bool LogJoystickAxis { get; set; } + public string LogLevel { get; set; } + public bool MinimizeOnAutoRun { get; set; } + public string ModuleSettings { get; set; } + public string[] RecentFiles { get; set; } + public int RecentFilesMaxCount { get; set; } + public int TestTimerInterval { get; set; } + + internal Settings() + { + } + + internal Settings(Properties.Settings settings) + { + ArcazeSupportEnabled = settings.ArcazeSupportEnabled; + AutoRetrigger = settings.AutoRetrigger; + AutoRun = settings.AutoRun; + AutoLoadLinkedConfig = settings.AutoLoadLinkedConfig; + BetaUpdates = settings.BetaUpdates; + CommunityFeedback = settings.CommunityFeedback; + EnableJoystickSupport = settings.EnableJoystickSupport; + EnableMidiSupport = settings.EnableMidiSupport; + ExcludedJoysticks = settings.ExcludedJoysticks; + ExcludedMidiBoards = settings.ExcludedMidiBoards; + FwAutoUpdateCheck = settings.FwAutoUpdateCheck; + HubHopAutoCheck = settings.HubHopAutoCheck; + IgnoredComPortsList = settings.IgnoredComPortsList; + Language = settings.Language; + LogEnabled = settings.LogEnabled; + LogJoystickAxis = settings.LogJoystickAxis; + LogLevel = settings.LogLevel; + MinimizeOnAutoRun = settings.MinimizeOnAutoRun; + ModuleSettings = settings.ModuleSettings; + // Skip: OfflineMode = settings.OfflineMode; + // Skip: PollInterval = settings.PollInterval; + RecentFiles = settings.RecentFiles.Cast().ToArray(); + RecentFilesMaxCount = settings.RecentFilesMaxCount; + TestTimerInterval = settings.TestTimerInterval; + // Properties.Settings.Default.AutoRetrigger = true; + // Properties.Settings.Default.AutoRun = true; + // Properties.Settings.Default.AutoLoadLinkedConfig = true; + // Properties.Settings.Default.BetaUpdates = true; + // Properties.Settings.Default.CommunityFeedback = true; + // Properties.Settings.Default.EnableJoystickSupport = true; + // Properties.Settings.Default.EnableMidiSupport = true; + // Properties.Settings.Default.ExcludedJoysticks + // Properties.Settings.Default.ExcludedMidiBoards + // Properties.Settings.Default.FwAutoUpdateCheck = true; + // Properties.Settings.Default.HubHopAutoCheck = true; + // Properties.Settings.Default.IgnoredComPortsList + // Properties.Settings.Default.Language = "en"; + // Properties.Settings.Default.LogEnabled = true; + // Properties.Settings.Default.LogJoystickAxis = false; + // Properties.Settings.Default.LogLevel = "Debug"; + // Properties.Settings.Default.MinimizeOnAutoRun = true; + // Properties.Settings.Default.ModuleSettings = true; + // Skip: Properties.Settings.Default.OfflineMode = false; + // Skip: Properties.Settings.Default.PollInterval = 100; + // Properties.Settings.Default.RecentFiles = new System.Collections.Specialized.StringCollection(); + // Properties.Settings.Default.RecentFilesMaxCount = 10; + // Skip: Properties.Settings.Default.TestTimerInterval = 1000; + } + + + } +} diff --git a/MobiFlightConnector.csproj b/MobiFlightConnector.csproj index f4920bdcf..dc305f3e3 100644 --- a/MobiFlightConnector.csproj +++ b/MobiFlightConnector.csproj @@ -256,6 +256,7 @@ + diff --git a/UI/Dialogs/ConfigWizard.cs b/UI/Dialogs/ConfigWizard.cs index 034ec5fbc..ab6707c88 100644 --- a/UI/Dialogs/ConfigWizard.cs +++ b/UI/Dialogs/ConfigWizard.cs @@ -122,7 +122,7 @@ protected void Init(ExecutionManager executionManager, OutputConfigItem cfg) testValuePanel1.TestModeStart += TestValuePanel_TestModeStart; testValuePanel1.TestModeStop += TestValuePanel_TestModeEnd; testValuePanel1.TestValueChanged += ModifierPanel1_ModifierChanged; - TestTimer.Interval = Settings.Default.TestTimerInterval; + TestTimer.Interval = Properties.Settings.Default.TestTimerInterval; TestTimer.Tick += TestTimer_Tick; modifierPanel1.ModifierChanged += ModifierPanel1_ModifierChanged; } @@ -135,7 +135,7 @@ private void ModifierPanel1_ModifierChanged(object sender, EventArgs e) private void TestTimer_Tick(object sender, EventArgs e) { - TestTimer.Interval = Settings.Default.TestTimerInterval; + TestTimer.Interval = Properties.Settings.Default.TestTimerInterval; var value = config.TestValue.Clone() as ConnectorValue; if (value == null) value = new ConnectorValue(); diff --git a/UI/MainForm.cs b/UI/MainForm.cs index b1d08b299..dac783abd 100644 --- a/UI/MainForm.cs +++ b/UI/MainForm.cs @@ -120,6 +120,12 @@ private void InitializeSettings() { UpgradeSettingsFromPreviousInstallation(); Properties.Settings.Default.SettingChanging += new System.Configuration.SettingChangingEventHandler(Default_SettingChanging); + + Properties.Settings.Default.SettingsSaving += (s, e) => + { + MessageExchange.Instance.Publish(new Settings(Properties.Settings.Default)); + }; + UpdateAutoLoadConfig(); RestoreAutoLoadConfig(); CurrentFilenameChanged += (s, e) => { UpdateAutoLoadMenu(); }; @@ -129,6 +135,9 @@ private void InitializeSettings() // there are no recent files which // could lead to a filename change UpdateAutoLoadMenu(); + + // Send the current settings to the UI + MessageExchange.Instance.Publish(new Settings(Properties.Settings.Default)); } public MainForm() diff --git a/UI/Panels/Settings/GeneralPanel.cs b/UI/Panels/Settings/GeneralPanel.cs index 935e8baf5..abf23ff39 100644 --- a/UI/Panels/Settings/GeneralPanel.cs +++ b/UI/Panels/Settings/GeneralPanel.cs @@ -25,6 +25,7 @@ protected void InitializeLanguageComboBox() languageOptions.Add(new ListItem() { Value = "", Label = "System Default" }); languageOptions.Add(new ListItem() { Value = "en-US", Label = "English" }); languageOptions.Add(new ListItem() { Value = "de-DE", Label = "Deutsch" }); + languageOptions.Add(new ListItem() { Value = "es-ES", Label = "Español" }); languageComboBox.DataSource = languageOptions; languageComboBox.DisplayMember = "Label"; diff --git a/frontend/.devcontainer/devcontainer.json b/frontend/.devcontainer/devcontainer.json index 8b909dd77..5de515911 100644 --- a/frontend/.devcontainer/devcontainer.json +++ b/frontend/.devcontainer/devcontainer.json @@ -28,4 +28,4 @@ // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. // "remoteUser": "root" -} +} \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f14c0f4f6..a5a05cc90 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -21,6 +21,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.0", + "i18next-browser-languagedetector": "^8.0.2", "i18next-http-backend": "^3.0.1", "lodash-es": "^4.17.21", "lucide-react": "^0.471.0", @@ -3643,6 +3644,14 @@ } } }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.2.tgz", + "integrity": "sha512-shBvPmnIyZeD2VU5jVGIOWP7u9qNG3Lj7mpaiPFpbJ3LVfHZJvVzKR4v1Cb91wAOFpNw442N+LGPzHOHsten2g==", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, "node_modules/i18next-http-backend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 32553613f..43dddfc41 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,6 +23,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.0.0", + "i18next-browser-languagedetector": "^8.0.2", "i18next-http-backend": "^3.0.1", "lodash-es": "^4.17.21", "lucide-react": "^0.471.0", diff --git a/frontend/public/locales/de/translation.json b/frontend/public/locales/de/translation.json new file mode 100644 index 000000000..d59761289 --- /dev/null +++ b/frontend/public/locales/de/translation.json @@ -0,0 +1,44 @@ +{ + "ConfigList": { + "Toolbar" : { + "Search" : { + "Placeholder": "Einträge filtern..." + }, + "Filter" : { + "Device": "Geräte", + "Type" : "Typen", + "Name" : "Namen", + "Selected" : "{{items}} ausgewählt", + "Clear" : "Filter zurücksetzen", + "NoResultsFound" : "Keine Ergebnisse gefunden." + }, + "Reset": "Filter zurücksetzen" + }, + "Header" : { + "Active": "Aktiv", + "Name": "Name / Beschreibung", + "Device": "Gerät", + "Component": "Komponente", + "Status": "Status", + "RawValue": "Rohwert", + "FinalValue": "Endwert", + "Actions": "Aktionen" + }, + "Cell" : { + "Waiting" : "Warten" + }, + "Table" : { + "NoResultsFound" : "Neue Konfiguration. Erstelle neue Einträge." + }, + "Actions": { + "OutputConfigItem" : { + "Add": "Neue Ausgabe-Konfiguration", + "DefaultName": "Neue Ausgabe-Konfiguration" + }, + "InputConfigItem" : { + "Add": "Neue Eingabe-konfiguration", + "DefaultName": "Neue Eingabe-Konfiguration" + } + } + } +} diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json index 0c59b52f9..f4a78b457 100644 --- a/frontend/public/locales/en/translation.json +++ b/frontend/public/locales/en/translation.json @@ -1,6 +1,44 @@ { - "interpolation": "Hello {{name}}!", - "app": { - "greeting": "Hello MobiFlight user! Test!" + "ConfigList": { + "Toolbar" : { + "Search" : { + "Placeholder": "Filter items..." + }, + "Filter" : { + "Device": "Devices", + "Type" : "Types", + "Name" : "Names", + "Selected" : "{{items}} selected", + "Clear" : "Clear filters", + "NoResultsFound" : "No results found." + }, + "Reset": "Reset filters" + }, + "Header" : { + "Active": "Active", + "Name": "Name / Description", + "Device": "Device", + "Component": "Component", + "Status": "Status", + "RawValue": "Raw Value", + "FinalValue": "Final Value", + "Actions": "Actions" + }, + "Cell" : { + "Waiting" : "Waiting" + }, + "Table" : { + "NoResultsFound" : "This is a new configuration. Please add some items." + }, + "Actions": { + "OutputConfigItem" : { + "Add": "Add Output Config", + "DefaultName": "New Output Config" + }, + "InputConfigItem" : { + "Add": "Add Input Config", + "DefaultName": "New Input Config" + } + } } } diff --git a/frontend/public/locales/es/translation.json b/frontend/public/locales/es/translation.json new file mode 100644 index 000000000..9119ba04e --- /dev/null +++ b/frontend/public/locales/es/translation.json @@ -0,0 +1,44 @@ +{ + "ConfigList": { + "Toolbar": { + "Search": { + "Placeholder": "Filtrar elementos..." + }, + "Filter": { + "Device": "Dispositivos", + "Type": "Tipos", + "Name": "Nombres", + "Selected": "{{items}} seleccionados", + "Clear": "Borrar filtros", + "NoResultsFound": "No se encontraron resultados." + }, + "Reset": "Restablecer filtros" + }, + "Header": { + "Active": "Activo", + "Name": "Nombre / Descripción", + "Device": "Dispositivo", + "Component": "Componente", + "Status": "Estado", + "RawValue": "Valor Original", + "FinalValue": "Valor Final", + "Actions": "Acciones" + }, + "Cell" : { + "Waiting" : "Esperando" + }, + "Table": { + "NoResultsFound": "Nueva configuratión. Por favor, añade algunos elementos." + }, + "Actions": { + "OutputConfigItem": { + "Add": "Nueva configuración de Output", + "DefaultName": "Nueva Configuración Output" + }, + "InputConfigItem": { + "Add": "Nueva configuración de Input", + "DefaultName": "Nueva Configuración Input" + } + } + } +} \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 63965111c..46e500e8b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,18 +1,22 @@ -import { Outlet, useNavigate, useOutlet, useSearchParams } from 'react-router' -import StartupProgress from './components/StartupProgress' -import { useEffect, useState } from 'react' -import { useAppMessage } from './lib/hooks/appMessage' -import { ConfigLoadedEvent, StatusBarUpdate } from './types' -import { useConfigStore } from './stores/configFileStore' +import { Outlet, useNavigate, useOutlet, useSearchParams } from "react-router" +import StartupProgress from "./components/StartupProgress" +import { useEffect, useState } from "react" +import { useAppMessage } from "./lib/hooks/appMessage" +import { ConfigLoadedEvent, StatusBarUpdate } from "./types" +import { useConfigStore } from "./stores/configFileStore" +import i18next from "i18next" +import Settings from "./types/settings" +import _ from "lodash" -function App() { +function App() { const [queryParameters] = useSearchParams() const navigate = useNavigate() - const { setItems } = useConfigStore(); + const { setItems } = useConfigStore() - const [startupProgress, setStartupProgress] = useState( - { Value: 0, Text: "Starting..." }, - ) + const [startupProgress, setStartupProgress] = useState({ + Value: 0, + Text: "Starting...", + }) useAppMessage("StatusBarUpdate", (message) => { setStartupProgress(message.payload as StatusBarUpdate) @@ -23,17 +27,28 @@ function App() { setItems((message.payload as ConfigLoadedEvent).ConfigItems) }) + useAppMessage("Settings", (message) => { + const settings = message.payload as Settings + console.log("Settings message received", settings) + + const language = settings.Language.split("-")[0] + if (!_.isEmpty(language)) + i18next.changeLanguage(settings.Language) + else + i18next.changeLanguage() + }) + // this allows to get beyond the startup screen // by setting the progress to 100 via url parameter useEffect(() => { - // convert string to number - const value = Number.parseInt(queryParameters.get("progress")?.toString() ?? "0") + // convert string to number + const value = Number.parseInt( + queryParameters.get("progress")?.toString() ?? "0", + ) if (value == 100) { console.log("Finished loading, navigating to config page") navigate("/config") - } - else - setStartupProgress({ Value: value, Text: "Loading..." }) + } else setStartupProgress({ Value: value, Text: "Loading..." }) }, [navigate, queryParameters]) useEffect(() => { @@ -47,30 +62,30 @@ function App() { return ( <> - { outlet ? ( -
- {/* */} -
- {/* */} -
- -
-
-
MobiFlight 2025
-
Version 1.0.0
+ {outlet ? ( +
+ {/* */} +
+ {/* */} +
+ +
+
+
MobiFlight 2025
+
Version 1.0.0
+
-
- {/* */} -
- ) : ( - ) - } +
+ ) : ( + + )} ) } diff --git a/frontend/src/components/tables/config-item-table/config-item-table-columns.tsx b/frontend/src/components/tables/config-item-table/config-item-table-columns.tsx index 0153b6671..8a1cff69c 100644 --- a/frontend/src/components/tables/config-item-table/config-item-table-columns.tsx +++ b/frontend/src/components/tables/config-item-table/config-item-table-columns.tsx @@ -1,78 +1,41 @@ import { Button } from "@/components/ui/button" -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, -} from "@/components/ui/dropdown-menu" -import { Switch } from "@/components/ui/switch" import { IConfigItem } from "@/types" -import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu" import { ColumnDef } from "@tanstack/react-table" import { - IconAlertSquareRounded, IconArrowsSort, IconBan, IconBuildingBroadcastTower, - IconCircleCheck, - IconDots, - IconEdit, - IconFlask, IconMathSymbols, - IconPlugConnectedX, - IconRouteOff, - IconX, } from "@tabler/icons-react" -import { publishOnMessageExchange } from "@/lib/hooks/appMessage" // import { Badge } from "@/components/ui/badge"; import DeviceIcon from "@/components/icons/DeviceIcon" import { DeviceElementType } from "@/types/deviceElements" -import { - ConfigItemStatusType, - IDeviceConfig, - IDictionary, -} from "@/types/config" -import { - CommandUpdateConfigItem, - CommandConfigContextMenu, -} from "@/types/commands" +import { IDeviceConfig } from "@/types/config" import { isEmpty } from "lodash" -import { useCallback, useEffect, useState } from "react" -import { Input } from "@/components/ui/input" +import { useTranslation } from "react-i18next" +import { + ConfigItemTableActionsCell, + ConfigItemTableActiveCell, + ConfigItemTableActiveHeader, + ConfigItemTableNameCell, + ConfigItemTableStatusCell, +} from "./items" export const columns: ColumnDef[] = [ { accessorKey: "Active", - header: () =>
Active
, - cell: ({ row }) => { - const { publish } = publishOnMessageExchange() - const item = row.original as IConfigItem - - return ( -
- { - item.Active = !item.Active - publish({ - key: "CommandUpdateConfigItem", - payload: { item: item }, - } as CommandUpdateConfigItem) - }} - /> -
- ) - }, + header: ConfigItemTableActiveHeader, + cell: ConfigItemTableActiveCell, }, { accessorKey: "Name", size: 1, header: ({ column }) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const { t } = useTranslation() return ( -
- Name / Description +
+ {t("ConfigList.Header.Name")}
) }, - cell: ({ row }) => { - const { publish } = publishOnMessageExchange() - // eslint-disable-next-line react-hooks/rules-of-hooks - const [isEditing, setIsEditing] = useState(false) - // eslint-disable-next-line react-hooks/rules-of-hooks - const [label, setLabel] = useState(row.getValue("Name") as string) - const realLabel = row.getValue("Name") as string - - const toggleEdit = () => { - setIsEditing(!isEditing) - } - - const moduleName = - (row.getValue("ModuleSerial") as string).split("/")[0] ?? "not set" - const deviceName = (row.getValue("Device") as IDeviceConfig)?.Name ?? "-" - - // eslint-disable-next-line react-hooks/rules-of-hooks - const saveChanges = useCallback(() => { - const item = row.original as IConfigItem - item.Name = label - console.log(item) - publish({ - key: "CommandUpdateConfigItem", - payload: { item: item }, - } as CommandUpdateConfigItem) - }, [label, row, publish]) - + cell: ConfigItemTableNameCell, + }, + { + accessorKey: "ModuleSerial", + header: () => { // eslint-disable-next-line react-hooks/rules-of-hooks - useEffect(() => { - setLabel(realLabel) - }, [realLabel]) - + const { t } = useTranslation() return ( -
- {!isEditing ? ( -
-
-

{label}

- -
-

- {moduleName} - {deviceName} -

-
- ) : ( -
- setLabel(e.target.value)} - /> - { - saveChanges() - toggleEdit() - }} - /> - { - setLabel(row.getValue("Name") as string) - toggleEdit() - }} - /> -
- )} +
+ {t("ConfigList.Header.Device")}
) }, - }, - { - accessorKey: "ModuleSerial", - header: () =>
Device
, cell: ({ row }) => { const label = (row.getValue("ModuleSerial") as string).split("/")[0] const serial = (row.getValue("ModuleSerial") as string).split("/")[1] return !isEmpty(label) ? ( -
+

{label}

{serial}

) : ( - + not set @@ -187,7 +80,15 @@ export const columns: ColumnDef[] = [ }, { accessorKey: "Device", - header: () =>
Component
, + header: () => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const { t } = useTranslation() + return ( +
+ {t("ConfigList.Header.Component")} +
+ ) + }, cell: ({ row }) => { const label = (row.getValue("Device") as IDeviceConfig)?.Name ?? "-" const type = (row.getValue("Device") as IDeviceConfig)?.Type ?? "-" @@ -198,7 +99,7 @@ export const columns: ColumnDef[] = [ /> ) return type != "-" ? ( -
+
{icon}

{label}

@@ -206,7 +107,7 @@ export const columns: ColumnDef[] = [
) : ( -
+
not set
@@ -219,10 +120,18 @@ export const columns: ColumnDef[] = [ { size: 80, accessorKey: "Type", - header: () =>
Component Type
, + header: () => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const { t } = useTranslation() + return ( +
+ Component Type{t("ConfigList.Header.Component")} +
+ ) + }, cell: ({ row }) => { const label = (row.getValue("Device") as IDeviceConfig)?.Type ?? "-" - return

{label}

+ return

{label}

}, filterFn: (row, _, value) => { return value.includes( @@ -248,83 +157,39 @@ export const columns: ColumnDef[] = [ { size: 100, accessorKey: "Status", - header: () =>
Status
, - cell: ({ row }) => { - const Status = row.getValue("Status") as IDictionary< - string, - ConfigItemStatusType - > - const Precondition = Status && !isEmpty(Status["Precondition"]) - const Source = Status && !isEmpty(Status["Source"]) - const Modifier = Status && !isEmpty(Status["Modifier"]) - const Device = Status && !isEmpty(Status["Device"]) - const Test = Status && !isEmpty(Status["Test"]) - const ConfigRef = Status && !isEmpty(Status["ConfigRef"]) - + header: () => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const { t } = useTranslation() return ( -
- - normal - - - normal - - - normal - - - normal - - - normal - - - normal - -
+
{t("ConfigList.Header.Status")}
) }, + cell: ConfigItemTableStatusCell, }, { size: 100, accessorKey: "RawValue", - header: () => ( -
Raw Value
- ), + header: () => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const { t } = useTranslation() + return ( +
+ {t("ConfigList.Header.RawValue")} +
+ ) + }, cell: ({ row }) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const { t } = useTranslation() const label = row.getValue("RawValue") as string return ( -
+
{!isEmpty(label) ? ( label ) : (
- waiting + {t("ConfigList.Cell.Waiting")}
)}
@@ -334,19 +199,27 @@ export const columns: ColumnDef[] = [ { size: 100, accessorKey: "Value", - header: () => ( -
Final Value
- ), + header: () => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const { t } = useTranslation() + return ( +
+ {t("ConfigList.Header.FinalValue")} +
+ ) + }, cell: ({ row }) => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const { t } = useTranslation() const label = row.getValue("Value") as string return ( -
+
{!isEmpty(label) ? ( label ) : (
- waiting + {t("ConfigList.Cell.Waiting")}
)}
@@ -355,70 +228,15 @@ export const columns: ColumnDef[] = [ }, { id: "actions", - header: () =>
Actions
, - cell: ({ row }) => { - const item = row.original - const { publish } = publishOnMessageExchange() - + header: () => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const { t } = useTranslation() return ( -
- - - - - - Actions - { - publish({ - key: "CommandConfigContextMenu", - payload: { action: "edit", item: item }, - } as CommandConfigContextMenu) - }} - > - Edit - - { - publish({ - key: "CommandConfigContextMenu", - payload: { action: "delete", item: item }, - } as CommandConfigContextMenu) - }} - > - Remove - - - { - publish({ - key: "CommandConfigContextMenu", - payload: { action: "duplicate", item: item }, - } as CommandConfigContextMenu) - }} - > - Duplicate - - {/* Copy - Paste */} - - { - publish({ - key: "CommandConfigContextMenu", - payload: { action: "test", item: item }, - } as CommandConfigContextMenu) - }} - > - Test - - - +
+ {t("ConfigList.Header.Actions")}
) }, + cell: ConfigItemTableActionsCell, }, ] diff --git a/frontend/src/components/tables/config-item-table/config-item-table.tsx b/frontend/src/components/tables/config-item-table/config-item-table.tsx index 7e5471784..031a04c36 100644 --- a/frontend/src/components/tables/config-item-table/config-item-table.tsx +++ b/frontend/src/components/tables/config-item-table/config-item-table.tsx @@ -27,6 +27,7 @@ import { Button } from "@/components/ui/button" import { publishOnMessageExchange, useAppMessage } from "@/lib/hooks/appMessage" import { CommandAddConfigItem } from "@/types/commands" import { useConfigStore } from "@/stores/configFileStore" +import { useTranslation } from "react-i18next" interface DataTableProps { columns: ColumnDef[] @@ -90,6 +91,8 @@ export function ConfigItemTable({ } }) + const { t } = useTranslation() + return (
@@ -143,7 +146,7 @@ export function ConfigItemTable({ colSpan={columns.length} className="h-24 text-center" > - No results. + { t("ConfigList.Table.NoResultsFound") } )} @@ -157,11 +160,11 @@ export function ConfigItemTable({ onClick={() => publish({ key: "CommandAddConfigItem", - payload: { name: "New Output Config", type: "OutputConfig" }, + payload: { name: t("ConfigList.Actions.OutputConfigItem.DefaultName"), type: "OutputConfig" }, } as CommandAddConfigItem) } > - Add Output Config + { t("ConfigList.Actions.OutputConfigItem.Add") }
diff --git a/frontend/src/components/tables/config-item-table/data-table-faceted-filter.tsx b/frontend/src/components/tables/config-item-table/data-table-faceted-filter.tsx index da3b70742..c73792e9c 100644 --- a/frontend/src/components/tables/config-item-table/data-table-faceted-filter.tsx +++ b/frontend/src/components/tables/config-item-table/data-table-faceted-filter.tsx @@ -20,6 +20,7 @@ import { PopoverTrigger, } from "@/components/ui/popover" import { Separator } from "@/components/ui/separator" +import { useTranslation } from "react-i18next" interface DataTableFacetedFilterProps { column?: Column @@ -39,6 +40,8 @@ export function DataTableFacetedFilter({ const facets = column?.getFacetedUniqueValues() const selectedValues = new Set(column?.getFilterValue() as string[]) + const { t } = useTranslation() + return ( @@ -60,7 +63,7 @@ export function DataTableFacetedFilter({ variant="secondary" className="rounded-sm px-1 font-normal" > - {selectedValues.size} selected + {t("ConfigList.Toolbar.Filter.Selected", { items: selectedValues.size})} ) : ( options @@ -84,7 +87,7 @@ export function DataTableFacetedFilter({ - No results found. + {t("ConfigList.Toolbar.Filter.NoResultsFound")} {options.map((option) => { const isSelected = selectedValues.has(option.value) @@ -134,7 +137,7 @@ export function DataTableFacetedFilter({ onSelect={() => column?.setFilterValue(undefined)} className="justify-center text-center" > - Clear filters + {t("ConfigList.Toolbar.Filter.Clear")} diff --git a/frontend/src/components/tables/config-item-table/data-table-toolbar.tsx b/frontend/src/components/tables/config-item-table/data-table-toolbar.tsx index a04caa9d0..9e25ace97 100644 --- a/frontend/src/components/tables/config-item-table/data-table-toolbar.tsx +++ b/frontend/src/components/tables/config-item-table/data-table-toolbar.tsx @@ -8,6 +8,7 @@ import { Input } from "@/components/ui/input" import { DataTableFacetedFilter } from "./data-table-faceted-filter" import { IConfigItem } from "@/types" import { isEmpty } from "lodash-es" +import { useTranslation } from "react-i18next" interface DataTableToolbarProps { table: Table @@ -42,12 +43,14 @@ export function DataTableToolbar({ }), ) + const { t } = useTranslation() + return (
table.getColumn("Name")?.setFilterValue(event.target.value) @@ -57,21 +60,21 @@ export function DataTableToolbar({ {table.getColumn("ModuleSerial") && ( )} {table.getColumn("Type") && ( )} {table.getColumn("Device") && ( )} @@ -81,12 +84,11 @@ export function DataTableToolbar({ onClick={() => table.resetColumnFilters()} className="h-8 px-2 lg:px-3" > - Reset + {t("ConfigList.Toolbar.Reset")} )}
- {/* */}
) } diff --git a/frontend/src/components/tables/config-item-table/items/ConfigItemTableActionsCell.tsx b/frontend/src/components/tables/config-item-table/items/ConfigItemTableActionsCell.tsx new file mode 100644 index 000000000..e0cf1c06b --- /dev/null +++ b/frontend/src/components/tables/config-item-table/items/ConfigItemTableActionsCell.tsx @@ -0,0 +1,79 @@ +import { Button } from '@/components/ui/button' +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' +import { publishOnMessageExchange } from '@/lib/hooks/appMessage' +import { CommandConfigContextMenu } from '@/types/commands' +import { IConfigItem } from '@/types/config' +import { IconDots } from '@tabler/icons-react' + +import { Row } from "@tanstack/react-table" + +interface ConfigItemTableActionsCellProps { + row: Row +} + +const ConfigItemTableActionsCell = ({ row } : ConfigItemTableActionsCellProps) => { + const item = row.original + const { publish } = publishOnMessageExchange() + + return ( +
+ + + + + + Actions + { + publish({ + key: "CommandConfigContextMenu", + payload: { action: "edit", item: item }, + } as CommandConfigContextMenu) + }} + > + Edit + + { + publish({ + key: "CommandConfigContextMenu", + payload: { action: "delete", item: item }, + } as CommandConfigContextMenu) + }} + > + Remove + + + { + publish({ + key: "CommandConfigContextMenu", + payload: { action: "duplicate", item: item }, + } as CommandConfigContextMenu) + }} + > + Duplicate + + {/* Copy + Paste */} + + { + publish({ + key: "CommandConfigContextMenu", + payload: { action: "test", item: item }, + } as CommandConfigContextMenu) + }} + > + Test + + + +
+ ) +} + +export default ConfigItemTableActionsCell \ No newline at end of file diff --git a/frontend/src/components/tables/config-item-table/items/ConfigItemTableActiveCell.tsx b/frontend/src/components/tables/config-item-table/items/ConfigItemTableActiveCell.tsx new file mode 100644 index 000000000..2b3cfa7a9 --- /dev/null +++ b/frontend/src/components/tables/config-item-table/items/ConfigItemTableActiveCell.tsx @@ -0,0 +1,33 @@ +import { Switch } from '@/components/ui/switch' +import { publishOnMessageExchange } from '@/lib/hooks/appMessage' +import { CommandUpdateConfigItem } from '@/types/commands' +import { IConfigItem } from '@/types/config' + +import { Row } from "@tanstack/react-table" + +interface ConfigItemTableActiveCellProps { + row: Row +} + +const ConfigItemTableActiveCell = ({ row } : ConfigItemTableActiveCellProps) => { + const { publish } = publishOnMessageExchange() + const item = row.original as IConfigItem + + return ( +
+ { + item.Active = !item.Active + publish({ + key: "CommandUpdateConfigItem", + payload: { item: item }, + } as CommandUpdateConfigItem) + }} + /> +
+ ) +} + +export default ConfigItemTableActiveCell \ No newline at end of file diff --git a/frontend/src/components/tables/config-item-table/items/ConfigItemTableActiveHeader.tsx b/frontend/src/components/tables/config-item-table/items/ConfigItemTableActiveHeader.tsx new file mode 100644 index 000000000..d84cb0a03 --- /dev/null +++ b/frontend/src/components/tables/config-item-table/items/ConfigItemTableActiveHeader.tsx @@ -0,0 +1,8 @@ +import { useTranslation } from "react-i18next" + +const ConfigItemTableActiveHeader = () => { + const { t } = useTranslation() + return (
{t("ConfigList.Header.Active")}
) +} + +export default ConfigItemTableActiveHeader diff --git a/frontend/src/components/tables/config-item-table/items/ConfigItemTableNameCell.tsx b/frontend/src/components/tables/config-item-table/items/ConfigItemTableNameCell.tsx new file mode 100644 index 000000000..46b81560b --- /dev/null +++ b/frontend/src/components/tables/config-item-table/items/ConfigItemTableNameCell.tsx @@ -0,0 +1,88 @@ +import { Input } from "@/components/ui/input" +import { publishOnMessageExchange } from "@/lib/hooks/appMessage" +import { CommandUpdateConfigItem } from "@/types/commands" +import { IConfigItem, IDeviceConfig } from "@/types/config" +import { IconCircleCheck, IconEdit, IconX } from "@tabler/icons-react" +import { Row } from "@tanstack/react-table" +import { useCallback, useEffect, useState } from "react" + +interface ConfigItemTableNameCellProps { + row: Row +} +const ConfigItemTableNameCell = ({ row } : ConfigItemTableNameCellProps) => { + const { publish } = publishOnMessageExchange() + const [isEditing, setIsEditing] = useState(false) + const [label, setLabel] = useState(row.getValue("Name") as string) + const realLabel = row.getValue("Name") as string + + const toggleEdit = () => { + setIsEditing(!isEditing) + } + + const moduleName = + (row.getValue("ModuleSerial") as string).split("/")[0] ?? "not set" + const deviceName = (row.getValue("Device") as IDeviceConfig)?.Name ?? "-" + + const saveChanges = useCallback(() => { + const item = row.original as IConfigItem + item.Name = label + console.log(item) + publish({ + key: "CommandUpdateConfigItem", + payload: { item: item }, + } as CommandUpdateConfigItem) + }, [label, row, publish]) + + useEffect(() => { + setLabel(realLabel) + }, [realLabel]) + + return ( +
+ {!isEditing ? ( +
+
+

{label}

+ +
+

+ {moduleName} - {deviceName} +

+
+ ) : ( +
+ setLabel(e.target.value)} + /> + { + saveChanges() + toggleEdit() + }} + /> + { + setLabel(row.getValue("Name") as string) + toggleEdit() + }} + /> +
+ )} +
+ ) +} + +export default ConfigItemTableNameCell \ No newline at end of file diff --git a/frontend/src/components/tables/config-item-table/items/ConfigItemTableStatusCell.tsx b/frontend/src/components/tables/config-item-table/items/ConfigItemTableStatusCell.tsx new file mode 100644 index 000000000..7e0257e34 --- /dev/null +++ b/frontend/src/components/tables/config-item-table/items/ConfigItemTableStatusCell.tsx @@ -0,0 +1,78 @@ +import { IConfigItem } from "@/types" +import { IDictionary, ConfigItemStatusType } from "@/types/config" +import { + IconAlertSquareRounded, + IconBuildingBroadcastTower, + IconPlugConnectedX, + IconMathSymbols, + IconFlask, + IconRouteOff, +} from "@tabler/icons-react" +import { Row } from "@tanstack/react-table" +import { isEmpty } from "lodash-es" + +interface ConfigItemTableStatusCellProps { + row: Row +} + +const ConfigItemTableStatusCell = ({ row }: ConfigItemTableStatusCellProps) => { + const Status = row.getValue("Status") as IDictionary< + string, + ConfigItemStatusType + > + const Precondition = Status && !isEmpty(Status["Precondition"]) + const Source = Status && !isEmpty(Status["Source"]) + const Modifier = Status && !isEmpty(Status["Modifier"]) + const Device = Status && !isEmpty(Status["Device"]) + const Test = Status && !isEmpty(Status["Test"]) + const ConfigRef = Status && !isEmpty(Status["ConfigRef"]) + + return ( +
+ + normal + + + normal + + + normal + + + normal + + + normal + + + normal + +
+ ) +} + +export default ConfigItemTableStatusCell diff --git a/frontend/src/components/tables/config-item-table/items/index.ts b/frontend/src/components/tables/config-item-table/items/index.ts new file mode 100644 index 000000000..f834c3dce --- /dev/null +++ b/frontend/src/components/tables/config-item-table/items/index.ts @@ -0,0 +1,5 @@ +export { default as ConfigItemTableActionsCell } from "./ConfigItemTableActionsCell" +export { default as ConfigItemTableActiveCell } from "./ConfigItemTableActiveCell" +export { default as ConfigItemTableActiveHeader } from "./ConfigItemTableActiveHeader" +export { default as ConfigItemTableNameCell } from "./ConfigItemTableNameCell" +export { default as ConfigItemTableStatusCell } from "./ConfigItemTableStatusCell" \ No newline at end of file diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index 3f1ee13c4..79b9b0b6a 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -1,12 +1,13 @@ -import i18n from 'i18next'; -import Backend from 'i18next-http-backend'; -import { initReactI18next } from 'react-i18next'; +import i18n from "i18next" +import Backend from "i18next-http-backend" +import LanguageDetector from "@/lib/languageDetector" +import { initReactI18next } from "react-i18next" -export default -i18n -.use(Backend) -.use(initReactI18next) -.init({ - fallbackLng: 'en', - debug: true, -}); +export default i18n + .use(LanguageDetector) + .use(Backend) + .use(initReactI18next) + .init({ + fallbackLng: "en", + debug: true, + }) diff --git a/frontend/src/lib/languageDetector.ts b/frontend/src/lib/languageDetector.ts new file mode 100644 index 000000000..8e05b73d0 --- /dev/null +++ b/frontend/src/lib/languageDetector.ts @@ -0,0 +1,11 @@ +import { LanguageDetectorModule } from "i18next" +const LanguageDetector: LanguageDetectorModule = { + type: "languageDetector", + detect: () => { + /* return detected language */ + const userLanguage = navigator.language || "en" + return userLanguage + }, +} + +export default LanguageDetector diff --git a/frontend/src/types/messages.d.ts b/frontend/src/types/messages.d.ts index bd19fbfae..c0b62af96 100644 --- a/frontend/src/types/messages.d.ts +++ b/frontend/src/types/messages.d.ts @@ -1,7 +1,10 @@ +import { Settings } from "http2" + export type AppMessageKey = | "StatusBarUpdate" | "ConfigFile" | "ConfigValueUpdate" + | "Settings" export type AppMessagePayload = | StatusBarUpdate @@ -12,7 +15,7 @@ export type AppMessagePayload = // when receiving messages from the backend export type AppMessage = { key: AppMessageKey - payload: AppMessagePayload + payload: AppMessagePayload | Settings } // ConfigLoadedEvent diff --git a/frontend/src/types/settings.d.ts b/frontend/src/types/settings.d.ts new file mode 100644 index 000000000..e5bbb0a6e --- /dev/null +++ b/frontend/src/types/settings.d.ts @@ -0,0 +1,24 @@ +export default interface Settings { + ArcazeSupportEnabled: boolean + AutoRetrigger: boolean + AutoRun: boolean + AutoLoadLinkedConfig: boolean + BetaUpdates: boolean + CommunityFeedback: boolean + EnableJoystickSupport: boolean + EnableMidiSupport: boolean + ExcludedJoysticks: string[] + ExcludedMidiBoards: string[] + FwAutoUpdateCheck: boolean + HubHopAutoCheck: boolean + IgnoredComPortsList: string + Language: string + LogEnabled: boolean + LogJoystickAxis: boolean + LogLevel: string + MinimizeOnAutoRun: boolean + ModuleSettings: string + RecentFiles: string[] + RecentFilesMaxCount: number + TestTimerInterval: number +} \ No newline at end of file diff --git a/frontend/tests/ConfigListView.spec.ts b/frontend/tests/ConfigListView.spec.ts index 28e532a18..f842b8cf3 100644 --- a/frontend/tests/ConfigListView.spec.ts +++ b/frontend/tests/ConfigListView.spec.ts @@ -3,7 +3,7 @@ import { test, expect } from './fixtures'; test('Confirm empty list view', async ({ configListPage, page }) => { await configListPage.gotoPage() await configListPage.initWithEmptyData() - await expect (page.getByRole('cell', { name: 'No results.' })).toBeVisible() + await expect (page.getByRole('cell', { name: 'This is a new configuration. Please add some items.' })).toBeVisible() await expect (page.getByRole('button', { name: 'Add Output Config' })).toBeVisible() await expect (page.getByRole('button', { name: 'Add Input Config' })).toBeVisible() await expect (page.getByRole('textbox', { name: 'Filter items...' })).toBeVisible()