Compare commits

..

2 Commits

Author SHA1 Message Date
Marc Brooks a3aa086e3c
Merge c6d4fffca4 into 74e64f69a7 2025-10-17 05:04:04 +00:00
Marc Brooks c6d4fffca4
Add inlang/paraglide-js localization
Remove the temporary directory after extracting buildkit
Localize the extension popovers.
Update package and fix tsconfig.json
Expand development directory guide
Move messages under localization
Popovers and sidebar
Update Chinese translations
Accidentally lost the changes that @ym provided, brought them back
File formatting pass
Localized all components, hooks, providers, hooks
Localize all pages except Settings
Bump packages
Settings Access page
Settings local auth page
Fix ref lint warning
Settings Advanced page
Fix UI lint warnings there were a bunch of ref and useEffect violations.
Settings appearance page
Settings general pages
Settings hardware page
Settings keyboard page
Settings macros pages
Settings mouse page
Settings page
Settings video page
Settings network page
Fix compilation issues
Ran machine translate
Use getLocale for date, relative time, and money formatting
Fix eslint
Delete unused messages
Added setting to choose locale
Merged in dev hotfix
Fix update status rendering
2025-10-17 00:03:49 -05:00
35 changed files with 216 additions and 163 deletions

View File

@ -1,23 +1,19 @@
<div align="center">
<img alt="JetKVM logo" src="https://jetkvm.com/logo-blue.png" height="28">
# JetKVM Development Guide
### Development Guide
<div align="center" width="100%">
<img src="https://jetkvm.com/logo-blue.png" align="center" height="28px">
[Discord](https://jetkvm.com/discord) | [Website](https://jetkvm.com) | [Issues](https://github.com/jetkvm/cloud-api/issues) | [Docs](https://jetkvm.com/docs)
[![Twitter](https://img.shields.io/twitter/url/https/twitter.com/jetkvm.svg?style=social&label=Follow%20%40JetKVM)](https://twitter.com/jetkvm)
[![Go Report Card](https://goreportcard.com/badge/github.com/jetkvm/kvm)](https://goreportcard.com/report/github.com/jetkvm/kvm)
</div>
# JetKVM Development Guide
Welcome to JetKVM development! This guide will help you get started quickly, whether you're fixing bugs, adding features, or just exploring the codebase.
## Get Started
### Prerequisites
- **A JetKVM device** (for full development)
- **[Go 1.24.4+](https://go.dev/doc/install)** and **[Node.js 22.15.0](https://nodejs.org/en/download/)**
- **[Git](https://git-scm.com/downloads)** for version control
@ -28,6 +24,7 @@ Welcome to JetKVM development! This guide will help you get started quickly, whe
**Recommended:** Development is best done on **Linux** or **macOS**.
If you're using Windows, we strongly recommend using **WSL (Windows Subsystem for Linux)** for the best development experience:
- [Install WSL on Windows](https://docs.microsoft.com/en-us/windows/wsl/install)
- [WSL Setup Guide](https://docs.microsoft.com/en-us/windows/wsl/setup/environment)
@ -36,12 +33,14 @@ This ensures compatibility with shell scripts and build tools used in the projec
### Project Setup
1. **Clone the repository:**
```bash
git clone https://github.com/jetkvm/kvm.git
cd kvm
```
2. **Check your tools:**
```bash
go version && node --version
```
@ -49,6 +48,7 @@ This ensures compatibility with shell scripts and build tools used in the projec
3. **Find your JetKVM IP address** (check your router or device screen)
4. **Deploy and test:**
```bash
./dev_deploy.sh -r 192.168.1.100 # Replace with your device IP
```
@ -95,7 +95,7 @@ tail -f /var/log/jetkvm.log
## Project Layout
```
```plaintext
/kvm/
├── main.go # App entry point
├── config.go # Settings & configuration
@ -130,7 +130,7 @@ tail -f /var/log/jetkvm.log
├── components/ # UI components
├── hooks/ # Hooks (stores, RPC handling, virtual devices)
├── keyboardLayouts/ # Keyboard layout definitions
├── paraglide/ # (localization compiled messages output)
├── paraglide/ # (localization compiled messages output)
├── providers/ # Feature flags
└── routes/ # Pages (login, settings, etc.)
```
@ -148,7 +148,7 @@ tail -f /var/log/jetkvm.log
### Full Development (Recommended)
*Best for: Complete feature development*
#### _Best for: Complete feature development_
```bash
# Deploy everything to your JetKVM device
@ -157,7 +157,7 @@ tail -f /var/log/jetkvm.log
### Frontend Only
*Best for: UI changes without device*
#### _Best for: UI changes without device_
```bash
cd ui
@ -171,7 +171,7 @@ Please click the `Build` button in EEZ Studio then run `./dev_deploy.sh -r <YOUR
### Quick Backend Changes
*Best for: API or backend logic changes*
#### _Best for: API or backend logic changes_
```bash
# Skip frontend build for faster deployment
@ -276,6 +276,7 @@ npm install
### "Device UI Fails to Build"
If while trying to build you run into an error message similar to :
```plaintext
In file included from /workspaces/kvm/internal/native/cgo/ctrl.c:15:
/workspaces/kvm/internal/native/cgo/ui_index.h:4:10: fatal error: ui/ui.h: No such file or directory
@ -283,17 +284,21 @@ In file included from /workspaces/kvm/internal/native/cgo/ctrl.c:15:
^~~~~~~~~
compilation terminated.
```
This means that your system didn't create the directory-link to from _./internal/native/cgo/ui_ to ./internal/native/eez/src/ui when the repository was checked out. You can verify this is the case if _./internal/native/cgo/ui_ appears as a plain text file with only the textual contents:
```plaintext
../eez/src/ui
```
If this happens to you need to [enable git creation of symbolic links](https://stackoverflow.com/a/59761201/2076) either globally or for the KVM repository:
```bash
# Globally enable git to create symlinks
git config --global core.symlinks true
git restore internal/native/cgo/ui
```
```bash
# Enable git to create symlinks only in this project
git config core.symlinks true
@ -301,13 +306,15 @@ If this happens to you need to [enable git creation of symbolic links](https://s
```
Or if you want to manually create the symlink use:
```bash
# linux
cd internal/native/cgo
rm ui
ln -s ../eez/src/ui ui
```
```dos
```batch
rem Windows
cd internal/native/cgo
del ui
@ -330,6 +337,7 @@ Or if you want to manually create the symlink use:
- **Go:** Follow standard Go conventions
- **TypeScript:** Use TypeScript for type safety
- **React:** Keep components small and reusable
- **Localization:** Ensure all user-facing strings in the frontend are [localized](#localization)
### Environment Variables
@ -362,11 +370,12 @@ export JETKVM_PROXY_URL="ws://<IP>"
4. Test thoroughly
5. Submit a pull request
### Before submitting:
### Before submitting
- [ ] Code works on device
- [ ] Tests pass
- [ ] Code follows style guidelines
- [ ] Frontend user-facing strings [localized](#localization)
- [ ] Documentation updated (if needed)
---
@ -422,7 +431,6 @@ The application uses a JSON configuration file stored at `/userdata/kvm_config.j
3. **Add migration logic if needed for existing installations**
### LVGL Build
We modified the LVGL code a little bit to remove unused fonts and examples.
@ -433,6 +441,79 @@ git diff --cached --diff-filter=d > ../internal/native/cgo/lvgl-minify.patch &&
git diff --name-only --diff-filter=D --cached > ../internal/native/cgo/lvgl-minify.del
```
### Localization
The browser/client frontend uses the [paraglide-js](https://inlang.com/m/gerre34r/library-inlang-paraglideJs) plug-in from the [inlang.com](https://inlang.com/) project to allow compile-time validated localization of all user-facing UI strings in the browser/client UI. This includes `title`, `text`, `name`, `description`, `placeholder`, `label`, `aria-label`, _message attributes_ (such as `confirmText`, `unit`, `badge`, `tag`, or `flag`), HTML _element text_ (such as `<h?>`, `<span>`, or `<p>` elements), _notifications messages_, and option _label_ strings, etc.
We **do not** translate the console log messages, CSS class names, theme names, nor the various _value_ strings (e.g. for value/label pair options), nor URL routes.
The localizations are stored in _.json_ files in the `ui/localizations/messages` directory, with one language-per-file using the [ISO 3166-1 alpha-2 country code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) (e.g. en for English, de for German, etc.)
#### m-function-matcher
The translations are extracted into language files (e.g. _en.json_ for English) and then paraglide-js compiles them into helpers for use with the [m-function-matcher](https://inlang.com/m/632iow21/plugin-inlang-mFunctionMatcher). An example:
```tsx
<SettingsPageHeader
title={m.extensions_atx_power_control()}
description={m.extensions_atx_power_control_description()}
/>
```
#### shakespere plug-in
If you enable the [Sherlock](https://inlang.com/m/r7kp499g/app-inlang-ideExtension) plug-in, the localized text "tooltip" is shown in the VSCode editor after any localized text in the language you've selected for preview. In this image, it's the blue text at the end of the line :
![Showing the translation preview](https://github.com/user-attachments/assets/f6d6dae6-919f-4319-b7bf-500cb1fd458d)
#### Process
##### Localizing a UI
1. Locate a string that is visible to the end user on the client/browser
2. Assign that string a "key" that reflects the logical meaning of the string in snake-case (look at existing localizations for examples), for example if there's a string `This is a test` on the _thing edit page_ it would be "thing_edit_this_is_a_test"
```json
"thing_edit_this_is_a_test": "This is a test",
```
3. Add the key and string to the _en.json_ like this:
- **Note** if the string has replacement parameters (line a user-entered name), the syntax for the localized string has `{ }` around the replacement token (e.g. _This is your name: {name}_). An complex example:
```react
{m.mount_button_showing_results({
from: indexOfFirstFile + 1,
to: Math.min(indexOfLastFile, onStorageFiles.length),
total: onStorageFiles.length
})}
```
4. Save the _en.json_ file and execute `npm run i18n` to resort the language files, validate the translations, and create the m-functions
5. Edit the _.tsx_ file and replace the string with the new m-function which will be the key-string you chose in snake-case. For example `This is a test` in _thing edit page_ turns into `m.thing_edit_this_is_a_test()`
- **Note** if the string has a replacement token, supply that to the m-function, for example `m.profile_your_name({ name: edit.value })`
6. When all your strings are extracted, run `npm run i18n:machine-translate` to get a first-stab at the translations for the other supported languages.
### Adding a new language
1. Get the [ISO 3166-1 alpha-2 country code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) (for example AT for Austria)
2. Create a new file in the _ui/localization/messages_ directory (example _at.json_)
3. Add the new country code to the _ui/localizations/settings.json_ file in both the `"locales"` and the `"languageTags"` section (inlang and Sherlock aren't exactly current to each other, so we need it in both places).
4. That file also declares the baseLocale/sourceLanguageTag which is `"en"` because this project started out in English. Do NOT change that.
5. Run `npm run i18n:machine-translate` to do an initial pass at localizing all existing messages to the new language.
- **Note** you will get an error _DB has been closed_, ignore that message, we're not using a database.
- **Note** you likely will get errors while running this command due to rate limits and such (it uses anonymous Google Translate). Just keep running the command over and over... it'll translate a bunch each time until it says _Machine translate complete_
### Other notes
- Run `npm run i18n:validate` to ensure that language files and settings are well-formed.
- Run `npm run i18n:find-excess` to look for extra keys in other language files that have been deleted from the master-list in _en.json_.
- Run `npm run i18n:find-dupes` to look for multiple keys in _en.json_ that have the same translated value (this is normal)
- Run `npm run i18n:find-unused` to look for keys in _en.json_ that are not referenced in the UI anywhere.
- **Note** there are a few that are not currently used, only concern yourself with ones you obsoleted.
- Run `npm run i18n:audit` to do all the above checks.
- Using [inlang CLI](https://inlang.com/m/2qj2w8pu/app-inlang-cli) to support the npm commands.
- You can install the [Sherlock VS Code extension](https://marketplace.visualstudio.com/items?itemName=inlang.vs-code-extension) in your devcontainer.
---

View File

@ -202,7 +202,7 @@
"dc_power_control_voltage": "Spænding",
"dc_power_control_voltage_unit": "V",
"delete": "Slet",
"deregister_button": "Afregistrering fra Cloud",
"deregister_from_cloud": "Afregistrering fra Cloud",
"deregister_cloud_devices": "Cloud-enheder",
"deregister_description": "Dette vil fjerne enheden fra din cloud-konto og tilbagekalde fjernadgang til den. Bemærk venligst, at lokal adgang stadig vil være mulig.",
"deregister_error": "Der opstod en fejl {status} under afregistreringen af din enhed. Prøv igen.",
@ -582,7 +582,6 @@
"network_dhcp_client_description": "Konfigurer hvilken DHCP-klient der skal bruges",
"network_dhcp_client_jetkvm": "JetKVM Intern",
"network_dhcp_client_title": "DHCP-klient",
"network_dhcp_lease_renew": "Forny DHCP-lease",
"network_dhcp_lease_renew_confirm": "Forny lejekontrakt",
"network_dhcp_lease_renew_confirm_description": "Dette vil anmode om en ny IP-adresse fra din DHCP-server. Din enhed kan midlertidigt miste netværksforbindelsen under denne proces.",
"network_dhcp_lease_renew_confirm_new_a": "Hvis du modtager en ny IP-adresse",
@ -700,7 +699,7 @@
"register_device_name_label": "Enhedsnavn",
"register_device_name_placeholder": "Plex-medieserver",
"register_device_no_name": "Angiv venligst et navn",
"rename_device_button": "Omdøb enhed",
"rename_device": "Omdøb enhed",
"rename_device_description": "Navngiv din enhed korrekt, så den nemt kan identificeres.",
"rename_device_error": "Der opstod en fejl {error} under omdøbning af din enhed.",
"rename_device_headline": "Omdøb {name}",

View File

@ -202,7 +202,7 @@
"dc_power_control_voltage": "Stromspannung",
"dc_power_control_voltage_unit": "V",
"delete": "Löschen",
"deregister_button": "Abmelden von der Cloud",
"deregister_from_cloud": "Abmelden von der Cloud",
"deregister_cloud_devices": "Cloud-Geräte",
"deregister_description": "Dadurch wird das Gerät aus Ihrem Cloud-Konto entfernt und der Fernzugriff darauf widerrufen. Bitte beachten Sie, dass der lokale Zugriff weiterhin möglich ist.",
"deregister_error": "Beim Abmelden Ihres Geräts ist ein Fehler aufgetreten {status} . Bitte versuchen Sie es erneut.",
@ -582,7 +582,6 @@
"network_dhcp_client_description": "Konfigurieren Sie, welcher DHCP-Client verwendet werden soll",
"network_dhcp_client_jetkvm": "JetKVM intern",
"network_dhcp_client_title": "DHCP-Client",
"network_dhcp_lease_renew": "DHCP-Lease erneuern",
"network_dhcp_lease_renew_confirm": "Mietvertrag verlängern",
"network_dhcp_lease_renew_confirm_description": "Dadurch wird eine neue IP-Adresse von Ihrem DHCP-Server angefordert. Während dieses Vorgangs kann die Netzwerkverbindung Ihres Geräts vorübergehend unterbrochen werden.",
"network_dhcp_lease_renew_confirm_new_a": "Wenn Sie eine neue IP-Adresse erhalten",
@ -700,7 +699,7 @@
"register_device_name_label": "Gerätename",
"register_device_name_placeholder": "Plex Media Server",
"register_device_no_name": "Bitte geben Sie einen Namen an",
"rename_device_button": "Gerät umbenennen",
"rename_device": "Gerät umbenennen",
"rename_device_description": "Geben Sie Ihrem Gerät einen passenden Namen, damit es leicht identifiziert werden kann.",
"rename_device_error": "Beim Umbenennen Ihres Geräts ist ein Fehler {error} aufgetreten.",
"rename_device_headline": "Umbenennen {name}",

View File

@ -202,7 +202,7 @@
"dc_power_control_voltage": "Voltage",
"dc_power_control_voltage_unit": "V",
"delete": "Delete",
"deregister_button": "Deregister from Cloud",
"deregister_from_cloud": "Deregister from Cloud",
"deregister_cloud_devices": "Cloud Devices",
"deregister_description": "This will remove the device from your cloud account and revoke remote access to it. Please note that local access will still be possible",
"deregister_error": "There was an error {status} deregistering your device. Please try again.",
@ -582,7 +582,6 @@
"network_dhcp_client_description": "Configure which DHCP client to use",
"network_dhcp_client_jetkvm": "JetKVM Internal",
"network_dhcp_client_title": "DHCP Client",
"network_dhcp_lease_renew": "Renew DHCP Lease",
"network_dhcp_lease_renew_confirm": "Renew Lease",
"network_dhcp_lease_renew_confirm_description": "This will request a new IP address from your DHCP server. Your device may temporarily lose network connectivity during this process.",
"network_dhcp_lease_renew_confirm_new_a": "If you receive a new IP address",
@ -700,7 +699,7 @@
"register_device_name_label": "Device Name",
"register_device_name_placeholder": "Plex Media Server",
"register_device_no_name": "Please specify a name",
"rename_device_button": "Rename Device",
"rename_device": "Rename Device",
"rename_device_description": "Properly name your device to easily identify it.",
"rename_device_error": "There was an error {error} renaming your device.",
"rename_device_headline": "Rename {name}",

View File

@ -202,7 +202,7 @@
"dc_power_control_voltage": "Voltaje",
"dc_power_control_voltage_unit": "V",
"delete": "Borrar",
"deregister_button": "Darse de baja de la nube",
"deregister_from_cloud": "Darse de baja de la nube",
"deregister_cloud_devices": "Dispositivos en la nube",
"deregister_description": "Esto eliminará el dispositivo de su cuenta en la nube y revocará el acceso remoto. Tenga en cuenta que el acceso local seguirá siendo posible.",
"deregister_error": "Se produjo un error {status} al cancelar el registro de su dispositivo. Inténtelo de nuevo.",
@ -582,7 +582,6 @@
"network_dhcp_client_description": "Configurar qué cliente DHCP utilizar",
"network_dhcp_client_jetkvm": "JetKVM interno",
"network_dhcp_client_title": "Cliente DHCP",
"network_dhcp_lease_renew": "Renovar la concesión de DHCP",
"network_dhcp_lease_renew_confirm": "Renovar el contrato de arrendamiento",
"network_dhcp_lease_renew_confirm_description": "Esto solicitará una nueva dirección IP a su servidor DHCP. Es posible que su dispositivo pierda temporalmente la conexión a la red durante este proceso.",
"network_dhcp_lease_renew_confirm_new_a": "Si recibe una nueva dirección IP",
@ -700,7 +699,7 @@
"register_device_name_label": "Nombre del dispositivo",
"register_device_name_placeholder": "Servidor multimedia Plex",
"register_device_no_name": "Por favor especifique un nombre",
"rename_device_button": "Cambiar el nombre del dispositivo",
"rename_device": "Cambiar el nombre del dispositivo",
"rename_device_description": "Nombra correctamente tu dispositivo para identificarlo fácilmente.",
"rename_device_error": "Se produjo un error {error} al cambiar el nombre de su dispositivo.",
"rename_device_headline": "Cambiar nombre {name}",

View File

@ -202,7 +202,7 @@
"dc_power_control_voltage": "Tension",
"dc_power_control_voltage_unit": "V",
"delete": "Supprimer",
"deregister_button": "Se désinscrire du Cloud",
"deregister_from_cloud": "Se désinscrire du Cloud",
"deregister_cloud_devices": "Appareils Cloud",
"deregister_description": "Cela supprimera l'appareil de votre compte cloud et révoquera l'accès à distance. Veuillez noter que l'accès local restera possible.",
"deregister_error": "Une erreur {status} s'est produite lors de l'annulation de l'enregistrement de votre appareil. Veuillez réessayer.",
@ -581,8 +581,6 @@
"network_description": "Configurez vos paramètres réseau",
"network_dhcp_client_description": "Configurer le client DHCP à utiliser",
"network_dhcp_client_jetkvm": "JetKVM interne",
"network_dhcp_client_title": "Client DHCP",
"network_dhcp_lease_renew": "Renouveler le bail DHCP",
"network_dhcp_lease_renew_confirm": "Renouveler le bail",
"network_dhcp_lease_renew_confirm_description": "Cette opération demandera une nouvelle adresse IP à votre serveur DHCP. Votre appareil pourrait perdre temporairement sa connectivité réseau pendant cette opération.",
"network_dhcp_lease_renew_confirm_new_a": "Si vous recevez une nouvelle adresse IP",
@ -700,7 +698,7 @@
"register_device_name_label": "Nom de l'appareil",
"register_device_name_placeholder": "Serveur multimédia Plex",
"register_device_no_name": "Veuillez spécifier un nom",
"rename_device_button": "Renommer l'appareil",
"rename_device": "Renommer l'appareil",
"rename_device_description": "Nommez correctement votre appareil pour l'identifier facilement.",
"rename_device_error": "Une erreur {error} s'est produite lors du changement de nom de votre appareil.",
"rename_device_headline": "Renommer {name}",

View File

@ -202,7 +202,7 @@
"dc_power_control_voltage": "Voltaggio",
"dc_power_control_voltage_unit": "V",
"delete": "Eliminare",
"deregister_button": "Annulla registrazione dal cloud",
"deregister_from_cloud": "Annulla registrazione dal cloud",
"deregister_cloud_devices": "Dispositivi cloud",
"deregister_description": "Questo rimuoverà il dispositivo dal tuo account cloud e ne revocherà l'accesso remoto. Tieni presente che l'accesso locale sarà comunque possibile.",
"deregister_error": "Si è verificato un errore {status} durante l'annullamento della registrazione del dispositivo. Riprova.",
@ -582,7 +582,6 @@
"network_dhcp_client_description": "Configurare quale client DHCP utilizzare",
"network_dhcp_client_jetkvm": "JetKVM interno",
"network_dhcp_client_title": "Cliente DHCP",
"network_dhcp_lease_renew": "Rinnova il contratto di locazione DHCP",
"network_dhcp_lease_renew_confirm": "Rinnovare il contratto di locazione",
"network_dhcp_lease_renew_confirm_description": "Verrà richiesto un nuovo indirizzo IP al server DHCP. Durante questo processo, il dispositivo potrebbe perdere temporaneamente la connettività di rete.",
"network_dhcp_lease_renew_confirm_new_a": "Se ricevi un nuovo indirizzo IP",
@ -700,7 +699,7 @@
"register_device_name_label": "Nome del dispositivo",
"register_device_name_placeholder": "Server multimediale Plex",
"register_device_no_name": "Si prega di specificare un nome",
"rename_device_button": "Rinomina dispositivo",
"rename_device": "Rinomina dispositivo",
"rename_device_description": "Assegna un nome appropriato al tuo dispositivo per identificarlo facilmente.",
"rename_device_error": "Si è verificato un errore {error} durante la ridenominazione del dispositivo.",
"rename_device_headline": "Rinomina {name}",

View File

@ -202,7 +202,7 @@
"dc_power_control_voltage": "Spenning",
"dc_power_control_voltage_unit": "V",
"delete": "Slett",
"deregister_button": "Avregistrer deg fra skyen",
"deregister_from_cloud": "Avregistrer deg fra skyen",
"deregister_cloud_devices": "Skyenheter",
"deregister_description": "Dette vil fjerne enheten fra skykontoen din og oppheve ekstern tilgang til den. Vær oppmerksom på at lokal tilgang fortsatt vil være mulig.",
"deregister_error": "Det oppsto en feil {status} enheten din skulle avregistreres. Prøv på nytt.",
@ -582,7 +582,6 @@
"network_dhcp_client_description": "Konfigurer hvilken DHCP-klient som skal brukes",
"network_dhcp_client_jetkvm": "JetKVM intern",
"network_dhcp_client_title": "DHCP-klient",
"network_dhcp_lease_renew": "Forny DHCP-leieavtale",
"network_dhcp_lease_renew_confirm": "Forny leieavtalen",
"network_dhcp_lease_renew_confirm_description": "Dette vil be om en ny IP-adresse fra DHCP-serveren din. Enheten din kan midlertidig miste nettverkstilkoblingen under denne prosessen.",
"network_dhcp_lease_renew_confirm_new_a": "Hvis du får en ny IP-adresse",
@ -700,7 +699,7 @@
"register_device_name_label": "Enhetsnavn",
"register_device_name_placeholder": "Plex Media Server",
"register_device_no_name": "Vennligst oppgi et navn",
"rename_device_button": "Gi nytt navn til enheten",
"rename_device": "Gi nytt navn til enheten",
"rename_device_description": "Gi enheten din riktig navn slik at du enkelt kan identifisere den.",
"rename_device_error": "Det oppsto en feil {error} enheten skulle gis nytt navn.",
"rename_device_headline": "Gi nytt navn til {name}",

View File

@ -202,7 +202,7 @@
"dc_power_control_voltage": "Spänning",
"dc_power_control_voltage_unit": "V",
"delete": "Radera",
"deregister_button": "Avregistrera dig från molnet",
"deregister_from_cloud": "Avregistrera dig från molnet",
"deregister_cloud_devices": "Molnenheter",
"deregister_description": "Detta kommer att ta bort enheten från ditt molnkonto och återkalla fjärråtkomst till den. Observera att lokal åtkomst fortfarande kommer att vara möjlig.",
"deregister_error": "Det uppstod ett fel {status} enheten avregistrerades. Försök igen.",
@ -582,7 +582,6 @@
"network_dhcp_client_description": "Konfigurera vilken DHCP-klient som ska användas",
"network_dhcp_client_jetkvm": "JetKVM Intern",
"network_dhcp_client_title": "DHCP-klient",
"network_dhcp_lease_renew": "Förnya DHCP-lease",
"network_dhcp_lease_renew_confirm": "Förnya hyresavtalet",
"network_dhcp_lease_renew_confirm_description": "Detta kommer att begära en ny IP-adress från din DHCP-server. Din enhet kan tillfälligt förlora nätverksanslutningen under denna process.",
"network_dhcp_lease_renew_confirm_new_a": "Om du får en ny IP-adress",
@ -700,7 +699,7 @@
"register_device_name_label": "Enhetsnamn",
"register_device_name_placeholder": "Plex Media Server",
"register_device_no_name": "Vänligen ange ett namn",
"rename_device_button": "Byt namn på enhet",
"rename_device": "Byt namn på enhet",
"rename_device_description": "Namnge din enhet korrekt för att enkelt kunna identifiera den.",
"rename_device_error": "Det uppstod ett fel {error} enheten döptes om.",
"rename_device_headline": "Byt namn på {name}",

View File

@ -202,7 +202,7 @@
"dc_power_control_voltage": "电压",
"dc_power_control_voltage_unit": "V",
"delete": "删除",
"deregister_button": "从云端注销",
"deregister_from_cloud": "从云端注销",
"deregister_cloud_devices": "云设备",
"deregister_description": "这将从您的云帐户中移除该设备,并撤销其远程访问权限。请注意,您仍然可以进行本地访问",
"deregister_error": "注销您的设备时出现错误{status} 。请重试。",
@ -582,7 +582,6 @@
"network_dhcp_client_description": "配置要使用的 DHCP 客户端",
"network_dhcp_client_jetkvm": "JetKVM 内部",
"network_dhcp_client_title": "DHCP客户端",
"network_dhcp_lease_renew": "续订 DHCP 租约",
"network_dhcp_lease_renew_confirm": "续租",
"network_dhcp_lease_renew_confirm_description": "这将从您的 DHCP 服务器请求新的 IP 地址。在此过程中,您的设备可能会暂时失去网络连接。",
"network_dhcp_lease_renew_confirm_new_a": "如果您收到新的 IP 地址",
@ -700,7 +699,7 @@
"register_device_name_label": "设备名称",
"register_device_name_placeholder": "Plex媒体服务器",
"register_device_no_name": "请指定名称",
"rename_device_button": "重命名设备",
"rename_device": "重命名设备",
"rename_device_description": "正确命名您的设备以便轻松识别它。",
"rename_device_error": "重命名您的设备时出现错误{error} 。",
"rename_device_headline": "重命名{name}",

31
ui/package-lock.json generated
View File

@ -11,7 +11,6 @@
"@headlessui/react": "^2.2.9",
"@headlessui/tailwindcss": "^0.2.2",
"@heroicons/react": "^2.2.0",
"@types/uuid": "^10.0.0",
"@vitejs/plugin-basic-ssl": "^2.1.0",
"@xterm/addon-clipboard": "^0.1.0",
"@xterm/addon-fit": "^0.10.0",
@ -1389,9 +1388,9 @@
}
},
"node_modules/@reduxjs/toolkit": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz",
"integrity": "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==",
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.1.tgz",
"integrity": "sha512-sETJ3qO72y7L7WiR5K54UFLT3jRzAtqeBPVO15xC3bGA6kDqCH8m/v7BKCPH4czydXzz/1lPEGLvew7GjOO3Qw==",
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.0.0",
@ -2435,12 +2434,6 @@
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
"license": "MIT"
},
"node_modules/@types/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
"license": "MIT"
},
"node_modules/@types/validator": {
"version": "13.15.3",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.3.tgz",
@ -3093,9 +3086,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.8.16",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz",
"integrity": "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==",
"version": "2.8.17",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.17.tgz",
"integrity": "sha512-j5zJcx6golJYTG6c05LUZ3Z8Gi+M62zRT/ycz4Xq4iCOdpcxwg7ngEYD4KA0eWZC7U17qh/Smq8bYbACJ0ipBA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@ -3217,9 +3210,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001750",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001750.tgz",
"integrity": "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==",
"version": "1.0.30001751",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz",
"integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==",
"dev": true,
"funding": [
{
@ -5986,9 +5979,9 @@
"license": "MIT"
},
"node_modules/node-releases": {
"version": "2.0.23",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz",
"integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==",
"version": "2.0.25",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.25.tgz",
"integrity": "sha512-4auku8B/vw5psvTiiN9j1dAOsXvMoGqJuKJcR+dTdqiXEK20mMTk1UEo3HS16LeGQsVG6+qKTPM9u/qQ2LqATA==",
"dev": true,
"license": "MIT"
},

View File

@ -11,16 +11,17 @@
"dev:ssl": "USE_SSL=true ./dev_device.sh",
"dev:cloud": "vite dev --mode=cloud-development",
"build": "npm run build:prod",
"build:device": "npm run paraglide && tsc && vite build --mode=device --emptyOutDir",
"build:staging": "npm run paraglide && tsc && vite build --mode=cloud-staging",
"build:prod": "npm run paraglide && tsc && vite build --mode=cloud-production",
"lint": "npm run paraglide && eslint './src/**/*.{ts,tsx}'",
"lint:fix": "npm run paraglide && eslint './src/**/*.{ts,tsx}' --fix",
"build:device": "npm run i18n:compile && tsc && vite build --mode=device --emptyOutDir",
"build:staging": "npm run i18n:compile && tsc && vite build --mode=cloud-staging",
"build:prod": "npm run i18n:compile && tsc && vite build --mode=cloud-production",
"lint": "npm run i18n:compile && eslint './src/**/*.{ts,tsx}'",
"lint:fix": "npm run i18n:compile && eslint './src/**/*.{ts,tsx}' --fix",
"i18n": "npm run i18n:resort && npm run i18n:validate && npm run i18n:compile",
"i18n:resort": "python3 tools/resort_messages.py",
"i18n:validate": "inlang validate --project ./localization/jetKVM.UI.inlang",
"i18n:compile": "paraglide-js compile --project ./localization/jetKVM.UI.inlang --outdir ./localization/paraglide",
"i18n:machine-translate": "inlang machine translate --project ./localization/jetKVM.UI.inlang",
"i18n:audit": "npm run i18n:find-dupes && npm i18n:find-excess && npm run i18n:find-unused",
"i18n:find-excess": "python3 ./tools/find_excess_messages.py",
"i18n:find-unused": "python3 ./tools/find_unused_messages.py",
"i18n:find-dupes": "python3 ./tools/find_duplicate_translations.py"
@ -29,7 +30,6 @@
"@headlessui/react": "^2.2.9",
"@headlessui/tailwindcss": "^0.2.2",
"@heroicons/react": "^2.2.0",
"@types/uuid": "^10.0.0",
"@vitejs/plugin-basic-ssl": "^2.1.0",
"@xterm/addon-clipboard": "^0.1.0",
"@xterm/addon-fit": "^0.10.0",

View File

@ -121,7 +121,7 @@ export default function KvmCard({
className="block w-full py-1.5 text-black dark:text-white"
to={`./${id}/rename`}
>
Rename
{m.rename_device()}
</Link>
</div>
</div>
@ -135,7 +135,7 @@ export default function KvmCard({
className="block w-full py-1.5 text-black dark:text-white"
to={`./${id}/deregister`}
>
Deregister from cloud
{m.deregister_from_cloud()}
</Link>
</div>
</div>

View File

@ -91,7 +91,7 @@ export function MacroForm({
await onSubmit(macro);
} catch (error) {
if (error instanceof Error) {
showTemporaryError(m.macro_save_failed_error(error.message || m.unknown_error));
showTemporaryError(m.macro_save_failed_error({error: error.message || m.unknown_error()}));
} else {
showTemporaryError(m.macro_save_failed());
}
@ -214,7 +214,7 @@ export function MacroForm({
<div className="mt-2 space-y-4">
{(macro.steps || []).map((step, stepIndex) => (
<MacroStepCard
key={`step-{stepIndex}`}
key={`step-${stepIndex}`}
step={step}
stepIndex={stepIndex}
onDelete={

View File

@ -212,7 +212,7 @@ export function MacroStepCard({
<div className="flex flex-wrap gap-1 pb-2">
{step.keys.map((key, keyIndex) => (
<span
key={`key-{keyIndex}`}
key={`key-${keyIndex}`}
className="inline-flex items-center rounded-md bg-blue-100 px-1 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-900/40 dark:text-blue-200"
>
<span className="px-1">{keyDisplay(keyDisplayMap, key)}</span>

View File

@ -72,7 +72,7 @@ export function UsbDeviceSetting() {
if ("error" in resp) {
console.error("Failed to load USB devices:", resp.error);
notifications.error(
m.usb_device_failed_load({ error: String(resp.error.data || "Unknown error") }),
m.usb_device_failed_load({ error: String(resp.error.data || m.unknown_error()) }),
);
} else {
const usbConfigState = resp.result as UsbDeviceConfig;
@ -101,7 +101,7 @@ export function UsbDeviceSetting() {
send("setUsbDevices", { devices }, async (resp: JsonRpcResponse) => {
if ("error" in resp) {
notifications.error(
m.usb_device_failed_set({ error: String(resp.error.data || "Unknown error") }),
m.usb_device_failed_set({ error: String(resp.error.data || m.unknown_error()) }),
);
setLoading(false);
return;

View File

@ -97,7 +97,7 @@ export function UsbInfoSetting() {
if ("error" in resp) {
console.error("Failed to load USB Config:", resp.error);
notifications.error(
m.usb_config_failed_load({ error: String(resp.error.data || "Unknown error") }),
m.usb_config_failed_load({ error: String(resp.error.data || m.unknown_error()) }),
);
} else {
const usbConfigState = resp.result as UsbConfigState;
@ -116,7 +116,7 @@ export function UsbInfoSetting() {
send("setUsbConfig", { usbConfig }, async (resp: JsonRpcResponse) => {
if ("error" in resp) {
notifications.error(
m.usb_config_failed_set({ error: String(resp.error.data || "Unknown error") }),
m.usb_config_failed_set({ error: String(resp.error.data || m.unknown_error()) }),
);
setLoading(false);
return;
@ -139,7 +139,7 @@ export function UsbInfoSetting() {
send("getDeviceID", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) {
return notifications.error(
`Failed to get device ID: ${resp.error.data || "Unknown error"}`,
`Failed to get device ID: ${resp.error.data || m.unknown_error()}`,
);
}
setDeviceId(resp.result as string);

View File

@ -116,7 +116,7 @@ export default function ConnectionStatsSidebar() {
metric: x.metric != null ? Math.round(x.metric * 1000) : null,
})}
domain={[0, 600]}
unit={m.connection_stats_units_milliseconds()}
unit={m.connection_stats_unit_milliseconds()}
/>
</div>
@ -140,7 +140,7 @@ export default function ConnectionStatsSidebar() {
metric: x.metric != null ? Math.round(x.metric * 1000) : null,
})}
domain={[0, 10]}
unit={m.connection_stats_units_milliseconds()}
unit={m.connection_stats_unit_milliseconds()}
/>
{/* Playback Delay */}

View File

@ -109,7 +109,7 @@ export default function DevicesIdDeregister() {
size="MD"
theme="danger"
type="submit"
text={m.deregister_button()}
text={m.deregister_from_cloud()}
textAlign="center"
/>
</div>

View File

@ -39,13 +39,10 @@ import {
export default function MountRoute() {
const navigate = useNavigate();
{
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
}
return <Dialog onClose={() => navigate("..")} />;
}
export function Dialog({ onClose }: { onClose: () => void }) {
export function Dialog({ onClose }: Readonly<{ onClose: () => void }>) {
const {
modalView,
setModalView,

View File

@ -2,7 +2,6 @@ import { Form, redirect, useActionData, useLoaderData } from "react-router";
import type { ActionFunction, ActionFunctionArgs, LoaderFunction, LoaderFunctionArgs } from "react-router";
import { ChevronLeftIcon } from "@heroicons/react/16/solid";
import { User } from "@hooks/stores";
import { Button, LinkButton } from "@components/Button";
import Card from "@components/Card";
import { CardHeader } from "@components/CardHeader";
@ -14,11 +13,6 @@ import { CLOUD_API } from "@/ui.config";
import api from "@/api";
import { m } from "@localizations/messages";
interface LoaderData {
device: { id: string; name: string; user: { googleId: string } };
user: User;
}
const action: ActionFunction = async ({ params, request }: ActionFunctionArgs) => {
const { id } = params;
const { name } = Object.fromEntries(await request.formData());
@ -65,7 +59,7 @@ const loader: LoaderFunction = async ({ params }: LoaderFunctionArgs) => {
};
export default function DeviceIdRename() {
const { device, user } = useLoaderData() as LoaderData;
const { device, user } = useLoaderData();
const error = useActionData() as { message: string };
return (
@ -114,7 +108,7 @@ export default function DeviceIdRename() {
size="MD"
theme="primary"
type="submit"
text={m.rename_device_button()}
text={m.rename_device()}
textAlign="center"
/>
</Form>

View File

@ -92,7 +92,7 @@ export default function SettingsAccessIndexRoute() {
send("deregisterDevice", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) {
notifications.error(
m.access_failed_deregister({ error: resp.error.data || "Unknown error" }),
m.access_failed_deregister({ error: resp.error.data || m.unknown_error() }),
);
return;
}
@ -114,7 +114,7 @@ export default function SettingsAccessIndexRoute() {
send("setCloudUrl", { apiUrl: cloudApiUrl, appUrl: cloudAppUrl }, (resp: JsonRpcResponse) => {
if ("error" in resp) {
notifications.error(
m.access_failed_update_cloud_url({ error: resp.error.data || "Unknown error" }),
m.access_failed_update_cloud_url({ error: resp.error.data || m.unknown_error() }),
);
return;
}
@ -160,7 +160,7 @@ export default function SettingsAccessIndexRoute() {
send("setTLSState", { state }, (resp: JsonRpcResponse) => {
if ("error" in resp) {
notifications.error(
m.access_failed_update_tls({ error: resp.error.data || "Unknown error" }),
m.access_failed_update_tls({ error: resp.error.data || m.unknown_error() }),
);
return;
}

View File

@ -22,13 +22,10 @@ export default function SecurityAccessLocalAuthRoute() {
}
}, [init, navigateTo, setModalView]);
{
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
}
return <Dialog onClose={() => navigateTo("..")} />;
}
export function Dialog({ onClose }: { onClose: () => void }) {
export function Dialog({ onClose }: Readonly<{ onClose: () => void }>) {
const { modalView, setModalView } = useLocalAuthModalStore();
const [error, setError] = useState<string | null>(null);
const revalidator = useRevalidator();

View File

@ -19,10 +19,10 @@ export default function SettingsGeneralRebootRoute() {
export function Dialog({
onClose,
onConfirmUpdate,
}: {
}: Readonly<{
onClose: () => void;
onConfirmUpdate: () => void;
}) {
}>) {
return (
<div className="pointer-events-auto relative mx-auto text-left">

View File

@ -36,19 +36,16 @@ export default function SettingsGeneralUpdateRoute() {
}
}, [otaState.updating, otaState.error, setModalView, updateSuccess]);
{
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
}
return <Dialog onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />;
}
export function Dialog({
onClose,
onConfirmUpdate,
}: {
}: Readonly<{
onClose: () => void;
onConfirmUpdate: () => void;
}) {
}>) {
const { navigateTo } = useDeviceUiNavigation();
const [versionInfo, setVersionInfo] = useState<null | SystemVersionInfo>(null);

View File

@ -58,7 +58,7 @@ export default function SettingsMacrosRoute() {
setActionLoadingId(macro.id);
const newMacroCopy: KeySequence = {
...JSON.parse(JSON.stringify(macro)),
...structuredClone(macro),
id: generateMacroId(),
name: `${macro.name} ${COPY_SUFFIX}`,
sortOrder: macros.length + 1,
@ -170,7 +170,7 @@ export default function SettingsMacrosRoute() {
const StepIcon = stepIndex === 0 ? LuMoveRight : LuCornerDownRight;
return (
<span key={stepIndex} className="inline-flex items-center">
<span key={`step-${stepIndex}`} className="inline-flex items-center">
<StepIcon className="mr-1 h-3 w-3 shrink-0 text-slate-400 dark:text-slate-500" />
<span className="rounded-md border border-slate-200/50 bg-slate-50 px-2 py-0.5 dark:border-slate-700/50 dark:bg-slate-800">
{(Array.isArray(step.modifiers) &&

View File

@ -50,7 +50,7 @@ const resolveOnRtcReady = () => {
});
};
export function LifeTimeLabel({ lifetime }: { lifetime: string }) {
export function LifeTimeLabel({ lifetime }: Readonly<{ lifetime: string }>) {
const [remaining, setRemaining] = useState<string | null>(null);
// rrecalculate remaining time every 30 seconds
@ -106,7 +106,7 @@ export default function SettingsNetworkRoute() {
getNetworkState(),
])) as [NetworkSettings, NetworkState];
setNetworkState(state as NetworkState);
setNetworkState(state);
const settingsWithDefaults = {
...settings,
@ -160,8 +160,8 @@ export default function SettingsNetworkRoute() {
const onSubmit = useCallback(async (settings: NetworkSettings) => {
if (settings.ipv4_static?.address?.includes("/")) {
const parts = settings.ipv4_static.address.split("/");
const cidrNotation = parseInt(parts[1]);
if (isNaN(cidrNotation) || cidrNotation < 0 || cidrNotation > 32) {
const cidrNotation = Number.parseInt(parts[1]);
if (Number.isNaN(cidrNotation) || cidrNotation < 0 || cidrNotation > 32) {
return notifications.error(m.network_ipv4_invalid_cidr());
}
settings.ipv4_static.netmask = netMaskFromCidr4(cidrNotation);
@ -309,7 +309,6 @@ export default function SettingsNetworkRoute() {
title={m.network_title()}
description={m.network_description()}
action={
<>
<div>
<Button
size="SM"
@ -320,7 +319,6 @@ export default function SettingsNetworkRoute() {
text={formState.isSubmitting ? m.saving() : m.network_save_settings()}
/>
</div>
</>
}
/>
<div className="space-y-4">

View File

@ -185,7 +185,7 @@ export default function SettingsVideoRoute() {
max="2.0"
step="0.1"
value={videoSaturation}
onChange={e => setVideoSaturation(parseFloat(e.target.value))}
onChange={e => setVideoSaturation(Number.parseFloat(e.target.value))}
className="h-2 w-32 cursor-pointer appearance-none rounded-lg bg-gray-200 dark:bg-gray-700"
/>
</SettingsItem>
@ -200,7 +200,7 @@ export default function SettingsVideoRoute() {
max="1.5"
step="0.1"
value={videoBrightness}
onChange={e => setVideoBrightness(parseFloat(e.target.value))}
onChange={e => setVideoBrightness(Number.parseFloat(e.target.value))}
className="h-2 w-32 cursor-pointer appearance-none rounded-lg bg-gray-200 dark:bg-gray-700"
/>
</SettingsItem>
@ -215,7 +215,7 @@ export default function SettingsVideoRoute() {
max="2.0"
step="0.1"
value={videoContrast}
onChange={e => setVideoContrast(parseFloat(e.target.value))}
onChange={e => setVideoContrast(Number.parseFloat(e.target.value))}
className="h-2 w-32 cursor-pointer appearance-none rounded-lg bg-gray-200 dark:bg-gray-700"
/>
</SettingsItem>
@ -226,9 +226,9 @@ export default function SettingsVideoRoute() {
theme="light"
text={m.video_reset_to_default()}
onClick={() => {
setVideoSaturation(1.0);
setVideoBrightness(1.0);
setVideoContrast(1.0);
setVideoSaturation(1);
setVideoBrightness(1);
setVideoContrast(1);
}}
/>
</div>
@ -250,7 +250,7 @@ export default function SettingsVideoRoute() {
setCustomEdidValue("");
} else {
setCustomEdidValue(null);
handleEDIDChange(e.target.value as string);
handleEDIDChange(e.target.value);
}
}}
options={[...edids, { value: "custom", label: m.video_edid_custom() }]}

View File

@ -54,10 +54,6 @@ import { FeatureFlagProvider } from "@providers/FeatureFlagProvider";
import { DeviceStatus } from "@routes/welcome-local";
import { m } from "@localizations/messages.js";
interface LocalLoaderResp {
authMode: "password" | "noPassword" | null;
}
interface CloudLoaderResp {
deviceName: string;
user: User | null;
@ -117,7 +113,7 @@ const loader: LoaderFunction = ({ params }: LoaderFunctionArgs) => {
};
export default function KvmIdRoute() {
const loaderResp = useLoaderData() as LocalLoaderResp | CloudLoaderResp;
const loaderResp = useLoaderData();
// Depending on the mode, we set the appropriate variables
const user = "user" in loaderResp ? loaderResp.user : null;
const deviceName = "deviceName" in loaderResp ? loaderResp.deviceName : null;
@ -280,7 +276,7 @@ export default function KvmIdRoute() {
},
onMessage(event: WebSocketEventMap['message']) {
const message = event as MessageEvent;
const message = event;
if (message.data === "pong") return;
/*
@ -877,6 +873,7 @@ export default function KvmIdRoute() {
<div
className="z-50"
role="form"
onClick={e => e.stopPropagation()}
onMouseUp={e => e.stopPropagation()}
onMouseDown={e => e.stopPropagation()}

View File

@ -1,5 +1,4 @@
import { useLoaderData, useRevalidator } from "react-router";
import type { LoaderFunction } from "react-router";
import { useLoaderData, useRevalidator, type LoaderFunction } from "react-router";
import { LuMonitorSmartphone } from "react-icons/lu";
import { ArrowRightIcon } from "@heroicons/react/16/solid";
import { useInterval } from "usehooks-ts";
@ -17,7 +16,6 @@ interface LoaderData {
devices: { id: string; name: string; online: boolean; lastSeen: string }[];
user: User;
}
const loader: LoaderFunction = async ()=> {
const user = await checkAuth();
@ -81,7 +79,6 @@ export default function DevicesRoute() {
/>
</div>
) : (
<>
<div className="grid gap-4 sm:grid-cols-2 md:grid-cols-3">
{devices.map(x => {
return (
@ -95,7 +92,6 @@ export default function DevicesRoute() {
);
})}
</div>
</>
)}
</div>
</div>

View File

@ -95,6 +95,8 @@ export default function LoginLocalRoute() {
<div
onClick={() => setShowPassword(false)}
className="pointer-events-auto"
role="switch"
aria-checked={showPassword}
>
<LuEye className="h-4 w-4 cursor-pointer text-slate-500 dark:text-slate-400" />
</div>
@ -102,6 +104,8 @@ export default function LoginLocalRoute() {
<div
onClick={() => setShowPassword(true)}
className="pointer-events-auto"
role="switch"
aria-checked={!showPassword}
>
<LuEyeOff className="h-4 w-4 cursor-pointer text-slate-500 dark:text-slate-400" />
</div>

View File

@ -98,6 +98,8 @@ export default function WelcomeLocalModeRoute() {
<div
className="relative flex cursor-pointer flex-col items-center p-6 select-none"
onClick={() => setSelectedMode(mode as "password" | "noPassword")}
role="switch"
aria-checked={selectedMode === "password"}
>
<div className="space-y-0 text-center">
<h3 className="text-base font-bold text-black dark:text-white">

View File

@ -112,6 +112,8 @@ export default function WelcomeLocalPasswordRoute() {
<div
onClick={() => setShowPassword(false)}
className="pointer-events-auto"
role="switch"
aria-checked={showPassword}
>
<LuEye className="h-4 w-4 cursor-pointer text-slate-500 dark:text-slate-400" />
</div>
@ -119,6 +121,8 @@ export default function WelcomeLocalPasswordRoute() {
<div
onClick={() => setShowPassword(true)}
className="pointer-events-auto"
role="switch"
aria-checked={!showPassword}
>
<LuEyeOff className="h-4 w-4 cursor-pointer text-slate-500 dark:text-slate-400" />
</div>

View File

@ -27,6 +27,10 @@ const loader: LoaderFunction = async () => {
return null;
};
const LogoLeadingIcon = ({ className }: { className?: string }) => (
<img src={LogoMark} className={cx(className, "mr-1.5 h-5!")} alt={m.jetkvm_logo()} />
);
export default function WelcomeRoute() {
const [imageLoaded, setImageLoaded] = useState(false);
@ -89,9 +93,7 @@ export default function WelcomeRoute() {
size="LG"
theme="light"
text={m.jetkvm_setup()}
LeadingIcon={({ className }) => (
<img src={LogoMark} className={cx(className, "mr-1.5 h-5!")} />
)}
LeadingIcon={LogoLeadingIcon}
textAlign="center"
to="/welcome/mode"
/>

View File

@ -4,6 +4,7 @@ import json
import re
from datetime import datetime
from pathlib import Path
import sys
def flatten_strings(obj, prefix=""):
if isinstance(obj, dict):
@ -24,7 +25,7 @@ def normalize(s, ignore_case=False, trim=False, collapse_ws=False):
s = s.lower()
return s
def main():
def main(argv):
p = argparse.ArgumentParser(
description="Find identical translation targets with different keys in en.json"
)