From 66ab743dfe4afc3d28313e680a1372248b3a2120 Mon Sep 17 00:00:00 2001
From: Marc Brooks
Date: Wed, 8 Oct 2025 22:02:06 -0500
Subject: [PATCH] Localize all pages except Settings
---
ui/eslint.config.cjs | 5 +-
ui/localization/messages/en.json | 608 +++++++++++-------
ui/package-lock.json | 76 ++-
ui/package.json | 10 +-
ui/src/components/ActionBar.tsx | 2 +-
ui/src/components/Button.tsx | 2 +-
ui/src/components/Header.tsx | 2 +-
ui/src/components/InfoBar.tsx | 11 +-
ui/src/components/Terminal.tsx | 4 +-
ui/src/components/USBStateStatus.tsx | 1 -
.../components/UpdateInProgressStatusCard.tsx | 1 -
ui/src/components/VirtualKeyboard.tsx | 2 +-
ui/src/components/WebRTCVideo.tsx | 25 +-
ui/src/hooks/useHidRpc.ts | 2 +-
ui/src/hooks/useJsonRpc.ts | 2 +-
ui/src/hooks/useKeyboard.ts | 122 ++--
ui/src/main.tsx | 8 +-
ui/src/notifications.tsx | 4 +-
ui/src/routes/devices.$id.deregister.tsx | 31 +-
ui/src/routes/devices.$id.mount.tsx | 166 +++--
ui/src/routes/devices.$id.other-session.tsx | 14 +-
ui/src/routes/devices.$id.rename.tsx | 28 +-
ui/src/routes/devices.$id.setup.tsx | 34 +-
ui/src/routes/devices.$id.tsx | 93 +--
ui/src/routes/devices.already-adopted.tsx | 12 +-
ui/src/routes/devices.tsx | 13 +-
ui/src/routes/login-local.tsx | 41 +-
ui/src/routes/signup.tsx | 2 +-
ui/src/routes/welcome-local.mode.tsx | 38 +-
ui/src/routes/welcome-local.password.tsx | 36 +-
ui/src/routes/welcome-local.tsx | 23 +-
31 files changed, 782 insertions(+), 636 deletions(-)
diff --git a/ui/eslint.config.cjs b/ui/eslint.config.cjs
index 8c6d57d6..ad4338a3 100644
--- a/ui/eslint.config.cjs
+++ b/ui/eslint.config.cjs
@@ -9,8 +9,6 @@ const {
fixupConfigRules,
} = require("@eslint/compat");
-const tsParser = require("@typescript-eslint/parser");
-const reactRefresh = require("eslint-plugin-react-refresh");
const js = require("@eslint/js");
const {
@@ -23,6 +21,9 @@ const compat = new FlatCompat({
allConfig: js.configs.all
});
+const tsParser = require("@typescript-eslint/parser");
+const reactRefresh = require("eslint-plugin-react-refresh");
+
module.exports = defineConfig([{
languageOptions: {
globals: {
diff --git a/ui/localization/messages/en.json b/ui/localization/messages/en.json
index ade66285..a7bf672d 100644
--- a/ui/localization/messages/en.json
+++ b/ui/localization/messages/en.json
@@ -1,19 +1,146 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
- "jetkvm": "JetKVM",
- "jetkvm_logo": "JetKVM Logo",
+ "action_bar_connection_stats": "Connection Stats",
+ "action_bar_exit_fullscreen": "Exit Fullscreen",
+ "action_bar_extension": "Extension",
+ "action_bar_fullscreen": "Fullscreen",
+ "action_bar_paste_text": "Paste text",
+ "action_bar_settings": "Settings",
+ "action_bar_virtual_keyboard": "Virtual Keyboard",
+ "action_bar_virtual_media": "Virtual Media",
+ "action_bar_wake_on_lan": "Wake on LAN",
+ "action_bar_web_terminal": "Web Terminal",
+ "already_adopted_new_owner": "If you're the new owner, please ask the previous owner to de-register the device from their account in the cloud dashboard. If you believe this is an error, contact our support team for assistance.",
+ "already_adopted_other_user": "This device is currently registered to another user in our cloud dashboard.",
+ "already_adopted_return_to_dashboard": "Return to Dashboard",
+ "already_adopted_title": "Device Already Registered",
"attach": "Attach",
+ "atx_power_control_get_state_error": "Failed to get ATX power state: {error}",
+ "atx_power_control_hdd_led": "HDD LED",
+ "atx_power_control_long_power_button": "Long Press",
+ "atx_power_control_power_button": "Power",
+ "atx_power_control_power_led": "Power LED",
+ "atx_power_control_reset_button": "Reset",
+ "atx_power_control_send_action_error": "Failed to send ATX power action {action}: {error}",
+ "atx_power_control_short_power_button": "Short Press",
+ "auth_authentication_mode_error": "An error occurred while setting the authentication mode",
+ "auth_authentication_mode_invalid": "Invalid authentication mode",
+ "auth_authentication_mode": "Please select an authentication mode",
+ "auth_connect_to_cloud_action": "Log in & Connect device",
+ "auth_connect_to_cloud_description": "Unlock remote access and advanced features for your device",
+ "auth_connect_to_cloud": "Connect your JetKVM to the cloud",
+ "auth_header_cta_already_have_account": "Already have an account?",
+ "auth_header_cta_dont_have_account": "Don't have an account?",
+ "auth_header_cta_new_to_jetkvm": "New to JetKVM?",
+ "auth_login_action": "Log in",
+ "auth_login_description": "Log in to access and manage your devices securely",
+ "auth_login": "Log in to your JetKVM account",
+ "auth_mode_local_change_later": "You can always change your authentication method later in the settings.",
+ "auth_mode_local_description": "Select how you would like to secure your JetKVM device locally.",
+ "auth_mode_local_no_password_description": "Quick access without password authentication.",
+ "auth_mode_local_no_password": "No Password",
+ "auth_mode_local_password_confirm_description": "Confirm your password",
+ "auth_mode_local_password_confirm_label": "Confirm Password",
+ "auth_mode_local_password_description": "Secure your device with a password for added protection.",
+ "auth_mode_local_password_do_not_match": "Passwords do not match",
+ "auth_mode_local_password_failed_set": "Failed to set password: {error}",
+ "auth_mode_local_password_note_local": "All data remains on your local device.",
+ "auth_mode_local_password_note": "This password will be used to secure your device data and protect against unauthorized access.",
+ "auth_mode_local_password_set_button": "Set Password",
+ "auth_mode_local_password_set_description": "Create a strong password to secure your JetKVM device locally.",
+ "auth_mode_local_password_set_label": "Enter a password",
+ "auth_mode_local_password_set": "Set a Password",
+ "auth_mode_local_password": "Password",
+ "auth_mode_local": "Local Authentication Method",
+ "auth_signup_connect_to_cloud_action": "Signup & Connect device",
+ "auth_signup_create_account_action": "Create Account",
+ "auth_signup_create_account_description": "Create your account and start managing your devices with ease.",
+ "auth_signup_create_account": "Create your JetKVM account",
+ "back_to_devices": "Back to Devices",
+ "back": "Back",
"cancel": "Cancel",
"close": "Close",
+ "cloud_kvms_description": "Manage your cloud KVMs and connect to them securely.",
+ "cloud_kvms_no_devices_description": "You don't have any devices with enabled JetKVM Cloud yet.",
+ "cloud_kvms_no_devices": "No devices found",
+ "cloud_kvms": "Cloud KVMs",
"confirm": "Confirm",
"connect_to_kvm": "Connect to KVM",
+ "connecting_to_device": "Connecting to device…",
+ "connection_established": "Connection established",
+ "connection_stats_badge_jitter_buffer_avg_delay": "Jitter Buffer Avg. Delay",
+ "connection_stats_badge_jitter": "Jitter",
+ "connection_stats_connection_description": "The connection between the client and the JetKVM.",
+ "connection_stats_connection": "Connection",
+ "connection_stats_frames_per_second_description": "Number of inbound video frames displayed per second.",
+ "connection_stats_frames_per_second": "Frames per second",
+ "connection_stats_network_stability_description": "How steady the flow of inbound video packets is across the network.",
+ "connection_stats_network_stability": "Network Stability",
+ "connection_stats_packets_lost_description": "Count of lost inbound video RTP packets.",
+ "connection_stats_packets_lost": "Packets Lost",
+ "connection_stats_playback_delay_description": "Delay added by the jitter buffer to smooth playback when frames arrive unevenly.",
+ "connection_stats_playback_delay": "Playback Delay",
+ "connection_stats_round_trip_time_description": "Round-trip time for the active ICE candidate pair between peers.",
+ "connection_stats_round_trip_time": "Round-Trip Time",
+ "connection_stats_sidebar": "Connection Stats",
+ "connection_stats_video_description": "The video stream from the JetKVM to the client.",
+ "connection_stats_video": "Video",
+ "continue": "Continue",
+ "creating_peer_connection": "Creating peer connection...",
+ "dc_power_control_current_unit": "A",
+ "dc_power_control_current": "Current",
+ "dc_power_control_get_state_error": "Failed to get DC power state: {error}",
+ "dc_power_control_power_off_button": "Power Off",
+ "dc_power_control_power_off_state": "Power OFF",
+ "dc_power_control_power_on_button": "Power On",
+ "dc_power_control_power_on_state": "Power ON",
+ "dc_power_control_power_unit": "W",
+ "dc_power_control_power": "Power",
+ "dc_power_control_restore_last_state": "Last State",
+ "dc_power_control_restore_power_state": "Restore Power Loss",
+ "dc_power_control_set_power_state_error": "Failed to send DC power state to {enabled}: {error}",
+ "dc_power_control_set_restore_state_error": "Failed to send DC power restore state to {state}: {error}",
+ "dc_power_control_voltage_unit": "V",
+ "dc_power_control_voltage": "Voltage",
"default": "Default",
"delete": "Delete",
+ "deregister_button": "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.",
"deregister_from_cloud": "Deregister from cloud",
+ "deregister_headline": "Deregister {device} from your cloud account",
"detach": "Detach",
+ "dhcp_lease_boot_file": "Boot File",
+ "dhcp_lease_boot_next_server": "Boot Next Server",
+ "dhcp_lease_boot_server_name": "Boot Server Name",
+ "dhcp_lease_broadcast": "Broadcast",
+ "dhcp_lease_domain": "Domain",
+ "dhcp_lease_gateway": "Gateway",
+ "dhcp_lease_header": "DHCP Lease Information",
+ "dhcp_lease_hostname": "Hostname",
+ "dhcp_lease_lease_expires": "Lease Expires",
+ "dhcp_lease_maximum_transfer_unit": "MTU",
+ "dhcp_lease_renew": "Renew DHCP Lease",
+ "dhcp_lease_time_to_live": "TTL",
"dhcp_server": "DHCP Server",
"dns_servers": "DNS Servers",
+ "establishing_secure_connection": "Establishing secure connection…",
+ "experimental": "Experimental",
+ "extension_popover_load_and_manage_extensions": "Load and manage your extensions",
+ "extension_popover_set_error_notification": "Failed to set active extension: {error}",
+ "extension_popover_unload_extension": "Unload Extension",
+ "extension_serial_console_description": "Access your serial console extension",
+ "extension_serial_console": "Serial Console",
+ "extensions_atx_power_control_description": "Control the power state of your machine via ATX power control.",
+ "extensions_atx_power_control": "ATX Power Control",
+ "extensions_dc_power_control_description": "Control your DC Power extension",
+ "extensions_dc_power_control": "DC Power Control",
+ "extensions_popover_extensions": "Extensions",
+ "gathering_ice_candidates": "Gathering ICE candidates...",
+ "getting_remote_session_description": "Getting remote session description attempt {attempt}",
"hide": "Hide",
+ "ice_gathering_completed": "ICE Gathering completed",
"info_caps_lock": "Caps Lock",
"info_compose": "Compose",
"info_hdmi_state": "HDMI State:",
@@ -32,73 +159,186 @@
"info_usb_state": "USB State:",
"info_video_size": "Video Size:",
"input_disabled": "Input disabled",
+ "invalid_password": "Invalid password",
"ip_address": "IP Address",
+ "ipv6_address_label": "Address",
+ "ipv6_addresses": "IPv6 Addresses",
+ "ipv6_information": "IPv6 Information",
+ "ipv6_link_local": "Link-local",
+ "ipv6_preferred_lifetime": "Preferred Lifetime",
+ "ipv6_valid_lifetime": "Valid Lifetime",
+ "jetkvm_description": "JetKVM combines powerful hardware with intuitive software to provide a seamless remote control experience.",
+ "jetkvm_device": "JetKVM Device",
+ "jetkvm_logo": "JetKVM Logo",
+ "jetkvm_setup": "Set up your JetKVM",
+ "jetkvm": "JetKVM",
+ "jiggler_cron_schedule_description": "Cron expression for scheduling",
+ "jiggler_cron_schedule_label": "Cron Schedule",
+ "jiggler_example_business_hours_early": "Business Hours 8-17",
+ "jiggler_example_business_hours_late": "Business Hours 9-17",
+ "jiggler_examples_label": "Examples",
+ "jiggler_inactivity_limit_description": "Inactivity time before jiggle",
+ "jiggler_inactivity_limit_label": "Inactivity Limit Seconds",
+ "jiggler_more_examples": "More examples",
+ "jiggler_random_delay_description": "To avoid recognizable patterns",
+ "jiggler_random_delay_label": "Random delay",
+ "jiggler_save_jiggler_config": "Save Jiggler Config",
+ "jiggler_timezone_description": "Timezone for cron schedule",
+ "jiggler_timezone_label": "Timezone",
+ "kvm_terminal": "KVM Terminal",
"last_online": "Last online {time}",
+ "learn_more": "Learn more",
"load": "Load",
+ "loading": "Loading…",
+ "log_in": "Log In",
"log_out": "Log out",
"logged_in_as": "Logged in as",
+ "login_enter_password_description": "Enter your password to access your JetKVM.",
+ "login_enter_password": "Enter your password",
+ "login_error": "An error occurred while logging in",
+ "login_forgot_password": "Forgot password?",
+ "login_password_label": "Password",
+ "login_welcome_back": "Welcome back to JetKVM",
+ "macro_add_step": "Add Step{maxed_out}",
+ "macro_at_least_one_step_keys_or_modifiers": "At least one step must have keys or modifiers",
+ "macro_at_least_one_step_required": "At least one step is required",
+ "macro_max_steps_error": "You can only add a maximum of {max} steps per macro.",
+ "macro_max_steps_reached": "({max} max)",
+ "macro_name_label": "Macro Name",
+ "macro_name_required": "Name is required",
+ "macro_name_too_long": "Name must be less than 50 characters",
+ "macro_please_fix_validation_errors": "Please fix the validation errors",
+ "macro_save_error": "An error occurred while saving.",
+ "macro_save": "Save Macro",
+ "macro_step_count": "{steps} / {max} steps",
+ "macro_step_duration_description": "Time to wait before executing the next step.",
+ "macro_step_duration_label": "Step Duration",
+ "macro_step_keys_description": "Maximum {max} keys per step.",
+ "macro_step_keys_label": "Keys",
+ "macro_step_max_keys_reached": "Maximum keys reached",
+ "macro_step_modifiers_description": "What modifiers (Shift/Ctrl/Alt/Meta) are pressed during this step.",
+ "macro_step_modifiers_label": "Modifiers",
+ "macro_step_no_matching_keys_found": "No matching keys found",
+ "macro_step_search_for_key": "Search for key…",
+ "macro_steps_description": "Keys/modifiers executed in sequence with a delay between each step.",
+ "macro_steps_label": "Steps",
+ "metric_not_supported": "Metric not supported",
+ "metric_waiting_for_data": "Waiting for data…",
+ "mount_add_file_to_get_started": "Add a file to get started",
+ "mount_add_new_media": "Add New Media",
+ "mount_available_storage": "Available Storage",
+ "mount_button_back_to_overview": "Back to Overview",
+ "mount_button_cancel_upload": "Cancel Upload",
+ "mount_button_continue_upload": "Continue uploading",
+ "mount_button_mount_file": "Mount File",
+ "mount_button_mount_url": "Mount URL",
+ "mount_button_next": "Next",
+ "mount_button_previous": "Previous",
+ "mount_button_select": "Select",
+ "mount_button_showing_results": "Showing {from} to {to} of {total} results",
+ "mount_button_upload_new_image": "Upload a new image",
+ "mount_bytes_free": "{bytesFree} free",
+ "mount_bytes_used": "{bytesUsed} used",
+ "mount_calculating": "Calculating…",
+ "mount_click_to_select_file": "Click to select a file",
+ "mount_click_to_select_incomplete": "Click to select \"{name}\"",
+ "mount_confirm_delete": "Are you sure you want to delete {name}?",
+ "mount_continue_uploading_with_name": "Continue uploading \"{name}\"",
+ "mount_description_mode": "Choose how you want to mount your virtual media",
+ "mount_error_delete_file": "Error deleting file: {error}",
+ "mount_error_description": "An error occurred while attempting to mount the media. Please try again.",
+ "mount_error_get_storage_space": "Error getting storage space: {error}",
+ "mount_error_list_storage": "Error listing storage files: {error}",
+ "mount_error_title": "Mount Error",
+ "mount_get_state_error": "Failed to get virtual media state: {error}",
+ "mount_jetkvm_storage_description": "Mount previously uploaded files from the JetKVM storage",
+ "mount_jetkvm_storage": "JetKVM Storage Mount",
+ "mount_label_mount_as": "Mount as",
+ "mount_label_url_description": "Mount files from any public web address",
+ "mount_label_url": "URL Mount",
+ "mount_mode_cdrom": "CD/DVD",
+ "mount_mode_disk": "Disk",
+ "mount_mounted_as": "Mounted as",
+ "mount_mounted_from_storage": "Mounted from JetKVM Storage",
+ "mount_no_images_description": "Upload an image to start virtual media mounting.",
+ "mount_no_images_title": "No images available",
+ "mount_no_mounted_media": "No mounted media",
+ "mount_percentage_used": "{percentageUsed}% used",
+ "mount_please_select_file_to_upload": "Please select the file to upload.",
+ "mount_please_select_file": "Please select the file \"{name}\" to continue the upload.",
+ "mount_popular_images": "Popular images",
+ "mount_streaming_from_url": "Streaming from URL",
+ "mount_supported_formats": "Supported formats: ISO, IMG",
+ "mount_tag_experimental": "Experimental",
+ "mount_title_mode": "Virtual Media Source",
+ "mount_unmount_error": "Failed to unmount image: {error}",
+ "mount_unmount": "Unmount",
+ "mount_upload_description": "Select an image file to upload to JetKVM storage",
+ "mount_upload_error": "Upload error: {error}",
+ "mount_upload_failed_datachannel": "Failed to create data channel for file upload",
+ "mount_upload_failed_rtc": "Upload failed: {error}",
+ "mount_upload_successful": "Upload successful",
+ "mount_upload_title": "Upload New Image",
+ "mount_uploaded_has_been_uploaded": "{name} has been uploaded",
+ "mount_uploading_with_name": "Uploading {name}",
+ "mount_uploading": "Uploading…",
+ "mount_url_description": "Mount files from any public web address",
+ "mount_url_input_label": "Image URL",
+ "mount_url_mount": "URL Mount",
+ "mount_view_device_description": "Select an image to mount from the JetKVM storage",
+ "mount_view_device_title": "Mount from JetKVM Storage",
+ "mount_view_url_description": "Enter an URL to the image file to mount",
+ "mount_view_url_title": "Mount from URL",
+ "mount_virtual_media_description": "Mount an image to boot from or install an operating system.",
+ "mount_virtual_media_source_description": "Choose how you want to mount your virtual media",
+ "mount_virtual_media_source": "Virtual Media Source",
+ "mount_virtual_media": "Virtual Media",
"never_seen_online": "Never seen online",
+ "next": "Next",
"no_results_found": "No results found",
"not_available": "N/A",
"not_found": "Not found",
"ntp_servers": "NTP Servers",
"oh_no": "Oh no!",
"online": "Online",
+ "other_session_detected": "Another Active Session Detected",
+ "other_session_take_over": " Only one active session is supported at a time. Would you like to take over this session?",
+ "other_session_use_here_button": "Use Here",
"page_not_found_description": "The page you were looking for does not exist.",
+ "paste_modal_confirm_paste": "Confirm Paste",
+ "paste_modal_delay_between_keys": "Delay between keys",
+ "paste_modal_delay_out_of_range": "Delay must be between {min} and {max}",
+ "paste_modal_failed_paste": "Failed to paste text: {error}",
+ "paste_modal_invalid_chars_intro": "The following characters won't be pasted:",
+ "paste_modal_paste_from_host": "Paste from host",
+ "paste_modal_paste_text_description": "Paste text from your client to the remote host",
+ "paste_modal_paste_text": "Paste text",
+ "paste_modal_sending_using_layout": "Sending text using keyboard layout: {iso}-{name}",
+ "peer_connection_closed": "Closed",
+ "peer_connection_closing": "Closing",
+ "peer_connection_connected": "Connected",
+ "peer_connection_connecting": "Connecting",
+ "peer_connection_disconnected": "Disconnected",
+ "peer_connection_error": "Connection error",
+ "peer_connection_failed": "Connection failed",
+ "peer_connection_new": "Connecting",
+ "previous": "Previous",
+ "register_device_error": "There was an error {error} registering your device.",
+ "register_device_finish_button": "Finish Setup",
+ "register_device_name_description": "Name your device so you can easily identify it later. You can change this name at any time.",
+ "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_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}",
+ "rename_device_new_name_label": "New device name",
+ "rename_device_new_name_placeholder": "Plex Media Server",
+ "rename_device_no_name": "Please specify a name",
"rename": "Rename",
- "saving": "Saving...",
- "search_placeholder": "Search...",
- "something_went_wrong": "Something went wrong. Please try again later or contact support",
- "subnet_mask": "Subnet Mask",
- "troubleshoot_connection": "Troubleshoot Connection",
- "unknown_error": "Unknown error",
- "update_in_progress": "Update in Progress",
- "updating_leave_device_on": "Please don't turn off your device...",
- "usb": "USB",
- "view_details": "View Details",
- "action_bar_connection_stats": "Connection Stats",
- "action_bar_exit_fullscreen": "Exit Fullscreen",
- "action_bar_extension": "Extension",
- "action_bar_fullscreen": "Fullscreen",
- "action_bar_paste_text": "Paste text",
- "action_bar_settings": "Settings",
- "action_bar_virtual_keyboard": "Virtual Keyboard",
- "action_bar_virtual_media": "Virtual Media",
- "action_bar_wake_on_lan": "Wake on LAN",
- "action_bar_web_terminal": "Web Terminal",
- "extension_popover_load_and_manage_extensions": "Load and manage your extensions",
- "extension_popover_set_error_notification": "Failed to set active extension: {error}",
- "extension_popover_unload_extension": "Unload Extension",
- "extension_serial_console_description": "Access your serial console extension",
- "extension_serial_console": "Serial Console",
- "extensions_atx_power_control_description": "Control the power state of your machine via ATX power control.",
- "extensions_atx_power_control": "ATX Power Control",
- "extensions_dc_power_control_description": "Control your DC Power extension",
- "extensions_dc_power_control": "DC Power Control",
- "extensions_popover_extensions": "Extensions",
- "atx_power_control_get_state_error": "Failed to get ATX power state: {error}",
- "atx_power_control_hdd_led": "HDD LED",
- "atx_power_control_long_power_button": "Long Press",
- "atx_power_control_power_button": "Power",
- "atx_power_control_power_led": "Power LED",
- "atx_power_control_reset_button": "Reset",
- "atx_power_control_send_action_error": "Failed to send ATX power action {action}: {error}",
- "atx_power_control_short_power_button": "Short Press",
- "dc_power_control_current_unit": "A",
- "dc_power_control_current": "Current",
- "dc_power_control_get_state_error": "Failed to get DC power state: {error}",
- "dc_power_control_power_off_button": "Power Off",
- "dc_power_control_power_off_state": "Power OFF",
- "dc_power_control_power_on_button": "Power On",
- "dc_power_control_power_on_state": "Power ON",
- "dc_power_control_power_unit": "W",
- "dc_power_control_power": "Power",
- "dc_power_control_restore_last_state": "Last State",
- "dc_power_control_restore_power_state": "Restore Power Loss",
- "dc_power_control_set_power_state_error": "Failed to send DC power state to {enabled}: {error}",
- "dc_power_control_set_restore_state_error": "Failed to send DC power restore state to {state}: {error}",
- "dc_power_control_voltage_unit": "V",
- "dc_power_control_voltage": "Voltage",
+ "saving": "Saving…",
+ "search_placeholder": "Search…",
"serial_console_baud_rate": "Baud Rate",
"serial_console_configure_description": "Configure your serial console settings",
"serial_console_data_bits": "Data Bits",
@@ -112,191 +352,24 @@
"serial_console_parity": "Parity",
"serial_console_set_settings_error": "Failed to set serial console settings to {settings}: {error}",
"serial_console_stop_bits": "Stop Bits",
- "wake_on_lan_add_device_back": "Back",
- "wake_on_lan_add_device_device_name": "Device Name",
- "wake_on_lan_add_device_example_device_name": "Plex Media Server",
- "wake_on_lan_add_device_mac_address": "MAC Address",
- "wake_on_lan_add_device_save_device": "Save Device",
- "paste_modal_confirm_paste": "Confirm Paste",
- "paste_modal_delay_between_keys": "Delay between keys",
- "paste_modal_delay_out_of_range": "Delay must be between {min} and {max}",
- "paste_modal_invalid_chars_intro": "The following characters won't be pasted:",
- "paste_modal_paste_from_host": "Paste from host",
- "paste_modal_paste_text_description": "Paste text from your client to the remote host",
- "paste_modal_paste_text": "Paste text",
- "paste_modal_sending_using_layout": "Sending text using keyboard layout: {iso}-{name}",
- "paste_modal_failed_paste": "Failed to paste text: {error}",
- "mount_add_file_to_get_started": "Add a file to get started",
- "mount_add_new_media": "Add New Media",
- "mount_mounted_from_storage": "Mounted from JetKVM Storage",
- "mount_no_mounted_media": "No mounted media",
- "mount_streaming_from_url": "Streaming from URL",
- "mount_unmount": "Unmount",
- "mount_virtual_media_description": "Mount an image to boot from or install an operating system.",
- "mount_virtual_media": "Virtual Media",
- "mount_get_state_error": "Failed to get virtual media state: {error}",
- "mount_mode_cdrom": "CD-ROM",
- "mount_mode_disk": "Disk",
- "mount_mounted_as": "Mounted as",
- "mount_unmount_error": "Failed to unmount image: {error}",
- "wake_on_lan_description": "Send a Magic Packet to wake up a remote device.",
- "wake_on_lan_device_list_add_new_device": "Add New Device",
- "wake_on_lan_device_list_delete_device": "Delete device",
- "wake_on_lan_device_list_wake": "Wake",
- "wake_on_lan_empty_add_device_to_start": "Add a device to start using Wake-on-LAN",
- "wake_on_lan_empty_add_new_device": "Add New Device",
- "wake_on_lan_empty_no_devices_added": "No devices added",
- "wake_on_lan_failed_add_device": "Failed to add device",
- "wake_on_lan_failed_send_magic": "Failed to send Magic Packet",
- "wake_on_lan_invalid_mac": "Invalid MAC address",
- "wake_on_lan_magic_sent_success": "Magic Packet sent successfully",
- "wake_on_lan": "Wake On LAN",
- "connection_stats_badge_jitter_buffer_avg_delay": "Jitter Buffer Avg. Delay",
- "connection_stats_badge_jitter": "Jitter",
- "connection_stats_connection_description": "The connection between the client and the JetKVM.",
- "connection_stats_connection": "Connection",
- "connection_stats_frames_per_second_description": "Number of inbound video frames displayed per second.",
- "connection_stats_frames_per_second": "Frames per second",
- "connection_stats_network_stability_description": "How steady the flow of inbound video packets is across the network.",
- "connection_stats_network_stability": "Network Stability",
- "connection_stats_packets_lost_description": "Count of lost inbound video RTP packets.",
- "connection_stats_packets_lost": "Packets Lost",
- "connection_stats_playback_delay_description": "Delay added by the jitter buffer to smooth playback when frames arrive unevenly.",
- "connection_stats_playback_delay": "Playback Delay",
- "connection_stats_round_trip_time_description": "Round-trip time for the active ICE candidate pair between peers.",
- "connection_stats_round_trip_time": "Round-Trip Time",
- "connection_stats_sidebar": "Connection Stats",
- "connection_stats_video_description": "The video stream from the JetKVM to the client.",
- "connection_stats_video": "Video",
- "peer_connection_connected": "Connected",
- "peer_connection_connecting": "Connecting",
- "peer_connection_disconnected": "Disconnected",
- "peer_connection_error": "Connection error",
- "peer_connection_closing": "Closing",
- "peer_connection_failed": "Connection failed",
- "peer_connection_closed": "Closed",
- "peer_connection_new": "Connecting",
- "usb_device_keyboard_mouse_and_mass_storage": "Keyboard, Mouse and Mass Storage",
- "usb_device_keyboard_only": "Keyboard Only",
- "usb_device_custom": "Custom",
- "usb_device_title": "USB Device",
- "usb_device_description": "USB devices to emulate on the target computer",
- "usb_device_classes_title": "Classes",
- "usb_device_classes_description": "USB device classes in the composite device",
- "usb_device_enable_keyboard_title": "Enable Keyboard",
- "usb_device_enable_keyboard_description": "Enable Keyboard",
- "usb_device_enable_absolute_mouse_title": "Enable Absolute Mouse (Pointer)",
- "usb_device_enable_absolute_mouse_description": "Enable Absolute Mouse (Pointer)",
- "usb_device_enable_relative_mouse_title": "Enable Relative Mouse",
- "usb_device_enable_relative_mouse_description": "Enable Relative Mouse",
- "usb_device_enable_mass_storage_title": "Enable USB Mass Storage",
- "usb_device_enable_mass_storage_description": "Sometimes it might need to be disabled to prevent issues with certain devices",
- "usb_device_update_classes": "Update USB Classes",
- "usb_device_restore_default": "Restore to Default",
- "usb_device_failed_load": "Failed to load USB devices: {error}",
- "usb_device_failed_set": "Failed to set USB devices: {error}",
- "usb_device_updated": "USB Devices updated",
+ "serial_console": "Serial Console",
+ "setting_remote_description": "Setting remote description",
+ "setting_remote_session_description": "Setting remote session description...",
+ "setting_up_connection_to_device": "Setting up connection to device...",
+ "something_went_wrong": "Something went wrong. Please try again later or contact support",
+ "step_counter_step": "Step {step}",
+ "subnet_mask": "Subnet Mask",
+ "troubleshoot_connection": "Troubleshoot Connection",
+ "unknown_error": "Unknown error",
+ "update_in_progress": "Update in Progress",
"updates_failed_check": "Failed to check for updates: {error}",
"updates_failed_get_device_version": "Failed to get device version: {error}",
- "auth_connect_to_cloud_action": "Log in & Connect device",
- "auth_connect_to_cloud_description": "Unlock remote access and advanced features for your device",
- "auth_connect_to_cloud": "Connect your JetKVM to the cloud",
- "auth_signup_connect_to_cloud_action": "Signup & Connect device",
- "auth_login_action": "Log in",
- "auth_login_description": "Log in to access and manage your devices securely",
- "auth_login": "Log in to your JetKVM account",
- "auth_header_cta_already_have_account": "Already have an account?",
- "auth_header_cta_dont_have_account": "Don't have an account?",
- "auth_header_cta_new_to_jetkvm": "New to JetKVM?",
- "auth_signup_create_account_action": "Create Account",
- "auth_signup_create_account_description": "Create your account and start managing your devices with ease.",
- "auth_signup_create_account": "Create your JetKVM account",
- "video_overlay_autoplay_permissions_required": "Autoplay permissions required",
- "video_overlay_conn_check_cables": "Check all cable connections for any loose or damaged wires",
- "video_overlay_conn_ensure_network": "Ensure your network connection is stable and active",
- "video_overlay_conn_restart": "Try restarting both the device and your computer",
- "video_overlay_conn_verify_power": "Verify that the device is powered on and properly connected",
- "video_overlay_connection_issue_title": "Connection Issue Detected",
- "video_overlay_enable_autoplay_settings": "Please adjust browser settings to enable autoplay",
- "video_overlay_hdmi_error_title": "HDMI signal error detected.",
- "video_overlay_hdmi_incompatible_resolution": "Incompatible resolution or refresh rate settings",
- "video_overlay_hdmi_loose_faulty": "A loose or faulty HDMI connection",
- "video_overlay_hdmi_source_issue": "Issues with the source device's HDMI output",
- "video_overlay_learn_more": "Learn more",
- "video_overlay_loading_stream": "Loading video stream...",
- "video_overlay_manually_start_stream": "Manually start stream",
- "video_overlay_no_hdmi_adapter_compat": "If using an adapter, ensure it's compatible and functioning correctly",
- "video_overlay_no_hdmi_ensure_cable": "Ensure the HDMI cable securely connected at both ends",
- "video_overlay_no_hdmi_ensure_power": "Ensure source device is powered on and outputting a signal",
- "video_overlay_no_hdmi_signal": "No HDMI signal detected.",
- "video_overlay_pointerlock_click_to_enable": "Click on the video to enable mouse control",
- "video_overlay_retrying_connection": "Retrying connection...",
- "video_overlay_troubleshooting_guide": "Troubleshooting Guide",
- "video_overlay_try_again": "Try again",
- "video_pointer_lock_enabled": "Pointer lock enabled — press Escape to unlock",
- "video_pointer_lock_disabled": "Pointer lock disabled",
- "ipv6_information": "IPv6 Information",
- "ipv6_link_local": "Link-local",
- "ipv6_addresses": "IPv6 Addresses",
- "ipv6_address_label": "Address",
- "ipv6_valid_lifetime": "Valid Lifetime",
- "ipv6_preferred_lifetime": "Preferred Lifetime",
- "dhcp_lease_boot_file": "Boot File",
- "dhcp_lease_boot_next_server": "Boot Next Server",
- "dhcp_lease_boot_server_name": "Boot Server Name",
- "dhcp_lease_broadcast": "Broadcast",
- "dhcp_lease_domain": "Domain",
- "dhcp_lease_gateway": "Gateway",
- "dhcp_lease_header": "DHCP Lease Information",
- "dhcp_lease_hostname": "Hostname",
- "dhcp_lease_lease_expires": "Lease Expires",
- "dhcp_lease_maximum_transfer_unit": "MTU",
- "dhcp_lease_renew": "Renew DHCP Lease",
- "dhcp_lease_time_to_live": "TTL",
- "step_counter_step": "Step {step}",
- "macro_add_step": "Add Step{maxed_out}",
- "macro_at_least_one_step_keys_or_modifiers": "At least one step must have keys or modifiers",
- "macro_at_least_one_step_required": "At least one step is required",
- "macro_max_steps_error": "You can only add a maximum of {max} steps per macro.",
- "macro_max_steps_reached": "({max} max)",
- "macro_name_label": "Macro Name",
- "macro_name_required": "Name is required",
- "macro_name_too_long": "Name must be less than 50 characters",
- "macro_please_fix_validation_errors": "Please fix the validation errors",
- "macro_save": "Save Macro",
- "macro_save_error": "An error occurred while saving.",
- "macro_step_count": "{steps} / {max} steps",
- "macro_steps_description": "Keys/modifiers executed in sequence with a delay between each step.",
- "macro_steps_label": "Steps",
- "macro_step_duration_description": "Time to wait before executing the next step.",
- "macro_step_duration_label": "Step Duration",
- "macro_step_keys_description": "Maximum {max} keys per step.",
- "macro_step_keys_label": "Keys",
- "macro_step_max_keys_reached": "Maximum keys reached",
- "macro_step_modifiers_description": "What modifiers (Shift/Ctrl/Alt/Meta) are pressed during this step.",
- "macro_step_modifiers_label": "Modifiers",
- "macro_step_no_matching_keys_found": "No matching keys found",
- "macro_step_search_for_key": "Search for key...",
- "jiggler_examples_label": "Examples",
- "jiggler_more_examples": "More examples",
- "jiggler_cron_schedule_label": "Cron Schedule",
- "jiggler_cron_schedule_description": "Cron expression for scheduling",
- "jiggler_example_business_hours_late": "Business Hours 9-17",
- "jiggler_example_business_hours_early": "Business Hours 8-17",
- "jiggler_inactivity_limit_label": "Inactivity Limit Seconds",
- "jiggler_inactivity_limit_description": "Inactivity time before jiggle",
- "jiggler_random_delay_label": "Random delay",
- "jiggler_random_delay_description": "To avoid recognizable patterns",
- "jiggler_timezone_label": "Timezone",
- "jiggler_timezone_description": "Timezone for cron schedule",
- "jiggler_save_jiggler_config": "Save Jiggler Config",
- "metric_waiting_for_data": "Waiting for data...",
- "metric_not_supported": "Metric not supported",
+ "updating_leave_device_on": "Please don't turn off your device…",
"usb_config_custom": "Custom",
"usb_config_default": "JetKVM Default",
"usb_config_dell": "Dell Multimedia Pro Keyboard",
"usb_config_failed_load": "Failed to load USB Config: {error}",
- "usb_config_failed_set": "Failed to set usb config: {error}",
+ "usb_config_failed_set": "Failed to set USB config: {error}",
"usb_config_identifiers_description": "USB device identifiers exposed to the target computer",
"usb_config_identifiers_title": "Identifiers",
"usb_config_logitech": "Logitech Universal Adapter",
@@ -314,10 +387,75 @@
"usb_config_update_identifiers": "Update USB Identifiers",
"usb_config_vendor_id_label": "Vendor ID",
"usb_config_vendor_id_placeholder": "Enter Vendor ID",
+ "usb_device_classes_description": "USB device classes in the composite device",
+ "usb_device_classes_title": "Classes",
+ "usb_device_custom": "Custom",
+ "usb_device_description": "USB devices to emulate on the target computer",
+ "usb_device_enable_absolute_mouse_description": "Enable Absolute Mouse (Pointer)",
+ "usb_device_enable_absolute_mouse_title": "Enable Absolute Mouse (Pointer)",
+ "usb_device_enable_keyboard_description": "Enable Keyboard",
+ "usb_device_enable_keyboard_title": "Enable Keyboard",
+ "usb_device_enable_mass_storage_description": "Sometimes it might need to be disabled to prevent issues with certain devices",
+ "usb_device_enable_mass_storage_title": "Enable USB Mass Storage",
+ "usb_device_enable_relative_mouse_description": "Enable Relative Mouse",
+ "usb_device_enable_relative_mouse_title": "Enable Relative Mouse",
+ "usb_device_failed_load": "Failed to load USB devices: {error}",
+ "usb_device_failed_set": "Failed to set USB devices: {error}",
+ "usb_device_keyboard_mouse_and_mass_storage": "Keyboard, Mouse and Mass Storage",
+ "usb_device_keyboard_only": "Keyboard Only",
+ "usb_device_restore_default": "Restore to Default",
+ "usb_device_title": "USB Device",
+ "usb_device_update_classes": "Update USB Classes",
+ "usb_device_updated": "USB Devices updated",
"usb_state_connected": "Connected",
"usb_state_connecting": "Connecting",
"usb_state_disconnected": "Disconnected",
"usb_state_low_power_mode": "Low power mode",
+ "usb": "USB",
+ "video_overlay_autoplay_permissions_required": "Autoplay permissions required",
+ "video_overlay_conn_check_cables": "Check all cable connections for any loose or damaged wires",
+ "video_overlay_conn_ensure_network": "Ensure your network connection is stable and active",
+ "video_overlay_conn_restart": "Try restarting both the device and your computer",
+ "video_overlay_conn_verify_power": "Verify that the device is powered on and properly connected",
+ "video_overlay_connection_issue_title": "Connection Issue Detected",
+ "video_overlay_enable_autoplay_settings": "Please adjust browser settings to enable autoplay",
+ "video_overlay_hdmi_error_title": "HDMI signal error detected.",
+ "video_overlay_hdmi_incompatible_resolution": "Incompatible resolution or refresh rate settings",
+ "video_overlay_hdmi_loose_faulty": "A loose or faulty HDMI connection",
+ "video_overlay_hdmi_source_issue": "Issues with the source device's HDMI output",
+ "video_overlay_learn_more": "Learn more",
+ "video_overlay_loading_stream": "Loading video stream…",
+ "video_overlay_manually_start_stream": "Manually start stream",
+ "video_overlay_no_hdmi_adapter_compat": "If using an adapter, ensure it's compatible and functioning correctly",
+ "video_overlay_no_hdmi_ensure_cable": "Ensure the HDMI cable securely connected at both ends",
+ "video_overlay_no_hdmi_ensure_power": "Ensure source device is powered on and outputting a signal",
+ "video_overlay_no_hdmi_signal": "No HDMI signal detected.",
+ "video_overlay_pointerlock_click_to_enable": "Click on the video to enable mouse control",
+ "video_overlay_retrying_connection": "Retrying connection…",
+ "video_overlay_troubleshooting_guide": "Troubleshooting Guide",
+ "video_overlay_try_again": "Try again",
+ "video_pointer_lock_disabled": "Pointer lock disabled",
+ "video_pointer_lock_enabled": "Pointer lock enabled — press Escape to unlock",
+ "view_details": "View Details",
+ "virtual_keyboard_description": "Use the virtual keyboard to send special keys or key combinations to the remote computer.",
"virtual_keyboard_header": "Virtual Keyboard",
- "virtual_keyboard_description": "Use the virtual keyboard to send special keys or key combinations to the remote computer."
+ "wake_on_lan_add_device_back": "Back",
+ "wake_on_lan_add_device_device_name": "Device Name",
+ "wake_on_lan_add_device_example_device_name": "Plex Media Server",
+ "wake_on_lan_add_device_mac_address": "MAC Address",
+ "wake_on_lan_add_device_save_device": "Save Device",
+ "wake_on_lan_description": "Send a Magic Packet to wake up a remote device.",
+ "wake_on_lan_device_list_add_new_device": "Add New Device",
+ "wake_on_lan_device_list_delete_device": "Delete device",
+ "wake_on_lan_device_list_wake": "Wake",
+ "wake_on_lan_empty_add_device_to_start": "Add a device to start using Wake-on-LAN",
+ "wake_on_lan_empty_add_new_device": "Add New Device",
+ "wake_on_lan_empty_no_devices_added": "No devices added",
+ "wake_on_lan_failed_add_device": "Failed to add device",
+ "wake_on_lan_failed_send_magic": "Failed to send Magic Packet",
+ "wake_on_lan_invalid_mac": "Invalid MAC address",
+ "wake_on_lan_magic_sent_success": "Magic Packet sent successfully",
+ "wake_on_lan": "Wake On LAN",
+ "welcome_to_jetkvm_description": "Control any computer remotely",
+ "welcome_to_jetkvm": "Welcome to JetKVM"
}
\ No newline at end of file
diff --git a/ui/package-lock.json b/ui/package-lock.json
index b900f0a8..677f894b 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "kvm-ui",
- "version": "2025.10.07.1700",
+ "version": "2025.10.09.0200",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "kvm-ui",
- "version": "2025.10.07.1700",
+ "version": "2025.10.09.0200",
"dependencies": {
"@headlessui/react": "^2.2.9",
"@headlessui/tailwindcss": "^0.2.2",
@@ -30,8 +30,8 @@
"react-dom": "^19.2.0",
"react-hot-toast": "^2.6.0",
"react-icons": "^5.5.0",
- "react-router": "^7.9.3",
- "react-simple-keyboard": "^3.8.126",
+ "react-router": "^7.9.4",
+ "react-simple-keyboard": "^3.8.127",
"react-use-websocket": "^4.13.0",
"react-xtermjs": "^1.0.10",
"recharts": "^3.2.1",
@@ -65,7 +65,7 @@
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-react": "^7.37.5",
- "eslint-plugin-react-hooks": "^6.1.1",
+ "eslint-plugin-react-hooks": "^7.0.0",
"eslint-plugin-react-refresh": "^0.4.23",
"globals": "^16.4.0",
"postcss": "^8.5.6",
@@ -3065,9 +3065,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
- "version": "2.8.13",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.13.tgz",
- "integrity": "sha512-7s16KR8io8nIBWQyCYhmFhd+ebIzb9VKTzki+wOJXHTxTnV6+mFGH3+Jwn1zoKaY9/H9T/0BcKCZnzXljPnpSQ==",
+ "version": "2.8.14",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.14.tgz",
+ "integrity": "sha512-GM9c0cWWR8Ga7//Ves/9KRgTS8nLausCkP3CGiFLrnwA2CDUluXgaQqvrULoR2Ujrd/mz/lkX87F5BHFsNr5sQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -3188,9 +3188,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001748",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001748.tgz",
- "integrity": "sha512-5P5UgAr0+aBmNiplks08JLw+AW/XG/SurlgZLgB1dDLfAw7EfRGxIwzPHxdSCGY/BTKDqIVyJL87cCN6s0ZR0w==",
+ "version": "1.0.30001749",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001749.tgz",
+ "integrity": "sha512-0rw2fJOmLfnzCRbkm8EyHL8SvI2Apu5UbnQuTsJ0ClgrH8hcwFooJ1s5R0EP8o8aVrFu8++ae29Kt9/gZAZp/Q==",
"dev": true,
"funding": [
{
@@ -3671,9 +3671,9 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.5.232",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.232.tgz",
- "integrity": "sha512-ENirSe7wf8WzyPCibqKUG1Cg43cPaxH4wRR7AJsX7MCABCHBIOFqvaYODSLKUuZdraxUTHRE/0A2Aq8BYKEHOg==",
+ "version": "1.5.233",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.233.tgz",
+ "integrity": "sha512-iUdTQSf7EFXsDdQsp8MwJz5SVk4APEFqXU/S47OtQ0YLqacSwPXdZ5vRlMX3neb07Cy2vgioNuRnWUXFwuslkg==",
"dev": true,
"license": "ISC"
},
@@ -3862,9 +3862,9 @@
}
},
"node_modules/es-toolkit": {
- "version": "1.39.10",
- "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.10.tgz",
- "integrity": "sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w==",
+ "version": "1.40.0",
+ "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.40.0.tgz",
+ "integrity": "sha512-8o6w0KFmU0CiIl0/Q/BCEOabF2IJaELM1T2PWj6e8KqzHv1gdx+7JtFnDwOx1kJH/isJ5NwlDG1nCr1HrRF94Q==",
"license": "MIT",
"workspaces": [
"docs",
@@ -4166,14 +4166,15 @@
}
},
"node_modules/eslint-plugin-react-hooks": {
- "version": "6.1.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-6.1.1.tgz",
- "integrity": "sha512-St9EKZzOAQF704nt2oJvAKZHjhrpg25ClQoaAlHmPZuajFldVLqRDW4VBNAS01NzeiQF0m0qhG1ZA807K6aVaQ==",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.0.tgz",
+ "integrity": "sha512-fNXaOwvKwq2+pXiRpXc825Vd63+KM4DLL40Rtlycb8m7fYpp6efrTp1sa6ZbP/Ap58K2bEKFXRmhURE+CJAQWw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.24.4",
"@babel/parser": "^7.24.4",
+ "hermes-parser": "^0.25.1",
"zod": "^3.22.4 || ^4.0.0",
"zod-validation-error": "^3.0.3 || ^4.0.0"
},
@@ -4860,6 +4861,23 @@
"node": ">= 0.4"
}
},
+ "node_modules/hermes-estree": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
+ "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/hermes-parser": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
+ "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hermes-estree": "0.25.1"
+ }
+ },
"node_modules/human-id": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/human-id/-/human-id-4.1.2.tgz",
@@ -6458,9 +6476,9 @@
}
},
"node_modules/react-router": {
- "version": "7.9.3",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.3.tgz",
- "integrity": "sha512-4o2iWCFIwhI/eYAIL43+cjORXYn/aRQPgtFRRZb3VzoyQ5Uej0Bmqj7437L97N9NJW4wnicSwLOLS+yCXfAPgg==",
+ "version": "7.9.4",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz",
+ "integrity": "sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
@@ -6480,9 +6498,9 @@
}
},
"node_modules/react-simple-keyboard": {
- "version": "3.8.126",
- "resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.126.tgz",
- "integrity": "sha512-eULRTm9Rrvo72+WnU9h3fTooSLMgJ52Yi5VvVm0SirMKQnigRgOlcx1VJeVdFVN4JfJEuSN1voV+Vsmeheg3vA==",
+ "version": "3.8.127",
+ "resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.127.tgz",
+ "integrity": "sha512-CncdXLnJ3tBlB6iEHtkgj5W21ns/DdKKO1bCy9pLWey5xONf+KAComVVnDsnAaC0b4LLI7frWBDjOT01vj8dew==",
"license": "MIT",
"peerDependencies": {
"react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
@@ -6758,9 +6776,9 @@
"license": "MIT"
},
"node_modules/semver": {
- "version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"license": "ISC",
"bin": {
diff --git a/ui/package.json b/ui/package.json
index 1b11d65f..1ec23a71 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -1,7 +1,7 @@
{
"name": "kvm-ui",
"private": true,
- "version": "2025.10.07.1700",
+ "version": "2025.10.09.0200",
"type": "module",
"engines": {
"node": "^22.15.0"
@@ -43,8 +43,8 @@
"react-dom": "^19.2.0",
"react-hot-toast": "^2.6.0",
"react-icons": "^5.5.0",
- "react-router": "^7.9.3",
- "react-simple-keyboard": "^3.8.126",
+ "react-router": "^7.9.4",
+ "react-simple-keyboard": "^3.8.127",
"react-use-websocket": "^4.13.0",
"react-xtermjs": "^1.0.10",
"recharts": "^3.2.1",
@@ -75,10 +75,10 @@
"@vitejs/plugin-react-swc": "^4.1.0",
"autoprefixer": "^10.4.21",
"eslint": "^9.37.0",
- "eslint-config-prettier": "^10.1.8",
+ "eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-react": "^7.37.5",
- "eslint-plugin-react-hooks": "^6.1.1",
+ "eslint-plugin-react-hooks": "^7.0.0",
"eslint-plugin-react-refresh": "^0.4.23",
"globals": "^16.4.0",
"postcss": "^8.5.6",
diff --git a/ui/src/components/ActionBar.tsx b/ui/src/components/ActionBar.tsx
index 1c34a6b8..456a8385 100644
--- a/ui/src/components/ActionBar.tsx
+++ b/ui/src/components/ActionBar.tsx
@@ -4,8 +4,8 @@ import { LuCable, LuHardDrive, LuMaximize, LuSettings, LuSignal } from "react-ic
import { FaKeyboard } from "react-icons/fa6";
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
import { CommandLineIcon } from "@heroicons/react/20/solid";
-import { cx } from "@/cva.config";
+import { cx } from "@/cva.config";
import {
useHidStore,
useMountMediaStore,
diff --git a/ui/src/components/Button.tsx b/ui/src/components/Button.tsx
index eba9a964..fcb0a614 100644
--- a/ui/src/components/Button.tsx
+++ b/ui/src/components/Button.tsx
@@ -1,7 +1,7 @@
import React, { JSX } from "react";
import { Link, type FetcherWithComponents, type LinkProps, useNavigation } from "react-router";
-import { cva, cx } from "@/cva.config";
+import { cva, cx } from "@/cva.config";
import ExtLink from "@components/ExtLink";
import LoadingSpinner from "@components/LoadingSpinner";
diff --git a/ui/src/components/Header.tsx b/ui/src/components/Header.tsx
index 2d9dd492..22e456e2 100644
--- a/ui/src/components/Header.tsx
+++ b/ui/src/components/Header.tsx
@@ -3,9 +3,9 @@ import { useNavigate } from "react-router";
import { ArrowLeftEndOnRectangleIcon, ChevronDownIcon } from "@heroicons/react/16/solid";
import { Button, Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
import { LuMonitorSmartphone } from "react-icons/lu";
+
import LogoBlueIcon from "@assets/logo-blue.svg";
import LogoWhiteIcon from "@assets/logo-white.svg";
-
import { useHidStore, useRTCStore, useUserStore } from "@hooks/stores";
import Card from "@components/Card";
import Container from "@components/Container";
diff --git a/ui/src/components/InfoBar.tsx b/ui/src/components/InfoBar.tsx
index 6d5db383..2c4eb9e0 100644
--- a/ui/src/components/InfoBar.tsx
+++ b/ui/src/components/InfoBar.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useMemo } from "react";
+import { useMemo } from "react";
import {
useHidStore,
@@ -26,17 +26,8 @@ export default function InfoBar() {
(state: VideoState) => `${Math.round(state.width)}x${Math.round(state.height)}`,
);
- const { rpcDataChannel } = useRTCStore();
const { debugMode, mouseMode, showPressedKeys } = useSettingsStore();
const { isPasteInProgress } = useHidStore();
-
- useEffect(() => {
- if (!rpcDataChannel) return;
- rpcDataChannel.onclose = () => console.log("rpcDataChannel has closed");
- rpcDataChannel.onerror = (e: Event) =>
- console.error(`Error on DataChannel '${rpcDataChannel.label}': ${e}`);
- }, [rpcDataChannel]);
-
const { keyboardLedState, usbState } = useHidStore();
const { isTurnServerInUse } = useRTCStore();
const { hdmiState } = useVideoStore();
diff --git a/ui/src/components/Terminal.tsx b/ui/src/components/Terminal.tsx
index 3ba09bfa..7e0d3673 100644
--- a/ui/src/components/Terminal.tsx
+++ b/ui/src/components/Terminal.tsx
@@ -7,8 +7,8 @@ import { WebLinksAddon } from "@xterm/addon-web-links";
import { WebglAddon } from "@xterm/addon-webgl";
import { Unicode11Addon } from "@xterm/addon-unicode11";
import { ClipboardAddon } from "@xterm/addon-clipboard";
-import { cx } from "@/cva.config";
+import { cx } from "@/cva.config";
import { AvailableTerminalTypes, useUiStore } from "@hooks/stores";
import { Button } from "@components/Button";
import { m } from "@localizations/messages.js";
@@ -54,6 +54,7 @@ const TERMINAL_CONFIG = {
// Add these configurations:
cursorStyle: "block",
rendererType: "canvas", // Ensure we're using the canvas renderer
+ unicode: { activeVersion: "11" }
} as const;
function Terminal({
@@ -144,7 +145,6 @@ function Terminal({
instance.loadAddon(new ClipboardAddon());
instance.loadAddon(new Unicode11Addon());
instance.loadAddon(new WebLinksAddon());
- instance.unicode.activeVersion = "11";
if (isWebGl2Supported) {
const webGl2Addon = new WebglAddon();
diff --git a/ui/src/components/USBStateStatus.tsx b/ui/src/components/USBStateStatus.tsx
index e93e26ec..1f7977fe 100644
--- a/ui/src/components/USBStateStatus.tsx
+++ b/ui/src/components/USBStateStatus.tsx
@@ -2,7 +2,6 @@ import React from "react";
import { cx } from "@/cva.config";
import KeyboardAndMouseConnectedIcon from "@assets/keyboard-and-mouse-connected.png";
-
import { USBStates } from "@hooks/stores";
import { m } from "@localizations/messages.js";
import LoadingSpinner from "@components/LoadingSpinner";
diff --git a/ui/src/components/UpdateInProgressStatusCard.tsx b/ui/src/components/UpdateInProgressStatusCard.tsx
index 25db23e7..e29aecff 100644
--- a/ui/src/components/UpdateInProgressStatusCard.tsx
+++ b/ui/src/components/UpdateInProgressStatusCard.tsx
@@ -1,5 +1,4 @@
import { cx } from "@/cva.config";
-
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
import { Button } from "@components/Button";
import { GridCard } from "@components/Card";
diff --git a/ui/src/components/VirtualKeyboard.tsx b/ui/src/components/VirtualKeyboard.tsx
index 37fdd134..f48352fe 100644
--- a/ui/src/components/VirtualKeyboard.tsx
+++ b/ui/src/components/VirtualKeyboard.tsx
@@ -3,10 +3,10 @@ import { ChevronDownIcon } from "@heroicons/react/16/solid";
import { AnimatePresence, motion } from "framer-motion";
import Keyboard from "react-simple-keyboard";
import { LuKeyboard } from "react-icons/lu";
+
import "react-simple-keyboard/build/css/index.css";
import DetachIconRaw from "@/assets/detach-icon.svg";
import { cx } from "@/cva.config";
-
import { useHidStore, useUiStore } from "@hooks/stores";
import useKeyboard from "@hooks/useKeyboard";
import useKeyboardLayout from "@hooks/useKeyboardLayout";
diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx
index c121ba3f..a14e61db 100644
--- a/ui/src/components/WebRTCVideo.tsx
+++ b/ui/src/components/WebRTCVideo.tsx
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useResizeObserver } from "usehooks-ts";
-import { cx } from "@/cva.config";
+import { cx } from "@/cva.config";
import useKeyboard from "@hooks/useKeyboard";
import useMouse from "@hooks/useMouse";
import {
@@ -234,6 +234,18 @@ export default function WebRTCVideo() {
[getMouseWheelHandler],
);
+ function getAdjustedKeyCode(e: KeyboardEvent) {
+ const key = e.key;
+ let code = e.code;
+
+ if (code == "IntlBackslash" && ["`", "~"].includes(key)) {
+ code = "Backquote";
+ } else if (code == "Backquote" && ["§", "±"].includes(key)) {
+ code = "IntlBackslash";
+ }
+ return code;
+ }
+
const keyDownHandler = useCallback(
(e: KeyboardEvent) => {
e.preventDefault();
@@ -468,17 +480,6 @@ export default function WebRTCVideo() {
};
}, [videoSaturation, videoBrightness, videoContrast]);
- function getAdjustedKeyCode(e: KeyboardEvent) {
- const key = e.key;
- let code = e.code;
-
- if (code == "IntlBackslash" && ["`", "~"].includes(key)) {
- code = "Backquote";
- } else if (code == "Backquote" && ["§", "±"].includes(key)) {
- code = "IntlBackslash";
- }
- return code;
- }
return (
diff --git a/ui/src/hooks/useHidRpc.ts b/ui/src/hooks/useHidRpc.ts
index 7fb57c20..3c08d6d6 100644
--- a/ui/src/hooks/useHidRpc.ts
+++ b/ui/src/hooks/useHidRpc.ts
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo } from "react";
-import { useRTCStore } from "@/hooks/stores";
+import { useRTCStore } from "@hooks/stores";
import {
CancelKeyboardMacroReportMessage,
diff --git a/ui/src/hooks/useJsonRpc.ts b/ui/src/hooks/useJsonRpc.ts
index a77c9ce8..2ff862b9 100644
--- a/ui/src/hooks/useJsonRpc.ts
+++ b/ui/src/hooks/useJsonRpc.ts
@@ -1,6 +1,6 @@
import { useCallback, useEffect } from "react";
-import { useRTCStore } from "@/hooks/stores";
+import { useRTCStore } from "@hooks/stores";
export interface JsonRpcRequest {
jsonrpc: string;
diff --git a/ui/src/hooks/useKeyboard.ts b/ui/src/hooks/useKeyboard.ts
index 57e061e2..c29eeb1b 100644
--- a/ui/src/hooks/useKeyboard.ts
+++ b/ui/src/hooks/useKeyboard.ts
@@ -149,67 +149,7 @@ export default function useKeyboard() {
}
}, [rpcHidReady, sendKeyboardEventHidRpc, handleLegacyKeyboardReport, cancelKeepAlive]);
- // handleKeyPress is used to handle a key press or release event.
- // This function handle both key press and key release events.
- // It checks if the keyPressReport API is available and sends the key press event.
- // If the keyPressReport API is not available, it simulates the device-side key
- // handling for legacy devices and updates the keysDownState accordingly.
- // It then sends the full keyboard state to the device.
-
- const sendKeypress = useCallback(
- (key: number, press: boolean) => {
- cancelKeepAlive();
-
- sendKeypressEventHidRpc(key, press);
-
- if (press) {
- scheduleKeepAlive();
- }
- },
- [sendKeypressEventHidRpc, scheduleKeepAlive, cancelKeepAlive],
- );
-
- const handleKeyPress = useCallback(
- async (key: number, press: boolean) => {
- if (rpcDataChannel?.readyState !== "open" && !rpcHidReady) return;
- if ((key || 0) === 0) return; // ignore zero key presses (they are bad mappings)
-
- if (rpcHidReady) {
- // if the keyPress api is available, we can just send the key press event
- // sendKeypressEvent is used to send a single key press/release event to the device.
- // It sends the key and whether it is pressed or released.
- // Older device version doesn't support this API, so we will switch to local key handling
- // In that case we will switch to local key handling and update the keysDownState
- // in client/browser-side code using simulateDeviceSideKeyHandlingForLegacyDevices.
- sendKeypress(key, press);
- } else {
- // Older backends don't support the hidRpc API, so we need:
- // 1. Calculate the state
- // 2. Send the newly calculated state to the device
- const downState = simulateDeviceSideKeyHandlingForLegacyDevices(
- keysDownState,
- key,
- press,
- );
-
- handleLegacyKeyboardReport(downState.keys, downState.modifier);
-
- // if we just sent ErrorRollOver, reset to empty state
- if (downState.keys[0] === hidErrorRollOver) {
- resetKeyboardState();
- }
- }
- },
- [
- rpcDataChannel?.readyState,
- rpcHidReady,
- keysDownState,
- handleLegacyKeyboardReport,
- resetKeyboardState,
- sendKeypress,
- ],
- );
-
+
// IMPORTANT: See the keyPressReportApiAvailable comment above for the reason this exists
function simulateDeviceSideKeyHandlingForLegacyDevices(
state: KeysDownState,
@@ -273,6 +213,66 @@ export default function useKeyboard() {
return { modifier: modifiers, keys };
}
+ const sendKeypress = useCallback(
+ (key: number, press: boolean) => {
+ cancelKeepAlive();
+
+ sendKeypressEventHidRpc(key, press);
+
+ if (press) {
+ scheduleKeepAlive();
+ }
+ },
+ [sendKeypressEventHidRpc, scheduleKeepAlive, cancelKeepAlive],
+ );
+
+ // handleKeyPress is used to handle a key press or release event.
+ // This function handle both key press and key release events.
+ // It checks if the keyPressReport API is available and sends the key press event.
+ // If the keyPressReport API is not available, it simulates the device-side key
+ // handling for legacy devices and updates the keysDownState accordingly.
+ // It then sends the full keyboard state to the device.
+ const handleKeyPress = useCallback(
+ async (key: number, press: boolean) => {
+ if (rpcDataChannel?.readyState !== "open" && !rpcHidReady) return;
+ if ((key || 0) === 0) return; // ignore zero key presses (they are bad mappings)
+
+ if (rpcHidReady) {
+ // if the keyPress api is available, we can just send the key press event
+ // sendKeypressEvent is used to send a single key press/release event to the device.
+ // It sends the key and whether it is pressed or released.
+ // Older device version doesn't support this API, so we will switch to local key handling
+ // In that case we will switch to local key handling and update the keysDownState
+ // in client/browser-side code using simulateDeviceSideKeyHandlingForLegacyDevices.
+ sendKeypress(key, press);
+ } else {
+ // Older backends don't support the hidRpc API, so we need:
+ // 1. Calculate the state
+ // 2. Send the newly calculated state to the device
+ const downState = simulateDeviceSideKeyHandlingForLegacyDevices(
+ keysDownState,
+ key,
+ press,
+ );
+
+ handleLegacyKeyboardReport(downState.keys, downState.modifier);
+
+ // if we just sent ErrorRollOver, reset to empty state
+ if (downState.keys[0] === hidErrorRollOver) {
+ resetKeyboardState();
+ }
+ }
+ },
+ [
+ rpcDataChannel?.readyState,
+ rpcHidReady,
+ keysDownState,
+ handleLegacyKeyboardReport,
+ resetKeyboardState,
+ sendKeypress,
+ ],
+ );
+
// Cleanup function to cancel keepalive timer
const cleanup = useCallback(() => {
cancelKeepAlive();
diff --git a/ui/src/main.tsx b/ui/src/main.tsx
index 5479eef0..378a8d91 100644
--- a/ui/src/main.tsx
+++ b/ui/src/main.tsx
@@ -117,7 +117,7 @@ if (isOnDevice) {
path: "/",
errorElement: ,
element: ,
- HydrateFallback: () =>
diff --git a/ui/src/notifications.tsx b/ui/src/notifications.tsx
index 257cff14..c635e3df 100644
--- a/ui/src/notifications.tsx
+++ b/ui/src/notifications.tsx
@@ -1,8 +1,8 @@
-import toast, { Toast, Toaster, useToasterStore } from "react-hot-toast";
import React, { useEffect } from "react";
+import toast, { Toast, Toaster, useToasterStore } from "react-hot-toast";
import { CheckCircleIcon, XCircleIcon } from "@heroicons/react/20/solid";
-import Card from "@/components/Card";
+import Card from "@components/Card";
interface NotificationOptions {
duration?: number;
diff --git a/ui/src/routes/devices.$id.deregister.tsx b/ui/src/routes/devices.$id.deregister.tsx
index e5dd2a35..2ae40783 100644
--- a/ui/src/routes/devices.$id.deregister.tsx
+++ b/ui/src/routes/devices.$id.deregister.tsx
@@ -2,14 +2,15 @@ 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";
import DashboardNavbar from "@components/Header";
-import { User } from "@/hooks/stores";
-import { checkAuth } from "@/main";
import Fieldset from "@components/Fieldset";
+import { checkAuth } from "@/main";
import { CLOUD_API } from "@/ui.config";
+import { m } from "@localizations/messages.js";
interface LoaderData {
device: { id: string; name: string; user: { googleId: string } };
@@ -28,11 +29,12 @@ const action: ActionFunction = async ({ request }: ActionFunctionArgs) => {
});
if (!res.ok) {
- return { message: "There was an error deregistering your device. Please try again." };
+ return { message: m.deregister_error({ status: res.statusText }) };
}
} catch (e) {
console.error(e);
- return { message: "There was an error deregistering your device. Please try again." };
+ const message = e instanceof Error ? e.message : String(e);
+ return { message: m.deregister_error({ status: message }) };
}
return redirect("/devices");
@@ -68,7 +70,7 @@ export default function DevicesIdDeregister() {
- This will remove the device from your cloud account and revoke
- remote access to it.
-
- Please note that local access will still be possible
- >
- }
+ headline={m.deregister_headline({ device: device.name || device.id })}
+ description={m.deregister_description()}
/>
}
@@ -163,9 +163,7 @@ export default function WelcomeLocalPasswordRoute() {
className="animate-fadeIn max-w-md text-center text-xs text-slate-500 opacity-0 dark:text-slate-400"
style={{ animationDelay: "800ms" }}
>
- This password will be used to secure your device data and protect against
- unauthorized access.{" "}
- All data remains on your local device.
+ {m.auth_mode_local_password_note()} {m.auth_mode_local_password_note_local()}
diff --git a/ui/src/routes/welcome-local.tsx b/ui/src/routes/welcome-local.tsx
index 337391f8..29cfd333 100644
--- a/ui/src/routes/welcome-local.tsx
+++ b/ui/src/routes/welcome-local.tsx
@@ -3,15 +3,15 @@ import { redirect } from "react-router";
import type { LoaderFunction } from "react-router";
import { cx } from "cva";
-import api from "@/api";
-import { DEVICE_API } from "@/ui.config";
-import GridBackground from "@components/GridBackground";
-import Container from "@components/Container";
-import { LinkButton } from "@components/Button";
import LogoBlueIcon from "@assets/logo-blue.png";
import LogoWhiteIcon from "@assets/logo-white.svg";
import DeviceImage from "@assets/jetkvm-device-still.png";
import LogoMark from "@assets/logo-mark.png";
+import Container from "@components/Container";
+import GridBackground from "@components/GridBackground";
+import { LinkButton } from "@components/Button";
+import api from "@/api";
+import { DEVICE_API } from "@/ui.config";
import { m } from "@localizations/messages.js";
export interface DeviceStatus {
@@ -54,17 +54,17 @@ export default function WelcomeRoute() {
/>
- Welcome to JetKVM
+ {m.welcome_to_jetkvm()}
- Control any computer remotely
+ {m.welcome_to_jetkvm_description()}
@@ -72,7 +72,7 @@ export default function WelcomeRoute() {
@@ -82,14 +82,13 @@ export default function WelcomeRoute() {
style={{ animationDelay: "2000ms" }}
className="animate-fadeIn mx-auto max-w-lg text-lg text-slate-700 opacity-0 dark:text-slate-300"
>
- JetKVM combines powerful hardware with intuitive software to provide a
- seamless remote control experience.
+ {m.jetkvm_description()}