Localize the client/browser UI with inlang paraglide-js (#864)

This commit is contained in:
Marc Brooks 2025-10-23 07:27:29 -05:00 committed by GitHub
parent 2444817455
commit 9a4d061034
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
138 changed files with 12635 additions and 3082 deletions

View File

@ -4,7 +4,7 @@
"features": { "features": {
"ghcr.io/devcontainers/features/node:1": { "ghcr.io/devcontainers/features/node:1": {
// Should match what is defined in ui/package.json // Should match what is defined in ui/package.json
"version": "22.19.0" "version": "22.20.0"
} }
}, },
"mounts": [ "mounts": [
@ -31,7 +31,10 @@
// Frontend // Frontend
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"bradlc.vscode-tailwindcss" "bradlc.vscode-tailwindcss",
"codeandstuff.package-json-upgrade",
// Localization
"inlang.vs-code-extension"
] ]
} }
} }

View File

@ -4,7 +4,7 @@
"features": { "features": {
"ghcr.io/devcontainers/features/node:1": { "ghcr.io/devcontainers/features/node:1": {
// Should match what is defined in ui/package.json // Should match what is defined in ui/package.json
"version": "22.19.0" "version": "22.20.0"
} }
}, },
"runArgs": [ "runArgs": [
@ -15,5 +15,36 @@
"containerUser": "vscode", "containerUser": "vscode",
"containerEnv": { "containerEnv": {
"HOME": "/home/vscode" "HOME": "/home/vscode"
},
"mounts": [
"source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached"
],
"onCreateCommand": ".devcontainer/install-deps.sh",
"customizations": {
"vscode": {
"extensions": [
// coding styles
"chrislajoie.vscode-modelines",
"editorconfig.editorconfig",
// GitHub
"GitHub.vscode-pull-request-github",
"github.vscode-github-actions",
// Golang
"golang.go",
// C / C++
"ms-vscode.cpptools",
"ms-vscode.cpptools-extension-pack",
// CMake / Makefile
"ms-vscode.makefile-tools",
"ms-vscode.cmake-tools",
// Frontend
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"bradlc.vscode-tailwindcss",
"codeandstuff.package-json-upgrade",
// Localization
"inlang.vs-code-extension"
]
}
} }
} }

2
.gitignore vendored
View File

@ -13,3 +13,5 @@ node_modules
# generated during the build process # generated during the build process
#internal/native/include #internal/native/include
#internal/native/lib #internal/native/lib
ui/reports

25
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,25 @@
{
"recommendations": [
// coding styles
"chrislajoie.vscode-modelines",
"editorconfig.editorconfig",
// GitHub
"GitHub.vscode-pull-request-github",
"github.vscode-github-actions",
// Golang
"golang.go",
// C / C++
"ms-vscode.cpptools",
"ms-vscode.cpptools-extension-pack",
// CMake / Makefile
"ms-vscode.makefile-tools",
"ms-vscode.cmake-tools",
// Frontend
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"bradlc.vscode-tailwindcss",
"codeandstuff.package-json-upgrade",
// Localization
"inlang.vs-code-extension"
]
}

View File

@ -1,23 +1,20 @@
<div align="center"> # JetKVM Development Guide
<img alt="JetKVM logo" src="https://jetkvm.com/logo-blue.png" height="28">
### 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) [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) [![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) [![Go Report Card](https://goreportcard.com/badge/github.com/jetkvm/kvm)](https://goreportcard.com/report/github.com/jetkvm/kvm)
</div> </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. 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 ## Get Started
### Prerequisites ### Prerequisites
- **A JetKVM device** (for full development) - **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/)** - **[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 - **[Git](https://git-scm.com/downloads)** for version control
@ -28,6 +25,7 @@ Welcome to JetKVM development! This guide will help you get started quickly, whe
**Recommended:** Development is best done on **Linux** or **macOS**. **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: 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) - [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) - [WSL Setup Guide](https://docs.microsoft.com/en-us/windows/wsl/setup/environment)
@ -36,12 +34,14 @@ This ensures compatibility with shell scripts and build tools used in the projec
### Project Setup ### Project Setup
1. **Clone the repository:** 1. **Clone the repository:**
```bash ```bash
git clone https://github.com/jetkvm/kvm.git git clone https://github.com/jetkvm/kvm.git
cd kvm cd kvm
``` ```
2. **Check your tools:** 2. **Check your tools:**
```bash ```bash
go version && node --version go version && node --version
``` ```
@ -49,6 +49,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) 3. **Find your JetKVM IP address** (check your router or device screen)
4. **Deploy and test:** 4. **Deploy and test:**
```bash ```bash
./dev_deploy.sh -r 192.168.1.100 # Replace with your device IP ./dev_deploy.sh -r 192.168.1.100 # Replace with your device IP
``` ```
@ -95,7 +96,7 @@ tail -f /var/log/jetkvm.log
## Project Layout ## Project Layout
``` ```plaintext
/kvm/ /kvm/
├── main.go # App entry point ├── main.go # App entry point
├── config.go # Settings & configuration ├── config.go # Settings & configuration
@ -121,12 +122,16 @@ tail -f /var/log/jetkvm.log
├── scripts/ # Bash shell scripts for building and deploying ├── scripts/ # Bash shell scripts for building and deploying
└── static/ # (react client build output) └── static/ # (react client build output)
└── ui/ # React frontend └── ui/ # React frontend
├── localization/ # Client UI localization (i18n)
│ ├── jetKVM.UI.inlang/ # Settings for inlang
│ └── messages/ # Messages localized
├── public/ # UI website static images and fonts ├── public/ # UI website static images and fonts
└── src/ # Client React UI └── src/ # Client React UI
├── assets/ # UI in-page images ├── assets/ # UI in-page images
├── components/ # UI components ├── components/ # UI components
├── hooks/ # Hooks (stores, RPC handling, virtual devices) ├── hooks/ # Hooks (stores, RPC handling, virtual devices)
├── keyboardLayouts/ # Keyboard layout definitions ├── keyboardLayouts/ # Keyboard layout definitions
├── paraglide/ # (localization compiled messages output)
├── providers/ # Feature flags ├── providers/ # Feature flags
└── routes/ # Pages (login, settings, etc.) └── routes/ # Pages (login, settings, etc.)
``` ```
@ -144,7 +149,7 @@ tail -f /var/log/jetkvm.log
### Full Development (Recommended) ### Full Development (Recommended)
*Best for: Complete feature development* #### _Best for: Complete feature development_
```bash ```bash
# Deploy everything to your JetKVM device # Deploy everything to your JetKVM device
@ -153,7 +158,7 @@ tail -f /var/log/jetkvm.log
### Frontend Only ### Frontend Only
*Best for: UI changes without device* #### _Best for: UI changes without device_
```bash ```bash
cd ui cd ui
@ -167,7 +172,7 @@ Please click the `Build` button in EEZ Studio then run `./dev_deploy.sh -r <YOUR
### Quick Backend Changes ### Quick Backend Changes
*Best for: API or backend logic changes* #### _Best for: API or backend logic changes_
```bash ```bash
# Skip frontend build for faster deployment # Skip frontend build for faster deployment
@ -272,6 +277,7 @@ npm install
### "Device UI Fails to Build" ### "Device UI Fails to Build"
If while trying to build you run into an error message similar to : If while trying to build you run into an error message similar to :
```plaintext ```plaintext
In file included from /workspaces/kvm/internal/native/cgo/ctrl.c:15: 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 /workspaces/kvm/internal/native/cgo/ui_index.h:4:10: fatal error: ui/ui.h: No such file or directory
@ -279,17 +285,21 @@ In file included from /workspaces/kvm/internal/native/cgo/ctrl.c:15:
^~~~~~~~~ ^~~~~~~~~
compilation terminated. 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: 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 ```plaintext
../eez/src/ui ../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: 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 ```bash
# Globally enable git to create symlinks # Globally enable git to create symlinks
git config --global core.symlinks true git config --global core.symlinks true
git restore internal/native/cgo/ui git restore internal/native/cgo/ui
``` ```
```bash ```bash
# Enable git to create symlinks only in this project # Enable git to create symlinks only in this project
git config core.symlinks true git config core.symlinks true
@ -297,13 +307,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: Or if you want to manually create the symlink use:
```bash ```bash
# linux # linux
cd internal/native/cgo cd internal/native/cgo
rm ui rm ui
ln -s ../eez/src/ui ui ln -s ../eez/src/ui ui
``` ```
```dos
```batch
rem Windows rem Windows
cd internal/native/cgo cd internal/native/cgo
del ui del ui
@ -326,6 +338,7 @@ Or if you want to manually create the symlink use:
- **Go:** Follow standard Go conventions - **Go:** Follow standard Go conventions
- **TypeScript:** Use TypeScript for type safety - **TypeScript:** Use TypeScript for type safety
- **React:** Keep components small and reusable - **React:** Keep components small and reusable
- **Localization:** Ensure all user-facing strings in the frontend are [localized](#localization)
### Environment Variables ### Environment Variables
@ -358,11 +371,12 @@ export JETKVM_PROXY_URL="ws://<IP>"
4. Test thoroughly 4. Test thoroughly
5. Submit a pull request 5. Submit a pull request
### Before submitting: ### Before submitting
- [ ] Code works on device - [ ] Code works on device
- [ ] Tests pass - [ ] Tests pass
- [ ] Code follows style guidelines - [ ] Code follows style guidelines
- [ ] Frontend user-facing strings [localized](#localization)
- [ ] Documentation updated (if needed) - [ ] Documentation updated (if needed)
--- ---
@ -418,7 +432,6 @@ The application uses a JSON configuration file stored at `/userdata/kvm_config.j
3. **Add migration logic if needed for existing installations** 3. **Add migration logic if needed for existing installations**
### LVGL Build ### LVGL Build
We modified the LVGL code a little bit to remove unused fonts and examples. We modified the LVGL code a little bit to remove unused fonts and examples.
@ -429,6 +442,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 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 calls to 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 for the literal `I will call you {name}`, use `m.profile_i_will_call_you({ 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. Make sure you use an LLM (you can use [aifiesta](https://chat.aifiesta.ai/chat/) to use multiple LLMs) or a [translator](https://translate.google.com) of some form to back-translate each **new** machine-generation in each _language_ to ensure those terms translate reasonably.
### 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.
--- ---

32
hw.go
View File

@ -3,6 +3,7 @@ package kvm
import ( import (
"fmt" "fmt"
"os" "os"
"os/exec"
"regexp" "regexp"
"strings" "strings"
"sync" "sync"
@ -36,6 +37,37 @@ func readOtpEntropy() ([]byte, error) { //nolint:unused
return content[0x17:0x1C], nil return content[0x17:0x1C], nil
} }
func hwReboot(force bool, postRebootAction *PostRebootAction, delay time.Duration) error {
logger.Info().Msgf("Reboot requested, rebooting in %d seconds...", delay)
writeJSONRPCEvent("willReboot", postRebootAction, currentSession)
time.Sleep(1 * time.Second) // Wait for the JSONRPCEvent to be sent
nativeInstance.SwitchToScreenIfDifferent("rebooting_screen")
time.Sleep(delay - (1 * time.Second)) // wait requested extra settle time
args := []string{}
if force {
args = append(args, "-f")
}
cmd := exec.Command("reboot", args...)
err := cmd.Start()
if err != nil {
logger.Error().Err(err).Msg("failed to reboot")
switchToMainScreen()
return fmt.Errorf("failed to reboot: %w", err)
}
// If the reboot command is successful, exit the program after 5 seconds
go func() {
time.Sleep(5 * time.Second)
os.Exit(0)
}()
return nil
}
var deviceID string var deviceID string
var deviceIDOnce sync.Once var deviceIDOnce sync.Once

View File

@ -173,34 +173,8 @@ func rpcGetDeviceID() (string, error) {
} }
func rpcReboot(force bool) error { func rpcReboot(force bool) error {
logger.Info().Msg("Got reboot request from JSONRPC, rebooting...") logger.Info().Msg("Got reboot request via RPC")
return hwReboot(force, nil, 0)
writeJSONRPCEvent("willReboot", nil, currentSession)
// Wait for the JSONRPCEvent to be sent
time.Sleep(1 * time.Second)
nativeInstance.SwitchToScreenIfDifferent("rebooting_screen")
args := []string{}
if force {
args = append(args, "-f")
}
cmd := exec.Command("reboot", args...)
err := cmd.Start()
if err != nil {
logger.Error().Err(err).Msg("failed to reboot")
switchToMainScreen()
return fmt.Errorf("failed to reboot: %w", err)
}
// If the reboot command is successful, exit the program after 5 seconds
go func() {
time.Sleep(5 * time.Second)
os.Exit(0)
}()
return nil
} }
var streamFactor = 1.0 var streamFactor = 1.0

View File

@ -37,14 +37,17 @@ func initNative(systemVersion *semver.Version, appVersion *semver.Version) {
nativeLogger.Trace().Str("event", event).Msg("rpc event received") nativeLogger.Trace().Str("event", event).Msg("rpc event received")
switch event { switch event {
case "resetConfig": case "resetConfig":
nativeLogger.Info().Msg("Reset configuration request via native rpc event")
err := rpcResetConfig() err := rpcResetConfig()
if err != nil { if err != nil {
nativeLogger.Warn().Err(err).Msg("error resetting config") nativeLogger.Warn().Err(err).Msg("error resetting config")
} }
_ = rpcReboot(true) _ = rpcReboot(true)
case "reboot": case "reboot":
nativeLogger.Info().Msg("Reboot request via native rpc event")
_ = rpcReboot(true) _ = rpcReboot(true)
case "toggleDHCPClient": case "toggleDHCPClient":
nativeLogger.Info().Msg("Toggle DHCP request via native rpc event")
_ = rpcToggleDHCPClient() _ = rpcToggleDHCPClient()
default: default:
nativeLogger.Warn().Str("event", event).Msg("unknown rpc event received") nativeLogger.Warn().Str("event", event).Msg("unknown rpc event received")

View File

@ -193,6 +193,7 @@ func shouldRebootForNetworkChange(oldConfig, newConfig *types.NetworkConfig) (re
oldIPv4Mode := oldConfig.IPv4Mode.String oldIPv4Mode := oldConfig.IPv4Mode.String
newIPv4Mode := newConfig.IPv4Mode.String newIPv4Mode := newConfig.IPv4Mode.String
// IPv4 mode change requires reboot // IPv4 mode change requires reboot
if newIPv4Mode != oldIPv4Mode { if newIPv4Mode != oldIPv4Mode {
rebootRequired = true rebootRequired = true
@ -284,7 +285,8 @@ func rpcSetNetworkSettings(settings RpcNetworkSettings) (*RpcNetworkSettings, er
} }
if rebootRequired { if rebootRequired {
if err := rpcReboot(false); err != nil { l.Info().Msg("Rebooting due to network changes")
if err := hwReboot(true, postRebootAction, 0); err != nil {
return nil, err return nil, err
} }
} }

24
ota.go
View File

@ -487,25 +487,15 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
} }
if rebootNeeded { if rebootNeeded {
scopedLogger.Info().Msg("System Rebooting in 10s") scopedLogger.Info().Msg("System Rebooting due to OTA update")
// TODO: Future enhancement - send postRebootAction to redirect to release notes postRebootAction := &PostRebootAction{
// Example: HealthCheck: "/device/status",
// postRebootAction := &PostRebootAction{ RedirectUrl: "/settings/general/update?version=" + remote.SystemVersion,
// HealthCheck: "[..]/device/status", }
// RedirectUrl: "[..]/settings/general/update?version=X.Y.Z",
// }
// writeJSONRPCEvent("willReboot", postRebootAction, currentSession)
time.Sleep(10 * time.Second) if err := hwReboot(true, postRebootAction, 10*time.Second); err != nil {
cmd := exec.Command("reboot") return fmt.Errorf("error requesting reboot: %w", err)
err := cmd.Start()
if err != nil {
otaState.Error = fmt.Sprintf("Failed to start reboot: %v", err)
scopedLogger.Error().Err(err).Msg("Failed to start reboot")
return fmt.Errorf("failed to start reboot: %w", err)
} else {
os.Exit(0)
} }
} }

View File

@ -9,8 +9,6 @@ const {
fixupConfigRules, fixupConfigRules,
} = require("@eslint/compat"); } = require("@eslint/compat");
const tsParser = require("@typescript-eslint/parser");
const reactRefresh = require("eslint-plugin-react-refresh");
const js = require("@eslint/js"); const js = require("@eslint/js");
const { const {
@ -23,6 +21,9 @@ const compat = new FlatCompat({
allConfig: js.configs.all allConfig: js.configs.all
}); });
const tsParser = require("@typescript-eslint/parser");
const reactRefresh = require("eslint-plugin-react-refresh");
module.exports = defineConfig([{ module.exports = defineConfig([{
languageOptions: { languageOptions: {
globals: { globals: {
@ -81,7 +82,10 @@ module.exports = defineConfig([{
map: [ map: [
["@components", "./src/components"], ["@components", "./src/components"],
["@routes", "./src/routes"], ["@routes", "./src/routes"],
["@hooks", "./src/hooks"],
["@providers", "./src/providers"],
["@assets", "./src/assets"], ["@assets", "./src/assets"],
["@localizations", "./localization/paraglide"],
["@", "./src"], ["@", "./src"],
], ],

View File

@ -45,31 +45,39 @@
<link rel="shortcut icon" href="/favicon.ico" /> <link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-title" content="JetKVM" /> <meta name="apple-mobile-web-app-title" content="JetKVM" />
<link rel="manifest" href="/site.webmanifest" /> <link rel="manifest" href="/public/site.webmanifest" />
<meta name="theme-color" content="#051946" /> <meta name="theme-color" content="#051946" />
<meta name="description" content="A web-based KVM console for managing remote servers." /> <meta
name="description"
content="A web-based KVM console for managing remote servers."
/>
<script> <script>
function applyThemeFromPreference() { function applyThemeFromPreference() {
// dark theme setup // dark theme setup
var darkDesired = localStorage.theme === "dark" || var darkDesired =
localStorage.theme === "dark" ||
(!("theme" in localStorage) && (!("theme" in localStorage) &&
window.matchMedia("(prefers-color-scheme: dark)").matches) window.matchMedia("(prefers-color-scheme: dark)").matches);
document.documentElement.classList.toggle("dark", darkDesired) document.documentElement.classList.toggle("dark", darkDesired);
} }
// initial theme application // initial theme application
applyThemeFromPreference(); applyThemeFromPreference();
// Listen for system theme changes // Listen for system theme changes
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", applyThemeFromPreference); window
window.matchMedia("(prefers-color-scheme: light)").addEventListener("change", applyThemeFromPreference); .matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", applyThemeFromPreference);
window
.matchMedia("(prefers-color-scheme: light)")
.addEventListener("change", applyThemeFromPreference);
</script> </script>
</head> </head>
<body <body
class="h-full w-full bg-[#f3f9ff] font-sans text-sm antialiased dark:bg-slate-900 md:text-base" class="h-full w-full bg-[#f3f9ff] font-sans text-sm antialiased md:text-base dark:bg-slate-900"
> >
<div id="root" class="w-full h-full"></div> <div id="root" class="h-full w-full"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>

View File

@ -0,0 +1 @@
cache

View File

@ -0,0 +1 @@
TI1a2RjjH4qkImNj0w

View File

@ -0,0 +1,48 @@
{
"$schema": "https://inlang.com/schema/project-settings",
"baseLocale": "en",
"sourceLanguageTag": "en",
"locales": [
"en",
"da",
"de",
"es",
"fr",
"it",
"nb",
"sv",
"zh"
],
"languageTags": [
"en",
"da",
"de",
"es",
"fr",
"it",
"nb",
"sv",
"zh"
],
"modules": [
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@latest/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@latest/dist/index.js"
],
"plugin.inlang.messageFormat": {
"pathPattern": "./messages/{locale}.json"
},
"plugin.inlang.mFunctionMatcher": {
"matchers": [
{
"type": "m-function",
"function": "plural",
"parameter": "count"
}
]
},
"strategy": [
"cookie",
"preferredLanguage",
"baseLocale"
]
}

View File

@ -0,0 +1,895 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"access_adopt_kvm": "Adopter KVM til skyen",
"access_adopted_message": "Din enhed er tilpasset til skyen",
"access_auth_mode_no_password": "Aktuel tilstand: Ingen adgangskode",
"access_auth_mode_password": "Aktuel tilstand: Adgangskodebeskyttet",
"access_authentication_mode_title": "Godkendelsestilstand",
"access_certificate_label": "Certifikat",
"access_change_password_button": "Skift adgangskode",
"access_change_password_description": "Opdater din adgangskode til enheden",
"access_change_password_title": "Skift adgangskode",
"access_cloud_api_url_label": "Cloud API-URL",
"access_cloud_app_url_label": "URL til cloud-applikation",
"access_cloud_provider_description": "Vælg cloud-udbyderen til din enhed",
"access_cloud_provider_title": "Cloud-udbyder",
"access_cloud_security_title": "Cloud-sikkerhed",
"access_confirm_deregister": "Er du sikker på, at du vil afregistrere denne enhed?",
"access_deregister": "Afregistrering fra Cloud",
"access_description": "Administrer adgangskontrollen for enheden",
"access_disable_protection": "Deaktiver beskyttelse",
"access_enable_password": "Aktivér adgangskode",
"access_failed_deregister": "Kunne ikke afregistrere enhed: {error}",
"access_failed_update_cloud_url": "Kunne ikke opdatere cloud-URL: {error}",
"access_failed_update_tls": "Kunne ikke opdatere TLS-indstillinger: {error}",
"access_github_link": "GitHub",
"access_https_description": "Konfigurer sikker HTTPS-adgang til din enhed",
"access_https_mode_title": "HTTPS-tilstand",
"access_learn_security": "Lær om vores cloud-sikkerhed",
"access_local_description": "Administrer tilstanden for lokal adgang til enheden",
"access_local_title": "Lokal",
"access_no_device_id": "Intet enheds-ID tilgængeligt",
"access_private_key_description": "Af sikkerhedsmæssige årsager vil den ikke blive vist efter lagring.",
"access_private_key_label": "Privat nøgle",
"access_provider_custom": "Tilpasset",
"access_provider_jetkvm": "JetKVM Cloud",
"access_remote_description": "Administrer tilstanden for fjernadgang til enheden",
"access_security_encryption": "End-to-end-kryptering ved hjælp af WebRTC (DTLS og SRTP)",
"access_security_oidc": "OIDC (OpenID Connect) godkendelse",
"access_security_open_source": "Alle cloud-komponenter er open source og tilgængelige på GitHub.",
"access_security_streams": "Alle streams krypteret under transit",
"access_security_zero_trust": "Zero Trust-sikkerhedsmodel",
"access_title": "Adgang",
"access_tls_certificate_description": "Indsæt dit TLS-certifikat nedenfor. For certifikatkæder skal du inkludere hele kæden (blad-, mellem- og rodcertifikater).",
"access_tls_certificate_title": "TLS-certifikat",
"access_tls_custom": "Tilpasset",
"access_tls_disabled": "Deaktiveret",
"access_tls_self_signed": "Selvsigneret",
"access_tls_updated": "TLS-indstillingerne er blevet opdateret",
"access_update_tls_settings": "Opdater TLS-indstillinger",
"action_bar_connection_stats": "Forbindelsesstatistik",
"action_bar_extension": "Udvidelse",
"action_bar_fullscreen": "Fuldskærm",
"action_bar_settings": "Indstillinger",
"action_bar_virtual_keyboard": "Virtuelt tastatur",
"action_bar_virtual_media": "Virtuelle medier",
"action_bar_wake_on_lan": "Vågn på LAN",
"action_bar_web_terminal": "Webterminal",
"advanced_description": "Få adgang til yderligere indstillinger til fejlfinding og tilpasning",
"advanced_dev_channel_description": "Modtag tidlige opdateringer fra udviklingskanalen",
"advanced_dev_channel_title": "Opdateringer af udviklerkanaler",
"advanced_developer_mode_description": "Aktivér avancerede funktioner for udviklere",
"advanced_developer_mode_enabled_title": "Udviklertilstand aktiveret",
"advanced_developer_mode_title": "Udviklertilstand",
"advanced_developer_mode_warning_advanced": "Kun for avancerede brugere. Ikke til produktionsbrug.",
"advanced_developer_mode_warning_risks": "Brug kun hvis du forstår risiciene",
"advanced_developer_mode_warning_security": "Sikkerheden svækkes, mens den er aktiv",
"advanced_disable_usb_emulation": "Deaktiver USB-emulering",
"advanced_enable_usb_emulation": "Aktivér USB-emulering",
"advanced_error_loopback_disable": "Kunne ikke deaktivere loopback-only-tilstand: {error}",
"advanced_error_loopback_enable": "Kunne ikke aktivere loopback-only-tilstand: {error}",
"advanced_error_reset_config": "Konfigurationen kunne ikke nulstilles: {error}",
"advanced_error_set_dev_channel": "Kunne ikke indstille udviklerkanaltilstand: {error}",
"advanced_error_set_dev_mode": "Kunne ikke indstille udviklertilstand: {error}",
"advanced_error_update_ssh_key": "Kunne ikke opdatere SSH-nøglen: {error}",
"advanced_error_usb_emulation_disable": "Kunne ikke deaktivere USB-emulering: {error}",
"advanced_error_usb_emulation_enable": "Kunne ikke aktivere USB-emulering: {error}",
"advanced_loopback_only_description": "Begræns webgrænsefladeadgang kun til localhost (127.0.0.1)",
"advanced_loopback_only_title": "Kun loopback-tilstand",
"advanced_loopback_warning_before": "Før du aktiverer denne funktion, skal du sørge for at du har enten:",
"advanced_loopback_warning_cloud": "Cloud-adgang aktiveret og fungerer",
"advanced_loopback_warning_confirm": "Jeg forstår, aktivér alligevel",
"advanced_loopback_warning_description": "ADVARSEL: Dette vil begrænse adgangen til webgrænsefladen til kun localhost (127.0.0.1).",
"advanced_loopback_warning_ssh": "SSH-adgang konfigureret og testet",
"advanced_loopback_warning_title": "Aktivér kun loopback-tilstand?",
"advanced_reset_config_button": "Nulstil konfiguration",
"advanced_reset_config_description": "Nulstil konfigurationen til standard. Dette vil logge dig ud.",
"advanced_reset_config_title": "Nulstil konfiguration",
"advanced_ssh_access_description": "Tilføj din offentlige SSH-nøgle for at aktivere sikker fjernadgang til enheden",
"advanced_ssh_access_title": "SSH-adgang",
"advanced_ssh_default_user": "Standard SSH-brugeren er",
"advanced_ssh_public_key_label": "Offentlig SSH-nøgle",
"advanced_ssh_public_key_placeholder": "Indtast din offentlige SSH-nøgle",
"advanced_success_loopback_disabled": "Kun loopback-tilstand er deaktiveret. Genstart din enhed for at anvende den.",
"advanced_success_loopback_enabled": "Kun loopback-tilstand aktiveret. Genstart din enhed for at anvende den.",
"advanced_success_reset_config": "Konfigurationen er nulstillet til standard",
"advanced_success_update_ssh_key": "SSH-nøglen er blevet opdateret",
"advanced_title": "Fremskreden",
"advanced_troubleshooting_mode_description": "Diagnostiske værktøjer og yderligere kontroller til fejlfinding og udviklingsformål",
"advanced_troubleshooting_mode_title": "Fejlfindingstilstand",
"advanced_update_ssh_key_button": "Opdater SSH-nøgle",
"advanced_usb_emulation_description": "Styr USB-emuleringstilstanden",
"advanced_usb_emulation_title": "USB-emulering",
"already_adopted_new_owner": "Hvis du er den nye ejer, bedes du bede den tidligere ejer om at afregistrere enheden fra sin konto i cloud-dashboardet. Hvis du mener, at dette er en fejl, kan du kontakte vores supportteam for at få hjælp.",
"already_adopted_other_user": "Denne enhed er i øjeblikket registreret til en anden bruger i vores cloud-dashboard.",
"already_adopted_return_to_dashboard": "Tilbage til dashboardet",
"already_adopted_title": "Enheden er allerede registreret",
"appearance_description": "Vælg dit foretrukne farvetema",
"appearance_page_description": "Tilpas udseendet og følelsen af din JetKVM-grænseflade",
"appearance_theme": "Tema",
"appearance_theme_dark": "Mørk",
"appearance_theme_light": "Lys",
"appearance_theme_system": "System",
"appearance_title": "Udseende",
"attach": "Vedhæft",
"atx_power_control_get_state_error": "Kunne ikke hente ATX-strømtilstand: {error}",
"atx_power_control_hdd_led": "HDD-LED",
"atx_power_control_long_power_button": "Langt tryk",
"atx_power_control_power_button": "Strøm",
"atx_power_control_power_led": "Strøm-LED",
"atx_power_control_reset_button": "Nulstil",
"atx_power_control_send_action_error": "Kunne ikke sende ATX-strømfunktion {action} : {error}",
"atx_power_control_short_power_button": "Kort tryk",
"auth_authentication_mode": "Vælg venligst en godkendelsestilstand",
"auth_authentication_mode_error": "Der opstod en fejl under indstilling af godkendelsestilstanden",
"auth_authentication_mode_invalid": "Ugyldig godkendelsestilstand",
"auth_connect_to_cloud": "Tilslut din JetKVM til skyen",
"auth_connect_to_cloud_action": "Log ind og tilslut enhed",
"auth_connect_to_cloud_description": "Lås op for fjernadgang og avancerede funktioner på din enhed",
"auth_header_cta_already_have_account": "Har du allerede en konto?",
"auth_header_cta_dont_have_account": "Har du ikke en konto?",
"auth_header_cta_new_to_jetkvm": "Ny bruger af JetKVM?",
"auth_login": "Log ind på din JetKVM-konto",
"auth_login_action": "Log ind",
"auth_login_description": "Log ind for at få adgang til og administrere dine enheder sikkert",
"auth_mode_local": "Lokal godkendelsesmetode",
"auth_mode_local_change_later": "Du kan altid ændre din godkendelsesmetode senere i indstillingerne.",
"auth_mode_local_description": "Vælg, hvordan du vil sikre din JetKVM-enhed lokalt.",
"auth_mode_local_no_password": "Ingen adgangskode",
"auth_mode_local_no_password_description": "Hurtig adgang uden adgangskodegodkendelse.",
"auth_mode_local_password": "Adgangskode",
"auth_mode_local_password_confirm_description": "Bekræft din adgangskode",
"auth_mode_local_password_confirm_label": "Bekræft adgangskode",
"auth_mode_local_password_description": "Sikr din enhed med en adgangskode for ekstra beskyttelse.",
"auth_mode_local_password_failed_set": "Kunne ikke angive adgangskode: {error}",
"auth_mode_local_password_note": "Denne adgangskode vil blive brugt til at sikre dine enhedsdata og beskytte mod uautoriseret adgang.",
"auth_mode_local_password_note_local": "Alle data forbliver på din lokale enhed.",
"auth_mode_local_password_set": "Indstil en adgangskode",
"auth_mode_local_password_set_button": "Indstil adgangskode",
"auth_mode_local_password_set_description": "Opret en stærk adgangskode for at sikre din JetKVM-enhed lokalt.",
"auth_mode_local_password_set_label": "Indtast en adgangskode",
"auth_signup_connect_to_cloud_action": "Tilmeld og tilslut enhed",
"auth_signup_create_account": "Opret din JetKVM-konto",
"auth_signup_create_account_action": "Opret konto",
"auth_signup_create_account_description": "Opret din konto, og begynd nemt at administrere dine enheder.",
"back": "Tilbage",
"back_to_devices": "Tilbage til Enheder",
"cancel": "Annuller",
"close": "Luk",
"cloud_kvms": "Cloud KVM'er",
"cloud_kvms_description": "Administrer dine cloud-KVM'er, og opret forbindelse til dem sikkert.",
"cloud_kvms_no_devices": "Ingen enheder fundet",
"cloud_kvms_no_devices_description": "Du har endnu ingen enheder med aktiveret JetKVM Cloud.",
"confirm": "Bekræfte",
"connect_to_kvm": "Opret forbindelse til KVM",
"connecting_to_device": "Forbinder til enhed…",
"connection_established": "Forbindelse etableret",
"connection_stats_badge_jitter": "Jitter",
"connection_stats_badge_jitter_buffer_avg_delay": "Jitterbuffer gennemsnitlig forsinkelse",
"connection_stats_connection": "Forbindelse",
"connection_stats_connection_description": "Forbindelsen mellem klienten og JetKVM'en.",
"connection_stats_frames_per_second": "Billeder per sekund",
"connection_stats_frames_per_second_description": "Antal indgående videobilleder vist pr. sekund.",
"connection_stats_network_stability": "Netværksstabilitet",
"connection_stats_network_stability_description": "Hvor stabil strømmen af indgående videopakker er på tværs af netværket.",
"connection_stats_packets_lost": "Pakker mistet",
"connection_stats_packets_lost_description": "Antal mistede indgående video-RTP-pakker.",
"connection_stats_playback_delay": "Afspilningsforsinkelse",
"connection_stats_playback_delay_description": "Forsinkelse tilføjet af jitterbufferen for at jævne afspilningen, når billeder ankommer ujævnt.",
"connection_stats_round_trip_time": "Rundturstid",
"connection_stats_round_trip_time_description": "Rundrejsetid for det aktive ICE-kandidatpar mellem peers.",
"connection_stats_sidebar": "Forbindelsesstatistik",
"connection_stats_unit_frames_per_second": " fps",
"connection_stats_unit_milliseconds": " ms",
"connection_stats_unit_packets": " pakker",
"connection_stats_video": "Video",
"connection_stats_video_description": "Videostreamen fra JetKVM'en til klienten.",
"continue": "Fortsætte",
"creating_peer_connection": "Opretter peer-forbindelse…",
"dc_power_control_current": "Strøm",
"dc_power_control_current_unit": "EN",
"dc_power_control_get_state_error": "Kunne ikke hente DC-strømtilstand: {error}",
"dc_power_control_power": "Strøm",
"dc_power_control_power_off_button": "Sluk",
"dc_power_control_power_off_state": "Sluk",
"dc_power_control_power_on_button": "Tænd",
"dc_power_control_power_on_state": "Tænd",
"dc_power_control_power_unit": "V",
"dc_power_control_restore_last_state": "Sidste stat",
"dc_power_control_restore_power_state": "Gendan strømtab",
"dc_power_control_set_power_state_error": "Kunne ikke sende DC-strømstatus til {enabled} : {error}",
"dc_power_control_set_restore_state_error": "Kunne ikke sende DC-strømgendannelsesstatus til {state} : {error}",
"dc_power_control_voltage": "Spænding",
"dc_power_control_voltage_unit": "V",
"delete": "Slet",
"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.",
"deregister_from_cloud": "Afregistrering fra Cloud",
"deregister_headline": "Afregistrér {device} fra din cloud-konto",
"detach": "Løsrive",
"dhcp_empty_lease_description": "Vi har endnu ikke modtaget nogen DHCP-leaseoplysninger fra enheden.",
"dhcp_empty_lease_headline": "Ingen DHCP-leaseoplysninger",
"dhcp_lease_boot_file": "Boot-fil",
"dhcp_lease_boot_next_server": "Start næste server",
"dhcp_lease_boot_server_name": "Navn på bootserver",
"dhcp_lease_broadcast": "Udsende",
"dhcp_lease_domain": "Domæne",
"dhcp_lease_gateway": "Gateway",
"dhcp_lease_header": "DHCP-leaseoplysninger",
"dhcp_lease_hostname": "Værtsnavn",
"dhcp_lease_lease_expires": "Lejekontrakten udløber",
"dhcp_lease_maximum_transfer_unit": "MTU",
"dhcp_lease_renew": "Forny DHCP-lease",
"dhcp_lease_time_to_live": "TTL",
"dhcp_server": "DHCP-server",
"dns_servers": "DNS-servere",
"establishing_secure_connection": "Opretter sikker forbindelse…",
"experimental": "Eksperimentel",
"extension_popover_load_and_manage_extensions": "Indlæs og administrer dine udvidelser",
"extension_popover_set_error_notification": "Kunne ikke angive aktiv udvidelse: {error}",
"extension_popover_unload_extension": "Fjern udvidelse",
"extension_serial_console": "Seriel konsol",
"extension_serial_console_description": "Få adgang til din serielle konsoludvidelse",
"extensions_atx_power_control": "ATX-strømstyring",
"extensions_atx_power_control_description": "Styr din maskines strømtilstand via ATX-strømstyring.",
"extensions_dc_power_control": "DC-strømstyring",
"extensions_dc_power_control_description": "Styr din DC-strømforlænger",
"extensions_popover_extensions": "Udvidelser",
"gathering_ice_candidates": "Samler ICE-kandidater…",
"general_app_version": "App: {version}",
"general_auto_update_description": "Opdater automatisk enheden til den nyeste version",
"general_auto_update_error": "Kunne ikke indstille automatisk opdatering: {error}",
"general_auto_update_title": "Automatisk opdatering",
"general_check_for_updates": "Tjek for opdateringer",
"general_page_description": "Konfigurer enhedsindstillinger og opdater præferencer",
"general_reboot_description": "Vil du fortsætte med at genstarte systemet?",
"general_reboot_device": "Genstart enhed",
"general_reboot_device_description": "Sluk og tænd for JetKVM'en",
"general_reboot_no_button": "Ingen",
"general_reboot_title": "Genstart JetKVM",
"general_reboot_yes_button": "Ja",
"general_system_version": "System: {version}",
"general_title": "Generel",
"general_update_app_update_title": "App-opdatering",
"general_update_application_type": "App",
"general_update_available_description": "En ny opdatering er tilgængelig for at forbedre systemets ydeevne og kompatibilitet. Vi anbefaler at opdatere for at sikre, at alt kører problemfrit.",
"general_update_available_title": "Opdatering tilgængelig",
"general_update_background_button": "Opdatering i baggrunden",
"general_update_check_again_button": "Tjek igen",
"general_update_checking_description": "Vi sørger for, at din enhed har de nyeste funktioner og forbedringer.",
"general_update_checking_title": "Søger efter opdateringer…",
"general_update_completed_description": "Din enhed er blevet opdateret til den nyeste version. Nyd de nye funktioner og forbedringer!",
"general_update_completed_title": "Opdatering gennemført",
"general_update_error_description": "Der opstod en fejl under opdateringen af din enhed. Prøv igen senere.",
"general_update_error_details": "Fejldetaljer: {errorMessage}",
"general_update_error_title": "Opdateringsfejl",
"general_update_later_button": "Gør det senere",
"general_update_now_button": "Opdater nu",
"general_update_rebooting": "Genstarter for at fuldføre opdateringen…",
"general_update_status_awaiting_reboot": "Venter på genstart",
"general_update_status_downloading": "Downloader {update_type} opdatering…",
"general_update_status_fetching": "Henter opdateringsoplysninger…",
"general_update_status_installing": "Installation af {update_type} opdatering…",
"general_update_status_progress": "{part} fremskridt",
"general_update_status_verifying": "Bekræfter {update_type} opdatering…",
"general_update_system_type": "System",
"general_update_system_update_title": "Linux-systemopdatering",
"general_update_up_to_date_description": "Dit system kører den nyeste version. Der er ingen tilgængelige opdateringer i øjeblikket.",
"general_update_up_to_date_title": "Systemet er opdateret",
"general_update_updating_description": "Sluk ikke din enhed. Denne proces kan tage et par minutter.",
"general_update_updating_title": "Opdatering af din enhed",
"getting_remote_session_description": "Henter beskrivelse af fjernsessionsforsøg {attempt}",
"hardware_backlight_settings_error": "Kunne ikke indstille baggrundsbelysningsindstillinger: {error}",
"hardware_backlight_settings_get_error": "Kunne ikke hente indstillinger for baggrundsbelysning: {error}",
"hardware_backlight_settings_success": "Baggrundsbelysningsindstillingerne er blevet opdateret",
"hardware_dim_display_after_description": "Indstil, hvor længe der skal ventes, før displayet dæmpes",
"hardware_dim_display_after_title": "Dæmp displayet efter",
"hardware_display_brightness_description": "Indstil skærmens lysstyrke",
"hardware_display_brightness_high": "Høj",
"hardware_display_brightness_low": "Lav",
"hardware_display_brightness_medium": "Medium",
"hardware_display_brightness_off": "Slukket",
"hardware_display_brightness_title": "Skærmens lysstyrke",
"hardware_display_orientation_description": "Indstil skærmens retning",
"hardware_display_orientation_error": "Kunne ikke indstille visningsretning: {error}",
"hardware_display_orientation_inverted": "Omvendt",
"hardware_display_orientation_normal": "Normal",
"hardware_display_orientation_success": "Skærmretningen er blevet opdateret",
"hardware_display_orientation_title": "Skærmretning",
"hardware_display_wake_up_note": "Skærmen vågner op, når forbindelsestilstanden ændres, eller når den berøres.",
"hardware_page_description": "Konfigurer skærmindstillinger og hardwareindstillinger for din JetKVM-enhed",
"hardware_power_saving_description": "Reducer strømforbruget, når det ikke er i brug",
"hardware_power_saving_disabled": "Strømsparetilstand deaktiveret",
"hardware_power_saving_enabled": "Strømsparetilstand aktiveret",
"hardware_power_saving_failed_error": "Kunne ikke indstille strømsparetilstand: {error}",
"hardware_power_saving_hdmi_sleep_description": "Slå optagelse fra efter 90 sekunders inaktivitet",
"hardware_power_saving_hdmi_sleep_title": "HDMI-dvaletilstand",
"hardware_power_saving_title": "Strømbesparelse",
"hardware_time_10_minutes": "10 minutter",
"hardware_time_1_hour": "1 time",
"hardware_time_1_minute": "1 minut",
"hardware_time_30_minutes": "30 minutter",
"hardware_time_5_minutes": "5 minutter",
"hardware_time_never": "Aldrig",
"hardware_title": "Hardware",
"hardware_turn_off_display_after_description": "Periode med inaktivitet før displayet automatisk slukker",
"hardware_turn_off_display_after_title": "Sluk skærmen efter",
"hide": "Skjule",
"ice_gathering_completed": "ICE-indsamlingen er afsluttet",
"info_caps_lock": "Caps Lock",
"info_compose": "Skriv",
"info_hdmi_state": "HDMI-tilstand:",
"info_hidrpc_state": "HidRPC-tilstand:",
"info_kana": "Kana",
"info_keys": "Nøgler:",
"info_last_move": "Sidste træk:",
"info_num_lock": "Num Lock",
"info_paste_enabled": "Aktiveret",
"info_paste_mode": "Indsætningstilstand:",
"info_pointer": "Peger:",
"info_relayed_by_cloudflare": "Videresendt af Cloudflare",
"info_resolution": "Opløsning:",
"info_scroll_lock": "Scroll Lock",
"info_shift": "Flytte",
"info_usb_state": "USB-tilstand:",
"info_video_size": "Videostørrelse:",
"input_disabled": "Input deaktiveret",
"invalid_password": "Ugyldig adgangskode",
"ip_address": "IP-adresse",
"ipv6_address_label": "Adresse",
"ipv6_gateway": "Gateway",
"ipv6_information": "IPv6-oplysninger",
"ipv6_link_local": "Link-lokal",
"ipv6_preferred_lifetime": "Foretrukken levetid",
"ipv6_valid_lifetime": "Gyldig livstid",
"jetkvm_description": "JetKVM kombinerer kraftfuld hardware med intuitiv software for at give en problemfri fjernbetjeningsoplevelse.",
"jetkvm_device": "JetKVM-enhed",
"jetkvm_logo": "JetKVM-logo",
"jetkvm_setup": "Opsæt din JetKVM",
"jiggler_cron_schedule_description": "Cron-udtryk til planlægning",
"jiggler_cron_schedule_label": "Cron-skema",
"jiggler_example_business_hours_early": "Åbningstider 8-17",
"jiggler_example_business_hours_late": "Åbningstider 9-17",
"jiggler_examples_label": "Eksempler",
"jiggler_inactivity_limit_description": "Inaktivitetstid før rystelse",
"jiggler_inactivity_limit_label": "Inaktivitetsgrænse i sekunder",
"jiggler_more_examples": "Flere eksempler",
"jiggler_random_delay_description": "For at undgå genkendelige mønstre",
"jiggler_random_delay_label": "Tilfældig forsinkelse",
"jiggler_save_jiggler_config": "Gem Jiggler-konfiguration",
"jiggler_timezone_description": "Tidszone for cron-plan",
"jiggler_timezone_label": "Tidszone",
"keyboard_description": "Konfigurer tastaturindstillinger for din enhed",
"keyboard_layout_description": "Tastaturlayout for måloperativsystemet",
"keyboard_layout_error": "Kunne ikke indstille tastaturlayout: {error}",
"keyboard_layout_long_description": "Det virtuelle tastatur, indsættelse af tekst og tastaturmakroer sender individuelle tastetryk til målenheden. Tastaturlayoutet bestemmer, hvilke tastekoder der sendes. Sørg for, at tastaturlayoutet i JetKVM matcher indstillingerne i operativsystemet.",
"keyboard_layout_success": "Tastaturlayoutet er nu indstillet til {layout}",
"keyboard_layout_title": "Tastaturlayout",
"keyboard_show_pressed_keys_description": "Vis de aktuelt nedtrykkede taster i statuslinjen",
"keyboard_show_pressed_keys_title": "Vis trykkede taster",
"keyboard_title": "Tastatur",
"kvm_terminal": "KVM-terminal",
"last_online": "Sidst online {time}",
"learn_more": "Lær mere",
"load": "Indlæs",
"loading": "Indlæser…",
"local_auth_change_local_device_password_description": "Indtast din nuværende adgangskode og en ny adgangskode for at opdatere din lokale enhedsbeskyttelse.",
"local_auth_change_local_device_password_title": "Skift adgangskode til den lokale enhed",
"local_auth_confirm_new_password_label": "Bekræft ny adgangskode",
"local_auth_create_confirm_password_placeholder": "Indtast din adgangskode igen",
"local_auth_create_description": "Opret en adgangskode for at beskytte din enhed mod uautoriseret lokal adgang.",
"local_auth_create_new_password_label": "Ny adgangskode",
"local_auth_create_new_password_placeholder": "Indtast en stærk adgangskode",
"local_auth_create_not_now_button": "Ikke nu",
"local_auth_create_secure_button": "Sikr enheden",
"local_auth_create_title": "Beskyttelse af lokal enhed",
"local_auth_current_password_label": "Nuværende adgangskode",
"local_auth_disable_local_device_protection_description": "Indtast din nuværende adgangskode for at deaktivere lokal enhedsbeskyttelse.",
"local_auth_disable_local_device_protection_title": "Deaktiver lokal enhedsbeskyttelse",
"local_auth_disable_protection_button": "Deaktiver beskyttelse",
"local_auth_enter_current_password_placeholder": "Indtast din nuværende adgangskode",
"local_auth_enter_new_password_placeholder": "Indtast en ny stærk adgangskode",
"local_auth_error_changing_password": "Der opstod en fejl under ændring af adgangskoden",
"local_auth_error_disabling_password": "Der opstod en fejl under deaktivering af adgangskoden",
"local_auth_error_enter_current_password": "Indtast venligst din nuværende adgangskode",
"local_auth_error_enter_new_password": "Indtast venligst en ny adgangskode",
"local_auth_error_enter_old_password": "Indtast venligst din gamle adgangskode",
"local_auth_error_enter_password": "Indtast venligst en adgangskode",
"local_auth_error_passwords_not_match": "Adgangskoderne stemmer ikke overens",
"local_auth_error_setting_password": "Der opstod en fejl under indstilling af adgangskoden",
"local_auth_new_password_label": "Ny adgangskode",
"local_auth_reenter_new_password_placeholder": "Indtast din nye adgangskode igen",
"local_auth_success_password_disabled_description": "Du har deaktiveret adgangskodebeskyttelsen for lokal adgang. Husk, at din enhed nu er mindre sikker.",
"local_auth_success_password_disabled_title": "Adgangskodebeskyttelse deaktiveret",
"local_auth_success_password_set_description": "Du har nu konfigureret lokal enhedsbeskyttelse. Din enhed er nu beskyttet mod uautoriseret lokal adgang.",
"local_auth_success_password_set_title": "Adgangskode indstillet",
"local_auth_success_password_updated_description": "Du har ændret din adgangskode til beskyttelse af din lokale enhed. Husk din nye adgangskode til senere brug.",
"local_auth_success_password_updated_title": "Adgangskode opdateret",
"local_auth_update_password_button": "Opdater adgangskode",
"locale_auto": "Auto",
"locale_change_success": "Sproget er ændret til {locale}",
"locale_da": "Dansk",
"locale_de": "Tysk",
"locale_en": "Engelsk",
"locale_es": "Spansk",
"locale_fr": "Fransk",
"locale_it": "Italiensk",
"locale_nb": "Norsk (bokmål)",
"locale_sv": "Svensk",
"locale_zh": "中文 (简体)",
"log_in": "Log ind",
"log_out": "Log ud",
"logged_in_as": "Logget ind som",
"login_enter_password": "Indtast din adgangskode",
"login_enter_password_description": "Indtast din adgangskode for at få adgang til din JetKVM.",
"login_error": "Der opstod en fejl under login",
"login_forgot_password": "Glemt adgangskode?",
"login_password_label": "Adgangskode",
"login_welcome_back": "Velkommen tilbage til JetKVM",
"macro_add_step": "Tilføj trin {maxed_out}",
"macro_at_least_one_step_keys_or_modifiers": "Mindst ét trin skal have nøgler eller modifikatorer",
"macro_at_least_one_step_required": "Mindst ét trin er påkrævet",
"macro_max_steps_error": "Du kan maksimalt tilføje {max} trin pr. makro.",
"macro_max_steps_reached": "( {max} maks)",
"macro_name_label": "Makronavn",
"macro_name_required": "Navn er påkrævet",
"macro_name_too_long": "Navnet skal være mindre end 50 tegn",
"macro_please_fix_validation_errors": "Ret venligst valideringsfejlene",
"macro_save": "Gem makro",
"macro_save_failed": "Der opstod en fejl under lagring.",
"macro_save_failed_error": "Der opstod en fejl under lagring: {error}.",
"macro_step_count": "{steps} / {max} trin",
"macro_step_duration_description": "Tid til at vente, før man udfører det næste trin.",
"macro_step_duration_label": "Trinvarighed",
"macro_step_keys_description": "Maksimalt antal {max} nøgler pr. trin.",
"macro_step_keys_label": "Nøgler",
"macro_step_max_keys_reached": "Maksimalt antal nøgler nået",
"macro_step_modifiers_description": "Hvilke modifikatorer (Shift/Ctrl/Alt/Meta) trykkes ned i dette trin?",
"macro_step_modifiers_label": "Modifikatorer",
"macro_step_no_matching_keys_found": "Ingen matchende nøgler fundet",
"macro_step_search_for_key": "Søg efter nøgle…",
"macro_steps_description": "Taster/modifikatorer udføres i rækkefølge med en forsinkelse mellem hvert trin.",
"macro_steps_label": "Trin",
"macros_add_description": "Opret en ny tastaturmakro",
"macros_add_new": "Tilføj ny makro",
"macros_add_new_macro": "Tilføj ny makro",
"macros_aria_add_new": "Tilføj ny makro",
"macros_aria_delete": "Slet makro {name}",
"macros_aria_duplicate": "Dupliker makro {name}",
"macros_aria_edit": "Rediger makro {name}",
"macros_aria_move_down": "Flyt {name} ned",
"macros_aria_move_up": "Flyt {name} op",
"macros_confirm_delete_description": "Er du sikker på, at du vil slette \" {name} \"? Denne handling kan ikke fortrydes.",
"macros_confirm_delete_title": "Slet makro",
"macros_confirm_deleting": "Sletter…",
"macros_create_first_description": "Kombinér tastetryk i én handling",
"macros_create_first_headline": "Opret din første makro",
"macros_created_success": "Makro \" {name} \" blev oprettet",
"macros_delay_only": "Kun forsinkelse",
"macros_delete_confirm": "Er du sikker på, at du vil slette denne makro? Denne handling kan ikke fortrydes.",
"macros_delete_macro": "Slet makro",
"macros_deleted_success": "Makro \" {name} \" slettet",
"macros_deleting": "Sletning",
"macros_duplicated_success": "Makro \" {name} \" duplikeret",
"macros_edit_button": "Redigere",
"macros_edit_description": "Rediger din tastaturmakro",
"macros_edit_title": "Rediger makro",
"macros_failed_create": "Kunne ikke oprette makro",
"macros_failed_create_error": "Kunne ikke oprette makro: {error}",
"macros_failed_delete": "Makroen kunne ikke slettes",
"macros_failed_delete_error": "Kunne ikke slette makroen: {error}",
"macros_failed_duplicate": "Makroen kunne ikke duplikeres",
"macros_failed_duplicate_error": "Kunne ikke duplikere makro: {error}",
"macros_failed_reorder": "Kunne ikke omarrangere makroer",
"macros_failed_reorder_error": "Kunne ikke omarrangere makroer: {error}",
"macros_failed_update": "Makroen kunne ikke opdateres",
"macros_failed_update_error": "Kunne ikke opdatere makroen: {error}",
"macros_invalid_data": "Ugyldige makrodata",
"macros_loading": "Indlæser makroer…",
"macros_max_reached": "Maksimum nået",
"macros_maximum_macros_reached": "Du har nået det maksimale antal {maximum} makroer.",
"macros_no_macros_available": "Ingen makroer tilgængelige",
"macros_order_updated": "Makroordren er blevet opdateret",
"macros_title": "Tastaturmakroer",
"macros_updated_success": "Makro \" {name} \" opdateret",
"metric_not_supported": "Metrik understøttes ikke",
"metric_waiting_for_data": "Venter på data…",
"mount_add_file_to_get_started": "Tilføj en fil for at komme i gang",
"mount_add_new_media": "Tilføj nyt medie",
"mount_available_storage": "Tilgængelig lagerplads",
"mount_button_back_to_overview": "Tilbage til oversigt",
"mount_button_cancel_upload": "Annuller upload",
"mount_button_continue_upload": "Fortsæt upload",
"mount_button_mount_file": "Monter fil",
"mount_button_mount_url": "Monterings-URL",
"mount_button_select": "Vælge",
"mount_button_showing_results": "Viser resultater fra {from} til {to} af resultaterne fra {total}",
"mount_button_upload_new_image": "Upload et nyt billede",
"mount_bytes_free": "{bytesFree} fri",
"mount_bytes_used": "{bytesUsed} brugt",
"mount_calculating": "Beregner…",
"mount_click_to_select_file": "Klik for at vælge en fil",
"mount_click_to_select_incomplete": "Klik for at vælge \" {name} \"",
"mount_confirm_delete": "Er du sikker på, at du vil slette {name} ?",
"mount_continue_uploading_with_name": "Fortsæt med at uploade \" {name} \"",
"mount_error_delete_file": "Fejl ved sletning af fil: {error}",
"mount_error_description": "Der opstod en fejl under forsøget på at montere mediet. Prøv igen.",
"mount_error_get_storage_space": "Fejl ved hentning af lagerplads: {error}",
"mount_error_list_storage": "Fejl ved liste over lagerfiler: {error}",
"mount_error_title": "Monteringsfejl",
"mount_get_state_error": "Kunne ikke hente virtuel medietilstand: {error}",
"mount_jetkvm_storage": "JetKVM-lagerbeslag",
"mount_jetkvm_storage_description": "Monter tidligere uploadede filer fra JetKVM-lageret",
"mount_mode_cdrom": "CD/DVD",
"mount_mode_disk": "Disk",
"mount_mounted_as": "Monteret som",
"mount_mounted_from_storage": "Monteret fra JetKVM-lager",
"mount_no_images_description": "Upload et billede for at starte montering af virtuel medie.",
"mount_no_images_title": "Ingen billeder tilgængelige",
"mount_no_mounted_media": "Ingen monterede medier",
"mount_percentage_used": "{percentageUsed} % brugt",
"mount_please_select_file": "Vælg venligst filen \" {name} \" for at fortsætte uploaden.",
"mount_popular_images": "Populære billeder",
"mount_streaming_from_url": "Streaming fra URL",
"mount_supported_formats": "Understøttede formater: ISO, IMG",
"mount_unmount": "Afmonter",
"mount_unmount_error": "Kunne ikke afmontere billede: {error}",
"mount_upload_description": "Vælg en billedfil, der skal uploades til JetKVM-lageret",
"mount_upload_error": "Uploadfejl: {error}",
"mount_upload_failed_datachannel": "Kunne ikke oprette datakanal til filupload",
"mount_upload_failed_rtc": "Upload mislykkedes: {error}",
"mount_upload_successful": "Uploaden er gennemført",
"mount_upload_title": "Upload nyt billede",
"mount_uploaded_has_been_uploaded": "{name} er blevet uploadet",
"mount_uploading": "Uploader…",
"mount_uploading_with_name": "Uploader {name}",
"mount_url_description": "Monter filer fra enhver offentlig webadresse",
"mount_url_input_label": "Billed-URL",
"mount_url_mount": "URL-montering",
"mount_view_device_description": "Vælg et billede, der skal monteres, fra JetKVM-lageret",
"mount_view_device_title": "Monter fra JetKVM-lager",
"mount_view_url_description": "Indtast en URL til den billedfil, der skal monteres",
"mount_view_url_title": "Monter fra URL",
"mount_virtual_media": "Virtuelle medier",
"mount_virtual_media_description": "Monter et billede for at starte fra eller installere et operativsystem.",
"mount_virtual_media_source": "Virtuel mediekilde",
"mount_virtual_media_source_description": "Vælg hvordan du vil montere dine virtuelle medier",
"mouse_alt_finger": "Fingerberøring af en skærm",
"mouse_alt_mouse": "Musikon",
"mouse_description": "Konfigurer markørens adfærd og interaktionsindstillinger for din enhed",
"mouse_hide_cursor_description": "Skjul markøren, når du sender musebevægelser",
"mouse_hide_cursor_title": "Skjul markør",
"mouse_jiggler_config_updated": "Jiggler-konfigurationen er blevet opdateret",
"mouse_jiggler_custom": "Tilpasset",
"mouse_jiggler_description": "Simuler bevægelsen af en computermus",
"mouse_jiggler_disabled": "Deaktiveret",
"mouse_jiggler_error_config": "Der opstod en fejl under indstilling af jiggler-konfigurationen",
"mouse_jiggler_failed_state": "Kunne ikke indstille jiggler-tilstand: {error}",
"mouse_jiggler_frequent": "Hyppig - 30'erne",
"mouse_jiggler_invalid_cron": "Ugyldigt cron-udtryk. Kontroller venligst dit tidsplanformat (f.eks. '0 * * * * *' for hvert minut).",
"mouse_jiggler_light": "Lys - 5m",
"mouse_jiggler_standard": "Standard - 1 m",
"mouse_jiggler_title": "Jiggler",
"mouse_mode_absolute": "Absolut",
"mouse_mode_absolute_description": "Mest bekvemme",
"mouse_mode_relative": "Relativ",
"mouse_mode_relative_description": "Mest kompatible",
"mouse_modes_description": "Vælg musens inputtilstand",
"mouse_modes_title": "Tilstande",
"mouse_scroll_high": "Høj",
"mouse_scroll_low": "Lav",
"mouse_scroll_medium": "Medium",
"mouse_scroll_off": "Slukket",
"mouse_scroll_throttling_description": "Reducer hyppigheden af rullehændelser",
"mouse_scroll_throttling_title": "Rulningsbegrænsning",
"mouse_scroll_very_high": "Meget høj",
"mouse_title": "Mus",
"network_custom_domain": "Brugerdefineret domæne",
"network_description": "Konfigurer dine netværksindstillinger",
"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_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",
"network_dhcp_lease_renew_confirm_new_b": "du skal muligvis genoprette forbindelsen ved hjælp af den nye adresse",
"network_dhcp_lease_renew_failed": "Kunne ikke forny leasing: {error}",
"network_dhcp_lease_renew_success": "DHCP-lease fornyet",
"network_domain_custom": "Tilpasset",
"network_domain_description": "Netværksdomænesuffiks for enheden",
"network_domain_dhcp_provided": "DHCP leveret",
"network_domain_local": ".lokal",
"network_domain_title": "Domæne",
"network_hostname_description": "Enhedsidentifikator på netværket. Tom for systemstandard",
"network_hostname_title": "Værtsnavn",
"network_http_proxy_description": "Proxyserver til udgående HTTP(S)-anmodninger fra enheden. Tom, hvis ingen er til stede.",
"network_http_proxy_invalid": "Ugyldig HTTP-proxy-URL",
"network_http_proxy_title": "HTTP-proxy",
"network_ipv4_address": "IPv4-adresse",
"network_ipv4_dns": "IPv4 DNS",
"network_ipv4_gateway": "IPv4-gateway",
"network_ipv4_invalid": "Ugyldig IPv4-adresse",
"network_ipv4_invalid_cidr": "Ugyldig CIDR-notation for IPv4-adresse",
"network_ipv4_mode_description": "Konfigurer IPv4-tilstanden",
"network_ipv4_mode_dhcp": "DHCP",
"network_ipv4_mode_static": "Statisk",
"network_ipv4_mode_title": "IPv4-tilstand",
"network_ipv4_netmask": "IPv4-netmaske",
"network_ipv6_addresses_header": "IPv6-adresser",
"network_ipv6_cidr_suggestion": "Brug venligst CIDR-notation (f.eks. 2001:db8::1/64)",
"network_ipv6_dns": "IPv6 DNS",
"network_ipv6_flag_dad_failed": "DAD mislykkedes",
"network_ipv6_flag_deprecated": "Udfaset",
"network_ipv6_gateway": "IPv6-gateway",
"network_ipv6_information": "IPv6-oplysninger",
"network_ipv6_invalid": "Ugyldig IPv6-adresse",
"network_ipv6_mode_description": "Konfigurer IPv6-tilstanden",
"network_ipv6_mode_dhcpv6": "DHCPv6",
"network_ipv6_mode_disabled": "Deaktiveret",
"network_ipv6_mode_link_local": "Kun link-lokal",
"network_ipv6_mode_slaac": "SLAAC",
"network_ipv6_mode_slaac_dhcpv6": "SLAAC + DHCPv6",
"network_ipv6_mode_static": "Statisk",
"network_ipv6_mode_title": "IPv6-tilstand",
"network_ipv6_prefix": "IP-præfiks",
"network_ipv6_prefix_invalid": "Præfikset skal være mellem 0 og 128",
"network_ll_dp_all": "Alle",
"network_ll_dp_basic": "Grundlæggende",
"network_ll_dp_description": "Styr hvilke TLV'er der sendes via Link Layer Discovery Protocol",
"network_ll_dp_disabled": "Deaktiveret",
"network_ll_dp_title": "LLDP",
"network_mac_address_copy_error": "Kunne ikke kopiere MAC-adressen",
"network_mac_address_copy_success": "MAC-adresse { mac } kopieret til udklipsholder",
"network_mac_address_description": "Hardware-identifikator for netværksgrænsefladen",
"network_mac_address_title": "MAC-adresse",
"network_mdns_auto": "Auto",
"network_mdns_description": "Styr mDNS (multicast DNS) driftstilstand",
"network_mdns_disabled": "Deaktiveret",
"network_mdns_ipv4_only": "Kun IPv4",
"network_mdns_ipv6_only": "Kun IPv6",
"network_mdns_title": "mDNS",
"network_no_information_description": "Ingen netværkskonfiguration tilgængelig",
"network_no_information_headline": "Netværksoplysninger",
"network_pending_dhcp_mode_change_description": "Gem indstillinger for at aktivere DHCP-tilstand og se leasingoplysninger",
"network_pending_dhcp_mode_change_headline": "Afventer ændring af DHCP IPv4-tilstand",
"network_save_settings": "Gem indstillinger",
"network_save_settings_apply_title": "Anvend netværksindstillinger",
"network_save_settings_confirm": "Anvend ændringer",
"network_save_settings_confirm_description": "Følgende netværksindstillinger vil blive anvendt. Disse ændringer kan kræve en genstart og forårsage en kortvarig afbrydelse.",
"network_save_settings_confirm_heading": "Konfigurationsændringer",
"network_save_settings_failed": "Kunne ikke gemme netværksindstillinger: {error}",
"network_save_settings_success": "Netværksindstillinger gemt",
"network_settings_add_dns": "Tilføj DNS-server",
"network_settings_load_error": "Kunne ikke indlæse netværksindstillinger: {error}",
"network_static_ipv4_header": "Statisk IPv4-konfiguration",
"network_static_ipv6_header": "Statisk IPv6-konfiguration",
"network_time_sync_description": "Konfigurer indstillinger for tidssynkronisering",
"network_time_sync_http_only": "Kun HTTP",
"network_time_sync_ntp_and_http": "NTP og HTTP",
"network_time_sync_ntp_only": "Kun NTP",
"network_time_sync_title": "Tidssynkronisering",
"network_title": "Netværk",
"never_seen_online": "Aldrig set online",
"next": "Næste",
"no_results_found": "Ingen resultater fundet",
"not_applicable": "Ikke tilgængelig",
"not_available": "Ikke tilgængelig",
"not_found": "Ikke fundet",
"ntp_servers": "NTP-servere",
"oh_no": "Åh nej!",
"online": "Online",
"other_session_detected": "Endnu en aktiv session registreret",
"other_session_take_over": " Kun én aktiv session understøttes ad gangen. Vil du overtage denne session?",
"other_session_use_here_button": "Brug her",
"page_not_found_description": "Den side, du ledte efter, findes ikke.",
"paste_modal_confirm_paste": "Bekræft indsætning",
"paste_modal_delay_between_keys": "Forsinkelse mellem taster",
"paste_modal_delay_out_of_range": "Forsinkelsen skal være mellem {min} og {max}",
"paste_modal_failed_paste": "Kunne ikke indsætte tekst: {error}",
"paste_modal_invalid_chars_intro": "Følgende tegn vil ikke blive indsat:",
"paste_modal_paste_from_host": "Indsæt fra vært",
"paste_modal_sending_using_layout": "Sender tekst ved hjælp af tastaturlayout: {iso} - {name}",
"paste_text": "Indsæt tekst",
"paste_text_description": "Indsæt tekst fra din klient til den eksterne vært",
"peer_connection_closed": "Lukket",
"peer_connection_closing": "Lukker",
"peer_connection_connected": "Forbundet",
"peer_connection_connecting": "Forbinder",
"peer_connection_disconnected": "Afbrudt",
"peer_connection_error": "Forbindelsesfejl",
"peer_connection_failed": "Forbindelsen mislykkedes",
"peer_connection_new": "Forbinder",
"previous": "Tidligere",
"register_device_error": "Der opstod en fejl {error} under registrering af din enhed.",
"register_device_finish_button": "Afslut opsætning",
"register_device_name_description": "Navngiv din enhed, så du nemt kan identificere den senere. Du kan til enhver tid ændre dette navn.",
"register_device_name_label": "Enhedsnavn",
"register_device_name_placeholder": "Plex-medieserver",
"register_device_no_name": "Angiv venligst et navn",
"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}",
"rename_device_new_name_label": "Nyt enhedsnavn",
"rename_device_new_name_placeholder": "Plex-medieserver",
"rename_device_no_name": "Angiv venligst et navn",
"retry": "Prøv igen",
"saving": "Gemmer…",
"search_placeholder": "Søge…",
"serial_console": "Seriel konsol",
"serial_console_baud_rate": "Baudhastighed",
"serial_console_configure_description": "Konfigurer dine serielle konsolindstillinger",
"serial_console_data_bits": "Databits",
"serial_console_get_settings_error": "Kunne ikke hente indstillinger for seriel konsol: {error}",
"serial_console_open_console": "Åbn konsol",
"serial_console_parity": "Paritet",
"serial_console_parity_even": "Lige paritet",
"serial_console_parity_mark": "Mark Paritet",
"serial_console_parity_none": "Ingen paritet",
"serial_console_parity_odd": "Ulige paritet",
"serial_console_parity_space": "Rumparitet",
"serial_console_set_settings_error": "Kunne ikke indstille seriel konsolindstillinger til {settings} : {error}",
"serial_console_stop_bits": "Stopbits",
"setting_remote_description": "Indstilling af fjernbetjeningsbeskrivelse",
"setting_remote_session_description": "Indstilling af beskrivelse af fjernsession...",
"setting_up_connection_to_device": "Opretter forbindelse til enhed...",
"settings_access": "Adgang",
"settings_advanced": "Fremskreden",
"settings_appearance": "Udseende",
"settings_back_to_kvm": "Tilbage til KVM",
"settings_general": "Generel",
"settings_hardware": "Hardware",
"settings_keyboard": "Tastatur",
"settings_keyboard_macros": "Tastaturmakroer",
"settings_mouse": "Mus",
"settings_network": "Netværk",
"settings_video": "Video",
"something_went_wrong": "Noget gik galt. Prøv igen senere, eller kontakt support.",
"step_counter_step": "Trin {step}",
"subnet_mask": "Undernetmaske",
"time_division_days": "dage",
"time_division_hours": "timer",
"time_division_minutes": "minutter",
"time_division_months": "måneder",
"time_division_seconds": "sekunder",
"time_division_weeks": "uger",
"time_division_years": "år",
"troubleshoot_connection": "Fejlfinding af forbindelse",
"unknown_error": "Ukendt fejl",
"update_in_progress": "Opdatering i gang",
"updates_failed_check": "Kunne ikke søge efter opdateringer: {error}",
"updates_failed_get_device_version": "Kunne ikke hente enhedsversion: {error}",
"updating_leave_device_on": "Sluk venligst ikke din enhed…",
"usb": "USB",
"usb_config_custom": "Tilpasset",
"usb_config_default": "JetKVM-standard",
"usb_config_dell": "Dell Multimedia Pro-tastatur",
"usb_config_failed_load": "Kunne ikke indlæse USB-konfiguration: {error}",
"usb_config_failed_set": "Kunne ikke indstille USB-konfiguration: {error}",
"usb_config_identifiers_description": "USB-enhedsidentifikatorer eksponeret for målcomputeren",
"usb_config_identifiers_title": "Identifikatorer",
"usb_config_logitech": "Logitech universaladapter",
"usb_config_manufacturer_label": "Fabrikant",
"usb_config_manufacturer_placeholder": "Indtast producent",
"usb_config_microsoft": "Microsoft trådløst multimedietastatur",
"usb_config_product_id_label": "Produkt-ID",
"usb_config_product_id_placeholder": "Indtast produkt-ID",
"usb_config_product_name_label": "Produktnavn",
"usb_config_product_name_placeholder": "Indtast produktnavn",
"usb_config_restore_default": "Gendan til standard",
"usb_config_serial_number_label": "Serienummer",
"usb_config_serial_number_placeholder": "Indtast serienummer",
"usb_config_set_success": "USB-konfiguration indstillet til {manufacturer} {product}",
"usb_config_update_identifiers": "Opdater USB-identifikatorer",
"usb_config_vendor_id_label": "Leverandør-ID",
"usb_config_vendor_id_placeholder": "Indtast leverandør-ID",
"usb_device_classes_description": "USB-enhedsklasser i den sammensatte enhed",
"usb_device_classes_title": "Klasser",
"usb_device_custom": "Tilpasset",
"usb_device_description": "USB-enheder, der skal emuleres på målcomputeren",
"usb_device_enable_absolute_mouse_description": "Aktivér absolut mus (markør)",
"usb_device_enable_absolute_mouse_title": "Aktivér absolut mus (markør)",
"usb_device_enable_keyboard_description": "Aktivér tastatur",
"usb_device_enable_keyboard_title": "Aktivér tastatur",
"usb_device_enable_mass_storage_description": "Nogle gange skal det muligvis deaktiveres for at forhindre problemer med bestemte enheder.",
"usb_device_enable_mass_storage_title": "Aktivér USB-masselagring",
"usb_device_enable_relative_mouse_description": "Aktivér relativ mus",
"usb_device_enable_relative_mouse_title": "Aktivér relativ mus",
"usb_device_failed_load": "Kunne ikke indlæse USB-enheder: {error}",
"usb_device_failed_set": "Kunne ikke indstille USB-enheder: {error}",
"usb_device_keyboard_mouse_and_mass_storage": "Tastatur, mus og masselagring",
"usb_device_keyboard_only": "Kun tastatur",
"usb_device_restore_default": "Gendan til standard",
"usb_device_title": "USB-enhed",
"usb_device_update_classes": "Opdater USB-klasser",
"usb_device_updated": "USB-enheder opdateret",
"usb_state_connected": "Forbundet",
"usb_state_connecting": "Forbinder",
"usb_state_disconnected": "Afbrudt",
"usb_state_low_power_mode": "Lavstrømstilstand",
"user_interface_language_description": "Vælg det sprog, der skal bruges i JetKVM-brugergrænsefladen",
"user_interface_language_title": "Grænsefladesprog",
"video_brightness_description": "Lysstyrkeniveau ( {value} x)",
"video_brightness_title": "Lysstyrke",
"video_contrast_description": "Kontrastniveau ( {value} x)",
"video_contrast_title": "Kontrast",
"video_custom_edid_description": "EDID beskriver kompatibilitet med videotilstande. Standardindstillingerne fungerer i de fleste tilfælde, men unikke UEFI/BIOS-indstillinger skal muligvis justeres.",
"video_custom_edid_title": "Brugerdefineret EDID",
"video_debugging_info_description": "Fejlfindingsoplysninger for video",
"video_debugging_info_title": "Fejlfindingsoplysninger",
"video_description": "Konfigurer skærmindstillinger og EDID for optimal kompatibilitet",
"video_edid_acer_b246wl": "Acer B246WL, 1920x1200",
"video_edid_asus_pa248qv": "ASUS PA248QV, 1920x1200",
"video_edid_custom": "Tilpasset",
"video_edid_dell_d2721h": "DELL D2721H, 1920x1080",
"video_edid_dell_idrac": "DELL IDRAC EDID, 1280x1024",
"video_edid_description": "Juster EDID-indstillingerne for skærmen",
"video_edid_file_label": "EDID-fil",
"video_edid_jetkvm_default": "JetKVM-standard",
"video_edid_set_success": "EDID er korrekt indstillet til {edid}",
"video_edid_title": "EDID",
"video_enhancement_description": "Juster farveindstillingerne for at gøre videooutputtet mere levende og farverigt",
"video_enhancement_title": "Videoforbedring",
"video_failed_get_debug_info": "Kunne ikke hente fejlfindingsoplysninger: {error}",
"video_failed_get_edid": "Kunne ikke hente EDID: {error}",
"video_failed_set_edid": "Kunne ikke indstille EDID: {error}",
"video_failed_set_stream_quality": "Kunne ikke indstille streamkvalitet: {error}",
"video_get_debugging_info": "Få fejlfindingsoplysninger",
"video_overlay_autoplay_permissions_required": "Tilladelser til automatisk afspilning kræves",
"video_overlay_conn_check_cables": "Kontroller alle kabelforbindelser for løse eller beskadigede ledninger",
"video_overlay_conn_ensure_network": "Sørg for, at din netværksforbindelse er stabil og aktiv",
"video_overlay_conn_restart": "Prøv at genstarte både enheden og din computer",
"video_overlay_conn_verify_power": "Kontroller, at enheden er tændt og korrekt tilsluttet",
"video_overlay_connection_issue_title": "Forbindelsesproblem registreret",
"video_overlay_enable_autoplay_settings": "Juster venligst browserindstillingerne for at aktivere automatisk afspilning",
"video_overlay_hdmi_error_title": "HDMI-signalfejl registreret.",
"video_overlay_hdmi_incompatible_resolution": "Inkompatible indstillinger for opløsning eller opdateringshastighed",
"video_overlay_hdmi_loose_faulty": "En løs eller defekt HDMI-forbindelse",
"video_overlay_hdmi_source_issue": "Problemer med kildeenhedens HDMI-udgang",
"video_overlay_learn_more": "Lær mere",
"video_overlay_loading_stream": "Indlæser videostream…",
"video_overlay_manually_start_stream": "Start stream manuelt",
"video_overlay_no_hdmi_adapter_compat": "Hvis du bruger en adapter, skal du sørge for, at den er kompatibel og fungerer korrekt.",
"video_overlay_no_hdmi_ensure_cable": "Sørg for, at HDMI-kablet er korrekt tilsluttet i begge ender",
"video_overlay_no_hdmi_ensure_power": "Sørg for, at kildeenheden er tændt og sender et signal",
"video_overlay_no_hdmi_signal": "Intet HDMI-signal registreret.",
"video_overlay_pointerlock_click_to_enable": "Klik på videoen for at aktivere musestyring",
"video_overlay_retrying_connection": "Prøver at oprette forbindelse igen…",
"video_overlay_troubleshooting_guide": "Fejlfindingsvejledning",
"video_overlay_try_again": "Prøv igen",
"video_pointer_lock_disabled": "Markørlås deaktiveret",
"video_pointer_lock_enabled": "Markørlås aktiveret — tryk på Escape for at låse op",
"video_quality_high": "Høj",
"video_quality_low": "Lav",
"video_quality_medium": "Medium",
"video_reset_to_default": "Nulstil til standard",
"video_restore_to_default": "Gendan til standard",
"video_saturation_description": "Farvemætning ( {value} x)",
"video_saturation_title": "Mætning",
"video_set_custom_edid": "Indstil brugerdefineret EDID",
"video_stream_quality_description": "Juster kvaliteten af videostreamen",
"video_stream_quality_set": "Streamkvalitet indstillet til {quality}",
"video_stream_quality_title": "Streamkvalitet",
"video_title": "Video",
"view_details": "Se detaljer",
"virtual_keyboard_header": "Virtuelt tastatur",
"wake_on_lan": "Vågn på LAN",
"wake_on_lan_add_device_device_name": "Enhedsnavn",
"wake_on_lan_add_device_example_device_name": "Plex-medieserver",
"wake_on_lan_add_device_mac_address": "MAC-adresse",
"wake_on_lan_add_device_save_device": "Gem enhed",
"wake_on_lan_description": "Send en magisk pakke for at vække en fjern enhed.",
"wake_on_lan_device_list_add_new_device": "Tilføj ny enhed",
"wake_on_lan_device_list_delete_device": "Slet enhed",
"wake_on_lan_device_list_wake": "Vågne",
"wake_on_lan_empty_add_device_to_start": "Tilføj en enhed for at begynde at bruge Wake-on-LAN",
"wake_on_lan_empty_add_new_device": "Tilføj ny enhed",
"wake_on_lan_empty_no_devices_added": "Ingen enheder tilføjet",
"wake_on_lan_failed_add_device": "Kunne ikke tilføje enhed",
"wake_on_lan_failed_send_magic": "Kunne ikke sende Magic Packet",
"wake_on_lan_invalid_mac": "Ugyldig MAC-adresse",
"wake_on_lan_magic_sent_success": "Magisk pakke sendt",
"welcome_to_jetkvm": "Velkommen til JetKVM",
"welcome_to_jetkvm_description": "Styr enhver computer eksternt"
}

View File

@ -0,0 +1,895 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"access_adopt_kvm": "KVM in die Cloud integrieren",
"access_adopted_message": "Ihr Gerät wird in die Cloud übernommen",
"access_auth_mode_no_password": "Aktueller Modus: Kein Passwort",
"access_auth_mode_password": "Aktueller Modus: Passwortgeschützt",
"access_authentication_mode_title": "Authentifizierungsmodus",
"access_certificate_label": "Zertifikat",
"access_change_password_button": "Kennwort ändern",
"access_change_password_description": "Aktualisieren Sie Ihr Gerätezugriffskennwort",
"access_change_password_title": "Kennwort ändern",
"access_cloud_api_url_label": "Cloud-API-URL",
"access_cloud_app_url_label": "Cloud-Anwendungs-URL",
"access_cloud_provider_description": "Wählen Sie den Cloud-Anbieter für Ihr Gerät",
"access_cloud_provider_title": "Cloud-Anbieter",
"access_cloud_security_title": "Cloud-Sicherheit",
"access_confirm_deregister": "Möchten Sie dieses Gerät wirklich abmelden?",
"access_deregister": "Abmelden von der Cloud",
"access_description": "Verwalten Sie die Zugriffskontrolle des Geräts",
"access_disable_protection": "Schutz deaktivieren",
"access_enable_password": "Kennwort aktivieren",
"access_failed_deregister": "Abmeldung des Geräts fehlgeschlagen: {error}",
"access_failed_update_cloud_url": "Fehler beim Aktualisieren der Cloud-URL: {error}",
"access_failed_update_tls": "TLS-Einstellungen konnten nicht aktualisiert werden: {error}",
"access_github_link": "GitHub",
"access_https_description": "Konfigurieren Sie den sicheren HTTPS-Zugriff auf Ihr Gerät",
"access_https_mode_title": "HTTPS-Modus",
"access_learn_security": "Erfahren Sie mehr über unsere Cloud-Sicherheit",
"access_local_description": "Verwalten Sie den Modus des lokalen Zugriffs auf das Gerät",
"access_local_title": "Lokal",
"access_no_device_id": "Keine Geräte-ID verfügbar",
"access_private_key_description": "Aus Sicherheitsgründen wird es nach dem Speichern nicht angezeigt.",
"access_private_key_label": "Privater Schlüssel",
"access_provider_custom": "Benutzerdefiniert",
"access_provider_jetkvm": "JetKVM Cloud",
"access_remote_description": "Verwalten Sie den Modus des Fernzugriffs auf das Gerät",
"access_security_encryption": "Ende-zu-Ende-Verschlüsselung mit WebRTC (DTLS und SRTP)",
"access_security_oidc": "OIDC (OpenID Connect)-Authentifizierung",
"access_security_open_source": "Alle Cloud-Komponenten sind Open Source und auf GitHub verfügbar.",
"access_security_streams": "Alle Streams werden während der Übertragung verschlüsselt",
"access_security_zero_trust": "Zero Trust-Sicherheitsmodell",
"access_title": "Zugang",
"access_tls_certificate_description": "Fügen Sie unten Ihr TLS-Zertifikat ein. Geben Sie bei Zertifikatsketten die gesamte Kette an (Blatt-, Zwischen- und Stammzertifikate).",
"access_tls_certificate_title": "TLS-Zertifikat",
"access_tls_custom": "Benutzerdefiniert",
"access_tls_disabled": "Deaktiviert",
"access_tls_self_signed": "Selbstsigniert",
"access_tls_updated": "TLS-Einstellungen erfolgreich aktualisiert",
"access_update_tls_settings": "TLS-Einstellungen aktualisieren",
"action_bar_connection_stats": "Verbindungsstatistiken",
"action_bar_extension": "Verlängerung",
"action_bar_fullscreen": "Vollbild",
"action_bar_settings": "Einstellungen",
"action_bar_virtual_keyboard": "Virtuelle Tastatur",
"action_bar_virtual_media": "Virtuelle Medien",
"action_bar_wake_on_lan": "Wake-on-LAN",
"action_bar_web_terminal": "Web-Terminal",
"advanced_description": "Greifen Sie auf zusätzliche Einstellungen zur Fehlerbehebung und Anpassung zu",
"advanced_dev_channel_description": "Erhalten Sie frühzeitig Updates vom Entwicklungskanal",
"advanced_dev_channel_title": "Dev Channel-Updates",
"advanced_developer_mode_description": "Erweiterte Funktionen für Entwickler aktivieren",
"advanced_developer_mode_enabled_title": "Entwicklermodus aktiviert",
"advanced_developer_mode_title": "Entwicklermodus",
"advanced_developer_mode_warning_advanced": "Nur für fortgeschrittene Benutzer. Nicht für den Produktionseinsatz.",
"advanced_developer_mode_warning_risks": "Verwenden Sie es nur, wenn Sie die Risiken verstehen",
"advanced_developer_mode_warning_security": "Die Sicherheit wird im aktiven Zustand geschwächt",
"advanced_disable_usb_emulation": "USB-Emulation deaktivieren",
"advanced_enable_usb_emulation": "USB-Emulation aktivieren",
"advanced_error_loopback_disable": "Der Nur-Loopback-Modus konnte nicht deaktiviert werden: {error}",
"advanced_error_loopback_enable": "Der Nur-Loopback-Modus konnte nicht aktiviert werden: {error}",
"advanced_error_reset_config": "Konfiguration konnte nicht zurückgesetzt werden: {error}",
"advanced_error_set_dev_channel": "Der Dev-Kanalstatus konnte nicht festgelegt werden: {error}",
"advanced_error_set_dev_mode": "Fehler beim Festlegen des Entwicklungsmodus: {error}",
"advanced_error_update_ssh_key": "SSH-Schlüssel konnte nicht aktualisiert werden: {error}",
"advanced_error_usb_emulation_disable": "USB-Emulation konnte nicht deaktiviert werden: {error}",
"advanced_error_usb_emulation_enable": "USB-Emulation konnte nicht aktiviert werden: {error}",
"advanced_loopback_only_description": "Beschränken Sie den Zugriff auf die Weboberfläche nur auf den lokalen Host (127.0.0.1).",
"advanced_loopback_only_title": "Nur-Loopback-Modus",
"advanced_loopback_warning_before": "Bevor Sie diese Funktion aktivieren, stellen Sie sicher, dass Sie über Folgendes verfügen:",
"advanced_loopback_warning_cloud": "Cloud-Zugriff aktiviert und funktioniert",
"advanced_loopback_warning_confirm": "Ich verstehe, trotzdem aktivieren",
"advanced_loopback_warning_description": "WARNUNG: Dadurch wird der Zugriff auf die Weboberfläche ausschließlich auf den lokalen Host (127.0.0.1) beschränkt.",
"advanced_loopback_warning_ssh": "SSH-Zugriff konfiguriert und getestet",
"advanced_loopback_warning_title": "Nur-Loopback-Modus aktivieren?",
"advanced_reset_config_button": "Konfiguration zurücksetzen",
"advanced_reset_config_description": "Konfiguration auf Standard zurücksetzen. Dadurch werden Sie abgemeldet.",
"advanced_reset_config_title": "Konfiguration zurücksetzen",
"advanced_ssh_access_description": "Fügen Sie Ihren öffentlichen SSH-Schlüssel hinzu, um einen sicheren Fernzugriff auf das Gerät zu ermöglichen",
"advanced_ssh_access_title": "SSH-Zugriff",
"advanced_ssh_default_user": "Der Standard-SSH-Benutzer ist",
"advanced_ssh_public_key_label": "Öffentlicher SSH-Schlüssel",
"advanced_ssh_public_key_placeholder": "Geben Sie Ihren öffentlichen SSH-Schlüssel ein",
"advanced_success_loopback_disabled": "Nur-Loopback-Modus deaktiviert. Starten Sie Ihr Gerät neu, um die Funktion anzuwenden.",
"advanced_success_loopback_enabled": "Nur-Loopback-Modus aktiviert. Starten Sie Ihr Gerät neu, um die Funktion anzuwenden.",
"advanced_success_reset_config": "Konfiguration erfolgreich auf Standard zurückgesetzt",
"advanced_success_update_ssh_key": "SSH-Schlüssel erfolgreich aktualisiert",
"advanced_title": "Fortschrittlich",
"advanced_troubleshooting_mode_description": "Diagnosetools und zusätzliche Steuerelemente für Fehlerbehebungs- und Entwicklungszwecke",
"advanced_troubleshooting_mode_title": "Fehlerbehebungsmodus",
"advanced_update_ssh_key_button": "SSH-Schlüssel aktualisieren",
"advanced_usb_emulation_description": "Steuern des USB-Emulationsstatus",
"advanced_usb_emulation_title": "USB-Emulation",
"already_adopted_new_owner": "Wenn Sie der neue Besitzer sind, bitten Sie den Vorbesitzer, das Gerät im Cloud-Dashboard von seinem Konto abzumelden. Wenn Sie glauben, dass dies ein Fehler ist, wenden Sie sich an unser Support-Team.",
"already_adopted_other_user": "Dieses Gerät ist derzeit in unserem Cloud-Dashboard auf einen anderen Benutzer registriert.",
"already_adopted_return_to_dashboard": "Zurück zum Dashboard",
"already_adopted_title": "Gerät bereits registriert",
"appearance_description": "Wählen Sie Ihr bevorzugtes Farbthema",
"appearance_page_description": "Passen Sie das Erscheinungsbild Ihrer JetKVM-Schnittstelle an",
"appearance_theme": "Thema",
"appearance_theme_dark": "Dunkel",
"appearance_theme_light": "Hell",
"appearance_theme_system": "System",
"appearance_title": "Aussehen",
"attach": "Befestigen",
"atx_power_control_get_state_error": "ATX-Stromversorgungsstatus konnte nicht abgerufen werden: {error}",
"atx_power_control_hdd_led": "Festplatten-LED",
"atx_power_control_long_power_button": "Langes Drücken",
"atx_power_control_power_button": "Leistung",
"atx_power_control_power_led": "Betriebs-LED",
"atx_power_control_reset_button": "Zurücksetzen",
"atx_power_control_send_action_error": "ATX-Stromversorgungsaktion {action} konnte nicht gesendet werden: {error}",
"atx_power_control_short_power_button": "Kurzes Drücken",
"auth_authentication_mode": "Bitte wählen Sie einen Authentifizierungsmodus",
"auth_authentication_mode_error": "Beim Einstellen des Authentifizierungsmodus ist ein Fehler aufgetreten",
"auth_authentication_mode_invalid": "Ungültiger Authentifizierungsmodus",
"auth_connect_to_cloud": "Verbinden Sie Ihr JetKVM mit der Cloud",
"auth_connect_to_cloud_action": "Anmelden und Gerät verbinden",
"auth_connect_to_cloud_description": "Schalten Sie den Fernzugriff und erweiterte Funktionen für Ihr Gerät frei",
"auth_header_cta_already_have_account": "Hast du schon ein Konto?",
"auth_header_cta_dont_have_account": "Sie haben noch kein Konto?",
"auth_header_cta_new_to_jetkvm": "Neu bei JetKVM?",
"auth_login": "Melden Sie sich bei Ihrem JetKVM-Konto an",
"auth_login_action": "Einloggen",
"auth_login_description": "Melden Sie sich an, um sicher auf Ihre Geräte zuzugreifen und sie zu verwalten",
"auth_mode_local": "Lokale Authentifizierungsmethode",
"auth_mode_local_change_later": "Sie können Ihre Authentifizierungsmethode später jederzeit in den Einstellungen ändern.",
"auth_mode_local_description": "Wählen Sie aus, wie Sie Ihr JetKVM-Gerät lokal sichern möchten.",
"auth_mode_local_no_password": "Kein Passwort",
"auth_mode_local_no_password_description": "Schneller Zugriff ohne Passwortauthentifizierung.",
"auth_mode_local_password": "Passwort",
"auth_mode_local_password_confirm_description": "Bestätigen Sie Ihr Passwort",
"auth_mode_local_password_confirm_label": "Passwort bestätigen",
"auth_mode_local_password_description": "Sichern Sie Ihr Gerät für zusätzlichen Schutz mit einem Passwort.",
"auth_mode_local_password_failed_set": "Kennwort konnte nicht festgelegt werden: {error}",
"auth_mode_local_password_note": "Dieses Passwort wird verwendet, um Ihre Gerätedaten zu sichern und vor unbefugtem Zugriff zu schützen.",
"auth_mode_local_password_note_local": "Alle Daten verbleiben auf Ihrem lokalen Gerät.",
"auth_mode_local_password_set": "Legen Sie ein Passwort fest",
"auth_mode_local_password_set_button": "Passwort festlegen",
"auth_mode_local_password_set_description": "Erstellen Sie ein sicheres Passwort, um Ihr JetKVM-Gerät lokal zu sichern.",
"auth_mode_local_password_set_label": "Geben Sie ein Passwort ein",
"auth_signup_connect_to_cloud_action": "Anmelden und Gerät verbinden",
"auth_signup_create_account": "Erstellen Sie Ihr JetKVM-Konto",
"auth_signup_create_account_action": "Benutzerkonto erstellen",
"auth_signup_create_account_description": "Erstellen Sie Ihr Konto und beginnen Sie mit der einfachen Verwaltung Ihrer Geräte.",
"back": "Zurück",
"back_to_devices": "Zurück zu Geräte",
"cancel": "Stornieren",
"close": "Schließen",
"cloud_kvms": "Cloud-KVMs",
"cloud_kvms_description": "Verwalten Sie Ihre Cloud-KVMs und stellen Sie eine sichere Verbindung zu ihnen her.",
"cloud_kvms_no_devices": "Keine Geräte gefunden",
"cloud_kvms_no_devices_description": "Sie haben noch keine Geräte mit aktivierter JetKVM Cloud.",
"confirm": "Bestätigen",
"connect_to_kvm": "Mit KVM verbinden",
"connecting_to_device": "Verbindung zum Gerät wird hergestellt…",
"connection_established": "Verbindung hergestellt",
"connection_stats_badge_jitter": "Jitter",
"connection_stats_badge_jitter_buffer_avg_delay": "Jitter-Puffer Durchschnittliche Verzögerung",
"connection_stats_connection": "Verbindung",
"connection_stats_connection_description": "Die Verbindung zwischen dem Client und dem JetKVM.",
"connection_stats_frames_per_second": "Bilder pro Sekunde",
"connection_stats_frames_per_second_description": "Anzahl der pro Sekunde angezeigten eingehenden Videobilder.",
"connection_stats_network_stability": "Netzwerkstabilität",
"connection_stats_network_stability_description": "Wie gleichmäßig der Fluss eingehender Videopakete im Netzwerk ist.",
"connection_stats_packets_lost": "Verlorene Pakete",
"connection_stats_packets_lost_description": "Anzahl der verlorenen eingehenden Video-RTP-Pakete.",
"connection_stats_playback_delay": "Wiedergabeverzögerung",
"connection_stats_playback_delay_description": "Durch den Jitter-Puffer hinzugefügte Verzögerung, um die Wiedergabe zu glätten, wenn die Frames ungleichmäßig ankommen.",
"connection_stats_round_trip_time": "Round-Trip-Zeit",
"connection_stats_round_trip_time_description": "Roundtrip-Zeit für das aktive ICE-Kandidatenpaar zwischen Peers.",
"connection_stats_sidebar": "Verbindungsstatistiken",
"connection_stats_unit_frames_per_second": " fps",
"connection_stats_unit_milliseconds": " ms",
"connection_stats_unit_packets": " Pakete",
"connection_stats_video": "Video",
"connection_stats_video_description": "Der Videostream vom JetKVM zum Client.",
"continue": "Weitermachen",
"creating_peer_connection": "Peer-Verbindung wird hergestellt …",
"dc_power_control_current": "Aktuell",
"dc_power_control_current_unit": "A",
"dc_power_control_get_state_error": "Der Gleichstromstatus konnte nicht abgerufen werden: {error}",
"dc_power_control_power": "Leistung",
"dc_power_control_power_off_button": "Ausschalten",
"dc_power_control_power_off_state": "Ausschalten",
"dc_power_control_power_on_button": "Einschalten",
"dc_power_control_power_on_state": "Einschalten",
"dc_power_control_power_unit": "W",
"dc_power_control_restore_last_state": "Letzter Zustand",
"dc_power_control_restore_power_state": "Wiederherstellung nach Stromausfall",
"dc_power_control_set_power_state_error": "Der DC-Stromversorgungsstatus konnte nicht an {enabled} werden: {error}",
"dc_power_control_set_restore_state_error": "Der Status zur Wiederherstellung der Gleichstromversorgung konnte nicht an {state} gesendet werden: {error}",
"dc_power_control_voltage": "Stromspannung",
"dc_power_control_voltage_unit": "V",
"delete": "Löschen",
"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.",
"deregister_from_cloud": "Abmelden von der Cloud",
"deregister_headline": "Melden Sie {device}",
"detach": "Abtrennen",
"dhcp_empty_lease_description": "Wir haben noch keine DHCP-Lease-Informationen vom Gerät erhalten.",
"dhcp_empty_lease_headline": "Keine DHCP-Lease-Informationen",
"dhcp_lease_boot_file": "Boot-Datei",
"dhcp_lease_boot_next_server": "Nächsten Server starten",
"dhcp_lease_boot_server_name": "Name des Boot-Servers",
"dhcp_lease_broadcast": "Übertragen",
"dhcp_lease_domain": "Domain",
"dhcp_lease_gateway": "Tor",
"dhcp_lease_header": "DHCP-Lease-Informationen",
"dhcp_lease_hostname": "Hostname",
"dhcp_lease_lease_expires": "Mietvertrag läuft ab",
"dhcp_lease_maximum_transfer_unit": "MTU",
"dhcp_lease_renew": "DHCP-Lease erneuern",
"dhcp_lease_time_to_live": "TTL",
"dhcp_server": "DHCP-Server",
"dns_servers": "DNS-Server",
"establishing_secure_connection": "Sichere Verbindung wird hergestellt …",
"experimental": "Experimental",
"extension_popover_load_and_manage_extensions": "Laden und verwalten Sie Ihre Erweiterungen",
"extension_popover_set_error_notification": "Fehler beim Festlegen der aktiven Erweiterung: {error}",
"extension_popover_unload_extension": "Erweiterung entladen",
"extension_serial_console": "Serielle Konsole",
"extension_serial_console_description": "Greifen Sie auf Ihre serielle Konsolenerweiterung zu",
"extensions_atx_power_control": "ATX-Stromsteuerung",
"extensions_atx_power_control_description": "Steuern Sie den Energiezustand Ihrer Maschine über die ATX-Energiesteuerung.",
"extensions_dc_power_control": "Gleichstromsteuerung",
"extensions_dc_power_control_description": "Steuern Sie Ihre DC-Stromerweiterung",
"extensions_popover_extensions": "Erweiterungen",
"gathering_ice_candidates": "ICE-Kandidaten zusammenbringen …",
"general_app_version": "App: {version}",
"general_auto_update_description": "Aktualisieren Sie das Gerät automatisch auf die neueste Version",
"general_auto_update_error": "Automatische Aktualisierung konnte nicht eingestellt werden: {error}",
"general_auto_update_title": "Automatische Aktualisierung",
"general_check_for_updates": "Nach Updates suchen",
"general_page_description": "Geräteeinstellungen konfigurieren und Voreinstellungen aktualisieren",
"general_reboot_description": "Möchten Sie mit dem Neustart des Systems fortfahren?",
"general_reboot_device": "Gerät neu starten",
"general_reboot_device_description": "Schalten Sie den JetKVM aus und wieder ein",
"general_reboot_no_button": "NEIN",
"general_reboot_title": "Starten Sie JetKVM neu",
"general_reboot_yes_button": "Ja",
"general_system_version": "System: {version}",
"general_title": "Allgemein",
"general_update_app_update_title": "App-Update",
"general_update_application_type": "App",
"general_update_available_description": "Ein neues Update zur Verbesserung der Systemleistung und Kompatibilität ist verfügbar. Wir empfehlen die Aktualisierung, um einen reibungslosen Betrieb zu gewährleisten.",
"general_update_available_title": "Update verfügbar",
"general_update_background_button": "Aktualisierung im Hintergrund",
"general_update_check_again_button": "Erneut prüfen",
"general_update_checking_description": "Wir stellen sicher, dass Ihr Gerät über die neuesten Funktionen und Verbesserungen verfügt.",
"general_update_checking_title": "Suche nach Updates…",
"general_update_completed_description": "Ihr Gerät wurde erfolgreich auf die neueste Version aktualisiert. Viel Spaß mit den neuen Funktionen und Verbesserungen!",
"general_update_completed_title": "Update erfolgreich abgeschlossen",
"general_update_error_description": "Beim Aktualisieren Ihres Geräts ist ein Fehler aufgetreten. Bitte versuchen Sie es später noch einmal.",
"general_update_error_details": "Fehlerdetails: {errorMessage}",
"general_update_error_title": "Aktualisierungsfehler",
"general_update_later_button": "Mach es später",
"general_update_now_button": "Jetzt aktualisieren",
"general_update_rebooting": "Neustart zum Abschließen des Updates …",
"general_update_status_awaiting_reboot": "Warte auf Neustart",
"general_update_status_downloading": "Das Update {update_type} wird heruntergeladen…",
"general_update_status_fetching": "Update-Informationen werden abgerufen …",
"general_update_status_installing": "Das Update {update_type} wird installiert…",
"general_update_status_progress": "{part} Fortschritt",
"general_update_status_verifying": "Überprüfung des Updates {update_type} …",
"general_update_system_type": "System",
"general_update_system_update_title": "Linux-Systemupdate",
"general_update_up_to_date_description": "Auf Ihrem System läuft die neueste Version. Derzeit sind keine Updates verfügbar.",
"general_update_up_to_date_title": "Das System ist auf dem neuesten Stand",
"general_update_updating_description": "Bitte schalten Sie Ihr Gerät nicht aus. Dieser Vorgang kann einige Minuten dauern.",
"general_update_updating_title": "Aktualisieren Ihres Geräts",
"getting_remote_session_description": "Versuch, eine Beschreibung der Remote-Sitzung abzurufen {attempt}",
"hardware_backlight_settings_error": "Fehler beim Festlegen der Hintergrundbeleuchtungseinstellungen: {error}",
"hardware_backlight_settings_get_error": "Die Einstellungen für die Hintergrundbeleuchtung konnten nicht abgerufen werden: {error}",
"hardware_backlight_settings_success": "Hintergrundbeleuchtungseinstellungen erfolgreich aktualisiert",
"hardware_dim_display_after_description": "Legen Sie fest, wie lange gewartet werden soll, bevor das Display gedimmt wird",
"hardware_dim_display_after_title": "Anzeige dimmen nach",
"hardware_display_brightness_description": "Stellen Sie die Helligkeit des Displays ein",
"hardware_display_brightness_high": "Hoch",
"hardware_display_brightness_low": "Niedrig",
"hardware_display_brightness_medium": "Medium",
"hardware_display_brightness_off": "Aus",
"hardware_display_brightness_title": "Displayhelligkeit",
"hardware_display_orientation_description": "Stellen Sie die Ausrichtung der Anzeige ein",
"hardware_display_orientation_error": "Fehler beim Festlegen der Anzeigeausrichtung: {error}",
"hardware_display_orientation_inverted": "Invertiert",
"hardware_display_orientation_normal": "Normal",
"hardware_display_orientation_success": "Displayausrichtung erfolgreich aktualisiert",
"hardware_display_orientation_title": "Anzeigeausrichtung",
"hardware_display_wake_up_note": "Das Display wird aktiviert, wenn sich der Verbindungsstatus ändert oder wenn es berührt wird.",
"hardware_page_description": "Konfigurieren Sie Anzeigeeinstellungen und Hardwareoptionen für Ihr JetKVM-Gerät",
"hardware_power_saving_description": "Reduzieren Sie den Stromverbrauch bei Nichtgebrauch",
"hardware_power_saving_disabled": "Energiesparmodus deaktiviert",
"hardware_power_saving_enabled": "Energiesparmodus aktiviert",
"hardware_power_saving_failed_error": "Fehler beim Einstellen des Energiesparmodus: {error}",
"hardware_power_saving_hdmi_sleep_description": "Schalten Sie die Aufnahme nach 90 Sekunden Inaktivität aus",
"hardware_power_saving_hdmi_sleep_title": "HDMI-Ruhemodus",
"hardware_power_saving_title": "Energiesparen",
"hardware_time_10_minutes": "10 Minuten",
"hardware_time_1_hour": "1 Stunde",
"hardware_time_1_minute": "1 Minute",
"hardware_time_30_minutes": "30 Minuten",
"hardware_time_5_minutes": "5 Minuten",
"hardware_time_never": "Niemals",
"hardware_title": "Hardware",
"hardware_turn_off_display_after_description": "Zeitraum der Inaktivität, bevor sich das Display automatisch ausschaltet",
"hardware_turn_off_display_after_title": "Display ausschalten nach",
"hide": "Verstecken",
"ice_gathering_completed": "ICE-Treffen abgeschlossen",
"info_caps_lock": "Feststelltaste",
"info_compose": "Komponieren",
"info_hdmi_state": "HDMI-Status:",
"info_hidrpc_state": "HidRPC-Status:",
"info_kana": "Kana",
"info_keys": "Schlüssel:",
"info_last_move": "Letzter Zug:",
"info_num_lock": "Num Lock",
"info_paste_enabled": "Ermöglicht",
"info_paste_mode": "Einfügemodus:",
"info_pointer": "Zeiger:",
"info_relayed_by_cloudflare": "Weitergeleitet von Cloudflare",
"info_resolution": "Auflösung:",
"info_scroll_lock": "Rollen-Taste",
"info_shift": "Schicht",
"info_usb_state": "USB-Status:",
"info_video_size": "Videogröße:",
"input_disabled": "Eingabe deaktiviert",
"invalid_password": "Ungültiges Passwort",
"ip_address": "IP-Adresse",
"ipv6_address_label": "Adresse",
"ipv6_gateway": "Tor",
"ipv6_information": "IPv6-Informationen",
"ipv6_link_local": "Link-lokal",
"ipv6_preferred_lifetime": "Bevorzugte Lebensdauer",
"ipv6_valid_lifetime": "Gültig für die gesamte Lebensdauer",
"jetkvm_description": "JetKVM kombiniert leistungsstarke Hardware mit intuitiver Software, um ein nahtloses Fernsteuerungserlebnis zu bieten.",
"jetkvm_device": "JetKVM-Gerät",
"jetkvm_logo": "JetKVM Logo",
"jetkvm_setup": "Richten Sie Ihr JetKVM ein",
"jiggler_cron_schedule_description": "Cron-Ausdruck für die Planung",
"jiggler_cron_schedule_label": "Cron-Zeitplan",
"jiggler_example_business_hours_early": "Öffnungszeiten 8-17",
"jiggler_example_business_hours_late": "Öffnungszeiten 9-17",
"jiggler_examples_label": "Beispiele",
"jiggler_inactivity_limit_description": "Inaktivitätszeit vor dem Wackeln",
"jiggler_inactivity_limit_label": "Inaktivitätslimit in Sekunden",
"jiggler_more_examples": "Weitere Beispiele",
"jiggler_random_delay_description": "Um erkennbare Muster zu vermeiden",
"jiggler_random_delay_label": "Zufällige Verzögerung",
"jiggler_save_jiggler_config": "Jiggler-Konfiguration speichern",
"jiggler_timezone_description": "Zeitzone für Cron-Zeitplan",
"jiggler_timezone_label": "Zeitzone",
"keyboard_description": "Konfigurieren Sie die Tastatureinstellungen für Ihr Gerät",
"keyboard_layout_description": "Tastaturlayout des Zielbetriebssystems",
"keyboard_layout_error": "Tastaturlayout konnte nicht festgelegt werden: {error}",
"keyboard_layout_long_description": "Die virtuelle Tastatur, das Einfügen von Text und Tastaturmakros senden einzelne Tastenanschläge an das Zielgerät. Das Tastaturlayout bestimmt, welche Tastencodes gesendet werden. Stellen Sie sicher, dass das Tastaturlayout in JetKVM mit den Einstellungen im Betriebssystem übereinstimmt.",
"keyboard_layout_success": "Tastaturlayout erfolgreich auf {layout} eingestellt",
"keyboard_layout_title": "Tastaturlayout",
"keyboard_show_pressed_keys_description": "Anzeige der aktuell gedrückten Tasten in der Statusleiste",
"keyboard_show_pressed_keys_title": "Gedrückte Tasten anzeigen",
"keyboard_title": "Tastatur",
"kvm_terminal": "KVM-Terminal",
"last_online": "Zuletzt online {time}",
"learn_more": "Mehr erfahren",
"load": "Laden",
"loading": "Laden…",
"local_auth_change_local_device_password_description": "Geben Sie Ihr aktuelles Passwort und ein neues Passwort ein, um den Schutz Ihres lokalen Geräts zu aktualisieren.",
"local_auth_change_local_device_password_title": "Ändern des lokalen Gerätekennworts",
"local_auth_confirm_new_password_label": "Neues Passwort bestätigen",
"local_auth_create_confirm_password_placeholder": "Geben Sie Ihr Passwort erneut ein",
"local_auth_create_description": "Erstellen Sie ein Passwort, um Ihr Gerät vor unbefugtem lokalem Zugriff zu schützen.",
"local_auth_create_new_password_label": "Neues Passwort",
"local_auth_create_new_password_placeholder": "Geben Sie ein sicheres Passwort ein",
"local_auth_create_not_now_button": "Nicht jetzt",
"local_auth_create_secure_button": "Sicheres Gerät",
"local_auth_create_title": "Lokaler Geräteschutz",
"local_auth_current_password_label": "Aktuelles Passwort",
"local_auth_disable_local_device_protection_description": "Geben Sie Ihr aktuelles Passwort ein, um den lokalen Geräteschutz zu deaktivieren.",
"local_auth_disable_local_device_protection_title": "Lokalen Geräteschutz deaktivieren",
"local_auth_disable_protection_button": "Schutz deaktivieren",
"local_auth_enter_current_password_placeholder": "Geben Sie Ihr aktuelles Passwort ein",
"local_auth_enter_new_password_placeholder": "Geben Sie ein neues sicheres Passwort ein",
"local_auth_error_changing_password": "Beim Ändern des Passworts ist ein Fehler aufgetreten",
"local_auth_error_disabling_password": "Beim Deaktivieren des Passworts ist ein Fehler aufgetreten",
"local_auth_error_enter_current_password": "Bitte geben Sie Ihr aktuelles Passwort ein",
"local_auth_error_enter_new_password": "Bitte geben Sie ein neues Passwort ein",
"local_auth_error_enter_old_password": "Bitte geben Sie Ihr altes Passwort ein",
"local_auth_error_enter_password": "Bitte geben Sie ein Passwort ein",
"local_auth_error_passwords_not_match": "Passwörter stimmen nicht überein",
"local_auth_error_setting_password": "Beim Festlegen des Passworts ist ein Fehler aufgetreten",
"local_auth_new_password_label": "Neues Passwort",
"local_auth_reenter_new_password_placeholder": "Geben Sie Ihr neues Passwort erneut ein",
"local_auth_success_password_disabled_description": "Sie haben den Passwortschutz für den lokalen Zugriff erfolgreich deaktiviert. Bedenken Sie, dass Ihr Gerät nun weniger sicher ist.",
"local_auth_success_password_disabled_title": "Kennwortschutz deaktiviert",
"local_auth_success_password_set_description": "Sie haben den lokalen Geräteschutz erfolgreich eingerichtet. Ihr Gerät ist nun vor unbefugtem lokalen Zugriff geschützt.",
"local_auth_success_password_set_title": "Passwort erfolgreich festgelegt",
"local_auth_success_password_updated_description": "Sie haben Ihr lokales Geräteschutzkennwort erfolgreich geändert. Merken Sie sich das neue Kennwort für zukünftige Zugriffe.",
"local_auth_success_password_updated_title": "Passwort erfolgreich aktualisiert",
"local_auth_update_password_button": "Kennwort aktualisieren",
"locale_auto": "Auto",
"locale_change_success": "Die Sprache wurde erfolgreich in {locale} geändert.",
"locale_da": "Dänisch",
"locale_de": "Deutsch",
"locale_en": "Englisch",
"locale_es": "Spanisch",
"locale_fr": "Französisch",
"locale_it": "Italienisch",
"locale_nb": "Norwegisch (bokmål)",
"locale_sv": "Schwedisch",
"locale_zh": "中文 (简体)",
"log_in": "Einloggen",
"log_out": "Ausloggen",
"logged_in_as": "Angemeldet als",
"login_enter_password": "Geben Sie Ihr Passwort ein",
"login_enter_password_description": "Geben Sie Ihr Passwort ein, um auf Ihr JetKVM zuzugreifen.",
"login_error": "Beim Anmelden ist ein Fehler aufgetreten",
"login_forgot_password": "Passwort vergessen?",
"login_password_label": "Passwort",
"login_welcome_back": "Willkommen zurück bei JetKVM",
"macro_add_step": "Schritt hinzufügen {maxed_out}",
"macro_at_least_one_step_keys_or_modifiers": "Mindestens ein Schritt muss Schlüssel oder Modifikatoren haben",
"macro_at_least_one_step_required": "Mindestens ein Schritt ist erforderlich",
"macro_max_steps_error": "Sie können pro Makro maximal {max} Schritte hinzufügen.",
"macro_max_steps_reached": "( {max} max)",
"macro_name_label": "Makroname",
"macro_name_required": "Name ist erforderlich",
"macro_name_too_long": "Der Name muss weniger als 50 Zeichen lang sein",
"macro_please_fix_validation_errors": "Bitte beheben Sie die Validierungsfehler",
"macro_save": "Makro speichern",
"macro_save_failed": "Beim Speichern ist ein Fehler aufgetreten.",
"macro_save_failed_error": "Beim Speichern ist ein Fehler aufgetreten: {error}.",
"macro_step_count": "{steps} / {max} Schritte",
"macro_step_duration_description": "Wartezeit vor der Ausführung des nächsten Schritts.",
"macro_step_duration_label": "Schrittdauer",
"macro_step_keys_description": "Maximale Anzahl {max} Schlüsseln pro Schritt.",
"macro_step_keys_label": "Schlüssel",
"macro_step_max_keys_reached": "Maximale Anzahl an Schlüsseln erreicht",
"macro_step_modifiers_description": "Welche Modifikatoren (Umschalt/Strg/Alt/Meta) werden während dieses Schritts gedrückt.",
"macro_step_modifiers_label": "Modifikatoren",
"macro_step_no_matching_keys_found": "Keine passenden Schlüssel gefunden",
"macro_step_search_for_key": "Suche nach Schlüssel…",
"macro_steps_description": "Tasten/Modifikatoren werden nacheinander mit einer Verzögerung zwischen den einzelnen Schritten ausgeführt.",
"macro_steps_label": "Schritte",
"macros_add_description": "Erstellen Sie ein neues Tastaturmakro",
"macros_add_new": "Neues Makro hinzufügen",
"macros_add_new_macro": "Neues Makro hinzufügen",
"macros_aria_add_new": "Neues Makro hinzufügen",
"macros_aria_delete": "Makro löschen {name}",
"macros_aria_duplicate": "Doppeltes Makro {name}",
"macros_aria_edit": "Makro bearbeiten {name}",
"macros_aria_move_down": "Verschiebe {name} unten",
"macros_aria_move_up": "Verschiebe {name} nach oben",
"macros_confirm_delete_description": "Möchten Sie „ {name} „ wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
"macros_confirm_delete_title": "Makro löschen",
"macros_confirm_deleting": "Löschen…",
"macros_create_first_description": "Kombinieren Sie Tastenanschläge zu einer einzigen Aktion",
"macros_create_first_headline": "Erstellen Sie Ihr erstes Makro",
"macros_created_success": "Makro \" {name} \" erfolgreich erstellt",
"macros_delay_only": "Nur Verzögerung",
"macros_delete_confirm": "Möchten Sie dieses Makro wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
"macros_delete_macro": "Makro löschen",
"macros_deleted_success": "Makro \" {name} \" erfolgreich gelöscht",
"macros_deleting": "Löschen",
"macros_duplicated_success": "Makro \" {name} \" erfolgreich dupliziert",
"macros_edit_button": "Bearbeiten",
"macros_edit_description": "Ändern Sie Ihr Tastaturmakro",
"macros_edit_title": "Makro bearbeiten",
"macros_failed_create": "Makro konnte nicht erstellt werden",
"macros_failed_create_error": "Makro konnte nicht erstellt werden: {error}",
"macros_failed_delete": "Makro konnte nicht gelöscht werden",
"macros_failed_delete_error": "Makro konnte nicht gelöscht werden: {error}",
"macros_failed_duplicate": "Makro konnte nicht dupliziert werden",
"macros_failed_duplicate_error": "Makro konnte nicht dupliziert werden: {error}",
"macros_failed_reorder": "Makros konnten nicht neu angeordnet werden",
"macros_failed_reorder_error": "Fehler beim Neuordnen der Makros: {error}",
"macros_failed_update": "Makro konnte nicht aktualisiert werden",
"macros_failed_update_error": "Makro konnte nicht aktualisiert werden: {error}",
"macros_invalid_data": "Ungültige Makrodaten",
"macros_loading": "Makros werden geladen …",
"macros_max_reached": "Max. erreicht",
"macros_maximum_macros_reached": "Sie haben die maximal zulässige Anzahl von {maximum} Makros erreicht.",
"macros_no_macros_available": "Keine Makros verfügbar",
"macros_order_updated": "Makroreihenfolge erfolgreich aktualisiert",
"macros_title": "Tastaturmakros",
"macros_updated_success": "Makro \" {name} \" erfolgreich aktualisiert",
"metric_not_supported": "Metrik wird nicht unterstützt",
"metric_waiting_for_data": "Warten auf Daten…",
"mount_add_file_to_get_started": "Fügen Sie eine Datei hinzu, um zu beginnen",
"mount_add_new_media": "Neue Medien hinzufügen",
"mount_available_storage": "Verfügbarer Speicher",
"mount_button_back_to_overview": "Zurück zur Übersicht",
"mount_button_cancel_upload": "Hochladen abbrechen",
"mount_button_continue_upload": "Weiter hochladen",
"mount_button_mount_file": "Datei einbinden",
"mount_button_mount_url": "Mount-URL",
"mount_button_select": "Wählen",
"mount_button_showing_results": "Anzeige von {from} bis {to} von {total} Ergebnissen",
"mount_button_upload_new_image": "Laden Sie ein neues Bild hoch",
"mount_bytes_free": "{bytesFree} frei",
"mount_bytes_used": "{bytesUsed} verwendet",
"mount_calculating": "Berechnung…",
"mount_click_to_select_file": "Klicken Sie, um eine Datei auszuwählen",
"mount_click_to_select_incomplete": "Klicken Sie, um \" {name} \" auszuwählen.",
"mount_confirm_delete": "Möchten Sie {name} wirklich löschen?",
"mount_continue_uploading_with_name": "Weiter hochladen \" {name} \"",
"mount_error_delete_file": "Fehler beim Löschen der Datei: {error}",
"mount_error_description": "Beim Einbinden des Mediums ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.",
"mount_error_get_storage_space": "Fehler beim Abrufen des Speicherplatzes: {error}",
"mount_error_list_storage": "Fehler beim Auflisten der Speicherdateien: {error}",
"mount_error_title": "Mount-Fehler",
"mount_get_state_error": "Der Status des virtuellen Mediums konnte nicht abgerufen werden: {error}",
"mount_jetkvm_storage": "JetKVM-Speicherhalterung",
"mount_jetkvm_storage_description": "Mounten Sie zuvor hochgeladene Dateien aus dem JetKVM-Speicher",
"mount_mode_cdrom": "CD/DVD",
"mount_mode_disk": "Scheibe",
"mount_mounted_as": "Montiert als",
"mount_mounted_from_storage": "Vom JetKVM-Speicher gemountet",
"mount_no_images_description": "Laden Sie ein Image hoch, um mit der Bereitstellung virtueller Medien zu beginnen.",
"mount_no_images_title": "Keine Bilder verfügbar",
"mount_no_mounted_media": "Keine gemounteten Medien",
"mount_percentage_used": "{percentageUsed} % verwendet",
"mount_please_select_file": "Bitte wählen Sie die Datei \" {name} \" aus, um den Upload fortzusetzen.",
"mount_popular_images": "Beliebte Bilder",
"mount_streaming_from_url": "Streaming von URL",
"mount_supported_formats": "Unterstützte Formate: ISO, IMG",
"mount_unmount": "Aushängen",
"mount_unmount_error": "Abbild konnte nicht ausgehängt werden: {error}",
"mount_upload_description": "Wählen Sie eine Bilddatei zum Hochladen in den JetKVM-Speicher aus",
"mount_upload_error": "Upload-Fehler: {error}",
"mount_upload_failed_datachannel": "Fehler beim Erstellen des Datenkanals für den Datei-Upload",
"mount_upload_failed_rtc": "Upload fehlgeschlagen: {error}",
"mount_upload_successful": "Upload erfolgreich",
"mount_upload_title": "Neues Bild hochladen",
"mount_uploaded_has_been_uploaded": "{name} wurde hochgeladen",
"mount_uploading": "Hochladen…",
"mount_uploading_with_name": "Hochladen von {name}",
"mount_url_description": "Mounten Sie Dateien von jeder öffentlichen Webadresse",
"mount_url_input_label": "Bild-URL",
"mount_url_mount": "URL-Mount",
"mount_view_device_description": "Wählen Sie ein Image zum Mounten aus dem JetKVM-Speicher aus",
"mount_view_device_title": "Mounten vom JetKVM-Speicher",
"mount_view_url_description": "Geben Sie eine URL zur zu mountenden Bilddatei ein",
"mount_view_url_title": "Von URL einbinden",
"mount_virtual_media": "Virtuelle Medien",
"mount_virtual_media_description": "Mounten Sie ein Image, um von einem Betriebssystem zu booten oder es zu installieren.",
"mount_virtual_media_source": "Virtuelle Medienquelle",
"mount_virtual_media_source_description": "Wählen Sie, wie Sie Ihr virtuelles Medium mounten möchten",
"mouse_alt_finger": "Finger berührt einen Bildschirm",
"mouse_alt_mouse": "Maussymbol",
"mouse_description": "Konfigurieren Sie das Cursorverhalten und die Interaktionseinstellungen für Ihr Gerät",
"mouse_hide_cursor_description": "Den Cursor beim Senden von Mausbewegungen ausblenden",
"mouse_hide_cursor_title": "Cursor ausblenden",
"mouse_jiggler_config_updated": "Jiggler-Konfiguration erfolgreich aktualisiert",
"mouse_jiggler_custom": "Brauch",
"mouse_jiggler_description": "Simulieren Sie die Bewegung einer Computermaus",
"mouse_jiggler_disabled": "Deaktiviert",
"mouse_jiggler_error_config": "Beim Festlegen der Jiggler-Konfiguration ist ein Fehler aufgetreten",
"mouse_jiggler_failed_state": "Jiggler-Status konnte nicht festgelegt werden: {error}",
"mouse_jiggler_frequent": "Häufig - 30er",
"mouse_jiggler_invalid_cron": "Ungültiger Cron-Ausdruck. Bitte überprüfen Sie Ihr Zeitplanformat (z. B. „0 * * * * *“ für jede Minute).",
"mouse_jiggler_light": "Licht - 5m",
"mouse_jiggler_standard": "Standard - 1 m",
"mouse_jiggler_title": "Wackel",
"mouse_mode_absolute": "Absolute",
"mouse_mode_absolute_description": "Am bequemsten",
"mouse_mode_relative": "Relativ",
"mouse_mode_relative_description": "Am kompatibelsten",
"mouse_modes_description": "Wählen Sie den Mauseingabemodus",
"mouse_modes_title": "Modi",
"mouse_scroll_high": "Hoch",
"mouse_scroll_low": "Niedrig",
"mouse_scroll_medium": "Medium",
"mouse_scroll_off": "Aus",
"mouse_scroll_throttling_description": "Reduzieren Sie die Häufigkeit von Scroll-Ereignissen",
"mouse_scroll_throttling_title": "Scroll-Drosselung",
"mouse_scroll_very_high": "Sehr hoch",
"mouse_title": "Maus",
"network_custom_domain": "Benutzerdefinierte Domäne",
"network_description": "Konfigurieren Sie Ihre Netzwerkeinstellungen",
"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_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",
"network_dhcp_lease_renew_confirm_new_b": "Möglicherweise müssen Sie die Verbindung mit der neuen Adresse erneut herstellen",
"network_dhcp_lease_renew_failed": "Leasing konnte nicht erneuert werden: {error}",
"network_dhcp_lease_renew_success": "DHCP-Lease erneuert",
"network_domain_custom": "Brauch",
"network_domain_description": "Netzwerkdomänensuffix für das Gerät",
"network_domain_dhcp_provided": "DHCP bereitgestellt",
"network_domain_local": ".lokal",
"network_domain_title": "Domain",
"network_hostname_description": "Gerätekennung im Netzwerk. Leer für Systemstandard",
"network_hostname_title": "Hostname",
"network_http_proxy_description": "Proxyserver für ausgehende HTTP(S)-Anfragen vom Gerät. Leer für keine.",
"network_http_proxy_invalid": "Ungültige HTTP-Proxy-URL",
"network_http_proxy_title": "HTTP-Proxy",
"network_ipv4_address": "IPv4-Adresse",
"network_ipv4_dns": "IPv4 DNS",
"network_ipv4_gateway": "IPv4-Gateway",
"network_ipv4_invalid": "Ungültige IPv4-Adresse",
"network_ipv4_invalid_cidr": "Ungültige CIDR-Notation für IPv4-Adresse",
"network_ipv4_mode_description": "Konfigurieren des IPv4-Modus",
"network_ipv4_mode_dhcp": "DHCP",
"network_ipv4_mode_static": "Statisch",
"network_ipv4_mode_title": "IPv4-Modus",
"network_ipv4_netmask": "IPv4-Netzmaske",
"network_ipv6_addresses_header": "IPv6-Adressen",
"network_ipv6_cidr_suggestion": "Bitte verwenden Sie die CIDR-Notation (z. B. 2001:db8::1/64).",
"network_ipv6_dns": "IPv6 DNS",
"network_ipv6_flag_dad_failed": "DAD ist fehlgeschlagen",
"network_ipv6_flag_deprecated": "Veraltet",
"network_ipv6_gateway": "IPv6-Gateway",
"network_ipv6_information": "IPv6-Informationen",
"network_ipv6_invalid": "Ungültige IPv6-Adresse",
"network_ipv6_mode_description": "Konfigurieren des IPv6-Modus",
"network_ipv6_mode_dhcpv6": "DHCPv6",
"network_ipv6_mode_disabled": "Deaktiviert",
"network_ipv6_mode_link_local": "Nur Link-Local",
"network_ipv6_mode_slaac": "SLAAC",
"network_ipv6_mode_slaac_dhcpv6": "SLAAC + DHCPv6",
"network_ipv6_mode_static": "Statisch",
"network_ipv6_mode_title": "IPv6-Modus",
"network_ipv6_prefix": "IP-Präfix",
"network_ipv6_prefix_invalid": "Das Präfix muss zwischen 0 und 128 liegen",
"network_ll_dp_all": "Alle",
"network_ll_dp_basic": "Basic",
"network_ll_dp_description": "Steuern Sie, welche TLVs über das Link Layer Discovery Protocol gesendet werden",
"network_ll_dp_disabled": "Deaktiviert",
"network_ll_dp_title": "LLDP",
"network_mac_address_copy_error": "MAC-Adresse konnte nicht kopiert werden",
"network_mac_address_copy_success": "MAC-Adresse { mac } in die Zwischenablage kopiert",
"network_mac_address_description": "Hardwarekennung für die Netzwerkschnittstelle",
"network_mac_address_title": "MAC-Adresse",
"network_mdns_auto": "Auto",
"network_mdns_description": "Steuern des mDNS-Betriebsmodus (Multicast-DNS)",
"network_mdns_disabled": "Deaktiviert",
"network_mdns_ipv4_only": "Nur IPv4",
"network_mdns_ipv6_only": "Nur IPv6",
"network_mdns_title": "mDNS",
"network_no_information_description": "Keine Netzwerkkonfiguration verfügbar",
"network_no_information_headline": "Netzwerkinformationen",
"network_pending_dhcp_mode_change_description": "Speichern Sie die Einstellungen, um den DHCP-Modus zu aktivieren und Leasinginformationen anzuzeigen",
"network_pending_dhcp_mode_change_headline": "Ausstehende Änderung des DHCP-IPv4-Modus",
"network_save_settings": "Einstellungen speichern",
"network_save_settings_apply_title": "Netzwerkeinstellungen anwenden",
"network_save_settings_confirm": "Änderungen übernehmen",
"network_save_settings_confirm_description": "Die folgenden Netzwerkeinstellungen werden angewendet. Diese Änderungen erfordern möglicherweise einen Neustart und können zu einer kurzen Unterbrechung der Verbindung führen.",
"network_save_settings_confirm_heading": "Konfigurationsänderungen",
"network_save_settings_failed": "Netzwerkeinstellungen konnten nicht gespeichert werden: {error}",
"network_save_settings_success": "Netzwerkeinstellungen gespeichert",
"network_settings_add_dns": "DNS-Server hinzufügen",
"network_settings_load_error": "Netzwerkeinstellungen konnten nicht geladen werden: {error}",
"network_static_ipv4_header": "Statische IPv4-Konfiguration",
"network_static_ipv6_header": "Statische IPv6-Konfiguration",
"network_time_sync_description": "Konfigurieren der Zeitsynchronisierungseinstellungen",
"network_time_sync_http_only": "Nur HTTP",
"network_time_sync_ntp_and_http": "NTP und HTTP",
"network_time_sync_ntp_only": "Nur NTP",
"network_time_sync_title": "Zeitsynchronisation",
"network_title": "Netzwerk",
"never_seen_online": "Noch nie online gesehen",
"next": "Nächste",
"no_results_found": "Keine Ergebnisse gefunden",
"not_applicable": "N / A",
"not_available": "N / A",
"not_found": "Nicht gefunden",
"ntp_servers": "NTP-Server",
"oh_no": "Oh nein!",
"online": "Online",
"other_session_detected": "Eine weitere aktive Sitzung erkannt",
"other_session_take_over": " Es wird jeweils nur eine aktive Sitzung unterstützt. Möchten Sie diese Sitzung übernehmen?",
"other_session_use_here_button": "Hier verwenden",
"page_not_found_description": "Die von Ihnen gesuchte Seite existiert nicht.",
"paste_modal_confirm_paste": "Einfügen bestätigen",
"paste_modal_delay_between_keys": "Verzögerung zwischen den Tasten",
"paste_modal_delay_out_of_range": "Die Verzögerung muss zwischen {min} und {max}",
"paste_modal_failed_paste": "Fehler beim Einfügen des Textes: {error}",
"paste_modal_invalid_chars_intro": "Die folgenden Zeichen werden nicht eingefügt:",
"paste_modal_paste_from_host": "Vom Host einfügen",
"paste_modal_sending_using_layout": "Senden von Text mithilfe des Tastaturlayouts: {iso} - {name}",
"paste_text": "Text einfügen",
"paste_text_description": "Fügen Sie Text von Ihrem Client in den Remote-Host ein",
"peer_connection_closed": "Geschlossen",
"peer_connection_closing": "Schließen",
"peer_connection_connected": "Verbunden",
"peer_connection_connecting": "Verbinden",
"peer_connection_disconnected": "Getrennt",
"peer_connection_error": "Verbindungsfehler",
"peer_connection_failed": "Verbindung fehlgeschlagen",
"peer_connection_new": "Verbinden",
"previous": "Vorherige",
"register_device_error": "Beim Registrieren Ihres Geräts ist ein Fehler {error} aufgetreten.",
"register_device_finish_button": "Einrichtung abschließen",
"register_device_name_description": "Geben Sie Ihrem Gerät einen Namen, damit Sie es später leicht identifizieren können. Sie können diesen Namen jederzeit ändern.",
"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": "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}",
"rename_device_new_name_label": "Neuer Gerätename",
"rename_device_new_name_placeholder": "Plex Media Server",
"rename_device_no_name": "Bitte geben Sie einen Namen an",
"retry": "Wiederholen",
"saving": "Speichern…",
"search_placeholder": "Suchen…",
"serial_console": "Serielle Konsole",
"serial_console_baud_rate": "Baudrate",
"serial_console_configure_description": "Konfigurieren Sie die Einstellungen Ihrer seriellen Konsole",
"serial_console_data_bits": "Datenbits",
"serial_console_get_settings_error": "Die seriellen Konsoleneinstellungen konnten nicht abgerufen werden: {error}",
"serial_console_open_console": "Konsole öffnen",
"serial_console_parity": "Parität",
"serial_console_parity_even": "Gerade Parität",
"serial_console_parity_mark": "Parität markieren",
"serial_console_parity_none": "Keine Parität",
"serial_console_parity_odd": "Ungerade Parität",
"serial_console_parity_space": "Raumparität",
"serial_console_set_settings_error": "Die Einstellungen der seriellen Konsole konnten nicht auf {settings} festgelegt werden: {error}",
"serial_console_stop_bits": "Stoppbits",
"setting_remote_description": "Beschreibung der Fernbedienung einstellen",
"setting_remote_session_description": "Beschreibung der Remote-Sitzung festlegen ...",
"setting_up_connection_to_device": "Verbindung zum Gerät wird eingerichtet …",
"settings_access": "Zugang",
"settings_advanced": "Fortschrittlich",
"settings_appearance": "Aussehen",
"settings_back_to_kvm": "Zurück zu KVM",
"settings_general": "Allgemein",
"settings_hardware": "Hardware",
"settings_keyboard": "Tastatur",
"settings_keyboard_macros": "Tastaturmakros",
"settings_mouse": "Maus",
"settings_network": "Netzwerk",
"settings_video": "Video",
"something_went_wrong": "Ein Fehler ist aufgetreten. Bitte versuchen Sie es später noch einmal oder wenden Sie sich an den Support.",
"step_counter_step": "Schritt {step}",
"subnet_mask": "Subnetzmaske",
"time_division_days": "Tage",
"time_division_hours": "Std.",
"time_division_minutes": "Minuten",
"time_division_months": "Monate",
"time_division_seconds": "Sekunden",
"time_division_weeks": "Wochen",
"time_division_years": "Jahre",
"troubleshoot_connection": "Verbindungsprobleme beheben",
"unknown_error": "Unbekannter Fehler",
"update_in_progress": "Aktualisierung läuft",
"updates_failed_check": "Fehler beim Suchen nach Updates: {error}",
"updates_failed_get_device_version": "Geräteversion konnte nicht abgerufen werden: {error}",
"updating_leave_device_on": "Bitte schalten Sie Ihr Gerät nicht aus…",
"usb": "USB",
"usb_config_custom": "Brauch",
"usb_config_default": "JetKVM-Standard",
"usb_config_dell": "Dell Multimedia Pro-Tastatur",
"usb_config_failed_load": "USB-Konfiguration konnte nicht geladen werden: {error}",
"usb_config_failed_set": "USB-Konfiguration konnte nicht festgelegt werden: {error}",
"usb_config_identifiers_description": "Dem Zielcomputer zugänglich gemachte USB-Gerätekennungen",
"usb_config_identifiers_title": "Kennungen",
"usb_config_logitech": "Logitech Universaladapter",
"usb_config_manufacturer_label": "Hersteller",
"usb_config_manufacturer_placeholder": "Hersteller eingeben",
"usb_config_microsoft": "Microsoft Wireless MultiMedia Keyboard",
"usb_config_product_id_label": "Produkt-ID",
"usb_config_product_id_placeholder": "Produkt-ID eingeben",
"usb_config_product_name_label": "Produktname",
"usb_config_product_name_placeholder": "Produktnamen eingeben",
"usb_config_restore_default": "Auf Standard zurücksetzen",
"usb_config_serial_number_label": "Seriennummer",
"usb_config_serial_number_placeholder": "Seriennummer eingeben",
"usb_config_set_success": "USB-Konfiguration eingestellt auf {manufacturer} {product}",
"usb_config_update_identifiers": "USB-Kennungen aktualisieren",
"usb_config_vendor_id_label": "Lieferanten-ID",
"usb_config_vendor_id_placeholder": "Geben Sie die Lieferanten-ID ein",
"usb_device_classes_description": "USB-Geräteklassen im Verbundgerät",
"usb_device_classes_title": "Klassen",
"usb_device_custom": "Brauch",
"usb_device_description": "USB-Geräte zum Emulieren auf dem Zielcomputer",
"usb_device_enable_absolute_mouse_description": "Absolute Maus (Zeiger) aktivieren",
"usb_device_enable_absolute_mouse_title": "Absolute Maus (Zeiger) aktivieren",
"usb_device_enable_keyboard_description": "Tastatur aktivieren",
"usb_device_enable_keyboard_title": "Tastatur aktivieren",
"usb_device_enable_mass_storage_description": "Manchmal muss es möglicherweise deaktiviert werden, um Probleme mit bestimmten Geräten zu vermeiden",
"usb_device_enable_mass_storage_title": "USB-Massenspeicher aktivieren",
"usb_device_enable_relative_mouse_description": "Relative Maus aktivieren",
"usb_device_enable_relative_mouse_title": "Relative Maus aktivieren",
"usb_device_failed_load": "USB-Geräte konnten nicht geladen werden: {error}",
"usb_device_failed_set": "Fehler beim Festlegen der USB-Geräte: {error}",
"usb_device_keyboard_mouse_and_mass_storage": "Tastatur, Maus und Massenspeicher",
"usb_device_keyboard_only": "Nur Tastatur",
"usb_device_restore_default": "Auf Standard zurücksetzen",
"usb_device_title": "USB-Gerät",
"usb_device_update_classes": "USB-Klassen aktualisieren",
"usb_device_updated": "USB-Geräte aktualisiert",
"usb_state_connected": "Verbunden",
"usb_state_connecting": "Verbinden",
"usb_state_disconnected": "Getrennt",
"usb_state_low_power_mode": "Energiesparmodus",
"user_interface_language_description": "Wählen Sie die Sprache aus, die in der JetKVM-Benutzeroberfläche verwendet werden soll",
"user_interface_language_title": "Schnittstellensprache",
"video_brightness_description": "Helligkeitsstufe ( {value} x)",
"video_brightness_title": "Helligkeit",
"video_contrast_description": "Kontraststufe ( {value} x)",
"video_contrast_title": "Kontrast",
"video_custom_edid_description": "EDID gibt die Kompatibilität des Videomodus an. Die Standardeinstellungen funktionieren in den meisten Fällen, aber individuelle UEFI/BIOS-Einstellungen müssen möglicherweise angepasst werden.",
"video_custom_edid_title": "Benutzerdefinierte EDID",
"video_debugging_info_description": "Debugging-Informationen für Videos",
"video_debugging_info_title": "Debugging-Informationen",
"video_description": "Konfigurieren Sie Anzeigeeinstellungen und EDID für optimale Kompatibilität",
"video_edid_acer_b246wl": "Acer B246WL, 1920x1200",
"video_edid_asus_pa248qv": "ASUS PA248QV, 1920x1200",
"video_edid_custom": "Brauch",
"video_edid_dell_d2721h": "DELL D2721H, 1920 x 1080",
"video_edid_dell_idrac": "DELL IDRAC EDID, 1280x1024",
"video_edid_description": "Passen Sie die EDID-Einstellungen für das Display an",
"video_edid_file_label": "EDID-Datei",
"video_edid_jetkvm_default": "JetKVM-Standard",
"video_edid_set_success": "EDID erfolgreich auf {edid} gesetzt",
"video_edid_title": "EDID",
"video_enhancement_description": "Passen Sie die Farbeinstellungen an, um die Videoausgabe lebendiger und farbenfroher zu gestalten",
"video_enhancement_title": "Videoverbesserung",
"video_failed_get_debug_info": "Debug-Informationen konnten nicht abgerufen werden: {error}",
"video_failed_get_edid": "EDID konnte nicht abgerufen werden: {error}",
"video_failed_set_edid": "EDID konnte nicht festgelegt werden: {error}",
"video_failed_set_stream_quality": "Fehler beim Festlegen der Streamqualität: {error}",
"video_get_debugging_info": "Debugging-Informationen abrufen",
"video_overlay_autoplay_permissions_required": "Autoplay-Berechtigungen erforderlich",
"video_overlay_conn_check_cables": "Überprüfen Sie alle Kabelverbindungen auf lose oder beschädigte Drähte",
"video_overlay_conn_ensure_network": "Stellen Sie sicher, dass Ihre Netzwerkverbindung stabil und aktiv ist",
"video_overlay_conn_restart": "Versuchen Sie, sowohl das Gerät als auch Ihren Computer neu zu starten",
"video_overlay_conn_verify_power": "Stellen Sie sicher, dass das Gerät eingeschaltet und richtig angeschlossen ist",
"video_overlay_connection_issue_title": "Verbindungsproblem erkannt",
"video_overlay_enable_autoplay_settings": "Bitte passen Sie die Browsereinstellungen an, um die automatische Wiedergabe zu aktivieren",
"video_overlay_hdmi_error_title": "HDMI-Signalfehler erkannt.",
"video_overlay_hdmi_incompatible_resolution": "Inkompatible Auflösungs- oder Bildwiederholfrequenzeinstellungen",
"video_overlay_hdmi_loose_faulty": "Eine lose oder fehlerhafte HDMI-Verbindung",
"video_overlay_hdmi_source_issue": "Probleme mit dem HDMI-Ausgang des Quellgeräts",
"video_overlay_learn_more": "Mehr erfahren",
"video_overlay_loading_stream": "Videostream wird geladen …",
"video_overlay_manually_start_stream": "Stream manuell starten",
"video_overlay_no_hdmi_adapter_compat": "Wenn Sie einen Adapter verwenden, stellen Sie sicher, dass dieser kompatibel ist und ordnungsgemäß funktioniert",
"video_overlay_no_hdmi_ensure_cable": "Stellen Sie sicher, dass das HDMI-Kabel an beiden Enden fest angeschlossen ist",
"video_overlay_no_hdmi_ensure_power": "Stellen Sie sicher, dass das Quellgerät eingeschaltet ist und ein Signal ausgibt",
"video_overlay_no_hdmi_signal": "Kein HDMI-Signal erkannt.",
"video_overlay_pointerlock_click_to_enable": "Klicken Sie auf das Video, um die Maussteuerung zu aktivieren",
"video_overlay_retrying_connection": "Verbindung wird erneut versucht …",
"video_overlay_troubleshooting_guide": "Handbuch zur Fehlerbehebung",
"video_overlay_try_again": "Versuchen Sie es erneut",
"video_pointer_lock_disabled": "Zeigersperre deaktiviert",
"video_pointer_lock_enabled": "Zeigersperre aktiviert drücken Sie Escape, um die Sperre aufzuheben",
"video_quality_high": "Hoch",
"video_quality_low": "Niedrig",
"video_quality_medium": "Medium",
"video_reset_to_default": "Auf Standard zurücksetzen",
"video_restore_to_default": "Auf Standard zurücksetzen",
"video_saturation_description": "Farbsättigung ( {value} x)",
"video_saturation_title": "Sättigung",
"video_set_custom_edid": "Benutzerdefinierte EDID festlegen",
"video_stream_quality_description": "Passen Sie die Qualität des Videostreams an",
"video_stream_quality_set": "Streamqualität eingestellt auf {quality}",
"video_stream_quality_title": "Stream-Qualität",
"video_title": "Video",
"view_details": "Details anzeigen",
"virtual_keyboard_header": "Virtuelle Tastatur",
"wake_on_lan": "Wake-On-LAN",
"wake_on_lan_add_device_device_name": "Gerätename",
"wake_on_lan_add_device_example_device_name": "Plex Media Server",
"wake_on_lan_add_device_mac_address": "MAC-Adresse",
"wake_on_lan_add_device_save_device": "Gerät speichern",
"wake_on_lan_description": "Senden Sie ein Magic Packet, um ein Remote-Gerät zu wecken.",
"wake_on_lan_device_list_add_new_device": "Neues Gerät hinzufügen",
"wake_on_lan_device_list_delete_device": "Gerät löschen",
"wake_on_lan_device_list_wake": "Aufwachen",
"wake_on_lan_empty_add_device_to_start": "Fügen Sie ein Gerät hinzu, um Wake-on-LAN zu verwenden",
"wake_on_lan_empty_add_new_device": "Neues Gerät hinzufügen",
"wake_on_lan_empty_no_devices_added": "Keine Geräte hinzugefügt",
"wake_on_lan_failed_add_device": "Gerät konnte nicht hinzugefügt werden",
"wake_on_lan_failed_send_magic": "Das Senden des Magic Packets ist fehlgeschlagen.",
"wake_on_lan_invalid_mac": "Ungültige MAC-Adresse",
"wake_on_lan_magic_sent_success": "Magic Packet erfolgreich gesendet",
"welcome_to_jetkvm": "Willkommen bei JetKVM",
"welcome_to_jetkvm_description": "Steuern Sie jeden Computer aus der Ferne"
}

View File

@ -0,0 +1,895 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"access_adopt_kvm": "Adopt KVM to Cloud",
"access_adopted_message": "Your device is adopted to the Cloud",
"access_auth_mode_no_password": "Current mode: No password",
"access_auth_mode_password": "Current mode: Password protected",
"access_authentication_mode_title": "Authentication Mode",
"access_certificate_label": "Certificate",
"access_change_password_button": "Change Password",
"access_change_password_description": "Update your device access password",
"access_change_password_title": "Change Password",
"access_cloud_api_url_label": "Cloud API URL",
"access_cloud_app_url_label": "Cloud Application URL",
"access_cloud_provider_description": "Select the cloud provider for your device",
"access_cloud_provider_title": "Cloud Provider",
"access_cloud_security_title": "Cloud Security",
"access_confirm_deregister": "Are you sure you want to de-register this device?",
"access_deregister": "De-register from Cloud",
"access_description": "Manage the Access Control of the device",
"access_disable_protection": "Disable Protection",
"access_enable_password": "Enable Password",
"access_failed_deregister": "Failed to de-register device: {error}",
"access_failed_update_cloud_url": "Failed to update cloud URL: {error}",
"access_failed_update_tls": "Failed to update TLS settings: {error}",
"access_github_link": "GitHub",
"access_https_description": "Configure secure HTTPS access to your device",
"access_https_mode_title": "HTTPS Mode",
"access_learn_security": "Learn about our cloud security",
"access_local_description": "Manage the mode of local access to the device",
"access_local_title": "Local",
"access_no_device_id": "No device ID available",
"access_private_key_description": "For security reasons, it will not be displayed after saving.",
"access_private_key_label": "Private Key",
"access_provider_custom": "Custom",
"access_provider_jetkvm": "JetKVM Cloud",
"access_remote_description": "Manage the mode of Remote access to the device",
"access_security_encryption": "End-to-end encryption using WebRTC (DTLS and SRTP)",
"access_security_oidc": "OIDC (OpenID Connect) authentication",
"access_security_open_source": "All cloud components are open-source and available on GitHub.",
"access_security_streams": "All streams encrypted in transit",
"access_security_zero_trust": "Zero Trust security model",
"access_title": "Access",
"access_tls_certificate_description": "Paste your TLS certificate below. For certificate chains, include the entire chain (leaf, intermediate, and root certificates).",
"access_tls_certificate_title": "TLS Certificate",
"access_tls_custom": "Custom",
"access_tls_disabled": "Disabled",
"access_tls_self_signed": "Self-signed",
"access_tls_updated": "TLS settings updated successfully",
"access_update_tls_settings": "Update TLS Settings",
"action_bar_connection_stats": "Connection Stats",
"action_bar_extension": "Extension",
"action_bar_fullscreen": "Fullscreen",
"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",
"advanced_description": "Access additional settings for troubleshooting and customization",
"advanced_dev_channel_description": "Receive early updates from the development channel",
"advanced_dev_channel_title": "Dev Channel Updates",
"advanced_developer_mode_description": "Enable advanced features for developers",
"advanced_developer_mode_enabled_title": "Developer Mode Enabled",
"advanced_developer_mode_title": "Developer Mode",
"advanced_developer_mode_warning_advanced": "For advanced users only. Not for production use.",
"advanced_developer_mode_warning_risks": "Only use if you understand the risks",
"advanced_developer_mode_warning_security": "Security is weakened while active",
"advanced_disable_usb_emulation": "Disable USB Emulation",
"advanced_enable_usb_emulation": "Enable USB Emulation",
"advanced_error_loopback_disable": "Failed to disable loopback-only mode: {error}",
"advanced_error_loopback_enable": "Failed to enable loopback-only mode: {error}",
"advanced_error_reset_config": "Failed to reset configuration: {error}",
"advanced_error_set_dev_channel": "Failed to set dev channel state: {error}",
"advanced_error_set_dev_mode": "Failed to set dev mode: {error}",
"advanced_error_update_ssh_key": "Failed to update SSH key: {error}",
"advanced_error_usb_emulation_disable": "Failed to disable USB emulation: {error}",
"advanced_error_usb_emulation_enable": "Failed to enable USB emulation: {error}",
"advanced_loopback_only_description": "Restrict web interface access to localhost only (127.0.0.1)",
"advanced_loopback_only_title": "Loopback-Only Mode",
"advanced_loopback_warning_before": "Before enabling this feature, make sure you have either:",
"advanced_loopback_warning_cloud": "Cloud access enabled and working",
"advanced_loopback_warning_confirm": "I Understand, Enable Anyway",
"advanced_loopback_warning_description": "WARNING: This will restrict web interface access to localhost (127.0.0.1) only.",
"advanced_loopback_warning_ssh": "SSH access configured and tested",
"advanced_loopback_warning_title": "Enable Loopback-Only Mode?",
"advanced_reset_config_button": "Reset Config",
"advanced_reset_config_description": "Reset configuration to default. This will log you out.",
"advanced_reset_config_title": "Reset Configuration",
"advanced_ssh_access_description": "Add your SSH public key to enable secure remote access to the device",
"advanced_ssh_access_title": "SSH Access",
"advanced_ssh_default_user": "The default SSH user is",
"advanced_ssh_public_key_label": "SSH Public Key",
"advanced_ssh_public_key_placeholder": "Enter your SSH public key",
"advanced_success_loopback_disabled": "Loopback-only mode disabled. Restart your device to apply.",
"advanced_success_loopback_enabled": "Loopback-only mode enabled. Restart your device to apply.",
"advanced_success_reset_config": "Configuration reset to default successfully",
"advanced_success_update_ssh_key": "SSH key updated successfully",
"advanced_title": "Advanced",
"advanced_troubleshooting_mode_description": "Diagnostic tools and additional controls for troubleshooting and development purposes",
"advanced_troubleshooting_mode_title": "Troubleshooting Mode",
"advanced_update_ssh_key_button": "Update SSH Key",
"advanced_usb_emulation_description": "Control the USB emulation state",
"advanced_usb_emulation_title": "USB Emulation",
"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",
"appearance_description": "Choose your preferred color theme",
"appearance_page_description": "Customize the look and feel of your JetKVM interface",
"appearance_theme": "Theme",
"appearance_theme_dark": "Dark",
"appearance_theme_light": "Light",
"appearance_theme_system": "System",
"appearance_title": "Appearance",
"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": "Please select an authentication mode",
"auth_authentication_mode_error": "An error occurred while setting the authentication mode",
"auth_authentication_mode_invalid": "Invalid authentication mode",
"auth_connect_to_cloud": "Connect your JetKVM to the cloud",
"auth_connect_to_cloud_action": "Log in & Connect device",
"auth_connect_to_cloud_description": "Unlock remote access and advanced features for your device",
"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": "Log in to your JetKVM account",
"auth_login_action": "Log in",
"auth_login_description": "Log in to access and manage your devices securely",
"auth_mode_local": "Local Authentication Method",
"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": "No Password",
"auth_mode_local_no_password_description": "Quick access without password authentication.",
"auth_mode_local_password": "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_failed_set": "Failed to set password: {error}",
"auth_mode_local_password_note": "This password will be used to secure your device data and protect against unauthorized access.",
"auth_mode_local_password_note_local": "All data remains on your local device.",
"auth_mode_local_password_set": "Set a Password",
"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_signup_connect_to_cloud_action": "Signup & Connect device",
"auth_signup_create_account": "Create your JetKVM account",
"auth_signup_create_account_action": "Create Account",
"auth_signup_create_account_description": "Create your account and start managing your devices with ease.",
"back": "Back",
"back_to_devices": "Back to Devices",
"cancel": "Cancel",
"close": "Close",
"cloud_kvms": "Cloud KVMs",
"cloud_kvms_description": "Manage your cloud KVMs and connect to them securely.",
"cloud_kvms_no_devices": "No devices found",
"cloud_kvms_no_devices_description": "You don't have any devices with enabled JetKVM Cloud yet.",
"confirm": "Confirm",
"connect_to_kvm": "Connect to KVM",
"connecting_to_device": "Connecting to device…",
"connection_established": "Connection established",
"connection_stats_badge_jitter": "Jitter",
"connection_stats_badge_jitter_buffer_avg_delay": "Jitter Buffer Avg. Delay",
"connection_stats_connection": "Connection",
"connection_stats_connection_description": "The connection between the client and the JetKVM.",
"connection_stats_frames_per_second": "Frames per second",
"connection_stats_frames_per_second_description": "Number of inbound video frames displayed per second.",
"connection_stats_network_stability": "Network Stability",
"connection_stats_network_stability_description": "How steady the flow of inbound video packets is across the network.",
"connection_stats_packets_lost": "Packets Lost",
"connection_stats_packets_lost_description": "Count of lost inbound video RTP packets.",
"connection_stats_playback_delay": "Playback Delay",
"connection_stats_playback_delay_description": "Delay added by the jitter buffer to smooth playback when frames arrive unevenly.",
"connection_stats_round_trip_time": "Round-Trip Time",
"connection_stats_round_trip_time_description": "Round-trip time for the active ICE candidate pair between peers.",
"connection_stats_sidebar": "Connection Stats",
"connection_stats_unit_frames_per_second": " fps",
"connection_stats_unit_milliseconds": " ms",
"connection_stats_unit_packets": " packets",
"connection_stats_video": "Video",
"connection_stats_video_description": "The video stream from the JetKVM to the client.",
"continue": "Continue",
"creating_peer_connection": "Creating peer connection…",
"dc_power_control_current": "Current",
"dc_power_control_current_unit": "A",
"dc_power_control_get_state_error": "Failed to get DC power state: {error}",
"dc_power_control_power": "Power",
"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_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": "Voltage",
"dc_power_control_voltage_unit": "V",
"delete": "Delete",
"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_empty_lease_description": "We haven't received any DHCP lease information from the device yet.",
"dhcp_empty_lease_headline": "No DHCP Lease information",
"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": "Serial Console",
"extension_serial_console_description": "Access your serial console extension",
"extensions_atx_power_control": "ATX Power Control",
"extensions_atx_power_control_description": "Control the power state of your machine via ATX power control.",
"extensions_dc_power_control": "DC Power Control",
"extensions_dc_power_control_description": "Control your DC Power extension",
"extensions_popover_extensions": "Extensions",
"gathering_ice_candidates": "Gathering ICE candidates…",
"general_app_version": "App: {version}",
"general_auto_update_description": "Automatically update the device to the latest version",
"general_auto_update_error": "Failed to set auto-update: {error}",
"general_auto_update_title": "Auto Update",
"general_check_for_updates": "Check for Updates",
"general_page_description": "Configure device settings and update preferences",
"general_reboot_description": "Do you want to proceed with rebooting the system?",
"general_reboot_device": "Reboot Device",
"general_reboot_device_description": "Power cycle the JetKVM",
"general_reboot_no_button": "No",
"general_reboot_title": "Reboot JetKVM",
"general_reboot_yes_button": "Yes",
"general_system_version": "System: {version}",
"general_title": "General",
"general_update_app_update_title": "App Update",
"general_update_application_type": "App",
"general_update_available_description": "A new update is available to enhance system performance and improve compatibility. We recommend updating to ensure everything runs smoothly.",
"general_update_available_title": "Update available",
"general_update_background_button": "Update in Background",
"general_update_check_again_button": "Check Again",
"general_update_checking_description": "We're ensuring your device has the latest features and improvements.",
"general_update_checking_title": "Checking for updates…",
"general_update_completed_description": "Your device has been successfully updated to the latest version. Enjoy the new features and improvements!",
"general_update_completed_title": "Update Completed Successfully",
"general_update_error_description": "An error occurred while updating your device. Please try again later.",
"general_update_error_details": "Error details: {errorMessage}",
"general_update_error_title": "Update Error",
"general_update_later_button": "Do it later",
"general_update_now_button": "Update Now",
"general_update_rebooting": "Rebooting to complete the update…",
"general_update_status_awaiting_reboot": "Awaiting reboot",
"general_update_status_downloading": "Downloading {update_type} update…",
"general_update_status_fetching": "Fetching update information…",
"general_update_status_installing": "Installing {update_type} update…",
"general_update_status_progress": "{part} progress",
"general_update_status_verifying": "Verifying {update_type} update…",
"general_update_system_type": "System",
"general_update_system_update_title": "Linux System Update",
"general_update_up_to_date_description": "Your system is running the latest version. No updates are currently available.",
"general_update_up_to_date_title": "System is up to date",
"general_update_updating_description": "Please don't turn off your device. This process may take a few minutes.",
"general_update_updating_title": "Updating your device",
"getting_remote_session_description": "Getting remote session description attempt {attempt}",
"hardware_backlight_settings_error": "Failed to set backlight settings: {error}",
"hardware_backlight_settings_get_error": "Failed to get backlight settings: {error}",
"hardware_backlight_settings_success": "Backlight settings updated successfully",
"hardware_dim_display_after_description": "Set how long to wait before dimming the display",
"hardware_dim_display_after_title": "Dim Display After",
"hardware_display_brightness_description": "Set the brightness of the display",
"hardware_display_brightness_high": "High",
"hardware_display_brightness_low": "Low",
"hardware_display_brightness_medium": "Medium",
"hardware_display_brightness_off": "Off",
"hardware_display_brightness_title": "Display Brightness",
"hardware_display_orientation_description": "Set the orientation of the display",
"hardware_display_orientation_error": "Failed to set display orientation: {error}",
"hardware_display_orientation_inverted": "Inverted",
"hardware_display_orientation_normal": "Normal",
"hardware_display_orientation_success": "Display orientation updated successfully",
"hardware_display_orientation_title": "Display Orientation",
"hardware_display_wake_up_note": "The display will wake up when the connection state changes, or when touched.",
"hardware_page_description": "Configure display settings and hardware options for your JetKVM device",
"hardware_power_saving_description": "Reduce power consumption when not in use",
"hardware_power_saving_disabled": "Power saving mode disabled",
"hardware_power_saving_enabled": "Power saving mode enabled",
"hardware_power_saving_failed_error": "Failed to set power saving mode: {error}",
"hardware_power_saving_hdmi_sleep_description": "Turn off capture after 90 seconds of inactivity",
"hardware_power_saving_hdmi_sleep_title": "HDMI Sleep Mode",
"hardware_power_saving_title": "Power Saving",
"hardware_time_10_minutes": "10 Minutes",
"hardware_time_1_hour": "1 Hour",
"hardware_time_1_minute": "1 Minute",
"hardware_time_30_minutes": "30 Minutes",
"hardware_time_5_minutes": "5 Minutes",
"hardware_time_never": "Never",
"hardware_title": "Hardware",
"hardware_turn_off_display_after_description": "Period of inactivity before display automatically turns off",
"hardware_turn_off_display_after_title": "Turn off Display After",
"hide": "Hide",
"ice_gathering_completed": "ICE Gathering completed",
"info_caps_lock": "Caps Lock",
"info_compose": "Compose",
"info_hdmi_state": "HDMI State:",
"info_hidrpc_state": "HidRPC State:",
"info_kana": "Kana",
"info_keys": "Keys:",
"info_last_move": "Last Move:",
"info_num_lock": "Num Lock",
"info_paste_enabled": "Enabled",
"info_paste_mode": "Paste Mode:",
"info_pointer": "Pointer:",
"info_relayed_by_cloudflare": "Relayed by Cloudflare",
"info_resolution": "Resolution:",
"info_scroll_lock": "Scroll Lock",
"info_shift": "Shift",
"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_gateway": "Gateway",
"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",
"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",
"keyboard_description": "Configure keyboard settings for your device",
"keyboard_layout_description": "Keyboard layout of target operating system",
"keyboard_layout_error": "Failed to set keyboard layout: {error}",
"keyboard_layout_long_description": "The virtual keyboard, paste text, and keyboard macros send individual key strokes to the target device. The keyboard layout determines which key codes are being sent. Ensure that the keyboard layout in JetKVM matches the settings in the operating system.",
"keyboard_layout_success": "Keyboard layout set successfully to {layout}",
"keyboard_layout_title": "Keyboard Layout",
"keyboard_show_pressed_keys_description": "Display currently pressed keys in the status bar",
"keyboard_show_pressed_keys_title": "Show Pressed Keys",
"keyboard_title": "Keyboard",
"kvm_terminal": "KVM Terminal",
"last_online": "Last online {time}",
"learn_more": "Learn more",
"load": "Load",
"loading": "Loading…",
"local_auth_change_local_device_password_description": "Enter your current password and a new password to update your local device protection.",
"local_auth_change_local_device_password_title": "Change Local Device Password",
"local_auth_confirm_new_password_label": "Confirm New Password",
"local_auth_create_confirm_password_placeholder": "Re-enter your password",
"local_auth_create_description": "Create a password to protect your device from unauthorized local access.",
"local_auth_create_new_password_label": "New Password",
"local_auth_create_new_password_placeholder": "Enter a strong password",
"local_auth_create_not_now_button": "Not Now",
"local_auth_create_secure_button": "Secure Device",
"local_auth_create_title": "Local Device Protection",
"local_auth_current_password_label": "Current Password",
"local_auth_disable_local_device_protection_description": "Enter your current password to disable local device protection.",
"local_auth_disable_local_device_protection_title": "Disable Local Device Protection",
"local_auth_disable_protection_button": "Disable Protection",
"local_auth_enter_current_password_placeholder": "Enter your current password",
"local_auth_enter_new_password_placeholder": "Enter a new strong password",
"local_auth_error_changing_password": "An error occurred while changing the password",
"local_auth_error_disabling_password": "An error occurred while disabling the password",
"local_auth_error_enter_current_password": "Please enter your current password",
"local_auth_error_enter_new_password": "Please enter a new password",
"local_auth_error_enter_old_password": "Please enter your old password",
"local_auth_error_enter_password": "Please enter a password",
"local_auth_error_passwords_not_match": "Passwords do not match",
"local_auth_error_setting_password": "An error occurred while setting the password",
"local_auth_new_password_label": "New Password",
"local_auth_reenter_new_password_placeholder": "Re-enter your new password",
"local_auth_success_password_disabled_description": "You've successfully disabled the password protection for local access. Remember, your device is now less secure.",
"local_auth_success_password_disabled_title": "Password Protection Disabled",
"local_auth_success_password_set_description": "You've successfully set up local device protection. Your device is now secure against unauthorized local access.",
"local_auth_success_password_set_title": "Password Set Successfully",
"local_auth_success_password_updated_description": "You've successfully changed your local device protection password. Make sure to remember your new password for future access.",
"local_auth_success_password_updated_title": "Password Updated Successfully",
"local_auth_update_password_button": "Update Password",
"locale_auto": "Auto",
"locale_change_success": "Language changed successfully to {locale}",
"locale_da": "Dansk",
"locale_de": "Deutsch",
"locale_en": "English",
"locale_es": "Español",
"locale_fr": "Français",
"locale_it": "Italiano",
"locale_nb": "Norsk (bokmål)",
"locale_sv": "Svenska",
"locale_zh": "中文 (简体)",
"log_in": "Log In",
"log_out": "Log out",
"logged_in_as": "Logged in as",
"login_enter_password": "Enter your password",
"login_enter_password_description": "Enter your password to access your JetKVM.",
"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": "Save Macro",
"macro_save_failed": "An error occurred while saving.",
"macro_save_failed_error": "An error occurred while saving: {error}.",
"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",
"macros_add_description": "Create a new keyboard macro",
"macros_add_new": "Add New Macro",
"macros_add_new_macro": "Add New Macro",
"macros_aria_add_new": "Add new macro",
"macros_aria_delete": "Delete macro {name}",
"macros_aria_duplicate": "Duplicate macro {name}",
"macros_aria_edit": "Edit macro {name}",
"macros_aria_move_down": "Move {name} down",
"macros_aria_move_up": "Move {name} up",
"macros_confirm_delete_description": "Are you sure you want to delete \"{name}\"? This action cannot be undone.",
"macros_confirm_delete_title": "Delete Macro",
"macros_confirm_deleting": "Deleting…",
"macros_create_first_description": "Combine keystrokes into a single action",
"macros_create_first_headline": "Create Your First Macro",
"macros_created_success": "Macro \"{name}\" created successfully",
"macros_delay_only": "Delay only",
"macros_delete_confirm": "Are you sure you want to delete this macro? This action cannot be undone.",
"macros_delete_macro": "Delete Macro",
"macros_deleted_success": "Macro \"{name}\" deleted successfully",
"macros_deleting": "Deleting",
"macros_duplicated_success": "Macro \"{name}\" duplicated successfully",
"macros_edit_button": "Edit",
"macros_edit_description": "Modify your keyboard macro",
"macros_edit_title": "Edit Macro",
"macros_failed_create": "Failed to create macro",
"macros_failed_create_error": "Failed to create macro: {error}",
"macros_failed_delete": "Failed to delete macro",
"macros_failed_delete_error": "Failed to delete macro: {error}",
"macros_failed_duplicate": "Failed to duplicate macro",
"macros_failed_duplicate_error": "Failed to duplicate macro: {error}",
"macros_failed_reorder": "Failed to reorder macros",
"macros_failed_reorder_error": "Failed to reorder macros: {error}",
"macros_failed_update": "Failed to update macro",
"macros_failed_update_error": "Failed to update macro: {error}",
"macros_invalid_data": "Invalid macro data",
"macros_loading": "Loading macros…",
"macros_max_reached": "Max Reached",
"macros_maximum_macros_reached": "You have reached the maximum number of {maximum} macros allowed.",
"macros_no_macros_available": "No macros available",
"macros_order_updated": "Macro order updated successfully",
"macros_title": "Keyboard Macros",
"macros_updated_success": "Macro \"{name}\" updated successfully",
"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_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_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": "JetKVM Storage Mount",
"mount_jetkvm_storage_description": "Mount previously uploaded files from the JetKVM storage",
"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": "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_unmount": "Unmount",
"mount_unmount_error": "Failed to unmount image: {error}",
"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": "Uploading…",
"mount_uploading_with_name": "Uploading {name}",
"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": "Virtual Media",
"mount_virtual_media_description": "Mount an image to boot from or install an operating system.",
"mount_virtual_media_source": "Virtual Media Source",
"mount_virtual_media_source_description": "Choose how you want to mount your virtual media",
"mouse_alt_finger": "Finger touching a screen",
"mouse_alt_mouse": "Mouse icon",
"mouse_description": "Configure cursor behavior and interaction settings for your device",
"mouse_hide_cursor_description": "Hide the cursor when sending mouse movements",
"mouse_hide_cursor_title": "Hide Cursor",
"mouse_jiggler_config_updated": "Jiggler config successfully updated",
"mouse_jiggler_custom": "Custom",
"mouse_jiggler_description": "Simulate movement of a computer mouse",
"mouse_jiggler_disabled": "Disabled",
"mouse_jiggler_error_config": "There was an error setting the jiggler config",
"mouse_jiggler_failed_state": "Failed to set jiggler state: {error}",
"mouse_jiggler_frequent": "Frequent - 30s",
"mouse_jiggler_invalid_cron": "Invalid cron expression. Please check your schedule format (e.g., '0 * * * * *' for every minute).",
"mouse_jiggler_light": "Light - 5m",
"mouse_jiggler_standard": "Standard - 1m",
"mouse_jiggler_title": "Jiggler",
"mouse_mode_absolute": "Absolute",
"mouse_mode_absolute_description": "Most convenient",
"mouse_mode_relative": "Relative",
"mouse_mode_relative_description": "Most compatible",
"mouse_modes_description": "Choose the mouse input mode",
"mouse_modes_title": "Modes",
"mouse_scroll_high": "High",
"mouse_scroll_low": "Low",
"mouse_scroll_medium": "Medium",
"mouse_scroll_off": "Off",
"mouse_scroll_throttling_description": "Reduce the frequency of scroll events",
"mouse_scroll_throttling_title": "Scroll Throttling",
"mouse_scroll_very_high": "Very High",
"mouse_title": "Mouse",
"network_custom_domain": "Custom Domain",
"network_description": "Configure your network settings",
"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_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",
"network_dhcp_lease_renew_confirm_new_b": "you may need to reconnect using the new address",
"network_dhcp_lease_renew_failed": "Failed to renew lease: {error}",
"network_dhcp_lease_renew_success": "DHCP lease renewed",
"network_domain_custom": "Custom",
"network_domain_description": "Network domain suffix for the device",
"network_domain_dhcp_provided": "DHCP provided",
"network_domain_local": ".local",
"network_domain_title": "Domain",
"network_hostname_description": "Device identifier on the network. Blank for system default",
"network_hostname_title": "Hostname",
"network_http_proxy_description": "Proxy server for outgoing HTTP(S) requests from the device. Blank for none.",
"network_http_proxy_invalid": "Invalid HTTP proxy URL",
"network_http_proxy_title": "HTTP Proxy",
"network_ipv4_address": "IPv4 Address",
"network_ipv4_dns": "IPv4 DNS",
"network_ipv4_gateway": "IPv4 Gateway",
"network_ipv4_invalid": "Invalid IPv4 address",
"network_ipv4_invalid_cidr": "Invalid CIDR notation for IPv4 address",
"network_ipv4_mode_description": "Configure the IPv4 mode",
"network_ipv4_mode_dhcp": "DHCP",
"network_ipv4_mode_static": "Static",
"network_ipv4_mode_title": "IPv4 Mode",
"network_ipv4_netmask": "IPv4 Netmask",
"network_ipv6_addresses_header": "IPv6 Addresses",
"network_ipv6_cidr_suggestion": "Please use CIDR notation (e.g., 2001:db8::1/64)",
"network_ipv6_dns": "IPv6 DNS",
"network_ipv6_flag_dad_failed": "DAD Failed",
"network_ipv6_flag_deprecated": "Deprecated",
"network_ipv6_gateway": "IPv6 Gateway",
"network_ipv6_information": "IPv6 Information",
"network_ipv6_invalid": "Invalid IPv6 address",
"network_ipv6_mode_description": "Configure the IPv6 mode",
"network_ipv6_mode_dhcpv6": "DHCPv6",
"network_ipv6_mode_disabled": "Disabled",
"network_ipv6_mode_link_local": "Link-local only",
"network_ipv6_mode_slaac": "SLAAC",
"network_ipv6_mode_slaac_dhcpv6": "SLAAC + DHCPv6",
"network_ipv6_mode_static": "Static",
"network_ipv6_mode_title": "IPv6 Mode",
"network_ipv6_prefix": "IP Prefix",
"network_ipv6_prefix_invalid": "Prefix must be between 0 and 128",
"network_ll_dp_all": "All",
"network_ll_dp_basic": "Basic",
"network_ll_dp_description": "Control which TLVs will be sent over Link Layer Discovery Protocol",
"network_ll_dp_disabled": "Disabled",
"network_ll_dp_title": "LLDP",
"network_mac_address_copy_error": "Failed to copy MAC address",
"network_mac_address_copy_success": "MAC address { mac } copied to clipboard",
"network_mac_address_description": "Hardware identifier for the network interface",
"network_mac_address_title": "MAC Address",
"network_mdns_auto": "Auto",
"network_mdns_description": "Control mDNS (multicast DNS) operational mode",
"network_mdns_disabled": "Disabled",
"network_mdns_ipv4_only": "IPv4 only",
"network_mdns_ipv6_only": "IPv6 only",
"network_mdns_title": "mDNS",
"network_no_information_description": "No network configuration available",
"network_no_information_headline": "Network Information",
"network_pending_dhcp_mode_change_description": "Save settings to enable DHCP mode and view lease information",
"network_pending_dhcp_mode_change_headline": "Pending DHCP IPv4 mode change",
"network_save_settings": "Save Settings",
"network_save_settings_apply_title": "Apply network settings",
"network_save_settings_confirm": "Apply changes",
"network_save_settings_confirm_description": "The following network settings will be applied. These changes may require a reboot and cause brief disconnection.",
"network_save_settings_confirm_heading": "Configuration changes",
"network_save_settings_failed": "Failed to save network settings: {error}",
"network_save_settings_success": "Network settings saved",
"network_settings_add_dns": "Add DNS Server",
"network_settings_load_error": "Failed to load network settings: {error}",
"network_static_ipv4_header": "Static IPv4 Configuration",
"network_static_ipv6_header": "Static IPv6 Configuration",
"network_time_sync_description": "Configure time synchronization settings",
"network_time_sync_http_only": "HTTP only",
"network_time_sync_ntp_and_http": "NTP and HTTP",
"network_time_sync_ntp_only": "NTP only",
"network_time_sync_title": "Time synchronization",
"network_title": "Network",
"never_seen_online": "Never seen online",
"next": "Next",
"no_results_found": "No results found",
"not_applicable": "N/A",
"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_sending_using_layout": "Sending text using keyboard layout: {iso}-{name}",
"paste_text": "Paste text",
"paste_text_description": "Paste text from your client to the remote host",
"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": "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",
"retry": "Retry",
"saving": "Saving…",
"search_placeholder": "Search…",
"serial_console": "Serial Console",
"serial_console_baud_rate": "Baud Rate",
"serial_console_configure_description": "Configure your serial console settings",
"serial_console_data_bits": "Data Bits",
"serial_console_get_settings_error": "Failed to get serial console settings: {error}",
"serial_console_open_console": "Open Console",
"serial_console_parity": "Parity",
"serial_console_parity_even": "Even Parity",
"serial_console_parity_mark": "Mark Parity",
"serial_console_parity_none": "No Parity",
"serial_console_parity_odd": "Odd Parity",
"serial_console_parity_space": "Space Parity",
"serial_console_set_settings_error": "Failed to set serial console settings to {settings}: {error}",
"serial_console_stop_bits": "Stop Bits",
"setting_remote_description": "Setting remote description",
"setting_remote_session_description": "Setting remote session description...",
"setting_up_connection_to_device": "Setting up connection to device...",
"settings_access": "Access",
"settings_advanced": "Advanced",
"settings_appearance": "Appearance",
"settings_back_to_kvm": "Back to KVM",
"settings_general": "General",
"settings_hardware": "Hardware",
"settings_keyboard": "Keyboard",
"settings_keyboard_macros": "Keyboard Macros",
"settings_mouse": "Mouse",
"settings_network": "Network",
"settings_video": "Video",
"something_went_wrong": "Something went wrong. Please try again later or contact support",
"step_counter_step": "Step {step}",
"subnet_mask": "Subnet Mask",
"time_division_days": "days",
"time_division_hours": "hours",
"time_division_minutes": "minutes",
"time_division_months": "months",
"time_division_seconds": "seconds",
"time_division_weeks": "weeks",
"time_division_years": "years",
"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}",
"updating_leave_device_on": "Please don't turn off your device…",
"usb": "USB",
"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_identifiers_description": "USB device identifiers exposed to the target computer",
"usb_config_identifiers_title": "Identifiers",
"usb_config_logitech": "Logitech Universal Adapter",
"usb_config_manufacturer_label": "Manufacturer",
"usb_config_manufacturer_placeholder": "Enter Manufacturer",
"usb_config_microsoft": "Microsoft Wireless MultiMedia Keyboard",
"usb_config_product_id_label": "Product ID",
"usb_config_product_id_placeholder": "Enter Product ID",
"usb_config_product_name_label": "Product Name",
"usb_config_product_name_placeholder": "Enter Product Name",
"usb_config_restore_default": "Restore to Default",
"usb_config_serial_number_label": "Serial Number",
"usb_config_serial_number_placeholder": "Enter Serial Number",
"usb_config_set_success": "USB Config set to {manufacturer} {product}",
"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",
"user_interface_language_description": "Select the language to use in the JetKVM user interface",
"user_interface_language_title": "Interface Language",
"video_brightness_description": "Brightness level ({value}x)",
"video_brightness_title": "Brightness",
"video_contrast_description": "Contrast level ({value}x)",
"video_contrast_title": "Contrast",
"video_custom_edid_description": "EDID details video mode compatibility. Default settings works in most cases, but unique UEFI/BIOS might need adjustments.",
"video_custom_edid_title": "Custom EDID",
"video_debugging_info_description": "Debugging information for video",
"video_debugging_info_title": "Debugging Info",
"video_description": "Configure display settings and EDID for optimal compatibility",
"video_edid_acer_b246wl": "Acer B246WL, 1920x1200",
"video_edid_asus_pa248qv": "ASUS PA248QV, 1920x1200",
"video_edid_custom": "Custom",
"video_edid_dell_d2721h": "DELL D2721H, 1920x1080",
"video_edid_dell_idrac": "DELL IDRAC EDID, 1280x1024",
"video_edid_description": "Adjust the EDID settings for the display",
"video_edid_file_label": "EDID File",
"video_edid_jetkvm_default": "JetKVM Default",
"video_edid_set_success": "EDID set successfully to {edid}",
"video_edid_title": "EDID",
"video_enhancement_description": "Adjust color settings to make the video output more vibrant and colorful",
"video_enhancement_title": "Video Enhancement",
"video_failed_get_debug_info": "Failed to get debug info: {error}",
"video_failed_get_edid": "Failed to get EDID: {error}",
"video_failed_set_edid": "Failed to set EDID: {error}",
"video_failed_set_stream_quality": "Failed to set stream quality: {error}",
"video_get_debugging_info": "Get Debugging Info",
"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",
"video_quality_high": "High",
"video_quality_low": "Low",
"video_quality_medium": "Medium",
"video_reset_to_default": "Reset to Default",
"video_restore_to_default": "Restore to default",
"video_saturation_description": "Color saturation ({value}x)",
"video_saturation_title": "Saturation",
"video_set_custom_edid": "Set Custom EDID",
"video_stream_quality_description": "Adjust the quality of the video stream",
"video_stream_quality_set": "Stream quality set to {quality}",
"video_stream_quality_title": "Stream Quality",
"video_title": "Video",
"view_details": "View Details",
"virtual_keyboard_header": "Virtual Keyboard",
"wake_on_lan": "Wake On LAN",
"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",
"welcome_to_jetkvm": "Welcome to JetKVM",
"welcome_to_jetkvm_description": "Control any computer remotely"
}

View File

@ -0,0 +1,895 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"access_adopt_kvm": "Adopte KVM en la nube",
"access_adopted_message": "Su dispositivo está adoptado en la Nube",
"access_auth_mode_no_password": "Modo actual: Sin contraseña",
"access_auth_mode_password": "Modo actual: Protegido con contraseña",
"access_authentication_mode_title": "Modo de autenticación",
"access_certificate_label": "Certificado",
"access_change_password_button": "Cambiar la contraseña",
"access_change_password_description": "Actualice la contraseña de acceso a su dispositivo",
"access_change_password_title": "Cambiar la contraseña",
"access_cloud_api_url_label": "URL de la API de la nube",
"access_cloud_app_url_label": "URL de la aplicación en la nube",
"access_cloud_provider_description": "Seleccione el proveedor de nube para su dispositivo",
"access_cloud_provider_title": "Proveedor de la nube",
"access_cloud_security_title": "Seguridad en la nube",
"access_confirm_deregister": "¿Está seguro de que desea cancelar el registro de este dispositivo?",
"access_deregister": "Darse de baja de la nube",
"access_description": "Gestionar el Control de Acceso del dispositivo",
"access_disable_protection": "Desactivar protección",
"access_enable_password": "Habilitar contraseña",
"access_failed_deregister": "No se pudo cancelar el registro del dispositivo: {error}",
"access_failed_update_cloud_url": "No se pudo actualizar la URL de la nube: {error}",
"access_failed_update_tls": "No se pudo actualizar la configuración de TLS: {error}",
"access_github_link": "GitHub",
"access_https_description": "Configurar el acceso HTTPS seguro a su dispositivo",
"access_https_mode_title": "Modo HTTPS",
"access_learn_security": "Conozca nuestra seguridad en la nube",
"access_local_description": "Administrar el modo de acceso local al dispositivo",
"access_local_title": "Local",
"access_no_device_id": "No hay ningún ID de dispositivo disponible",
"access_private_key_description": "Por razones de seguridad, no se mostrará después de guardar.",
"access_private_key_label": "Clave privada",
"access_provider_custom": "Costumbre",
"access_provider_jetkvm": "Nube JetKVM",
"access_remote_description": "Administrar el modo de acceso remoto al dispositivo",
"access_security_encryption": "Cifrado de extremo a extremo mediante WebRTC (DTLS y SRTP)",
"access_security_oidc": "Autenticación OIDC (OpenID Connect)",
"access_security_open_source": "Todos los componentes de la nube son de código abierto y están disponibles en GitHub.",
"access_security_streams": "Todas las transmisiones están cifradas en tránsito",
"access_security_zero_trust": "Modelo de seguridad de confianza cero",
"access_title": "Acceso",
"access_tls_certificate_description": "Pegue su certificado TLS a continuación. Para las cadenas de certificados, incluya la cadena completa (certificados hoja, intermedios y raíz).",
"access_tls_certificate_title": "Certificado TLS",
"access_tls_custom": "Costumbre",
"access_tls_disabled": "Desactivado",
"access_tls_self_signed": "Autofirmado",
"access_tls_updated": "La configuración de TLS se actualizó correctamente",
"access_update_tls_settings": "Actualizar la configuración de TLS",
"action_bar_connection_stats": "Estadísticas de conexión",
"action_bar_extension": "Extensión",
"action_bar_fullscreen": "Pantalla completa",
"action_bar_settings": "Ajustes",
"action_bar_virtual_keyboard": "Teclado virtual",
"action_bar_virtual_media": "Medios virtuales",
"action_bar_wake_on_lan": "Activación en LAN",
"action_bar_web_terminal": "Terminal web",
"advanced_description": "Acceda a configuraciones adicionales para la resolución de problemas y personalización",
"advanced_dev_channel_description": "Reciba actualizaciones anticipadas del canal de desarrollo",
"advanced_dev_channel_title": "Actualizaciones del canal de desarrollo",
"advanced_developer_mode_description": "Habilitar funciones avanzadas para desarrolladores",
"advanced_developer_mode_enabled_title": "Modo de desarrollador habilitado",
"advanced_developer_mode_title": "Modo de desarrollador",
"advanced_developer_mode_warning_advanced": "Solo para usuarios avanzados. No apto para producción.",
"advanced_developer_mode_warning_risks": "Úselo solo si comprende los riesgos.",
"advanced_developer_mode_warning_security": "La seguridad se debilita mientras está activa",
"advanced_disable_usb_emulation": "Deshabilitar la emulación USB",
"advanced_enable_usb_emulation": "Habilitar emulación USB",
"advanced_error_loopback_disable": "No se pudo deshabilitar el modo de solo bucle invertido: {error}",
"advanced_error_loopback_enable": "No se pudo habilitar el modo de solo bucle invertido: {error}",
"advanced_error_reset_config": "No se pudo restablecer la configuración: {error}",
"advanced_error_set_dev_channel": "No se pudo establecer el estado del canal de desarrollo: {error}",
"advanced_error_set_dev_mode": "No se pudo establecer el modo de desarrollo: {error}",
"advanced_error_update_ssh_key": "No se pudo actualizar la clave SSH: {error}",
"advanced_error_usb_emulation_disable": "No se pudo deshabilitar la emulación USB: {error}",
"advanced_error_usb_emulation_enable": "No se pudo habilitar la emulación USB: {error}",
"advanced_loopback_only_description": "Restringir el acceso a la interfaz web solo al host local (127.0.0.1)",
"advanced_loopback_only_title": "Modo de solo bucle invertido",
"advanced_loopback_warning_before": "Antes de habilitar esta función, asegúrese de tener:",
"advanced_loopback_warning_cloud": "Acceso a la nube habilitado y funcionando",
"advanced_loopback_warning_confirm": "Entiendo, habilitar de todas formas",
"advanced_loopback_warning_description": "ADVERTENCIA: Esto restringirá el acceso a la interfaz web únicamente al host local (127.0.0.1).",
"advanced_loopback_warning_ssh": "Acceso SSH configurado y probado",
"advanced_loopback_warning_title": "¿Habilitar el modo de solo bucle invertido?",
"advanced_reset_config_button": "Restablecer configuración",
"advanced_reset_config_description": "Restablecer la configuración predeterminada. Esto cerrará la sesión.",
"advanced_reset_config_title": "Restablecer configuración",
"advanced_ssh_access_description": "Agregue su clave pública SSH para habilitar el acceso remoto seguro al dispositivo",
"advanced_ssh_access_title": "Acceso SSH",
"advanced_ssh_default_user": "El usuario SSH predeterminado es",
"advanced_ssh_public_key_label": "Clave pública SSH",
"advanced_ssh_public_key_placeholder": "Ingrese su clave pública SSH",
"advanced_success_loopback_disabled": "El modo de solo bucle invertido está deshabilitado. Reinicie el dispositivo para aplicarlo.",
"advanced_success_loopback_enabled": "Modo de solo bucle invertido habilitado. Reinicie el dispositivo para aplicarlo.",
"advanced_success_reset_config": "La configuración se restableció a los valores predeterminados correctamente",
"advanced_success_update_ssh_key": "Clave SSH actualizada exitosamente",
"advanced_title": "Avanzado",
"advanced_troubleshooting_mode_description": "Herramientas de diagnóstico y controles adicionales para resolución de problemas y fines de desarrollo",
"advanced_troubleshooting_mode_title": "Modo de solución de problemas",
"advanced_update_ssh_key_button": "Actualizar clave SSH",
"advanced_usb_emulation_description": "Controlar el estado de emulación USB",
"advanced_usb_emulation_title": "Emulación USB",
"already_adopted_new_owner": "Si eres el nuevo propietario, solicita al anterior propietario que cancele el registro del dispositivo en su cuenta en el panel de control de la nube. Si crees que se trata de un error, contacta con nuestro equipo de soporte para obtener ayuda.",
"already_adopted_other_user": "Este dispositivo está actualmente registrado por otro usuario en nuestro panel de control en la nube.",
"already_adopted_return_to_dashboard": "Regresar al panel de control",
"already_adopted_title": "Dispositivo ya registrado",
"appearance_description": "Elige tu tema de color preferido",
"appearance_page_description": "Personalice la apariencia de su interfaz JetKVM",
"appearance_theme": "Tema",
"appearance_theme_dark": "Oscuro",
"appearance_theme_light": "Luz",
"appearance_theme_system": "Sistema",
"appearance_title": "Apariencia",
"attach": "Adjuntar",
"atx_power_control_get_state_error": "No se pudo obtener el estado de energía ATX: {error}",
"atx_power_control_hdd_led": "LED del disco duro",
"atx_power_control_long_power_button": "Pulsación larga",
"atx_power_control_power_button": "Fuerza",
"atx_power_control_power_led": "LED de encendido",
"atx_power_control_reset_button": "Reiniciar",
"atx_power_control_send_action_error": "No se pudo enviar la acción de alimentación ATX {action} : {error}",
"atx_power_control_short_power_button": "Prensa corta",
"auth_authentication_mode": "Por favor seleccione un modo de autenticación",
"auth_authentication_mode_error": "Se produjo un error al configurar el modo de autenticación",
"auth_authentication_mode_invalid": "Modo de autenticación no válido",
"auth_connect_to_cloud": "Conecte su JetKVM a la nube",
"auth_connect_to_cloud_action": "Iniciar sesión y conectar el dispositivo",
"auth_connect_to_cloud_description": "Desbloquee el acceso remoto y las funciones avanzadas para su dispositivo",
"auth_header_cta_already_have_account": "¿Ya tienes una cuenta?",
"auth_header_cta_dont_have_account": "¿No tienes una cuenta?",
"auth_header_cta_new_to_jetkvm": "¿Eres nuevo en JetKVM?",
"auth_login": "Inicie sesión en su cuenta JetKVM",
"auth_login_action": "Acceso",
"auth_login_description": "Inicie sesión para acceder y administrar sus dispositivos de forma segura",
"auth_mode_local": "Método de autenticación local",
"auth_mode_local_change_later": "Siempre puedes cambiar tu método de autenticación más tarde en la configuración.",
"auth_mode_local_description": "Seleccione cómo desea proteger su dispositivo JetKVM localmente.",
"auth_mode_local_no_password": "Sin contraseña",
"auth_mode_local_no_password_description": "Acceso rápido sin autenticación de contraseña.",
"auth_mode_local_password": "Contraseña",
"auth_mode_local_password_confirm_description": "Confirma tu contraseña",
"auth_mode_local_password_confirm_label": "confirmar Contraseña",
"auth_mode_local_password_description": "Proteja su dispositivo con una contraseña para mayor protección.",
"auth_mode_local_password_failed_set": "No se pudo establecer la contraseña: {error}",
"auth_mode_local_password_note": "Esta contraseña se utilizará para proteger los datos de su dispositivo y contra accesos no autorizados.",
"auth_mode_local_password_note_local": "Todos los datos permanecen en su dispositivo local.",
"auth_mode_local_password_set": "Establecer una contraseña",
"auth_mode_local_password_set_button": "Establecer contraseña",
"auth_mode_local_password_set_description": "Cree una contraseña segura para proteger su dispositivo JetKVM localmente.",
"auth_mode_local_password_set_label": "Introduzca una contraseña",
"auth_signup_connect_to_cloud_action": "Registrarse y conectar dispositivo",
"auth_signup_create_account": "Crea tu cuenta JetKVM",
"auth_signup_create_account_action": "Crear una cuenta",
"auth_signup_create_account_description": "Crea tu cuenta y comienza a administrar tus dispositivos con facilidad.",
"back": "Atrás",
"back_to_devices": "Volver a Dispositivos",
"cancel": "Cancelar",
"close": "Cerca",
"cloud_kvms": "KVM en la nube",
"cloud_kvms_description": "Administre sus KVM en la nube y conéctese a ellos de forma segura.",
"cloud_kvms_no_devices": "No se encontraron dispositivos",
"cloud_kvms_no_devices_description": "Aún no tienes ningún dispositivo con JetKVM Cloud habilitado.",
"confirm": "Confirmar",
"connect_to_kvm": "Conectarse a KVM",
"connecting_to_device": "Conectando al dispositivo…",
"connection_established": "Conexión establecida",
"connection_stats_badge_jitter": "Estar nervioso",
"connection_stats_badge_jitter_buffer_avg_delay": "Retardo promedio del búfer de fluctuación",
"connection_stats_connection": "Conexión",
"connection_stats_connection_description": "La conexión entre el cliente y JetKVM.",
"connection_stats_frames_per_second": "Fotogramas por segundo",
"connection_stats_frames_per_second_description": "Número de fotogramas de vídeo entrantes que se muestran por segundo.",
"connection_stats_network_stability": "Estabilidad de la red",
"connection_stats_network_stability_description": "Qué tan constante es el flujo de paquetes de vídeo entrantes a través de la red.",
"connection_stats_packets_lost": "Paquetes perdidos",
"connection_stats_packets_lost_description": "Recuento de paquetes de vídeo RTP entrantes perdidos.",
"connection_stats_playback_delay": "Retraso de reproducción",
"connection_stats_playback_delay_description": "Retraso agregado por el buffer de fluctuación para suavizar la reproducción cuando los cuadros llegan de manera desigual.",
"connection_stats_round_trip_time": "Tiempo de ida y vuelta",
"connection_stats_round_trip_time_description": "Tiempo de ida y vuelta para el par de candidatos ICE activos entre pares.",
"connection_stats_sidebar": "Estadísticas de conexión",
"connection_stats_unit_frames_per_second": " fps",
"connection_stats_unit_milliseconds": " ms",
"connection_stats_unit_packets": " paquetes",
"connection_stats_video": "Video",
"connection_stats_video_description": "La transmisión de vídeo desde JetKVM al cliente.",
"continue": "Continuar",
"creating_peer_connection": "Creando conexión entre pares…",
"dc_power_control_current": "Actual",
"dc_power_control_current_unit": "A",
"dc_power_control_get_state_error": "No se pudo obtener el estado de la alimentación de CC: {error}",
"dc_power_control_power": "Fuerza",
"dc_power_control_power_off_button": "Apagado",
"dc_power_control_power_off_state": "Apagado",
"dc_power_control_power_on_button": "Encendido",
"dc_power_control_power_on_state": "Encendido",
"dc_power_control_power_unit": "O",
"dc_power_control_restore_last_state": "Último estado",
"dc_power_control_restore_power_state": "Restaurar pérdida de energía",
"dc_power_control_set_power_state_error": "No se pudo enviar el estado de alimentación de CC a {enabled} : {error}",
"dc_power_control_set_restore_state_error": "No se pudo enviar el estado de restauración de energía de CC a {state} : {error}",
"dc_power_control_voltage": "Voltaje",
"dc_power_control_voltage_unit": "V",
"delete": "Borrar",
"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.",
"deregister_from_cloud": "Darse de baja de la nube",
"deregister_headline": "Anular el registro de {device} en su cuenta en la nube",
"detach": "Despegar",
"dhcp_empty_lease_description": "Aún no hemos recibido ninguna información de concesión de DHCP del dispositivo.",
"dhcp_empty_lease_headline": "No hay información de arrendamiento de DHCP",
"dhcp_lease_boot_file": "Archivo de arranque",
"dhcp_lease_boot_next_server": "Arrancar el siguiente servidor",
"dhcp_lease_boot_server_name": "Nombre del servidor de arranque",
"dhcp_lease_broadcast": "Transmisión",
"dhcp_lease_domain": "Dominio",
"dhcp_lease_gateway": "Puerta",
"dhcp_lease_header": "Información de arrendamiento de DHCP",
"dhcp_lease_hostname": "Nombre de host",
"dhcp_lease_lease_expires": "El contrato de arrendamiento vence",
"dhcp_lease_maximum_transfer_unit": "Unidad de transmisión máxima (MTU)",
"dhcp_lease_renew": "Renovar la concesión de DHCP",
"dhcp_lease_time_to_live": "Tiempo de vida",
"dhcp_server": "Servidor DHCP",
"dns_servers": "Servidores DNS",
"establishing_secure_connection": "Estableciendo conexión segura…",
"experimental": "Experimental",
"extension_popover_load_and_manage_extensions": "Cargar y administrar sus extensiones",
"extension_popover_set_error_notification": "No se pudo establecer la extensión activa: {error}",
"extension_popover_unload_extension": "Extensión de descarga",
"extension_serial_console": "Consola serial",
"extension_serial_console_description": "Acceda a la extensión de su consola serie",
"extensions_atx_power_control": "Control de alimentación ATX",
"extensions_atx_power_control_description": "Controle el estado de energía de su máquina a través del control de energía ATX.",
"extensions_dc_power_control": "Control de potencia de CC",
"extensions_dc_power_control_description": "Controle su extensión de alimentación de CC",
"extensions_popover_extensions": "Extensiones",
"gathering_ice_candidates": "Reuniendo candidatos del ICE…",
"general_app_version": "Aplicación: {version}",
"general_auto_update_description": "Actualizar automáticamente el dispositivo a la última versión",
"general_auto_update_error": "No se pudo configurar la actualización automática: {error}",
"general_auto_update_title": "Actualización automática",
"general_check_for_updates": "Buscar actualizaciones",
"general_page_description": "Configurar los ajustes del dispositivo y actualizar las preferencias",
"general_reboot_description": "¿Desea continuar con el reinicio del sistema?",
"general_reboot_device": "Reiniciar el dispositivo",
"general_reboot_device_description": "Apague y encienda el JetKVM",
"general_reboot_no_button": "No",
"general_reboot_title": "Reiniciar JetKVM",
"general_reboot_yes_button": "Sí",
"general_system_version": "Sistema: {version}",
"general_title": "General",
"general_update_app_update_title": "Actualización de la aplicación",
"general_update_application_type": "Aplicación",
"general_update_available_description": "Hay una nueva actualización disponible para mejorar el rendimiento del sistema y la compatibilidad. Recomendamos actualizar para garantizar un funcionamiento fluido.",
"general_update_available_title": "Actualización disponible",
"general_update_background_button": "Actualización en segundo plano",
"general_update_check_again_button": "Revisalo de nuevo",
"general_update_checking_description": "Nos aseguramos de que su dispositivo tenga las últimas funciones y mejoras.",
"general_update_checking_title": "Buscando actualizaciones…",
"general_update_completed_description": "Tu dispositivo se ha actualizado correctamente a la última versión. ¡Disfruta de las nuevas funciones y mejoras!",
"general_update_completed_title": "Actualización completada exitosamente",
"general_update_error_description": "Se produjo un error al actualizar tu dispositivo. Inténtalo de nuevo más tarde.",
"general_update_error_details": "Detalles del error: {errorMessage}",
"general_update_error_title": "Error de actualización",
"general_update_later_button": "Hazlo más tarde",
"general_update_now_button": "Actualizar ahora",
"general_update_rebooting": "Reiniciando para completar la actualización…",
"general_update_status_awaiting_reboot": "Esperando reinicio",
"general_update_status_downloading": "Descargando actualización {update_type} …",
"general_update_status_fetching": "Obteniendo información de actualización…",
"general_update_status_installing": "Instalando {update_type} actualización…",
"general_update_status_progress": "{part} progreso",
"general_update_status_verifying": "Verificando la actualización {update_type} …",
"general_update_system_type": "Sistema",
"general_update_system_update_title": "Actualización del sistema Linux",
"general_update_up_to_date_description": "Su sistema está ejecutando la última versión. No hay actualizaciones disponibles actualmente.",
"general_update_up_to_date_title": "El sistema está actualizado",
"general_update_updating_description": "No apagues tu dispositivo. Este proceso puede tardar unos minutos.",
"general_update_updating_title": "Actualizar su dispositivo",
"getting_remote_session_description": "Obtener un intento de descripción de sesión remota {attempt}",
"hardware_backlight_settings_error": "No se pudieron configurar los ajustes de la retroiluminación: {error}",
"hardware_backlight_settings_get_error": "No se pudieron obtener los ajustes de la retroiluminación: {error}",
"hardware_backlight_settings_success": "La configuración de la luz de fondo se actualizó correctamente",
"hardware_dim_display_after_description": "Establezca cuánto tiempo esperar antes de atenuar la pantalla",
"hardware_dim_display_after_title": "Atenuar la pantalla después",
"hardware_display_brightness_description": "Establecer el brillo de la pantalla",
"hardware_display_brightness_high": "Alto",
"hardware_display_brightness_low": "Bajo",
"hardware_display_brightness_medium": "Medio",
"hardware_display_brightness_off": "Apagado",
"hardware_display_brightness_title": "Brillo de la pantalla",
"hardware_display_orientation_description": "Establecer la orientación de la pantalla",
"hardware_display_orientation_error": "No se pudo establecer la orientación de la pantalla: {error}",
"hardware_display_orientation_inverted": "Invertido",
"hardware_display_orientation_normal": "Normal",
"hardware_display_orientation_success": "La orientación de la pantalla se actualizó correctamente",
"hardware_display_orientation_title": "Orientación de la pantalla",
"hardware_display_wake_up_note": "La pantalla se activará cuando cambie el estado de la conexión o cuando se toque.",
"hardware_page_description": "Configure los ajustes de pantalla y las opciones de hardware para su dispositivo JetKVM",
"hardware_power_saving_description": "Reduce el consumo de energía cuando no esté en uso",
"hardware_power_saving_disabled": "Modo de ahorro de energía deshabilitado",
"hardware_power_saving_enabled": "Modo de ahorro de energía habilitado",
"hardware_power_saving_failed_error": "No se pudo establecer el modo de ahorro de energía: {error}",
"hardware_power_saving_hdmi_sleep_description": "Desactivar la captura después de 90 segundos de inactividad",
"hardware_power_saving_hdmi_sleep_title": "Modo de suspensión HDMI",
"hardware_power_saving_title": "Ahorro de energía",
"hardware_time_10_minutes": "10 minutos",
"hardware_time_1_hour": "1 hora",
"hardware_time_1_minute": "1 minuto",
"hardware_time_30_minutes": "30 minutos",
"hardware_time_5_minutes": "5 minutos",
"hardware_time_never": "Nunca",
"hardware_title": "Hardware",
"hardware_turn_off_display_after_description": "Periodo de inactividad antes de que la pantalla se apague automáticamente",
"hardware_turn_off_display_after_title": "Apagar la pantalla después",
"hide": "Esconder",
"ice_gathering_completed": "Reunión de ICE completada",
"info_caps_lock": "Bloq Mayús",
"info_compose": "Componer",
"info_hdmi_state": "Estado HDMI:",
"info_hidrpc_state": "Estado de HidRPC:",
"info_kana": "Kana",
"info_keys": "Llaves:",
"info_last_move": "Último movimiento:",
"info_num_lock": "Bloq Num",
"info_paste_enabled": "Activado",
"info_paste_mode": "Modo pegar:",
"info_pointer": "Puntero:",
"info_relayed_by_cloudflare": "Retransmitido por Cloudflare",
"info_resolution": "Resolución:",
"info_scroll_lock": "Bloq Despl",
"info_shift": "Cambio",
"info_usb_state": "Estado USB:",
"info_video_size": "Tamaño del vídeo:",
"input_disabled": "Entrada deshabilitada",
"invalid_password": "Contraseña inválida",
"ip_address": "Dirección IP",
"ipv6_address_label": "DIRECCIÓN",
"ipv6_gateway": "Puerta",
"ipv6_information": "Información de IPv6",
"ipv6_link_local": "Enlace local",
"ipv6_preferred_lifetime": "Vida útil preferida",
"ipv6_valid_lifetime": "Válido de por vida",
"jetkvm_description": "JetKVM combina hardware potente con software intuitivo para brindar una experiencia de control remoto perfecta.",
"jetkvm_device": "Dispositivo JetKVM",
"jetkvm_logo": "Logotipo de JetKVM",
"jetkvm_setup": "Configura tu JetKVM",
"jiggler_cron_schedule_description": "Expresión cron para la programación",
"jiggler_cron_schedule_label": "Programación cron",
"jiggler_example_business_hours_early": "Horario comercial 8-17",
"jiggler_example_business_hours_late": "Horario comercial: 9 a 17",
"jiggler_examples_label": "Ejemplos",
"jiggler_inactivity_limit_description": "Tiempo de inactividad antes del movimiento",
"jiggler_inactivity_limit_label": "Límite de segundos de inactividad",
"jiggler_more_examples": "Más ejemplos",
"jiggler_random_delay_description": "Para evitar patrones reconocibles",
"jiggler_random_delay_label": "Retraso aleatorio",
"jiggler_save_jiggler_config": "Guardar configuración de Jiggler",
"jiggler_timezone_description": "Zona horaria para la programación cron",
"jiggler_timezone_label": "Zona horaria",
"keyboard_description": "Configure los ajustes del teclado para su dispositivo",
"keyboard_layout_description": "Disposición del teclado del sistema operativo de destino",
"keyboard_layout_error": "No se pudo establecer la distribución del teclado: {error}",
"keyboard_layout_long_description": "El teclado virtual, la función de pegar texto y las macros de teclado envían pulsaciones de teclas individuales al dispositivo de destino. La distribución del teclado determina qué códigos de tecla se envían. Asegúrese de que la distribución del teclado en JetKVM coincida con la configuración del sistema operativo.",
"keyboard_layout_success": "La distribución del teclado se ha establecido correctamente en {layout}",
"keyboard_layout_title": "Disposición del teclado",
"keyboard_show_pressed_keys_description": "Mostrar las teclas presionadas actualmente en la barra de estado",
"keyboard_show_pressed_keys_title": "Mostrar teclas presionadas",
"keyboard_title": "Teclado",
"kvm_terminal": "Terminal KVM",
"last_online": "Última conexión {time}",
"learn_more": "Más información",
"load": "Carga",
"loading": "Cargando…",
"local_auth_change_local_device_password_description": "Ingrese su contraseña actual y una nueva contraseña para actualizar la protección de su dispositivo local.",
"local_auth_change_local_device_password_title": "Cambiar la contraseña del dispositivo local",
"local_auth_confirm_new_password_label": "Confirmar nueva contraseña",
"local_auth_create_confirm_password_placeholder": "Vuelva a introducir su contraseña",
"local_auth_create_description": "Cree una contraseña para proteger su dispositivo contra acceso local no autorizado.",
"local_auth_create_new_password_label": "Nueva contraseña",
"local_auth_create_new_password_placeholder": "Introduzca una contraseña segura",
"local_auth_create_not_now_button": "Ahora no",
"local_auth_create_secure_button": "Dispositivo seguro",
"local_auth_create_title": "Protección de dispositivos locales",
"local_auth_current_password_label": "Contraseña actual",
"local_auth_disable_local_device_protection_description": "Ingrese su contraseña actual para deshabilitar la protección del dispositivo local.",
"local_auth_disable_local_device_protection_title": "Deshabilitar la protección del dispositivo local",
"local_auth_disable_protection_button": "Desactivar protección",
"local_auth_enter_current_password_placeholder": "Ingrese su contraseña actual",
"local_auth_enter_new_password_placeholder": "Introduzca una nueva contraseña segura",
"local_auth_error_changing_password": "Se produjo un error al cambiar la contraseña",
"local_auth_error_disabling_password": "Se produjo un error al deshabilitar la contraseña",
"local_auth_error_enter_current_password": "Por favor, introduzca su contraseña actual",
"local_auth_error_enter_new_password": "Por favor, introduzca una nueva contraseña",
"local_auth_error_enter_old_password": "Por favor, introduzca su contraseña anterior",
"local_auth_error_enter_password": "Por favor, introduzca una contraseña",
"local_auth_error_passwords_not_match": "Las contraseñas no coinciden",
"local_auth_error_setting_password": "Se produjo un error al configurar la contraseña",
"local_auth_new_password_label": "Nueva contraseña",
"local_auth_reenter_new_password_placeholder": "Vuelva a ingresar su nueva contraseña",
"local_auth_success_password_disabled_description": "Has desactivado correctamente la protección con contraseña para el acceso local. Recuerda que tu dispositivo ahora es menos seguro.",
"local_auth_success_password_disabled_title": "Protección con contraseña deshabilitada",
"local_auth_success_password_set_description": "Has configurado correctamente la protección local del dispositivo. Tu dispositivo ahora está protegido contra el acceso local no autorizado.",
"local_auth_success_password_set_title": "Contraseña establecida exitosamente",
"local_auth_success_password_updated_description": "Has cambiado correctamente la contraseña de protección de tu dispositivo local. Recuerda la nueva contraseña para acceder en el futuro.",
"local_auth_success_password_updated_title": "Contraseña actualizada exitosamente",
"local_auth_update_password_button": "Actualizar contraseña",
"locale_auto": "Auto",
"locale_change_success": "El idioma se cambió correctamente a {locale}",
"locale_da": "Danés",
"locale_de": "Alemán",
"locale_en": "Inglés",
"locale_es": "Español",
"locale_fr": "Francés",
"locale_it": "Italiano",
"locale_nb": "Noruego (bokmål)",
"locale_sv": "Sueco",
"locale_zh": "中文 (简体)",
"log_in": "Acceso",
"log_out": "Finalizar la sesión",
"logged_in_as": "Inició sesión como",
"login_enter_password": "Ingrese su contraseña",
"login_enter_password_description": "Introduzca su contraseña para acceder a su JetKVM.",
"login_error": "Se produjo un error al iniciar sesión",
"login_forgot_password": "¿Has olvidado tu contraseña?",
"login_password_label": "Contraseña",
"login_welcome_back": "Bienvenido de nuevo a JetKVM",
"macro_add_step": "Agregar paso {maxed_out}",
"macro_at_least_one_step_keys_or_modifiers": "Al menos un paso debe tener claves o modificadores",
"macro_at_least_one_step_required": "Se requiere al menos un paso",
"macro_max_steps_error": "Solo puede agregar un máximo de {max} pasos por macro.",
"macro_max_steps_reached": "( {max} máx.)",
"macro_name_label": "Nombre de la macro",
"macro_name_required": "El nombre es obligatorio",
"macro_name_too_long": "El nombre debe tener menos de 50 caracteres.",
"macro_please_fix_validation_errors": "Por favor corrija los errores de validación",
"macro_save": "Guardar macro",
"macro_save_failed": "Se produjo un error al guardar.",
"macro_save_failed_error": "Se produjo un error al guardar: {error}.",
"macro_step_count": "{steps} / {max} pasos",
"macro_step_duration_description": "Tiempo de espera antes de ejecutar el siguiente paso.",
"macro_step_duration_label": "Duración del paso",
"macro_step_keys_description": "Máximo de {max} claves por paso.",
"macro_step_keys_label": "Llaves",
"macro_step_max_keys_reached": "Se alcanzó el máximo de claves",
"macro_step_modifiers_description": "¿Qué modificadores (Shift/Ctrl/Alt/Meta) se presionan durante este paso?",
"macro_step_modifiers_label": "Modificadores",
"macro_step_no_matching_keys_found": "No se encontraron claves coincidentes",
"macro_step_search_for_key": "Buscar clave…",
"macro_steps_description": "Teclas/modificadores que se ejecutan en secuencia con un retraso entre cada paso.",
"macro_steps_label": "Pasos",
"macros_add_description": "Crear una nueva macro de teclado",
"macros_add_new": "Agregar nueva macro",
"macros_add_new_macro": "Agregar nueva macro",
"macros_aria_add_new": "Agregar nueva macro",
"macros_aria_delete": "Eliminar macro {name}",
"macros_aria_duplicate": "Macro duplicada {name}",
"macros_aria_edit": "Editar macro {name}",
"macros_aria_move_down": "Mover {name} hacia abajo",
"macros_aria_move_up": "Mover {name} hacia arriba",
"macros_confirm_delete_description": "¿Seguro que desea eliminar \" {name} \"? Esta acción no se puede deshacer.",
"macros_confirm_delete_title": "Eliminar macro",
"macros_confirm_deleting": "Borrando…",
"macros_create_first_description": "Combine pulsaciones de teclas en una sola acción",
"macros_create_first_headline": "Crea tu primera macro",
"macros_created_success": "Macro \" {name} \" creada correctamente",
"macros_delay_only": "Solo retraso",
"macros_delete_confirm": "¿Seguro que desea eliminar esta macro? Esta acción no se puede deshacer.",
"macros_delete_macro": "Eliminar macro",
"macros_deleted_success": "Macro \" {name} \" eliminada correctamente",
"macros_deleting": "Eliminando",
"macros_duplicated_success": "Macro \" {name} \" duplicada correctamente",
"macros_edit_button": "Editar",
"macros_edit_description": "Modificar la macro del teclado",
"macros_edit_title": "Editar macro",
"macros_failed_create": "No se pudo crear la macro",
"macros_failed_create_error": "No se pudo crear la macro: {error}",
"macros_failed_delete": "No se pudo eliminar la macro",
"macros_failed_delete_error": "No se pudo eliminar la macro: {error}",
"macros_failed_duplicate": "No se pudo duplicar la macro",
"macros_failed_duplicate_error": "No se pudo duplicar la macro: {error}",
"macros_failed_reorder": "No se pudieron reordenar las macros",
"macros_failed_reorder_error": "No se pudieron reordenar las macros: {error}",
"macros_failed_update": "No se pudo actualizar la macro",
"macros_failed_update_error": "No se pudo actualizar la macro: {error}",
"macros_invalid_data": "Datos de macro no válidos",
"macros_loading": "Cargando macros…",
"macros_max_reached": "Máximo alcanzado",
"macros_maximum_macros_reached": "Ha alcanzado el número máximo de macros {maximum} permitidas.",
"macros_no_macros_available": "No hay macros disponibles",
"macros_order_updated": "La orden macro se actualizó correctamente",
"macros_title": "Macros del teclado",
"macros_updated_success": "Macro \" {name} \" actualizada correctamente",
"metric_not_supported": "Métrica no compatible",
"metric_waiting_for_data": "Esperando datos…",
"mount_add_file_to_get_started": "Añade un archivo para comenzar",
"mount_add_new_media": "Agregar nuevos medios",
"mount_available_storage": "Almacenamiento disponible",
"mount_button_back_to_overview": "Volver a la descripción general",
"mount_button_cancel_upload": "Cancelar carga",
"mount_button_continue_upload": "Continuar subiendo",
"mount_button_mount_file": "Montar archivo",
"mount_button_mount_url": "URL de montaje",
"mount_button_select": "Seleccionar",
"mount_button_showing_results": "Mostrando {from} a {to} de {total} resultados",
"mount_button_upload_new_image": "Subir una nueva imagen",
"mount_bytes_free": "{bytesFree} libre",
"mount_bytes_used": "{bytesUsed} usado",
"mount_calculating": "Calculador…",
"mount_click_to_select_file": "Haga clic para seleccionar un archivo",
"mount_click_to_select_incomplete": "Haga clic para seleccionar \" {name} \"",
"mount_confirm_delete": "¿Estás seguro de que deseas eliminar {name} ?",
"mount_continue_uploading_with_name": "Continuar cargando \" {name} \"",
"mount_error_delete_file": "Error al eliminar el archivo: {error}",
"mount_error_description": "Se produjo un error al intentar montar el medio. Inténtelo de nuevo.",
"mount_error_get_storage_space": "Error al obtener espacio de almacenamiento: {error}",
"mount_error_list_storage": "Error al listar archivos de almacenamiento: {error}",
"mount_error_title": "Error de montaje",
"mount_get_state_error": "No se pudo obtener el estado del medio virtual: {error}",
"mount_jetkvm_storage": "Soporte de almacenamiento JetKVM",
"mount_jetkvm_storage_description": "Montar archivos cargados previamente desde el almacenamiento JetKVM",
"mount_mode_cdrom": "CD/DVD",
"mount_mode_disk": "Disco",
"mount_mounted_as": "Montado como",
"mount_mounted_from_storage": "Montado desde el almacenamiento JetKVM",
"mount_no_images_description": "Sube una imagen para iniciar el montaje de medios virtuales.",
"mount_no_images_title": "No hay imágenes disponibles",
"mount_no_mounted_media": "No hay medios montados",
"mount_percentage_used": "{percentageUsed} % usado",
"mount_please_select_file": "Seleccione el archivo \" {name} \" para continuar con la carga.",
"mount_popular_images": "Imágenes populares",
"mount_streaming_from_url": "Transmisión desde URL",
"mount_supported_formats": "Formatos compatibles: ISO, IMG",
"mount_unmount": "Desmontar",
"mount_unmount_error": "Error al desmontar la imagen: {error}",
"mount_upload_description": "Seleccione un archivo de imagen para cargar al almacenamiento de JetKVM",
"mount_upload_error": "Error de carga: {error}",
"mount_upload_failed_datachannel": "No se pudo crear el canal de datos para la carga de archivos",
"mount_upload_failed_rtc": "Error de carga: {error}",
"mount_upload_successful": "Subida exitosa",
"mount_upload_title": "Subir nueva imagen",
"mount_uploaded_has_been_uploaded": "Se ha cargado {name}",
"mount_uploading": "Subiendo…",
"mount_uploading_with_name": "Subiendo {name}",
"mount_url_description": "Montar archivos desde cualquier dirección web pública",
"mount_url_input_label": "URL de la imagen",
"mount_url_mount": "Montaje de URL",
"mount_view_device_description": "Seleccione una imagen para montar desde el almacenamiento JetKVM",
"mount_view_device_title": "Montar desde el almacenamiento JetKVM",
"mount_view_url_description": "Introduzca una URL al archivo de imagen que desea montar",
"mount_view_url_title": "Montar desde URL",
"mount_virtual_media": "Medios virtuales",
"mount_virtual_media_description": "Montar una imagen para arrancar o instalar un sistema operativo.",
"mount_virtual_media_source": "Fuente de medios virtuales",
"mount_virtual_media_source_description": "Elige cómo quieres montar tus medios virtuales",
"mouse_alt_finger": "Dedo tocando una pantalla",
"mouse_alt_mouse": "Icono del ratón",
"mouse_description": "Configure el comportamiento del cursor y los ajustes de interacción para su dispositivo",
"mouse_hide_cursor_description": "Ocultar el cursor al enviar movimientos del mouse",
"mouse_hide_cursor_title": "Ocultar el cursor",
"mouse_jiggler_config_updated": "La configuración de Jiggler se actualizó correctamente",
"mouse_jiggler_custom": "Costumbre",
"mouse_jiggler_description": "Simular el movimiento de un ratón de ordenador",
"mouse_jiggler_disabled": "Desactivado",
"mouse_jiggler_error_config": "Se produjo un error al configurar el jiggler.",
"mouse_jiggler_failed_state": "No se pudo establecer el estado del jiggler: {error}",
"mouse_jiggler_frequent": "Frecuente - 30s",
"mouse_jiggler_invalid_cron": "Expresión cron no válida. Verifique el formato de su programación (p. ej., '0 * * * * *' para cada minuto).",
"mouse_jiggler_light": "Luz - 5m",
"mouse_jiggler_standard": "Estándar - 1 m",
"mouse_jiggler_title": "Jiggler",
"mouse_mode_absolute": "Absoluto",
"mouse_mode_absolute_description": "Lo más conveniente",
"mouse_mode_relative": "Relativo",
"mouse_mode_relative_description": "Más compatible",
"mouse_modes_description": "Elija el modo de entrada del mouse",
"mouse_modes_title": "Modos",
"mouse_scroll_high": "Alto",
"mouse_scroll_low": "Bajo",
"mouse_scroll_medium": "Medio",
"mouse_scroll_off": "Apagado",
"mouse_scroll_throttling_description": "Reducir la frecuencia de los eventos de desplazamiento",
"mouse_scroll_throttling_title": "Limitación de desplazamiento",
"mouse_scroll_very_high": "Muy alto",
"mouse_title": "Ratón",
"network_custom_domain": "Dominio personalizado",
"network_description": "Configurar sus ajustes de red",
"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_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",
"network_dhcp_lease_renew_confirm_new_b": "Es posible que necesites volver a conectarte usando la nueva dirección",
"network_dhcp_lease_renew_failed": "No se pudo renovar el contrato de arrendamiento: {error}",
"network_dhcp_lease_renew_success": "Se renovó la concesión de DHCP",
"network_domain_custom": "Costumbre",
"network_domain_description": "Sufijo de dominio de red para el dispositivo",
"network_domain_dhcp_provided": "DHCP proporcionado",
"network_domain_local": ".local",
"network_domain_title": "Dominio",
"network_hostname_description": "Identificador del dispositivo en la red. En blanco para el valor predeterminado del sistema.",
"network_hostname_title": "Nombre de host",
"network_http_proxy_description": "Servidor proxy para solicitudes HTTP(S) salientes desde el dispositivo. En blanco si no hay ninguna.",
"network_http_proxy_invalid": "URL de proxy HTTP no válida",
"network_http_proxy_title": "Proxy HTTP",
"network_ipv4_address": "Dirección IPv4",
"network_ipv4_dns": "DNS IPv4",
"network_ipv4_gateway": "Puerta de enlace IPv4",
"network_ipv4_invalid": "Dirección IPv4 no válida",
"network_ipv4_invalid_cidr": "Notación CIDR no válida para la dirección IPv4",
"network_ipv4_mode_description": "Configurar el modo IPv4",
"network_ipv4_mode_dhcp": "DHCP",
"network_ipv4_mode_static": "Estático",
"network_ipv4_mode_title": "Modo IPv4",
"network_ipv4_netmask": "Máscara de red IPv4",
"network_ipv6_addresses_header": "Direcciones IPv6",
"network_ipv6_cidr_suggestion": "Utilice la notación CIDR (por ejemplo, 2001:db8::1/64)",
"network_ipv6_dns": "DNS IPv6",
"network_ipv6_flag_dad_failed": "Papá falló",
"network_ipv6_flag_deprecated": "Obsoleto",
"network_ipv6_gateway": "Puerta de enlace IPv6",
"network_ipv6_information": "Información de IPv6",
"network_ipv6_invalid": "Dirección IPv6 no válida",
"network_ipv6_mode_description": "Configurar el modo IPv6",
"network_ipv6_mode_dhcpv6": "DHCPv6",
"network_ipv6_mode_disabled": "Desactivado",
"network_ipv6_mode_link_local": "Solo enlace local",
"network_ipv6_mode_slaac": "SLAAC",
"network_ipv6_mode_slaac_dhcpv6": "SLAAC + DHCPv6",
"network_ipv6_mode_static": "Estático",
"network_ipv6_mode_title": "Modo IPv6",
"network_ipv6_prefix": "Prefijo IP",
"network_ipv6_prefix_invalid": "El prefijo debe estar entre 0 y 128",
"network_ll_dp_all": "Todo",
"network_ll_dp_basic": "Básico",
"network_ll_dp_description": "Controlar qué TLV se enviarán a través del Protocolo de descubrimiento de capa de enlace",
"network_ll_dp_disabled": "Desactivado",
"network_ll_dp_title": "LLDP",
"network_mac_address_copy_error": "No se pudo copiar la dirección MAC",
"network_mac_address_copy_success": "Dirección MAC { mac } copiada al portapapeles",
"network_mac_address_description": "Identificador de hardware para la interfaz de red",
"network_mac_address_title": "Dirección MAC",
"network_mdns_auto": "Auto",
"network_mdns_description": "Controlar el modo operativo mDNS (DNS multidifusión)",
"network_mdns_disabled": "Desactivado",
"network_mdns_ipv4_only": "Sólo IPv4",
"network_mdns_ipv6_only": "Sólo IPv6",
"network_mdns_title": "mDNS",
"network_no_information_description": "No hay configuración de red disponible",
"network_no_information_headline": "Información de la red",
"network_pending_dhcp_mode_change_description": "Guardar la configuración para habilitar el modo DHCP y ver la información de arrendamiento",
"network_pending_dhcp_mode_change_headline": "Cambio de modo DHCP IPv4 pendiente",
"network_save_settings": "Guardar configuración",
"network_save_settings_apply_title": "Aplicar configuración de red",
"network_save_settings_confirm": "Aplicar cambios",
"network_save_settings_confirm_description": "Se aplicará la siguiente configuración de red. Estos cambios pueden requerir un reinicio y causar una breve desconexión.",
"network_save_settings_confirm_heading": "Cambios de configuración",
"network_save_settings_failed": "No se pudo guardar la configuración de red: {error}",
"network_save_settings_success": "Configuración de red guardada",
"network_settings_add_dns": "Agregar servidor DNS",
"network_settings_load_error": "No se pudo cargar la configuración de red: {error}",
"network_static_ipv4_header": "Configuración estática de IPv4",
"network_static_ipv6_header": "Configuración estática de IPv6",
"network_time_sync_description": "Configurar los ajustes de sincronización horaria",
"network_time_sync_http_only": "Sólo HTTP",
"network_time_sync_ntp_and_http": "NTP y HTTP",
"network_time_sync_ntp_only": "Sólo NTP",
"network_time_sync_title": "Sincronización horaria",
"network_title": "Red",
"never_seen_online": "Nunca visto en línea",
"next": "Próximo",
"no_results_found": "No se encontraron resultados",
"not_applicable": "N / A",
"not_available": "N / A",
"not_found": "Extraviado",
"ntp_servers": "Servidores NTP",
"oh_no": "¡Oh, no!",
"online": "En línea",
"other_session_detected": "Otra sesión activa detectada",
"other_session_take_over": " Solo se admite una sesión activa a la vez. ¿Desea controlar esta sesión?",
"other_session_use_here_button": "Usar aquí",
"page_not_found_description": "La página que estás buscando no existe.",
"paste_modal_confirm_paste": "Confirmar Pegar",
"paste_modal_delay_between_keys": "Retraso entre teclas",
"paste_modal_delay_out_of_range": "El retraso debe estar entre {min} y {max}",
"paste_modal_failed_paste": "No se pudo pegar el texto: {error}",
"paste_modal_invalid_chars_intro": "Los siguientes caracteres no se pegarán:",
"paste_modal_paste_from_host": "Pegar desde el host",
"paste_modal_sending_using_layout": "Envío de texto mediante la distribución del teclado: {iso} - {name}",
"paste_text": "Pegar texto",
"paste_text_description": "Pegue el texto de su cliente al host remoto",
"peer_connection_closed": "Cerrado",
"peer_connection_closing": "Cierre",
"peer_connection_connected": "Conectado",
"peer_connection_connecting": "Conectando",
"peer_connection_disconnected": "Desconectado",
"peer_connection_error": "Error de conexión",
"peer_connection_failed": "La conexión falló",
"peer_connection_new": "Conectando",
"previous": "Anterior",
"register_device_error": "Se produjo un error {error} al registrar su dispositivo.",
"register_device_finish_button": "Finalizar configuración",
"register_device_name_description": "Ponle un nombre a tu dispositivo para que puedas identificarlo fácilmente más tarde. Puedes cambiarlo en cualquier momento.",
"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": "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}",
"rename_device_new_name_label": "Nuevo nombre del dispositivo",
"rename_device_new_name_placeholder": "Servidor multimedia Plex",
"rename_device_no_name": "Por favor especifique un nombre",
"retry": "Rever",
"saving": "Ahorro…",
"search_placeholder": "Buscar…",
"serial_console": "Consola serial",
"serial_console_baud_rate": "Tasa de Baud",
"serial_console_configure_description": "Configure los ajustes de su consola serie",
"serial_console_data_bits": "Bits de datos",
"serial_console_get_settings_error": "No se pudo obtener la configuración de la consola serial: {error}",
"serial_console_open_console": "Consola abierta",
"serial_console_parity": "Paridad",
"serial_console_parity_even": "Paridad uniforme",
"serial_console_parity_mark": "Paridad de marca",
"serial_console_parity_none": "Sin paridad",
"serial_console_parity_odd": "Paridad impar",
"serial_console_parity_space": "Paridad espacial",
"serial_console_set_settings_error": "No se pudieron establecer los ajustes de la consola serial en {settings} : {error}",
"serial_console_stop_bits": "Bits de parada",
"setting_remote_description": "Configuración de la descripción remota",
"setting_remote_session_description": "Establecer la descripción de la sesión remota...",
"setting_up_connection_to_device": "Configurando la conexión al dispositivo...",
"settings_access": "Acceso",
"settings_advanced": "Avanzado",
"settings_appearance": "Apariencia",
"settings_back_to_kvm": "Volver a KVM",
"settings_general": "General",
"settings_hardware": "Hardware",
"settings_keyboard": "Teclado",
"settings_keyboard_macros": "Macros del teclado",
"settings_mouse": "Ratón",
"settings_network": "Red",
"settings_video": "Video",
"something_went_wrong": "Algo salió mal. Inténtalo de nuevo más tarde o contacta con el servicio de asistencia.",
"step_counter_step": "Paso {step}",
"subnet_mask": "Máscara de subred",
"time_division_days": "días",
"time_division_hours": "horas",
"time_division_minutes": "minutos",
"time_division_months": "meses",
"time_division_seconds": "artículos de segunda clase",
"time_division_weeks": "semanas",
"time_division_years": "años",
"troubleshoot_connection": "Solucionar problemas de conexión",
"unknown_error": "Error desconocido",
"update_in_progress": "Actualización en progreso",
"updates_failed_check": "No se pudieron buscar actualizaciones: {error}",
"updates_failed_get_device_version": "No se pudo obtener la versión del dispositivo: {error}",
"updating_leave_device_on": "Por favor, no apagues tu dispositivo…",
"usb": "USB",
"usb_config_custom": "Costumbre",
"usb_config_default": "JetKVM predeterminado",
"usb_config_dell": "Teclado multimedia Dell Pro",
"usb_config_failed_load": "No se pudo cargar la configuración USB: {error}",
"usb_config_failed_set": "No se pudo establecer la configuración USB: {error}",
"usb_config_identifiers_description": "Identificadores de dispositivos USB expuestos al ordenador de destino",
"usb_config_identifiers_title": "Identificadores",
"usb_config_logitech": "Adaptador universal Logitech",
"usb_config_manufacturer_label": "Fabricante",
"usb_config_manufacturer_placeholder": "Introduzca el fabricante",
"usb_config_microsoft": "Teclado multimedia inalámbrico de Microsoft",
"usb_config_product_id_label": "Identificación del producto",
"usb_config_product_id_placeholder": "Ingrese el ID del producto",
"usb_config_product_name_label": "Nombre del producto",
"usb_config_product_name_placeholder": "Introduzca el nombre del producto",
"usb_config_restore_default": "Restaurar a valores predeterminados",
"usb_config_serial_number_label": "Número de serie",
"usb_config_serial_number_placeholder": "Introduzca el número de serie",
"usb_config_set_success": "Configuración USB establecida en {manufacturer} {product}",
"usb_config_update_identifiers": "Actualizar identificadores USB",
"usb_config_vendor_id_label": "Identificación del proveedor",
"usb_config_vendor_id_placeholder": "Ingrese el ID del proveedor",
"usb_device_classes_description": "Clases de dispositivos USB en el dispositivo compuesto",
"usb_device_classes_title": "Clases",
"usb_device_custom": "Costumbre",
"usb_device_description": "Dispositivos USB para emular en la computadora de destino",
"usb_device_enable_absolute_mouse_description": "Habilitar el puntero absoluto del ratón",
"usb_device_enable_absolute_mouse_title": "Habilitar el puntero absoluto del ratón",
"usb_device_enable_keyboard_description": "Habilitar el teclado",
"usb_device_enable_keyboard_title": "Habilitar el teclado",
"usb_device_enable_mass_storage_description": "A veces puede ser necesario desactivarlo para evitar problemas con ciertos dispositivos.",
"usb_device_enable_mass_storage_title": "Habilitar almacenamiento masivo USB",
"usb_device_enable_relative_mouse_description": "Habilitar el ratón relativo",
"usb_device_enable_relative_mouse_title": "Habilitar el ratón relativo",
"usb_device_failed_load": "No se pudieron cargar los dispositivos USB: {error}",
"usb_device_failed_set": "No se pudieron configurar los dispositivos USB: {error}",
"usb_device_keyboard_mouse_and_mass_storage": "Teclado, ratón y almacenamiento masivo",
"usb_device_keyboard_only": "Sólo teclado",
"usb_device_restore_default": "Restaurar a valores predeterminados",
"usb_device_title": "Dispositivo USB",
"usb_device_update_classes": "Actualizar clases USB",
"usb_device_updated": "Dispositivos USB actualizados",
"usb_state_connected": "Conectado",
"usb_state_connecting": "Conectando",
"usb_state_disconnected": "Desconectado",
"usb_state_low_power_mode": "Modo de bajo consumo",
"user_interface_language_description": "Seleccione el idioma que se utilizará en la interfaz de usuario de JetKVM",
"user_interface_language_title": "Lenguaje de interfaz",
"video_brightness_description": "Nivel de brillo ( {value} x)",
"video_brightness_title": "Brillo",
"video_contrast_description": "Nivel de contraste ( {value} x)",
"video_contrast_title": "Contraste",
"video_custom_edid_description": "El EDID detalla la compatibilidad de los modos de vídeo. La configuración predeterminada funciona en la mayoría de los casos, pero es posible que sea necesario ajustar la UEFI/BIOS específica.",
"video_custom_edid_title": "EDID personalizado",
"video_debugging_info_description": "Información de depuración para vídeo",
"video_debugging_info_title": "Información de depuración",
"video_description": "Configure los ajustes de pantalla y EDID para una compatibilidad óptima",
"video_edid_acer_b246wl": "Acer B246WL, 1920x1200",
"video_edid_asus_pa248qv": "ASUS PA248QV, 1920x1200",
"video_edid_custom": "Costumbre",
"video_edid_dell_d2721h": "DELL D2721H, 1920 x 1080",
"video_edid_dell_idrac": "EDID de DELL IDRAC, 1280 x 1024",
"video_edid_description": "Ajuste la configuración EDID para la pantalla",
"video_edid_file_label": "Archivo EDID",
"video_edid_jetkvm_default": "JetKVM predeterminado",
"video_edid_set_success": "EDID establecido correctamente en {edid}",
"video_edid_title": "EDID",
"video_enhancement_description": "Ajuste la configuración de color para que la salida de video sea más vibrante y colorida.",
"video_enhancement_title": "Mejora de vídeo",
"video_failed_get_debug_info": "No se pudo obtener la información de depuración: {error}",
"video_failed_get_edid": "No se pudo obtener el EDID: {error}",
"video_failed_set_edid": "No se pudo establecer EDID: {error}",
"video_failed_set_stream_quality": "No se pudo establecer la calidad de la transmisión: {error}",
"video_get_debugging_info": "Obtener información de depuración",
"video_overlay_autoplay_permissions_required": "Se requieren permisos de reproducción automática",
"video_overlay_conn_check_cables": "Revise todas las conexiones de cables para detectar cables sueltos o dañados.",
"video_overlay_conn_ensure_network": "Asegúrese de que su conexión de red sea estable y activa",
"video_overlay_conn_restart": "Intente reiniciar tanto el dispositivo como su computadora",
"video_overlay_conn_verify_power": "Verifique que el dispositivo esté encendido y conectado correctamente",
"video_overlay_connection_issue_title": "Problema de conexión detectado",
"video_overlay_enable_autoplay_settings": "Ajuste la configuración del navegador para habilitar la reproducción automática.",
"video_overlay_hdmi_error_title": "Se detectó un error en la señal HDMI.",
"video_overlay_hdmi_incompatible_resolution": "Configuraciones de resolución o frecuencia de actualización incompatibles",
"video_overlay_hdmi_loose_faulty": "Una conexión HDMI suelta o defectuosa",
"video_overlay_hdmi_source_issue": "Problemas con la salida HDMI del dispositivo fuente",
"video_overlay_learn_more": "Más información",
"video_overlay_loading_stream": "Cargando transmisión de video…",
"video_overlay_manually_start_stream": "Iniciar transmisión manualmente",
"video_overlay_no_hdmi_adapter_compat": "Si utiliza un adaptador, asegúrese de que sea compatible y funcione correctamente.",
"video_overlay_no_hdmi_ensure_cable": "Asegúrese de que el cable HDMI esté conectado de forma segura en ambos extremos",
"video_overlay_no_hdmi_ensure_power": "Asegúrese de que el dispositivo fuente esté encendido y emitiendo una señal",
"video_overlay_no_hdmi_signal": "No se detectó señal HDMI.",
"video_overlay_pointerlock_click_to_enable": "Haga clic en el vídeo para habilitar el control del mouse.",
"video_overlay_retrying_connection": "Reintentando conexión…",
"video_overlay_troubleshooting_guide": "Guía de solución de problemas",
"video_overlay_try_again": "Intentar otra vez",
"video_pointer_lock_disabled": "Bloqueo del puntero deshabilitado",
"video_pointer_lock_enabled": "Bloqueo del puntero habilitado: presione Escape para desbloquear",
"video_quality_high": "Alto",
"video_quality_low": "Bajo",
"video_quality_medium": "Medio",
"video_reset_to_default": "Restablecer a valores predeterminados",
"video_restore_to_default": "Restaurar a valores predeterminados",
"video_saturation_description": "Saturación de color ( {value} x)",
"video_saturation_title": "Saturación",
"video_set_custom_edid": "Establecer EDID personalizado",
"video_stream_quality_description": "Ajustar la calidad de la transmisión de vídeo",
"video_stream_quality_set": "Calidad de transmisión establecida en {quality}",
"video_stream_quality_title": "Calidad de la transmisión",
"video_title": "Video",
"view_details": "Ver detalles",
"virtual_keyboard_header": "Teclado virtual",
"wake_on_lan": "Activación en LAN",
"wake_on_lan_add_device_device_name": "Nombre del dispositivo",
"wake_on_lan_add_device_example_device_name": "Servidor multimedia Plex",
"wake_on_lan_add_device_mac_address": "Dirección MAC",
"wake_on_lan_add_device_save_device": "Guardar dispositivo",
"wake_on_lan_description": "Envíe un paquete mágico para activar un dispositivo remoto.",
"wake_on_lan_device_list_add_new_device": "Agregar nuevo dispositivo",
"wake_on_lan_device_list_delete_device": "Eliminar dispositivo",
"wake_on_lan_device_list_wake": "Despertar",
"wake_on_lan_empty_add_device_to_start": "Agregue un dispositivo para comenzar a usar Wake-on-LAN",
"wake_on_lan_empty_add_new_device": "Agregar nuevo dispositivo",
"wake_on_lan_empty_no_devices_added": "No hay dispositivos añadidos",
"wake_on_lan_failed_add_device": "No se pudo agregar el dispositivo",
"wake_on_lan_failed_send_magic": "No se pudo enviar el paquete mágico",
"wake_on_lan_invalid_mac": "Dirección MAC no válida",
"wake_on_lan_magic_sent_success": "Paquete mágico enviado con éxito",
"welcome_to_jetkvm": "Bienvenido a JetKVM",
"welcome_to_jetkvm_description": "Controla cualquier computadora de forma remota"
}

View File

@ -0,0 +1,895 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"access_adopt_kvm": "Adopter KVM vers le Cloud",
"access_adopted_message": "Votre appareil est adopté dans le Cloud",
"access_auth_mode_no_password": "Mode actuel : Aucun mot de passe",
"access_auth_mode_password": "Mode actuel : protégé par mot de passe",
"access_authentication_mode_title": "Mode d'authentification",
"access_certificate_label": "Certificat",
"access_change_password_button": "Changer le mot de passe",
"access_change_password_description": "Mettez à jour le mot de passe d'accès de votre appareil",
"access_change_password_title": "Changer le mot de passe",
"access_cloud_api_url_label": "URL de l'API Cloud",
"access_cloud_app_url_label": "URL de l'application cloud",
"access_cloud_provider_description": "Sélectionnez le fournisseur de cloud pour votre appareil",
"access_cloud_provider_title": "Fournisseur de cloud",
"access_cloud_security_title": "Sécurité du cloud",
"access_confirm_deregister": "Êtes-vous sûr de vouloir désenregistrer cet appareil ?",
"access_deregister": "Se désinscrire du Cloud",
"access_description": "Gérer le contrôle d'accès de l'appareil",
"access_disable_protection": "Désactiver la protection",
"access_enable_password": "Activer le mot de passe",
"access_failed_deregister": "Échec de la désinscription du périphérique : {error}",
"access_failed_update_cloud_url": "Échec de la mise à jour de l'URL du cloud : {error}",
"access_failed_update_tls": "Échec de la mise à jour des paramètres TLS : {error}",
"access_github_link": "GitHub",
"access_https_description": "Configurer un accès HTTPS sécurisé à votre appareil",
"access_https_mode_title": "Mode HTTPS",
"access_learn_security": "En savoir plus sur notre sécurité cloud",
"access_local_description": "Gérer le mode d'accès local à l'appareil",
"access_local_title": "Locale",
"access_no_device_id": "Aucun identifiant d'appareil disponible",
"access_private_key_description": "Pour des raisons de sécurité, il ne sera pas affiché après l'enregistrement.",
"access_private_key_label": "Clé privée",
"access_provider_custom": "Coutume",
"access_provider_jetkvm": "JetKVM Cloud",
"access_remote_description": "Gérer le mode d'accès à distance à l'appareil",
"access_security_encryption": "Chiffrement de bout en bout utilisant WebRTC (DTLS et SRTP)",
"access_security_oidc": "Authentification OIDC (OpenID Connect)",
"access_security_open_source": "Tous les composants cloud sont open source et disponibles sur GitHub.",
"access_security_streams": "Tous les flux sont cryptés en transit",
"access_security_zero_trust": "Modèle de sécurité Zero Trust",
"access_title": "Accéder",
"access_tls_certificate_description": "Collez votre certificat TLS ci-dessous. Pour les chaînes de certificats, incluez la chaîne complète (certificats feuille, intermédiaire et racine).",
"access_tls_certificate_title": "Certificat TLS",
"access_tls_custom": "Coutume",
"access_tls_disabled": "Désactivé",
"access_tls_self_signed": "Auto-signé",
"access_tls_updated": "Les paramètres TLS ont été mis à jour avec succès",
"access_update_tls_settings": "Mettre à jour les paramètres TLS",
"action_bar_connection_stats": "Statistiques de connexion",
"action_bar_extension": "Extension",
"action_bar_fullscreen": "Plein écran",
"action_bar_settings": "Paramètres",
"action_bar_virtual_keyboard": "Clavier virtuel",
"action_bar_virtual_media": "Médias virtuels",
"action_bar_wake_on_lan": "Réveil sur LAN",
"action_bar_web_terminal": "Terminal Web",
"advanced_description": "Accéder à des paramètres supplémentaires pour le dépannage et la personnalisation",
"advanced_dev_channel_description": "Recevez les premières mises à jour du canal de développement",
"advanced_dev_channel_title": "Mises à jour du canal de développement",
"advanced_developer_mode_description": "Activer les fonctionnalités avancées pour les développeurs",
"advanced_developer_mode_enabled_title": "Mode développeur activé",
"advanced_developer_mode_title": "Mode développeur",
"advanced_developer_mode_warning_advanced": "Réservé aux utilisateurs avancés. Non destiné à une utilisation en production.",
"advanced_developer_mode_warning_risks": "À utiliser uniquement si vous comprenez les risques",
"advanced_developer_mode_warning_security": "La sécurité est affaiblie lorsqu'elle est active",
"advanced_disable_usb_emulation": "Désactiver l'émulation USB",
"advanced_enable_usb_emulation": "Activer l'émulation USB",
"advanced_error_loopback_disable": "Échec de la désactivation du mode de bouclage uniquement : {error}",
"advanced_error_loopback_enable": "Échec de l'activation du mode de bouclage uniquement : {error}",
"advanced_error_reset_config": "Échec de la réinitialisation de la configuration : {error}",
"advanced_error_set_dev_channel": "Échec de la définition de l'état du canal de développement : {error}",
"advanced_error_set_dev_mode": "Échec de la définition du mode de développement : {error}",
"advanced_error_update_ssh_key": "Échec de la mise à jour de la clé SSH : {error}",
"advanced_error_usb_emulation_disable": "Échec de la désactivation de l'émulation USB : {error}",
"advanced_error_usb_emulation_enable": "Échec de l'activation de l'émulation USB : {error}",
"advanced_loopback_only_description": "Restreindre l'accès à l'interface Web à l'hôte local uniquement (127.0.0.1)",
"advanced_loopback_only_title": "Mode de bouclage uniquement",
"advanced_loopback_warning_before": "Avant d'activer cette fonctionnalité, assurez-vous d'avoir :",
"advanced_loopback_warning_cloud": "Accès au cloud activé et fonctionnel",
"advanced_loopback_warning_confirm": "Je comprends, j'active quand même",
"advanced_loopback_warning_description": "AVERTISSEMENT : cela restreindra l'accès à l'interface Web à localhost (127.0.0.1) uniquement.",
"advanced_loopback_warning_ssh": "Accès SSH configuré et testé",
"advanced_loopback_warning_title": "Activer le mode de bouclage uniquement ?",
"advanced_reset_config_button": "Réinitialiser la configuration",
"advanced_reset_config_description": "Réinitialiser la configuration par défaut. Cela vous déconnectera.",
"advanced_reset_config_title": "Réinitialiser la configuration",
"advanced_ssh_access_description": "Ajoutez votre clé publique SSH pour activer l'accès à distance sécurisé à l'appareil",
"advanced_ssh_access_title": "Accès SSH",
"advanced_ssh_default_user": "L'utilisateur SSH par défaut est",
"advanced_ssh_public_key_label": "Clé publique SSH",
"advanced_ssh_public_key_placeholder": "Entrez votre clé publique SSH",
"advanced_success_loopback_disabled": "Mode de bouclage désactivé. Redémarrez votre appareil pour appliquer le mode de bouclage.",
"advanced_success_loopback_enabled": "Mode de bouclage activé. Redémarrez votre appareil pour appliquer la fonction.",
"advanced_success_reset_config": "La configuration par défaut a été réinitialisée avec succès",
"advanced_success_update_ssh_key": "Clé SSH mise à jour avec succès",
"advanced_title": "Avancé",
"advanced_troubleshooting_mode_description": "Outils de diagnostic et contrôles supplémentaires à des fins de dépannage et de développement",
"advanced_troubleshooting_mode_title": "Mode de dépannage",
"advanced_update_ssh_key_button": "Mettre à jour la clé SSH",
"advanced_usb_emulation_description": "Contrôler l'état de l'émulation USB",
"advanced_usb_emulation_title": "Émulation USB",
"already_adopted_new_owner": "Si vous êtes le nouveau propriétaire, veuillez demander à l'ancien propriétaire de désenregistrer l'appareil de son compte dans le tableau de bord cloud. Si vous pensez qu'il s'agit d'une erreur, contactez notre équipe d'assistance pour obtenir de l'aide.",
"already_adopted_other_user": "Cet appareil est actuellement enregistré auprès d'un autre utilisateur dans notre tableau de bord cloud.",
"already_adopted_return_to_dashboard": "Retour au tableau de bord",
"already_adopted_title": "Appareil déjà enregistré",
"appearance_description": "Choisissez votre thème de couleur préféré",
"appearance_page_description": "Personnalisez l'apparence de votre interface JetKVM",
"appearance_theme": "Thème",
"appearance_theme_dark": "Sombre",
"appearance_theme_light": "Lumière",
"appearance_theme_system": "Système",
"appearance_title": "Apparence",
"attach": "Attacher",
"atx_power_control_get_state_error": "Échec de l'obtention de l'état d'alimentation ATX : {error}",
"atx_power_control_hdd_led": "Voyant du disque dur",
"atx_power_control_long_power_button": "Appui long",
"atx_power_control_power_button": "Pouvoir",
"atx_power_control_power_led": "LED d'alimentation",
"atx_power_control_reset_button": "Réinitialiser",
"atx_power_control_send_action_error": "Échec de l'envoi de l'action d'alimentation ATX {action} : {error}",
"atx_power_control_short_power_button": "Appui court",
"auth_authentication_mode": "Veuillez sélectionner un mode d'authentification",
"auth_authentication_mode_error": "Une erreur s'est produite lors de la définition du mode d'authentification",
"auth_authentication_mode_invalid": "Mode d'authentification non valide",
"auth_connect_to_cloud": "Connectez votre JetKVM au cloud",
"auth_connect_to_cloud_action": "Connectez-vous et connectez l'appareil",
"auth_connect_to_cloud_description": "Déverrouillez l'accès à distance et les fonctionnalités avancées de votre appareil",
"auth_header_cta_already_have_account": "Vous avez déjà un compte ?",
"auth_header_cta_dont_have_account": "Vous n'avez pas de compte ?",
"auth_header_cta_new_to_jetkvm": "Nouveau sur JetKVM ?",
"auth_login": "Connectez-vous à votre compte JetKVM",
"auth_login_action": "Se connecter",
"auth_login_description": "Connectez-vous pour accéder et gérer vos appareils en toute sécurité",
"auth_mode_local": "Méthode d'authentification locale",
"auth_mode_local_change_later": "Vous pouvez toujours modifier votre méthode d'authentification ultérieurement dans les paramètres.",
"auth_mode_local_description": "Sélectionnez la manière dont vous souhaitez sécuriser votre périphérique JetKVM localement.",
"auth_mode_local_no_password": "Pas de mot de passe",
"auth_mode_local_no_password_description": "Accès rapide sans authentification par mot de passe.",
"auth_mode_local_password": "Mot de passe",
"auth_mode_local_password_confirm_description": "Confirmez votre mot de passe",
"auth_mode_local_password_confirm_label": "Confirmez le mot de passe",
"auth_mode_local_password_description": "Sécurisez votre appareil avec un mot de passe pour une protection supplémentaire.",
"auth_mode_local_password_failed_set": "Échec de la définition du mot de passe : {error}",
"auth_mode_local_password_note": "Ce mot de passe sera utilisé pour sécuriser les données de votre appareil et les protéger contre tout accès non autorisé.",
"auth_mode_local_password_note_local": "Toutes les données restent sur votre appareil local.",
"auth_mode_local_password_set": "Définir un mot de passe",
"auth_mode_local_password_set_button": "Définir le mot de passe",
"auth_mode_local_password_set_description": "Créez un mot de passe fort pour sécuriser votre périphérique JetKVM localement.",
"auth_mode_local_password_set_label": "Entrez un mot de passe",
"auth_signup_connect_to_cloud_action": "Inscription et connexion de l'appareil",
"auth_signup_create_account": "Créez votre compte JetKVM",
"auth_signup_create_account_action": "Créer un compte",
"auth_signup_create_account_description": "Créez votre compte et commencez à gérer vos appareils en toute simplicité.",
"back": "Dos",
"back_to_devices": "Retour aux appareils",
"cancel": "Annuler",
"close": "Fermer",
"cloud_kvms": "KVM Cloud",
"cloud_kvms_description": "Gérez vos KVM cloud et connectez-vous à eux en toute sécurité.",
"cloud_kvms_no_devices": "Aucun appareil trouvé",
"cloud_kvms_no_devices_description": "Vous n'avez pas encore d'appareils avec JetKVM Cloud activé.",
"confirm": "Confirmer",
"connect_to_kvm": "Se connecter à KVM",
"connecting_to_device": "Connexion à l'appareil…",
"connection_established": "Connexion établie",
"connection_stats_badge_jitter": "Gigue",
"connection_stats_badge_jitter_buffer_avg_delay": "Délai moyen du tampon de gigue",
"connection_stats_connection": "Connexion",
"connection_stats_connection_description": "La connexion entre le client et le JetKVM.",
"connection_stats_frames_per_second": "Images par seconde",
"connection_stats_frames_per_second_description": "Nombre d'images vidéo entrantes affichées par seconde.",
"connection_stats_network_stability": "Stabilité du réseau",
"connection_stats_network_stability_description": "La stabilité du flux de paquets vidéo entrants sur le réseau.",
"connection_stats_packets_lost": "Paquets perdus",
"connection_stats_packets_lost_description": "Nombre de paquets vidéo RTP entrants perdus.",
"connection_stats_playback_delay": "Délai de lecture",
"connection_stats_playback_delay_description": "Retard ajouté par le tampon de gigue pour fluidifier la lecture lorsque les images arrivent de manière inégale.",
"connection_stats_round_trip_time": "Temps de trajet aller-retour",
"connection_stats_round_trip_time_description": "Temps de trajet aller-retour pour la paire de candidats ICE actifs entre pairs.",
"connection_stats_sidebar": "Statistiques de connexion",
"connection_stats_unit_frames_per_second": " fps",
"connection_stats_unit_milliseconds": " ms",
"connection_stats_unit_packets": " paquets",
"connection_stats_video": "Vidéo",
"connection_stats_video_description": "Le flux vidéo du JetKVM vers le client.",
"continue": "Continuer",
"creating_peer_connection": "Créer des liens entre pairs…",
"dc_power_control_current": "Actuel",
"dc_power_control_current_unit": "UN",
"dc_power_control_get_state_error": "Échec de l'obtention de l'état d'alimentation CC : {error}",
"dc_power_control_power": "Pouvoir",
"dc_power_control_power_off_button": "Éteindre",
"dc_power_control_power_off_state": "Éteindre",
"dc_power_control_power_on_button": "Mise sous tension",
"dc_power_control_power_on_state": "Mise sous tension",
"dc_power_control_power_unit": "W",
"dc_power_control_restore_last_state": "Dernier état",
"dc_power_control_restore_power_state": "Restaurer la perte de puissance",
"dc_power_control_set_power_state_error": "Échec de l'envoi de l'état d'alimentation CC à {enabled} : {error}",
"dc_power_control_set_restore_state_error": "Échec de l'envoi de l'état de restauration de l'alimentation CC à {state} : {error}",
"dc_power_control_voltage": "Tension",
"dc_power_control_voltage_unit": "V",
"delete": "Supprimer",
"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.",
"deregister_from_cloud": "Se désinscrire du Cloud",
"deregister_headline": "Désinscrivez {device} de votre compte cloud",
"detach": "Détacher",
"dhcp_empty_lease_description": "Nous n'avons pas encore reçu d'informations sur le bail DHCP de l'appareil.",
"dhcp_empty_lease_headline": "Aucune information sur le bail DHCP",
"dhcp_lease_boot_file": "Fichier de démarrage",
"dhcp_lease_boot_next_server": "Démarrer le serveur suivant",
"dhcp_lease_boot_server_name": "Nom du serveur de démarrage",
"dhcp_lease_broadcast": "Diffuser",
"dhcp_lease_domain": "Domaine",
"dhcp_lease_gateway": "Porte",
"dhcp_lease_header": "Informations sur le bail DHCP",
"dhcp_lease_hostname": "Nom d'hôte",
"dhcp_lease_lease_expires": "Le bail expire",
"dhcp_lease_maximum_transfer_unit": "MTU",
"dhcp_lease_renew": "Renouveler le bail DHCP",
"dhcp_lease_time_to_live": "TTL",
"dhcp_server": "Serveur DHCP",
"dns_servers": "Serveurs DNS",
"establishing_secure_connection": "Établissement dune connexion sécurisée…",
"experimental": "Expérimental",
"extension_popover_load_and_manage_extensions": "Chargez et gérez vos extensions",
"extension_popover_set_error_notification": "Échec de la définition de l'extension active : {error}",
"extension_popover_unload_extension": "Extension de déchargement",
"extension_serial_console": "Console série",
"extension_serial_console_description": "Accédez à votre extension de console série",
"extensions_atx_power_control": "Contrôle d'alimentation ATX",
"extensions_atx_power_control_description": "Contrôlez l'état d'alimentation de votre machine via le contrôle d'alimentation ATX.",
"extensions_dc_power_control": "Contrôle de l'alimentation CC",
"extensions_dc_power_control_description": "Contrôlez votre extension d'alimentation CC",
"extensions_popover_extensions": "Extensions",
"gathering_ice_candidates": "Rassemblement des candidats de l'ICE…",
"general_app_version": "Application : {version}",
"general_auto_update_description": "Mettre à jour automatiquement l'appareil vers la dernière version",
"general_auto_update_error": "Échec de la définition de la mise à jour automatique : {error}",
"general_auto_update_title": "Mise à jour automatique",
"general_check_for_updates": "Vérifier les mises à jour",
"general_page_description": "Configurer les paramètres de l'appareil et mettre à jour les préférences",
"general_reboot_description": "Voulez-vous procéder au redémarrage du système ?",
"general_reboot_device": "Redémarrer l'appareil",
"general_reboot_device_description": "Redémarrez le JetKVM",
"general_reboot_no_button": "Non",
"general_reboot_title": "Redémarrer JetKVM",
"general_reboot_yes_button": "Oui",
"general_system_version": "Système : {version}",
"general_title": "Général",
"general_update_app_update_title": "Mise à jour de l'application",
"general_update_application_type": "Application",
"general_update_available_description": "Une nouvelle mise à jour est disponible pour améliorer les performances du système et la compatibilité. Nous vous recommandons de la mettre à jour pour garantir le bon fonctionnement de votre système.",
"general_update_available_title": "Mise à jour disponible",
"general_update_background_button": "Mise à jour en arrière-plan",
"general_update_check_again_button": "Revérifier",
"general_update_checking_description": "Nous veillons à ce que votre appareil dispose des dernières fonctionnalités et améliorations.",
"general_update_checking_title": "Vérification des mises à jour…",
"general_update_completed_description": "Votre appareil a été mis à jour avec succès vers la dernière version. Profitez des nouvelles fonctionnalités et améliorations !",
"general_update_completed_title": "Mise à jour terminée avec succès",
"general_update_error_description": "Une erreur s'est produite lors de la mise à jour de votre appareil. Veuillez réessayer ultérieurement.",
"general_update_error_details": "Détails de l'erreur : {errorMessage}",
"general_update_error_title": "Erreur de mise à jour",
"general_update_later_button": "Fais-le plus tard",
"general_update_now_button": "Mettre à jour maintenant",
"general_update_rebooting": "Redémarrage pour terminer la mise à jour…",
"general_update_status_awaiting_reboot": "En attente de redémarrage",
"general_update_status_downloading": "Téléchargement de la mise à jour {update_type} …",
"general_update_status_fetching": "Récupération des informations de mise à jour…",
"general_update_status_installing": "Installation de la mise à jour {update_type} …",
"general_update_status_progress": "{part} progression",
"general_update_status_verifying": "Vérification de la mise à jour de {update_type} …",
"general_update_system_type": "Système",
"general_update_system_update_title": "Mise à jour du système Linux",
"general_update_up_to_date_description": "Votre système utilise la dernière version. Aucune mise à jour n'est actuellement disponible.",
"general_update_up_to_date_title": "Le système est à jour",
"general_update_updating_description": "Veuillez ne pas éteindre votre appareil. Ce processus peut prendre quelques minutes.",
"general_update_updating_title": "Mise à jour de votre appareil",
"getting_remote_session_description": "Obtention d' {attempt} description de session à distance",
"hardware_backlight_settings_error": "Échec de la définition des paramètres de rétroéclairage : {error}",
"hardware_backlight_settings_get_error": "Échec de l'obtention des paramètres de rétroéclairage : {error}",
"hardware_backlight_settings_success": "Les paramètres de rétroéclairage ont été mis à jour avec succès",
"hardware_dim_display_after_description": "Définir le délai d'attente avant de réduire la luminosité de l'écran",
"hardware_dim_display_after_title": "Affichage atténué après",
"hardware_display_brightness_description": "Régler la luminosité de l'écran",
"hardware_display_brightness_high": "Haut",
"hardware_display_brightness_low": "Faible",
"hardware_display_brightness_medium": "Moyen",
"hardware_display_brightness_off": "Désactivé",
"hardware_display_brightness_title": "Luminosité de l'écran",
"hardware_display_orientation_description": "Définir l'orientation de l'affichage",
"hardware_display_orientation_error": "Échec de la définition de l'orientation d'affichage : {error}",
"hardware_display_orientation_inverted": "Inversé",
"hardware_display_orientation_normal": "Normale",
"hardware_display_orientation_success": "L'orientation de l'affichage a été mise à jour avec succès",
"hardware_display_orientation_title": "Orientation de l'affichage",
"hardware_display_wake_up_note": "L'écran se réveille lorsque l'état de connexion change ou lorsqu'il est touché.",
"hardware_page_description": "Configurer les paramètres d'affichage et les options matérielles de votre périphérique JetKVM",
"hardware_power_saving_description": "Réduisez la consommation d'énergie lorsque vous ne l'utilisez pas",
"hardware_power_saving_disabled": "Mode d'économie d'énergie désactivé",
"hardware_power_saving_enabled": "Mode d'économie d'énergie activé",
"hardware_power_saving_failed_error": "Échec de la définition du mode d'économie d'énergie : {error}",
"hardware_power_saving_hdmi_sleep_description": "Désactiver la capture après 90 secondes d'inactivité",
"hardware_power_saving_hdmi_sleep_title": "Mode veille HDMI",
"hardware_power_saving_title": "Économie d'énergie",
"hardware_time_10_minutes": "10 minutes",
"hardware_time_1_hour": "1 heure",
"hardware_time_1_minute": "1 minute",
"hardware_time_30_minutes": "30 minutes",
"hardware_time_5_minutes": "5 minutes",
"hardware_time_never": "Jamais",
"hardware_title": "Matériel",
"hardware_turn_off_display_after_description": "Période d'inactivité avant que l'écran ne s'éteigne automatiquement",
"hardware_turn_off_display_after_title": "Désactiver l'affichage après",
"hide": "Cacher",
"ice_gathering_completed": "Rassemblement ICE terminé",
"info_caps_lock": "Verrouillage des majuscules",
"info_compose": "Composer",
"info_hdmi_state": "État HDMI :",
"info_hidrpc_state": "État HidRPC :",
"info_kana": "Kana",
"info_keys": "Clés:",
"info_last_move": "Dernier mouvement :",
"info_num_lock": "Verr Num",
"info_paste_enabled": "Activé",
"info_paste_mode": "Mode Coller :",
"info_pointer": "Aiguille:",
"info_relayed_by_cloudflare": "Relayé par Cloudflare",
"info_resolution": "Résolution:",
"info_scroll_lock": "Verrouillage du défilement",
"info_shift": "Changement",
"info_usb_state": "État USB :",
"info_video_size": "Taille de la vidéo :",
"input_disabled": "Entrée désactivée",
"invalid_password": "Mot de passe invalide",
"ip_address": "Adresse IP",
"ipv6_address_label": "Adresse",
"ipv6_gateway": "Porte",
"ipv6_information": "Informations IPv6",
"ipv6_link_local": "Lien local",
"ipv6_preferred_lifetime": "Durée de vie préférée",
"ipv6_valid_lifetime": "Valable à vie",
"jetkvm_description": "JetKVM combine un matériel puissant avec un logiciel intuitif pour offrir une expérience de contrôle à distance transparente.",
"jetkvm_device": "Périphérique JetKVM",
"jetkvm_logo": "Logo JetKVM",
"jetkvm_setup": "Configurez votre JetKVM",
"jiggler_cron_schedule_description": "Expression Cron pour la planification",
"jiggler_cron_schedule_label": "Planification Cron",
"jiggler_example_business_hours_early": "Heures d'ouverture 8-17",
"jiggler_example_business_hours_late": "Horaires d'ouverture 9-17",
"jiggler_examples_label": "Exemples",
"jiggler_inactivity_limit_description": "Temps d'inactivité avant le tremblement",
"jiggler_inactivity_limit_label": "Limite d'inactivité en secondes",
"jiggler_more_examples": "Plus d'exemples",
"jiggler_random_delay_description": "Pour éviter les modèles reconnaissables",
"jiggler_random_delay_label": "Délai aléatoire",
"jiggler_save_jiggler_config": "Enregistrer la configuration de Jiggler",
"jiggler_timezone_description": "Fuseau horaire pour la planification cron",
"jiggler_timezone_label": "Fuseau horaire",
"keyboard_description": "Configurer les paramètres du clavier pour votre appareil",
"keyboard_layout_description": "Disposition du clavier du système d'exploitation cible",
"keyboard_layout_error": "Échec de la définition de la disposition du clavier : {error}",
"keyboard_layout_long_description": "Le clavier virtuel, le collage de texte et les macros clavier envoient des frappes de touches individuelles au périphérique cible. La disposition du clavier détermine les codes de touches envoyés. Assurez-vous que la disposition du clavier dans JetKVM correspond aux paramètres du système d'exploitation.",
"keyboard_layout_success": "La disposition du clavier a été définie avec succès sur {layout}",
"keyboard_layout_title": "Disposition du clavier",
"keyboard_show_pressed_keys_description": "Afficher les touches actuellement enfoncées dans la barre d'état",
"keyboard_show_pressed_keys_title": "Afficher les touches enfoncées",
"keyboard_title": "Clavier",
"kvm_terminal": "Terminal KVM",
"last_online": "Dernière connexion {time}",
"learn_more": "Apprendre encore plus",
"load": "Charger",
"loading": "Chargement…",
"local_auth_change_local_device_password_description": "Saisissez votre mot de passe actuel et un nouveau mot de passe pour mettre à jour la protection de votre appareil local.",
"local_auth_change_local_device_password_title": "Modifier le mot de passe de l'appareil local",
"local_auth_confirm_new_password_label": "Confirmer le nouveau mot de passe",
"local_auth_create_confirm_password_placeholder": "Ré-entrez votre mot de passe",
"local_auth_create_description": "Créez un mot de passe pour protéger votre appareil contre tout accès local non autorisé.",
"local_auth_create_new_password_label": "Nouveau mot de passe",
"local_auth_create_new_password_placeholder": "Entrez un mot de passe fort",
"local_auth_create_not_now_button": "Pas maintenant",
"local_auth_create_secure_button": "Appareil sécurisé",
"local_auth_create_title": "Protection des périphériques locaux",
"local_auth_current_password_label": "Mot de passe actuel",
"local_auth_disable_local_device_protection_description": "Saisissez votre mot de passe actuel pour désactiver la protection de l'appareil local.",
"local_auth_disable_local_device_protection_title": "Désactiver la protection des périphériques locaux",
"local_auth_disable_protection_button": "Désactiver la protection",
"local_auth_enter_current_password_placeholder": "Entrez votre mot de passe actuel",
"local_auth_enter_new_password_placeholder": "Entrez un nouveau mot de passe fort",
"local_auth_error_changing_password": "Une erreur s'est produite lors du changement du mot de passe",
"local_auth_error_disabling_password": "Une erreur s'est produite lors de la désactivation du mot de passe",
"local_auth_error_enter_current_password": "Veuillez entrer votre mot de passe actuel",
"local_auth_error_enter_new_password": "Veuillez entrer un nouveau mot de passe",
"local_auth_error_enter_old_password": "Veuillez saisir votre ancien mot de passe",
"local_auth_error_enter_password": "Veuillez entrer un mot de passe",
"local_auth_error_passwords_not_match": "Les mots de passe ne correspondent pas",
"local_auth_error_setting_password": "Une erreur s'est produite lors de la définition du mot de passe",
"local_auth_new_password_label": "Nouveau mot de passe",
"local_auth_reenter_new_password_placeholder": "Saisissez à nouveau votre nouveau mot de passe",
"local_auth_success_password_disabled_description": "Vous avez désactivé avec succès la protection par mot de passe pour l'accès local. N'oubliez pas que votre appareil est désormais moins sécurisé.",
"local_auth_success_password_disabled_title": "Protection par mot de passe désactivée",
"local_auth_success_password_set_description": "Vous avez correctement configuré la protection locale de votre appareil. Votre appareil est désormais protégé contre tout accès local non autorisé.",
"local_auth_success_password_set_title": "Mot de passe défini avec succès",
"local_auth_success_password_updated_description": "Vous avez modifié avec succès le mot de passe de protection de votre appareil local. N'oubliez pas de le mémoriser pour y accéder ultérieurement.",
"local_auth_success_password_updated_title": "Mot de passe mis à jour avec succès",
"local_auth_update_password_button": "Mettre à jour le mot de passe",
"locale_auto": "Auto",
"locale_change_success": "La langue a été modifiée avec succès en {locale}",
"locale_da": "danois",
"locale_de": "Allemand",
"locale_en": "Anglais",
"locale_es": "Espagnol",
"locale_fr": "Français",
"locale_it": "italien",
"locale_nb": "Norvégien (bokmål)",
"locale_sv": "suédois",
"locale_zh": "中文 (简体)",
"log_in": "Se connecter",
"log_out": "Se déconnecter",
"logged_in_as": "Connecté en tant que",
"login_enter_password": "Entrez votre mot de passe",
"login_enter_password_description": "Entrez votre mot de passe pour accéder à votre JetKVM.",
"login_error": "Une erreur s'est produite lors de la connexion",
"login_forgot_password": "Mot de passe oublié?",
"login_password_label": "Mot de passe",
"login_welcome_back": "Bienvenue à JetKVM",
"macro_add_step": "Ajouter l'étape {maxed_out}",
"macro_at_least_one_step_keys_or_modifiers": "Au moins une étape doit avoir des clés ou des modificateurs",
"macro_at_least_one_step_required": "Au moins une étape est requise",
"macro_max_steps_error": "Vous ne pouvez ajouter qu'un maximum de {max} étapes par macro.",
"macro_max_steps_reached": "( {max} max)",
"macro_name_label": "Nom de la macro",
"macro_name_required": "Le nom est obligatoire",
"macro_name_too_long": "Le nom doit comporter moins de 50 caractères",
"macro_please_fix_validation_errors": "Veuillez corriger les erreurs de validation",
"macro_save": "Enregistrer la macro",
"macro_save_failed": "Une erreur s'est produite lors de l'enregistrement.",
"macro_save_failed_error": "Une erreur s'est produite lors de l'enregistrement: {error}.",
"macro_step_count": "{steps} / {max} étapes",
"macro_step_duration_description": "Il est temps dattendre avant dexécuter létape suivante.",
"macro_step_duration_label": "Durée de l'étape",
"macro_step_keys_description": "Nombre maximal de clés {max} par étape.",
"macro_step_keys_label": "Clés",
"macro_step_max_keys_reached": "Nombre maximal de clés atteint",
"macro_step_modifiers_description": "Quels modificateurs (Maj/Ctrl/Alt/Meta) sont enfoncés pendant cette étape.",
"macro_step_modifiers_label": "Modificateurs",
"macro_step_no_matching_keys_found": "Aucune clé correspondante trouvée",
"macro_step_search_for_key": "Rechercher la clé…",
"macro_steps_description": "Clés/modificateurs exécutés en séquence avec un délai entre chaque étape.",
"macro_steps_label": "Mesures",
"macros_add_description": "Créer une nouvelle macro de clavier",
"macros_add_new": "Ajouter une nouvelle macro",
"macros_add_new_macro": "Ajouter une nouvelle macro",
"macros_aria_add_new": "Ajouter une nouvelle macro",
"macros_aria_delete": "Supprimer la macro {name}",
"macros_aria_duplicate": "Macro dupliquée {name}",
"macros_aria_edit": "Modifier la macro {name}",
"macros_aria_move_down": "Déplacer {name} vers le bas",
"macros_aria_move_up": "Déplacer {name} vers le haut",
"macros_confirm_delete_description": "Voulez-vous vraiment supprimer « {name} » ? Cette action est irréversible.",
"macros_confirm_delete_title": "Supprimer la macro",
"macros_confirm_deleting": "Suppression…",
"macros_create_first_description": "Combinez les frappes en une seule action",
"macros_create_first_headline": "Créez votre première macro",
"macros_created_success": "Macro « {name} » créée avec succès",
"macros_delay_only": "Retard seulement",
"macros_delete_confirm": "Êtes-vous sûr de vouloir supprimer cette macro ? Cette action est irréversible.",
"macros_delete_macro": "Supprimer la macro",
"macros_deleted_success": "Macro « {name} » supprimée avec succès",
"macros_deleting": "Suppression",
"macros_duplicated_success": "Macro « {name} » dupliquée avec succès",
"macros_edit_button": "Modifier",
"macros_edit_description": "Modifiez votre macro de clavier",
"macros_edit_title": "Modifier la macro",
"macros_failed_create": "Échec de la création de la macro",
"macros_failed_create_error": "Échec de la création de la macro : {error}",
"macros_failed_delete": "Échec de la suppression de la macro",
"macros_failed_delete_error": "Échec de la suppression de la macro : {error}",
"macros_failed_duplicate": "Échec de la duplication de la macro",
"macros_failed_duplicate_error": "Échec de la duplication de la macro : {error}",
"macros_failed_reorder": "Échec de la réorganisation des macros",
"macros_failed_reorder_error": "Échec de la réorganisation des macros : {error}",
"macros_failed_update": "Échec de la mise à jour de la macro",
"macros_failed_update_error": "Échec de la mise à jour de la macro : {error}",
"macros_invalid_data": "Données de macro non valides",
"macros_loading": "Chargement des macros…",
"macros_max_reached": "Max atteint",
"macros_maximum_macros_reached": "Vous avez atteint le nombre maximal de macros {maximum} autorisées.",
"macros_no_macros_available": "Aucune macro disponible",
"macros_order_updated": "L'ordre des macros a été mis à jour avec succès",
"macros_title": "Macros de clavier",
"macros_updated_success": "Macro « {name} » mise à jour avec succès",
"metric_not_supported": "Métrique non prise en charge",
"metric_waiting_for_data": "En attente de données…",
"mount_add_file_to_get_started": "Ajoutez un fichier pour commencer",
"mount_add_new_media": "Ajouter un nouveau média",
"mount_available_storage": "Stockage disponible",
"mount_button_back_to_overview": "Retour à l'aperçu",
"mount_button_cancel_upload": "Annuler le téléchargement",
"mount_button_continue_upload": "Continuer le téléchargement",
"mount_button_mount_file": "Fichier de montage",
"mount_button_mount_url": "URL de montage",
"mount_button_select": "Sélectionner",
"mount_button_showing_results": "Affichage des résultats {from} à {to} sur {total}",
"mount_button_upload_new_image": "Télécharger une nouvelle image",
"mount_bytes_free": "{bytesFree} gratuit",
"mount_bytes_used": "{bytesUsed} utilisé",
"mount_calculating": "Calculateur…",
"mount_click_to_select_file": "Cliquez pour sélectionner un fichier",
"mount_click_to_select_incomplete": "Cliquez pour sélectionner « {name} »",
"mount_confirm_delete": "Êtes-vous sûr de vouloir supprimer {name} ?",
"mount_continue_uploading_with_name": "Continuer le téléchargement de « {name} »",
"mount_error_delete_file": "Erreur lors de la suppression du fichier : {error}",
"mount_error_description": "Une erreur s'est produite lors du montage du support. Veuillez réessayer.",
"mount_error_get_storage_space": "Erreur lors de l'obtention de l'espace de stockage : {error}",
"mount_error_list_storage": "Erreur lors de la liste des fichiers de stockage : {error}",
"mount_error_title": "Erreur de montage",
"mount_get_state_error": "Échec de l'obtention de l'état du support virtuel : {error}",
"mount_jetkvm_storage": "Support de stockage JetKVM",
"mount_jetkvm_storage_description": "Monter les fichiers précédemment téléchargés à partir du stockage JetKVM",
"mount_mode_cdrom": "CD/DVD",
"mount_mode_disk": "Disque",
"mount_mounted_as": "Monté comme",
"mount_mounted_from_storage": "Monté à partir du stockage JetKVM",
"mount_no_images_description": "Téléchargez une image pour démarrer le montage du support virtuel.",
"mount_no_images_title": "Aucune image disponible",
"mount_no_mounted_media": "Aucun support monté",
"mount_percentage_used": "{percentageUsed} % utilisé",
"mount_please_select_file": "Veuillez sélectionner le fichier « {name} » pour continuer le téléchargement.",
"mount_popular_images": "Images populaires",
"mount_streaming_from_url": "Diffusion à partir d'une URL",
"mount_supported_formats": "Formats pris en charge : ISO, IMG",
"mount_unmount": "Démonter",
"mount_unmount_error": "Échec du démontage de l'image : {error}",
"mount_upload_description": "Sélectionnez un fichier image à télécharger sur le stockage JetKVM",
"mount_upload_error": "Erreur de téléchargement : {error}",
"mount_upload_failed_datachannel": "Échec de la création du canal de données pour le téléchargement du fichier",
"mount_upload_failed_rtc": "Échec du téléchargement : {error}",
"mount_upload_successful": "Téléchargement réussi",
"mount_upload_title": "Télécharger une nouvelle image",
"mount_uploaded_has_been_uploaded": "{name} a été téléchargé",
"mount_uploading": "Téléchargement en cours…",
"mount_uploading_with_name": "Téléchargement de {name}",
"mount_url_description": "Monter des fichiers à partir de n'importe quelle adresse Web publique",
"mount_url_input_label": "URL de l'image",
"mount_url_mount": "Montage d'URL",
"mount_view_device_description": "Sélectionnez une image à monter à partir du stockage JetKVM",
"mount_view_device_title": "Montage à partir du stockage JetKVM",
"mount_view_url_description": "Entrez une URL vers le fichier image à monter",
"mount_view_url_title": "Monter à partir de l'URL",
"mount_virtual_media": "Médias virtuels",
"mount_virtual_media_description": "Monter une image pour démarrer ou installer un système d'exploitation.",
"mount_virtual_media_source": "Source de média virtuel",
"mount_virtual_media_source_description": "Choisissez comment vous souhaitez monter votre support virtuel",
"mouse_alt_finger": "Doigt touchant un écran",
"mouse_alt_mouse": "Icône de la souris",
"mouse_description": "Configurer le comportement du curseur et les paramètres d'interaction pour votre appareil",
"mouse_hide_cursor_description": "Masquer le curseur lors de l'envoi de mouvements de souris",
"mouse_hide_cursor_title": "Masquer le curseur",
"mouse_jiggler_config_updated": "Configuration de Jiggler mise à jour avec succès",
"mouse_jiggler_custom": "Coutume",
"mouse_jiggler_description": "Simuler le mouvement d'une souris d'ordinateur",
"mouse_jiggler_disabled": "Désactivé",
"mouse_jiggler_error_config": "Une erreur s'est produite lors de la configuration du jiggler",
"mouse_jiggler_failed_state": "Échec de la définition de l'état du jiggler : {error}",
"mouse_jiggler_frequent": "Fréquent - 30 s",
"mouse_jiggler_invalid_cron": "Expression cron non valide. Veuillez vérifier le format de votre planification (par exemple, « 0 * * * * * » pour chaque minute).",
"mouse_jiggler_light": "Lumière - 5m",
"mouse_jiggler_standard": "Norme - 1 m",
"mouse_jiggler_title": "Jiggler",
"mouse_mode_absolute": "Absolu",
"mouse_mode_absolute_description": "Le plus pratique",
"mouse_mode_relative": "Relatif",
"mouse_mode_relative_description": "Le plus compatible",
"mouse_modes_description": "Choisissez le mode de saisie de la souris",
"mouse_modes_title": "Modes",
"mouse_scroll_high": "Haut",
"mouse_scroll_low": "Faible",
"mouse_scroll_medium": "Moyen",
"mouse_scroll_off": "Désactivé",
"mouse_scroll_throttling_description": "Réduire la fréquence des événements de défilement",
"mouse_scroll_throttling_title": "Limitation du défilement",
"mouse_scroll_very_high": "Très élevé",
"mouse_title": "Souris",
"network_custom_domain": "Domaine personnalisé",
"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_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",
"network_dhcp_lease_renew_confirm_new_b": "vous devrez peut-être vous reconnecter en utilisant la nouvelle adresse",
"network_dhcp_lease_renew_failed": "Échec du renouvellement du bail : {error}",
"network_dhcp_lease_renew_success": "Renouvellement du bail DHCP",
"network_domain_custom": "Coutume",
"network_domain_description": "Suffixe de domaine réseau pour l'appareil",
"network_domain_dhcp_provided": "DHCP fourni",
"network_domain_local": ".locale",
"network_domain_title": "Domaine",
"network_hostname_description": "Identifiant de l'appareil sur le réseau. Vide pour la valeur par défaut du système.",
"network_hostname_title": "Nom d'hôte",
"network_http_proxy_description": "Serveur proxy pour les requêtes HTTP(S) sortantes de l'appareil. Vide pour aucun.",
"network_http_proxy_invalid": "URL de proxy HTTP non valide",
"network_http_proxy_title": "Proxy HTTP",
"network_ipv4_address": "Adresse IPv4",
"network_ipv4_dns": "DNS IPv4",
"network_ipv4_gateway": "Passerelle IPv4",
"network_ipv4_invalid": "Adresse IPv4 non valide",
"network_ipv4_invalid_cidr": "Notation CIDR non valide pour l'adresse IPv4",
"network_ipv4_mode_description": "Configurer le mode IPv4",
"network_ipv4_mode_dhcp": "DHCP",
"network_ipv4_mode_static": "Statique",
"network_ipv4_mode_title": "Mode IPv4",
"network_ipv4_netmask": "Masque de réseau IPv4",
"network_ipv6_addresses_header": "Adresses IPv6",
"network_ipv6_cidr_suggestion": "Veuillez utiliser la notation CIDR (par exemple, 2001:db8::1/64)",
"network_ipv6_dns": "DNS IPv6",
"network_ipv6_flag_dad_failed": "Papa a échoué",
"network_ipv6_flag_deprecated": "Obsolète",
"network_ipv6_gateway": "Passerelle IPv6",
"network_ipv6_information": "Informations IPv6",
"network_ipv6_invalid": "Adresse IPv6 non valide",
"network_ipv6_mode_description": "Configurer le mode IPv6",
"network_ipv6_mode_dhcpv6": "DHCPv6",
"network_ipv6_mode_disabled": "Désactivé",
"network_ipv6_mode_link_local": "Lien local uniquement",
"network_ipv6_mode_slaac": "SLAAC",
"network_ipv6_mode_slaac_dhcpv6": "SLAAC + DHCPv6",
"network_ipv6_mode_static": "Statique",
"network_ipv6_mode_title": "Mode IPv6",
"network_ipv6_prefix": "Préfixe IP",
"network_ipv6_prefix_invalid": "Le préfixe doit être compris entre 0 et 128",
"network_ll_dp_all": "Tous",
"network_ll_dp_basic": "Basique",
"network_ll_dp_description": "Contrôler les TLV qui seront envoyés via le protocole Link Layer Discovery",
"network_ll_dp_disabled": "Désactivé",
"network_ll_dp_title": "LLDP",
"network_mac_address_copy_error": "Échec de la copie de l'adresse MAC",
"network_mac_address_copy_success": "Adresse MAC { mac } copiée dans le presse-papiers",
"network_mac_address_description": "Identifiant matériel de l'interface réseau",
"network_mac_address_title": "Adresse MAC",
"network_mdns_auto": "Auto",
"network_mdns_description": "Contrôler le mode opérationnel mDNS (DNS multicast)",
"network_mdns_disabled": "Désactivé",
"network_mdns_ipv4_only": "IPv4 uniquement",
"network_mdns_ipv6_only": "IPv6 uniquement",
"network_mdns_title": "mDNS",
"network_no_information_description": "Aucune configuration réseau disponible",
"network_no_information_headline": "Informations sur le réseau",
"network_pending_dhcp_mode_change_description": "Enregistrer les paramètres pour activer le mode DHCP et afficher les informations de bail",
"network_pending_dhcp_mode_change_headline": "Changement de mode DHCP IPv4 en attente",
"network_save_settings": "Enregistrer les paramètres",
"network_save_settings_apply_title": "Appliquer les paramètres réseau",
"network_save_settings_confirm": "Appliquer les modifications",
"network_save_settings_confirm_description": "Les paramètres réseau suivants seront appliqués. Ces modifications peuvent nécessiter un redémarrage et provoquer une brève déconnexion.",
"network_save_settings_confirm_heading": "Modifications de configuration",
"network_save_settings_failed": "Échec de l'enregistrement des paramètres réseau : {error}",
"network_save_settings_success": "Paramètres réseau enregistrés",
"network_settings_add_dns": "Ajouter un serveur DNS",
"network_settings_load_error": "Échec du chargement des paramètres réseau : {error}",
"network_static_ipv4_header": "Configuration IPv4 statique",
"network_static_ipv6_header": "Configuration IPv6 statique",
"network_time_sync_description": "Configurer les paramètres de synchronisation de l'heure",
"network_time_sync_http_only": "HTTP uniquement",
"network_time_sync_ntp_and_http": "NTP et HTTP",
"network_time_sync_ntp_only": "NTP uniquement",
"network_time_sync_title": "Synchronisation horaire",
"network_title": "Réseau",
"never_seen_online": "Jamais vu en ligne",
"next": "Suivant",
"no_results_found": "Aucun résultat trouvé",
"not_applicable": "N / A",
"not_available": "N / A",
"not_found": "Non trouvé",
"ntp_servers": "Serveurs NTP",
"oh_no": "Oh non!",
"online": "En ligne",
"other_session_detected": "Une autre session active détectée",
"other_session_take_over": " Une seule session active est prise en charge à la fois. Souhaitez-vous prendre le contrôle de cette session ?",
"other_session_use_here_button": "Utiliser ici",
"page_not_found_description": "La page que vous recherchez n'existe pas.",
"paste_modal_confirm_paste": "Confirmer le collage",
"paste_modal_delay_between_keys": "Délai entre les touches",
"paste_modal_delay_out_of_range": "Le délai doit être compris entre {min} et {max}",
"paste_modal_failed_paste": "Échec du collage du texte : {error}",
"paste_modal_invalid_chars_intro": "Les caractères suivants ne seront pas collés :",
"paste_modal_paste_from_host": "Coller depuis l'hôte",
"paste_modal_sending_using_layout": "Envoi de texte à l'aide de la disposition du clavier : {iso} - {name}",
"paste_text": "Coller du texte",
"paste_text_description": "Collez le texte de votre client sur l'hôte distant",
"peer_connection_closed": "Fermé",
"peer_connection_closing": "Clôture",
"peer_connection_connected": "Connecté",
"peer_connection_connecting": "De liaison",
"peer_connection_disconnected": "Déconnecté",
"peer_connection_error": "Erreur de connexion",
"peer_connection_failed": "La connexion a échoué",
"peer_connection_new": "De liaison",
"previous": "Précédent",
"register_device_error": "Une erreur {error} s'est produite lors de l'enregistrement de votre appareil.",
"register_device_finish_button": "Terminer la configuration",
"register_device_name_description": "Nommez votre appareil pour pouvoir l'identifier facilement plus tard. Vous pouvez modifier ce nom à tout moment.",
"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": "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}",
"rename_device_new_name_label": "Nouveau nom de l'appareil",
"rename_device_new_name_placeholder": "Serveur multimédia Plex",
"rename_device_no_name": "Veuillez spécifier un nom",
"retry": "Réessayer",
"saving": "Économie…",
"search_placeholder": "Rechercher…",
"serial_console": "Console série",
"serial_console_baud_rate": "Débit en bauds",
"serial_console_configure_description": "Configurez les paramètres de votre console série",
"serial_console_data_bits": "Bits de données",
"serial_console_get_settings_error": "Échec de l'obtention des paramètres de la console série : {error}",
"serial_console_open_console": "Ouvrir la console",
"serial_console_parity": "Parité",
"serial_console_parity_even": "Parité égale",
"serial_console_parity_mark": "Marquer la parité",
"serial_console_parity_none": "Pas de parité",
"serial_console_parity_odd": "Parité impaire",
"serial_console_parity_space": "Parité spatiale",
"serial_console_set_settings_error": "Échec de la définition des paramètres de la console série sur {settings} : {error}",
"serial_console_stop_bits": "Bits d'arrêt",
"setting_remote_description": "Description de la télécommande",
"setting_remote_session_description": "Définition de la description de la session à distance...",
"setting_up_connection_to_device": "Configuration de la connexion à l'appareil...",
"settings_access": "Accéder",
"settings_advanced": "Avancé",
"settings_appearance": "Apparence",
"settings_back_to_kvm": "Retour à KVM",
"settings_general": "Général",
"settings_hardware": "Matériel",
"settings_keyboard": "Clavier",
"settings_keyboard_macros": "Macros de clavier",
"settings_mouse": "Souris",
"settings_network": "Réseau",
"settings_video": "Vidéo",
"something_went_wrong": "Une erreur s'est produite. Veuillez réessayer ultérieurement ou contacter le support.",
"step_counter_step": "Étape {step}",
"subnet_mask": "Masque de sous-réseau",
"time_division_days": "jours",
"time_division_hours": "heures",
"time_division_minutes": "minutes",
"time_division_months": "mois",
"time_division_seconds": "secondes",
"time_division_weeks": "semaines",
"time_division_years": "années",
"troubleshoot_connection": "Dépannage de connexion",
"unknown_error": "Erreur inconnue",
"update_in_progress": "Mise à jour en cours",
"updates_failed_check": "Échec de la vérification des mises à jour : {error}",
"updates_failed_get_device_version": "Échec de l'obtention de la version de l'appareil : {error}",
"updating_leave_device_on": "S'il vous plaît, n'éteignez pas votre appareil…",
"usb": "USB",
"usb_config_custom": "Coutume",
"usb_config_default": "JetKVM par défaut",
"usb_config_dell": "Clavier Dell Multimedia Pro",
"usb_config_failed_load": "Échec du chargement de la configuration USB : {error}",
"usb_config_failed_set": "Échec de la définition de la configuration USB : {error}",
"usb_config_identifiers_description": "Identifiants de périphériques USB exposés à l'ordinateur cible",
"usb_config_identifiers_title": "Identifiants",
"usb_config_logitech": "Adaptateur universel Logitech",
"usb_config_manufacturer_label": "Fabricant",
"usb_config_manufacturer_placeholder": "Entrez le fabricant",
"usb_config_microsoft": "Clavier multimédia sans fil Microsoft",
"usb_config_product_id_label": "ID du produit",
"usb_config_product_id_placeholder": "Entrez l'ID du produit",
"usb_config_product_name_label": "Nom du produit",
"usb_config_product_name_placeholder": "Entrez le nom du produit",
"usb_config_restore_default": "Restaurer les paramètres par défaut",
"usb_config_serial_number_label": "Numéro de série",
"usb_config_serial_number_placeholder": "Entrez le numéro de série",
"usb_config_set_success": "Configuration USB définie sur {manufacturer} {product}",
"usb_config_update_identifiers": "Mettre à jour les identifiants USB",
"usb_config_vendor_id_label": "ID du fournisseur",
"usb_config_vendor_id_placeholder": "Entrez l'ID du fournisseur",
"usb_device_classes_description": "Classes de périphériques USB dans le périphérique composite",
"usb_device_classes_title": "Cours",
"usb_device_custom": "Coutume",
"usb_device_description": "Périphériques USB à émuler sur l'ordinateur cible",
"usb_device_enable_absolute_mouse_description": "Activer la souris absolue (pointeur)",
"usb_device_enable_absolute_mouse_title": "Activer la souris absolue (pointeur)",
"usb_device_enable_keyboard_description": "Activer le clavier",
"usb_device_enable_keyboard_title": "Activer le clavier",
"usb_device_enable_mass_storage_description": "Parfois, il peut être nécessaire de le désactiver pour éviter des problèmes avec certains appareils",
"usb_device_enable_mass_storage_title": "Activer le stockage de masse USB",
"usb_device_enable_relative_mouse_description": "Activer la souris relative",
"usb_device_enable_relative_mouse_title": "Activer la souris relative",
"usb_device_failed_load": "Échec du chargement des périphériques USB : {error}",
"usb_device_failed_set": "Échec de la configuration des périphériques USB : {error}",
"usb_device_keyboard_mouse_and_mass_storage": "Clavier, souris et stockage de masse",
"usb_device_keyboard_only": "Clavier uniquement",
"usb_device_restore_default": "Restaurer les paramètres par défaut",
"usb_device_title": "périphérique USB",
"usb_device_update_classes": "Mettre à jour les classes USB",
"usb_device_updated": "Périphériques USB mis à jour",
"usb_state_connected": "Connecté",
"usb_state_connecting": "De liaison",
"usb_state_disconnected": "Déconnecté",
"usb_state_low_power_mode": "Mode basse consommation",
"user_interface_language_description": "Sélectionnez la langue à utiliser dans l'interface utilisateur de JetKVM",
"user_interface_language_title": "Langue de l'interface",
"video_brightness_description": "Niveau de luminosité ( {value} x)",
"video_brightness_title": "Luminosité",
"video_contrast_description": "Niveau de contraste ( {value} x)",
"video_contrast_title": "Contraste",
"video_custom_edid_description": "Compatibilité du mode vidéo avec les détails EDID. Les paramètres par défaut fonctionnent dans la plupart des cas, mais des ajustements spécifiques à l'UEFI/BIOS peuvent être nécessaires.",
"video_custom_edid_title": "EDID personnalisé",
"video_debugging_info_description": "Informations de débogage pour la vidéo",
"video_debugging_info_title": "Informations de débogage",
"video_description": "Configurer les paramètres d'affichage et l'EDID pour une compatibilité optimale",
"video_edid_acer_b246wl": "Acer B246WL, 1920x1200",
"video_edid_asus_pa248qv": "ASUS PA248QV, 1920x1200",
"video_edid_custom": "Coutume",
"video_edid_dell_d2721h": "DELL D2721H, 1920x1080",
"video_edid_dell_idrac": "DELL IDRAC EDID, 1280x1024",
"video_edid_description": "Ajuster les paramètres EDID pour l'affichage",
"video_edid_file_label": "Fichier EDID",
"video_edid_jetkvm_default": "JetKVM par défaut",
"video_edid_set_success": "EDID défini avec succès sur {edid}",
"video_edid_title": "EDID",
"video_enhancement_description": "Ajustez les paramètres de couleur pour rendre la sortie vidéo plus dynamique et colorée",
"video_enhancement_title": "Amélioration vidéo",
"video_failed_get_debug_info": "Échec de l'obtention des informations de débogage : {error}",
"video_failed_get_edid": "Échec de l'obtention de l'EDID : {error}",
"video_failed_set_edid": "Échec de la définition de l'EDID : {error}",
"video_failed_set_stream_quality": "Échec de la définition de la qualité du flux : {error}",
"video_get_debugging_info": "Obtenir des informations de débogage",
"video_overlay_autoplay_permissions_required": "Autorisations de lecture automatique requises",
"video_overlay_conn_check_cables": "Vérifiez toutes les connexions de câbles pour détecter tout fil desserré ou endommagé",
"video_overlay_conn_ensure_network": "Assurez-vous que votre connexion réseau est stable et active",
"video_overlay_conn_restart": "Essayez de redémarrer l'appareil et votre ordinateur",
"video_overlay_conn_verify_power": "Vérifiez que l'appareil est sous tension et correctement connecté",
"video_overlay_connection_issue_title": "Problème de connexion détecté",
"video_overlay_enable_autoplay_settings": "Veuillez ajuster les paramètres du navigateur pour activer la lecture automatique",
"video_overlay_hdmi_error_title": "Erreur de signal HDMI détectée.",
"video_overlay_hdmi_incompatible_resolution": "Paramètres de résolution ou de taux de rafraîchissement incompatibles",
"video_overlay_hdmi_loose_faulty": "Une connexion HDMI lâche ou défectueuse",
"video_overlay_hdmi_source_issue": "Problèmes avec la sortie HDMI de l'appareil source",
"video_overlay_learn_more": "Apprendre encore plus",
"video_overlay_loading_stream": "Chargement du flux vidéo…",
"video_overlay_manually_start_stream": "Démarrer le flux manuellement",
"video_overlay_no_hdmi_adapter_compat": "Si vous utilisez un adaptateur, assurez-vous qu'il est compatible et qu'il fonctionne correctement",
"video_overlay_no_hdmi_ensure_cable": "Assurez-vous que le câble HDMI est bien connecté aux deux extrémités",
"video_overlay_no_hdmi_ensure_power": "Assurez-vous que l'appareil source est sous tension et émet un signal",
"video_overlay_no_hdmi_signal": "Aucun signal HDMI détecté.",
"video_overlay_pointerlock_click_to_enable": "Cliquez sur la vidéo pour activer le contrôle de la souris",
"video_overlay_retrying_connection": "Nouvelle tentative de connexion…",
"video_overlay_troubleshooting_guide": "Guide de dépannage",
"video_overlay_try_again": "Essayer à nouveau",
"video_pointer_lock_disabled": "Verrouillage du pointeur désactivé",
"video_pointer_lock_enabled": "Verrouillage du pointeur activé — appuyez sur Échap pour déverrouiller",
"video_quality_high": "Haut",
"video_quality_low": "Faible",
"video_quality_medium": "Moyen",
"video_reset_to_default": "Réinitialiser aux paramètres par défaut",
"video_restore_to_default": "Restaurer les paramètres par défaut",
"video_saturation_description": "Saturation des couleurs ( {value} x)",
"video_saturation_title": "Saturation",
"video_set_custom_edid": "Définir un EDID personnalisé",
"video_stream_quality_description": "Ajuster la qualité du flux vidéo",
"video_stream_quality_set": "Qualité du flux définie sur {quality}",
"video_stream_quality_title": "Qualité du flux",
"video_title": "Vidéo",
"view_details": "Voir les détails",
"virtual_keyboard_header": "Clavier virtuel",
"wake_on_lan": "Wake On LAN",
"wake_on_lan_add_device_device_name": "Nom de l'appareil",
"wake_on_lan_add_device_example_device_name": "Serveur multimédia Plex",
"wake_on_lan_add_device_mac_address": "Adresse MAC",
"wake_on_lan_add_device_save_device": "Enregistrer l'appareil",
"wake_on_lan_description": "Envoyez un paquet magique pour réveiller un appareil distant.",
"wake_on_lan_device_list_add_new_device": "Ajouter un nouvel appareil",
"wake_on_lan_device_list_delete_device": "Supprimer l'appareil",
"wake_on_lan_device_list_wake": "Se réveiller",
"wake_on_lan_empty_add_device_to_start": "Ajoutez un appareil pour commencer à utiliser Wake-on-LAN",
"wake_on_lan_empty_add_new_device": "Ajouter un nouvel appareil",
"wake_on_lan_empty_no_devices_added": "Aucun appareil ajouté",
"wake_on_lan_failed_add_device": "Échec de l'ajout de l'appareil",
"wake_on_lan_failed_send_magic": "Échec de l'envoi du paquet magique",
"wake_on_lan_invalid_mac": "Adresse MAC invalide",
"wake_on_lan_magic_sent_success": "Paquet magique envoyé avec succès",
"welcome_to_jetkvm": "Bienvenue chez JetKVM",
"welcome_to_jetkvm_description": "Contrôlez n'importe quel ordinateur à distance"
}

View File

@ -0,0 +1,895 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"access_adopt_kvm": "Adotta KVM nel cloud",
"access_adopted_message": "Il tuo dispositivo è adottato nel Cloud",
"access_auth_mode_no_password": "Modalità corrente: Nessuna password",
"access_auth_mode_password": "Modalità corrente: protetta da password",
"access_authentication_mode_title": "Modalità di autenticazione",
"access_certificate_label": "Certificato",
"access_change_password_button": "Cambiare la password",
"access_change_password_description": "Aggiorna la password di accesso al tuo dispositivo",
"access_change_password_title": "Cambiare la password",
"access_cloud_api_url_label": "URL dell'API cloud",
"access_cloud_app_url_label": "URL dell'applicazione cloud",
"access_cloud_provider_description": "Seleziona il provider cloud per il tuo dispositivo",
"access_cloud_provider_title": "Fornitore di servizi cloud",
"access_cloud_security_title": "Sicurezza del cloud",
"access_confirm_deregister": "Sei sicuro di voler annullare la registrazione di questo dispositivo?",
"access_deregister": "Annulla la registrazione dal cloud",
"access_description": "Gestire il controllo degli accessi del dispositivo",
"access_disable_protection": "Disattiva protezione",
"access_enable_password": "Abilita password",
"access_failed_deregister": "Impossibile annullare la registrazione del dispositivo: {error}",
"access_failed_update_cloud_url": "Impossibile aggiornare l'URL del cloud: {error}",
"access_failed_update_tls": "Impossibile aggiornare le impostazioni TLS: {error}",
"access_github_link": "GitHub",
"access_https_description": "Configura l'accesso HTTPS sicuro al tuo dispositivo",
"access_https_mode_title": "Modalità HTTPS",
"access_learn_security": "Scopri di più sulla nostra sicurezza cloud",
"access_local_description": "Gestire la modalità di accesso locale al dispositivo",
"access_local_title": "Locale",
"access_no_device_id": "Nessun ID dispositivo disponibile",
"access_private_key_description": "Per motivi di sicurezza, non verrà visualizzato dopo il salvataggio.",
"access_private_key_label": "Chiave privata",
"access_provider_custom": "Costume",
"access_provider_jetkvm": "JetKVM Cloud",
"access_remote_description": "Gestire la modalità di accesso remoto al dispositivo",
"access_security_encryption": "Crittografia end-to-end tramite WebRTC (DTLS e SRTP)",
"access_security_oidc": "Autenticazione OIDC (OpenID Connect)",
"access_security_open_source": "Tutti i componenti cloud sono open source e disponibili su GitHub.",
"access_security_streams": "Tutti i flussi crittografati in transito",
"access_security_zero_trust": "Modello di sicurezza Zero Trust",
"access_title": "Accesso",
"access_tls_certificate_description": "Incolla il tuo certificato TLS qui sotto. Per le catene di certificati, includi l'intera catena (certificati foglia, intermedi e radice).",
"access_tls_certificate_title": "Certificato TLS",
"access_tls_custom": "Costume",
"access_tls_disabled": "Disabili",
"access_tls_self_signed": "Autofirmato",
"access_tls_updated": "Impostazioni TLS aggiornate correttamente",
"access_update_tls_settings": "Aggiorna le impostazioni TLS",
"action_bar_connection_stats": "Statistiche di connessione",
"action_bar_extension": "Estensione",
"action_bar_fullscreen": "A schermo intero",
"action_bar_settings": "Impostazioni",
"action_bar_virtual_keyboard": "Tastiera virtuale",
"action_bar_virtual_media": "Media virtuali",
"action_bar_wake_on_lan": "Wake on LAN",
"action_bar_web_terminal": "Terminale Web",
"advanced_description": "Accedi a impostazioni aggiuntive per la risoluzione dei problemi e la personalizzazione",
"advanced_dev_channel_description": "Ricevi aggiornamenti in anteprima dal canale di sviluppo",
"advanced_dev_channel_title": "Aggiornamenti del canale Dev",
"advanced_developer_mode_description": "Abilita funzionalità avanzate per gli sviluppatori",
"advanced_developer_mode_enabled_title": "Modalità sviluppatore abilitata",
"advanced_developer_mode_title": "Modalità sviluppatore",
"advanced_developer_mode_warning_advanced": "Solo per utenti avanzati. Non adatto all'uso in produzione.",
"advanced_developer_mode_warning_risks": "Utilizzare solo se si comprendono i rischi",
"advanced_developer_mode_warning_security": "La sicurezza è indebolita mentre è attiva",
"advanced_disable_usb_emulation": "Disabilita l'emulazione USB",
"advanced_enable_usb_emulation": "Abilita emulazione USB",
"advanced_error_loopback_disable": "Impossibile disabilitare la modalità solo loopback: {error}",
"advanced_error_loopback_enable": "Impossibile abilitare la modalità solo loopback: {error}",
"advanced_error_reset_config": "Impossibile reimpostare la configurazione: {error}",
"advanced_error_set_dev_channel": "Impossibile impostare lo stato del canale di sviluppo: {error}",
"advanced_error_set_dev_mode": "Impossibile impostare la modalità di sviluppo: {error}",
"advanced_error_update_ssh_key": "Impossibile aggiornare la chiave SSH: {error}",
"advanced_error_usb_emulation_disable": "Impossibile disabilitare l'emulazione USB: {error}",
"advanced_error_usb_emulation_enable": "Impossibile abilitare l'emulazione USB: {error}",
"advanced_loopback_only_description": "Limita l'accesso all'interfaccia web solo a localhost (127.0.0.1)",
"advanced_loopback_only_title": "Modalità solo loopback",
"advanced_loopback_warning_before": "Prima di abilitare questa funzione, assicurati di avere:",
"advanced_loopback_warning_cloud": "Accesso al cloud abilitato e funzionante",
"advanced_loopback_warning_confirm": "Capisco, abilita comunque",
"advanced_loopback_warning_description": "ATTENZIONE: questo limiterà l'accesso all'interfaccia web solo a localhost (127.0.0.1).",
"advanced_loopback_warning_ssh": "Accesso SSH configurato e testato",
"advanced_loopback_warning_title": "Abilitare la modalità solo loopback?",
"advanced_reset_config_button": "Ripristina configurazione",
"advanced_reset_config_description": "Ripristina la configurazione predefinita. Questo ti disconnetterà.",
"advanced_reset_config_title": "Ripristina configurazione",
"advanced_ssh_access_description": "Aggiungi la tua chiave pubblica SSH per abilitare l'accesso remoto sicuro al dispositivo",
"advanced_ssh_access_title": "Accesso SSH",
"advanced_ssh_default_user": "L'utente SSH predefinito è",
"advanced_ssh_public_key_label": "Chiave pubblica SSH",
"advanced_ssh_public_key_placeholder": "Inserisci la tua chiave pubblica SSH",
"advanced_success_loopback_disabled": "Modalità loopback-only disattivata. Riavvia il dispositivo per applicare la modifica.",
"advanced_success_loopback_enabled": "Modalità loopback abilitata. Riavvia il dispositivo per applicare la modifica.",
"advanced_success_reset_config": "Configurazione ripristinata ai valori predefiniti con successo",
"advanced_success_update_ssh_key": "Chiave SSH aggiornata correttamente",
"advanced_title": "Avanzato",
"advanced_troubleshooting_mode_description": "Strumenti diagnostici e controlli aggiuntivi per la risoluzione dei problemi e lo sviluppo",
"advanced_troubleshooting_mode_title": "Modalità di risoluzione dei problemi",
"advanced_update_ssh_key_button": "Aggiorna la chiave SSH",
"advanced_usb_emulation_description": "Controlla lo stato di emulazione USB",
"advanced_usb_emulation_title": "Emulazione USB",
"already_adopted_new_owner": "Se sei il nuovo proprietario, chiedi al precedente proprietario di annullare la registrazione del dispositivo dal suo account nella dashboard cloud. Se ritieni che si tratti di un errore, contatta il nostro team di supporto per ricevere assistenza.",
"already_adopted_other_user": "Questo dispositivo è attualmente registrato a un altro utente nella nostra dashboard cloud.",
"already_adopted_return_to_dashboard": "Torna alla dashboard",
"already_adopted_title": "Dispositivo già registrato",
"appearance_description": "Scegli il tuo tema colore preferito",
"appearance_page_description": "Personalizza l'aspetto e le funzionalità della tua interfaccia JetKVM",
"appearance_theme": "Tema",
"appearance_theme_dark": "Buio",
"appearance_theme_light": "Leggero",
"appearance_theme_system": "Sistema",
"appearance_title": "Aspetto",
"attach": "Allegare",
"atx_power_control_get_state_error": "Impossibile ottenere lo stato di alimentazione ATX: {error}",
"atx_power_control_hdd_led": "LED dell'HDD",
"atx_power_control_long_power_button": "Pressione lunga",
"atx_power_control_power_button": "Energia",
"atx_power_control_power_led": "LED di potenza",
"atx_power_control_reset_button": "Reset",
"atx_power_control_send_action_error": "Impossibile inviare l'azione di alimentazione ATX {action} : {error}",
"atx_power_control_short_power_button": "Pressione breve",
"auth_authentication_mode": "Seleziona una modalità di autenticazione",
"auth_authentication_mode_error": "Si è verificato un errore durante l'impostazione della modalità di autenticazione",
"auth_authentication_mode_invalid": "Modalità di autenticazione non valida",
"auth_connect_to_cloud": "Collega il tuo JetKVM al cloud",
"auth_connect_to_cloud_action": "Accedi e connetti il dispositivo",
"auth_connect_to_cloud_description": "Sblocca l'accesso remoto e le funzionalità avanzate per il tuo dispositivo",
"auth_header_cta_already_have_account": "Hai già un account?",
"auth_header_cta_dont_have_account": "Non hai un account?",
"auth_header_cta_new_to_jetkvm": "Nuovo su JetKVM?",
"auth_login": "Accedi al tuo account JetKVM",
"auth_login_action": "Login",
"auth_login_description": "Accedi per accedere e gestire i tuoi dispositivi in modo sicuro",
"auth_mode_local": "Metodo di autenticazione locale",
"auth_mode_local_change_later": "Potrai sempre modificare il metodo di autenticazione in un secondo momento nelle impostazioni.",
"auth_mode_local_description": "Seleziona come desideri proteggere localmente il tuo dispositivo JetKVM.",
"auth_mode_local_no_password": "Nessuna password",
"auth_mode_local_no_password_description": "Accesso rapido senza autenticazione tramite password.",
"auth_mode_local_password": "Password",
"auth_mode_local_password_confirm_description": "Conferma la tua password",
"auth_mode_local_password_confirm_label": "Conferma password",
"auth_mode_local_password_description": "Per una maggiore protezione, proteggi il tuo dispositivo con una password.",
"auth_mode_local_password_failed_set": "Impossibile impostare la password: {error}",
"auth_mode_local_password_note": "Questa password verrà utilizzata per proteggere i dati del tuo dispositivo e proteggerli da accessi non autorizzati.",
"auth_mode_local_password_note_local": "Tutti i dati rimangono sul tuo dispositivo locale.",
"auth_mode_local_password_set": "Imposta una password",
"auth_mode_local_password_set_button": "Imposta password",
"auth_mode_local_password_set_description": "Crea una password complessa per proteggere localmente il tuo dispositivo JetKVM.",
"auth_mode_local_password_set_label": "Inserisci una password",
"auth_signup_connect_to_cloud_action": "Registrati e connetti il dispositivo",
"auth_signup_create_account": "Crea il tuo account JetKVM",
"auth_signup_create_account_action": "Creare un account",
"auth_signup_create_account_description": "Crea il tuo account e inizia a gestire i tuoi dispositivi con facilità.",
"back": "Indietro",
"back_to_devices": "Torna ai dispositivi",
"cancel": "Cancellare",
"close": "Vicino",
"cloud_kvms": "KVM cloud",
"cloud_kvms_description": "Gestisci i tuoi KVM cloud e connettiti ad essi in modo sicuro.",
"cloud_kvms_no_devices": "Nessun dispositivo trovato",
"cloud_kvms_no_devices_description": "Non hai ancora alcun dispositivo con JetKVM Cloud abilitato.",
"confirm": "Confermare",
"connect_to_kvm": "Connettiti a KVM",
"connecting_to_device": "Connessione al dispositivo…",
"connection_established": "Connessione stabilita",
"connection_stats_badge_jitter": "tremolio",
"connection_stats_badge_jitter_buffer_avg_delay": "Ritardo medio del buffer di jitter",
"connection_stats_connection": "Connessione",
"connection_stats_connection_description": "La connessione tra il client e JetKVM.",
"connection_stats_frames_per_second": "Fotogrammi al secondo",
"connection_stats_frames_per_second_description": "Numero di fotogrammi video in entrata visualizzati al secondo.",
"connection_stats_network_stability": "Stabilità della rete",
"connection_stats_network_stability_description": "Quanto è costante il flusso di pacchetti video in entrata sulla rete.",
"connection_stats_packets_lost": "Pacchetti persi",
"connection_stats_packets_lost_description": "Conteggio dei pacchetti video RTP in entrata persi.",
"connection_stats_playback_delay": "Ritardo di riproduzione",
"connection_stats_playback_delay_description": "Ritardo aggiunto dal buffer jitter per rendere più fluida la riproduzione quando i fotogrammi arrivano in modo non uniforme.",
"connection_stats_round_trip_time": "Tempo di andata e ritorno",
"connection_stats_round_trip_time_description": "Tempo di andata e ritorno per la coppia di candidati ICE attivi tra pari.",
"connection_stats_sidebar": "Statistiche di connessione",
"connection_stats_unit_frames_per_second": " fps",
"connection_stats_unit_milliseconds": " ms",
"connection_stats_unit_packets": " pacchetti",
"connection_stats_video": "Video",
"connection_stats_video_description": "Il flusso video dal JetKVM al client.",
"continue": "Continuare",
"creating_peer_connection": "Creazione di una connessione tra pari…",
"dc_power_control_current": "Attuale",
"dc_power_control_current_unit": "UN",
"dc_power_control_get_state_error": "Impossibile ottenere lo stato di alimentazione CC: {error}",
"dc_power_control_power": "Energia",
"dc_power_control_power_off_button": "Spegnimento",
"dc_power_control_power_off_state": "Spegnimento",
"dc_power_control_power_on_button": "Accensione",
"dc_power_control_power_on_state": "Accensione",
"dc_power_control_power_unit": "O",
"dc_power_control_restore_last_state": "Ultimo stato",
"dc_power_control_restore_power_state": "Ripristinare la perdita di potenza",
"dc_power_control_set_power_state_error": "Impossibile inviare lo stato di alimentazione CC a {enabled} : {error}",
"dc_power_control_set_restore_state_error": "Impossibile inviare lo stato di ripristino dell'alimentazione CC a {state} : {error}",
"dc_power_control_voltage": "Voltaggio",
"dc_power_control_voltage_unit": "V",
"delete": "Eliminare",
"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.",
"deregister_from_cloud": "Annulla registrazione dal cloud",
"deregister_headline": "Annulla la registrazione di {device} dal tuo account cloud",
"detach": "Staccare",
"dhcp_empty_lease_description": "Non abbiamo ancora ricevuto alcuna informazione di lease DHCP dal dispositivo.",
"dhcp_empty_lease_headline": "Nessuna informazione sul lease DHCP",
"dhcp_lease_boot_file": "File di avvio",
"dhcp_lease_boot_next_server": "Avvia il server successivo",
"dhcp_lease_boot_server_name": "Nome del server di avvio",
"dhcp_lease_broadcast": "Trasmissione",
"dhcp_lease_domain": "Dominio",
"dhcp_lease_gateway": "Portale",
"dhcp_lease_header": "Informazioni sul lease DHCP",
"dhcp_lease_hostname": "Nome host",
"dhcp_lease_lease_expires": "Scadenza del contratto di locazione",
"dhcp_lease_maximum_transfer_unit": "MTU",
"dhcp_lease_renew": "Rinnova il contratto di locazione DHCP",
"dhcp_lease_time_to_live": "Tempo di esecuzione",
"dhcp_server": "Server DHCP",
"dns_servers": "Server DNS",
"establishing_secure_connection": "Creazione di una connessione sicura…",
"experimental": "Sperimentale",
"extension_popover_load_and_manage_extensions": "Carica e gestisci le tue estensioni",
"extension_popover_set_error_notification": "Impossibile impostare l'estensione attiva: {error}",
"extension_popover_unload_extension": "Estensione di scaricamento",
"extension_serial_console": "Console seriale",
"extension_serial_console_description": "Accedi all'estensione della tua console seriale",
"extensions_atx_power_control": "Controllo di potenza ATX",
"extensions_atx_power_control_description": "Controlla lo stato di alimentazione del tuo computer tramite il controllo di alimentazione ATX.",
"extensions_dc_power_control": "Controllo di potenza CC",
"extensions_dc_power_control_description": "Controlla la tua estensione di alimentazione CC",
"extensions_popover_extensions": "Estensioni",
"gathering_ice_candidates": "Raduno dei candidati ICE…",
"general_app_version": "App: {version}",
"general_auto_update_description": "Aggiorna automaticamente il dispositivo all'ultima versione",
"general_auto_update_error": "Impossibile impostare l'aggiornamento automatico: {error}",
"general_auto_update_title": "Aggiornamento automatico",
"general_check_for_updates": "Controlla gli aggiornamenti",
"general_page_description": "Configurare le impostazioni del dispositivo e aggiornare le preferenze",
"general_reboot_description": "Vuoi procedere con il riavvio del sistema?",
"general_reboot_device": "Riavvia il dispositivo",
"general_reboot_device_description": "Spegnere e riaccendere JetKVM",
"general_reboot_no_button": "NO",
"general_reboot_title": "Riavviare JetKVM",
"general_reboot_yes_button": "SÌ",
"general_system_version": "Sistema: {version}",
"general_title": "Generale",
"general_update_app_update_title": "Aggiornamento dell'app",
"general_update_application_type": "Applicazione",
"general_update_available_description": "È disponibile un nuovo aggiornamento per migliorare le prestazioni del sistema e la compatibilità. Consigliamo di effettuare l'aggiornamento per garantire il corretto funzionamento di tutto.",
"general_update_available_title": "Aggiornamento disponibile",
"general_update_background_button": "Aggiornamento in background",
"general_update_check_again_button": "Ricontrollare",
"general_update_checking_description": "Ci assicuriamo che il tuo dispositivo abbia le funzionalità e i miglioramenti più recenti.",
"general_update_checking_title": "Controllo degli aggiornamenti…",
"general_update_completed_description": "Il tuo dispositivo è stato aggiornato con successo all'ultima versione. Goditi le nuove funzionalità e i miglioramenti!",
"general_update_completed_title": "Aggiornamento completato con successo",
"general_update_error_description": "Si è verificato un errore durante l'aggiornamento del dispositivo. Riprova più tardi.",
"general_update_error_details": "Dettagli errore: {errorMessage}",
"general_update_error_title": "Errore di aggiornamento",
"general_update_later_button": "Fallo più tardi",
"general_update_now_button": "Aggiorna ora",
"general_update_rebooting": "Riavvio per completare l'aggiornamento…",
"general_update_status_awaiting_reboot": "In attesa di riavvio",
"general_update_status_downloading": "Scaricamento dell'aggiornamento {update_type} …",
"general_update_status_fetching": "Recupero delle informazioni di aggiornamento in corso…",
"general_update_status_installing": "Installazione dell'aggiornamento {update_type} …",
"general_update_status_progress": "{part} progresso",
"general_update_status_verifying": "Verifica dell'aggiornamento {update_type} …",
"general_update_system_type": "Sistema",
"general_update_system_update_title": "Aggiornamento del sistema Linux",
"general_update_up_to_date_description": "Il tuo sistema utilizza la versione più recente. Al momento non sono disponibili aggiornamenti.",
"general_update_up_to_date_title": "Il sistema è aggiornato",
"general_update_updating_description": "Non spegnere il dispositivo. Questo processo potrebbe richiedere alcuni minuti.",
"general_update_updating_title": "Aggiornamento del dispositivo",
"getting_remote_session_description": "Tentativo di ottenimento della descrizione della sessione remota {attempt}",
"hardware_backlight_settings_error": "Impossibile impostare le impostazioni della retroilluminazione: {error}",
"hardware_backlight_settings_get_error": "Impossibile ottenere le impostazioni della retroilluminazione: {error}",
"hardware_backlight_settings_success": "Impostazioni di retroilluminazione aggiornate correttamente",
"hardware_dim_display_after_description": "Imposta il tempo di attesa prima di oscurare il display",
"hardware_dim_display_after_title": "Display scuro dopo",
"hardware_display_brightness_description": "Imposta la luminosità del display",
"hardware_display_brightness_high": "Alto",
"hardware_display_brightness_low": "Basso",
"hardware_display_brightness_medium": "Medio",
"hardware_display_brightness_off": "Spento",
"hardware_display_brightness_title": "Luminosità dello schermo",
"hardware_display_orientation_description": "Imposta l'orientamento del display",
"hardware_display_orientation_error": "Impossibile impostare l'orientamento del display: {error}",
"hardware_display_orientation_inverted": "Invertito",
"hardware_display_orientation_normal": "Normale",
"hardware_display_orientation_success": "Orientamento del display aggiornato correttamente",
"hardware_display_orientation_title": "Orientamento dello schermo",
"hardware_display_wake_up_note": "Il display si riattiverà quando cambia lo stato della connessione o quando viene toccato.",
"hardware_page_description": "Configura le impostazioni di visualizzazione e le opzioni hardware per il tuo dispositivo JetKVM",
"hardware_power_saving_description": "Ridurre il consumo energetico quando non in uso",
"hardware_power_saving_disabled": "Modalità di risparmio energetico disabilitata",
"hardware_power_saving_enabled": "Modalità di risparmio energetico abilitata",
"hardware_power_saving_failed_error": "Impossibile impostare la modalità di risparmio energetico: {error}",
"hardware_power_saving_hdmi_sleep_description": "Disattiva l'acquisizione dopo 90 secondi di inattività",
"hardware_power_saving_hdmi_sleep_title": "Modalità sospensione HDMI",
"hardware_power_saving_title": "Risparmio energetico",
"hardware_time_10_minutes": "10 minuti",
"hardware_time_1_hour": "1 ora",
"hardware_time_1_minute": "1 minuto",
"hardware_time_30_minutes": "30 minuti",
"hardware_time_5_minutes": "5 minuti",
"hardware_time_never": "Mai",
"hardware_title": "Hardware",
"hardware_turn_off_display_after_description": "Periodo di inattività prima che il display si spenga automaticamente",
"hardware_turn_off_display_after_title": "Disattiva display dopo",
"hide": "Nascondere",
"ice_gathering_completed": "Raduno ICE completato",
"info_caps_lock": "Blocco maiuscole",
"info_compose": "Comporre",
"info_hdmi_state": "Stato HDMI:",
"info_hidrpc_state": "Stato HidRPC:",
"info_kana": "Cana",
"info_keys": "Chiavi:",
"info_last_move": "Ultima mossa:",
"info_num_lock": "Bloc Num",
"info_paste_enabled": "Abilitato",
"info_paste_mode": "Modalità Incolla:",
"info_pointer": "Puntatore:",
"info_relayed_by_cloudflare": "Rilasciato da Cloudflare",
"info_resolution": "Risoluzione:",
"info_scroll_lock": "Blocco scorrimento",
"info_shift": "Spostare",
"info_usb_state": "Stato USB:",
"info_video_size": "Dimensioni video:",
"input_disabled": "Input disabilitato",
"invalid_password": "Password non valida",
"ip_address": "Indirizzo IP",
"ipv6_address_label": "Indirizzo",
"ipv6_gateway": "Portale",
"ipv6_information": "Informazioni IPv6",
"ipv6_link_local": "Collegamento locale",
"ipv6_preferred_lifetime": "Durata preferita",
"ipv6_valid_lifetime": "Valido a vita",
"jetkvm_description": "JetKVM combina un hardware potente con un software intuitivo per offrire un'esperienza di controllo remoto impeccabile.",
"jetkvm_device": "Dispositivo JetKVM",
"jetkvm_logo": "Logo JetKVM",
"jetkvm_setup": "Configura il tuo JetKVM",
"jiggler_cron_schedule_description": "Espressione cron per la pianificazione",
"jiggler_cron_schedule_label": "Cron Schedule",
"jiggler_example_business_hours_early": "Orario di lavoro 8-17",
"jiggler_example_business_hours_late": "Orario di lavoro 9-17",
"jiggler_examples_label": "Esempi",
"jiggler_inactivity_limit_description": "Tempo di inattività prima del tremolio",
"jiggler_inactivity_limit_label": "Limite di inattività in secondi",
"jiggler_more_examples": "Altri esempi",
"jiggler_random_delay_description": "Per evitare schemi riconoscibili",
"jiggler_random_delay_label": "Ritardo casuale",
"jiggler_save_jiggler_config": "Salva la configurazione di Jiggler",
"jiggler_timezone_description": "Fuso orario per la pianificazione cron",
"jiggler_timezone_label": "Fuso orario",
"keyboard_description": "Configura le impostazioni della tastiera per il tuo dispositivo",
"keyboard_layout_description": "Layout della tastiera del sistema operativo di destinazione",
"keyboard_layout_error": "Impossibile impostare il layout della tastiera: {error}",
"keyboard_layout_long_description": "La tastiera virtuale, la funzione \"Incolla testo\" e le macro della tastiera inviano singole sequenze di tasti al dispositivo di destinazione. Il layout della tastiera determina quali codici tasto vengono inviati. Assicurarsi che il layout della tastiera in JetKVM corrisponda alle impostazioni del sistema operativo.",
"keyboard_layout_success": "Layout della tastiera impostato correttamente su {layout}",
"keyboard_layout_title": "Layout della tastiera",
"keyboard_show_pressed_keys_description": "Visualizza i tasti attualmente premuti nella barra di stato",
"keyboard_show_pressed_keys_title": "Mostra i tasti premuti",
"keyboard_title": "Tastiera",
"kvm_terminal": "Terminale KVM",
"last_online": "Ultimo accesso {time}",
"learn_more": "Saperne di più",
"load": "Carico",
"loading": "Caricamento…",
"local_auth_change_local_device_password_description": "Inserisci la tua password attuale e una nuova password per aggiornare la protezione del tuo dispositivo locale.",
"local_auth_change_local_device_password_title": "Cambia la password del dispositivo locale",
"local_auth_confirm_new_password_label": "Conferma nuova password",
"local_auth_create_confirm_password_placeholder": "Reinserisci la tua password",
"local_auth_create_description": "Crea una password per proteggere il tuo dispositivo da accessi locali non autorizzati.",
"local_auth_create_new_password_label": "Nuova password",
"local_auth_create_new_password_placeholder": "Inserisci una password complessa",
"local_auth_create_not_now_button": "Non adesso",
"local_auth_create_secure_button": "Dispositivo sicuro",
"local_auth_create_title": "Protezione del dispositivo locale",
"local_auth_current_password_label": "password attuale",
"local_auth_disable_local_device_protection_description": "Inserisci la tua password attuale per disattivare la protezione del dispositivo locale.",
"local_auth_disable_local_device_protection_title": "Disabilita la protezione del dispositivo locale",
"local_auth_disable_protection_button": "Disattiva protezione",
"local_auth_enter_current_password_placeholder": "Inserisci la tua password attuale",
"local_auth_enter_new_password_placeholder": "Inserisci una nuova password complessa",
"local_auth_error_changing_password": "Si è verificato un errore durante la modifica della password",
"local_auth_error_disabling_password": "Si è verificato un errore durante la disabilitazione della password",
"local_auth_error_enter_current_password": "Inserisci la tua password attuale",
"local_auth_error_enter_new_password": "Inserisci una nuova password",
"local_auth_error_enter_old_password": "Inserisci la tua vecchia password",
"local_auth_error_enter_password": "Inserisci una password",
"local_auth_error_passwords_not_match": "Le password non corrispondono",
"local_auth_error_setting_password": "Si è verificato un errore durante l'impostazione della password",
"local_auth_new_password_label": "Nuova password",
"local_auth_reenter_new_password_placeholder": "Reinserisci la tua nuova password",
"local_auth_success_password_disabled_description": "Hai disattivato correttamente la protezione tramite password per l'accesso locale. Ricorda, il tuo dispositivo ora è meno sicuro.",
"local_auth_success_password_disabled_title": "Protezione password disabilitata",
"local_auth_success_password_set_description": "Hai configurato correttamente la protezione del dispositivo locale. Ora il tuo dispositivo è protetto da accessi locali non autorizzati.",
"local_auth_success_password_set_title": "Password impostata correttamente",
"local_auth_success_password_updated_description": "Hai modificato correttamente la password di protezione del tuo dispositivo locale. Assicurati di ricordare la nuova password per gli accessi futuri.",
"local_auth_success_password_updated_title": "Password aggiornata con successo",
"local_auth_update_password_button": "Aggiorna password",
"locale_auto": "Auto",
"locale_change_success": "Lingua modificata correttamente in {locale}",
"locale_da": "Danese",
"locale_de": "Tedesco",
"locale_en": "Inglese",
"locale_es": "Spagnolo",
"locale_fr": "Francese",
"locale_it": "Italiano",
"locale_nb": "Norvegese (bokmål)",
"locale_sv": "Svedese",
"locale_zh": "中文 (简体)",
"log_in": "Login",
"log_out": "Disconnetti",
"logged_in_as": "Accedi come",
"login_enter_password": "Inserisci la tua password",
"login_enter_password_description": "Inserisci la tua password per accedere al tuo JetKVM.",
"login_error": "Si è verificato un errore durante l'accesso",
"login_forgot_password": "Ha dimenticato la password?",
"login_password_label": "Password",
"login_welcome_back": "Bentornati a JetKVM",
"macro_add_step": "Aggiungi passaggio {maxed_out}",
"macro_at_least_one_step_keys_or_modifiers": "Almeno un passaggio deve avere chiavi o modificatori",
"macro_at_least_one_step_required": "È richiesto almeno un passaggio",
"macro_max_steps_error": "È possibile aggiungere al massimo {max} passaggi per macro.",
"macro_max_steps_reached": "( {max} max)",
"macro_name_label": "Nome macro",
"macro_name_required": "Il nome è obbligatorio",
"macro_name_too_long": "Il nome deve contenere meno di 50 caratteri",
"macro_please_fix_validation_errors": "Si prega di correggere gli errori di convalida",
"macro_save": "Salva macro",
"macro_save_failed": "Si è verificato un errore durante il salvataggio.",
"macro_save_failed_error": "Si è verificato un errore durante il salvataggio: {error}.",
"macro_step_count": "{steps} / {max} steps",
"macro_step_duration_description": "Tempo di attesa prima di eseguire il passaggio successivo.",
"macro_step_duration_label": "Durata del passo",
"macro_step_keys_description": "Numero massimo di chiavi {max} per passaggio.",
"macro_step_keys_label": "Chiavi",
"macro_step_max_keys_reached": "Numero massimo di chiavi raggiunto",
"macro_step_modifiers_description": "Quali modificatori (Shift/Ctrl/Alt/Meta) vengono premuti durante questo passaggio.",
"macro_step_modifiers_label": "Modificatori",
"macro_step_no_matching_keys_found": "Nessuna chiave corrispondente trovata",
"macro_step_search_for_key": "Cerca la chiave…",
"macro_steps_description": "Tasti/modificatori eseguiti in sequenza con un ritardo tra ogni passaggio.",
"macro_steps_label": "Passi",
"macros_add_description": "Crea una nuova macro della tastiera",
"macros_add_new": "Aggiungi nuova macro",
"macros_add_new_macro": "Aggiungi nuova macro",
"macros_aria_add_new": "Aggiungi nuova macro",
"macros_aria_delete": "Elimina macro {name}",
"macros_aria_duplicate": "Macro duplicata {name}",
"macros_aria_edit": "Modifica macro {name}",
"macros_aria_move_down": "Sposta {name} in basso",
"macros_aria_move_up": "Sposta {name} in alto",
"macros_confirm_delete_description": "Vuoi davvero eliminare \" {name} \"? Questa azione non può essere annullata.",
"macros_confirm_delete_title": "Elimina macro",
"macros_confirm_deleting": "Eliminazione in corso…",
"macros_create_first_description": "Combina le sequenze di tasti in un'unica azione",
"macros_create_first_headline": "Crea la tua prima macro",
"macros_created_success": "Macro \" {name} \" creata con successo",
"macros_delay_only": "Solo ritardo",
"macros_delete_confirm": "Vuoi davvero eliminare questa macro? Questa azione non può essere annullata.",
"macros_delete_macro": "Elimina macro",
"macros_deleted_success": "Macro \" {name} \" eliminata con successo",
"macros_deleting": "Eliminazione",
"macros_duplicated_success": "Macro \" {name} \" duplicata correttamente",
"macros_edit_button": "Modificare",
"macros_edit_description": "Modifica la macro della tastiera",
"macros_edit_title": "Modifica macro",
"macros_failed_create": "Impossibile creare la macro",
"macros_failed_create_error": "Impossibile creare la macro: {error}",
"macros_failed_delete": "Impossibile eliminare la macro",
"macros_failed_delete_error": "Impossibile eliminare la macro: {error}",
"macros_failed_duplicate": "Impossibile duplicare la macro",
"macros_failed_duplicate_error": "Impossibile duplicare la macro: {error}",
"macros_failed_reorder": "Impossibile riordinare le macro",
"macros_failed_reorder_error": "Impossibile riordinare le macro: {error}",
"macros_failed_update": "Impossibile aggiornare la macro",
"macros_failed_update_error": "Impossibile aggiornare la macro: {error}",
"macros_invalid_data": "Dati macro non validi",
"macros_loading": "Caricamento macro in corso…",
"macros_max_reached": "Massimo raggiunto",
"macros_maximum_macros_reached": "Hai raggiunto il numero massimo di {maximum} consentite.",
"macros_no_macros_available": "Nessuna macro disponibile",
"macros_order_updated": "Ordine macro aggiornato con successo",
"macros_title": "Macro della tastiera",
"macros_updated_success": "Macro \" {name} \" aggiornata con successo",
"metric_not_supported": "Metrica non supportata",
"metric_waiting_for_data": "In attesa di dati…",
"mount_add_file_to_get_started": "Aggiungi un file per iniziare",
"mount_add_new_media": "Aggiungi nuovi media",
"mount_available_storage": "Spazio di archiviazione disponibile",
"mount_button_back_to_overview": "Torna alla panoramica",
"mount_button_cancel_upload": "Annulla caricamento",
"mount_button_continue_upload": "Continua a caricare",
"mount_button_mount_file": "Monta file",
"mount_button_mount_url": "Monta URL",
"mount_button_select": "Selezionare",
"mount_button_showing_results": "Visualizzazione di {from} a {to} di {total} risultati",
"mount_button_upload_new_image": "Carica una nuova immagine",
"mount_bytes_free": "{bytesFree} gratuito",
"mount_bytes_used": "{bytesUsed} utilizzato",
"mount_calculating": "Calcolo in corso…",
"mount_click_to_select_file": "Fare clic per selezionare un file",
"mount_click_to_select_incomplete": "Fai clic per selezionare \" {name} \"",
"mount_confirm_delete": "Sei sicuro di voler eliminare {name} ?",
"mount_continue_uploading_with_name": "Continua a caricare \" {name} \"",
"mount_error_delete_file": "Errore durante l'eliminazione del file: {error}",
"mount_error_description": "Si è verificato un errore durante il tentativo di montare il supporto. Riprova.",
"mount_error_get_storage_space": "Errore durante l'ottenimento dello spazio di archiviazione: {error}",
"mount_error_list_storage": "Errore nell'elenco dei file di archiviazione: {error}",
"mount_error_title": "Errore di montaggio",
"mount_get_state_error": "Impossibile ottenere lo stato del supporto virtuale: {error}",
"mount_jetkvm_storage": "Montaggio di archiviazione JetKVM",
"mount_jetkvm_storage_description": "Montare i file caricati in precedenza dall'archiviazione JetKVM",
"mount_mode_cdrom": "CD/DVD",
"mount_mode_disk": "Disco",
"mount_mounted_as": "Montato come",
"mount_mounted_from_storage": "Montato da JetKVM Storage",
"mount_no_images_description": "Carica un'immagine per avviare il montaggio del supporto virtuale.",
"mount_no_images_title": "Nessuna immagine disponibile",
"mount_no_mounted_media": "Nessun supporto montato",
"mount_percentage_used": "{percentageUsed} % utilizzata",
"mount_please_select_file": "Seleziona il file \" {name} \" per continuare il caricamento.",
"mount_popular_images": "Immagini popolari",
"mount_streaming_from_url": "Streaming da URL",
"mount_supported_formats": "Formati supportati: ISO, IMG",
"mount_unmount": "Smontare",
"mount_unmount_error": "Impossibile smontare l'immagine: {error}",
"mount_upload_description": "Seleziona un file immagine da caricare nell'archiviazione JetKVM",
"mount_upload_error": "Errore di caricamento: {error}",
"mount_upload_failed_datachannel": "Impossibile creare il canale dati per il caricamento del file",
"mount_upload_failed_rtc": "Caricamento non riuscito: {error}",
"mount_upload_successful": "Caricamento riuscito",
"mount_upload_title": "Carica nuova immagine",
"mount_uploaded_has_been_uploaded": "{name} è stato caricato",
"mount_uploading": "Caricamento in corso…",
"mount_uploading_with_name": "Caricamento in corso {name}",
"mount_url_description": "Montare file da qualsiasi indirizzo web pubblico",
"mount_url_input_label": "URL dell'immagine",
"mount_url_mount": "Montaggio URL",
"mount_view_device_description": "Seleziona un'immagine da montare dall'archiviazione JetKVM",
"mount_view_device_title": "Monta da JetKVM Storage",
"mount_view_url_description": "Inserisci un URL al file immagine da montare",
"mount_view_url_title": "Monta da URL",
"mount_virtual_media": "Media virtuali",
"mount_virtual_media_description": "Montare un'immagine da cui avviare o installare un sistema operativo.",
"mount_virtual_media_source": "Fonte multimediale virtuale",
"mount_virtual_media_source_description": "Scegli come vuoi montare i tuoi media virtuali",
"mouse_alt_finger": "Dito che tocca uno schermo",
"mouse_alt_mouse": "Icona del mouse",
"mouse_description": "Configura il comportamento del cursore e le impostazioni di interazione per il tuo dispositivo",
"mouse_hide_cursor_description": "Nascondi il cursore quando invii i movimenti del mouse",
"mouse_hide_cursor_title": "Nascondi cursore",
"mouse_jiggler_config_updated": "Configurazione di Jiggler aggiornata con successo",
"mouse_jiggler_custom": "Costume",
"mouse_jiggler_description": "Simula il movimento del mouse di un computer",
"mouse_jiggler_disabled": "Disabili",
"mouse_jiggler_error_config": "Si è verificato un errore durante l'impostazione della configurazione di Jiggler",
"mouse_jiggler_failed_state": "Impossibile impostare lo stato del jiggler: {error}",
"mouse_jiggler_frequent": "Frequente - 30s",
"mouse_jiggler_invalid_cron": "Espressione cron non valida. Controlla il formato della tua pianificazione (ad esempio, '0 * * * * *' per ogni minuto).",
"mouse_jiggler_light": "Luce - 5m",
"mouse_jiggler_standard": "Standard - 1m",
"mouse_jiggler_title": "Jiggler",
"mouse_mode_absolute": "Assoluto",
"mouse_mode_absolute_description": "Il più conveniente",
"mouse_mode_relative": "Relativo",
"mouse_mode_relative_description": "Più compatibile",
"mouse_modes_description": "Scegli la modalità di input del mouse",
"mouse_modes_title": "Modalità",
"mouse_scroll_high": "Alto",
"mouse_scroll_low": "Basso",
"mouse_scroll_medium": "Medio",
"mouse_scroll_off": "Spento",
"mouse_scroll_throttling_description": "Ridurre la frequenza degli eventi di scorrimento",
"mouse_scroll_throttling_title": "Limitazione dello scorrimento",
"mouse_scroll_very_high": "Molto alto",
"mouse_title": "Topo",
"network_custom_domain": "Dominio personalizzato",
"network_description": "Configura le impostazioni di rete",
"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_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",
"network_dhcp_lease_renew_confirm_new_b": "potrebbe essere necessario riconnettersi utilizzando il nuovo indirizzo",
"network_dhcp_lease_renew_failed": "Impossibile rinnovare il contratto di locazione: {error}",
"network_dhcp_lease_renew_success": "Rinnovo del contratto di locazione DHCP",
"network_domain_custom": "Costume",
"network_domain_description": "Suffisso del dominio di rete per il dispositivo",
"network_domain_dhcp_provided": "DHCP fornito",
"network_domain_local": ".locale",
"network_domain_title": "Dominio",
"network_hostname_description": "Identificatore del dispositivo sulla rete. Vuoto per impostazione predefinita del sistema",
"network_hostname_title": "Nome host",
"network_http_proxy_description": "Server proxy per le richieste HTTP(S) in uscita dal dispositivo. Vuoto per nessuna richiesta.",
"network_http_proxy_invalid": "URL proxy HTTP non valido",
"network_http_proxy_title": "Proxy HTTP",
"network_ipv4_address": "Indirizzo IPv4",
"network_ipv4_dns": "DNS IPv4",
"network_ipv4_gateway": "Gateway IPv4",
"network_ipv4_invalid": "Indirizzo IPv4 non valido",
"network_ipv4_invalid_cidr": "Notazione CIDR non valida per l'indirizzo IPv4",
"network_ipv4_mode_description": "Configurare la modalità IPv4",
"network_ipv4_mode_dhcp": "DHCP",
"network_ipv4_mode_static": "Statico",
"network_ipv4_mode_title": "Modalità IPv4",
"network_ipv4_netmask": "Maschera di rete IPv4",
"network_ipv6_addresses_header": "Indirizzi IPv6",
"network_ipv6_cidr_suggestion": "Si prega di utilizzare la notazione CIDR (ad esempio, 2001:db8::1/64)",
"network_ipv6_dns": "DNS IPv6",
"network_ipv6_flag_dad_failed": "DAD fallito",
"network_ipv6_flag_deprecated": "Obsoleto",
"network_ipv6_gateway": "Gateway IPv6",
"network_ipv6_information": "Informazioni IPv6",
"network_ipv6_invalid": "Indirizzo IPv6 non valido",
"network_ipv6_mode_description": "Configurare la modalità IPv6",
"network_ipv6_mode_dhcpv6": "DHCPv6",
"network_ipv6_mode_disabled": "Disabili",
"network_ipv6_mode_link_local": "Solo collegamento locale",
"network_ipv6_mode_slaac": "SLAAC",
"network_ipv6_mode_slaac_dhcpv6": "SLAAC + DHCPv6",
"network_ipv6_mode_static": "Statico",
"network_ipv6_mode_title": "Modalità IPv6",
"network_ipv6_prefix": "Prefisso IP",
"network_ipv6_prefix_invalid": "Il prefisso deve essere compreso tra 0 e 128",
"network_ll_dp_all": "Tutto",
"network_ll_dp_basic": "Di base",
"network_ll_dp_description": "Controlla quali TLV verranno inviati tramite Link Layer Discovery Protocol",
"network_ll_dp_disabled": "Disabili",
"network_ll_dp_title": "LLDP",
"network_mac_address_copy_error": "Impossibile copiare l'indirizzo MAC",
"network_mac_address_copy_success": "Indirizzo MAC { mac } copiato negli appunti",
"network_mac_address_description": "Identificatore hardware per l'interfaccia di rete",
"network_mac_address_title": "Indirizzo MAC",
"network_mdns_auto": "Auto",
"network_mdns_description": "Controlla la modalità operativa mDNS (DNS multicast)",
"network_mdns_disabled": "Disabili",
"network_mdns_ipv4_only": "Solo IPv4",
"network_mdns_ipv6_only": "Solo IPv6",
"network_mdns_title": "mDNS",
"network_no_information_description": "Nessuna configurazione di rete disponibile",
"network_no_information_headline": "Informazioni di rete",
"network_pending_dhcp_mode_change_description": "Salva le impostazioni per abilitare la modalità DHCP e visualizzare le informazioni di locazione",
"network_pending_dhcp_mode_change_headline": "In attesa di modifica della modalità DHCP IPv4",
"network_save_settings": "Salva impostazioni",
"network_save_settings_apply_title": "Applica le impostazioni di rete",
"network_save_settings_confirm": "Applica modifiche",
"network_save_settings_confirm_description": "Verranno applicate le seguenti impostazioni di rete. Queste modifiche potrebbero richiedere un riavvio e causare una breve disconnessione.",
"network_save_settings_confirm_heading": "Modifiche alla configurazione",
"network_save_settings_failed": "Impossibile salvare le impostazioni di rete: {error}",
"network_save_settings_success": "Impostazioni di rete salvate",
"network_settings_add_dns": "Aggiungi server DNS",
"network_settings_load_error": "Impossibile caricare le impostazioni di rete: {error}",
"network_static_ipv4_header": "Configurazione IPv4 statica",
"network_static_ipv6_header": "Configurazione IPv6 statica",
"network_time_sync_description": "Configurare le impostazioni di sincronizzazione dell'ora",
"network_time_sync_http_only": "Solo HTTP",
"network_time_sync_ntp_and_http": "NTP e HTTP",
"network_time_sync_ntp_only": "Solo NTP",
"network_time_sync_title": "Sincronizzazione oraria",
"network_title": "Rete",
"never_seen_online": "Mai visto online",
"next": "Prossimo",
"no_results_found": "Nessun risultato trovato",
"not_applicable": "N / A",
"not_available": "N / A",
"not_found": "Non trovato",
"ntp_servers": "Server NTP",
"oh_no": "Oh no!",
"online": "In linea",
"other_session_detected": "Un'altra sessione attiva rilevata",
"other_session_take_over": " È supportata una sola sessione attiva alla volta. Vuoi prendere il controllo di questa sessione?",
"other_session_use_here_button": "Usa qui",
"page_not_found_description": "La pagina che stavi cercando non esiste.",
"paste_modal_confirm_paste": "Conferma Incolla",
"paste_modal_delay_between_keys": "Ritardo tra i tasti",
"paste_modal_delay_out_of_range": "Il ritardo deve essere compreso tra {min} e {max}",
"paste_modal_failed_paste": "Impossibile incollare il testo: {error}",
"paste_modal_invalid_chars_intro": "I seguenti caratteri non verranno incollati:",
"paste_modal_paste_from_host": "Incolla dall'host",
"paste_modal_sending_using_layout": "Invio di testo tramite layout di tastiera: {iso} - {name}",
"paste_text": "Incolla il testo",
"paste_text_description": "Incolla il testo dal tuo client all'host remoto",
"peer_connection_closed": "Chiuso",
"peer_connection_closing": "Chiusura",
"peer_connection_connected": "Collegato",
"peer_connection_connecting": "Collegamento",
"peer_connection_disconnected": "Disconnesso",
"peer_connection_error": "Errore di connessione",
"peer_connection_failed": "Connessione fallita",
"peer_connection_new": "Collegamento",
"previous": "Precedente",
"register_device_error": "Si è verificato un errore {error} durante la registrazione del dispositivo.",
"register_device_finish_button": "Completa l'installazione",
"register_device_name_description": "Assegna un nome al tuo dispositivo per poterlo identificare facilmente in seguito. Puoi cambiare questo nome in qualsiasi momento.",
"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": "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}",
"rename_device_new_name_label": "Nuovo nome del dispositivo",
"rename_device_new_name_placeholder": "Server multimediale Plex",
"rename_device_no_name": "Si prega di specificare un nome",
"retry": "Riprova",
"saving": "Risparmio…",
"search_placeholder": "Ricerca…",
"serial_console": "Console seriale",
"serial_console_baud_rate": "Velocità in baud",
"serial_console_configure_description": "Configura le impostazioni della tua console seriale",
"serial_console_data_bits": "Bit di dati",
"serial_console_get_settings_error": "Impossibile ottenere le impostazioni della console seriale: {error}",
"serial_console_open_console": "Apri console",
"serial_console_parity": "Parità",
"serial_console_parity_even": "Parità pari",
"serial_console_parity_mark": "Segna parità",
"serial_console_parity_none": "Nessuna parità",
"serial_console_parity_odd": "Parità dispari",
"serial_console_parity_space": "Parità spaziale",
"serial_console_set_settings_error": "Impossibile impostare le impostazioni della console seriale su {settings} : {error}",
"serial_console_stop_bits": "Bit di stop",
"setting_remote_description": "Impostazione della descrizione remota",
"setting_remote_session_description": "Impostazione della descrizione della sessione remota...",
"setting_up_connection_to_device": "Impostazione della connessione al dispositivo...",
"settings_access": "Accesso",
"settings_advanced": "Avanzato",
"settings_appearance": "Aspetto",
"settings_back_to_kvm": "Torna a KVM",
"settings_general": "Generale",
"settings_hardware": "Hardware",
"settings_keyboard": "Tastiera",
"settings_keyboard_macros": "Macro della tastiera",
"settings_mouse": "Topo",
"settings_network": "Rete",
"settings_video": "Video",
"something_went_wrong": "Qualcosa è andato storto. Riprova più tardi o contatta l'assistenza.",
"step_counter_step": "Passaggio {step}",
"subnet_mask": "Maschera di sottorete",
"time_division_days": "giorni",
"time_division_hours": "ore",
"time_division_minutes": "minuti",
"time_division_months": "mesi",
"time_division_seconds": "secondi",
"time_division_weeks": "settimane",
"time_division_years": "anni",
"troubleshoot_connection": "Risoluzione dei problemi di connessione",
"unknown_error": "Errore sconosciuto",
"update_in_progress": "Aggiornamento in corso",
"updates_failed_check": "Impossibile verificare gli aggiornamenti: {error}",
"updates_failed_get_device_version": "Impossibile ottenere la versione del dispositivo: {error}",
"updating_leave_device_on": "Per favore, non spegnere il tuo dispositivo…",
"usb": "USB",
"usb_config_custom": "Costume",
"usb_config_default": "JetKVM predefinito",
"usb_config_dell": "Tastiera Dell Multimedia Pro",
"usb_config_failed_load": "Impossibile caricare la configurazione USB: {error}",
"usb_config_failed_set": "Impossibile impostare la configurazione USB: {error}",
"usb_config_identifiers_description": "Identificatori di dispositivi USB esposti al computer di destinazione",
"usb_config_identifiers_title": "Identificatori",
"usb_config_logitech": "Adattatore universale Logitech",
"usb_config_manufacturer_label": "Produttore",
"usb_config_manufacturer_placeholder": "Inserisci il produttore",
"usb_config_microsoft": "Tastiera multimediale wireless Microsoft",
"usb_config_product_id_label": "ID prodotto",
"usb_config_product_id_placeholder": "Inserisci l'ID prodotto",
"usb_config_product_name_label": "Nome del prodotto",
"usb_config_product_name_placeholder": "Inserisci il nome del prodotto",
"usb_config_restore_default": "Ripristina impostazioni predefinite",
"usb_config_serial_number_label": "Numero di serie",
"usb_config_serial_number_placeholder": "Inserisci il numero di serie",
"usb_config_set_success": "Configurazione USB impostata su {manufacturer} {product}",
"usb_config_update_identifiers": "Aggiorna gli identificatori USB",
"usb_config_vendor_id_label": "ID fornitore",
"usb_config_vendor_id_placeholder": "Inserisci l'ID del fornitore",
"usb_device_classes_description": "Classi di dispositivi USB nel dispositivo composito",
"usb_device_classes_title": "Classi",
"usb_device_custom": "Costume",
"usb_device_description": "Dispositivi USB da emulare sul computer di destinazione",
"usb_device_enable_absolute_mouse_description": "Abilita mouse assoluto (puntatore)",
"usb_device_enable_absolute_mouse_title": "Abilita mouse assoluto (puntatore)",
"usb_device_enable_keyboard_description": "Abilita tastiera",
"usb_device_enable_keyboard_title": "Abilita tastiera",
"usb_device_enable_mass_storage_description": "A volte potrebbe essere necessario disattivarlo per evitare problemi con determinati dispositivi",
"usb_device_enable_mass_storage_title": "Abilita archiviazione di massa USB",
"usb_device_enable_relative_mouse_description": "Abilita mouse relativo",
"usb_device_enable_relative_mouse_title": "Abilita mouse relativo",
"usb_device_failed_load": "Impossibile caricare i dispositivi USB: {error}",
"usb_device_failed_set": "Impossibile impostare i dispositivi USB: {error}",
"usb_device_keyboard_mouse_and_mass_storage": "Tastiera, mouse e memoria di massa",
"usb_device_keyboard_only": "Solo tastiera",
"usb_device_restore_default": "Ripristina impostazioni predefinite",
"usb_device_title": "Dispositivo USB",
"usb_device_update_classes": "Aggiorna classi USB",
"usb_device_updated": "Dispositivi USB aggiornati",
"usb_state_connected": "Collegato",
"usb_state_connecting": "Collegamento",
"usb_state_disconnected": "Disconnesso",
"usb_state_low_power_mode": "Modalità a basso consumo",
"user_interface_language_description": "Seleziona la lingua da utilizzare nell'interfaccia utente JetKVM",
"user_interface_language_title": "Lingua dell'interfaccia",
"video_brightness_description": "Livello di luminosità ( {value} x)",
"video_brightness_title": "Luminosità",
"video_contrast_description": "Livello di contrasto ( {value} x)",
"video_contrast_title": "Contrasto",
"video_custom_edid_description": "L'EDID specifica la compatibilità della modalità video. Le impostazioni predefinite funzionano nella maggior parte dei casi, ma potrebbero essere necessarie modifiche specifiche per UEFI/BIOS.",
"video_custom_edid_title": "EDID personalizzato",
"video_debugging_info_description": "Informazioni di debug per il video",
"video_debugging_info_title": "Informazioni di debug",
"video_description": "Configurare le impostazioni di visualizzazione e EDID per una compatibilità ottimale",
"video_edid_acer_b246wl": "Acer B246WL, 1920x1200",
"video_edid_asus_pa248qv": "ASUS PA248QV, 1920x1200",
"video_edid_custom": "Costume",
"video_edid_dell_d2721h": "DELL D2721H, 1920x1080",
"video_edid_dell_idrac": "DELL IDRAC EDID, 1280x1024",
"video_edid_description": "Regola le impostazioni EDID per il display",
"video_edid_file_label": "File EDID",
"video_edid_jetkvm_default": "JetKVM predefinito",
"video_edid_set_success": "EDID impostato correttamente su {edid}",
"video_edid_title": "EDID",
"video_enhancement_description": "Regola le impostazioni del colore per rendere l'output video più vivace e colorato",
"video_enhancement_title": "Miglioramento video",
"video_failed_get_debug_info": "Impossibile ottenere informazioni di debug: {error}",
"video_failed_get_edid": "Impossibile ottenere EDID: {error}",
"video_failed_set_edid": "Impossibile impostare EDID: {error}",
"video_failed_set_stream_quality": "Impossibile impostare la qualità del flusso: {error}",
"video_get_debugging_info": "Ottieni informazioni di debug",
"video_overlay_autoplay_permissions_required": "Sono richieste autorizzazioni di riproduzione automatica",
"video_overlay_conn_check_cables": "Controllare tutti i collegamenti dei cavi per eventuali fili allentati o danneggiati",
"video_overlay_conn_ensure_network": "Assicurati che la tua connessione di rete sia stabile e attiva",
"video_overlay_conn_restart": "Prova a riavviare sia il dispositivo che il computer",
"video_overlay_conn_verify_power": "Verificare che il dispositivo sia acceso e correttamente collegato",
"video_overlay_connection_issue_title": "Problema di connessione rilevato",
"video_overlay_enable_autoplay_settings": "Si prega di modificare le impostazioni del browser per abilitare la riproduzione automatica",
"video_overlay_hdmi_error_title": "Rilevato errore del segnale HDMI.",
"video_overlay_hdmi_incompatible_resolution": "Impostazioni di risoluzione o frequenza di aggiornamento incompatibili",
"video_overlay_hdmi_loose_faulty": "Una connessione HDMI allentata o difettosa",
"video_overlay_hdmi_source_issue": "Problemi con l'uscita HDMI del dispositivo sorgente",
"video_overlay_learn_more": "Saperne di più",
"video_overlay_loading_stream": "Caricamento del flusso video in corso…",
"video_overlay_manually_start_stream": "Avvia manualmente lo streaming",
"video_overlay_no_hdmi_adapter_compat": "Se si utilizza un adattatore, assicurarsi che sia compatibile e funzioni correttamente",
"video_overlay_no_hdmi_ensure_cable": "Assicurarsi che il cavo HDMI sia collegato saldamente a entrambe le estremità",
"video_overlay_no_hdmi_ensure_power": "Assicurarsi che il dispositivo sorgente sia acceso e che emetta un segnale",
"video_overlay_no_hdmi_signal": "Nessun segnale HDMI rilevato.",
"video_overlay_pointerlock_click_to_enable": "Clicca sul video per abilitare il controllo del mouse",
"video_overlay_retrying_connection": "Nuovo tentativo di connessione…",
"video_overlay_troubleshooting_guide": "Guida alla risoluzione dei problemi",
"video_overlay_try_again": "Riprova",
"video_pointer_lock_disabled": "Blocco puntatore disabilitato",
"video_pointer_lock_enabled": "Blocco puntatore abilitato: premi Esc per sbloccarlo",
"video_quality_high": "Alto",
"video_quality_low": "Basso",
"video_quality_medium": "Medio",
"video_reset_to_default": "Ripristina impostazioni predefinite",
"video_restore_to_default": "Ripristina impostazioni predefinite",
"video_saturation_description": "Saturazione del colore ( {value} x)",
"video_saturation_title": "Saturazione",
"video_set_custom_edid": "Imposta EDID personalizzato",
"video_stream_quality_description": "Regola la qualità del flusso video",
"video_stream_quality_set": "Qualità dello streaming impostata su {quality}",
"video_stream_quality_title": "Qualità dello streaming",
"video_title": "Video",
"view_details": "Visualizza dettagli",
"virtual_keyboard_header": "Tastiera virtuale",
"wake_on_lan": "Wake On LAN",
"wake_on_lan_add_device_device_name": "Nome del dispositivo",
"wake_on_lan_add_device_example_device_name": "Server multimediale Plex",
"wake_on_lan_add_device_mac_address": "Indirizzo MAC",
"wake_on_lan_add_device_save_device": "Salva dispositivo",
"wake_on_lan_description": "Invia un Magic Packet per riattivare un dispositivo remoto.",
"wake_on_lan_device_list_add_new_device": "Aggiungi nuovo dispositivo",
"wake_on_lan_device_list_delete_device": "Elimina dispositivo",
"wake_on_lan_device_list_wake": "Veglia",
"wake_on_lan_empty_add_device_to_start": "Aggiungi un dispositivo per iniziare a utilizzare Wake-on-LAN",
"wake_on_lan_empty_add_new_device": "Aggiungi nuovo dispositivo",
"wake_on_lan_empty_no_devices_added": "Nessun dispositivo aggiunto",
"wake_on_lan_failed_add_device": "Impossibile aggiungere il dispositivo",
"wake_on_lan_failed_send_magic": "Impossibile inviare il pacchetto magico",
"wake_on_lan_invalid_mac": "Indirizzo MAC non valido",
"wake_on_lan_magic_sent_success": "Pacchetto magico inviato con successo",
"welcome_to_jetkvm": "Benvenuti a JetKVM",
"welcome_to_jetkvm_description": "Controlla qualsiasi computer da remoto"
}

View File

@ -0,0 +1,895 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"access_adopt_kvm": "Ta i bruk KVM i skyen",
"access_adopted_message": "Enheten din er tilpasset skyen",
"access_auth_mode_no_password": "Nåværende modus: Ingen passord",
"access_auth_mode_password": "Gjeldende modus: Passordbeskyttet",
"access_authentication_mode_title": "Autentiseringsmodus",
"access_certificate_label": "Sertifikat",
"access_change_password_button": "Endre passord",
"access_change_password_description": "Oppdater passordet for enhetstilgang",
"access_change_password_title": "Endre passord",
"access_cloud_api_url_label": "URL-adresse for Cloud API",
"access_cloud_app_url_label": "URL-adresse for skyapplikasjon",
"access_cloud_provider_description": "Velg skyleverandøren for enheten din",
"access_cloud_provider_title": "Skyleverandør",
"access_cloud_security_title": "Skysikkerhet",
"access_confirm_deregister": "Er du sikker på at du vil avregistrere denne enheten?",
"access_deregister": "Avregistrer deg fra skyen",
"access_description": "Administrer tilgangskontrollen til enheten",
"access_disable_protection": "Deaktiver beskyttelse",
"access_enable_password": "Aktiver passord",
"access_failed_deregister": "Kunne ikke avregistrere enheten: {error}",
"access_failed_update_cloud_url": "Kunne ikke oppdatere nettadressen til skyen: {error}",
"access_failed_update_tls": "Kunne ikke oppdatere TLS-innstillingene: {error}",
"access_github_link": "GitHub",
"access_https_description": "Konfigurer sikker HTTPS-tilgang til enheten din",
"access_https_mode_title": "HTTPS-modus",
"access_learn_security": "Lær om vår skysikkerhet",
"access_local_description": "Administrer modusen for lokal tilgang til enheten",
"access_local_title": "Lokalt",
"access_no_device_id": "Ingen enhets-ID tilgjengelig",
"access_private_key_description": "Av sikkerhetshensyn vil den ikke vises etter lagring.",
"access_private_key_label": "Privat nøkkel",
"access_provider_custom": "Tilpasset",
"access_provider_jetkvm": "JetKVM Cloud",
"access_remote_description": "Administrer modusen for ekstern tilgang til enheten",
"access_security_encryption": "Ende-til-ende-kryptering ved bruk av WebRTC (DTLS og SRTP)",
"access_security_oidc": "OIDC (OpenID Connect)-autentisering",
"access_security_open_source": "Alle skykomponenter er åpen kildekode og tilgjengelige på GitHub.",
"access_security_streams": "Alle strømmer kryptert under overføring",
"access_security_zero_trust": "Nulltillitssikkerhetsmodell",
"access_title": "Adgang",
"access_tls_certificate_description": "Lim inn TLS-sertifikatet ditt nedenfor. For sertifikatkjeder, inkluder hele kjeden (blad-, mellom- og rotsertifikater).",
"access_tls_certificate_title": "TLS-sertifikat",
"access_tls_custom": "Tilpasset",
"access_tls_disabled": "Deaktivert",
"access_tls_self_signed": "Selvsignert",
"access_tls_updated": "TLS-innstillingene er oppdatert",
"access_update_tls_settings": "Oppdater TLS-innstillinger",
"action_bar_connection_stats": "Tilkoblingsstatistikk",
"action_bar_extension": "Forlengelse",
"action_bar_fullscreen": "Fullskjerm",
"action_bar_settings": "Innstillinger",
"action_bar_virtual_keyboard": "Virtuelt tastatur",
"action_bar_virtual_media": "Virtuelle medier",
"action_bar_wake_on_lan": "Vekk på LAN",
"action_bar_web_terminal": "Nettterminal",
"advanced_description": "Få tilgang til flere innstillinger for feilsøking og tilpasning",
"advanced_dev_channel_description": "Motta tidlige oppdateringer fra utviklingskanalen",
"advanced_dev_channel_title": "Oppdateringer for utviklerkanaler",
"advanced_developer_mode_description": "Aktiver avanserte funksjoner for utviklere",
"advanced_developer_mode_enabled_title": "Utviklermodus aktivert",
"advanced_developer_mode_title": "Utviklermodus",
"advanced_developer_mode_warning_advanced": "Kun for avanserte brukere. Ikke for produksjonsbruk.",
"advanced_developer_mode_warning_risks": "Bruk kun hvis du forstår risikoene",
"advanced_developer_mode_warning_security": "Sikkerheten svekkes mens den er aktiv",
"advanced_disable_usb_emulation": "Deaktiver USB-emulering",
"advanced_enable_usb_emulation": "Aktiver USB-emulering",
"advanced_error_loopback_disable": "Klarte ikke å deaktivere kun loopback-modus: {error}",
"advanced_error_loopback_enable": "Kunne ikke aktivere kun loopback-modus: {error}",
"advanced_error_reset_config": "Kunne ikke tilbakestille konfigurasjonen: {error}",
"advanced_error_set_dev_channel": "Klarte ikke å angi tilstanden til utviklerkanalen: {error}",
"advanced_error_set_dev_mode": "Kunne ikke angi utviklermodus: {error}",
"advanced_error_update_ssh_key": "Kunne ikke oppdatere SSH-nøkkelen: {error}",
"advanced_error_usb_emulation_disable": "Kunne ikke deaktivere USB-emulering: {error}",
"advanced_error_usb_emulation_enable": "Kunne ikke aktivere USB-emulering: {error}",
"advanced_loopback_only_description": "Begrens tilgang til webgrensesnittet kun til lokal vert (127.0.0.1)",
"advanced_loopback_only_title": "Kun tilbakekoblingsmodus",
"advanced_loopback_warning_before": "Før du aktiverer denne funksjonen, må du sørge for at du har enten:",
"advanced_loopback_warning_cloud": "Skytilgang aktivert og fungerer",
"advanced_loopback_warning_confirm": "Jeg forstår, aktiver uansett",
"advanced_loopback_warning_description": "ADVARSEL: Dette vil begrense tilgangen til webgrensesnittet kun til localhost (127.0.0.1).",
"advanced_loopback_warning_ssh": "SSH-tilgang konfigurert og testet",
"advanced_loopback_warning_title": "Aktivere kun tilbakekoblingsmodus?",
"advanced_reset_config_button": "Tilbakestill konfigurasjon",
"advanced_reset_config_description": "Tilbakestill konfigurasjonen til standard. Dette vil logge deg ut.",
"advanced_reset_config_title": "Tilbakestill konfigurasjon",
"advanced_ssh_access_description": "Legg til din offentlige SSH-nøkkel for å aktivere sikker ekstern tilgang til enheten",
"advanced_ssh_access_title": "SSH-tilgang",
"advanced_ssh_default_user": "Standard SSH-brukeren er",
"advanced_ssh_public_key_label": "SSH offentlig nøkkel",
"advanced_ssh_public_key_placeholder": "Skriv inn din offentlige SSH-nøkkel",
"advanced_success_loopback_disabled": "Kun tilbakekoblingsmodus deaktivert. Start enheten på nytt for å bruke den.",
"advanced_success_loopback_enabled": "Kun tilbakekoblingsmodus aktivert. Start enheten på nytt for å bruke den.",
"advanced_success_reset_config": "Konfigurasjonen ble tilbakestilt til standard",
"advanced_success_update_ssh_key": "SSH-nøkkelen er oppdatert",
"advanced_title": "Avansert",
"advanced_troubleshooting_mode_description": "Diagnostiske verktøy og tilleggskontroller for feilsøking og utviklingsformål",
"advanced_troubleshooting_mode_title": "Feilsøkingsmodus",
"advanced_update_ssh_key_button": "Oppdater SSH-nøkkel",
"advanced_usb_emulation_description": "Kontroller USB-emuleringstilstanden",
"advanced_usb_emulation_title": "USB-emulering",
"already_adopted_new_owner": "Hvis du er den nye eieren, ber du den forrige eieren om å avregistrere enheten fra kontoen sin i skydashbordet. Hvis du mener dette er en feil, kan du kontakte supportteamet vårt for å få hjelp.",
"already_adopted_other_user": "Denne enheten er for øyeblikket registrert til en annen bruker i vårt skydashbord.",
"already_adopted_return_to_dashboard": "Gå tilbake til dashbordet",
"already_adopted_title": "Enheten er allerede registrert",
"appearance_description": "Velg ditt foretrukne fargetema",
"appearance_page_description": "Tilpass utseendet og følelsen til JetKVM-grensesnittet ditt",
"appearance_theme": "Tema",
"appearance_theme_dark": "Mørk",
"appearance_theme_light": "Lys",
"appearance_theme_system": "System",
"appearance_title": "Utseende",
"attach": "Feste",
"atx_power_control_get_state_error": "Klarte ikke å hente ATX-strømstatus: {error}",
"atx_power_control_hdd_led": "HDD-LED",
"atx_power_control_long_power_button": "Langt trykk",
"atx_power_control_power_button": "Strøm",
"atx_power_control_power_led": "Strøm-LED",
"atx_power_control_reset_button": "Tilbakestill",
"atx_power_control_send_action_error": "Kunne ikke sende ATX-strømhandling {action} : {error}",
"atx_power_control_short_power_button": "Kort trykk",
"auth_authentication_mode": "Vennligst velg en autentiseringsmodus",
"auth_authentication_mode_error": "Det oppsto en feil under angivelse av autentiseringsmodus",
"auth_authentication_mode_invalid": "Ugyldig autentiseringsmodus",
"auth_connect_to_cloud": "Koble JetKVM-en din til skyen",
"auth_connect_to_cloud_action": "Logg inn og koble til enheten",
"auth_connect_to_cloud_description": "Lås opp fjerntilgang og avanserte funksjoner for enheten din",
"auth_header_cta_already_have_account": "Har du allerede en konto?",
"auth_header_cta_dont_have_account": "Har du ikke en konto?",
"auth_header_cta_new_to_jetkvm": "Ny bruker av JetKVM?",
"auth_login": "Logg inn på JetKVM-kontoen din",
"auth_login_action": "Logg inn",
"auth_login_description": "Logg inn for å få tilgang til og administrere enhetene dine på en sikker måte",
"auth_mode_local": "Lokal autentiseringsmetode",
"auth_mode_local_change_later": "Du kan alltid endre autentiseringsmetoden din senere i innstillingene.",
"auth_mode_local_description": "Velg hvordan du vil sikre JetKVM-enheten din lokalt.",
"auth_mode_local_no_password": "Ikke noe passord",
"auth_mode_local_no_password_description": "Rask tilgang uten passordgodkjenning.",
"auth_mode_local_password": "Passord",
"auth_mode_local_password_confirm_description": "Bekreft passordet ditt",
"auth_mode_local_password_confirm_label": "Bekreft passord",
"auth_mode_local_password_description": "Sikre enheten din med et passord for ekstra beskyttelse.",
"auth_mode_local_password_failed_set": "Klarte ikke å angi passord: {error}",
"auth_mode_local_password_note": "Dette passordet vil bli brukt til å sikre enhetsdataene dine og beskytte mot uautorisert tilgang.",
"auth_mode_local_password_note_local": "Alle dataene forblir på din lokale enhet.",
"auth_mode_local_password_set": "Angi et passord",
"auth_mode_local_password_set_button": "Angi passord",
"auth_mode_local_password_set_description": "Opprett et sterkt passord for å sikre JetKVM-enheten din lokalt.",
"auth_mode_local_password_set_label": "Skriv inn et passord",
"auth_signup_connect_to_cloud_action": "Registrer og koble til enhet",
"auth_signup_create_account": "Opprett JetKVM-kontoen din",
"auth_signup_create_account_action": "Opprett konto",
"auth_signup_create_account_description": "Opprett kontoen din og begynn å administrere enhetene dine med letthet.",
"back": "Tilbake",
"back_to_devices": "Tilbake til Enheter",
"cancel": "Avbryt",
"close": "Lukk",
"cloud_kvms": "Cloud KVM-er",
"cloud_kvms_description": "Administrer skybaserte KVM-er og koble til dem sikkert.",
"cloud_kvms_no_devices": "Ingen enheter funnet",
"cloud_kvms_no_devices_description": "Du har ingen enheter med aktivert JetKVM Cloud ennå.",
"confirm": "Bekrefte",
"connect_to_kvm": "Koble til KVM",
"connecting_to_device": "Kobler til enhet …",
"connection_established": "Forbindelse opprettet",
"connection_stats_badge_jitter": "Jitter",
"connection_stats_badge_jitter_buffer_avg_delay": "Jitterbuffer Gjns. forsinkelse",
"connection_stats_connection": "Forbindelse",
"connection_stats_connection_description": "Forbindelsen mellom klienten og JetKVM-en.",
"connection_stats_frames_per_second": "Bilder per sekund",
"connection_stats_frames_per_second_description": "Antall innkommende videobilder som vises per sekund.",
"connection_stats_network_stability": "Nettverksstabilitet",
"connection_stats_network_stability_description": "Hvor jevn flyten av innkommende videopakker er over nettverket.",
"connection_stats_packets_lost": "Pakker tapt",
"connection_stats_packets_lost_description": "Antall tapte innkommende RTP-videopakker.",
"connection_stats_playback_delay": "Avspillingsforsinkelse",
"connection_stats_playback_delay_description": "Forsinkelse lagt til av jitterbufferen for jevn avspilling når bilder ankommer ujevnt.",
"connection_stats_round_trip_time": "Tur-retur-tid",
"connection_stats_round_trip_time_description": "Rundturstid for det aktive ICE-kandidatparet mellom jevnaldrende.",
"connection_stats_sidebar": "Tilkoblingsstatistikk",
"connection_stats_unit_frames_per_second": " fps",
"connection_stats_unit_milliseconds": " ms",
"connection_stats_unit_packets": " pakker",
"connection_stats_video": "Video",
"connection_stats_video_description": "Videostrømmen fra JetKVM til klienten.",
"continue": "Fortsette",
"creating_peer_connection": "Oppretter kontakt med andre personer …",
"dc_power_control_current": "Nåværende",
"dc_power_control_current_unit": "EN",
"dc_power_control_get_state_error": "Klarte ikke å hente likestrømsstatus: {error}",
"dc_power_control_power": "Strøm",
"dc_power_control_power_off_button": "Slå av",
"dc_power_control_power_off_state": "Slå av",
"dc_power_control_power_on_button": "Slå på",
"dc_power_control_power_on_state": "Slå PÅ",
"dc_power_control_power_unit": "V",
"dc_power_control_restore_last_state": "Siste stat",
"dc_power_control_restore_power_state": "Gjenopprett strømtap",
"dc_power_control_set_power_state_error": "Kunne ikke sende likestrømsstatus til {enabled} : {error}",
"dc_power_control_set_restore_state_error": "Kunne ikke sende gjenopprettingsstatus for likestrøm til {state} : {error}",
"dc_power_control_voltage": "Spenning",
"dc_power_control_voltage_unit": "V",
"delete": "Slett",
"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.",
"deregister_from_cloud": "Avregistrer deg fra skyen",
"deregister_headline": "Avregistrer {device} fra skykontoen din",
"detach": "Løsne",
"dhcp_empty_lease_description": "Vi har ikke mottatt noen DHCP-leaseinformasjon fra enheten ennå.",
"dhcp_empty_lease_headline": "Ingen DHCP-leaseinformasjon",
"dhcp_lease_boot_file": "Oppstartsfil",
"dhcp_lease_boot_next_server": "Start opp neste server",
"dhcp_lease_boot_server_name": "Navn på oppstartsserver",
"dhcp_lease_broadcast": "Kringkaste",
"dhcp_lease_domain": "Domene",
"dhcp_lease_gateway": "Inngangsport",
"dhcp_lease_header": "DHCP-leaseinformasjon",
"dhcp_lease_hostname": "Vertsnavn",
"dhcp_lease_lease_expires": "Leieavtalen utløper",
"dhcp_lease_maximum_transfer_unit": "MTU",
"dhcp_lease_renew": "Forny DHCP-leieavtale",
"dhcp_lease_time_to_live": "TTL",
"dhcp_server": "DHCP-server",
"dns_servers": "DNS-servere",
"establishing_secure_connection": "Oppretter sikker forbindelse…",
"experimental": "Eksperimentell",
"extension_popover_load_and_manage_extensions": "Last inn og administrer utvidelsene dine",
"extension_popover_set_error_notification": "Klarte ikke å angi aktiv utvidelse: {error}",
"extension_popover_unload_extension": "Fjern utvidelse",
"extension_serial_console": "Seriell konsoll",
"extension_serial_console_description": "Få tilgang til seriekonsollutvidelsen din",
"extensions_atx_power_control": "ATX-strømstyring",
"extensions_atx_power_control_description": "Kontroller maskinens strømstatus via ATX-strømkontroll.",
"extensions_dc_power_control": "DC-strømkontroll",
"extensions_dc_power_control_description": "Kontroller DC-strømutvidelsen din",
"extensions_popover_extensions": "Utvidelser",
"gathering_ice_candidates": "Samler ICE-kandidater…",
"general_app_version": "App: {version}",
"general_auto_update_description": "Oppdater enheten automatisk til den nyeste versjonen",
"general_auto_update_error": "Klarte ikke å angi automatisk oppdatering: {error}",
"general_auto_update_title": "Automatisk oppdatering",
"general_check_for_updates": "Se etter oppdateringer",
"general_page_description": "Konfigurer enhetsinnstillinger og oppdater preferanser",
"general_reboot_description": "Vil du fortsette med å starte systemet på nytt?",
"general_reboot_device": "Start enheten på nytt",
"general_reboot_device_description": "Slå av og på JetKVM-en",
"general_reboot_no_button": "Ingen",
"general_reboot_title": "Start JetKVM på nytt",
"general_reboot_yes_button": "Ja",
"general_system_version": "System: {version}",
"general_title": "General",
"general_update_app_update_title": "Appoppdatering",
"general_update_application_type": "App",
"general_update_available_description": "En ny oppdatering er tilgjengelig for å forbedre systemytelsen og kompatibiliteten. Vi anbefaler å oppdatere for å sikre at alt går knirkefritt.",
"general_update_available_title": "Oppdatering tilgjengelig",
"general_update_background_button": "Oppdater i bakgrunnen",
"general_update_check_again_button": "Sjekk igjen",
"general_update_checking_description": "Vi sørger for at enheten din har de nyeste funksjonene og forbedringene.",
"general_update_checking_title": "Ser etter oppdateringer …",
"general_update_completed_description": "Enheten din er oppdatert til den nyeste versjonen. Kos deg med de nye funksjonene og forbedringene!",
"general_update_completed_title": "Oppdatering fullført",
"general_update_error_description": "Det oppsto en feil under oppdatering av enheten din. Prøv på nytt senere.",
"general_update_error_details": "Feildetaljer: {errorMessage}",
"general_update_error_title": "Oppdateringsfeil",
"general_update_later_button": "Gjør det senere",
"general_update_now_button": "Oppdater nå",
"general_update_rebooting": "Starter på nytt for å fullføre oppdateringen …",
"general_update_status_awaiting_reboot": "Venter på omstart",
"general_update_status_downloading": "Laster ned {update_type} oppdatering…",
"general_update_status_fetching": "Henter oppdateringsinformasjon …",
"general_update_status_installing": "Installerer {update_type} oppdatering…",
"general_update_status_progress": "{part} fremgang",
"general_update_status_verifying": "Verifiserer {update_type} oppdatering…",
"general_update_system_type": "System",
"general_update_system_update_title": "Linux-systemoppdatering",
"general_update_up_to_date_description": "Systemet ditt kjører den nyeste versjonen. Ingen oppdateringer er tilgjengelige for øyeblikket.",
"general_update_up_to_date_title": "Systemet er oppdatert",
"general_update_updating_description": "Ikke slå av enheten. Denne prosessen kan ta noen minutter.",
"general_update_updating_title": "Oppdaterer enheten din",
"getting_remote_session_description": "Henter beskrivelse av ekstern øktforsøk {attempt}",
"hardware_backlight_settings_error": "Kunne ikke angi innstillinger for bakgrunnsbelysning: {error}",
"hardware_backlight_settings_get_error": "Klarte ikke å hente bakgrunnsbelysningsinnstillinger: {error}",
"hardware_backlight_settings_success": "Bakgrunnsbelysningsinnstillingene er oppdatert",
"hardware_dim_display_after_description": "Angi hvor lenge det skal ventes før displayet dimmes",
"hardware_dim_display_after_title": "Dimme skjermen etter",
"hardware_display_brightness_description": "Angi lysstyrken på skjermen",
"hardware_display_brightness_high": "Høy",
"hardware_display_brightness_low": "Lav",
"hardware_display_brightness_medium": "Medium",
"hardware_display_brightness_off": "Av",
"hardware_display_brightness_title": "Skjermens lysstyrke",
"hardware_display_orientation_description": "Angi retningen på skjermen",
"hardware_display_orientation_error": "Klarte ikke å angi skjermretning: {error}",
"hardware_display_orientation_inverted": "Invertert",
"hardware_display_orientation_normal": "Normal",
"hardware_display_orientation_success": "Skjermretningen er oppdatert",
"hardware_display_orientation_title": "Skjermretning",
"hardware_display_wake_up_note": "Skjermen vil våkne når tilkoblingsstatusen endres, eller når den berøres.",
"hardware_page_description": "Konfigurer skjerminnstillinger og maskinvarealternativer for JetKVM-enheten din",
"hardware_power_saving_description": "Reduser strømforbruket når det ikke er i bruk",
"hardware_power_saving_disabled": "Strømsparingsmodus deaktivert",
"hardware_power_saving_enabled": "Strømsparingsmodus aktivert",
"hardware_power_saving_failed_error": "Kunne ikke angi strømsparingsmodus: {error}",
"hardware_power_saving_hdmi_sleep_description": "Slå av opptak etter 90 sekunder med inaktivitet",
"hardware_power_saving_hdmi_sleep_title": "HDMI-hvilemodus",
"hardware_power_saving_title": "Strømsparing",
"hardware_time_10_minutes": "10 minutter",
"hardware_time_1_hour": "1 time",
"hardware_time_1_minute": "1 minutt",
"hardware_time_30_minutes": "30 minutter",
"hardware_time_5_minutes": "5 minutter",
"hardware_time_never": "Aldri",
"hardware_title": "Maskinvare",
"hardware_turn_off_display_after_description": "Periode med inaktivitet før skjermen slår seg av automatisk",
"hardware_turn_off_display_after_title": "Slå av skjermen etter",
"hide": "Gjemme",
"ice_gathering_completed": "ICE-samlingen er fullført",
"info_caps_lock": "Caps Lock",
"info_compose": "Skriv",
"info_hdmi_state": "HDMI-tilstand:",
"info_hidrpc_state": "HidRPC-status:",
"info_kana": "Kana",
"info_keys": "Nøkler:",
"info_last_move": "Siste trekk:",
"info_num_lock": "Num Lock",
"info_paste_enabled": "Aktivert",
"info_paste_mode": "Lim inn-modus:",
"info_pointer": "Peker:",
"info_relayed_by_cloudflare": "Videresendt av Cloudflare",
"info_resolution": "Oppløsning:",
"info_scroll_lock": "Scroll Lock",
"info_shift": "Skifte",
"info_usb_state": "USB-tilstand:",
"info_video_size": "Videostørrelse:",
"input_disabled": "Inndata deaktivert",
"invalid_password": "Ugyldig passord",
"ip_address": "IP-adresse",
"ipv6_address_label": "Adresse",
"ipv6_gateway": "Inngangsport",
"ipv6_information": "IPv6-informasjon",
"ipv6_link_local": "Link-local",
"ipv6_preferred_lifetime": "Foretrukket levetid",
"ipv6_valid_lifetime": "Gyldig livstid",
"jetkvm_description": "JetKVM kombinerer kraftig maskinvare med intuitiv programvare for å gi en sømløs fjernkontrollopplevelse.",
"jetkvm_device": "JetKVM-enhet",
"jetkvm_logo": "JetKVM-logo",
"jetkvm_setup": "Sett opp JetKVM-en din",
"jiggler_cron_schedule_description": "Cron-uttrykk for planlegging",
"jiggler_cron_schedule_label": "Cron-plan",
"jiggler_example_business_hours_early": "Åpningstider 8-17",
"jiggler_example_business_hours_late": "Åpningstider 917",
"jiggler_examples_label": "Eksempler",
"jiggler_inactivity_limit_description": "Inaktivitetstid før risting",
"jiggler_inactivity_limit_label": "Inaktivitetsgrense i sekunder",
"jiggler_more_examples": "Flere eksempler",
"jiggler_random_delay_description": "For å unngå gjenkjennelige mønstre",
"jiggler_random_delay_label": "Tilfeldig forsinkelse",
"jiggler_save_jiggler_config": "Lagre Jiggler-konfigurasjon",
"jiggler_timezone_description": "Tidssone for cron-plan",
"jiggler_timezone_label": "Tidssone",
"keyboard_description": "Konfigurer tastaturinnstillinger for enheten din",
"keyboard_layout_description": "Tastaturoppsett for måloperativsystemet",
"keyboard_layout_error": "Klarte ikke å angi tastaturoppsett: {error}",
"keyboard_layout_long_description": "Det virtuelle tastaturet, limetekst og tastaturmakroer sender individuelle tastetrykk til målenheten. Tastaturoppsettet bestemmer hvilke tastekoder som sendes. Sørg for at tastaturoppsettet i JetKVM samsvarer med innstillingene i operativsystemet.",
"keyboard_layout_success": "Tastaturoppsettet er satt til {layout}",
"keyboard_layout_title": "Tastaturoppsett",
"keyboard_show_pressed_keys_description": "Vis taster som for øyeblikket trykkes ned i statuslinjen",
"keyboard_show_pressed_keys_title": "Vis trykkede taster",
"keyboard_title": "Tastatur",
"kvm_terminal": "KVM-terminal",
"last_online": "Sist online {time}",
"learn_more": "Lær mer",
"load": "Laste",
"loading": "Laster inn…",
"local_auth_change_local_device_password_description": "Skriv inn ditt nåværende passord og et nytt passord for å oppdatere den lokale enhetsbeskyttelsen.",
"local_auth_change_local_device_password_title": "Endre passord for lokal enhet",
"local_auth_confirm_new_password_label": "Bekreft nytt passord",
"local_auth_create_confirm_password_placeholder": "Skriv inn passordet ditt på nytt",
"local_auth_create_description": "Opprett et passord for å beskytte enheten din mot uautorisert lokal tilgang.",
"local_auth_create_new_password_label": "Nytt passord",
"local_auth_create_new_password_placeholder": "Skriv inn et sterkt passord",
"local_auth_create_not_now_button": "Ikke nå",
"local_auth_create_secure_button": "Sikre enheten",
"local_auth_create_title": "Lokal enhetsbeskyttelse",
"local_auth_current_password_label": "Nåværende passord",
"local_auth_disable_local_device_protection_description": "Skriv inn ditt nåværende passord for å deaktivere lokal enhetsbeskyttelse.",
"local_auth_disable_local_device_protection_title": "Deaktiver lokal enhetsbeskyttelse",
"local_auth_disable_protection_button": "Deaktiver beskyttelse",
"local_auth_enter_current_password_placeholder": "Skriv inn ditt nåværende passord",
"local_auth_enter_new_password_placeholder": "Skriv inn et nytt sterkt passord",
"local_auth_error_changing_password": "Det oppsto en feil under endring av passordet",
"local_auth_error_disabling_password": "Det oppsto en feil under deaktivering av passordet",
"local_auth_error_enter_current_password": "Vennligst skriv inn ditt nåværende passord",
"local_auth_error_enter_new_password": "Vennligst skriv inn et nytt passord",
"local_auth_error_enter_old_password": "Vennligst skriv inn det gamle passordet ditt",
"local_auth_error_enter_password": "Vennligst skriv inn et passord",
"local_auth_error_passwords_not_match": "Passordene stemmer ikke overens",
"local_auth_error_setting_password": "Det oppsto en feil under angivelse av passordet",
"local_auth_new_password_label": "Nytt passord",
"local_auth_reenter_new_password_placeholder": "Skriv inn det nye passordet ditt på nytt",
"local_auth_success_password_disabled_description": "Du har deaktivert passordbeskyttelsen for lokal tilgang. Husk at enheten din nå er mindre sikker.",
"local_auth_success_password_disabled_title": "Passordbeskyttelse deaktivert",
"local_auth_success_password_set_description": "Du har konfigurert lokal enhetsbeskyttelse. Enheten din er nå sikret mot uautorisert lokal tilgang.",
"local_auth_success_password_set_title": "Passord angitt",
"local_auth_success_password_updated_description": "Du har endret passordet for beskyttelse av den lokale enheten. Husk det nye passordet for fremtidig tilgang.",
"local_auth_success_password_updated_title": "Passord oppdatert",
"local_auth_update_password_button": "Oppdater passord",
"locale_auto": "Auto",
"locale_change_success": "Språket er endret til {locale}",
"locale_da": "Dansk",
"locale_de": "Tysk",
"locale_en": "Engelsk",
"locale_es": "Spansk",
"locale_fr": "Fransk",
"locale_it": "Italiensk",
"locale_nb": "Norsk (bokmål)",
"locale_sv": "Svensk",
"locale_zh": "中文 (简体)",
"log_in": "Logg inn",
"log_out": "Logg ut",
"logged_in_as": "Logget inn som",
"login_enter_password": "Skriv inn passordet ditt",
"login_enter_password_description": "Skriv inn passordet ditt for å få tilgang til JetKVM-en din.",
"login_error": "Det oppsto en feil under innlogging",
"login_forgot_password": "Glemt passord?",
"login_password_label": "Passord",
"login_welcome_back": "Velkommen tilbake til JetKVM",
"macro_add_step": "Legg til trinn {maxed_out}",
"macro_at_least_one_step_keys_or_modifiers": "Minst ett trinn må ha nøkler eller modifikatorer",
"macro_at_least_one_step_required": "Minst ett trinn er nødvendig",
"macro_max_steps_error": "Du kan bare legge til maksimalt {max} trinn per makro.",
"macro_max_steps_reached": "( {max} maks)",
"macro_name_label": "Makronavn",
"macro_name_required": "Navn er obligatorisk",
"macro_name_too_long": "Navnet må være mindre enn 50 tegn",
"macro_please_fix_validation_errors": "Vennligst rett opp valideringsfeilene",
"macro_save": "Lagre makro",
"macro_save_failed": "Det oppsto en feil under lagring.",
"macro_save_failed_error": "Det oppsto en feil under lagring: {error}.",
"macro_step_count": "{steps} / {max} trinn",
"macro_step_duration_description": "Tid for å vente før man tar neste steg.",
"macro_step_duration_label": "Stegvarighet",
"macro_step_keys_description": "Maksimalt antall {max} nøkler per trinn.",
"macro_step_keys_label": "Nøkler",
"macro_step_max_keys_reached": "Maksimalt antall nøkler er nådd",
"macro_step_modifiers_description": "Hvilke modifikatorer (Shift/Ctrl/Alt/Meta) trykkes ned i dette trinnet?",
"macro_step_modifiers_label": "Modifikatorer",
"macro_step_no_matching_keys_found": "Ingen samsvarende nøkler funnet",
"macro_step_search_for_key": "Søk etter nøkkel…",
"macro_steps_description": "Taster/modifikatorer utføres i rekkefølge med en forsinkelse mellom hvert trinn.",
"macro_steps_label": "Trinn",
"macros_add_description": "Opprett en ny tastaturmakro",
"macros_add_new": "Legg til ny makro",
"macros_add_new_macro": "Legg til ny makro",
"macros_aria_add_new": "Legg til ny makro",
"macros_aria_delete": "Slett makro {name}",
"macros_aria_duplicate": "Duplikatmakro {name}",
"macros_aria_edit": "Rediger makro {name}",
"macros_aria_move_down": "Flytt {name} ned",
"macros_aria_move_up": "Flytt {name} opp",
"macros_confirm_delete_description": "Er du sikker på at du vil slette « {name} «? Denne handlingen kan ikke angres.",
"macros_confirm_delete_title": "Slett makro",
"macros_confirm_deleting": "Sletter…",
"macros_create_first_description": "Kombiner tastetrykk til én handling",
"macros_create_first_headline": "Lag din første makro",
"macros_created_success": "Makroen « {name} « ble opprettet",
"macros_delay_only": "Bare forsinkelse",
"macros_delete_confirm": "Er du sikker på at du vil slette denne makroen? Denne handlingen kan ikke angres.",
"macros_delete_macro": "Slett makro",
"macros_deleted_success": "Makroen « {name} « ble slettet",
"macros_deleting": "Sletter",
"macros_duplicated_success": "Makroen « {name} « ble duplisert",
"macros_edit_button": "Redigere",
"macros_edit_description": "Endre tastaturmakroen din",
"macros_edit_title": "Rediger makro",
"macros_failed_create": "Kunne ikke opprette makroen",
"macros_failed_create_error": "Klarte ikke å opprette makro: {error}",
"macros_failed_delete": "Kunne ikke slette makroen",
"macros_failed_delete_error": "Klarte ikke å slette makroen: {error}",
"macros_failed_duplicate": "Kunne ikke duplisere makroen",
"macros_failed_duplicate_error": "Klarte ikke å duplisere makroen: {error}",
"macros_failed_reorder": "Kunne ikke endre rekkefølgen på makroene",
"macros_failed_reorder_error": "Kunne ikke endre rekkefølgen på makroer: {error}",
"macros_failed_update": "Kunne ikke oppdatere makroen",
"macros_failed_update_error": "Kunne ikke oppdatere makroen: {error}",
"macros_invalid_data": "Ugyldige makrodata",
"macros_loading": "Laster inn makroer …",
"macros_max_reached": "Maks nådd",
"macros_maximum_macros_reached": "Du har nådd det maksimale antallet tillatte makroer {maximum}",
"macros_no_macros_available": "Ingen makroer tilgjengelig",
"macros_order_updated": "Makroordren er oppdatert",
"macros_title": "Tastaturmakroer",
"macros_updated_success": "Makroen « {name} « ble oppdatert",
"metric_not_supported": "Målingen støttes ikke",
"metric_waiting_for_data": "Venter på data …",
"mount_add_file_to_get_started": "Legg til en fil for å komme i gang",
"mount_add_new_media": "Legg til nye medier",
"mount_available_storage": "Tilgjengelig lagringsplass",
"mount_button_back_to_overview": "Tilbake til oversikt",
"mount_button_cancel_upload": "Avbryt opplasting",
"mount_button_continue_upload": "Fortsett opplastingen",
"mount_button_mount_file": "Monter fil",
"mount_button_mount_url": "Monterings-URL",
"mount_button_select": "Velge",
"mount_button_showing_results": "Viser {from} til {to} av {total} resultater",
"mount_button_upload_new_image": "Last opp et nytt bilde",
"mount_bytes_free": "{bytesFree} ledig",
"mount_bytes_used": "{bytesUsed} brukt",
"mount_calculating": "Beregner…",
"mount_click_to_select_file": "Klikk for å velge en fil",
"mount_click_to_select_incomplete": "Klikk for å velge « {name} »",
"mount_confirm_delete": "Er du sikker på at du vil slette {name} ?",
"mount_continue_uploading_with_name": "Fortsett å laste opp « {name} »",
"mount_error_delete_file": "Feil ved sletting av fil: {error}",
"mount_error_description": "Det oppsto en feil under forsøk på å montere mediet. Prøv på nytt.",
"mount_error_get_storage_space": "Feil ved henting av lagringsplass: {error}",
"mount_error_list_storage": "Feil ved oppføring av lagringsfiler: {error}",
"mount_error_title": "Monteringsfeil",
"mount_get_state_error": "Klarte ikke å hente status for virtuelle medier: {error}",
"mount_jetkvm_storage": "JetKVM-lagringsmontering",
"mount_jetkvm_storage_description": "Monter tidligere opplastede filer fra JetKVM-lagringen",
"mount_mode_cdrom": "CD/DVD",
"mount_mode_disk": "Disk",
"mount_mounted_as": "Montert som",
"mount_mounted_from_storage": "Montert fra JetKVM-lagring",
"mount_no_images_description": "Last opp et bilde for å starte montering av virtuelle medier.",
"mount_no_images_title": "Ingen bilder tilgjengelig",
"mount_no_mounted_media": "Ingen monterte medier",
"mount_percentage_used": "{percentageUsed} % brukt",
"mount_please_select_file": "Vennligst velg filen « {name} » for å fortsette opplastingen.",
"mount_popular_images": "Populære bilder",
"mount_streaming_from_url": "Strømming fra URL",
"mount_supported_formats": "Støttede formater: ISO, IMG",
"mount_unmount": "Avmonter",
"mount_unmount_error": "Klarte ikke å demontere bildet: {error}",
"mount_upload_description": "Velg en bildefil som skal lastes opp til JetKVM-lagring",
"mount_upload_error": "Opplastingsfeil: {error}",
"mount_upload_failed_datachannel": "Kunne ikke opprette datakanal for filopplasting",
"mount_upload_failed_rtc": "Opplasting mislyktes: {error}",
"mount_upload_successful": "Opplastingen var vellykket",
"mount_upload_title": "Last opp nytt bilde",
"mount_uploaded_has_been_uploaded": "{name} har blitt lastet opp",
"mount_uploading": "Laster opp…",
"mount_uploading_with_name": "Laster opp {name}",
"mount_url_description": "Monter filer fra en hvilken som helst offentlig nettadresse",
"mount_url_input_label": "Bilde-URL",
"mount_url_mount": "URL-montering",
"mount_view_device_description": "Velg et bilde som skal monteres fra JetKVM-lagringen",
"mount_view_device_title": "Monter fra JetKVM-lagring",
"mount_view_url_description": "Skriv inn en URL til bildefilen som skal monteres",
"mount_view_url_title": "Monter fra URL",
"mount_virtual_media": "Virtuelle medier",
"mount_virtual_media_description": "Monter et image for å starte opp fra eller installere et operativsystem.",
"mount_virtual_media_source": "Virtuell mediekilde",
"mount_virtual_media_source_description": "Velg hvordan du vil montere virtuelle medier",
"mouse_alt_finger": "Fingerberøring av en skjerm",
"mouse_alt_mouse": "Musikon",
"mouse_description": "Konfigurer markørens oppførsel og interaksjonsinnstillinger for enheten din",
"mouse_hide_cursor_description": "Skjul markøren når du sender musebevegelser",
"mouse_hide_cursor_title": "Skjul markør",
"mouse_jiggler_config_updated": "Jiggler-konfigurasjonen er oppdatert",
"mouse_jiggler_custom": "Tilpasset",
"mouse_jiggler_description": "Simuler bevegelsen til en datamus",
"mouse_jiggler_disabled": "Deaktivert",
"mouse_jiggler_error_config": "Det oppsto en feil under innstilling av jiggler-konfigurasjonen",
"mouse_jiggler_failed_state": "Klarte ikke å angi jiggler-tilstand: {error}",
"mouse_jiggler_frequent": "Hyppig - 30-tallet",
"mouse_jiggler_invalid_cron": "Ugyldig cron-uttrykk. Sjekk planformatet ditt (f.eks. '0 * * * * *' for hvert minutt).",
"mouse_jiggler_light": "Lys - 5m",
"mouse_jiggler_standard": "Standard - 1 m",
"mouse_jiggler_title": "Jiggler",
"mouse_mode_absolute": "Absolutt",
"mouse_mode_absolute_description": "Mest praktisk",
"mouse_mode_relative": "Slektning",
"mouse_mode_relative_description": "Mest kompatible",
"mouse_modes_description": "Velg museinndatamodus",
"mouse_modes_title": "Moduser",
"mouse_scroll_high": "Høy",
"mouse_scroll_low": "Lav",
"mouse_scroll_medium": "Medium",
"mouse_scroll_off": "Av",
"mouse_scroll_throttling_description": "Reduser hyppigheten av rullehendelser",
"mouse_scroll_throttling_title": "Rullebegrensning",
"mouse_scroll_very_high": "Svært høy",
"mouse_title": "Mus",
"network_custom_domain": "Tilpasset domene",
"network_description": "Konfigurer nettverksinnstillingene dine",
"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_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",
"network_dhcp_lease_renew_confirm_new_b": "du må kanskje koble til på nytt med den nye adressen",
"network_dhcp_lease_renew_failed": "Kunne ikke fornye leieavtalen: {error}",
"network_dhcp_lease_renew_success": "DHCP-leieavtale fornyet",
"network_domain_custom": "Tilpasset",
"network_domain_description": "Nettverksdomenesuffiks for enheten",
"network_domain_dhcp_provided": "DHCP levert",
"network_domain_local": ".lokal",
"network_domain_title": "Domene",
"network_hostname_description": "Enhetsidentifikator på nettverket. Blank for systemstandard",
"network_hostname_title": "Vertsnavn",
"network_http_proxy_description": "Proxy-server for utgående HTTP(S)-forespørsler fra enheten. Tomt hvis ingen.",
"network_http_proxy_invalid": "Ugyldig HTTP-proxy-URL",
"network_http_proxy_title": "HTTP-proxy",
"network_ipv4_address": "IPv4-adresse",
"network_ipv4_dns": "IPv4 DNS",
"network_ipv4_gateway": "IPv4-gateway",
"network_ipv4_invalid": "Ugyldig IPv4-adresse",
"network_ipv4_invalid_cidr": "Ugyldig CIDR-notasjon for IPv4-adresse",
"network_ipv4_mode_description": "Konfigurer IPv4-modusen",
"network_ipv4_mode_dhcp": "DHCP",
"network_ipv4_mode_static": "Statisk",
"network_ipv4_mode_title": "IPv4-modus",
"network_ipv4_netmask": "IPv4-nettmaske",
"network_ipv6_addresses_header": "IPv6-adresser",
"network_ipv6_cidr_suggestion": "Vennligst bruk CIDR-notasjon (f.eks. 2001:db8::1/64)",
"network_ipv6_dns": "IPv6 DNS",
"network_ipv6_flag_dad_failed": "DAD mislyktes",
"network_ipv6_flag_deprecated": "Utdatert",
"network_ipv6_gateway": "IPv6-gateway",
"network_ipv6_information": "IPv6-informasjon",
"network_ipv6_invalid": "Ugyldig IPv6-adresse",
"network_ipv6_mode_description": "Konfigurer IPv6-modusen",
"network_ipv6_mode_dhcpv6": "DHCPv6",
"network_ipv6_mode_disabled": "Deaktivert",
"network_ipv6_mode_link_local": "Kun lenkelokal",
"network_ipv6_mode_slaac": "SLAAC",
"network_ipv6_mode_slaac_dhcpv6": "SLAAC + DHCPv6",
"network_ipv6_mode_static": "Statisk",
"network_ipv6_mode_title": "IPv6-modus",
"network_ipv6_prefix": "IP-prefiks",
"network_ipv6_prefix_invalid": "Prefikset må være mellom 0 og 128",
"network_ll_dp_all": "Alle",
"network_ll_dp_basic": "Grunnleggende",
"network_ll_dp_description": "Kontroller hvilke TLV-er som skal sendes over Link Layer Discovery Protocol",
"network_ll_dp_disabled": "Deaktivert",
"network_ll_dp_title": "LLDP",
"network_mac_address_copy_error": "Kunne ikke kopiere MAC-adressen",
"network_mac_address_copy_success": "MAC-adresse { mac } kopiert til utklippstavlen",
"network_mac_address_description": "Maskinvareidentifikator for nettverksgrensesnittet",
"network_mac_address_title": "MAC-adresse",
"network_mdns_auto": "Auto",
"network_mdns_description": "Kontrollmodus for mDNS (multicast DNS)",
"network_mdns_disabled": "Deaktivert",
"network_mdns_ipv4_only": "Kun IPv4",
"network_mdns_ipv6_only": "Kun IPv6",
"network_mdns_title": "mDNS",
"network_no_information_description": "Ingen nettverkskonfigurasjon tilgjengelig",
"network_no_information_headline": "Nettverksinformasjon",
"network_pending_dhcp_mode_change_description": "Lagre innstillinger for å aktivere DHCP-modus og vise leieavtaleinformasjon",
"network_pending_dhcp_mode_change_headline": "Venter på endring av DHCP IPv4-modus",
"network_save_settings": "Lagre innstillinger",
"network_save_settings_apply_title": "Bruk nettverksinnstillinger",
"network_save_settings_confirm": "Bruk endringer",
"network_save_settings_confirm_description": "Følgende nettverksinnstillinger vil bli brukt. Disse endringene kan kreve en omstart og forårsake en kortvarig frakobling.",
"network_save_settings_confirm_heading": "Konfigurasjonsendringer",
"network_save_settings_failed": "Kunne ikke lagre nettverksinnstillinger: {error}",
"network_save_settings_success": "Nettverksinnstillinger lagret",
"network_settings_add_dns": "Legg til DNS-server",
"network_settings_load_error": "Kunne ikke laste inn nettverksinnstillinger: {error}",
"network_static_ipv4_header": "Statisk IPv4-konfigurasjon",
"network_static_ipv6_header": "Statisk IPv6-konfigurasjon",
"network_time_sync_description": "Konfigurer innstillinger for tidssynkronisering",
"network_time_sync_http_only": "Kun HTTP",
"network_time_sync_ntp_and_http": "NTP og HTTP",
"network_time_sync_ntp_only": "Kun NTP",
"network_time_sync_title": "Tidssynkronisering",
"network_title": "Nettverk",
"never_seen_online": "Aldri sett på nett",
"next": "Neste",
"no_results_found": "Ingen resultater funnet",
"not_applicable": "Ikke aktuelt",
"not_available": "Ikke aktuelt",
"not_found": "Ikke funnet",
"ntp_servers": "NTP-servere",
"oh_no": "Å nei!",
"online": "På nett",
"other_session_detected": "En annen aktiv økt oppdaget",
"other_session_take_over": " Bare én aktiv økt støttes om gangen. Vil du overta denne økten?",
"other_session_use_here_button": "Bruk her",
"page_not_found_description": "Siden du lette etter finnes ikke.",
"paste_modal_confirm_paste": "Bekreft liming",
"paste_modal_delay_between_keys": "Forsinkelse mellom taster",
"paste_modal_delay_out_of_range": "Forsinkelsen må være mellom {min} og {max}",
"paste_modal_failed_paste": "Klarte ikke å lime inn tekst: {error}",
"paste_modal_invalid_chars_intro": "Følgende tegn vil ikke bli limt inn:",
"paste_modal_paste_from_host": "Lim inn fra verten",
"paste_modal_sending_using_layout": "Sende tekst ved hjelp av tastaturoppsett: {iso} - {name}",
"paste_text": "Lim inn tekst",
"paste_text_description": "Lim inn tekst fra klienten din til den eksterne verten",
"peer_connection_closed": "Lukket",
"peer_connection_closing": "Lukking",
"peer_connection_connected": "Tilkoblet",
"peer_connection_connecting": "Tilkobling",
"peer_connection_disconnected": "Frakoblet",
"peer_connection_error": "Tilkoblingsfeil",
"peer_connection_failed": "Tilkoblingen mislyktes",
"peer_connection_new": "Tilkobling",
"previous": "Tidligere",
"register_device_error": "Det oppsto en feil {error} under registrering av enheten din.",
"register_device_finish_button": "Fullfør oppsettet",
"register_device_name_description": "Gi enheten din et navn slik at du enkelt kan identifisere den senere. Du kan endre dette navnet når som helst.",
"register_device_name_label": "Enhetsnavn",
"register_device_name_placeholder": "Plex Media Server",
"register_device_no_name": "Vennligst oppgi et navn",
"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}",
"rename_device_new_name_label": "Nytt enhetsnavn",
"rename_device_new_name_placeholder": "Plex Media Server",
"rename_device_no_name": "Vennligst oppgi et navn",
"retry": "Prøv på nytt",
"saving": "Lagrer…",
"search_placeholder": "Søk…",
"serial_console": "Seriell konsoll",
"serial_console_baud_rate": "Baudhastighet",
"serial_console_configure_description": "Konfigurer innstillingene for seriekonsollen",
"serial_console_data_bits": "Databiter",
"serial_console_get_settings_error": "Klarte ikke å hente innstillinger for seriell konsoll: {error}",
"serial_console_open_console": "Åpne konsollen",
"serial_console_parity": "Paritet",
"serial_console_parity_even": "Paritet",
"serial_console_parity_mark": "Mark Paritet",
"serial_console_parity_none": "Ingen paritet",
"serial_console_parity_odd": "Oddeparitet",
"serial_console_parity_space": "Romparitet",
"serial_console_set_settings_error": "Klarte ikke å sette innstillingene for seriell konsoll til {settings} : {error}",
"serial_console_stop_bits": "Stoppbiter",
"setting_remote_description": "Innstilling av fjernkontrollbeskrivelse",
"setting_remote_session_description": "Angi beskrivelse av ekstern økt...",
"setting_up_connection_to_device": "Setter opp tilkobling til enhet...",
"settings_access": "Adgang",
"settings_advanced": "Avansert",
"settings_appearance": "Utseende",
"settings_back_to_kvm": "Tilbake til KVM",
"settings_general": "General",
"settings_hardware": "Maskinvare",
"settings_keyboard": "Tastatur",
"settings_keyboard_macros": "Tastaturmakroer",
"settings_mouse": "Mus",
"settings_network": "Nettverk",
"settings_video": "Video",
"something_went_wrong": "Noe gikk galt. Prøv igjen senere, eller kontakt kundestøtte.",
"step_counter_step": "Trinn {step}",
"subnet_mask": "Nettmaske",
"time_division_days": "dager",
"time_division_hours": "timer",
"time_division_minutes": "minutter",
"time_division_months": "måneder",
"time_division_seconds": "sekunder",
"time_division_weeks": "uker",
"time_division_years": "år",
"troubleshoot_connection": "Feilsøking av tilkobling",
"unknown_error": "Ukjent feil",
"update_in_progress": "Oppdatering pågår",
"updates_failed_check": "Klarte ikke å se etter oppdateringer: {error}",
"updates_failed_get_device_version": "Klarte ikke å hente enhetsversjon: {error}",
"updating_leave_device_on": "Vennligst ikke slå av enheten din ...",
"usb": "USB",
"usb_config_custom": "Tilpasset",
"usb_config_default": "JetKVM-standard",
"usb_config_dell": "Dell Multimedia Pro-tastatur",
"usb_config_failed_load": "Klarte ikke å laste inn USB-konfigurasjon: {error}",
"usb_config_failed_set": "Kunne ikke angi USB-konfigurasjon: {error}",
"usb_config_identifiers_description": "USB-enhetsidentifikatorer eksponert for måldatamaskinen",
"usb_config_identifiers_title": "Identifikatorer",
"usb_config_logitech": "Logitech universaladapter",
"usb_config_manufacturer_label": "Produsent",
"usb_config_manufacturer_placeholder": "Angi produsent",
"usb_config_microsoft": "Microsoft trådløst multimedietastatur",
"usb_config_product_id_label": "Produkt-ID",
"usb_config_product_id_placeholder": "Skriv inn produkt-ID",
"usb_config_product_name_label": "Produktnavn",
"usb_config_product_name_placeholder": "Skriv inn produktnavn",
"usb_config_restore_default": "Gjenopprett til standard",
"usb_config_serial_number_label": "Serienummer",
"usb_config_serial_number_placeholder": "Skriv inn serienummeret",
"usb_config_set_success": "USB-konfigurasjon satt til {manufacturer} {product}",
"usb_config_update_identifiers": "Oppdater USB-identifikatorer",
"usb_config_vendor_id_label": "Leverandør-ID",
"usb_config_vendor_id_placeholder": "Skriv inn leverandør-ID",
"usb_device_classes_description": "USB-enhetsklasser i den sammensatte enheten",
"usb_device_classes_title": "Klasser",
"usb_device_custom": "Tilpasset",
"usb_device_description": "USB-enheter som skal emuleres på måldatamaskinen",
"usb_device_enable_absolute_mouse_description": "Aktiver absolutt mus (peker)",
"usb_device_enable_absolute_mouse_title": "Aktiver absolutt mus (peker)",
"usb_device_enable_keyboard_description": "Aktiver tastatur",
"usb_device_enable_keyboard_title": "Aktiver tastatur",
"usb_device_enable_mass_storage_description": "Noen ganger må det kanskje deaktiveres for å forhindre problemer med visse enheter.",
"usb_device_enable_mass_storage_title": "Aktiver USB-masselagring",
"usb_device_enable_relative_mouse_description": "Aktiver relativ mus",
"usb_device_enable_relative_mouse_title": "Aktiver relativ mus",
"usb_device_failed_load": "Klarte ikke å laste inn USB-enheter: {error}",
"usb_device_failed_set": "Kunne ikke angi USB-enheter: {error}",
"usb_device_keyboard_mouse_and_mass_storage": "Tastatur, mus og masselagring",
"usb_device_keyboard_only": "Kun tastatur",
"usb_device_restore_default": "Gjenopprett til standard",
"usb_device_title": "USB-enhet",
"usb_device_update_classes": "Oppdater USB-klasser",
"usb_device_updated": "USB-enheter oppdatert",
"usb_state_connected": "Tilkoblet",
"usb_state_connecting": "Tilkobling",
"usb_state_disconnected": "Frakoblet",
"usb_state_low_power_mode": "Lavstrømsmodus",
"user_interface_language_description": "Velg språket som skal brukes i JetKVM-brukergrensesnittet",
"user_interface_language_title": "Grensesnittspråk",
"video_brightness_description": "Lysstyrkenivå ( {value} x)",
"video_brightness_title": "Lysstyrke",
"video_contrast_description": "Kontrastnivå ( {value} x)",
"video_contrast_title": "Kontrast",
"video_custom_edid_description": "EDID beskriver kompatibilitet med videomodus. Standardinnstillingene fungerer i de fleste tilfeller, men unike UEFI/BIOS-innstillinger kan trenge justeringer.",
"video_custom_edid_title": "Tilpasset EDID",
"video_debugging_info_description": "Feilsøkingsinformasjon for video",
"video_debugging_info_title": "Feilsøkingsinformasjon",
"video_description": "Konfigurer skjerminnstillinger og EDID for optimal kompatibilitet",
"video_edid_acer_b246wl": "Acer B246WL, 1920x1200",
"video_edid_asus_pa248qv": "ASUS PA248QV, 1920x1200",
"video_edid_custom": "Tilpasset",
"video_edid_dell_d2721h": "DELL D2721H, 1920x1080",
"video_edid_dell_idrac": "DELL IDRAC EDID, 1280x1024",
"video_edid_description": "Juster EDID-innstillingene for skjermen",
"video_edid_file_label": "EDID-fil",
"video_edid_jetkvm_default": "JetKVM-standard",
"video_edid_set_success": "EDID satt til {edid}",
"video_edid_title": "EDID",
"video_enhancement_description": "Juster fargeinnstillingene for å gjøre videoutgangen mer levende og fargerik",
"video_enhancement_title": "Videoforbedring",
"video_failed_get_debug_info": "Klarte ikke å hente feilsøkingsinformasjon: {error}",
"video_failed_get_edid": "Klarte ikke å hente EDID: {error}",
"video_failed_set_edid": "Klarte ikke å angi EDID: {error}",
"video_failed_set_stream_quality": "Kunne ikke angi strømkvalitet: {error}",
"video_get_debugging_info": "Få feilsøkingsinformasjon",
"video_overlay_autoplay_permissions_required": "Autoavspillingstillatelser kreves",
"video_overlay_conn_check_cables": "Sjekk alle kabeltilkoblinger for løse eller skadede ledninger",
"video_overlay_conn_ensure_network": "Sørg for at nettverkstilkoblingen din er stabil og aktiv",
"video_overlay_conn_restart": "Prøv å starte både enheten og datamaskinen på nytt",
"video_overlay_conn_verify_power": "Kontroller at enheten er slått på og riktig tilkoblet",
"video_overlay_connection_issue_title": "Tilkoblingsproblem oppdaget",
"video_overlay_enable_autoplay_settings": "Vennligst juster nettleserinnstillingene for å aktivere autoavspilling",
"video_overlay_hdmi_error_title": "HDMI-signalfeil oppdaget.",
"video_overlay_hdmi_incompatible_resolution": "Inkompatible innstillinger for oppløsning eller oppdateringsfrekvens",
"video_overlay_hdmi_loose_faulty": "En løs eller defekt HDMI-tilkobling",
"video_overlay_hdmi_source_issue": "Problemer med kildeenhetens HDMI-utgang",
"video_overlay_learn_more": "Lær mer",
"video_overlay_loading_stream": "Laster inn videostrøm …",
"video_overlay_manually_start_stream": "Start strømmen manuelt",
"video_overlay_no_hdmi_adapter_compat": "Hvis du bruker en adapter, må du sørge for at den er kompatibel og fungerer som den skal.",
"video_overlay_no_hdmi_ensure_cable": "Sørg for at HDMI-kabelen er ordentlig koblet til i begge ender",
"video_overlay_no_hdmi_ensure_power": "Sørg for at kildeenheten er slått på og sender ut et signal",
"video_overlay_no_hdmi_signal": "Ingen HDMI-signal oppdaget.",
"video_overlay_pointerlock_click_to_enable": "Klikk på videoen for å aktivere musekontroll",
"video_overlay_retrying_connection": "Prøver tilkobling på nytt …",
"video_overlay_troubleshooting_guide": "Feilsøkingsveiledning",
"video_overlay_try_again": "Prøv igjen",
"video_pointer_lock_disabled": "Pekerlås deaktivert",
"video_pointer_lock_enabled": "Pekerlås aktivert trykk Escape for å låse opp",
"video_quality_high": "Høy",
"video_quality_low": "Lav",
"video_quality_medium": "Medium",
"video_reset_to_default": "Tilbakestill til standard",
"video_restore_to_default": "Gjenopprett til standard",
"video_saturation_description": "Fargemetning ( {value} x)",
"video_saturation_title": "Metning",
"video_set_custom_edid": "Angi tilpasset EDID",
"video_stream_quality_description": "Juster kvaliteten på videostrømmen",
"video_stream_quality_set": "Strømkvalitet satt til {quality}",
"video_stream_quality_title": "Strømmekvalitet",
"video_title": "Video",
"view_details": "Vis detaljer",
"virtual_keyboard_header": "Virtuelt tastatur",
"wake_on_lan": "Vekk på LAN",
"wake_on_lan_add_device_device_name": "Enhetsnavn",
"wake_on_lan_add_device_example_device_name": "Plex Media Server",
"wake_on_lan_add_device_mac_address": "MAC-adresse",
"wake_on_lan_add_device_save_device": "Lagre enhet",
"wake_on_lan_description": "Send en magisk pakke for å vekke en ekstern enhet.",
"wake_on_lan_device_list_add_new_device": "Legg til ny enhet",
"wake_on_lan_device_list_delete_device": "Slett enhet",
"wake_on_lan_device_list_wake": "Våkne",
"wake_on_lan_empty_add_device_to_start": "Legg til en enhet for å begynne å bruke Wake-on-LAN",
"wake_on_lan_empty_add_new_device": "Legg til ny enhet",
"wake_on_lan_empty_no_devices_added": "Ingen enheter lagt til",
"wake_on_lan_failed_add_device": "Kunne ikke legge til enhet",
"wake_on_lan_failed_send_magic": "Kunne ikke sende magisk pakke",
"wake_on_lan_invalid_mac": "Ugyldig MAC-adresse",
"wake_on_lan_magic_sent_success": "Magisk pakke sendt",
"welcome_to_jetkvm": "Velkommen til JetKVM",
"welcome_to_jetkvm_description": "Kontroller hvilken som helst datamaskin eksternt"
}

View File

@ -0,0 +1,895 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"access_adopt_kvm": "Använd KVM i molnet",
"access_adopted_message": "Din enhet är ansluten till molnet",
"access_auth_mode_no_password": "Nuvarande läge: Inget lösenord",
"access_auth_mode_password": "Nuvarande läge: Lösenordsskyddat",
"access_authentication_mode_title": "Autentiseringsläge",
"access_certificate_label": "Certifikat",
"access_change_password_button": "Ändra lösenord",
"access_change_password_description": "Uppdatera ditt lösenord för åtkomst till enheten",
"access_change_password_title": "Ändra lösenord",
"access_cloud_api_url_label": "Cloud API-URL",
"access_cloud_app_url_label": "URL för molnapplikation",
"access_cloud_provider_description": "Välj molnleverantör för din enhet",
"access_cloud_provider_title": "Molnleverantör",
"access_cloud_security_title": "Molnsäkerhet",
"access_confirm_deregister": "Är du säker på att du vill avregistrera den här enheten?",
"access_deregister": "Avregistrera dig från molnet",
"access_description": "Hantera enhetens åtkomstkontroll",
"access_disable_protection": "Inaktivera skydd",
"access_enable_password": "Aktivera lösenord",
"access_failed_deregister": "Misslyckades med att avregistrera enheten: {error}",
"access_failed_update_cloud_url": "Misslyckades med att uppdatera moln-URL: {error}",
"access_failed_update_tls": "Misslyckades med att uppdatera TLS-inställningarna: {error}",
"access_github_link": "GitHub",
"access_https_description": "Konfigurera säker HTTPS-åtkomst till din enhet",
"access_https_mode_title": "HTTPS-läge",
"access_learn_security": "Läs mer om vår molnsäkerhet",
"access_local_description": "Hantera läget för lokal åtkomst till enheten",
"access_local_title": "Lokal",
"access_no_device_id": "Inget enhets-ID tillgängligt",
"access_private_key_description": "Av säkerhetsskäl kommer den inte att visas efter att den har sparats.",
"access_private_key_label": "Privat nyckel",
"access_provider_custom": "Anpassad",
"access_provider_jetkvm": "JetKVM-molnet",
"access_remote_description": "Hantera läget för fjärråtkomst till enheten",
"access_security_encryption": "End-to-end-kryptering med WebRTC (DTLS och SRTP)",
"access_security_oidc": "OIDC-autentisering (OpenID Connect)",
"access_security_open_source": "Alla molnkomponenter är öppen källkod och tillgängliga på GitHub.",
"access_security_streams": "Alla strömmar krypterade under överföring",
"access_security_zero_trust": "Zero Trust-säkerhetsmodell",
"access_title": "Tillträde",
"access_tls_certificate_description": "Klistra in ditt TLS-certifikat nedan. För certifikatkedjor, inkludera hela kedjan (löv-, mellan- och rotcertifikat).",
"access_tls_certificate_title": "TLS-certifikat",
"access_tls_custom": "Anpassad",
"access_tls_disabled": "Inaktiverad",
"access_tls_self_signed": "Självsignerad",
"access_tls_updated": "TLS-inställningarna har uppdaterats",
"access_update_tls_settings": "Uppdatera TLS-inställningar",
"action_bar_connection_stats": "Anslutningsstatistik",
"action_bar_extension": "Förlängning",
"action_bar_fullscreen": "Helskärm",
"action_bar_settings": "Inställningar",
"action_bar_virtual_keyboard": "Virtuellt tangentbord",
"action_bar_virtual_media": "Virtuella medier",
"action_bar_wake_on_lan": "Vakna på LAN",
"action_bar_web_terminal": "Webbterminal",
"advanced_description": "Få åtkomst till ytterligare inställningar för felsökning och anpassning",
"advanced_dev_channel_description": "Få tidiga uppdateringar från utvecklingskanalen",
"advanced_dev_channel_title": "Uppdateringar av utvecklarkanaler",
"advanced_developer_mode_description": "Aktivera avancerade funktioner för utvecklare",
"advanced_developer_mode_enabled_title": "Utvecklarläge aktiverat",
"advanced_developer_mode_title": "Utvecklarläge",
"advanced_developer_mode_warning_advanced": "Endast för avancerade användare. Ej för produktionsbruk.",
"advanced_developer_mode_warning_risks": "Använd endast om du förstår riskerna",
"advanced_developer_mode_warning_security": "Säkerheten försvagas medan den är aktiv",
"advanced_disable_usb_emulation": "Inaktivera USB-emulering",
"advanced_enable_usb_emulation": "Aktivera USB-emulering",
"advanced_error_loopback_disable": "Misslyckades med att inaktivera endast loopback-läge: {error}",
"advanced_error_loopback_enable": "Misslyckades med att aktivera endast loopback-läge: {error}",
"advanced_error_reset_config": "Misslyckades med att återställa konfigurationen: {error}",
"advanced_error_set_dev_channel": "Misslyckades med att ställa in status för utvecklarkanalen: {error}",
"advanced_error_set_dev_mode": "Misslyckades med att ställa in utvecklarläge: {error}",
"advanced_error_update_ssh_key": "Misslyckades med att uppdatera SSH-nyckeln: {error}",
"advanced_error_usb_emulation_disable": "Misslyckades med att inaktivera USB-emulering: {error}",
"advanced_error_usb_emulation_enable": "Misslyckades med att aktivera USB-emulering: {error}",
"advanced_loopback_only_description": "Begränsa åtkomst till webbgränssnittet endast till lokal värd (127.0.0.1)",
"advanced_loopback_only_title": "Endast loopback-läge",
"advanced_loopback_warning_before": "Innan du aktiverar den här funktionen, se till att du har antingen:",
"advanced_loopback_warning_cloud": "Molnåtkomst aktiverad och fungerar",
"advanced_loopback_warning_confirm": "Jag förstår, aktivera ändå",
"advanced_loopback_warning_description": "VARNING: Detta begränsar åtkomsten till webbgränssnittet endast till localhost (127.0.0.1).",
"advanced_loopback_warning_ssh": "SSH-åtkomst konfigurerad och testad",
"advanced_loopback_warning_title": "Aktivera endast loopback-läge?",
"advanced_reset_config_button": "Återställ konfiguration",
"advanced_reset_config_description": "Återställ konfigurationen till standard. Detta loggar ut dig.",
"advanced_reset_config_title": "Återställ konfigurationen",
"advanced_ssh_access_description": "Lägg till din offentliga SSH-nyckel för att aktivera säker fjärråtkomst till enheten",
"advanced_ssh_access_title": "SSH-åtkomst",
"advanced_ssh_default_user": "Standard SSH-användaren är",
"advanced_ssh_public_key_label": "SSH-publik nyckel",
"advanced_ssh_public_key_placeholder": "Ange din offentliga SSH-nyckel",
"advanced_success_loopback_disabled": "Endast loopback-läge inaktiverat. Starta om enheten för att tillämpa det.",
"advanced_success_loopback_enabled": "Endast loopback-läge aktiverat. Starta om enheten för att tillämpa.",
"advanced_success_reset_config": "Konfigurationen återställdes till standardinställningarna",
"advanced_success_update_ssh_key": "SSH-nyckeln har uppdaterats",
"advanced_title": "Avancerad",
"advanced_troubleshooting_mode_description": "Diagnostikverktyg och ytterligare kontroller för felsökning och utvecklingsändamål",
"advanced_troubleshooting_mode_title": "Felsökningsläge",
"advanced_update_ssh_key_button": "Uppdatera SSH-nyckel",
"advanced_usb_emulation_description": "Kontrollera USB-emuleringsstatusen",
"advanced_usb_emulation_title": "USB-emulering",
"already_adopted_new_owner": "Om du är den nya ägaren ber du den tidigare ägaren att avregistrera enheten från sitt konto i molnöversikten. Om du tror att detta är ett fel kan du kontakta vårt supportteam för hjälp.",
"already_adopted_other_user": "Den här enheten är för närvarande registrerad till en annan användare i vår molnpanel.",
"already_adopted_return_to_dashboard": "Återgå till instrumentpanelen",
"already_adopted_title": "Enheten är redan registrerad",
"appearance_description": "Välj ditt önskade färgtema",
"appearance_page_description": "Anpassa utseendet och känslan hos ditt JetKVM-gränssnitt",
"appearance_theme": "Tema",
"appearance_theme_dark": "Mörk",
"appearance_theme_light": "Ljus",
"appearance_theme_system": "System",
"appearance_title": "Utseende",
"attach": "Bifoga",
"atx_power_control_get_state_error": "Misslyckades med att hämta ATX-strömstatus: {error}",
"atx_power_control_hdd_led": "Hårddisk-LED",
"atx_power_control_long_power_button": "Långt tryck",
"atx_power_control_power_button": "Driva",
"atx_power_control_power_led": "Ström-LED",
"atx_power_control_reset_button": "Återställa",
"atx_power_control_send_action_error": "Misslyckades med att skicka ATX-strömåtgärd {action} : {error}",
"atx_power_control_short_power_button": "Kort tryck",
"auth_authentication_mode": "Välj ett autentiseringsläge",
"auth_authentication_mode_error": "Ett fel uppstod när autentiseringsläget ställdes in",
"auth_authentication_mode_invalid": "Ogiltigt autentiseringsläge",
"auth_connect_to_cloud": "Anslut din JetKVM till molnet",
"auth_connect_to_cloud_action": "Logga in och anslut enheten",
"auth_connect_to_cloud_description": "Lås upp fjärråtkomst och avancerade funktioner för din enhet",
"auth_header_cta_already_have_account": "Har du redan ett konto?",
"auth_header_cta_dont_have_account": "Har du inget konto?",
"auth_header_cta_new_to_jetkvm": "Nybörjare på JetKVM?",
"auth_login": "Logga in på ditt JetKVM-konto",
"auth_login_action": "Logga in",
"auth_login_description": "Logga in för att få åtkomst till och hantera dina enheter säkert",
"auth_mode_local": "Lokal autentiseringsmetod",
"auth_mode_local_change_later": "Du kan alltid ändra din autentiseringsmetod senare i inställningarna.",
"auth_mode_local_description": "Välj hur du vill säkra din JetKVM-enhet lokalt.",
"auth_mode_local_no_password": "Inget lösenord",
"auth_mode_local_no_password_description": "Snabb åtkomst utan lösenordsautentisering.",
"auth_mode_local_password": "Lösenord",
"auth_mode_local_password_confirm_description": "Bekräfta ditt lösenord",
"auth_mode_local_password_confirm_label": "Bekräfta lösenord",
"auth_mode_local_password_description": "Säkra din enhet med ett lösenord för extra skydd.",
"auth_mode_local_password_failed_set": "Misslyckades med att ange lösenord: {error}",
"auth_mode_local_password_note": "Detta lösenord kommer att användas för att säkra dina enhetsdata och skydda mot obehörig åtkomst.",
"auth_mode_local_password_note_local": "All data finns kvar på din lokala enhet.",
"auth_mode_local_password_set": "Ange ett lösenord",
"auth_mode_local_password_set_button": "Ange lösenord",
"auth_mode_local_password_set_description": "Skapa ett starkt lösenord för att säkra din JetKVM-enhet lokalt.",
"auth_mode_local_password_set_label": "Ange ett lösenord",
"auth_signup_connect_to_cloud_action": "Registrera och anslut enhet",
"auth_signup_create_account": "Skapa ditt JetKVM-konto",
"auth_signup_create_account_action": "Skapa konto",
"auth_signup_create_account_description": "Skapa ditt konto och börja enkelt hantera dina enheter.",
"back": "Tillbaka",
"back_to_devices": "Tillbaka till Enheter",
"cancel": "Avbryt",
"close": "Stäng",
"cloud_kvms": "Moln-KVM:er",
"cloud_kvms_description": "Hantera dina moln-KVM:er och anslut till dem säkert.",
"cloud_kvms_no_devices": "Inga enheter hittades",
"cloud_kvms_no_devices_description": "Du har inga enheter med aktiverat JetKVM Cloud ännu.",
"confirm": "Bekräfta",
"connect_to_kvm": "Anslut till KVM",
"connecting_to_device": "Ansluter till enhet…",
"connection_established": "Anslutning upprättad",
"connection_stats_badge_jitter": "Jitter",
"connection_stats_badge_jitter_buffer_avg_delay": "Genomsnittlig fördröjning för jitterbuffert",
"connection_stats_connection": "Förbindelse",
"connection_stats_connection_description": "Anslutningen mellan klienten och JetKVM:n.",
"connection_stats_frames_per_second": "Bildrutor per sekund",
"connection_stats_frames_per_second_description": "Antal inkommande videobildrutor som visas per sekund.",
"connection_stats_network_stability": "Nätverksstabilitet",
"connection_stats_network_stability_description": "Hur jämnt flödet av inkommande videopaket är över nätverket.",
"connection_stats_packets_lost": "Paket förlorade",
"connection_stats_packets_lost_description": "Antal förlorade inkommande RTP-videopaket.",
"connection_stats_playback_delay": "Uppspelningsfördröjning",
"connection_stats_playback_delay_description": "Fördröjning som läggs till av jitterbufferten för att jämna ut uppspelningen när bildrutor anländer ojämnt.",
"connection_stats_round_trip_time": "Tur- och returtid",
"connection_stats_round_trip_time_description": "Tur- och returtid för det aktiva ICE-kandidatparet mellan peers.",
"connection_stats_sidebar": "Anslutningsstatistik",
"connection_stats_unit_frames_per_second": " fps",
"connection_stats_unit_milliseconds": " ms",
"connection_stats_unit_packets": " paket",
"connection_stats_video": "Video",
"connection_stats_video_description": "Videoströmmen från JetKVM till klienten.",
"continue": "Fortsätta",
"creating_peer_connection": "Skapar peer-kontakt…",
"dc_power_control_current": "Nuvarande",
"dc_power_control_current_unit": "En",
"dc_power_control_get_state_error": "Misslyckades med att hämta likströmsstatus: {error}",
"dc_power_control_power": "Driva",
"dc_power_control_power_off_button": "Stäng av",
"dc_power_control_power_off_state": "Stäng av",
"dc_power_control_power_on_button": "Slå på",
"dc_power_control_power_on_state": "Slå på",
"dc_power_control_power_unit": "V",
"dc_power_control_restore_last_state": "Senaste delstaten",
"dc_power_control_restore_power_state": "Återställ strömförlust",
"dc_power_control_set_power_state_error": "Misslyckades med att skicka likströmsstatus till {enabled} : {error}",
"dc_power_control_set_restore_state_error": "Misslyckades med att skicka återställningsstatus för likström till {state} : {error}",
"dc_power_control_voltage": "Spänning",
"dc_power_control_voltage_unit": "V",
"delete": "Radera",
"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.",
"deregister_from_cloud": "Avregistrera dig från molnet",
"deregister_headline": "Avregistrera {device} från ditt molnkonto",
"detach": "Lösgöra",
"dhcp_empty_lease_description": "Vi har inte mottagit någon DHCP-leaseinformation från enheten ännu.",
"dhcp_empty_lease_headline": "Ingen DHCP-leaseinformation",
"dhcp_lease_boot_file": "Startfil",
"dhcp_lease_boot_next_server": "Starta nästa server",
"dhcp_lease_boot_server_name": "Namn på startserver",
"dhcp_lease_broadcast": "Utsända",
"dhcp_lease_domain": "Domän",
"dhcp_lease_gateway": "Inkörsport",
"dhcp_lease_header": "DHCP-leasinginformation",
"dhcp_lease_hostname": "Värdnamn",
"dhcp_lease_lease_expires": "Hyresavtalet löper ut",
"dhcp_lease_maximum_transfer_unit": "MTU",
"dhcp_lease_renew": "Förnya DHCP-lease",
"dhcp_lease_time_to_live": "TTL",
"dhcp_server": "DHCP-server",
"dns_servers": "DNS-servrar",
"establishing_secure_connection": "Upprättar säker anslutning…",
"experimental": "Experimentell",
"extension_popover_load_and_manage_extensions": "Ladda och hantera dina tillägg",
"extension_popover_set_error_notification": "Misslyckades med att ange aktivt tillägg: {error}",
"extension_popover_unload_extension": "Avlasta tillägg",
"extension_serial_console": "Seriell konsol",
"extension_serial_console_description": "Åtkomst till din seriella konsoltillägg",
"extensions_atx_power_control": "ATX-strömkontroll",
"extensions_atx_power_control_description": "Styr din maskins strömförsörjning via ATX-strömkontroll.",
"extensions_dc_power_control": "DC-strömstyrning",
"extensions_dc_power_control_description": "Styr din DC-strömförlängning",
"extensions_popover_extensions": "Tillägg",
"gathering_ice_candidates": "Samlar ICE-kandidater…",
"general_app_version": "App: {version}",
"general_auto_update_description": "Uppdatera enheten automatiskt till den senaste versionen",
"general_auto_update_error": "Misslyckades med att ställa in automatisk uppdatering: {error}",
"general_auto_update_title": "Automatisk uppdatering",
"general_check_for_updates": "Kontrollera efter uppdateringar",
"general_page_description": "Konfigurera enhetsinställningar och uppdatera inställningar",
"general_reboot_description": "Vill du fortsätta med att starta om systemet?",
"general_reboot_device": "Starta om enheten",
"general_reboot_device_description": "Stäng av och på JetKVM:en",
"general_reboot_no_button": "Inga",
"general_reboot_title": "Starta om JetKVM",
"general_reboot_yes_button": "Ja",
"general_system_version": "System: {version}",
"general_title": "Allmän",
"general_update_app_update_title": "Appuppdatering",
"general_update_application_type": "App",
"general_update_available_description": "En ny uppdatering är tillgänglig för att förbättra systemets prestanda och kompatibilitet. Vi rekommenderar att du uppdaterar för att säkerställa att allt fungerar smidigt.",
"general_update_available_title": "Uppdatering tillgänglig",
"general_update_background_button": "Uppdatera i bakgrunden",
"general_update_check_again_button": "Kontrollera igen",
"general_update_checking_description": "Vi ser till att din enhet har de senaste funktionerna och förbättringarna.",
"general_update_checking_title": "Söker efter uppdateringar…",
"general_update_completed_description": "Din enhet har uppdaterats till den senaste versionen. Njut av de nya funktionerna och förbättringarna!",
"general_update_completed_title": "Uppdateringen är slutförd",
"general_update_error_description": "Ett fel uppstod när enheten uppdaterades. Försök igen senare.",
"general_update_error_details": "Felinformation: {errorMessage}",
"general_update_error_title": "Uppdateringsfel",
"general_update_later_button": "Gör det senare",
"general_update_now_button": "Uppdatera nu",
"general_update_rebooting": "Startar om för att slutföra uppdateringen…",
"general_update_status_awaiting_reboot": "Väntar på omstart",
"general_update_status_downloading": "Laddar ner {update_type} uppdatering…",
"general_update_status_fetching": "Hämtar uppdateringsinformation…",
"general_update_status_installing": "Installerar {update_type} uppdatering…",
"general_update_status_progress": "{part} framsteg",
"general_update_status_verifying": "Verifierar {update_type} uppdatering…",
"general_update_system_type": "System",
"general_update_system_update_title": "Linux-systemuppdatering",
"general_update_up_to_date_description": "Ditt system kör den senaste versionen. Inga uppdateringar finns tillgängliga för närvarande.",
"general_update_up_to_date_title": "Systemet är uppdaterat",
"general_update_updating_description": "Stäng inte av enheten. Den här processen kan ta några minuter.",
"general_update_updating_title": "Uppdaterar din enhet",
"getting_remote_session_description": "Hämtar beskrivning av fjärrsession försök {attempt}",
"hardware_backlight_settings_error": "Misslyckades med att ställa in bakgrundsbelysning: {error}",
"hardware_backlight_settings_get_error": "Misslyckades med att hämta inställningar för bakgrundsbelysning: {error}",
"hardware_backlight_settings_success": "Bakgrundsbelysningsinställningarna har uppdaterats",
"hardware_dim_display_after_description": "Ställ in hur länge det ska vänta innan displayen dimmas",
"hardware_dim_display_after_title": "Dimma displayen efter",
"hardware_display_brightness_description": "Ställ in skärmens ljusstyrka",
"hardware_display_brightness_high": "Hög",
"hardware_display_brightness_low": "Låg",
"hardware_display_brightness_medium": "Medium",
"hardware_display_brightness_off": "Av",
"hardware_display_brightness_title": "Skärmens ljusstyrka",
"hardware_display_orientation_description": "Ställ in skärmens orientering",
"hardware_display_orientation_error": "Misslyckades med att ställa in visningsorientering: {error}",
"hardware_display_orientation_inverted": "Omvänd",
"hardware_display_orientation_normal": "Normal",
"hardware_display_orientation_success": "Skärmorientering har uppdaterats",
"hardware_display_orientation_title": "Skärmorientering",
"hardware_display_wake_up_note": "Skärmen vaknar när anslutningsstatusen ändras eller när den berörs.",
"hardware_page_description": "Konfigurera skärminställningar och maskinvarualternativ för din JetKVM-enhet",
"hardware_power_saving_description": "Minska strömförbrukningen när den inte används",
"hardware_power_saving_disabled": "Energisparläge inaktiverat",
"hardware_power_saving_enabled": "Energisparläge aktiverat",
"hardware_power_saving_failed_error": "Misslyckades med att ställa in energisparläge: {error}",
"hardware_power_saving_hdmi_sleep_description": "Stäng av inspelning efter 90 sekunders inaktivitet",
"hardware_power_saving_hdmi_sleep_title": "HDMI-viloläge",
"hardware_power_saving_title": "Energisparande",
"hardware_time_10_minutes": "10 minuter",
"hardware_time_1_hour": "1 timme",
"hardware_time_1_minute": "1 minut",
"hardware_time_30_minutes": "30 minuter",
"hardware_time_5_minutes": "5 minuter",
"hardware_time_never": "Aldrig",
"hardware_title": "Hårdvara",
"hardware_turn_off_display_after_description": "Period av inaktivitet innan skärmen stängs av automatiskt",
"hardware_turn_off_display_after_title": "Stäng av skärmen efter",
"hide": "Dölja",
"ice_gathering_completed": "ICE-insamlingen är klar",
"info_caps_lock": "Caps Lock",
"info_compose": "Komponera",
"info_hdmi_state": "HDMI-tillstånd:",
"info_hidrpc_state": "HidRPC-tillstånd:",
"info_kana": "Kana",
"info_keys": "Nycklar:",
"info_last_move": "Senaste drag:",
"info_num_lock": "Num Lock",
"info_paste_enabled": "Aktiverad",
"info_paste_mode": "Klistra in-läge:",
"info_pointer": "Pekare:",
"info_relayed_by_cloudflare": "Vidarebefordras av Cloudflare",
"info_resolution": "Upplösning:",
"info_scroll_lock": "Scroll Lock",
"info_shift": "Flytta",
"info_usb_state": "USB-status:",
"info_video_size": "Videostorlek:",
"input_disabled": "Inmatning inaktiverad",
"invalid_password": "Ogiltigt lösenord",
"ip_address": "IP-adress",
"ipv6_address_label": "Adress",
"ipv6_gateway": "Inkörsport",
"ipv6_information": "IPv6-information",
"ipv6_link_local": "Länklokal",
"ipv6_preferred_lifetime": "Föredragen livslängd",
"ipv6_valid_lifetime": "Giltig livstids",
"jetkvm_description": "JetKVM kombinerar kraftfull hårdvara med intuitiv programvara för att ge en sömlös fjärrstyrningsupplevelse.",
"jetkvm_device": "JetKVM-enhet",
"jetkvm_logo": "JetKVM-logotyp",
"jetkvm_setup": "Konfigurera din JetKVM",
"jiggler_cron_schedule_description": "Cron-uttryck för schemaläggning",
"jiggler_cron_schedule_label": "Cron-schema",
"jiggler_example_business_hours_early": "Öppettider 8-17",
"jiggler_example_business_hours_late": "Öppettider 9-17",
"jiggler_examples_label": "Exempel",
"jiggler_inactivity_limit_description": "Inaktivitetstid före skakning",
"jiggler_inactivity_limit_label": "Inaktivitetsgräns i sekunder",
"jiggler_more_examples": "Fler exempel",
"jiggler_random_delay_description": "För att undvika igenkännbara mönster",
"jiggler_random_delay_label": "Slumpmässig fördröjning",
"jiggler_save_jiggler_config": "Spara Jiggler-konfiguration",
"jiggler_timezone_description": "Tidszon för cron-schema",
"jiggler_timezone_label": "Tidszon",
"keyboard_description": "Konfigurera tangentbordsinställningar för din enhet",
"keyboard_layout_description": "Tangentbordslayout för måloperativsystemet",
"keyboard_layout_error": "Misslyckades med att ställa in tangentbordslayout: {error}",
"keyboard_layout_long_description": "Det virtuella tangentbordet, textklistringen och tangentbordsmakron skickar individuella tangenttryckningar till målenheten. Tangentbordslayouten avgör vilka tangentkoder som skickas. Se till att tangentbordslayouten i JetKVM matchar inställningarna i operativsystemet.",
"keyboard_layout_success": "Tangentbordslayouten har ställts in på {layout}",
"keyboard_layout_title": "Tangentbordslayout",
"keyboard_show_pressed_keys_description": "Visa tangenterna som för närvarande är nedtryckta i statusfältet",
"keyboard_show_pressed_keys_title": "Visa nedtryckta tangenter",
"keyboard_title": "Tangentbord",
"kvm_terminal": "KVM-terminal",
"last_online": "Senast online {time}",
"learn_more": "Läs mer",
"load": "Ladda",
"loading": "Belastning…",
"local_auth_change_local_device_password_description": "Ange ditt nuvarande lösenord och ett nytt lösenord för att uppdatera ditt lokala enhetsskydd.",
"local_auth_change_local_device_password_title": "Ändra lösenord för lokal enhet",
"local_auth_confirm_new_password_label": "Bekräfta nytt lösenord",
"local_auth_create_confirm_password_placeholder": "Ange ditt lösenord igen",
"local_auth_create_description": "Skapa ett lösenord för att skydda din enhet från obehörig lokal åtkomst.",
"local_auth_create_new_password_label": "Nytt lösenord",
"local_auth_create_new_password_placeholder": "Ange ett starkt lösenord",
"local_auth_create_not_now_button": "Inte nu",
"local_auth_create_secure_button": "Säkra enheten",
"local_auth_create_title": "Lokalt enhetsskydd",
"local_auth_current_password_label": "Nuvarande lösenord",
"local_auth_disable_local_device_protection_description": "Ange ditt nuvarande lösenord för att inaktivera lokalt enhetsskydd.",
"local_auth_disable_local_device_protection_title": "Inaktivera skydd för lokala enheter",
"local_auth_disable_protection_button": "Inaktivera skydd",
"local_auth_enter_current_password_placeholder": "Ange ditt nuvarande lösenord",
"local_auth_enter_new_password_placeholder": "Ange ett nytt starkt lösenord",
"local_auth_error_changing_password": "Ett fel uppstod när lösenordet ändrades",
"local_auth_error_disabling_password": "Ett fel uppstod när lösenordet inaktiverades",
"local_auth_error_enter_current_password": "Ange ditt nuvarande lösenord",
"local_auth_error_enter_new_password": "Ange ett nytt lösenord",
"local_auth_error_enter_old_password": "Ange ditt gamla lösenord",
"local_auth_error_enter_password": "Ange ett lösenord",
"local_auth_error_passwords_not_match": "Lösenorden matchar inte",
"local_auth_error_setting_password": "Ett fel uppstod när lösenordet ställdes in",
"local_auth_new_password_label": "Nytt lösenord",
"local_auth_reenter_new_password_placeholder": "Ange ditt nya lösenord igen",
"local_auth_success_password_disabled_description": "Du har inaktiverat lösenordsskyddet för lokal åtkomst. Kom ihåg att din enhet nu är mindre säker.",
"local_auth_success_password_disabled_title": "Lösenordsskydd inaktiverat",
"local_auth_success_password_set_description": "Du har konfigurerat lokalt enhetsskydd. Din enhet är nu skyddad mot obehörig lokal åtkomst.",
"local_auth_success_password_set_title": "Lösenordet har ställts in",
"local_auth_success_password_updated_description": "Du har ändrat ditt lösenord för lokal enhetsskydd. Se till att komma ihåg ditt nya lösenord för framtida åtkomst.",
"local_auth_success_password_updated_title": "Lösenordet har uppdaterats",
"local_auth_update_password_button": "Uppdatera lösenord",
"locale_auto": "Auto",
"locale_change_success": "Språket har ändrats till {locale}",
"locale_da": "Danska",
"locale_de": "Tyska",
"locale_en": "Engelska",
"locale_es": "Spanska",
"locale_fr": "Franska",
"locale_it": "Italienska",
"locale_nb": "Norska (bokmål)",
"locale_sv": "Svenska",
"locale_zh": "中文 (简体)",
"log_in": "Logga in",
"log_out": "Logga ut",
"logged_in_as": "Inloggad som",
"login_enter_password": "Ange ditt lösenord",
"login_enter_password_description": "Ange ditt lösenord för att komma åt din JetKVM.",
"login_error": "Ett fel uppstod vid inloggning",
"login_forgot_password": "Glömt lösenordet?",
"login_password_label": "Lösenord",
"login_welcome_back": "Välkommen tillbaka till JetKVM",
"macro_add_step": "Lägg till steg {maxed_out}",
"macro_at_least_one_step_keys_or_modifiers": "Minst ett steg måste ha nycklar eller modifierare",
"macro_at_least_one_step_required": "Minst ett steg krävs",
"macro_max_steps_error": "Du kan bara lägga till maximalt {max} steg per makro.",
"macro_max_steps_reached": "( {max} max)",
"macro_name_label": "Makronamn",
"macro_name_required": "Namn krävs",
"macro_name_too_long": "Namnet måste vara kortare än 50 tecken",
"macro_please_fix_validation_errors": "Vänligen åtgärda valideringsfelen",
"macro_save": "Spara makro",
"macro_save_failed": "Ett fel uppstod när dokumentet skulle sparas.",
"macro_save_failed_error": "Ett fel uppstod när dokumentet skulle sparas: {error}.",
"macro_step_count": "{steps} / {max} steg",
"macro_step_duration_description": "Dags att vänta innan nästa steg genomförs.",
"macro_step_duration_label": "Steglängd",
"macro_step_keys_description": "Maximalt antal {max} nycklar per steg.",
"macro_step_keys_label": "Nycklar",
"macro_step_max_keys_reached": "Maximalt antal nycklar uppnått",
"macro_step_modifiers_description": "Vilka modifierare (Shift/Ctrl/Alt/Meta) trycks ned under detta steg.",
"macro_step_modifiers_label": "Modifierare",
"macro_step_no_matching_keys_found": "Inga matchande nycklar hittades",
"macro_step_search_for_key": "Sök efter nyckel…",
"macro_steps_description": "Tangenter/modifierare exekveras i sekvens med en fördröjning mellan varje steg.",
"macro_steps_label": "Steg",
"macros_add_description": "Skapa ett nytt tangentbordsmakro",
"macros_add_new": "Lägg till nytt makro",
"macros_add_new_macro": "Lägg till nytt makro",
"macros_aria_add_new": "Lägg till nytt makro",
"macros_aria_delete": "Ta bort makro {name}",
"macros_aria_duplicate": "Duplicera makro {name}",
"macros_aria_edit": "Redigera makro {name}",
"macros_aria_move_down": "Flytta {name} nedåt",
"macros_aria_move_up": "Flytta {name} uppåt",
"macros_confirm_delete_description": "Är du säker på att du vill ta bort \" {name} \"? Den här åtgärden kan inte ångras.",
"macros_confirm_delete_title": "Ta bort makro",
"macros_confirm_deleting": "Tar bort…",
"macros_create_first_description": "Kombinera tangenttryckningar till en enda åtgärd",
"macros_create_first_headline": "Skapa ditt första makro",
"macros_created_success": "Makrot \" {name} \" skapades",
"macros_delay_only": "Endast fördröjning",
"macros_delete_confirm": "Är du säker på att du vill ta bort det här makrot? Åtgärden kan inte ångras.",
"macros_delete_macro": "Ta bort makro",
"macros_deleted_success": "Makrot \" {name} \" har raderats",
"macros_deleting": "Tar bort",
"macros_duplicated_success": "Makrot \" {name} \" duplicerades",
"macros_edit_button": "Redigera",
"macros_edit_description": "Ändra ditt tangentbordsmakro",
"macros_edit_title": "Redigera makro",
"macros_failed_create": "Misslyckades med att skapa makrot",
"macros_failed_create_error": "Misslyckades med att skapa makrot: {error}",
"macros_failed_delete": "Misslyckades med att ta bort makrot",
"macros_failed_delete_error": "Misslyckades med att ta bort makrot: {error}",
"macros_failed_duplicate": "Misslyckades med att duplicera makrot",
"macros_failed_duplicate_error": "Misslyckades med att duplicera makrot: {error}",
"macros_failed_reorder": "Misslyckades med att ändra ordningen på makrona",
"macros_failed_reorder_error": "Misslyckades med att ändra ordning på makron: {error}",
"macros_failed_update": "Misslyckades med att uppdatera makrot",
"macros_failed_update_error": "Misslyckades med att uppdatera makrot: {error}",
"macros_invalid_data": "Ogiltig makrodata",
"macros_loading": "Läser in makron…",
"macros_max_reached": "Max uppnått",
"macros_maximum_macros_reached": "Du har nått det maximala antalet tillåtna makron {maximum} .",
"macros_no_macros_available": "Inga makron tillgängliga",
"macros_order_updated": "Makroordningen har uppdaterats",
"macros_title": "Tangentbordsmakron",
"macros_updated_success": "Makrot \" {name} \" uppdaterades",
"metric_not_supported": "Måttvärden stöds inte",
"metric_waiting_for_data": "Väntar på data…",
"mount_add_file_to_get_started": "Lägg till en fil för att komma igång",
"mount_add_new_media": "Lägg till nytt media",
"mount_available_storage": "Tillgänglig lagring",
"mount_button_back_to_overview": "Tillbaka till översikten",
"mount_button_cancel_upload": "Avbryt uppladdning",
"mount_button_continue_upload": "Fortsätt uppladdningen",
"mount_button_mount_file": "Montera fil",
"mount_button_mount_url": "Monterings-URL",
"mount_button_select": "Välja",
"mount_button_showing_results": "Visar {from} till {to} av {total} resultat",
"mount_button_upload_new_image": "Ladda upp en ny bild",
"mount_bytes_free": "{bytesFree} ledig",
"mount_bytes_used": "{bytesUsed} används",
"mount_calculating": "Beräknande…",
"mount_click_to_select_file": "Klicka för att välja en fil",
"mount_click_to_select_incomplete": "Klicka för att välja \" {name} \"",
"mount_confirm_delete": "Är du säker på att du vill ta bort {name} ?",
"mount_continue_uploading_with_name": "Fortsätt ladda upp \" {name} \"",
"mount_error_delete_file": "Fel vid borttagning av fil: {error}",
"mount_error_description": "Ett fel uppstod när mediet skulle monteras. Försök igen.",
"mount_error_get_storage_space": "Fel vid hämtning av lagringsutrymme: {error}",
"mount_error_list_storage": "Fel vid lista av lagringsfiler: {error}",
"mount_error_title": "Monteringsfel",
"mount_get_state_error": "Misslyckades med att hämta virtuellt medietillstånd: {error}",
"mount_jetkvm_storage": "JetKVM-lagringsmontering",
"mount_jetkvm_storage_description": "Montera tidigare uppladdade filer från JetKVM-lagringen",
"mount_mode_cdrom": "CD/DVD",
"mount_mode_disk": "Disk",
"mount_mounted_as": "Monterad som",
"mount_mounted_from_storage": "Monterad från JetKVM-lagring",
"mount_no_images_description": "Ladda upp en bild för att starta montering av virtuell media.",
"mount_no_images_title": "Inga bilder tillgängliga",
"mount_no_mounted_media": "Inget monterat media",
"mount_percentage_used": "{percentageUsed} % använd",
"mount_please_select_file": "Välj filen \" {name} \" för att fortsätta uppladdningen.",
"mount_popular_images": "Populära bilder",
"mount_streaming_from_url": "Streaming från URL",
"mount_supported_formats": "Format som stöds: ISO, IMG",
"mount_unmount": "Avmontera",
"mount_unmount_error": "Misslyckades med att avmontera bilden: {error}",
"mount_upload_description": "Välj en bildfil att ladda upp till JetKVM-lagring",
"mount_upload_error": "Uppladdningsfel: {error}",
"mount_upload_failed_datachannel": "Misslyckades med att skapa datakanal för filuppladdning",
"mount_upload_failed_rtc": "Uppladdningen misslyckades: {error}",
"mount_upload_successful": "Uppladdningen lyckades",
"mount_upload_title": "Ladda upp ny bild",
"mount_uploaded_has_been_uploaded": "{name} har laddats upp",
"mount_uploading": "Laddar upp…",
"mount_uploading_with_name": "Laddar upp {name}",
"mount_url_description": "Montera filer från valfri offentlig webbadress",
"mount_url_input_label": "Bild-URL",
"mount_url_mount": "URL-montering",
"mount_view_device_description": "Välj en avbildning att montera från JetKVM-lagringen",
"mount_view_device_title": "Montera från JetKVM-lagring",
"mount_view_url_description": "Ange en URL till bildfilen som ska monteras",
"mount_view_url_title": "Montera från URL",
"mount_virtual_media": "Virtuella medier",
"mount_virtual_media_description": "Montera en avbildning för att starta från eller installera ett operativsystem.",
"mount_virtual_media_source": "Virtuell mediekälla",
"mount_virtual_media_source_description": "Välj hur du vill montera ditt virtuella media",
"mouse_alt_finger": "Finger som rör vid en skärm",
"mouse_alt_mouse": "Musikon",
"mouse_description": "Konfigurera markörens beteende och interaktionsinställningar för din enhet",
"mouse_hide_cursor_description": "Dölj markören när du skickar musrörelser",
"mouse_hide_cursor_title": "Dölj markören",
"mouse_jiggler_config_updated": "Jiggler-konfigurationen har uppdaterats",
"mouse_jiggler_custom": "Anpassad",
"mouse_jiggler_description": "Simulera rörelsen hos en datormus",
"mouse_jiggler_disabled": "Inaktiverad",
"mouse_jiggler_error_config": "Det uppstod ett fel vid konfiguration av jiggler",
"mouse_jiggler_failed_state": "Misslyckades med att ställa in jiggler-tillstånd: {error}",
"mouse_jiggler_frequent": "Frekvent - 30-talet",
"mouse_jiggler_invalid_cron": "Ogiltigt cron-uttryck. Kontrollera schemaformatet (t.ex. '0 * * * * *' för varje minut).",
"mouse_jiggler_light": "Ljus - 5m",
"mouse_jiggler_standard": "Standard - 1 m",
"mouse_jiggler_title": "Jiggler",
"mouse_mode_absolute": "Absolut",
"mouse_mode_absolute_description": "Mest bekvämt",
"mouse_mode_relative": "Relativ",
"mouse_mode_relative_description": "Mest kompatibla",
"mouse_modes_description": "Välj musinmatningsläge",
"mouse_modes_title": "Lägen",
"mouse_scroll_high": "Hög",
"mouse_scroll_low": "Låg",
"mouse_scroll_medium": "Medium",
"mouse_scroll_off": "Av",
"mouse_scroll_throttling_description": "Minska frekvensen av rullningshändelser",
"mouse_scroll_throttling_title": "Scrollbegränsning",
"mouse_scroll_very_high": "Mycket hög",
"mouse_title": "Mus",
"network_custom_domain": "Anpassad domän",
"network_description": "Konfigurera dina nätverksinställningar",
"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_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",
"network_dhcp_lease_renew_confirm_new_b": "du kan behöva återansluta med den nya adressen",
"network_dhcp_lease_renew_failed": "Misslyckades med att förnya leasingavtalet: {error}",
"network_dhcp_lease_renew_success": "DHCP-lease förnyad",
"network_domain_custom": "Anpassad",
"network_domain_description": "Nätverksdomänsuffix för enheten",
"network_domain_dhcp_provided": "DHCP tillhandahålls",
"network_domain_local": ".lokal",
"network_domain_title": "Domän",
"network_hostname_description": "Enhetsidentifierare i nätverket. Tomt för systemstandard",
"network_hostname_title": "Värdnamn",
"network_http_proxy_description": "Proxyserver för utgående HTTP(S)-förfrågningar från enheten. Tomt för inga.",
"network_http_proxy_invalid": "Ogiltig HTTP-proxy-URL",
"network_http_proxy_title": "HTTP-proxy",
"network_ipv4_address": "IPv4-adress",
"network_ipv4_dns": "IPv4 DNS",
"network_ipv4_gateway": "IPv4-gateway",
"network_ipv4_invalid": "Ogiltig IPv4-adress",
"network_ipv4_invalid_cidr": "Ogiltig CIDR-notation för IPv4-adress",
"network_ipv4_mode_description": "Konfigurera IPv4-läget",
"network_ipv4_mode_dhcp": "DHCP",
"network_ipv4_mode_static": "Statisk",
"network_ipv4_mode_title": "IPv4-läge",
"network_ipv4_netmask": "IPv4-nätmask",
"network_ipv6_addresses_header": "IPv6-adresser",
"network_ipv6_cidr_suggestion": "Använd CIDR-notation (t.ex. 2001:db8::1/64)",
"network_ipv6_dns": "IPv6 DNS",
"network_ipv6_flag_dad_failed": "DAD misslyckades",
"network_ipv6_flag_deprecated": "Föråldrad",
"network_ipv6_gateway": "IPv6-gateway",
"network_ipv6_information": "IPv6-information",
"network_ipv6_invalid": "Ogiltig IPv6-adress",
"network_ipv6_mode_description": "Konfigurera IPv6-läget",
"network_ipv6_mode_dhcpv6": "DHCPv6",
"network_ipv6_mode_disabled": "Inaktiverad",
"network_ipv6_mode_link_local": "Endast länklokal",
"network_ipv6_mode_slaac": "SLAAC",
"network_ipv6_mode_slaac_dhcpv6": "SLAAC + DHCPv6",
"network_ipv6_mode_static": "Statisk",
"network_ipv6_mode_title": "IPv6-läge",
"network_ipv6_prefix": "IP-prefix",
"network_ipv6_prefix_invalid": "Prefixet måste vara mellan 0 och 128",
"network_ll_dp_all": "Alla",
"network_ll_dp_basic": "Grundläggande",
"network_ll_dp_description": "Kontrollera vilka TLV:er som ska skickas via Link Layer Discovery Protocol",
"network_ll_dp_disabled": "Inaktiverad",
"network_ll_dp_title": "LLDP",
"network_mac_address_copy_error": "Misslyckades med att kopiera MAC-adressen",
"network_mac_address_copy_success": "MAC-adress { mac } kopierad till urklipp",
"network_mac_address_description": "Maskinvaruidentifierare för nätverksgränssnittet",
"network_mac_address_title": "MAC-adress",
"network_mdns_auto": "Auto",
"network_mdns_description": "Kontroll av mDNS (multicast DNS) driftläge",
"network_mdns_disabled": "Inaktiverad",
"network_mdns_ipv4_only": "Endast IPv4",
"network_mdns_ipv6_only": "Endast IPv6",
"network_mdns_title": "mDNS",
"network_no_information_description": "Ingen nätverkskonfiguration tillgänglig",
"network_no_information_headline": "Nätverksinformation",
"network_pending_dhcp_mode_change_description": "Spara inställningar för att aktivera DHCP-läge och visa leasinginformation",
"network_pending_dhcp_mode_change_headline": "Väntar på ändring av DHCP IPv4-läge",
"network_save_settings": "Spara inställningar",
"network_save_settings_apply_title": "Tillämpa nätverksinställningar",
"network_save_settings_confirm": "Tillämpa ändringar",
"network_save_settings_confirm_description": "Följande nätverksinställningar kommer att tillämpas. Dessa ändringar kan kräva en omstart och orsaka en kortvarig frånkoppling.",
"network_save_settings_confirm_heading": "Konfigurationsändringar",
"network_save_settings_failed": "Misslyckades med att spara nätverksinställningar: {error}",
"network_save_settings_success": "Nätverksinställningar sparade",
"network_settings_add_dns": "Lägg till DNS-server",
"network_settings_load_error": "Misslyckades med att läsa in nätverksinställningar: {error}",
"network_static_ipv4_header": "Statisk IPv4-konfiguration",
"network_static_ipv6_header": "Statisk IPv6-konfiguration",
"network_time_sync_description": "Konfigurera inställningar för tidssynkronisering",
"network_time_sync_http_only": "Endast HTTP",
"network_time_sync_ntp_and_http": "NTP och HTTP",
"network_time_sync_ntp_only": "Endast NTP",
"network_time_sync_title": "Tidssynkronisering",
"network_title": "Nätverk",
"never_seen_online": "Aldrig sett online",
"next": "Nästa",
"no_results_found": "Inga resultat hittades",
"not_applicable": "Ej tillämpligt",
"not_available": "Ej tillämpligt",
"not_found": "Inte hittad",
"ntp_servers": "NTP-servrar",
"oh_no": "nej då!",
"online": "Online",
"other_session_detected": "En annan aktiv session upptäckt",
"other_session_take_over": " Endast en aktiv session stöds åt gången. Vill du ta över den här sessionen?",
"other_session_use_here_button": "Använd här",
"page_not_found_description": "Sidan du sökte efter finns inte.",
"paste_modal_confirm_paste": "Bekräfta inklistring",
"paste_modal_delay_between_keys": "Fördröjning mellan tangenter",
"paste_modal_delay_out_of_range": "Fördröjningen måste vara mellan {min} och {max}",
"paste_modal_failed_paste": "Misslyckades med att klistra in text: {error}",
"paste_modal_invalid_chars_intro": "Följande tecken klistras inte in:",
"paste_modal_paste_from_host": "Klistra in från värd",
"paste_modal_sending_using_layout": "Skicka text med tangentbordslayout: {iso} - {name}",
"paste_text": "Klistra in text",
"paste_text_description": "Klistra in text från din klient till fjärrdatorn",
"peer_connection_closed": "Stängd",
"peer_connection_closing": "Stängning",
"peer_connection_connected": "Ansluten",
"peer_connection_connecting": "Ansluter",
"peer_connection_disconnected": "Osammanhängande",
"peer_connection_error": "Anslutningsfel",
"peer_connection_failed": "Anslutningen misslyckades",
"peer_connection_new": "Ansluter",
"previous": "Tidigare",
"register_device_error": "Det uppstod ett fel {error} din enhet registrerades.",
"register_device_finish_button": "Slutför installationen",
"register_device_name_description": "Namnge din enhet så att du enkelt kan identifiera den senare. Du kan ändra namnet när som helst.",
"register_device_name_label": "Enhetsnamn",
"register_device_name_placeholder": "Plex Media Server",
"register_device_no_name": "Vänligen ange ett namn",
"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}",
"rename_device_new_name_label": "Nytt enhetsnamn",
"rename_device_new_name_placeholder": "Plex Media Server",
"rename_device_no_name": "Vänligen ange ett namn",
"retry": "Försöka igen",
"saving": "Sparande…",
"search_placeholder": "Söka…",
"serial_console": "Seriell konsol",
"serial_console_baud_rate": "Baudhastighet",
"serial_console_configure_description": "Konfigurera dina seriella konsolinställningar",
"serial_console_data_bits": "Databitar",
"serial_console_get_settings_error": "Misslyckades med att hämta inställningar för seriekonsolen: {error}",
"serial_console_open_console": "Öppna konsolen",
"serial_console_parity": "Paritet",
"serial_console_parity_even": "Jämn paritet",
"serial_console_parity_mark": "Markera paritet",
"serial_console_parity_none": "Ingen paritet",
"serial_console_parity_odd": "Udda paritet",
"serial_console_parity_space": "Rymdparitet",
"serial_console_set_settings_error": "Misslyckades med att ställa in seriekonsolinställningarna till {settings} : {error}",
"serial_console_stop_bits": "Stoppbitar",
"setting_remote_description": "Ställa in fjärrkontrollens beskrivning",
"setting_remote_session_description": "Ställer in beskrivning av fjärrsession...",
"setting_up_connection_to_device": "Konfigurerar anslutning till enhet...",
"settings_access": "Tillträde",
"settings_advanced": "Avancerad",
"settings_appearance": "Utseende",
"settings_back_to_kvm": "Tillbaka till KVM",
"settings_general": "Allmän",
"settings_hardware": "Hårdvara",
"settings_keyboard": "Tangentbord",
"settings_keyboard_macros": "Tangentbordsmakron",
"settings_mouse": "Mus",
"settings_network": "Nätverk",
"settings_video": "Video",
"something_went_wrong": "Något gick fel. Försök igen senare eller kontakta supporten.",
"step_counter_step": "Steg {step}",
"subnet_mask": "Subnätmask",
"time_division_days": "dagar",
"time_division_hours": "timmar",
"time_division_minutes": "minuter",
"time_division_months": "månader",
"time_division_seconds": "sekunder",
"time_division_weeks": "veckor",
"time_division_years": "år",
"troubleshoot_connection": "Felsök anslutning",
"unknown_error": "Okänt fel",
"update_in_progress": "Uppdatering pågår",
"updates_failed_check": "Misslyckades med att söka efter uppdateringar: {error}",
"updates_failed_get_device_version": "Misslyckades med att hämta enhetsversionen: {error}",
"updating_leave_device_on": "Stäng inte av din enhet…",
"usb": "USB",
"usb_config_custom": "Anpassad",
"usb_config_default": "JetKVM-standard",
"usb_config_dell": "Dell Multimedia Pro-tangentbord",
"usb_config_failed_load": "Misslyckades med att ladda USB-konfigurationen: {error}",
"usb_config_failed_set": "Misslyckades med att ställa in USB-konfiguration: {error}",
"usb_config_identifiers_description": "USB-enhetsidentifierare som exponeras för måldatorn",
"usb_config_identifiers_title": "Identifierare",
"usb_config_logitech": "Logitech universaladapter",
"usb_config_manufacturer_label": "Tillverkare",
"usb_config_manufacturer_placeholder": "Ange tillverkare",
"usb_config_microsoft": "Microsoft trådlöst multimediatangentbord",
"usb_config_product_id_label": "Produkt-ID",
"usb_config_product_id_placeholder": "Ange produkt-ID",
"usb_config_product_name_label": "Produktnamn",
"usb_config_product_name_placeholder": "Ange produktnamn",
"usb_config_restore_default": "Återställ till standard",
"usb_config_serial_number_label": "Serienummer",
"usb_config_serial_number_placeholder": "Ange serienummer",
"usb_config_set_success": "USB-konfiguration inställd på {manufacturer} {product}",
"usb_config_update_identifiers": "Uppdatera USB-identifierare",
"usb_config_vendor_id_label": "Leverantörs-ID",
"usb_config_vendor_id_placeholder": "Ange leverantörs-ID",
"usb_device_classes_description": "USB-enhetsklasser i den sammansatta enheten",
"usb_device_classes_title": "Klasser",
"usb_device_custom": "Anpassad",
"usb_device_description": "USB-enheter att emulera på måldatorn",
"usb_device_enable_absolute_mouse_description": "Aktivera absolut mus (pekare)",
"usb_device_enable_absolute_mouse_title": "Aktivera absolut mus (pekare)",
"usb_device_enable_keyboard_description": "Aktivera tangentbord",
"usb_device_enable_keyboard_title": "Aktivera tangentbord",
"usb_device_enable_mass_storage_description": "Ibland kan det behöva inaktiveras för att förhindra problem med vissa enheter.",
"usb_device_enable_mass_storage_title": "Aktivera USB-masslagring",
"usb_device_enable_relative_mouse_description": "Aktivera relativ mus",
"usb_device_enable_relative_mouse_title": "Aktivera relativ mus",
"usb_device_failed_load": "Misslyckades med att ladda USB-enheter: {error}",
"usb_device_failed_set": "Misslyckades med att ställa in USB-enheter: {error}",
"usb_device_keyboard_mouse_and_mass_storage": "Tangentbord, mus och masslagring",
"usb_device_keyboard_only": "Endast tangentbord",
"usb_device_restore_default": "Återställ till standard",
"usb_device_title": "USB-enhet",
"usb_device_update_classes": "Uppdatera USB-klasser",
"usb_device_updated": "USB-enheter uppdaterade",
"usb_state_connected": "Ansluten",
"usb_state_connecting": "Ansluter",
"usb_state_disconnected": "Osammanhängande",
"usb_state_low_power_mode": "Lågströmsläge",
"user_interface_language_description": "Välj språket som ska användas i JetKVM-användargränssnittet",
"user_interface_language_title": "Gränssnittsspråk",
"video_brightness_description": "Ljusstyrka ( {value} x)",
"video_brightness_title": "Ljusstyrka",
"video_contrast_description": "Kontrastnivå ( {value} x)",
"video_contrast_title": "Kontrast",
"video_custom_edid_description": "EDID beskriver kompatibilitet med videolägen. Standardinställningarna fungerar i de flesta fall, men unika UEFI/BIOS-inställningar kan behöva justeras.",
"video_custom_edid_title": "Anpassat EDID",
"video_debugging_info_description": "Felsökningsinformation för video",
"video_debugging_info_title": "Felsökningsinformation",
"video_description": "Konfigurera skärminställningar och EDID för optimal kompatibilitet",
"video_edid_acer_b246wl": "Acer B246WL, 1920x1200",
"video_edid_asus_pa248qv": "ASUS PA248QV, 1920x1200",
"video_edid_custom": "Anpassad",
"video_edid_dell_d2721h": "DELL D2721H, 1920x1080",
"video_edid_dell_idrac": "DELL IDRAC EDID, 1280x1024",
"video_edid_description": "Justera EDID-inställningarna för skärmen",
"video_edid_file_label": "EDID-fil",
"video_edid_jetkvm_default": "JetKVM-standard",
"video_edid_set_success": "EDID har ställts in på {edid}",
"video_edid_title": "EDID",
"video_enhancement_description": "Justera färginställningarna för att göra videoutgången mer levande och färgglad",
"video_enhancement_title": "Videoförbättring",
"video_failed_get_debug_info": "Misslyckades med att hämta felsökningsinformation: {error}",
"video_failed_get_edid": "Misslyckades med att hämta EDID: {error}",
"video_failed_set_edid": "Misslyckades med att ange EDID: {error}",
"video_failed_set_stream_quality": "Misslyckades med att ställa in strömkvalitet: {error}",
"video_get_debugging_info": "Hämta felsökningsinformation",
"video_overlay_autoplay_permissions_required": "Behörigheter för automatisk uppspelning krävs",
"video_overlay_conn_check_cables": "Kontrollera alla kabelanslutningar för lösa eller skadade kablar",
"video_overlay_conn_ensure_network": "Se till att din nätverksanslutning är stabil och aktiv",
"video_overlay_conn_restart": "Försök att starta om både enheten och datorn",
"video_overlay_conn_verify_power": "Kontrollera att enheten är påslagen och korrekt ansluten",
"video_overlay_connection_issue_title": "Anslutningsproblem upptäckt",
"video_overlay_enable_autoplay_settings": "Justera webbläsarinställningarna för att aktivera automatisk uppspelning",
"video_overlay_hdmi_error_title": "HDMI-signalfel upptäckt.",
"video_overlay_hdmi_incompatible_resolution": "Inkompatibla inställningar för upplösning eller uppdateringsfrekvens",
"video_overlay_hdmi_loose_faulty": "En lös eller felaktig HDMI-anslutning",
"video_overlay_hdmi_source_issue": "Problem med källenhetens HDMI-utgång",
"video_overlay_learn_more": "Läs mer",
"video_overlay_loading_stream": "Laddar videoström…",
"video_overlay_manually_start_stream": "Starta strömning manuellt",
"video_overlay_no_hdmi_adapter_compat": "Om du använder en adapter, se till att den är kompatibel och fungerar korrekt.",
"video_overlay_no_hdmi_ensure_cable": "Se till att HDMI-kabeln är ordentligt ansluten i båda ändar",
"video_overlay_no_hdmi_ensure_power": "Se till att källenheten är påslagen och matar ut en signal",
"video_overlay_no_hdmi_signal": "Ingen HDMI-signal detekterad.",
"video_overlay_pointerlock_click_to_enable": "Klicka på videon för att aktivera muskontroll",
"video_overlay_retrying_connection": "Försöker ansluta igen…",
"video_overlay_troubleshooting_guide": "Felsökningsguide",
"video_overlay_try_again": "Försök igen",
"video_pointer_lock_disabled": "Pekarlås inaktiverat",
"video_pointer_lock_enabled": "Pekarlås aktiverat — tryck på Escape för att låsa upp",
"video_quality_high": "Hög",
"video_quality_low": "Låg",
"video_quality_medium": "Medium",
"video_reset_to_default": "Återställ till standard",
"video_restore_to_default": "Återställ till standard",
"video_saturation_description": "Färgmättnad ( {value} x)",
"video_saturation_title": "Mättnad",
"video_set_custom_edid": "Ange anpassat EDID",
"video_stream_quality_description": "Justera kvaliteten på videoströmmen",
"video_stream_quality_set": "Strömkvaliteten är inställd på {quality}",
"video_stream_quality_title": "Strömningskvalitet",
"video_title": "Video",
"view_details": "Visa detaljer",
"virtual_keyboard_header": "Virtuellt tangentbord",
"wake_on_lan": "Vakna på LAN",
"wake_on_lan_add_device_device_name": "Enhetsnamn",
"wake_on_lan_add_device_example_device_name": "Plex Media Server",
"wake_on_lan_add_device_mac_address": "MAC-adress",
"wake_on_lan_add_device_save_device": "Spara enhet",
"wake_on_lan_description": "Skicka ett magiskt paket för att väcka en fjärrenhet.",
"wake_on_lan_device_list_add_new_device": "Lägg till ny enhet",
"wake_on_lan_device_list_delete_device": "Ta bort enhet",
"wake_on_lan_device_list_wake": "Vakna",
"wake_on_lan_empty_add_device_to_start": "Lägg till en enhet för att börja använda Wake-on-LAN",
"wake_on_lan_empty_add_new_device": "Lägg till ny enhet",
"wake_on_lan_empty_no_devices_added": "Inga enheter tillagda",
"wake_on_lan_failed_add_device": "Misslyckades med att lägga till enhet",
"wake_on_lan_failed_send_magic": "Misslyckades med att skicka Magic Packet",
"wake_on_lan_invalid_mac": "Ogiltig MAC-adress",
"wake_on_lan_magic_sent_success": "Magiskt paket skickades",
"welcome_to_jetkvm": "Välkommen till JetKVM",
"welcome_to_jetkvm_description": "Styr vilken dator som helst på distans"
}

View File

@ -0,0 +1,895 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"access_adopt_kvm": "采用 KVM 到云",
"access_adopted_message": "您的设备已接入云端",
"access_auth_mode_no_password": "当前模式:无密码",
"access_auth_mode_password": "当前模式:密码保护",
"access_authentication_mode_title": "认证模式",
"access_certificate_label": "证书",
"access_change_password_button": "更改密码",
"access_change_password_description": "更新您的设备访问密码",
"access_change_password_title": "更改密码",
"access_cloud_api_url_label": "云 API 网址",
"access_cloud_app_url_label": "云应用程序 URL",
"access_cloud_provider_description": "为您的设备选择云提供商",
"access_cloud_provider_title": "云提供商",
"access_cloud_security_title": "云安全",
"access_confirm_deregister": "您确定要取消注册该设备吗?",
"access_deregister": "从云端取消注册",
"access_description": "管理设备的访问控制",
"access_disable_protection": "禁用保护",
"access_enable_password": "启用密码",
"access_failed_deregister": "无法取消注册设备: {error}",
"access_failed_update_cloud_url": "无法更新云 URL {error}",
"access_failed_update_tls": "无法更新 TLS 设置: {error}",
"access_github_link": "GitHub",
"access_https_description": "配置设备的安全 HTTPS 访问",
"access_https_mode_title": "HTTPS模式",
"access_learn_security": "了解我们的云安全",
"access_local_description": "管理设备本地访问模式",
"access_local_title": "当地的",
"access_no_device_id": "没有可用的设备 ID",
"access_private_key_description": "出于安全原因,保存后将不会显示。",
"access_private_key_label": "私钥",
"access_provider_custom": "风俗",
"access_provider_jetkvm": "JetKVM 云",
"access_remote_description": "管理远程访问设备的模式",
"access_security_encryption": "使用 WebRTCDTLS 和 SRTP进行端到端加密",
"access_security_oidc": "OIDCOpenID Connect身份验证",
"access_security_open_source": "所有云组件都是开源的,可以在 GitHub 上获取。",
"access_security_streams": "所有流在传输过程中加密",
"access_security_zero_trust": "零信任安全模型",
"access_title": "使用权",
"access_tls_certificate_description": "请在下方粘贴您的 TLS 证书。对于证书链,请包含整个证书链(叶证书、中间证书和根证书)。",
"access_tls_certificate_title": "TLS 证书",
"access_tls_custom": "风俗",
"access_tls_disabled": "已禁用",
"access_tls_self_signed": "自签名",
"access_tls_updated": "TLS 设置更新成功",
"access_update_tls_settings": "更新 TLS 设置",
"action_bar_connection_stats": "连接统计",
"action_bar_extension": "扩展",
"action_bar_fullscreen": "全屏",
"action_bar_settings": "设置",
"action_bar_virtual_keyboard": "虚拟键盘",
"action_bar_virtual_media": "虚拟媒体",
"action_bar_wake_on_lan": "局域网唤醒",
"action_bar_web_terminal": "网页终端",
"advanced_description": "访问故障排除和自定义的其他设置",
"advanced_dev_channel_description": "从开发渠道获取早期更新",
"advanced_dev_channel_title": "开发频道更新",
"advanced_developer_mode_description": "为开发人员启用高级功能",
"advanced_developer_mode_enabled_title": "已启用开发者模式",
"advanced_developer_mode_title": "开发者模式",
"advanced_developer_mode_warning_advanced": "仅适用于高级用户。不可用于生产用途。",
"advanced_developer_mode_warning_risks": "仅在了解风险的情况下使用",
"advanced_developer_mode_warning_security": "活跃时安全性会减弱",
"advanced_disable_usb_emulation": "禁用 USB 模拟",
"advanced_enable_usb_emulation": "启用 USB 模拟",
"advanced_error_loopback_disable": "无法禁用仅环回模式: {error}",
"advanced_error_loopback_enable": "无法启用仅环回模式: {error}",
"advanced_error_reset_config": "无法重置配置: {error}",
"advanced_error_set_dev_channel": "无法设置开发通道状态: {error}",
"advanced_error_set_dev_mode": "无法设置开发模式: {error}",
"advanced_error_update_ssh_key": "无法更新 SSH 密钥: {error}",
"advanced_error_usb_emulation_disable": "无法禁用 USB 仿真: {error}",
"advanced_error_usb_emulation_enable": "无法启用 USB 仿真: {error}",
"advanced_loopback_only_description": "限制 Web 界面仅可访问本地主机127.0.0.1",
"advanced_loopback_only_title": "仅环回模式",
"advanced_loopback_warning_before": "在启用此功能之前,请确保您已:",
"advanced_loopback_warning_cloud": "云访问已启用并正在运行",
"advanced_loopback_warning_confirm": "我明白,无论如何启用",
"advanced_loopback_warning_description": "警告:这将限制 Web 界面仅访问本地主机127.0.0.1)。",
"advanced_loopback_warning_ssh": "配置并测试 SSH 访问",
"advanced_loopback_warning_title": "启用仅环回模式?",
"advanced_reset_config_button": "重置配置",
"advanced_reset_config_description": "将配置重置为默认值。这将使您退出登录。",
"advanced_reset_config_title": "重置配置",
"advanced_ssh_access_description": "添加您的 SSH 公钥以启用对设备的安全远程访问",
"advanced_ssh_access_title": "SSH 访问",
"advanced_ssh_default_user": "默认 SSH 用户是",
"advanced_ssh_public_key_label": "SSH 公钥",
"advanced_ssh_public_key_placeholder": "输入您的 SSH 公钥",
"advanced_success_loopback_disabled": "仅环回模式已禁用。请重启设备以应用此模式。",
"advanced_success_loopback_enabled": "仅环回模式已启用。请重启设备以应用此模式。",
"advanced_success_reset_config": "配置重置为默认值已成功",
"advanced_success_update_ssh_key": "SSH 密钥更新成功",
"advanced_title": "先进的",
"advanced_troubleshooting_mode_description": "用于故障排除和开发目的的诊断工具和附加控件",
"advanced_troubleshooting_mode_title": "故障排除模式",
"advanced_update_ssh_key_button": "更新 SSH 密钥",
"advanced_usb_emulation_description": "控制 USB 仿真状态",
"advanced_usb_emulation_title": "USB 仿真",
"already_adopted_new_owner": "如果您是新用户,请要求前用户在云端控制面板中从其帐户中取消注册该设备。如果您认为此操作有误,请联系我们的支持团队寻求帮助。",
"already_adopted_other_user": "该设备目前已在我们的云仪表板中注册给另一个用户。",
"already_adopted_return_to_dashboard": "返回仪表板",
"already_adopted_title": "设备已注册",
"appearance_description": "选择您喜欢的颜色主题",
"appearance_page_description": "自定义 JetKVM 界面的外观和感觉",
"appearance_theme": "主题",
"appearance_theme_dark": "黑暗的",
"appearance_theme_light": "光",
"appearance_theme_system": "系统",
"appearance_title": "外貌",
"attach": "附",
"atx_power_control_get_state_error": "无法获取 ATX 电源状态:{error}",
"atx_power_control_hdd_led": "硬盘指示灯",
"atx_power_control_long_power_button": "长按",
"atx_power_control_power_button": "电源",
"atx_power_control_power_led": "电源 LED",
"atx_power_control_reset_button": "重置",
"atx_power_control_send_action_error": "无法发送 ATX 电源操作 {action} : {error}",
"atx_power_control_short_power_button": "短按",
"auth_authentication_mode": "请选择身份验证方式",
"auth_authentication_mode_error": "设置身份验证模式时发生错误",
"auth_authentication_mode_invalid": "身份验证模式无效",
"auth_connect_to_cloud": "将您的 JetKVM 连接到云端",
"auth_connect_to_cloud_action": "登录并连接设备",
"auth_connect_to_cloud_description": "解锁设备的远程访问和高级功能",
"auth_header_cta_already_have_account": "已有账户?",
"auth_header_cta_dont_have_account": "沒有帳戶?",
"auth_header_cta_new_to_jetkvm": "JetKVM 新手?",
"auth_login": "登录您的 JetKVM 帐户",
"auth_login_action": "登录",
"auth_login_description": "登录以安全地访问和管理您的设备",
"auth_mode_local": "本地身份验证方法",
"auth_mode_local_change_later": "您可以随时在设置中更改您的身份验证方法。",
"auth_mode_local_description": "选择您希望如何在本地保护您的 JetKVM 设备。",
"auth_mode_local_no_password": "没有密码",
"auth_mode_local_no_password_description": "无需密码验证即可快速访问。",
"auth_mode_local_password": "密码",
"auth_mode_local_password_confirm_description": "确认您的密码",
"auth_mode_local_password_confirm_label": "确认密码",
"auth_mode_local_password_description": "使用密码保护您的设备以增强保护。",
"auth_mode_local_password_failed_set": "无法设置密码: {error}",
"auth_mode_local_password_note": "此密码将用于保护您的设备数据并防止未经授权的访问。",
"auth_mode_local_password_note_local": "所有数据都保留在您的本地设备上。",
"auth_mode_local_password_set": "设置密码",
"auth_mode_local_password_set_button": "设置密码",
"auth_mode_local_password_set_description": "创建一个强密码来本地保护您的 JetKVM 设备。",
"auth_mode_local_password_set_label": "输入密码",
"auth_signup_connect_to_cloud_action": "注册并连接设备",
"auth_signup_create_account": "创建您的 JetKVM 帐户",
"auth_signup_create_account_action": "创建账户",
"auth_signup_create_account_description": "创建您的帐户并开始轻松管理您的设备。",
"back": "后退",
"back_to_devices": "返回设备",
"cancel": "取消",
"close": "关闭",
"cloud_kvms": "云 KVM",
"cloud_kvms_description": "管理您的云 KVM 并安全地连接到它们。",
"cloud_kvms_no_devices": "未找到设备",
"cloud_kvms_no_devices_description": "您还没有任何启用 JetKVM Cloud 的设备。",
"confirm": "确认",
"connect_to_kvm": "连接到 KVM",
"connecting_to_device": "正在连接设备…",
"connection_established": "已建立连接",
"connection_stats_badge_jitter": "抖动",
"connection_stats_badge_jitter_buffer_avg_delay": "抖动缓冲区平均延迟",
"connection_stats_connection": "联系",
"connection_stats_connection_description": "客户端与JetKVM之间的连接。",
"connection_stats_frames_per_second": "每秒帧数",
"connection_stats_frames_per_second_description": "每秒显示的入站视频帧数。",
"connection_stats_network_stability": "网络稳定性",
"connection_stats_network_stability_description": "网络上传入的视频数据包的流动有多稳定。",
"connection_stats_packets_lost": "数据包丢失",
"connection_stats_packets_lost_description": "丢失的入站视频 RTP 数据包的数量。",
"connection_stats_playback_delay": "播放延迟",
"connection_stats_playback_delay_description": "当帧不均匀到达时,抖动缓冲区添加延迟以平滑播放。",
"connection_stats_round_trip_time": "往返时间",
"connection_stats_round_trip_time_description": "对等体之间活跃 ICE 候选对的往返时间。",
"connection_stats_sidebar": "连接统计",
"connection_stats_unit_frames_per_second": " 帧每秒",
"connection_stats_unit_milliseconds": " 毫秒",
"connection_stats_unit_packets": " 数据包",
"connection_stats_video": "视频",
"connection_stats_video_description": "从 JetKVM 到客户端的视频流。",
"continue": "继续",
"creating_peer_connection": "正在创建对等连接...",
"dc_power_control_current": "电流",
"dc_power_control_current_unit": "A",
"dc_power_control_get_state_error": "无法获取直流电源状态:{error}",
"dc_power_control_power": "瓦特",
"dc_power_control_power_off_button": "关闭电源",
"dc_power_control_power_off_state": "关闭电源",
"dc_power_control_power_on_button": "开机",
"dc_power_control_power_on_state": "开启电源",
"dc_power_control_power_unit": "W",
"dc_power_control_restore_last_state": "最后状态",
"dc_power_control_restore_power_state": "恢复断电",
"dc_power_control_set_power_state_error": "无法将直流电源状态发送到 {enabled} : {error}",
"dc_power_control_set_restore_state_error": "无法将直流电源恢复状态发送到 {state} : {error}",
"dc_power_control_voltage": "电压",
"dc_power_control_voltage_unit": "V",
"delete": "删除",
"deregister_cloud_devices": "云设备",
"deregister_description": "这将从您的云帐户中移除该设备,并撤销其远程访问权限。请注意,您仍然可以进行本地访问",
"deregister_error": "注销您的设备时出现错误{status} 。请重试。",
"deregister_from_cloud": "从云端注销",
"deregister_headline": "从您的云帐户中取消注册{device}",
"detach": "分离",
"dhcp_empty_lease_description": "我们尚未收到来自该设备的任何 DHCP 租约信息。",
"dhcp_empty_lease_headline": "无 DHCP 租约信息",
"dhcp_lease_boot_file": "引导文件",
"dhcp_lease_boot_next_server": "启动下一个服务器",
"dhcp_lease_boot_server_name": "启动服务器名称",
"dhcp_lease_broadcast": "播送",
"dhcp_lease_domain": "领域",
"dhcp_lease_gateway": "网关",
"dhcp_lease_header": "DHCP 租约信息",
"dhcp_lease_hostname": "主机名",
"dhcp_lease_lease_expires": "租约到期",
"dhcp_lease_maximum_transfer_unit": "最大传输单元",
"dhcp_lease_renew": "续订 DHCP 租约",
"dhcp_lease_time_to_live": "TTL",
"dhcp_server": "DHCP 服务器",
"dns_servers": "DNS服务器",
"establishing_secure_connection": "正在建立安全连接...",
"experimental": "实验",
"extension_popover_load_and_manage_extensions": "加载和管理您的扩展",
"extension_popover_set_error_notification": "无法设置活动扩展:{error}",
"extension_popover_unload_extension": "卸载扩展",
"extension_serial_console": "串行控制台",
"extension_serial_console_description": "访问串行控制台扩展",
"extensions_atx_power_control": "ATX 电源控制",
"extensions_atx_power_control_description": "通过 ATX 电源控制来控制机器的电源状态。",
"extensions_dc_power_control": "直流电源控制",
"extensions_dc_power_control_description": "控制您的直流电源扩展",
"extensions_popover_extensions": "扩展",
"gathering_ice_candidates": "召集 ICE 候选人……",
"general_app_version": "应用程序: {version}",
"general_auto_update_description": "自动将设备更新到最新版本",
"general_auto_update_error": "无法设置自动更新: {error}",
"general_auto_update_title": "自动更新",
"general_check_for_updates": "检查更新",
"general_page_description": "配置设备设置并更新首选项",
"general_reboot_description": "您想继续重新启动系统吗?",
"general_reboot_device": "重启设备",
"general_reboot_device_description": "对 JetKVM 进行电源循环",
"general_reboot_no_button": "不",
"general_reboot_title": "重启 JetKVM",
"general_reboot_yes_button": "是的",
"general_system_version": "系统: {version}",
"general_title": "一般的",
"general_update_app_update_title": "应用程序更新",
"general_update_application_type": "应用程序",
"general_update_available_description": "新的更新现已推出,旨在提升系统性能并改善兼容性。建议您更新以确保一切顺利运行。",
"general_update_available_title": "有可用更新",
"general_update_background_button": "后台更新",
"general_update_check_again_button": "再次检查",
"general_update_checking_description": "我们确保您的设备具有最新的功能和改进。",
"general_update_checking_title": "正在检查更新...",
"general_update_completed_description": "您的设备已成功更新至最新版本。尽情享受新功能和改进吧!",
"general_update_completed_title": "更新已成功完成",
"general_update_error_description": "更新您的设备时出错。请稍后重试。",
"general_update_error_details": "错误详细信息: {errorMessage}",
"general_update_error_title": "更新错误",
"general_update_later_button": "稍后再做",
"general_update_now_button": "立即更新",
"general_update_rebooting": "重新启动以完成更新...",
"general_update_status_awaiting_reboot": "等待重启",
"general_update_status_downloading": "正在下载{update_type}更新…",
"general_update_status_fetching": "正在获取更新信息...",
"general_update_status_installing": "正在安装{update_type}更新...",
"general_update_status_progress": "{part}进度",
"general_update_status_verifying": "验证{update_type}更新…",
"general_update_system_type": "系统",
"general_update_system_update_title": "Linux 系统更新",
"general_update_up_to_date_description": "您的系统正在运行最新版本。目前没有可用的更新。",
"general_update_up_to_date_title": "系统已更新",
"general_update_updating_description": "请不要关闭您的设备。此过程可能需要几分钟。",
"general_update_updating_title": "更新您的设备",
"getting_remote_session_description": "获取远程会话描述尝试{attempt}",
"hardware_backlight_settings_error": "无法设置背光设置: {error}",
"hardware_backlight_settings_get_error": "无法获取背光设置: {error}",
"hardware_backlight_settings_success": "背光设置更新成功",
"hardware_dim_display_after_description": "设置显示屏变暗前等待的时间",
"hardware_dim_display_after_title": "之后显示变暗",
"hardware_display_brightness_description": "设置显示屏亮度",
"hardware_display_brightness_high": "高的",
"hardware_display_brightness_low": "低的",
"hardware_display_brightness_medium": "中等的",
"hardware_display_brightness_off": "离开",
"hardware_display_brightness_title": "显示屏亮度",
"hardware_display_orientation_description": "设置显示方向",
"hardware_display_orientation_error": "无法设置显示方向: {error}",
"hardware_display_orientation_inverted": "倒",
"hardware_display_orientation_normal": "普通的",
"hardware_display_orientation_success": "显示方向更新成功",
"hardware_display_orientation_title": "显示方向",
"hardware_display_wake_up_note": "当连接状态改变或被触摸时,显示屏将会唤醒。",
"hardware_page_description": "为您的 JetKVM 设备配置显示设置和硬件选项",
"hardware_power_saving_description": "不使用时减少功耗",
"hardware_power_saving_disabled": "省电模式已禁用",
"hardware_power_saving_enabled": "启用省电模式",
"hardware_power_saving_failed_error": "无法设置省电模式: {error}",
"hardware_power_saving_hdmi_sleep_description": "90 秒不活动后关闭捕获",
"hardware_power_saving_hdmi_sleep_title": "HDMI睡眠模式",
"hardware_power_saving_title": "节能",
"hardware_time_10_minutes": "10分钟",
"hardware_time_1_hour": "1小时",
"hardware_time_1_minute": "1分钟",
"hardware_time_30_minutes": "30分钟",
"hardware_time_5_minutes": "5分钟",
"hardware_time_never": "绝不",
"hardware_title": "硬件",
"hardware_turn_off_display_after_description": "显示屏自动关闭前的非活动时间",
"hardware_turn_off_display_after_title": "关闭显示后",
"hide": "隐藏",
"ice_gathering_completed": "ICE 聚会结束",
"info_caps_lock": "大写锁定",
"info_compose": "撰写",
"info_hdmi_state": "HDMI 状态:",
"info_hidrpc_state": "HidRPC 状态:",
"info_kana": "假名",
"info_keys": "按键:",
"info_last_move": "最后一步:",
"info_num_lock": "数字锁定",
"info_paste_enabled": "已启用",
"info_paste_mode": "粘贴模式:",
"info_pointer": "指针:",
"info_relayed_by_cloudflare": "由 Cloudflare 转发",
"info_resolution": "解决:",
"info_scroll_lock": "滚动锁定",
"info_shift": "转移",
"info_usb_state": "USB 状态:",
"info_video_size": "视频大小:",
"input_disabled": "输入禁用",
"invalid_password": "密码无效",
"ip_address": "IP 地址",
"ipv6_address_label": "地址",
"ipv6_gateway": "网关",
"ipv6_information": "IPv6 信息",
"ipv6_link_local": "本地链路",
"ipv6_preferred_lifetime": "首选寿命",
"ipv6_valid_lifetime": "有效期",
"jetkvm_description": "JetKVM 将强大的硬件与直观的软件相结合,提供无缝的远程控制体验。",
"jetkvm_device": "JetKVM 设备",
"jetkvm_logo": "JetKVM 徽标",
"jetkvm_setup": "设置您的 JetKVM",
"jiggler_cron_schedule_description": "用于调度的 Cron 表达式",
"jiggler_cron_schedule_label": "Cron 计划",
"jiggler_example_business_hours_early": "营业时间 8-17",
"jiggler_example_business_hours_late": "营业时间 9-17",
"jiggler_examples_label": "示例",
"jiggler_inactivity_limit_description": "抖动前不活动时间",
"jiggler_inactivity_limit_label": "不活动限制秒数",
"jiggler_more_examples": "更多示例",
"jiggler_random_delay_description": "为了避免可识别的模式",
"jiggler_random_delay_label": "随机延迟",
"jiggler_save_jiggler_config": "保存 Jiggler 配置",
"jiggler_timezone_description": "cron 计划的时区",
"jiggler_timezone_label": "时区",
"keyboard_description": "为您的设备配置键盘设置",
"keyboard_layout_description": "目标操作系统的键盘布局",
"keyboard_layout_error": "无法设置键盘布局: {error}",
"keyboard_layout_long_description": "虚拟键盘、粘贴文本和键盘宏会将单个按键发送到目标设备。键盘布局决定了要发送哪些键码。请确保 JetKVM 中的键盘布局与操作系统中的设置匹配。",
"keyboard_layout_success": "键盘布局已成功设置为{layout}",
"keyboard_layout_title": "键盘布局",
"keyboard_show_pressed_keys_description": "在状态栏中显示当前按下的键",
"keyboard_show_pressed_keys_title": "显示按下的键",
"keyboard_title": "键盘",
"kvm_terminal": "KVM终端",
"last_online": "最后在线{time}",
"learn_more": "了解更多",
"load": "加载",
"loading": "加载中…",
"local_auth_change_local_device_password_description": "输入您当前的密码和新密码以更新您的本地设备保护。",
"local_auth_change_local_device_password_title": "更改本地设备密码",
"local_auth_confirm_new_password_label": "确认新密码",
"local_auth_create_confirm_password_placeholder": "重新输入您的密码",
"local_auth_create_description": "创建密码以保护您的设备免遭未经授权的本地访问。",
"local_auth_create_new_password_label": "新密码",
"local_auth_create_new_password_placeholder": "输入强密码",
"local_auth_create_not_now_button": "现在不要",
"local_auth_create_secure_button": "安全设备",
"local_auth_create_title": "本地设备保护",
"local_auth_current_password_label": "当前密码",
"local_auth_disable_local_device_protection_description": "输入当前密码以禁用本地设备保护。",
"local_auth_disable_local_device_protection_title": "禁用本地设备保护",
"local_auth_disable_protection_button": "禁用保护",
"local_auth_enter_current_password_placeholder": "输入您当前的密码",
"local_auth_enter_new_password_placeholder": "输入新的强密码",
"local_auth_error_changing_password": "更改密码时发生错误",
"local_auth_error_disabling_password": "禁用密码时发生错误",
"local_auth_error_enter_current_password": "请输入您当前的密码",
"local_auth_error_enter_new_password": "请输入新密码",
"local_auth_error_enter_old_password": "请输入您的旧密码",
"local_auth_error_enter_password": "请输入密码",
"local_auth_error_passwords_not_match": "密码不匹配",
"local_auth_error_setting_password": "设置密码时发生错误",
"local_auth_new_password_label": "新密码",
"local_auth_reenter_new_password_placeholder": "重新输入您的新密码",
"local_auth_success_password_disabled_description": "您已成功禁用本地访问的密码保护。请注意,您的设备现在安全性较低。",
"local_auth_success_password_disabled_title": "密码保护已禁用",
"local_auth_success_password_set_description": "您已成功设置本地设备保护。您的设备现已安全,可防止未经授权的本地访问。",
"local_auth_success_password_set_title": "密码设置成功",
"local_auth_success_password_updated_description": "您已成功更改本地设备保护密码。请务必记住新密码,以便日后访问。",
"local_auth_success_password_updated_title": "密码更新成功",
"local_auth_update_password_button": "更新密码",
"locale_auto": "汽车",
"locale_change_success": "语言已成功更改为{locale}",
"locale_da": "丹麦语",
"locale_de": "德语",
"locale_en": "英语",
"locale_es": "西班牙语",
"locale_fr": "法语",
"locale_it": "意大利语",
"locale_nb": "挪威语(博克马尔语)",
"locale_sv": "瑞典语",
"locale_zh": "中文 (简体)",
"log_in": "登录",
"log_out": "登出",
"logged_in_as": "登录身份",
"login_enter_password": "输入您的密码",
"login_enter_password_description": "输入您的密码以访问您的 JetKVM。",
"login_error": "登录时发生错误",
"login_forgot_password": "忘记密码?",
"login_password_label": "密码",
"login_welcome_back": "欢迎回到 JetKVM",
"macro_add_step": "添加步骤{maxed_out}",
"macro_at_least_one_step_keys_or_modifiers": "至少一个步骤必须包含键或修饰符",
"macro_at_least_one_step_required": "至少需要一步",
"macro_max_steps_error": "每个宏最多只能添加{max}步骤。",
"macro_max_steps_reached": " {max} max",
"macro_name_label": "宏名称",
"macro_name_required": "姓名为必填项",
"macro_name_too_long": "名称必须少于 50 个字符",
"macro_please_fix_validation_errors": "请修复验证错误",
"macro_save": "保存宏",
"macro_save_failed": "保存时发生错误。",
"macro_save_failed_error": "保存时发生错误:{error}。",
"macro_step_count": "{steps} / {max}步骤",
"macro_step_duration_description": "执行下一步之前需要等待的时间。",
"macro_step_duration_label": "步骤持续时间",
"macro_step_keys_description": "每步最多{max}键。",
"macro_step_keys_label": "按键",
"macro_step_max_keys_reached": "已达到最大密钥数",
"macro_step_modifiers_description": "在此步骤中按下了哪些修饰键Shift/Ctrl/Alt/Meta。",
"macro_step_modifiers_label": "修饰符",
"macro_step_no_matching_keys_found": "未找到匹配的键",
"macro_step_search_for_key": "搜索密钥...",
"macro_steps_description": "键/修饰键按顺序执行,每个步骤之间有延迟。",
"macro_steps_label": "步骤",
"macros_add_description": "创建新的键盘宏",
"macros_add_new": "添加新宏",
"macros_add_new_macro": "添加新宏",
"macros_aria_add_new": "添加新宏",
"macros_aria_delete": "删除宏{name}",
"macros_aria_duplicate": "重复宏{name}",
"macros_aria_edit": "编辑宏{name}",
"macros_aria_move_down": "向下移动{name}",
"macros_aria_move_up": "向上移动{name}",
"macros_confirm_delete_description": "您确定要删除“ {name} ”吗?此操作无法撤消。",
"macros_confirm_delete_title": "删除宏",
"macros_confirm_deleting": "正在删除…",
"macros_create_first_description": "将按键组合成一个动作",
"macros_create_first_headline": "创建您的第一个宏",
"macros_created_success": "宏“ {name} ”已成功创建",
"macros_delay_only": "仅延迟",
"macros_delete_confirm": "您确定要删除此宏吗?此操作无法撤消。",
"macros_delete_macro": "删除宏",
"macros_deleted_success": "宏“ {name} ”已成功删除",
"macros_deleting": "删除",
"macros_duplicated_success": "宏“ {name} ”复制成功",
"macros_edit_button": "编辑",
"macros_edit_description": "修改键盘宏",
"macros_edit_title": "编辑宏",
"macros_failed_create": "无法创建宏",
"macros_failed_create_error": "无法创建宏: {error}",
"macros_failed_delete": "删除宏失败",
"macros_failed_delete_error": "无法删除宏: {error}",
"macros_failed_duplicate": "复制宏失败",
"macros_failed_duplicate_error": "无法复制宏: {error}",
"macros_failed_reorder": "无法重新排序宏",
"macros_failed_reorder_error": "无法重新排序宏: {error}",
"macros_failed_update": "更新宏失败",
"macros_failed_update_error": "无法更新宏: {error}",
"macros_invalid_data": "无效的宏数据",
"macros_loading": "正在加载宏...",
"macros_max_reached": "已达到最大值",
"macros_maximum_macros_reached": "您已达到允许的最大{maximum}宏数量。",
"macros_no_macros_available": "没有可用的宏",
"macros_order_updated": "宏命令更新成功",
"macros_title": "键盘宏",
"macros_updated_success": "宏“ {name} ”已成功更新",
"metric_not_supported": "不支持指标",
"metric_waiting_for_data": "正在等待数据...",
"mount_add_file_to_get_started": "添加文件以开始",
"mount_add_new_media": "添加新媒体",
"mount_available_storage": "可用存储空间",
"mount_button_back_to_overview": "返回概览",
"mount_button_cancel_upload": "取消上传",
"mount_button_continue_upload": "继续上传",
"mount_button_mount_file": "挂载文件",
"mount_button_mount_url": "安装 URL",
"mount_button_select": "选择",
"mount_button_showing_results": "显示{from}至{to}共{total}个结果",
"mount_button_upload_new_image": "上传新图片",
"mount_bytes_free": "{bytesFree}免费",
"mount_bytes_used": "{bytesUsed}已使用",
"mount_calculating": "正在计算…",
"mount_click_to_select_file": "单击以选择文件",
"mount_click_to_select_incomplete": "点击选择“ {name} ”",
"mount_confirm_delete": "您确定要删除{name} ",
"mount_continue_uploading_with_name": "继续上传“ {name} ”",
"mount_error_delete_file": "删除文件时出错: {error}",
"mount_error_description": "尝试装载媒体时出错。请重试。",
"mount_error_get_storage_space": "获取存储空间时出错: {error}",
"mount_error_list_storage": "列出存储文件时出错: {error}",
"mount_error_title": "安装错误",
"mount_get_state_error": "无法获取虚拟媒体状态: {error}",
"mount_jetkvm_storage": "JetKVM 存储支架",
"mount_jetkvm_storage_description": "从 JetKVM 存储挂载之前上传的文件",
"mount_mode_cdrom": "CD/DVD",
"mount_mode_disk": "磁盘",
"mount_mounted_as": "安装为",
"mount_mounted_from_storage": "从 JetKVM 存储挂载",
"mount_no_images_description": "上传图像以开始虚拟媒体安装。",
"mount_no_images_title": "没有可用的图像",
"mount_no_mounted_media": "没有安装媒体",
"mount_percentage_used": "{percentageUsed}使用百分比",
"mount_please_select_file": "请选择文件“ {name} ”继续上传。",
"mount_popular_images": "热门图片",
"mount_streaming_from_url": "从 URL 流式传输",
"mount_supported_formats": "支持的格式ISO、IMG",
"mount_unmount": "卸载",
"mount_unmount_error": "无法卸载映像: {error}",
"mount_upload_description": "选择要上传到 JetKVM 存储的图像文件",
"mount_upload_error": "上传错误: {error}",
"mount_upload_failed_datachannel": "无法创建文件上传数据通道",
"mount_upload_failed_rtc": "上传失败: {error}",
"mount_upload_successful": "上传成功",
"mount_upload_title": "上传新图片",
"mount_uploaded_has_been_uploaded": "{name}已上传",
"mount_uploading": "正在上传…",
"mount_uploading_with_name": "正在上传{name}",
"mount_url_description": "从任何公共网址挂载文件",
"mount_url_input_label": "图片网址",
"mount_url_mount": "URL 挂载",
"mount_view_device_description": "从 JetKVM 存储中选择要挂载的图像",
"mount_view_device_title": "从 JetKVM 存储挂载",
"mount_view_url_description": "输入要挂载的镜像文件的 URL",
"mount_view_url_title": "从 URL 挂载",
"mount_virtual_media": "虚拟媒体",
"mount_virtual_media_description": "挂载映像以进行启动或安装操作系统。",
"mount_virtual_media_source": "虚拟媒体源",
"mount_virtual_media_source_description": "选择如何安装虚拟媒体",
"mouse_alt_finger": "手指触摸屏幕",
"mouse_alt_mouse": "鼠标图标",
"mouse_description": "为您的设备配置光标行为和交互设置",
"mouse_hide_cursor_description": "发送鼠标移动时隐藏光标",
"mouse_hide_cursor_title": "隐藏光标",
"mouse_jiggler_config_updated": "Jiggler 配置已成功更新",
"mouse_jiggler_custom": "风俗",
"mouse_jiggler_description": "模拟计算机鼠标的移动",
"mouse_jiggler_disabled": "已禁用",
"mouse_jiggler_error_config": "设置抖动器配置时出错",
"mouse_jiggler_failed_state": "无法设置抖动器状态: {error}",
"mouse_jiggler_frequent": "频繁 - 30 秒",
"mouse_jiggler_invalid_cron": "cron 表达式无效。请检查您的计划格式例如每分钟为“0 * * * * *”)。",
"mouse_jiggler_light": "光 - 5米",
"mouse_jiggler_standard": "标准 - 1米",
"mouse_jiggler_title": "吉格勒",
"mouse_mode_absolute": "绝对",
"mouse_mode_absolute_description": "最方便",
"mouse_mode_relative": "相对的",
"mouse_mode_relative_description": "最兼容",
"mouse_modes_description": "选择鼠标输入模式",
"mouse_modes_title": "模式",
"mouse_scroll_high": "高的",
"mouse_scroll_low": "低的",
"mouse_scroll_medium": "中等的",
"mouse_scroll_off": "离开",
"mouse_scroll_throttling_description": "降低滚动事件的频率",
"mouse_scroll_throttling_title": "滚动节流",
"mouse_scroll_very_high": "非常高",
"mouse_title": "老鼠",
"network_custom_domain": "自定义域",
"network_description": "配置您的网络设置",
"network_dhcp_client_description": "配置要使用的 DHCP 客户端",
"network_dhcp_client_jetkvm": "JetKVM 内部",
"network_dhcp_client_title": "DHCP客户端",
"network_dhcp_lease_renew_confirm": "续租",
"network_dhcp_lease_renew_confirm_description": "这将从您的 DHCP 服务器请求新的 IP 地址。在此过程中,您的设备可能会暂时失去网络连接。",
"network_dhcp_lease_renew_confirm_new_a": "如果您收到新的 IP 地址",
"network_dhcp_lease_renew_confirm_new_b": "您可能需要使用新地址重新连接",
"network_dhcp_lease_renew_failed": "无法续订租约: {error}",
"network_dhcp_lease_renew_success": "DHCP 租约已续订",
"network_domain_custom": "风俗",
"network_domain_description": "设备的网络域后缀",
"network_domain_dhcp_provided": "DHCP 提供",
"network_domain_local": "。当地的",
"network_domain_title": "领域",
"network_hostname_description": "网络上的设备标识符。系统默认为空白",
"network_hostname_title": "主机名",
"network_http_proxy_description": "设备发出 HTTP(S) 请求的代理服务器。空白表示无。",
"network_http_proxy_invalid": "HTTP 代理 URL 无效",
"network_http_proxy_title": "HTTP 代理",
"network_ipv4_address": "IPv4 地址",
"network_ipv4_dns": "IPv4 域名服务器",
"network_ipv4_gateway": "IPv4 网关",
"network_ipv4_invalid": "IPv4 地址无效",
"network_ipv4_invalid_cidr": "IPv4 地址的 CIDR 表示法无效",
"network_ipv4_mode_description": "配置 IPv4 模式",
"network_ipv4_mode_dhcp": "DHCP",
"network_ipv4_mode_static": "静止的",
"network_ipv4_mode_title": "IPv4 模式",
"network_ipv4_netmask": "IPv4 网络掩码",
"network_ipv6_addresses_header": "IPv6 地址",
"network_ipv6_cidr_suggestion": "请使用 CIDR 表示法例如2001:db8::1/64",
"network_ipv6_dns": "IPv6 域名服务器",
"network_ipv6_flag_dad_failed": "DAD 失败",
"network_ipv6_flag_deprecated": "已弃用",
"network_ipv6_gateway": "IPv6网关",
"network_ipv6_information": "IPv6 信息",
"network_ipv6_invalid": "IPv6 地址无效",
"network_ipv6_mode_description": "配置 IPv6 模式",
"network_ipv6_mode_dhcpv6": "DHCPv6",
"network_ipv6_mode_disabled": "已禁用",
"network_ipv6_mode_link_local": "仅限本地链路",
"network_ipv6_mode_slaac": "斯坦福直线加速器",
"network_ipv6_mode_slaac_dhcpv6": "SLAAC + DHCPv6",
"network_ipv6_mode_static": "静止的",
"network_ipv6_mode_title": "IPv6模式",
"network_ipv6_prefix": "IP 前缀",
"network_ipv6_prefix_invalid": "前缀必须介于 0 到 128 之间",
"network_ll_dp_all": "全部",
"network_ll_dp_basic": "基本的",
"network_ll_dp_description": "控制哪些 TLV 将通过链路层发现协议发送",
"network_ll_dp_disabled": "已禁用",
"network_ll_dp_title": "链路层发现协议",
"network_mac_address_copy_error": "复制 MAC 地址失败",
"network_mac_address_copy_success": "MAC 地址{ mac }已复制到剪贴板",
"network_mac_address_description": "网络接口的硬件标识符",
"network_mac_address_title": "MAC 地址",
"network_mdns_auto": "汽车",
"network_mdns_description": "控制 mDNS多播 DNS运行模式",
"network_mdns_disabled": "已禁用",
"network_mdns_ipv4_only": "仅限 IPv4",
"network_mdns_ipv6_only": "仅限 IPv6",
"network_mdns_title": "移动DNS",
"network_no_information_description": "没有可用的网络配置",
"network_no_information_headline": "网络信息",
"network_pending_dhcp_mode_change_description": "保存设置以启用 DHCP 模式并查看租约信息",
"network_pending_dhcp_mode_change_headline": "待处理的 DHCP IPv4 模式更改",
"network_save_settings": "保存设置",
"network_save_settings_apply_title": "应用网络设置",
"network_save_settings_confirm": "应用更改",
"network_save_settings_confirm_description": "将应用以下网络设置。这些更改可能需要重新启动并导致短暂断网。",
"network_save_settings_confirm_heading": "配置更改",
"network_save_settings_failed": "无法保存网络设置: {error}",
"network_save_settings_success": "网络设置已保存",
"network_settings_add_dns": "添加 DNS 服务器",
"network_settings_load_error": "无法加载网络设置: {error}",
"network_static_ipv4_header": "静态 IPv4 配置",
"network_static_ipv6_header": "静态 IPv6 配置",
"network_time_sync_description": "配置时间同步设置",
"network_time_sync_http_only": "仅 HTTP",
"network_time_sync_ntp_and_http": "NTP 和 HTTP",
"network_time_sync_ntp_only": "仅NTP",
"network_time_sync_title": "时间同步",
"network_title": "网络",
"never_seen_online": "网上没见过",
"next": "下一个",
"no_results_found": "未找到结果",
"not_applicable": "不适用",
"not_available": "不适用",
"not_found": "未找到",
"ntp_servers": "NTP 服务器",
"oh_no": "噢不!",
"online": "在线的",
"other_session_detected": "检测到另一个活动会话",
"other_session_take_over": " 每次仅支持一个活动会话。您要接管此会话吗?",
"other_session_use_here_button": "在这里使用",
"page_not_found_description": "您要查找的页面不存在。",
"paste_modal_confirm_paste": "确认粘贴",
"paste_modal_delay_between_keys": "按键之间的延迟",
"paste_modal_delay_out_of_range": "延迟必须介于{min}和{max}",
"paste_modal_failed_paste": "粘贴文本失败: {error}",
"paste_modal_invalid_chars_intro": "以下字符将不会被粘贴:",
"paste_modal_paste_from_host": "从主机粘贴",
"paste_modal_sending_using_layout": "使用键盘布局发送文本: {iso} - {name}",
"paste_text": "粘贴文本",
"paste_text_description": "将文本从客户端粘贴到远程主机",
"peer_connection_closed": "关闭",
"peer_connection_closing": "结束语",
"peer_connection_connected": "已连接",
"peer_connection_connecting": "正在连接",
"peer_connection_disconnected": "断开连接",
"peer_connection_error": "连接错误",
"peer_connection_failed": "连接失败",
"peer_connection_new": "正在连接",
"previous": "以前的",
"register_device_error": "注册您的设备时出现错误{error} 。",
"register_device_finish_button": "完成设置",
"register_device_name_description": "为您的设备命名,以便日后轻松识别。您可以随时更改此名称。",
"register_device_name_label": "设备名称",
"register_device_name_placeholder": "Plex媒体服务器",
"register_device_no_name": "请指定名称",
"rename_device": "重命名设备",
"rename_device_description": "正确命名您的设备以便轻松识别它。",
"rename_device_error": "重命名您的设备时出现错误{error} 。",
"rename_device_headline": "重命名{name}",
"rename_device_new_name_label": "新设备名称",
"rename_device_new_name_placeholder": "Plex媒体服务器",
"rename_device_no_name": "请指定名称",
"retry": "重试",
"saving": "保存…",
"search_placeholder": "搜索…",
"serial_console": "串行控制台",
"serial_console_baud_rate": "波特率",
"serial_console_configure_description": "配置串行控制台设置",
"serial_console_data_bits": "数据位",
"serial_console_get_settings_error": "无法获取串行控制台设置: {error}",
"serial_console_open_console": "打开控制台",
"serial_console_parity": "奇偶校验位",
"serial_console_parity_even": "偶校验",
"serial_console_parity_mark": "Mark",
"serial_console_parity_none": "无",
"serial_console_parity_odd": "奇校验",
"serial_console_parity_space": "Space",
"serial_console_set_settings_error": "无法将串行控制台设置设置为 {settings} : {error}",
"serial_console_stop_bits": "停止位",
"setting_remote_description": "设置远程描述",
"setting_remote_session_description": "正在设置远程会话描述...",
"setting_up_connection_to_device": "正在设置与设备的连接...",
"settings_access": "使用权",
"settings_advanced": "先进的",
"settings_appearance": "外貌",
"settings_back_to_kvm": "返回 KVM",
"settings_general": "一般的",
"settings_hardware": "硬件",
"settings_keyboard": "键盘",
"settings_keyboard_macros": "键盘宏",
"settings_mouse": "老鼠",
"settings_network": "网络",
"settings_video": "视频",
"something_went_wrong": "出了点问题。请稍后重试或联系客服",
"step_counter_step": "步骤{step}",
"subnet_mask": "子网掩码",
"time_division_days": "天",
"time_division_hours": "小时",
"time_division_minutes": "分钟",
"time_division_months": "个月",
"time_division_seconds": "秒",
"time_division_weeks": "周",
"time_division_years": "年",
"troubleshoot_connection": "连接故障排除",
"unknown_error": "未知错误",
"update_in_progress": "正在更新",
"updates_failed_check": "无法检查更新: {error}",
"updates_failed_get_device_version": "无法获取设备版本: {error}",
"updating_leave_device_on": "请不要关闭您的设备……",
"usb": "USB",
"usb_config_custom": "风俗",
"usb_config_default": "JetKVM 默认",
"usb_config_dell": "戴尔多媒体专业键盘",
"usb_config_failed_load": "无法加载 USB 配置: {error}",
"usb_config_failed_set": "无法设置 USB 配置: {error}",
"usb_config_identifiers_description": "向目标计算机公开的 USB 设备标识符",
"usb_config_identifiers_title": "标识符",
"usb_config_logitech": "罗技通用适配器",
"usb_config_manufacturer_label": "制造商",
"usb_config_manufacturer_placeholder": "输入制造商",
"usb_config_microsoft": "微软无线多媒体键盘",
"usb_config_product_id_label": "产品 ID",
"usb_config_product_id_placeholder": "输入产品编号",
"usb_config_product_name_label": "产品名称",
"usb_config_product_name_placeholder": "输入产品名称",
"usb_config_restore_default": "恢复默认设置",
"usb_config_serial_number_label": "序列号",
"usb_config_serial_number_placeholder": "输入序列号",
"usb_config_set_success": "USB 配置设置为{manufacturer} {product}",
"usb_config_update_identifiers": "更新 USB 标识符",
"usb_config_vendor_id_label": "供应商 ID",
"usb_config_vendor_id_placeholder": "输入供应商ID",
"usb_device_classes_description": "复合设备中的 USB 设备类",
"usb_device_classes_title": "课程",
"usb_device_custom": "风俗",
"usb_device_description": "在目标计算机上模拟的 USB 设备",
"usb_device_enable_absolute_mouse_description": "启用绝对鼠标(指针)",
"usb_device_enable_absolute_mouse_title": "启用绝对鼠标(指针)",
"usb_device_enable_keyboard_description": "启用键盘",
"usb_device_enable_keyboard_title": "启用键盘",
"usb_device_enable_mass_storage_description": "有时可能需要禁用它以防止某些设备出现问题",
"usb_device_enable_mass_storage_title": "启用 USB 大容量存储",
"usb_device_enable_relative_mouse_description": "启用相对鼠标",
"usb_device_enable_relative_mouse_title": "启用相对鼠标",
"usb_device_failed_load": "无法加载 USB 设备: {error}",
"usb_device_failed_set": "无法设置 USB 设备: {error}",
"usb_device_keyboard_mouse_and_mass_storage": "键盘、鼠标和大容量存储器",
"usb_device_keyboard_only": "仅限键盘",
"usb_device_restore_default": "恢复默认设置",
"usb_device_title": "USB 设备",
"usb_device_update_classes": "更新 USB 类",
"usb_device_updated": "USB 设备已更新",
"usb_state_connected": "已连接",
"usb_state_connecting": "正在连接",
"usb_state_disconnected": "断开连接",
"usb_state_low_power_mode": "低功耗模式",
"user_interface_language_description": "选择 JetKVM 用户界面使用的语言",
"user_interface_language_title": "界面语言",
"video_brightness_description": "亮度级别( {value} x",
"video_brightness_title": "亮度",
"video_contrast_description": "对比度级别( {value} x",
"video_contrast_title": "对比",
"video_custom_edid_description": "EDID 详细说明了视频模式的兼容性。大多数情况下默认设置即可正常工作,但某些 UEFI/BIOS 可能需要进行调整。",
"video_custom_edid_title": "自定义 EDID",
"video_debugging_info_description": "视频调试信息",
"video_debugging_info_title": "调试信息",
"video_description": "配置显示设置和 EDID 以实现最佳兼容性",
"video_edid_acer_b246wl": "宏碁 B246WL1920x1200",
"video_edid_asus_pa248qv": "华硕 PA248QV1920x1200",
"video_edid_custom": "风俗",
"video_edid_dell_d2721h": "戴尔 D2721H1920x1080",
"video_edid_dell_idrac": "戴尔 IDRAC EDID1280x1024",
"video_edid_description": "调整显示器的 EDID 设置",
"video_edid_file_label": "EDID文件",
"video_edid_jetkvm_default": "JetKVM 默认",
"video_edid_set_success": "EDID 成功设置为{edid}",
"video_edid_title": "EDID",
"video_enhancement_description": "调整颜色设置,使视频输出更加鲜艳多彩",
"video_enhancement_title": "视频增强",
"video_failed_get_debug_info": "无法获取调试信息: {error}",
"video_failed_get_edid": "无法获取 EDID {error}",
"video_failed_set_edid": "无法设置 EDID {error}",
"video_failed_set_stream_quality": "无法设置流质量: {error}",
"video_get_debugging_info": "获取调试信息",
"video_overlay_autoplay_permissions_required": "需要自动播放权限",
"video_overlay_conn_check_cables": "检查所有电缆连接是否有松动或损坏的电线",
"video_overlay_conn_ensure_network": "确保您的网络连接稳定且活跃",
"video_overlay_conn_restart": "尝试重新启动设备和计算机",
"video_overlay_conn_verify_power": "验证设备是否已打开电源并正确连接",
"video_overlay_connection_issue_title": "检测到连接问题",
"video_overlay_enable_autoplay_settings": "请调整浏览器设置以启用自动播放",
"video_overlay_hdmi_error_title": "检测到 HDMI 信号错误。",
"video_overlay_hdmi_incompatible_resolution": "分辨率或刷新率设置不兼容",
"video_overlay_hdmi_loose_faulty": "HDMI 连接松动或故障",
"video_overlay_hdmi_source_issue": "源设备的 HDMI 输出问题",
"video_overlay_learn_more": "了解更多",
"video_overlay_loading_stream": "正在加载视频流...",
"video_overlay_manually_start_stream": "手动启动流",
"video_overlay_no_hdmi_adapter_compat": "如果使用适配器,请确保其兼容且功能正常",
"video_overlay_no_hdmi_ensure_cable": "确保 HDMI 线缆两端牢固连接",
"video_overlay_no_hdmi_ensure_power": "确保源设备已打开并输出信号",
"video_overlay_no_hdmi_signal": "未检测到 HDMI 信号。",
"video_overlay_pointerlock_click_to_enable": "点击视频即可启用鼠标控制",
"video_overlay_retrying_connection": "正在重试连接...",
"video_overlay_troubleshooting_guide": "故障排除指南",
"video_overlay_try_again": "再试一次",
"video_pointer_lock_disabled": "指针锁定已禁用",
"video_pointer_lock_enabled": "指针锁定已启用 — 按 Esc 键解锁",
"video_quality_high": "高的",
"video_quality_low": "低的",
"video_quality_medium": "中等的",
"video_reset_to_default": "重置为默认值",
"video_restore_to_default": "恢复默认设置",
"video_saturation_description": "颜色饱和度( {value} x",
"video_saturation_title": "饱和",
"video_set_custom_edid": "设置自定义 EDID",
"video_stream_quality_description": "调整视频流的质量",
"video_stream_quality_set": "流质量设置为{quality}",
"video_stream_quality_title": "流质量",
"video_title": "视频",
"view_details": "查看详情",
"virtual_keyboard_header": "虚拟键盘",
"wake_on_lan": "局域网唤醒",
"wake_on_lan_add_device_device_name": "设备名称",
"wake_on_lan_add_device_example_device_name": "Plex媒体服务器",
"wake_on_lan_add_device_mac_address": "MAC 地址",
"wake_on_lan_add_device_save_device": "保存设备",
"wake_on_lan_description": "发送魔术包来唤醒远程设备。",
"wake_on_lan_device_list_add_new_device": "添加新设备",
"wake_on_lan_device_list_delete_device": "删除设备",
"wake_on_lan_device_list_wake": "唤醒",
"wake_on_lan_empty_add_device_to_start": "添加设备以开始使用网络唤醒",
"wake_on_lan_empty_add_new_device": "添加新设备",
"wake_on_lan_empty_no_devices_added": "未添加任何设备",
"wake_on_lan_failed_add_device": "添加设备失败",
"wake_on_lan_failed_send_magic": "发送魔术包失败",
"wake_on_lan_invalid_mac": "无效的 MAC 地址",
"wake_on_lan_magic_sent_success": "魔包发送成功",
"welcome_to_jetkvm": "欢迎来到 JetKVM",
"welcome_to_jetkvm_description": "远程控制任何计算机"
}

1945
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,22 +1,30 @@
{ {
"name": "kvm-ui", "name": "kvm-ui",
"private": true, "private": true,
"version": "2025.10.01.1900", "version": "2025.10.20.1400",
"type": "module", "type": "module",
"engines": { "engines": {
"node": "^22.15.0" "node": "^22.20.0"
}, },
"scripts": { "scripts": {
"dev": "./dev_device.sh", "dev": "./dev_device.sh",
"dev:ssl": "USE_SSL=true ./dev_device.sh", "dev:ssl": "USE_SSL=true ./dev_device.sh",
"dev:cloud": "vite dev --mode=cloud-development", "dev:cloud": "vite dev --mode=cloud-development",
"build": "npm run build:prod", "build": "npm run build:prod",
"build:device": "tsc && vite build --mode=device --emptyOutDir", "build:device": "npm run i18n:compile && tsc && vite build --mode=device --emptyOutDir",
"build:staging": "tsc && vite build --mode=cloud-staging", "build:staging": "npm run i18n:compile && tsc && vite build --mode=cloud-staging",
"build:prod": "tsc && vite build --mode=cloud-production", "build:prod": "npm run i18n:compile && tsc && vite build --mode=cloud-production",
"lint": "eslint './src/**/*.{ts,tsx}'", "lint": "npm run i18n:compile && eslint './src/**/*.{ts,tsx}'",
"lint:fix": "eslint './src/**/*.{ts,tsx}' --fix", "lint:fix": "npm run i18n:compile && eslint './src/**/*.{ts,tsx}' --fix",
"preview": "vite preview" "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 run 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"
}, },
"dependencies": { "dependencies": {
"@headlessui/react": "^2.2.9", "@headlessui/react": "^2.2.9",
@ -33,20 +41,20 @@
"dayjs": "^1.11.18", "dayjs": "^1.11.18",
"eslint-import-resolver-alias": "^1.1.2", "eslint-import-resolver-alias": "^1.1.2",
"focus-trap-react": "^11.0.4", "focus-trap-react": "^11.0.4",
"framer-motion": "^12.23.22", "framer-motion": "^12.23.24",
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
"mini-svg-data-uri": "^1.4.4", "mini-svg-data-uri": "^1.4.4",
"react": "^19.1.1", "react": "^19.2.0",
"react-animate-height": "^3.2.3", "react-animate-height": "^3.2.3",
"react-dom": "^19.1.1", "react-dom": "^19.2.0",
"react-hot-toast": "^2.6.0",
"react-hook-form": "^7.65.0", "react-hook-form": "^7.65.0",
"react-hot-toast": "^2.6.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-router": "^7.9.3", "react-router": "^7.9.4",
"react-simple-keyboard": "^3.8.125", "react-simple-keyboard": "^3.8.130",
"react-use-websocket": "^4.13.0", "react-use-websocket": "^4.13.0",
"react-xtermjs": "^1.0.10", "react-xtermjs": "^1.0.10",
"recharts": "^3.2.1", "recharts": "^3.3.0",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"usehooks-ts": "^3.1.1", "usehooks-ts": "^3.1.1",
"validator": "^13.15.15", "validator": "^13.15.15",
@ -55,32 +63,38 @@
"devDependencies": { "devDependencies": {
"@eslint/compat": "^1.4.0", "@eslint/compat": "^1.4.0",
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.36.0", "@eslint/js": "^9.38.0",
"@inlang/cli": "^3.0.12",
"@inlang/paraglide-js": "^2.4.0",
"@inlang/plugin-m-function-matcher": "^2.1.0",
"@inlang/plugin-message-format": "^4.0.0",
"@inlang/sdk": "^2.4.9",
"@tailwindcss/forms": "^0.5.10", "@tailwindcss/forms": "^0.5.10",
"@tailwindcss/postcss": "^4.1.14", "@tailwindcss/postcss": "^4.1.15",
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.14", "@tailwindcss/vite": "^4.1.15",
"@types/react": "^19.1.17", "@types/react": "^19.2.2",
"@types/react-dom": "^19.1.10", "@types/react-dom": "^19.2.2",
"@types/semver": "^7.7.1", "@types/semver": "^7.7.1",
"@types/validator": "^13.15.3", "@types/validator": "^13.15.3",
"@typescript-eslint/eslint-plugin": "^8.45.0", "@typescript-eslint/eslint-plugin": "^8.46.2",
"@typescript-eslint/parser": "^8.45.0", "@typescript-eslint/parser": "^8.46.2",
"@vitejs/plugin-react-swc": "^4.1.0", "@vitejs/plugin-react-swc": "^4.1.0",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"eslint": "^9.36.0", "eslint": "^9.38.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-import": "^2.32.0", "eslint-plugin-import": "^2.32.0",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^7.0.0",
"eslint-plugin-react-refresh": "^0.4.22", "eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.4.0", "globals": "^16.4.0",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.6.14", "prettier-plugin-tailwindcss": "^0.7.1",
"tailwindcss": "^4.1.14", "tailwindcss": "^4.1.15",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^7.1.7", "vite": "^7.1.11",
"vite-tsconfig-paths": "^5.1.4" "vite-tsconfig-paths": "^5.1.4"
} }
} }

View File

@ -1,24 +1,25 @@
import { Fragment, useCallback, useRef } from "react";
import { MdOutlineContentPasteGo } from "react-icons/md"; import { MdOutlineContentPasteGo } from "react-icons/md";
import { LuCable, LuHardDrive, LuMaximize, LuSettings, LuSignal } from "react-icons/lu"; import { LuCable, LuHardDrive, LuMaximize, LuSettings, LuSignal } from "react-icons/lu";
import { FaKeyboard } from "react-icons/fa6"; import { FaKeyboard } from "react-icons/fa6";
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react"; import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
import { Fragment, useCallback, useRef } from "react";
import { CommandLineIcon } from "@heroicons/react/20/solid"; import { CommandLineIcon } from "@heroicons/react/20/solid";
import { Button } from "@components/Button"; import { cx } from "@/cva.config";
import { import {
useHidStore, useHidStore,
useMountMediaStore, useMountMediaStore,
useSettingsStore, useSettingsStore,
useUiStore, useUiStore,
} from "@/hooks/stores"; } from "@hooks/stores";
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
import { Button } from "@components/Button";
import Container from "@components/Container"; import Container from "@components/Container";
import { cx } from "@/cva.config"; import PasteModal from "@components/popovers/PasteModal";
import PasteModal from "@/components/popovers/PasteModal"; import WakeOnLanModal from "@components/popovers/WakeOnLan/Index";
import WakeOnLanModal from "@/components/popovers/WakeOnLan/Index"; import MountPopopover from "@components/popovers/MountPopover";
import MountPopopover from "@/components/popovers/MountPopover"; import ExtensionPopover from "@components/popovers/ExtensionPopover";
import ExtensionPopover from "@/components/popovers/ExtensionPopover"; import { m } from "@localizations/messages.js";
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
export default function Actionbar({ export default function Actionbar({
requestFullscreen, requestFullscreen,
@ -28,10 +29,7 @@ export default function Actionbar({
const { navigateTo } = useDeviceUiNavigation(); const { navigateTo } = useDeviceUiNavigation();
const { isVirtualKeyboardEnabled, setVirtualKeyboardEnabled } = useHidStore(); const { isVirtualKeyboardEnabled, setVirtualKeyboardEnabled } = useHidStore();
const { setDisableVideoFocusTrap, terminalType, setTerminalType, toggleSidebarView } = useUiStore(); const { setDisableVideoFocusTrap, terminalType, setTerminalType, toggleSidebarView } = useUiStore();
const { remoteVirtualMediaState } = useMountMediaStore();
const remoteVirtualMediaState = useMountMediaStore(
state => state.remoteVirtualMediaState,
);
const { developerMode } = useSettingsStore(); const { developerMode } = useSettingsStore();
// This is the only way to get a reliable state change for the popover // This is the only way to get a reliable state change for the popover
@ -64,7 +62,7 @@ export default function Actionbar({
<Button <Button
size="XS" size="XS"
theme="light" theme="light"
text="Web Terminal" text={m.action_bar_web_terminal()}
LeadingIcon={({ className }) => <CommandLineIcon className={className} />} LeadingIcon={({ className }) => <CommandLineIcon className={className} />}
onClick={() => setTerminalType(terminalType === "kvm" ? "none" : "kvm")} onClick={() => setTerminalType(terminalType === "kvm" ? "none" : "kvm")}
/> />
@ -74,7 +72,7 @@ export default function Actionbar({
<Button <Button
size="XS" size="XS"
theme="light" theme="light"
text="Paste text" text={m.paste_text()}
LeadingIcon={MdOutlineContentPasteGo} LeadingIcon={MdOutlineContentPasteGo}
onClick={() => { onClick={() => {
setDisableVideoFocusTrap(true); setDisableVideoFocusTrap(true);
@ -105,7 +103,7 @@ export default function Actionbar({
<Button <Button
size="XS" size="XS"
theme="light" theme="light"
text="Virtual Media" text={m.action_bar_virtual_media()}
LeadingIcon={({ className }) => { LeadingIcon={({ className }) => {
return ( return (
<> <>
@ -148,7 +146,7 @@ export default function Actionbar({
<Button <Button
size="XS" size="XS"
theme="light" theme="light"
text="Wake on LAN" text={m.action_bar_wake_on_lan()}
onClick={() => { onClick={() => {
setDisableVideoFocusTrap(true); setDisableVideoFocusTrap(true);
}} }}
@ -198,7 +196,7 @@ export default function Actionbar({
<Button <Button
size="XS" size="XS"
theme="light" theme="light"
text="Virtual Keyboard" text={m.action_bar_virtual_keyboard()}
LeadingIcon={FaKeyboard} LeadingIcon={FaKeyboard}
onClick={() => setVirtualKeyboardEnabled(!isVirtualKeyboardEnabled)} onClick={() => setVirtualKeyboardEnabled(!isVirtualKeyboardEnabled)}
/> />
@ -211,7 +209,7 @@ export default function Actionbar({
<Button <Button
size="XS" size="XS"
theme="light" theme="light"
text="Extension" text={m.action_bar_extension()}
LeadingIcon={LuCable} LeadingIcon={LuCable}
onClick={() => { onClick={() => {
setDisableVideoFocusTrap(true); setDisableVideoFocusTrap(true);
@ -237,7 +235,7 @@ export default function Actionbar({
<Button <Button
size="XS" size="XS"
theme="light" theme="light"
text="Virtual Keyboard" text={m.action_bar_virtual_keyboard()}
LeadingIcon={FaKeyboard} LeadingIcon={FaKeyboard}
onClick={() => setVirtualKeyboardEnabled(!isVirtualKeyboardEnabled)} onClick={() => setVirtualKeyboardEnabled(!isVirtualKeyboardEnabled)}
/> />
@ -246,7 +244,7 @@ export default function Actionbar({
<Button <Button
size="XS" size="XS"
theme="light" theme="light"
text="Connection Stats" text={m.action_bar_connection_stats()}
LeadingIcon={({ className }) => ( LeadingIcon={({ className }) => (
<LuSignal <LuSignal
className={cx(className, "mb-0.5 text-green-500")} className={cx(className, "mb-0.5 text-green-500")}
@ -262,7 +260,7 @@ export default function Actionbar({
<Button <Button
size="XS" size="XS"
theme="light" theme="light"
text="Settings" text={m.action_bar_settings()}
LeadingIcon={LuSettings} LeadingIcon={LuSettings}
onClick={() => { onClick={() => {
setDisableVideoFocusTrap(true); setDisableVideoFocusTrap(true);
@ -276,7 +274,7 @@ export default function Actionbar({
<Button <Button
size="XS" size="XS"
theme="light" theme="light"
text="Fullscreen" text={m.action_bar_fullscreen()}
LeadingIcon={LuMaximize} LeadingIcon={LuMaximize}
onClick={() => requestFullscreen()} onClick={() => requestFullscreen()}
/> />

View File

@ -1,10 +1,9 @@
import React, { JSX } from "react"; import React, { JSX } from "react";
import { Link, useNavigation } from "react-router"; import { Link, type FetcherWithComponents, type LinkProps, useNavigation } from "react-router";
import type { FetcherWithComponents, LinkProps } from "react-router";
import ExtLink from "@/components/ExtLink";
import LoadingSpinner from "@/components/LoadingSpinner";
import { cva, cx } from "@/cva.config"; import { cva, cx } from "@/cva.config";
import ExtLink from "@components/ExtLink";
import LoadingSpinner from "@components/LoadingSpinner";
const sizes = { const sizes = {
XS: "h-[28px] px-2 text-xs", XS: "h-[28px] px-2 text-xs",

View File

@ -2,7 +2,7 @@ import type { Ref } from "react";
import React, { forwardRef, JSX } from "react"; import React, { forwardRef, JSX } from "react";
import clsx from "clsx"; import clsx from "clsx";
import FieldLabel from "@/components/FieldLabel"; import FieldLabel from "@components/FieldLabel";
import { cva, cx } from "@/cva.config"; import { cva, cx } from "@/cva.config";
const sizes = { const sizes = {

View File

@ -7,10 +7,10 @@ import {
ComboboxOptions, ComboboxOptions,
} from "@headlessui/react"; } from "@headlessui/react";
import { m } from "@localizations/messages.js";
import Card from "@components/Card";
import { cva } from "@/cva.config"; import { cva } from "@/cva.config";
import Card from "./Card";
export interface ComboboxOption { export interface ComboboxOption {
value: string; value: string;
label: string; label: string;
@ -44,11 +44,11 @@ export function Combobox({
displayValue, displayValue,
options, options,
disabled = false, disabled = false,
placeholder = "Search...", placeholder = m.search_placeholder(),
emptyMessage = "No results found", emptyMessage = m.no_results_found(),
size = "MD", size = "MD",
onChange, onChange,
disabledMessage = "Input disabled", disabledMessage = m.input_disabled(),
...otherProps ...otherProps
}: ComboboxProps) { }: ComboboxProps) {
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);

View File

@ -1,8 +1,9 @@
import { CloseButton } from "@headlessui/react"; import { CloseButton } from "@headlessui/react";
import { LuCircleAlert, LuInfo, LuTriangleAlert } from "react-icons/lu"; import { LuCircleAlert, LuInfo, LuTriangleAlert } from "react-icons/lu";
import { Button } from "@/components/Button"; import { m } from "@localizations/messages.js";
import Modal from "@/components/Modal"; import { Button } from "@components/Button";
import Modal from "@components/Modal";
import { cx } from "@/cva.config"; import { cx } from "@/cva.config";
type Variant = "danger" | "success" | "warning" | "info"; type Variant = "danger" | "success" | "warning" | "info";
@ -55,8 +56,8 @@ export function ConfirmDialog({
title, title,
description, description,
variant = "info", variant = "info",
confirmText = "Confirm", confirmText = m.confirm(),
cancelText = "Cancel", cancelText = m.cancel(),
onConfirm, onConfirm,
isConfirming = false, isConfirming = false,
}: ConfirmDialogProps) { }: ConfirmDialogProps) {

View File

@ -1,11 +1,12 @@
import { LuRefreshCcw } from "react-icons/lu"; import { LuRefreshCcw } from "react-icons/lu";
import { Button } from "@/components/Button"; import { Button } from "@components/Button";
import { GridCard } from "@/components/Card"; import EmptyCard from "@components/EmptyCard";
import { LifeTimeLabel } from "@/routes/devices.$id.settings.network"; import { GridCard } from "@components/Card";
import { NetworkState } from "@/hooks/stores"; import { LifeTimeLabel } from "@routes/devices.$id.settings.network";
import { NetworkState } from "@hooks/stores";
import { m } from "@localizations/messages.js";
import EmptyCard from "./EmptyCard";
export default function DhcpLeaseCard({ export default function DhcpLeaseCard({
networkState, networkState,
@ -19,8 +20,8 @@ export default function DhcpLeaseCard({
if (isDhcpLeaseEmpty) { if (isDhcpLeaseEmpty) {
return ( return (
<EmptyCard <EmptyCard
headline="No DHCP Lease information" headline={m.dhcp_empty_lease_headline()}
description="We haven't received any DHCP lease information from the device yet." description={m.dhcp_empty_lease_description()}
/> />
); );
} }
@ -31,7 +32,7 @@ export default function DhcpLeaseCard({
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h3 className="text-base font-bold text-slate-900 dark:text-white"> <h3 className="text-base font-bold text-slate-900 dark:text-white">
DHCP Lease Information {m.dhcp_lease_header()}
</h3> </h3>
<div> <div>
@ -40,7 +41,7 @@ export default function DhcpLeaseCard({
theme="light" theme="light"
type="button" type="button"
className="text-red-500" className="text-red-500"
text="Renew DHCP Lease" text={m.dhcp_lease_renew()}
LeadingIcon={LuRefreshCcw} LeadingIcon={LuRefreshCcw}
onClick={() => setShowRenewLeaseConfirm(true)} onClick={() => setShowRenewLeaseConfirm(true)}
/> />
@ -52,8 +53,8 @@ export default function DhcpLeaseCard({
{networkState?.dhcp_lease?.ip && ( {networkState?.dhcp_lease?.ip && (
<div className="flex justify-between border-slate-800/10 pt-2 dark:border-slate-300/20"> <div className="flex justify-between border-slate-800/10 pt-2 dark:border-slate-300/20">
<span className="text-sm text-slate-600 dark:text-slate-400"> <span className="text-sm text-slate-600 dark:text-slate-400">
IP Address {m.ip_address()}
</span> </span>&nbsp;
<span className="text-sm font-medium"> <span className="text-sm font-medium">
{networkState?.dhcp_lease?.ip} {networkState?.dhcp_lease?.ip}
</span> </span>
@ -63,8 +64,8 @@ export default function DhcpLeaseCard({
{networkState?.dhcp_lease?.netmask && ( {networkState?.dhcp_lease?.netmask && (
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20"> <div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
<span className="text-sm text-slate-600 dark:text-slate-400"> <span className="text-sm text-slate-600 dark:text-slate-400">
Subnet Mask {m.subnet_mask()}
</span> </span>&nbsp;
<span className="text-sm font-medium"> <span className="text-sm font-medium">
{networkState?.dhcp_lease?.netmask} {networkState?.dhcp_lease?.netmask}
</span> </span>
@ -74,8 +75,8 @@ export default function DhcpLeaseCard({
{networkState?.dhcp_lease?.dns_servers && ( {networkState?.dhcp_lease?.dns_servers && (
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20"> <div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
<span className="text-sm text-slate-600 dark:text-slate-400"> <span className="text-sm text-slate-600 dark:text-slate-400">
DNS Servers {m.dns_servers()}
</span> </span>&nbsp;
<span className="text-right text-sm font-medium"> <span className="text-right text-sm font-medium">
{networkState?.dhcp_lease?.dns_servers.map(dns => ( {networkState?.dhcp_lease?.dns_servers.map(dns => (
<div key={dns}>{dns}</div> <div key={dns}>{dns}</div>
@ -84,11 +85,22 @@ export default function DhcpLeaseCard({
</div> </div>
)} )}
{networkState?.dhcp_lease?.broadcast && (
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
<span className="text-sm text-slate-600 dark:text-slate-400">
{m.dhcp_lease_broadcast()}
</span>&nbsp;
<span className="text-sm font-medium">
{networkState?.dhcp_lease?.broadcast}
</span>
</div>
)}
{networkState?.dhcp_lease?.domain && ( {networkState?.dhcp_lease?.domain && (
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20"> <div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
<span className="text-sm text-slate-600 dark:text-slate-400"> <span className="text-sm text-slate-600 dark:text-slate-400">
Domain {m.dhcp_lease_domain()}
</span> </span>&nbsp;
<span className="text-sm font-medium"> <span className="text-sm font-medium">
{networkState?.dhcp_lease?.domain} {networkState?.dhcp_lease?.domain}
</span> </span>
@ -99,8 +111,8 @@ export default function DhcpLeaseCard({
networkState?.dhcp_lease?.ntp_servers.length > 0 && ( networkState?.dhcp_lease?.ntp_servers.length > 0 && (
<div className="flex justify-between gap-x-8 border-t border-slate-800/10 pt-2 dark:border-slate-300/20"> <div className="flex justify-between gap-x-8 border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
<div className="w-full grow text-sm text-slate-600 dark:text-slate-400"> <div className="w-full grow text-sm text-slate-600 dark:text-slate-400">
NTP Servers {m.ntp_servers()}
</div> </div>&nbsp;
<div className="shrink text-right text-sm font-medium"> <div className="shrink text-right text-sm font-medium">
{networkState?.dhcp_lease?.ntp_servers.map(server => ( {networkState?.dhcp_lease?.ntp_servers.map(server => (
<div key={server}>{server}</div> <div key={server}>{server}</div>
@ -112,8 +124,8 @@ export default function DhcpLeaseCard({
{networkState?.dhcp_lease?.hostname && ( {networkState?.dhcp_lease?.hostname && (
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20"> <div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
<span className="text-sm text-slate-600 dark:text-slate-400"> <span className="text-sm text-slate-600 dark:text-slate-400">
Hostname {m.dhcp_lease_hostname()}
</span> </span>&nbsp;
<span className="text-sm font-medium"> <span className="text-sm font-medium">
{networkState?.dhcp_lease?.hostname} {networkState?.dhcp_lease?.hostname}
</span> </span>
@ -126,8 +138,8 @@ export default function DhcpLeaseCard({
networkState?.dhcp_lease?.routers.length > 0 && ( networkState?.dhcp_lease?.routers.length > 0 && (
<div className="flex justify-between pt-2"> <div className="flex justify-between pt-2">
<span className="text-sm text-slate-600 dark:text-slate-400"> <span className="text-sm text-slate-600 dark:text-slate-400">
Gateway {m.dhcp_lease_gateway()}
</span> </span>&nbsp;
<span className="text-right text-sm font-medium"> <span className="text-right text-sm font-medium">
{networkState?.dhcp_lease?.routers.map(router => ( {networkState?.dhcp_lease?.routers.map(router => (
<div key={router}>{router}</div> <div key={router}>{router}</div>
@ -139,8 +151,8 @@ export default function DhcpLeaseCard({
{networkState?.dhcp_lease?.server_id && ( {networkState?.dhcp_lease?.server_id && (
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20"> <div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
<span className="text-sm text-slate-600 dark:text-slate-400"> <span className="text-sm text-slate-600 dark:text-slate-400">
DHCP Server {m.dhcp_server()}
</span> </span>&nbsp;
<span className="text-sm font-medium"> <span className="text-sm font-medium">
{networkState?.dhcp_lease?.server_id} {networkState?.dhcp_lease?.server_id}
</span> </span>
@ -150,8 +162,8 @@ export default function DhcpLeaseCard({
{networkState?.dhcp_lease?.lease_expiry && ( {networkState?.dhcp_lease?.lease_expiry && (
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20"> <div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
<span className="text-sm text-slate-600 dark:text-slate-400"> <span className="text-sm text-slate-600 dark:text-slate-400">
Lease Expires {m.dhcp_lease_lease_expires()}
</span> </span>&nbsp;
<span className="text-sm font-medium"> <span className="text-sm font-medium">
<LifeTimeLabel <LifeTimeLabel
lifetime={`${networkState?.dhcp_lease?.lease_expiry}`} lifetime={`${networkState?.dhcp_lease?.lease_expiry}`}
@ -163,8 +175,8 @@ export default function DhcpLeaseCard({
{networkState?.dhcp_lease?.broadcast && ( {networkState?.dhcp_lease?.broadcast && (
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20"> <div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
<span className="text-sm text-slate-600 dark:text-slate-400"> <span className="text-sm text-slate-600 dark:text-slate-400">
Broadcast {m.dhcp_lease_broadcast()}
</span> </span>&nbsp;
<span className="text-sm font-medium"> <span className="text-sm font-medium">
{networkState?.dhcp_lease?.broadcast} {networkState?.dhcp_lease?.broadcast}
</span> </span>
@ -173,7 +185,9 @@ export default function DhcpLeaseCard({
{networkState?.dhcp_lease?.mtu && ( {networkState?.dhcp_lease?.mtu && (
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20"> <div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
<span className="text-sm text-slate-600 dark:text-slate-400">MTU</span> <span className="text-sm text-slate-600 dark:text-slate-400">
{m.dhcp_lease_maximum_transfer_unit()}
</span>&nbsp;
<span className="text-sm font-medium"> <span className="text-sm font-medium">
{networkState?.dhcp_lease?.mtu} {networkState?.dhcp_lease?.mtu}
</span> </span>
@ -182,7 +196,9 @@ export default function DhcpLeaseCard({
{networkState?.dhcp_lease?.ttl && ( {networkState?.dhcp_lease?.ttl && (
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20"> <div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
<span className="text-sm text-slate-600 dark:text-slate-400">TTL</span> <span className="text-sm text-slate-600 dark:text-slate-400">
{m.dhcp_lease_time_to_live()}
</span>&nbsp;
<span className="text-sm font-medium"> <span className="text-sm font-medium">
{networkState?.dhcp_lease?.ttl} {networkState?.dhcp_lease?.ttl}
</span> </span>
@ -192,8 +208,8 @@ export default function DhcpLeaseCard({
{networkState?.dhcp_lease?.bootp_next_server && ( {networkState?.dhcp_lease?.bootp_next_server && (
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20"> <div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
<span className="text-sm text-slate-600 dark:text-slate-400"> <span className="text-sm text-slate-600 dark:text-slate-400">
Boot Next Server {m.dhcp_lease_boot_next_server()}
</span> </span>&nbsp;
<span className="text-sm font-medium"> <span className="text-sm font-medium">
{networkState?.dhcp_lease?.bootp_next_server} {networkState?.dhcp_lease?.bootp_next_server}
</span> </span>
@ -203,8 +219,8 @@ export default function DhcpLeaseCard({
{networkState?.dhcp_lease?.bootp_server_name && ( {networkState?.dhcp_lease?.bootp_server_name && (
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20"> <div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
<span className="text-sm text-slate-600 dark:text-slate-400"> <span className="text-sm text-slate-600 dark:text-slate-400">
Boot Server Name {m.dhcp_lease_boot_server_name()}
</span> </span>&nbsp;
<span className="text-sm font-medium"> <span className="text-sm font-medium">
{networkState?.dhcp_lease?.bootp_server_name} {networkState?.dhcp_lease?.bootp_server_name}
</span> </span>
@ -214,8 +230,8 @@ export default function DhcpLeaseCard({
{networkState?.dhcp_lease?.bootp_file && ( {networkState?.dhcp_lease?.bootp_file && (
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20"> <div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
<span className="text-sm text-slate-600 dark:text-slate-400"> <span className="text-sm text-slate-600 dark:text-slate-400">
Boot File {m.dhcp_lease_boot_file()}
</span> </span>&nbsp;
<span className="text-sm font-medium"> <span className="text-sm font-medium">
{networkState?.dhcp_lease?.bootp_file} {networkState?.dhcp_lease?.bootp_file}
</span> </span>
@ -224,8 +240,12 @@ export default function DhcpLeaseCard({
{networkState?.dhcp_lease?.dhcp_client && ( {networkState?.dhcp_lease?.dhcp_client && (
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20"> <div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
<span className="text-sm text-slate-600 dark:text-slate-400">DHCP Client</span> <span className="text-sm text-slate-600 dark:text-slate-400">
<span className="text-sm font-medium">{networkState?.dhcp_lease?.dhcp_client}</span> {m.network_dhcp_client_title()}
</span>&nbsp;
<span className="text-sm font-medium">
{networkState?.dhcp_lease?.dhcp_client}
</span>
</div> </div>
)} )}
</div> </div>

View File

@ -1,8 +1,7 @@
import React from "react"; import React from "react";
import { GridCard } from "@/components/Card"; import { GridCard } from "@components/Card";
import { cx } from "@/cva.config";
import { cx } from "../cva.config";
interface Props { interface Props {
IconElm?: React.FC<{ className: string | undefined }>; IconElm?: React.FC<{ className: string | undefined }>;

View File

@ -1,6 +1,6 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { useFeatureFlag } from "../hooks/useFeatureFlag"; import { useFeatureFlag } from "@hooks/useFeatureFlag";
export function FeatureFlag({ export function FeatureFlag({
minAppVersion, minAppVersion,

View File

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import clsx from "clsx";
import { useNavigation } from "react-router"; import { useNavigation } from "react-router";
import type { FetcherWithComponents } from "react-router"; import type { FetcherWithComponents } from "react-router";
import clsx from "clsx";
export default function Fieldset({ export default function Fieldset({
children, children,

View File

@ -4,19 +4,18 @@ import { ArrowLeftEndOnRectangleIcon, ChevronDownIcon } from "@heroicons/react/1
import { Button, Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react"; import { Button, Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
import { LuMonitorSmartphone } from "react-icons/lu"; import { LuMonitorSmartphone } from "react-icons/lu";
import Container from "@/components/Container"; import LogoBlueIcon from "@assets/logo-blue.svg";
import Card from "@/components/Card"; import LogoWhiteIcon from "@assets/logo-white.svg";
import { useHidStore, useRTCStore, useUserStore } from "@/hooks/stores"; import { useHidStore, useRTCStore, useUserStore } from "@hooks/stores";
import LogoBlueIcon from "@/assets/logo-blue.svg"; import Card from "@components/Card";
import LogoWhiteIcon from "@/assets/logo-white.svg"; import Container from "@components/Container";
import USBStateStatus from "@components/USBStateStatus"; import { LinkButton } from "@components/Button";
import PeerConnectionStatusCard from "@components/PeerConnectionStatusCard"; import PeerConnectionStatusCard from "@components/PeerConnectionStatusCard";
import USBStateStatus from "@components/USBStateStatus";
import { CLOUD_API, DEVICE_API } from "@/ui.config"; import { CLOUD_API, DEVICE_API } from "@/ui.config";
import api from "@/api";
import api from "../api"; import { isOnDevice } from "@/main";
import { isOnDevice } from "../main"; import { m } from "@localizations/messages.js";
import { LinkButton } from "./Button";
interface NavbarProps { interface NavbarProps {
isLoggedIn: boolean; isLoggedIn: boolean;
@ -131,7 +130,7 @@ export default function DashboardNavbar({
<div className="border-b border-b-slate-800/20 dark:border-slate-300/20"> <div className="border-b border-b-slate-800/20 dark:border-slate-300/20">
<div className="p-2"> <div className="p-2">
<div className="font-display text-xs"> <div className="font-display text-xs">
Logged in as {m.logged_in_as()}
</div> </div>
<div className="font-display max-w-[200px] truncate text-sm font-semibold"> <div className="font-display max-w-[200px] truncate text-sm font-semibold">
{userEmail} {userEmail}
@ -146,7 +145,7 @@ export default function DashboardNavbar({
> >
<button className="group flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700"> <button className="group flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors hover:bg-slate-100 dark:hover:bg-slate-700">
<ArrowLeftEndOnRectangleIcon className="size-4" /> <ArrowLeftEndOnRectangleIcon className="size-4" />
<div className="font-display">Log out</div> <div className="font-display">{m.log_out()}</div>
</button> </button>
</div> </div>
</Card> </Card>

View File

@ -1,6 +1,5 @@
import { useEffect, useMemo } from "react"; import { useMemo } from "react";
import { cx } from "@/cva.config";
import { import {
useHidStore, useHidStore,
useMouseStore, useMouseStore,
@ -8,9 +7,11 @@ import {
useSettingsStore, useSettingsStore,
useVideoStore, useVideoStore,
VideoState VideoState
} from "@/hooks/stores"; } from "@hooks/stores";
import { useHidRpc } from "@hooks/useHidRpc";
import { keys, modifiers } from "@/keyboardMappings"; import { keys, modifiers } from "@/keyboardMappings";
import { useHidRpc } from "@/hooks/useHidRpc"; import { cx } from "@/cva.config";
import { m } from "@localizations/messages.js";
export default function InfoBar() { export default function InfoBar() {
const { keysDownState } = useHidStore(); const { keysDownState } = useHidStore();
@ -25,29 +26,23 @@ export default function InfoBar() {
(state: VideoState) => `${Math.round(state.width)}x${Math.round(state.height)}`, (state: VideoState) => `${Math.round(state.width)}x${Math.round(state.height)}`,
); );
const { rpcDataChannel } = useRTCStore();
const { debugMode, mouseMode, showPressedKeys } = useSettingsStore(); const { debugMode, mouseMode, showPressedKeys } = useSettingsStore();
const { isPasteInProgress } = useHidStore(); 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 { keyboardLedState, usbState } = useHidStore();
const { isTurnServerInUse } = useRTCStore(); const { isTurnServerInUse } = useRTCStore();
const { hdmiState } = useVideoStore(); const { hdmiState } = useVideoStore();
const displayKeys = useMemo(() => { const displayKeys = useMemo(() => {
if (!showPressedKeys) if (!showPressedKeys) return "";
return "";
const activeModifierMask = keysDownState.modifier || 0; const activeModifierMask = keysDownState.modifier || 0;
const keysDown = keysDownState.keys || []; const keysDown = keysDownState.keys || [];
const modifierNames = Object.entries(modifiers).filter(([_, mask]) => (activeModifierMask & mask) !== 0).map(([name, _]) => name); const modifierNames = Object.entries(modifiers)
const keyNames = Object.entries(keys).filter(([_, value]) => keysDown.includes(value)).map(([name, _]) => name); .filter(([_, mask]) => (activeModifierMask & mask) !== 0)
.map(([name]) => name);
const keyNames = Object.entries(keys)
.filter(([_, value]) => keysDown.includes(value))
.map(([name]) => name);
return [...modifierNames, ...keyNames].join(", "); return [...modifierNames, ...keyNames].join(", ");
}, [keysDownState, showPressedKeys]); }, [keysDownState, showPressedKeys]);
@ -59,76 +54,75 @@ export default function InfoBar() {
<div className="flex flex-wrap items-center pl-2 gap-x-4"> <div className="flex flex-wrap items-center pl-2 gap-x-4">
{debugMode ? ( {debugMode ? (
<div className="flex"> <div className="flex">
<span className="text-xs font-semibold">Resolution:</span>{" "} <span className="text-xs font-semibold">{m.info_resolution()}</span>{" "}
<span className="text-xs">{videoSize}</span> <span className="text-xs">{videoSize}</span>
</div> </div>
) : null} ) : null}
{debugMode ? ( {debugMode ? (
<div className="flex"> <div className="flex">
<span className="text-xs font-semibold">Video Size: </span> <span className="text-xs font-semibold">{m.info_video_size()}</span>
<span className="text-xs">{videoClientSize}</span> <span className="text-xs">{videoClientSize}</span>
</div> </div>
) : null} ) : null}
{(debugMode && mouseMode == "absolute") ? ( {(debugMode && mouseMode == "absolute") ? (
<div className="flex w-[118px] items-center gap-x-1"> <div className="flex w-[118px] items-center gap-x-1">
<span className="text-xs font-semibold">Pointer:</span> <span className="text-xs font-semibold">{m.info_pointer()}</span>
<span className="text-xs"> <span className="text-xs">{mouseX},{mouseY}</span>
{mouseX},{mouseY}
</span>
</div> </div>
) : null} ) : null}
{(debugMode && mouseMode == "relative") ? ( {(debugMode && mouseMode == "relative") ? (
<div className="flex w-[118px] items-center gap-x-1"> <div className="flex w-[118px] items-center gap-x-1">
<span className="text-xs font-semibold">Last Move:</span> <span className="text-xs font-semibold">{m.info_last_move()}</span>
<span className="text-xs"> <span className="text-xs">
{mouseMove ? {mouseMove ? `${mouseMove.x},${mouseMove.y} ${mouseMove.buttons ? `(${mouseMove.buttons})` : ""}` : "N/A"}
`${mouseMove.x},${mouseMove.y} ${mouseMove.buttons ? `(${mouseMove.buttons})` : ""}` :
"N/A"}
</span> </span>
</div> </div>
) : null} ) : null}
{debugMode && ( {debugMode && (
<div className="flex w-[156px] items-center gap-x-1"> <div className="flex w-[156px] items-center gap-x-1">
<span className="text-xs font-semibold">USB State:</span> <span className="text-xs font-semibold">{m.info_usb_state()}</span>
<span className="text-xs">{usbState}</span> <span className="text-xs">{usbState}</span>
</div> </div>
)} )}
{debugMode && ( {debugMode && (
<div className="flex w-[156px] items-center gap-x-1"> <div className="flex w-[156px] items-center gap-x-1">
<span className="text-xs font-semibold">HDMI State:</span> <span className="text-xs font-semibold">{m.info_hdmi_state()}</span>
<span className="text-xs">{hdmiState}</span> <span className="text-xs">{hdmiState}</span>
</div> </div>
)} )}
{debugMode && ( {debugMode && (
<div className="flex w-[156px] items-center gap-x-1"> <div className="flex w-[156px] items-center gap-x-1">
<span className="text-xs font-semibold">HidRPC State:</span> <span className="text-xs font-semibold">{m.info_hidrpc_state()}</span>
<span className="text-xs">{rpcHidStatus}</span> <span className="text-xs">{rpcHidStatus}</span>
</div> </div>
)} )}
{isPasteInProgress && ( {isPasteInProgress && (
<div className="flex w-[156px] items-center gap-x-1"> <div className="flex w-[156px] items-center gap-x-1">
<span className="text-xs font-semibold">Paste Mode:</span> <span className="text-xs font-semibold">{m.info_paste_mode()}</span>
<span className="text-xs">Enabled</span> <span className="text-xs">{m.info_paste_enabled()}</span>
</div> </div>
)} )}
{showPressedKeys && ( {showPressedKeys && (
<div className="flex items-center gap-x-1"> <div className="flex items-center gap-x-1">
<span className="text-xs font-semibold">Keys:</span> <span className="text-xs font-semibold">{m.info_keys()}</span>
<h2 className="text-xs"> <h2 className="text-xs">{displayKeys}</h2>
{displayKeys}
</h2>
</div> </div>
)} )}
</div> </div>
</div> </div>
<div className="flex items-center divide-x first:divide-l divide-slate-800/20 dark:divide-slate-300/20"> <div className="flex items-center divide-x first:divide-l divide-slate-800/20 dark:divide-slate-300/20">
{isTurnServerInUse && ( {isTurnServerInUse && (
<div className="shrink-0 p-1 px-1.5 text-xs text-black dark:text-white"> <div className="shrink-0 p-1 px-1.5 text-xs text-black dark:text-white">
Relayed by Cloudflare {m.info_relayed_by_cloudflare()}
</div> </div>
)} )}
@ -140,8 +134,9 @@ export default function InfoBar() {
: "text-slate-800/20 dark:text-slate-300/20", : "text-slate-800/20 dark:text-slate-300/20",
)} )}
> >
Caps Lock {m.info_caps_lock()}
</div> </div>
<div <div
className={cx( className={cx(
"shrink-0 p-1 px-1.5 text-xs", "shrink-0 p-1 px-1.5 text-xs",
@ -150,8 +145,9 @@ export default function InfoBar() {
: "text-slate-800/20 dark:text-slate-300/20", : "text-slate-800/20 dark:text-slate-300/20",
)} )}
> >
Num Lock {m.info_num_lock()}
</div> </div>
<div <div
className={cx( className={cx(
"shrink-0 p-1 px-1.5 text-xs", "shrink-0 p-1 px-1.5 text-xs",
@ -160,22 +156,19 @@ export default function InfoBar() {
: "text-slate-800/20 dark:text-slate-300/20", : "text-slate-800/20 dark:text-slate-300/20",
)} )}
> >
Scroll Lock {m.info_scroll_lock()}
</div> </div>
{keyboardLedState.compose ? ( {keyboardLedState.compose ? (
<div className="shrink-0 p-1 px-1.5 text-xs"> <div className="shrink-0 p-1 px-1.5 text-xs">{m.info_compose()}</div>
Compose
</div>
) : null} ) : null}
{keyboardLedState.kana ? ( {keyboardLedState.kana ? (
<div className="shrink-0 p-1 px-1.5 text-xs"> <div className="shrink-0 p-1 px-1.5 text-xs">{m.info_kana()}</div>
Kana
</div>
) : null} ) : null}
{keyboardLedState.shift ? ( {keyboardLedState.shift ? (
<div className="shrink-0 p-1 px-1.5 text-xs"> <div className="shrink-0 p-1 px-1.5 text-xs">{m.info_shift()}</div>
Shift
</div>
) : null} ) : null}
</div> </div>
</div> </div>

View File

@ -1,9 +1,8 @@
import type { Ref } from "react"; import React, { forwardRef, JSX, type Ref } from "react";
import React, { forwardRef, JSX } from "react";
import clsx from "clsx"; import clsx from "clsx";
import FieldLabel from "@/components/FieldLabel"; import FieldLabel from "@components/FieldLabel";
import Card from "@/components/Card"; import Card from "@components/Card";
import { cva } from "@/cva.config"; import { cva } from "@/cva.config";
const sizes = { const sizes = {

View File

@ -1,9 +1,8 @@
import { cx } from "@/cva.config"; import { cx } from "@/cva.config";
import { NetworkState } from "@hooks/stores";
import { NetworkState } from "../hooks/stores"; import { GridCard } from "@components/Card";
import { LifeTimeLabel } from "../routes/devices.$id.settings.network"; import { LifeTimeLabel } from "@routes/devices.$id.settings.network";
import { m } from "@localizations/messages.js";
import { GridCard } from "./Card";
export function FlagLabel({ flag, className }: { flag: string, className?: string }) { export function FlagLabel({ flag, className }: { flag: string, className?: string }) {
const classes = cx( const classes = cx(
@ -27,22 +26,23 @@ export default function Ipv6NetworkCard({
<div className="animate-fadeIn p-4 text-black opacity-0 animation-duration-500 dark:text-white"> <div className="animate-fadeIn p-4 text-black opacity-0 animation-duration-500 dark:text-white">
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-base font-bold text-slate-900 dark:text-white"> <h3 className="text-base font-bold text-slate-900 dark:text-white">
IPv6 Information {m.ipv6_information()}
</h3> </h3>
<div className="grid grid-cols-2 gap-x-6 gap-y-2"> <div className="grid grid-cols-2 gap-x-6 gap-y-2">
<div className="flex flex-col justify-between"> <div className="flex flex-col justify-between">
<span className="text-sm text-slate-600 dark:text-slate-400"> <span className="text-sm text-slate-600 dark:text-slate-400">
Link-local {m.ipv6_link_local()}
</span> </span>&nbsp;
<span className="text-sm font-medium"> <span className="text-sm font-medium">
{networkState?.ipv6_link_local} {networkState?.ipv6_link_local}
</span> </span>
</div> </div>
<div className="flex flex-col justify-between"> <div className="flex flex-col justify-between">
<span className="text-sm text-slate-600 dark:text-slate-400"> <span className="text-sm text-slate-600 dark:text-slate-400">
Gateway {m.ipv6_gateway()}
</span> </span>&nbsp;
<span className="text-sm font-medium"> <span className="text-sm font-medium">
{networkState?.ipv6_gateway} {networkState?.ipv6_gateway}
</span> </span>
@ -52,7 +52,9 @@ export default function Ipv6NetworkCard({
<div className="space-y-3 pt-2"> <div className="space-y-3 pt-2">
{networkState?.ipv6_addresses && networkState?.ipv6_addresses.length > 0 && ( {networkState?.ipv6_addresses && networkState?.ipv6_addresses.length > 0 && (
<div className="space-y-3"> <div className="space-y-3">
<h4 className="text-sm font-semibold">IPv6 Addresses</h4> <h4 className="text-sm font-semibold">
{m.network_ipv6_addresses_header()}
</h4>
{networkState.ipv6_addresses.map(addr => ( {networkState.ipv6_addresses.map(addr => (
<div <div
key={addr.address} key={addr.address}
@ -61,13 +63,13 @@ export default function Ipv6NetworkCard({
<div className="grid grid-cols-2 gap-x-8 gap-y-4"> <div className="grid grid-cols-2 gap-x-8 gap-y-4">
<div className="col-span-2 flex flex-col justify-between"> <div className="col-span-2 flex flex-col justify-between">
<span className="text-sm text-slate-600 dark:text-slate-400"> <span className="text-sm text-slate-600 dark:text-slate-400">
Address {m.ipv6_address_label()}
</span> </span>&nbsp;
<span className="text-sm font-medium flex"> <span className="text-sm font-medium flex">
<span className="flex-1">{addr.address}</span> <span className="flex-1">{addr.address}</span>&nbsp;
<span className="text-sm font-medium flex gap-x-1"> <span className="text-sm font-medium flex gap-x-1">
{addr.flag_deprecated ? <FlagLabel flag="Deprecated" /> : null} {addr.flag_deprecated ? <FlagLabel flag={m.network_ipv6_flag_deprecated()} /> : null}
{addr.flag_dad_failed ? <FlagLabel flag="DAD Failed" /> : null} {addr.flag_dad_failed ? <FlagLabel flag={m.network_ipv6_flag_dad_failed()} /> : null}
</span> </span>
</span> </span>
</div> </div>
@ -75,12 +77,12 @@ export default function Ipv6NetworkCard({
{addr.valid_lifetime && ( {addr.valid_lifetime && (
<div className="flex flex-col justify-between"> <div className="flex flex-col justify-between">
<span className="text-sm text-slate-600 dark:text-slate-400"> <span className="text-sm text-slate-600 dark:text-slate-400">
Valid Lifetime {m.ipv6_valid_lifetime()}
</span> </span>&nbsp;
<span className="text-sm font-medium"> <span className="text-sm font-medium">
{addr.valid_lifetime === "" ? ( {addr.valid_lifetime === "" ? (
<span className="text-slate-400 dark:text-slate-600"> <span className="text-slate-400 dark:text-slate-600">
N/A {m.not_applicable()}
</span> </span>
) : ( ) : (
<LifeTimeLabel lifetime={`${addr.valid_lifetime}`} /> <LifeTimeLabel lifetime={`${addr.valid_lifetime}`} />
@ -88,15 +90,16 @@ export default function Ipv6NetworkCard({
</span> </span>
</div> </div>
)} )}
{addr.preferred_lifetime && ( {addr.preferred_lifetime && (
<div className="flex flex-col justify-between"> <div className="flex flex-col justify-between">
<span className="text-sm text-slate-600 dark:text-slate-400"> <span className="text-sm text-slate-600 dark:text-slate-400">
Preferred Lifetime {m.ipv6_preferred_lifetime()}
</span> </span>&nbsp;
<span className="text-sm font-medium"> <span className="text-sm font-medium">
{addr.preferred_lifetime === "" ? ( {addr.preferred_lifetime === "" ? (
<span className="text-slate-400 dark:text-slate-600"> <span className="text-slate-400 dark:text-slate-600">
N/A {m.not_applicable()}
</span> </span>
) : ( ) : (
<LifeTimeLabel lifetime={`${addr.preferred_lifetime}`} /> <LifeTimeLabel lifetime={`${addr.preferred_lifetime}`} />

View File

@ -1,11 +1,11 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { LuExternalLink } from "react-icons/lu"; import { LuExternalLink } from "react-icons/lu";
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import { Button, LinkButton } from "@components/Button"; import { Button, LinkButton } from "@components/Button";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc"; import { InputFieldWithLabel } from "@components/InputField";
import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { InputFieldWithLabel } from "./InputField"; import { m } from "@localizations/messages.js";
import { SelectMenuBasic } from "./SelectMenuBasic";
export interface JigglerConfig { export interface JigglerConfig {
inactivity_limit_seconds: number; inactivity_limit_seconds: number;
@ -51,7 +51,7 @@ export function JigglerSetting({
const exampleConfigs = [ const exampleConfigs = [
{ {
name: "Business Hours 9-17", name: m.jiggler_example_business_hours_late(),
config: { config: {
inactivity_limit_seconds: 60, inactivity_limit_seconds: 60,
jitter_percentage: 25, jitter_percentage: 25,
@ -60,7 +60,7 @@ export function JigglerSetting({
}, },
}, },
{ {
name: "Business Hours 8-17", name: m.jiggler_example_business_hours_early(),
config: { config: {
inactivity_limit_seconds: 60, inactivity_limit_seconds: 60,
jitter_percentage: 25, jitter_percentage: 25,
@ -69,13 +69,10 @@ export function JigglerSetting({
}, },
}, },
]; ];
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<h4 className="text-sm font-semibold text-gray-900 dark:text-gray-100"> <h4 className="text-sm font-semibold text-gray-900 dark:text-gray-100">{m.jiggler_examples_label()}</h4>
Examples
</h4>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{exampleConfigs.map((example, index) => ( {exampleConfigs.map((example, index) => (
<Button <Button
@ -90,7 +87,7 @@ export function JigglerSetting({
to="https://crontab.guru/examples.html" to="https://crontab.guru/examples.html"
size="XS" size="XS"
theme="light" theme="light"
text="More examples" text={m.jiggler_more_examples()}
LeadingIcon={LuExternalLink} LeadingIcon={LuExternalLink}
/> />
</div> </div>
@ -100,8 +97,8 @@ export function JigglerSetting({
<InputFieldWithLabel <InputFieldWithLabel
required required
size="SM" size="SM"
label="Cron Schedule" label={m.jiggler_cron_schedule_label()}
description="Cron expression for scheduling" description={m.jiggler_cron_schedule_description()}
placeholder="*/20 * * * * *" placeholder="*/20 * * * * *"
value={jigglerConfigState.schedule_cron_tab} value={jigglerConfigState.schedule_cron_tab}
onChange={e => onChange={e =>
@ -114,8 +111,8 @@ export function JigglerSetting({
<InputFieldWithLabel <InputFieldWithLabel
size="SM" size="SM"
label="Inactivity Limit Seconds" label={m.jiggler_inactivity_limit_label()}
description="Inactivity time before jiggle" description={m.jiggler_inactivity_limit_description()}
value={jigglerConfigState.inactivity_limit_seconds} value={jigglerConfigState.inactivity_limit_seconds}
type="number" type="number"
min="1" min="1"
@ -131,8 +128,8 @@ export function JigglerSetting({
<InputFieldWithLabel <InputFieldWithLabel
required required
size="SM" size="SM"
label="Random delay" label={m.jiggler_random_delay_label()}
description="To avoid recognizable patterns" description={m.jiggler_random_delay_description()}
placeholder="25" placeholder="25"
TrailingElm={<span className="px-2 text-xs text-slate-500">%</span>} TrailingElm={<span className="px-2 text-xs text-slate-500">%</span>}
value={jigglerConfigState.jitter_percentage} value={jigglerConfigState.jitter_percentage}
@ -149,8 +146,8 @@ export function JigglerSetting({
<SelectMenuBasic <SelectMenuBasic
size="SM" size="SM"
label="Timezone" label={m.jiggler_timezone_label()}
description="Timezone for cron schedule" description={m.jiggler_timezone_description()}
value={jigglerConfigState.timezone || "UTC"} value={jigglerConfigState.timezone || "UTC"}
disabled={timezones.length === 0} disabled={timezones.length === 0}
onChange={e => onChange={e =>
@ -167,7 +164,7 @@ export function JigglerSetting({
<Button <Button
size="SM" size="SM"
theme="primary" theme="primary"
text="Save Jiggler Config" text={m.jiggler_save_jiggler_config()}
onClick={() => onSave(jigglerConfigState)} onClick={() => onSave(jigglerConfigState)}
/> />
</div> </div>

View File

@ -1,10 +1,11 @@
import { Link } from "react-router";
import { MdConnectWithoutContact } from "react-icons/md"; import { MdConnectWithoutContact } from "react-icons/md";
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react"; import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
import { Link } from "react-router";
import { LuEllipsisVertical } from "react-icons/lu"; import { LuEllipsisVertical } from "react-icons/lu";
import Card from "@components/Card"; import Card from "@components/Card";
import { Button, LinkButton } from "@components/Button"; import { Button, LinkButton } from "@components/Button";
import { m } from "@localizations/messages.js";
function getRelativeTimeString(date: Date | number, lang = navigator.language): string { function getRelativeTimeString(date: Date | number, lang = navigator.language): string {
// Allow dates or times to be passed // Allow dates or times to be passed
@ -62,16 +63,16 @@ export default function KvmCard({
{online ? ( {online ? (
<div className="flex items-center gap-x-1.5"> <div className="flex items-center gap-x-1.5">
<div className="h-2.5 w-2.5 rounded-full border border-green-600 bg-green-500" /> <div className="h-2.5 w-2.5 rounded-full border border-green-600 bg-green-500" />
<div className="text-sm text-black dark:text-white">Online</div> <div className="text-sm text-black dark:text-white">{m.online()}</div>
</div> </div>
) : ( ) : (
<div className="flex items-center gap-x-1.5"> <div className="flex items-center gap-x-1.5">
<div className="h-2.5 w-2.5 rounded-full border border-slate-400/60 dark:border-slate-500 bg-slate-200 dark:bg-slate-600" /> <div className="h-2.5 w-2.5 rounded-full border border-slate-400/60 dark:border-slate-500 bg-slate-200 dark:bg-slate-600" />
<div className="text-sm text-black dark:text-white"> <div className="text-sm text-black dark:text-white">
{lastSeen ? ( {lastSeen ? (
<>Last online {getRelativeTimeString(lastSeen)}</> <>{m.last_online({ time: getRelativeTimeString(lastSeen) })}</>
) : ( ) : (
<>Never seen online</> <>{m.never_seen_online()}</>
)} )}
</div> </div>
</div> </div>
@ -85,7 +86,7 @@ export default function KvmCard({
<LinkButton <LinkButton
size="MD" size="MD"
theme="light" theme="light"
text="Connect to KVM" text={m.connect_to_kvm()}
LeadingIcon={MdConnectWithoutContact} LeadingIcon={MdConnectWithoutContact}
textAlign="center" textAlign="center"
to={`/devices/${id}`} to={`/devices/${id}`}
@ -94,7 +95,7 @@ export default function KvmCard({
<Button <Button
size="MD" size="MD"
theme="light" theme="light"
text="Troubleshoot Connection" text={m.troubleshoot_connection()}
textAlign="center" textAlign="center"
/> />
)} )}
@ -120,7 +121,7 @@ export default function KvmCard({
className="block w-full py-1.5 text-black dark:text-white" className="block w-full py-1.5 text-black dark:text-white"
to={`./${id}/rename`} to={`./${id}/rename`}
> >
Rename {m.rename_device()}
</Link> </Link>
</div> </div>
</div> </div>
@ -134,7 +135,7 @@ export default function KvmCard({
className="block w-full py-1.5 text-black dark:text-white" className="block w-full py-1.5 text-black dark:text-white"
to={`./${id}/deregister`} to={`./${id}/deregister`}
> >
Deregister from cloud {m.deregister_from_cloud()}
</Link> </Link>
</div> </div>
</div> </div>

View File

@ -1,11 +1,11 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { LuCommand } from "react-icons/lu"; import { LuCommand } from "react-icons/lu";
import { useMacrosStore } from "@hooks/stores";
import useKeyboard from "@hooks/useKeyboard";
import { useJsonRpc } from "@hooks/useJsonRpc";
import { Button } from "@components/Button"; import { Button } from "@components/Button";
import Container from "@components/Container"; import Container from "@components/Container";
import { useMacrosStore } from "@/hooks/stores";
import useKeyboard from "@/hooks/useKeyboard";
import { useJsonRpc } from "@/hooks/useJsonRpc";
export default function MacroBar() { export default function MacroBar() {
const { macros, initialized, loadMacros, setSendFn } = useMacrosStore(); const { macros, initialized, loadMacros, setSendFn } = useMacrosStore();

View File

@ -1,18 +1,19 @@
import { useState } from "react"; import { useState } from "react";
import { LuPlus } from "react-icons/lu"; import { LuPlus } from "react-icons/lu";
import { Button } from "@/components/Button"; import { KeySequence } from "@hooks/stores";
import FieldLabel from "@/components/FieldLabel"; import useKeyboardLayout from "@hooks/useKeyboardLayout";
import Fieldset from "@/components/Fieldset"; import { Button } from "@components/Button";
import { InputFieldWithLabel, FieldError } from "@/components/InputField"; import FieldLabel from "@components/FieldLabel";
import { MacroStepCard } from "@/components/MacroStepCard"; import Fieldset from "@components/Fieldset";
import { InputFieldWithLabel, FieldError } from "@components/InputField";
import { MacroStepCard } from "@components/MacroStepCard";
import { import {
DEFAULT_DELAY, DEFAULT_DELAY,
MAX_STEPS_PER_MACRO, MAX_STEPS_PER_MACRO,
MAX_KEYS_PER_STEP, MAX_KEYS_PER_STEP,
} from "@/constants/macros"; } from "@/constants/macros";
import { KeySequence } from "@/hooks/stores"; import { m } from "@localizations/messages.js";
import useKeyboardLayout from "@/hooks/useKeyboardLayout";
interface ValidationErrors { interface ValidationErrors {
name?: string; name?: string;
@ -31,7 +32,6 @@ interface MacroFormProps {
onSubmit: (macro: Partial<KeySequence>) => Promise<void>; onSubmit: (macro: Partial<KeySequence>) => Promise<void>;
onCancel: () => void; onCancel: () => void;
isSubmitting?: boolean; isSubmitting?: boolean;
submitText?: string;
} }
export function MacroForm({ export function MacroForm({
@ -39,8 +39,7 @@ export function MacroForm({
onSubmit, onSubmit,
onCancel, onCancel,
isSubmitting = false, isSubmitting = false,
submitText = "Save Macro", }: Readonly<MacroFormProps>) {
}: MacroFormProps) {
const [macro, setMacro] = useState<Partial<KeySequence>>(initialData); const [macro, setMacro] = useState<Partial<KeySequence>>(initialData);
const [keyQueries, setKeyQueries] = useState<Record<number, string>>({}); const [keyQueries, setKeyQueries] = useState<Record<number, string>>({});
const [errors, setErrors] = useState<ValidationErrors>({}); const [errors, setErrors] = useState<ValidationErrors>({});
@ -57,23 +56,25 @@ export function MacroForm({
// Name validation // Name validation
if (!macro.name?.trim()) { if (!macro.name?.trim()) {
newErrors.name = "Name is required"; newErrors.name = m.macro_name_required();
} else if (macro.name.trim().length > 50) { } else if (macro.name.trim().length > 50) {
newErrors.name = "Name must be less than 50 characters"; newErrors.name = m.macro_name_too_long();
} }
if (!macro.steps?.length) { const steps = (macro.steps || []);
newErrors.steps = { 0: { keys: "At least one step is required" } };
} else { if (steps.length) {
const hasKeyOrModifier = macro.steps.some( const hasKeyOrModifier = steps.some(
step => (step.keys?.length || 0) > 0 || (step.modifiers?.length || 0) > 0, step => step.keys.length > 0 || step.modifiers.length > 0,
); );
if (!hasKeyOrModifier) { if (!hasKeyOrModifier) {
newErrors.steps = { newErrors.steps = {
0: { keys: "At least one step must have keys or modifiers" }, 0: { keys: m.macro_at_least_one_step_keys_or_modifiers() },
}; };
} }
} else {
newErrors.steps = { 0: { keys: m.macro_at_least_one_step_required() } };
} }
setErrors(newErrors); setErrors(newErrors);
@ -82,7 +83,7 @@ export function MacroForm({
const handleSubmit = async () => { const handleSubmit = async () => {
if (!validateForm()) { if (!validateForm()) {
showTemporaryError("Please fix the validation errors"); showTemporaryError(m.macro_please_fix_validation_errors());
return; return;
} }
@ -90,9 +91,9 @@ export function MacroForm({
await onSubmit(macro); await onSubmit(macro);
} catch (error) { } catch (error) {
if (error instanceof Error) { if (error instanceof Error) {
showTemporaryError(error.message); showTemporaryError(m.macro_save_failed_error({error: error.message || m.unknown_error()}));
} else { } else {
showTemporaryError("An error occurred while saving"); showTemporaryError(m.macro_save_failed());
} }
} }
}; };
@ -105,16 +106,16 @@ export function MacroForm({
if (!newSteps[stepIndex]) return; if (!newSteps[stepIndex]) return;
if (option.keys) { if (option.keys) {
// they gave us a full set of keys (e.g. from deleting one)
newSteps[stepIndex].keys = option.keys; newSteps[stepIndex].keys = option.keys;
} else if (option.value) { } else if (option.value) {
// they gave us a single key to add
if (!newSteps[stepIndex].keys) { if (!newSteps[stepIndex].keys) {
newSteps[stepIndex].keys = []; newSteps[stepIndex].keys = [];
} }
const keysArray = Array.isArray(newSteps[stepIndex].keys) const keysArray = newSteps[stepIndex].keys;
? newSteps[stepIndex].keys
: [];
if (keysArray.length >= MAX_KEYS_PER_STEP) { if (keysArray.length >= MAX_KEYS_PER_STEP) {
showTemporaryError(`Maximum of ${MAX_KEYS_PER_STEP} keys per step allowed`); showTemporaryError(m.macro_max_steps_error({ max: MAX_KEYS_PER_STEP }));
return; return;
} }
newSteps[stepIndex].keys = [...keysArray, option.value]; newSteps[stepIndex].keys = [...keysArray, option.value];
@ -173,13 +174,12 @@ export function MacroForm({
const isMaxStepsReached = (macro.steps?.length || 0) >= MAX_STEPS_PER_MACRO; const isMaxStepsReached = (macro.steps?.length || 0) >= MAX_STEPS_PER_MACRO;
return ( return (
<>
<div className="space-y-4"> <div className="space-y-4">
<Fieldset> <Fieldset>
<InputFieldWithLabel <InputFieldWithLabel
type="text" type="text"
label="Macro Name" label={m.macro_name_label()}
placeholder="Macro Name" placeholder={m.macro_name_label()}
value={macro.name} value={macro.name}
error={errors.name} error={errors.name}
onChange={e => { onChange={e => {
@ -197,24 +197,24 @@ export function MacroForm({
<div className="flex items-center justify-between text-sm"> <div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<FieldLabel <FieldLabel
label="Steps" label={m.macro_steps_label()}
description={`Keys/modifiers executed in sequence with a delay between each step.`} description={m.macro_steps_description()}
/> />
</div> </div>
<span className="text-slate-500 dark:text-slate-400"> <span className="text-slate-500 dark:text-slate-400">
{macro.steps?.length || 0}/{MAX_STEPS_PER_MACRO} steps {m.macro_step_count({ steps: macro.steps?.length || 0, max: MAX_STEPS_PER_MACRO })}
</span> </span>
</div> </div>
{errors.steps && errors.steps[0]?.keys && ( {errors.steps?.[0]?.keys && (
<div className="mt-2"> <div className="mt-2">
<FieldError error={errors.steps[0].keys} /> <FieldError error={errors.steps?.[0]?.keys} />
</div> </div>
)} )}
<Fieldset> <Fieldset>
<div className="mt-2 space-y-4"> <div className="mt-2 space-y-4">
{(macro.steps || []).map((step, stepIndex) => ( {(macro.steps || []).map((step, stepIndex) => (
<MacroStepCard <MacroStepCard
key={stepIndex} key={`step-${stepIndex}`}
step={step} step={step}
stepIndex={stepIndex} stepIndex={stepIndex}
onDelete={ onDelete={
@ -248,12 +248,10 @@ export function MacroForm({
theme="light" theme="light"
fullWidth fullWidth
LeadingIcon={LuPlus} LeadingIcon={LuPlus}
text={`Add Step ${isMaxStepsReached ? `(${MAX_STEPS_PER_MACRO} max)` : ""}`} text={m.macro_add_step({ maxed_out: isMaxStepsReached ? m.macro_max_steps_reached({ max: MAX_STEPS_PER_MACRO }) : "" })}
onClick={() => { onClick={() => {
if (isMaxStepsReached) { if (isMaxStepsReached) {
showTemporaryError( showTemporaryError(m.macro_max_steps_error({ max: MAX_STEPS_PER_MACRO }));
`You can only add a maximum of ${MAX_STEPS_PER_MACRO} steps per macro.`,
);
return; return;
} }
@ -280,14 +278,13 @@ export function MacroForm({
<Button <Button
size="SM" size="SM"
theme="primary" theme="primary"
text={isSubmitting ? "Saving..." : submitText} text={isSubmitting ? m.saving() : m.macro_save()}
onClick={handleSubmit} onClick={handleSubmit}
disabled={isSubmitting} disabled={isSubmitting}
/> />
<Button size="SM" theme="light" text="Cancel" onClick={onCancel} /> <Button size="SM" theme="light" text={m.cancel()} onClick={onCancel} />
</div> </div>
</div> </div>
</div> </div>
</>
); );
} }

View File

@ -1,17 +1,18 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { LuArrowUp, LuArrowDown, LuX, LuTrash2 } from "react-icons/lu"; import { LuArrowUp, LuArrowDown, LuX, LuTrash2 } from "react-icons/lu";
import { Button } from "@/components/Button"; import { Button } from "@components/Button";
import { Combobox, ComboboxOption } from "@/components/Combobox"; import { Combobox, ComboboxOption } from "@components/Combobox";
import { SelectMenuBasic } from "@/components/SelectMenuBasic"; import Card from "@components/Card";
import Card from "@/components/Card"; import FieldLabel from "@components/FieldLabel";
import FieldLabel from "@/components/FieldLabel"; import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { MAX_KEYS_PER_STEP, DEFAULT_DELAY } from "@/constants/macros"; import { MAX_KEYS_PER_STEP, DEFAULT_DELAY } from "@/constants/macros";
import { KeyboardLayout } from "@/keyboardLayouts"; import { KeyboardLayout } from "@/keyboardLayouts";
import { keys, modifiers } from "@/keyboardMappings"; import { keys, modifiers } from "@/keyboardMappings";
import { m } from "@localizations/messages.js";
// Filter out modifier keys since they're handled in the modifiers section // Filter out modifier keys since they're handled in the modifiers section
const modifierKeyPrefixes = ['Alt', 'Control', 'Shift', 'Meta']; const modifierKeyPrefixes = ["Alt", "Control", "Shift", "Meta"];
const modifierOptions = Object.keys(modifiers).map(modifier => ({ const modifierOptions = Object.keys(modifiers).map(modifier => ({
value: modifier, value: modifier,
@ -19,12 +20,13 @@ const modifierOptions = Object.keys(modifiers).map(modifier => ({
})); }));
const groupedModifiers: Record<string, typeof modifierOptions> = { const groupedModifiers: Record<string, typeof modifierOptions> = {
Control: modifierOptions.filter(mod => mod.value.startsWith('Control')), Control: modifierOptions.filter(mod => mod.value.startsWith("Control")),
Shift: modifierOptions.filter(mod => mod.value.startsWith('Shift')), Shift: modifierOptions.filter(mod => mod.value.startsWith("Shift")),
Alt: modifierOptions.filter(mod => mod.value.startsWith('Alt')), Alt: modifierOptions.filter(mod => mod.value.startsWith("Alt")),
Meta: modifierOptions.filter(mod => mod.value.startsWith('Meta')), Meta: modifierOptions.filter(mod => mod.value.startsWith("Meta")),
}; };
// not going to localize these since they're short time intervals
const basePresetDelays = [ const basePresetDelays = [
{ value: "50", label: "50ms" }, { value: "50", label: "50ms" },
{ value: "100", label: "100ms" }, { value: "100", label: "100ms" },
@ -38,7 +40,7 @@ const basePresetDelays = [
]; ];
const PRESET_DELAYS = basePresetDelays.map(delay => { const PRESET_DELAYS = basePresetDelays.map(delay => {
if (parseInt(delay.value, 10) === DEFAULT_DELAY) { if (Number.parseInt(delay.value, 10) === DEFAULT_DELAY) {
return { ...delay, label: "Default" }; return { ...delay, label: "Default" };
} }
return delay; return delay;
@ -62,13 +64,17 @@ interface MacroStepCardProps {
onModifierChange: (modifiers: string[]) => void; onModifierChange: (modifiers: string[]) => void;
onDelayChange: (delay: number) => void; onDelayChange: (delay: number) => void;
isLastStep: boolean; isLastStep: boolean;
keyboard: KeyboardLayout keyboard: KeyboardLayout;
} }
const ensureArray = <T,>(arr: T[] | null | undefined): T[] => { const ensureArray = <T,>(arr: T[] | null | undefined): T[] => {
return Array.isArray(arr) ? arr : []; return Array.isArray(arr) ? arr : [];
}; };
const keyDisplay = (keyDisplayMap: Record<string, string>, key: string): string => {
return keyDisplayMap[key] || key
};
export function MacroStepCard({ export function MacroStepCard({
step, step,
stepIndex, stepIndex,
@ -81,26 +87,42 @@ export function MacroStepCard({
onModifierChange, onModifierChange,
onDelayChange, onDelayChange,
isLastStep, isLastStep,
keyboard keyboard,
}: MacroStepCardProps) { }: Readonly<MacroStepCardProps>) {
const { keyDisplayMap } = keyboard; const { keyDisplayMap } = keyboard;
const keyOptions = useMemo(() => const keyOptions = useMemo(
() =>
Object.keys(keys) Object.keys(keys)
.filter(key => !modifierKeyPrefixes.some(prefix => key.startsWith(prefix))) .filter(key => !modifierKeyPrefixes.some(prefix => key.startsWith(prefix)))
.map(key => ({ .map(key => ({
value: key, value: key,
label: keyDisplayMap[key] || key, label: keyDisplay(keyDisplayMap, key),
})), })),
[keyDisplayMap] [keyDisplayMap],
); );
const handleModifierToggle = (optionValue: string) => {
const modifiersArray = ensureArray(step.modifiers);
const isSelected = modifiersArray.includes(optionValue);
const newModifiers = isSelected
? modifiersArray.filter(m => m !== optionValue)
: [...modifiersArray, optionValue];
onModifierChange(newModifiers);
};
const filteredKeys = useMemo(() => { const filteredKeys = useMemo(() => {
const selectedKeys = ensureArray(step.keys); const selectedKeys = ensureArray(step.keys);
const availableKeys = keyOptions.filter(option => !selectedKeys.includes(option.value)); const availableKeys = keyOptions.filter(
if (keyQuery === '') { option => !selectedKeys.includes(option.value),
);
if (keyQuery === "") {
return availableKeys; return availableKeys;
} else { } else {
return availableKeys.filter(option => option.label.toLowerCase().includes(keyQuery.toLowerCase())); return availableKeys.filter(option =>
option.label.toLowerCase().includes(keyQuery.toLowerCase()),
);
} }
}, [keyOptions, keyQuery, step.keys]); }, [keyOptions, keyQuery, step.keys]);
@ -135,7 +157,7 @@ export function MacroStepCard({
size="XS" size="XS"
theme="light" theme="light"
className="text-red-500 dark:text-red-400" className="text-red-500 dark:text-red-400"
text="Delete" text={m.delete()}
LeadingIcon={LuTrash2} LeadingIcon={LuTrash2}
onClick={onDelete} onClick={onDelete}
/> />
@ -143,13 +165,19 @@ export function MacroStepCard({
</div> </div>
</div> </div>
<div className="space-y-4 mt-2"> <div className="mt-2 space-y-4">
<div className="w-full flex flex-col gap-2"> <div className="flex w-full flex-col gap-2">
<FieldLabel label="Modifiers" /> <FieldLabel
label={m.macro_step_modifiers_label()}
description={m.macro_step_modifiers_description()}
/>
<div className="inline-flex flex-wrap gap-3"> <div className="inline-flex flex-wrap gap-3">
{Object.entries(groupedModifiers).map(([group, mods]) => ( {Object.entries(groupedModifiers).map(([group, mods]) => (
<div key={group} className="relative min-w-[120px] rounded-md border border-slate-200 dark:border-slate-700 p-2"> <div
<span className="absolute -top-2.5 left-2 px-1 text-xs font-medium bg-white dark:bg-slate-800 text-slate-500 dark:text-slate-400"> key={group}
className="relative min-w-[120px] rounded-md border border-slate-200 p-2 dark:border-slate-700"
>
<span className="absolute -top-2.5 left-2 bg-white px-1 text-xs font-medium text-slate-500 dark:bg-slate-800 dark:text-slate-400">
{group} {group}
</span> </span>
<div className="flex flex-wrap gap-4 pt-1"> <div className="flex flex-wrap gap-4 pt-1">
@ -157,16 +185,13 @@ export function MacroStepCard({
<Button <Button
key={option.value} key={option.value}
size="XS" size="XS"
theme={ensureArray(step.modifiers).includes(option.value) ? "primary" : "light"} theme={
text={option.label.split(' ')[1] || option.label} ensureArray(step.modifiers).includes(option.value)
onClick={() => { ? "primary"
const modifiersArray = ensureArray(step.modifiers); : "light"
const isSelected = modifiersArray.includes(option.value); }
const newModifiers = isSelected text={option.label.split(" ")[1] || option.label}
? modifiersArray.filter(m => m !== option.value) onClick={() => handleModifierToggle(option.value)}
: [...modifiersArray, option.value];
onModifierChange(newModifiers);
}}
/> />
))} ))}
</div> </div>
@ -174,26 +199,31 @@ export function MacroStepCard({
))} ))}
</div> </div>
</div> </div>
<div className="w-full flex flex-col gap-1">
<div className="flex w-full flex-col gap-1">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<FieldLabel label="Keys" description={`Maximum ${MAX_KEYS_PER_STEP} keys per step.`} /> <FieldLabel
label={m.macro_step_keys_label()}
description={m.macro_step_keys_description({ max: MAX_KEYS_PER_STEP })}
/>
</div> </div>
{ensureArray(step.keys) && step.keys.length > 0 && (
{step.keys?.length > 0 && (
<div className="flex flex-wrap gap-1 pb-2"> <div className="flex flex-wrap gap-1 pb-2">
{step.keys.map((key, keyIndex) => ( {step.keys.map((key, keyIndex) => (
<span <span
key={keyIndex} key={`key-${keyIndex}`}
className="inline-flex items-center py-0.5 rounded-md bg-blue-100 px-1 text-xs font-medium text-blue-800 dark:bg-blue-900/40 dark:text-blue-200" 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"> <span className="px-1">{keyDisplay(keyDisplayMap, key)}</span>
{keyDisplayMap[key] || key}
</span>
<Button <Button
size="XS" size="XS"
className="" className=""
theme="blank" theme="blank"
onClick={() => { onClick={() => {
const newKeys = ensureArray(step.keys).filter((_, i) => i !== keyIndex); const newKeys = step.keys.filter(
(_, i) => i !== keyIndex,
);
onKeySelect({ value: null, keys: newKeys }); onKeySelect({ value: null, keys: newKeys });
}} }}
LeadingIcon={LuX} LeadingIcon={LuX}
@ -204,33 +234,41 @@ export function MacroStepCard({
)} )}
<div className="relative w-full"> <div className="relative w-full">
<Combobox <Combobox
onChange={(option) => { onChange={option => {
const selectedOption = option as ComboboxOption | null; const selectedOption = option as ComboboxOption | null;
onKeySelect({ value: selectedOption?.value ?? null }); onKeySelect({ value: selectedOption?.value ?? null });
onKeyQueryChange(''); onKeyQueryChange("");
}} }}
displayValue={() => keyQuery} displayValue={() => keyQuery}
onInputChange={onKeyQueryChange} onInputChange={onKeyQueryChange}
options={() => filteredKeys} options={() => filteredKeys}
disabledMessage="Max keys reached" disabledMessage={m.macro_step_max_keys_reached({ max: MAX_KEYS_PER_STEP })}
size="SM" size="SM"
immediate immediate
disabled={ensureArray(step.keys).length >= MAX_KEYS_PER_STEP} disabled={ensureArray(step.keys).length >= MAX_KEYS_PER_STEP}
placeholder={ensureArray(step.keys).length >= MAX_KEYS_PER_STEP ? "Max keys reached" : "Search for key..."} placeholder={
emptyMessage="No matching keys found" ensureArray(step.keys).length >= MAX_KEYS_PER_STEP
? m.macro_step_max_keys_reached()
: m.macro_step_search_for_key()
}
emptyMessage={m.macro_step_no_matching_keys_found()}
/> />
</div> </div>
</div> </div>
<div className="w-full flex flex-col gap-1">
<div className="flex w-full flex-col gap-1">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<FieldLabel label="Step Duration" description="Time to wait before executing the next step." /> <FieldLabel
label={m.macro_step_duration_label()}
description={m.macro_step_duration_description()}
/>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<SelectMenuBasic <SelectMenuBasic
size="SM" size="SM"
fullWidth fullWidth
value={step.delay.toString()} value={step.delay.toString()}
onChange={(e) => onDelayChange(parseInt(e.target.value, 10))} onChange={e => onDelayChange(Number.parseInt(e.target.value, 10))}
options={PRESET_DELAYS} options={PRESET_DELAYS}
/> />
</div> </div>

View File

@ -2,10 +2,10 @@
import { ComponentProps } from "react"; import { ComponentProps } from "react";
import { cva, cx } from "cva"; import { cva, cx } from "cva";
import { someIterable } from "../utils"; import { GridCard } from "@components/Card";
import MetricsChart from "@components/MetricsChart";
import { GridCard } from "./Card"; import { someIterable } from "@/utils";
import MetricsChart from "./MetricsChart"; import { m } from "@localizations/messages.js";
interface ChartPoint { interface ChartPoint {
date: number; date: number;
@ -159,7 +159,7 @@ export function Metric<T, K extends keyof T>({
> >
{!ready ? ( {!ready ? (
<div className="flex flex-col items-center space-y-1"> <div className="flex flex-col items-center space-y-1">
<p className="text-slate-700">Waiting for data...</p> <p className="text-slate-700">{m.metric_waiting_for_data()}</p>
</div> </div>
) : supportedFinal ? ( ) : supportedFinal ? (
<MetricsChart <MetricsChart
@ -170,7 +170,7 @@ export function Metric<T, K extends keyof T>({
/> />
) : ( ) : (
<div className="flex flex-col items-center space-y-1"> <div className="flex flex-col items-center space-y-1">
<p className="text-black">Metric not supported</p> <p className="text-black">{m.metric_not_supported()}</p>
</div> </div>
)} )}
</div> </div>

View File

@ -10,6 +10,7 @@ import {
YAxis, YAxis,
} from "recharts"; } from "recharts";
import { getLocale } from '@localizations/runtime.js';
import CustomTooltip, { CustomTooltipProps } from "@components/CustomTooltip"; import CustomTooltip, { CustomTooltipProps } from "@components/CustomTooltip";
export default function MetricsChart({ export default function MetricsChart({
@ -51,7 +52,7 @@ export default function MetricsChart({
axisLine={{ stroke: "rgba(30, 41, 59, 0.3)" }} axisLine={{ stroke: "rgba(30, 41, 59, 0.3)" }}
tickLine={{ stroke: "rgba(30, 41, 59, 0.3)" }} tickLine={{ stroke: "rgba(30, 41, 59, 0.3)" }}
tickFormatter={date => { tickFormatter={date => {
return new Date(date * 1000).toLocaleString("en-US", { return new Date(date * 1000).toLocaleString(getLocale() || "en-US", {
hourCycle: "h23", hourCycle: "h23",
hour: "numeric", hour: "numeric",
minute: "2-digit", minute: "2-digit",

View File

@ -1,6 +1,7 @@
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
import EmptyCard from "@/components/EmptyCard"; import EmptyCard from "@components/EmptyCard";
import { m } from "@localizations/messages.js";
export default function NotFoundPage() { export default function NotFoundPage() {
return ( return (
@ -9,8 +10,8 @@ export default function NotFoundPage() {
<div className="w-full max-w-2xl"> <div className="w-full max-w-2xl">
<EmptyCard <EmptyCard
IconElm={ExclamationTriangleIcon} IconElm={ExclamationTriangleIcon}
headline="Not found" headline={m.not_found()}
description="The page you were looking for does not exist." description={m.page_not_found_description()}
/> />
</div> </div>
</div> </div>

View File

@ -1,14 +1,15 @@
import StatusCard from "@components/StatusCards"; import StatusCard from "@components/StatusCards";
import { m } from "@localizations/messages.js";
const PeerConnectionStatusMap = { const PeerConnectionStatusMap = {
connected: "Connected", connected: m.peer_connection_connected(),
connecting: "Connecting", connecting: m.peer_connection_connecting(),
disconnected: "Disconnected", disconnected: m.peer_connection_disconnected(),
error: "Connection error", error: m.peer_connection_error(),
closing: "Closing", closing: m.peer_connection_closing(),
failed: "Connection failed", failed: m.peer_connection_failed(),
closed: "Closed", closed: m.peer_connection_closed(),
new: "Connecting", new: m.peer_connection_new(),
} as Record<RTCPeerConnectionState | "error" | "closing", string>; } as Record<RTCPeerConnectionState | "error" | "closing", string>;
export type PeerConnections = keyof typeof PeerConnectionStatusMap; export type PeerConnections = keyof typeof PeerConnectionStatusMap;

View File

@ -1,12 +1,10 @@
import React, { JSX } from "react"; import React, { JSX } from "react";
import clsx from "clsx"; import clsx from "clsx";
import FieldLabel from "@/components/FieldLabel"; import Card from "@components/Card";
import FieldLabel from "@components/FieldLabel";
import { cva } from "@/cva.config"; import { cva } from "@/cva.config";
import Card from "./Card";
type SelectMenuProps = Pick< type SelectMenuProps = Pick<
JSX.IntrinsicElements["select"], JSX.IntrinsicElements["select"],
"disabled" | "onChange" | "name" | "value" "disabled" | "onChange" | "name" | "value"

View File

@ -1,6 +1,7 @@
import { AvailableSidebarViews } from "@hooks/stores";
import { Button } from "@components/Button"; import { Button } from "@components/Button";
import { cx } from "@/cva.config"; import { cx } from "@/cva.config";
import { AvailableSidebarViews } from "@/hooks/stores"; import { m } from "@localizations/messages.js";
export default function SidebarHeader({ export default function SidebarHeader({
title, title,
@ -17,7 +18,7 @@ export default function SidebarHeader({
<Button <Button
size="XS" size="XS"
theme="blank" theme="blank"
text="Hide" text={m.hide()}
LeadingIcon={({ className }) => ( LeadingIcon={({ className }) => (
<svg <svg
className={cx(className, "rotate-180")} className={cx(className, "rotate-180")}

View File

@ -1,9 +1,9 @@
import { Link } from "react-router";
import React from "react"; import React from "react";
import { Link } from "react-router";
import Container from "@/components/Container"; import LogoBlueIcon from "@assets/logo-blue.png";
import LogoBlueIcon from "@/assets/logo-blue.png"; import LogoWhiteIcon from "@assets/logo-white.svg";
import LogoWhiteIcon from "@/assets/logo-white.svg"; import Container from "@components/Container";
interface Props { logoHref?: string; actionElement?: React.ReactNode } interface Props { logoHref?: string; actionElement?: React.ReactNode }

View File

@ -1,14 +1,15 @@
import { LuPlus, LuX } from "react-icons/lu";
import { useFieldArray, useFormContext } from "react-hook-form";
import { useEffect } from "react"; import { useEffect } from "react";
import validator from "validator"; import validator from "validator";
import { LuPlus, LuX } from "react-icons/lu";
import { useFieldArray, useFormContext } from "react-hook-form";
import { cx } from "cva"; import { cx } from "cva";
import { GridCard } from "@/components/Card"; import { NetworkSettings } from "@hooks/stores";
import { Button } from "@/components/Button"; import { Button } from "@components/Button";
import { InputFieldWithLabel } from "@/components/InputField"; import { GridCard } from "@components/Card";
import { NetworkSettings } from "@/hooks/stores"; import { InputFieldWithLabel } from "@components/InputField";
import { netMaskFromCidr4 } from "@/utils/ip"; import { netMaskFromCidr4 } from "@/utils/ip";
import { m } from "@localizations/messages.js";
export default function StaticIpv4Card() { export default function StaticIpv4Card() {
const formMethods = useFormContext<NetworkSettings>(); const formMethods = useFormContext<NetworkSettings>();
@ -24,6 +25,7 @@ export default function StaticIpv4Card() {
const ipv4StaticAddress = watch("ipv4_static.address"); const ipv4StaticAddress = watch("ipv4_static.address");
const hideSubnetMask = ipv4StaticAddress?.includes("/"); const hideSubnetMask = ipv4StaticAddress?.includes("/");
useEffect(() => { useEffect(() => {
const parts = ipv4StaticAddress?.split("/", 2); const parts = ipv4StaticAddress?.split("/", 2);
if (parts?.length !== 2) return; if (parts?.length !== 2) return;
@ -35,13 +37,13 @@ export default function StaticIpv4Card() {
setValue("ipv4_static.netmask", mask); setValue("ipv4_static.netmask", mask);
}, [ipv4StaticAddress, setValue]); }, [ipv4StaticAddress, setValue]);
const validate = (value: string) => { const ipv4Validation = (value: string) => {
if (!validator.isIP(value)) return "Invalid IP address"; if (!validator.isIP(value, 4)) return m.network_ipv4_invalid()
return true; return true;
}; };
const validateIsIPOrCIDR4 = (value: string) => { const validateIsIPOrCIDR4 = (value: string) => {
if (!validator.isIP(value, 4) && !validator.isIPRange(value, 4)) return "Invalid IP address or CIDR notation"; if (!validator.isIP(value) && !validator.isIPRange(value, 4)) return m.network_ipv4_invalid_cidr();
return true; return true;
}; };
@ -50,12 +52,12 @@ export default function StaticIpv4Card() {
<div className="animate-fadeIn p-4 text-black opacity-0 animation-duration-500 dark:text-white"> <div className="animate-fadeIn p-4 text-black opacity-0 animation-duration-500 dark:text-white">
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-base font-bold text-slate-900 dark:text-white"> <h3 className="text-base font-bold text-slate-900 dark:text-white">
Static IPv4 Configuration {m.network_static_ipv4_header()}
</h3> </h3>
<div className={cx("grid grid-cols-1 gap-4", hideSubnetMask ? "md:grid-cols-1" : "md:grid-cols-2")}> <div className={cx("grid grid-cols-1 gap-4", hideSubnetMask ? "md:grid-cols-1" : "md:grid-cols-2")}>
<InputFieldWithLabel <InputFieldWithLabel
label="IP Address" label={m.network_ipv4_address()}
type="text" type="text"
size="SM" size="SM"
placeholder="192.168.1.100" placeholder="192.168.1.100"
@ -67,21 +69,21 @@ export default function StaticIpv4Card() {
/> />
{!hideSubnetMask && <InputFieldWithLabel {!hideSubnetMask && <InputFieldWithLabel
label="Subnet Mask" label={m.network_ipv4_netmask()}
type="text" type="text"
size="SM" size="SM"
placeholder="255.255.255.0" placeholder="255.255.255.0"
{...register("ipv4_static.netmask", { validate: (value: string | undefined) => validate(value ?? "") })} {...register("ipv4_static.netmask", { validate: (value: string | undefined) => ipv4Validation(value ?? "") })}
error={formState.errors.ipv4_static?.netmask?.message} error={formState.errors.ipv4_static?.netmask?.message}
/>} />}
</div> </div>
<InputFieldWithLabel <InputFieldWithLabel
label="Gateway" label={m.network_ipv4_gateway()}
type="text" type="text"
size="SM" size="SM"
placeholder="192.168.1.1" placeholder="192.168.1.1"
{...register("ipv4_static.gateway", { validate: (value: string | undefined) => validate(value ?? "") })} {...register("ipv4_static.gateway", { validate: (value: string | undefined) => ipv4Validation(value ?? "") })}
error={formState.errors.ipv4_static?.gateway?.message} error={formState.errors.ipv4_static?.gateway?.message}
/> />
@ -93,13 +95,13 @@ export default function StaticIpv4Card() {
<div className="flex items-start gap-x-2"> <div className="flex items-start gap-x-2">
<div className="flex-1"> <div className="flex-1">
<InputFieldWithLabel <InputFieldWithLabel
label={index === 0 ? "DNS Server" : null} label={index === 0 ? m.network_ipv4_dns() : null}
type="text" type="text"
size="SM" size="SM"
placeholder="1.1.1.1" placeholder="1.1.1.1"
{...register( {...register(
`ipv4_static.dns.${index}`, `ipv4_static.dns.${index}`,
{ validate: (value: string | undefined) => validate(value ?? "") } { validate: (value: string | undefined) => ipv4Validation(value ?? "") }
)} )}
error={formState.errors.ipv4_static?.dns?.[index]?.message} error={formState.errors.ipv4_static?.dns?.[index]?.message}
/> />
@ -127,7 +129,7 @@ export default function StaticIpv4Card() {
onClick={() => append("", { shouldFocus: true })} onClick={() => append("", { shouldFocus: true })}
LeadingIcon={LuPlus} LeadingIcon={LuPlus}
type="button" type="button"
text="Add DNS Server" text={m.network_settings_add_dns()}
disabled={dns?.[0] === ""} disabled={dns?.[0] === ""}
/> />
</div> </div>

View File

@ -1,12 +1,13 @@
import { useEffect } from "react";
import validator from "validator";
import { LuPlus, LuX } from "react-icons/lu"; import { LuPlus, LuX } from "react-icons/lu";
import { useFieldArray, useFormContext } from "react-hook-form"; import { useFieldArray, useFormContext } from "react-hook-form";
import validator from "validator";
import { useEffect } from "react";
import { GridCard } from "@/components/Card"; import { NetworkSettings } from "@hooks/stores";
import { Button } from "@/components/Button"; import { Button } from "@components/Button";
import { InputFieldWithLabel } from "@/components/InputField"; import { GridCard } from "@components/Card";
import { NetworkSettings } from "@/hooks/stores"; import { InputFieldWithLabel } from "@components/InputField";
import { m } from "@localizations/messages.js";
export default function StaticIpv6Card() { export default function StaticIpv6Card() {
const formMethods = useFormContext<NetworkSettings>(); const formMethods = useFormContext<NetworkSettings>();
@ -25,20 +26,20 @@ export default function StaticIpv6Card() {
// Check if it's a valid IPv6 address with CIDR notation // Check if it's a valid IPv6 address with CIDR notation
const parts = value.split("/"); const parts = value.split("/");
if (parts.length !== 2) return "Please use CIDR notation (e.g., 2001:db8::1/64)"; if (parts.length !== 2) return m.network_ipv6_cidr_suggestion();
const [address, prefix] = parts; const [address, prefix] = parts;
if (!validator.isIP(address, 6)) return "Invalid IPv6 address"; if (!validator.isIP(address, 6)) return m.network_ipv6_invalid();
const prefixNum = parseInt(prefix); const prefixNum = parseInt(prefix);
if (isNaN(prefixNum) || prefixNum < 0 || prefixNum > 128) { if (isNaN(prefixNum) || prefixNum < 0 || prefixNum > 128) {
return "Prefix must be between 0 and 128"; return m.network_ipv6_prefix_invalid();
} }
return true; return true;
}; };
const ipv6Validation = (value: string) => { const ipv6Validation = (value: string) => {
if (!validator.isIP(value, 6)) return "Invalid IPv6 address"; if (!validator.isIP(value, 6)) return m.network_ipv6_invalid()
return true; return true;
}; };
@ -47,11 +48,11 @@ export default function StaticIpv6Card() {
<div className="animate-fadeIn p-4 text-black opacity-0 animation-duration-500 dark:text-white"> <div className="animate-fadeIn p-4 text-black opacity-0 animation-duration-500 dark:text-white">
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-base font-bold text-slate-900 dark:text-white"> <h3 className="text-base font-bold text-slate-900 dark:text-white">
Static IPv6 Configuration {m.network_static_ipv6_header()}
</h3> </h3>
<InputFieldWithLabel <InputFieldWithLabel
label="IP Prefix" label={m.network_ipv6_prefix()}
type="text" type="text"
size="SM" size="SM"
placeholder="2001:db8::1/64" placeholder="2001:db8::1/64"
@ -60,7 +61,7 @@ export default function StaticIpv6Card() {
/> />
<InputFieldWithLabel <InputFieldWithLabel
label="Gateway" label={m.network_ipv6_gateway()}
type="text" type="text"
size="SM" size="SM"
placeholder="2001:db8::1" placeholder="2001:db8::1"
@ -76,7 +77,7 @@ export default function StaticIpv6Card() {
<div className="flex items-start gap-x-2"> <div className="flex items-start gap-x-2">
<div className="flex-1"> <div className="flex-1">
<InputFieldWithLabel <InputFieldWithLabel
label={index === 0 ? "DNS Server" : null} label={index === 0 ? m.network_ipv6_dns() : null}
type="text" type="text"
size="SM" size="SM"
placeholder="2001:4860:4860::8888" placeholder="2001:4860:4860::8888"
@ -107,7 +108,7 @@ export default function StaticIpv6Card() {
onClick={() => append("", { shouldFocus: true })} onClick={() => append("", { shouldFocus: true })}
LeadingIcon={LuPlus} LeadingIcon={LuPlus}
type="button" type="button"
text="Add DNS Server" text={m.network_settings_add_dns()}
disabled={dns?.[0] === ""} disabled={dns?.[0] === ""}
/> />
</div> </div>

View File

@ -1,7 +1,8 @@
import { CheckIcon } from "@heroicons/react/16/solid"; import { CheckIcon } from "@heroicons/react/16/solid";
import Card from "@components/Card";
import { m } from "@localizations/messages.js";
import { cva, cx } from "@/cva.config"; import { cva, cx } from "@/cva.config";
import Card from "@/components/Card";
interface Props { interface Props {
nSteps: number; nSteps: number;
@ -49,7 +50,7 @@ export default function StepCounter({ nSteps, currStepIdx, size = "MD" }: Props)
)} )}
key={`${i}-${currStepIdx}`} key={`${i}-${currStepIdx}`}
> >
Step {i + 1} {m.step_counter_step({ step: i + 1 })}
</div> </div>
); );
} }

View File

@ -1,6 +1,6 @@
import { useEffect, useMemo } from "react";
import "react-simple-keyboard/build/css/index.css"; import "react-simple-keyboard/build/css/index.css";
import { ChevronDownIcon } from "@heroicons/react/16/solid"; import { ChevronDownIcon } from "@heroicons/react/16/solid";
import { useEffect, useMemo } from "react";
import { useXTerm } from "react-xtermjs"; import { useXTerm } from "react-xtermjs";
import { FitAddon } from "@xterm/addon-fit"; import { FitAddon } from "@xterm/addon-fit";
import { WebLinksAddon } from "@xterm/addon-web-links"; import { WebLinksAddon } from "@xterm/addon-web-links";
@ -9,9 +9,9 @@ import { Unicode11Addon } from "@xterm/addon-unicode11";
import { ClipboardAddon } from "@xterm/addon-clipboard"; import { ClipboardAddon } from "@xterm/addon-clipboard";
import { cx } from "@/cva.config"; import { cx } from "@/cva.config";
import { AvailableTerminalTypes, useUiStore } from "@/hooks/stores"; import { AvailableTerminalTypes, useUiStore } from "@hooks/stores";
import { Button } from "@components/Button";
import { Button } from "./Button"; import { m } from "@localizations/messages.js";
const isWebGl2Supported = !!document.createElement("canvas").getContext("webgl2"); const isWebGl2Supported = !!document.createElement("canvas").getContext("webgl2");
@ -54,6 +54,7 @@ const TERMINAL_CONFIG = {
// Add these configurations: // Add these configurations:
cursorStyle: "block", cursorStyle: "block",
rendererType: "canvas", // Ensure we're using the canvas renderer rendererType: "canvas", // Ensure we're using the canvas renderer
unicode: { activeVersion: "11" }
} as const; } as const;
function Terminal({ function Terminal({
@ -144,7 +145,6 @@ function Terminal({
instance.loadAddon(new ClipboardAddon()); instance.loadAddon(new ClipboardAddon());
instance.loadAddon(new Unicode11Addon()); instance.loadAddon(new Unicode11Addon());
instance.loadAddon(new WebLinksAddon()); instance.loadAddon(new WebLinksAddon());
instance.unicode.activeVersion = "11";
if (isWebGl2Supported) { if (isWebGl2Supported) {
const webGl2Addon = new WebglAddon(); const webGl2Addon = new WebglAddon();
@ -191,7 +191,7 @@ function Terminal({
<Button <Button
size="XS" size="XS"
theme="light" theme="light"
text="Hide" text={m.hide()}
LeadingIcon={ChevronDownIcon} LeadingIcon={ChevronDownIcon}
onClick={() => setTerminalType("none")} onClick={() => setTerminalType("none")}
/> />

View File

@ -1,9 +1,9 @@
import React, { JSX } from "react"; import React, { JSX } from "react";
import clsx from "clsx"; import clsx from "clsx";
import FieldLabel from "@/components/FieldLabel"; import FieldLabel from "@components/FieldLabel";
import { FieldError } from "@/components/InputField"; import { FieldError } from "@components/InputField";
import Card from "@/components/Card"; import Card from "@components/Card";
import { cx } from "@/cva.config"; import { cx } from "@/cva.config";
type TextAreaProps = JSX.IntrinsicElements["textarea"] & { type TextAreaProps = JSX.IntrinsicElements["textarea"] & {

View File

@ -1,10 +1,11 @@
import React from "react"; import React from "react";
import { cx } from "@/cva.config"; import { cx } from "@/cva.config";
import KeyboardAndMouseConnectedIcon from "@/assets/keyboard-and-mouse-connected.png"; 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"; import LoadingSpinner from "@components/LoadingSpinner";
import StatusCard from "@components/StatusCards"; import StatusCard from "@components/StatusCards";
import { USBStates } from "@/hooks/stores";
type StatusProps = Record< type StatusProps = Record<
USBStates, USBStates,
@ -16,11 +17,11 @@ type StatusProps = Record<
>; >;
const USBStateMap: Record<USBStates, string> = { const USBStateMap: Record<USBStates, string> = {
configured: "Connected", configured: m.usb_state_connected(),
attached: "Connecting", attached: m.usb_state_connecting(),
addressed: "Connecting", addressed: m.usb_state_connecting(),
"not attached": "Disconnected", "not attached": m.usb_state_disconnected(),
suspended: "Low power mode", suspended: m.usb_state_low_power_mode(),
}; };
const StatusCardProps: StatusProps = { const StatusCardProps: StatusProps = {
configured: { configured: {
@ -80,8 +81,8 @@ export default function USBStateStatus({
return ( return (
<StatusCard <StatusCard
title="USB" title={m.usb()}
status="Disconnected" status={m.usb_state_disconnected()}
icon={Icon} icon={Icon}
iconClassName={iconClassName} iconClassName={iconClassName}
statusIndicatorClassName={statusIndicatorClassName} statusIndicatorClassName={statusIndicatorClassName}
@ -90,6 +91,6 @@ export default function USBStateStatus({
} }
return ( return (
<StatusCard title="USB" status={USBStateMap[state]} {...StatusCardProps[state]} /> <StatusCard title={m.usb()} status={USBStateMap[state]} {...StatusCardProps[state]} />
); );
} }

View File

@ -1,10 +1,9 @@
import { cx } from "@/cva.config"; import { cx } from "@/cva.config";
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
import { useDeviceUiNavigation } from "../hooks/useAppNavigation"; import { Button } from "@components/Button";
import { GridCard } from "@components/Card";
import { Button } from "./Button"; import LoadingSpinner from "@components/LoadingSpinner";
import { GridCard } from "./Card"; import { m } from "@localizations/messages.js";
import LoadingSpinner from "./LoadingSpinner";
export default function UpdateInProgressStatusCard() { export default function UpdateInProgressStatusCard() {
const { navigateTo } = useDeviceUiNavigation(); const { navigateTo } = useDeviceUiNavigation();
@ -17,12 +16,12 @@ export default function UpdateInProgressStatusCard() {
<LoadingSpinner className={cx("h-5 w-5", "shrink-0 text-blue-700")} /> <LoadingSpinner className={cx("h-5 w-5", "shrink-0 text-blue-700")} />
<div className="space-y-1"> <div className="space-y-1">
<div className="text-ellipsis text-sm font-semibold leading-none transition"> <div className="text-ellipsis text-sm font-semibold leading-none transition">
Update in Progress {m.update_in_progress()}
</div> </div>
<div className="text-sm leading-none"> <div className="text-sm leading-none">
<div className="flex items-center gap-x-1"> <div className="flex items-center gap-x-1">
<span className={cx("transition")}> <span className={cx("transition")}>
Please don{"'"}t turn off your device... {m.updating_leave_device_on()}
</span> </span>
</div> </div>
</div> </div>
@ -32,7 +31,7 @@ export default function UpdateInProgressStatusCard() {
size="SM" size="SM"
className="pointer-events-auto" className="pointer-events-auto"
theme="light" theme="light"
text="View Details" text={m.view_details()}
onClick={() => navigateTo("/settings/general/update")} onClick={() => navigateTo("/settings/general/update")}
/> />
</div> </div>

View File

@ -0,0 +1,50 @@
import { CheckCircleIcon } from "@heroicons/react/24/solid"; // adjust import if you use a different icon set
import LoadingSpinner from "@components/LoadingSpinner"; // adjust import path if needed
import { m } from "@localizations/messages.js";
export interface UpdatePart {
pending: boolean;
status: string;
progress: number;
complete: boolean;
}
export default function UpdatingStatusCard({
label,
part,
}: {
label: string;
part: UpdatePart;
}) {
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<p className="text-sm font-semibold text-black dark:text-white">{label}</p>
{part.progress < 100 ? (
<LoadingSpinner className="h-4 w-4 text-blue-700 dark:text-blue-500" />
) : (
<CheckCircleIcon className="h-4 w-4 text-blue-700 dark:text-blue-500" />
)}
</div>
<div
className="h-2.5 w-full overflow-hidden rounded-full bg-slate-300 dark:bg-slate-600"
role="progressbar"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={Math.round(part.progress)}
aria-label={m.general_update_status_progress({part: label})}
>
<div
className="h-2.5 rounded-full bg-blue-700 transition-all duration-500 ease-linear dark:bg-blue-500"
style={{ width: `${part.progress}%` }}
/>
</div>
<div className="flex justify-between text-sm text-slate-600 dark:text-slate-300">
<span>{part.status}</span>
{part.progress < 100 ? <span>{`${Math.round(part.progress)}%`}</span> : null}
</div>
</div>
);
}

View File

@ -1,15 +1,15 @@
import { useCallback , useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import { m } from "@localizations/messages.js";
import { SettingsItem } from "@components/SettingsItem"; import { SettingsItem } from "@components/SettingsItem";
import Checkbox from "@components/Checkbox";
import { Button } from "@components/Button";
import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { SettingsSectionHeader } from "@components/SettingsSectionHeader";
import Fieldset from "@components/Fieldset";
import notifications from "@/notifications";
import { JsonRpcResponse, useJsonRpc } from "../hooks/useJsonRpc";
import notifications from "../notifications";
import Checkbox from "./Checkbox";
import { Button } from "./Button";
import { SelectMenuBasic } from "./SelectMenuBasic";
import { SettingsSectionHeader } from "./SettingsSectionHeader";
import Fieldset from "./Fieldset";
export interface USBConfig { export interface USBConfig {
vendor_id: string; vendor_id: string;
product_id: string; product_id: string;
@ -34,7 +34,7 @@ const defaultUsbDeviceConfig: UsbDeviceConfig = {
const usbPresets = [ const usbPresets = [
{ {
label: "Keyboard, Mouse and Mass Storage", label: m.usb_device_keyboard_mouse_and_mass_storage(),
value: "default", value: "default",
config: { config: {
keyboard: true, keyboard: true,
@ -44,7 +44,7 @@ const usbPresets = [
}, },
}, },
{ {
label: "Keyboard Only", label: m.usb_device_keyboard_only(),
value: "keyboard_only", value: "keyboard_only",
config: { config: {
keyboard: true, keyboard: true,
@ -54,7 +54,7 @@ const usbPresets = [
}, },
}, },
{ {
label: "Custom", label: m.usb_device_custom(),
value: "custom", value: "custom",
}, },
]; ];
@ -72,7 +72,7 @@ export function UsbDeviceSetting() {
if ("error" in resp) { if ("error" in resp) {
console.error("Failed to load USB devices:", resp.error); console.error("Failed to load USB devices:", resp.error);
notifications.error( notifications.error(
`Failed to load USB devices: ${resp.error.data || "Unknown error"}`, m.usb_device_failed_load({ error: String(resp.error.data || m.unknown_error()) }),
); );
} else { } else {
const usbConfigState = resp.result as UsbDeviceConfig; const usbConfigState = resp.result as UsbDeviceConfig;
@ -101,7 +101,7 @@ export function UsbDeviceSetting() {
send("setUsbDevices", { devices }, async (resp: JsonRpcResponse) => { send("setUsbDevices", { devices }, async (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(
`Failed to set usb devices: ${resp.error.data || "Unknown error"}`, m.usb_device_failed_set({ error: String(resp.error.data || m.unknown_error()) }),
); );
setLoading(false); setLoading(false);
return; return;
@ -111,7 +111,7 @@ export function UsbDeviceSetting() {
await new Promise(resolve => setTimeout(resolve, 2000)); await new Promise(resolve => setTimeout(resolve, 2000));
setLoading(false); setLoading(false);
syncUsbDeviceConfig(); syncUsbDeviceConfig();
notifications.success(`USB Devices updated`); notifications.success(m.usb_device_updated());
}); });
}, },
[send, syncUsbDeviceConfig], [send, syncUsbDeviceConfig],
@ -154,14 +154,14 @@ export function UsbDeviceSetting() {
<div className="h-px w-full bg-slate-800/10 dark:bg-slate-300/20" /> <div className="h-px w-full bg-slate-800/10 dark:bg-slate-300/20" />
<SettingsSectionHeader <SettingsSectionHeader
title="USB Device" title={m.usb_device_title()}
description="USB devices to emulate on the target computer" description={m.usb_device_description()}
/> />
<SettingsItem <SettingsItem
loading={loading} loading={loading}
title="Classes" title={m.usb_device_classes_title()}
description="USB device classes in the composite device" description={m.usb_device_classes_description()}
> >
<SelectMenuBasic <SelectMenuBasic
size="SM" size="SM"
@ -178,7 +178,7 @@ export function UsbDeviceSetting() {
<div className="ml-2 border-l border-slate-800/10 pl-4 dark:border-slate-300/20 "> <div className="ml-2 border-l border-slate-800/10 pl-4 dark:border-slate-300/20 ">
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-4"> <div className="space-y-4">
<SettingsItem title="Enable Keyboard" description="Enable Keyboard"> <SettingsItem title={m.usb_device_enable_keyboard_title()} description={m.usb_device_enable_keyboard_description()}>
<Checkbox <Checkbox
checked={usbDeviceConfig.keyboard} checked={usbDeviceConfig.keyboard}
onChange={onUsbConfigItemChange("keyboard")} onChange={onUsbConfigItemChange("keyboard")}
@ -187,8 +187,8 @@ export function UsbDeviceSetting() {
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<SettingsItem <SettingsItem
title="Enable Absolute Mouse (Pointer)" title={m.usb_device_enable_absolute_mouse_title()}
description="Enable Absolute Mouse (Pointer)" description={m.usb_device_enable_absolute_mouse_description()}
> >
<Checkbox <Checkbox
checked={usbDeviceConfig.absolute_mouse} checked={usbDeviceConfig.absolute_mouse}
@ -198,8 +198,8 @@ export function UsbDeviceSetting() {
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<SettingsItem <SettingsItem
title="Enable Relative Mouse" title={m.usb_device_enable_relative_mouse_title()}
description="Enable Relative Mouse" description={m.usb_device_enable_relative_mouse_description()}
> >
<Checkbox <Checkbox
checked={usbDeviceConfig.relative_mouse} checked={usbDeviceConfig.relative_mouse}
@ -209,8 +209,8 @@ export function UsbDeviceSetting() {
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<SettingsItem <SettingsItem
title="Enable USB Mass Storage" title={m.usb_device_enable_mass_storage_title()}
description="Sometimes it might need to be disabled to prevent issues with certain devices" description={m.usb_device_enable_mass_storage_description()}
> >
<Checkbox <Checkbox
checked={usbDeviceConfig.mass_storage} checked={usbDeviceConfig.mass_storage}
@ -224,13 +224,13 @@ export function UsbDeviceSetting() {
size="SM" size="SM"
loading={loading} loading={loading}
theme="primary" theme="primary"
text="Update USB Classes" text={m.usb_device_update_classes()}
onClick={() => handleUsbConfigChange(usbDeviceConfig)} onClick={() => handleUsbConfigChange(usbDeviceConfig)}
/> />
<Button <Button
size="SM" size="SM"
theme="light" theme="light"
text="Restore to Default" text={m.usb_device_restore_default()}
onClick={() => handleUsbConfigChange(defaultUsbDeviceConfig)} onClick={() => handleUsbConfigChange(defaultUsbDeviceConfig)}
/> />
</div> </div>

View File

@ -1,15 +1,14 @@
import { useMemo , useCallback , useEffect, useState } from "react"; import { useMemo, useCallback, useEffect, useState } from "react";
import { UsbConfigState } from "@hooks/stores";
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import { Button } from "@components/Button"; import { Button } from "@components/Button";
import Fieldset from "@components/Fieldset";
import { InputFieldWithLabel } from "@components/InputField";
import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { SettingsItem } from "@components/SettingsItem"; import { SettingsItem } from "@components/SettingsItem";
import notifications from "@/notifications";
import { UsbConfigState } from "../hooks/stores"; import { m } from "@localizations/messages.js";
import { JsonRpcResponse, useJsonRpc } from "../hooks/useJsonRpc";
import notifications from "../notifications";
import { InputFieldWithLabel } from "./InputField";
import { SelectMenuBasic } from "./SelectMenuBasic";
import Fieldset from "./Fieldset";
const generatedSerialNumber = [generateNumber(1, 9), generateHex(7, 7), 0, 1].join("&"); const generatedSerialNumber = [generateNumber(1, 9), generateHex(7, 7), 0, 1].join("&");
@ -31,21 +30,22 @@ export interface USBConfig {
product: string; product: string;
} }
const usbConfigs = [ const usbConfigs = [
{ {
label: "JetKVM Default", label: m.usb_config_default(),
value: "USB Emulation Device", value: "USB Emulation Device",
}, },
{ {
label: "Logitech Universal Adapter", label: m.usb_config_logitech(),
value: "Logitech USB Input Device", value: "Logitech USB Input Device",
}, },
{ {
label: "Microsoft Wireless MultiMedia Keyboard", label: m.usb_config_microsoft(),
value: "Wireless MultiMedia Keyboard", value: "Wireless MultiMedia Keyboard",
}, },
{ {
label: "Dell Multimedia Pro Keyboard", label: m.usb_config_dell(),
value: "Multimedia Pro Keyboard", value: "Multimedia Pro Keyboard",
}, },
]; ];
@ -97,7 +97,7 @@ export function UsbInfoSetting() {
if ("error" in resp) { if ("error" in resp) {
console.error("Failed to load USB Config:", resp.error); console.error("Failed to load USB Config:", resp.error);
notifications.error( notifications.error(
`Failed to load USB Config: ${resp.error.data || "Unknown error"}`, m.usb_config_failed_load({ error: String(resp.error.data || m.unknown_error()) }),
); );
} else { } else {
const usbConfigState = resp.result as UsbConfigState; const usbConfigState = resp.result as UsbConfigState;
@ -116,7 +116,7 @@ export function UsbInfoSetting() {
send("setUsbConfig", { usbConfig }, async (resp: JsonRpcResponse) => { send("setUsbConfig", { usbConfig }, async (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(
`Failed to set usb config: ${resp.error.data || "Unknown error"}`, m.usb_config_failed_set({ error: String(resp.error.data || m.unknown_error()) }),
); );
setLoading(false); setLoading(false);
return; return;
@ -126,7 +126,7 @@ export function UsbInfoSetting() {
await new Promise(resolve => setTimeout(resolve, 2000)); await new Promise(resolve => setTimeout(resolve, 2000));
setLoading(false); setLoading(false);
notifications.success( notifications.success(
`USB Config set to ${usbConfig.manufacturer} ${usbConfig.product}`, m.usb_config_set_success({ manufacturer: usbConfig.manufacturer, product: usbConfig.product }),
); );
syncUsbConfigProduct(); syncUsbConfigProduct();
@ -139,7 +139,7 @@ export function UsbInfoSetting() {
send("getDeviceID", {}, (resp: JsonRpcResponse) => { send("getDeviceID", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
return notifications.error( 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); setDeviceId(resp.result as string);
@ -152,8 +152,8 @@ export function UsbInfoSetting() {
<Fieldset disabled={loading} className="space-y-4"> <Fieldset disabled={loading} className="space-y-4">
<SettingsItem <SettingsItem
loading={loading} loading={loading}
title="Identifiers" title={m.usb_config_identifiers_title()}
description="USB device identifiers exposed to the target computer" description={m.usb_config_identifiers_description()}
> >
<SelectMenuBasic <SelectMenuBasic
size="SM" size="SM"
@ -169,7 +169,7 @@ export function UsbInfoSetting() {
handleUsbConfigChange(usbConfig); handleUsbConfigChange(usbConfig);
} }
}} }}
options={[...usbConfigs, { value: "custom", label: "Custom" }]} options={[...usbConfigs, { value: "custom", label: m.usb_config_custom() }]}
/> />
</SettingsItem> </SettingsItem>
{usbConfigProduct === "custom" && ( {usbConfigProduct === "custom" && (
@ -246,38 +246,38 @@ function USBConfigDialog({
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<InputFieldWithLabel <InputFieldWithLabel
required required
label="Vendor ID" label={m.usb_config_vendor_id_label()}
placeholder="Enter Vendor ID" placeholder={m.usb_config_vendor_id_placeholder()}
pattern="^0[xX][\da-fA-F]{4}$" pattern="^0[xX][\da-fA-F]{4}$"
defaultValue={usbConfigState?.vendor_id} defaultValue={usbConfigState?.vendor_id}
onChange={e => handleUsbVendorIdChange(e.target.value)} onChange={e => handleUsbVendorIdChange(e.target.value)}
/> />
<InputFieldWithLabel <InputFieldWithLabel
required required
label="Product ID" label={m.usb_config_product_id_label()}
placeholder="Enter Product ID" placeholder={m.usb_config_product_id_placeholder()}
pattern="^0[xX][\da-fA-F]{4}$" pattern="^0[xX][\da-fA-F]{4}$"
defaultValue={usbConfigState?.product_id} defaultValue={usbConfigState?.product_id}
onChange={e => handleUsbProductIdChange(e.target.value)} onChange={e => handleUsbProductIdChange(e.target.value)}
/> />
<InputFieldWithLabel <InputFieldWithLabel
required required
label="Serial Number" label={m.usb_config_serial_number_label()}
placeholder="Enter Serial Number" placeholder={m.usb_config_serial_number_placeholder()}
defaultValue={usbConfigState?.serial_number} defaultValue={usbConfigState?.serial_number}
onChange={e => handleUsbSerialChange(e.target.value)} onChange={e => handleUsbSerialChange(e.target.value)}
/> />
<InputFieldWithLabel <InputFieldWithLabel
required required
label="Manufacturer" label={m.usb_config_manufacturer_label()}
placeholder="Enter Manufacturer" placeholder={m.usb_config_manufacturer_placeholder()}
defaultValue={usbConfigState?.manufacturer} defaultValue={usbConfigState?.manufacturer}
onChange={e => handleUsbManufacturer(e.target.value)} onChange={e => handleUsbManufacturer(e.target.value)}
/> />
<InputFieldWithLabel <InputFieldWithLabel
required required
label="Product Name" label={m.usb_config_product_name_label()}
placeholder="Enter Product Name" placeholder={m.usb_config_product_name_placeholder()}
defaultValue={usbConfigState?.product} defaultValue={usbConfigState?.product}
onChange={e => handleUsbProduct(e.target.value)} onChange={e => handleUsbProduct(e.target.value)}
/> />
@ -287,13 +287,13 @@ function USBConfigDialog({
loading={loading} loading={loading}
size="SM" size="SM"
theme="primary" theme="primary"
text="Update USB Identifiers" text={m.usb_config_update_identifiers()}
onClick={() => onSetUsbConfig(usbConfigState)} onClick={() => onSetUsbConfig(usbConfigState)}
/> />
<Button <Button
size="SM" size="SM"
theme="light" theme="light"
text="Restore to Default" text={m.usb_config_restore_default()}
onClick={onRestoreToDefault} onClick={onRestoreToDefault}
/> />
</div> </div>

View File

@ -5,6 +5,7 @@ import { motion, AnimatePresence } from "framer-motion";
import { LuPlay } from "react-icons/lu"; import { LuPlay } from "react-icons/lu";
import { BsMouseFill } from "react-icons/bs"; import { BsMouseFill } from "react-icons/bs";
import { m } from "@localizations/messages.js";
import { Button, LinkButton } from "@components/Button"; import { Button, LinkButton } from "@components/Button";
import LoadingSpinner from "@components/LoadingSpinner"; import LoadingSpinner from "@components/LoadingSpinner";
import Card, { GridCard } from "@components/Card"; import Card, { GridCard } from "@components/Card";
@ -51,7 +52,7 @@ export function LoadingVideoOverlay({ show }: LoadingOverlayProps) {
<LoadingSpinner className="h-8 w-8 text-blue-800 dark:text-blue-200" /> <LoadingSpinner className="h-8 w-8 text-blue-800 dark:text-blue-200" />
</div> </div>
<p className="text-center text-sm text-slate-700 dark:text-slate-300"> <p className="text-center text-sm text-slate-700 dark:text-slate-300">
Loading video stream... {m.video_overlay_loading_stream()}
</p> </p>
</div> </div>
</OverlayContent> </OverlayContent>
@ -123,26 +124,26 @@ export function ConnectionFailedOverlay({
<div className="text-left text-sm text-slate-700 dark:text-slate-300"> <div className="text-left text-sm text-slate-700 dark:text-slate-300">
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2 text-black dark:text-white"> <div className="space-y-2 text-black dark:text-white">
<h2 className="text-xl font-bold">Connection Issue Detected</h2> <h2 className="text-xl font-bold">{m.video_overlay_connection_issue_title()}</h2>
<ul className="list-disc space-y-2 pl-4 text-left"> <ul className="list-disc space-y-2 pl-4 text-left">
<li>Verify that the device is powered on and properly connected</li> <li>{m.video_overlay_conn_verify_power()}</li>
<li>Check all cable connections for any loose or damaged wires</li> <li>{m.video_overlay_conn_check_cables()}</li>
<li>Ensure your network connection is stable and active</li> <li>{m.video_overlay_conn_ensure_network()}</li>
<li>Try restarting both the device and your computer</li> <li>{m.video_overlay_conn_restart()}</li>
</ul> </ul>
</div> </div>
<div className="flex items-center gap-x-2"> <div className="flex items-center gap-x-2">
<LinkButton <LinkButton
to={"https://jetkvm.com/docs/getting-started/troubleshooting"} to={"https://jetkvm.com/docs/getting-started/troubleshooting"}
theme="primary" theme="primary"
text="Troubleshooting Guide" text={m.video_overlay_troubleshooting_guide()}
TrailingIcon={ArrowRightIcon} TrailingIcon={ArrowRightIcon}
size="SM" size="SM"
/> />
<Button <Button
onClick={() => setupPeerConnection()} onClick={() => setupPeerConnection()}
LeadingIcon={ArrowPathIcon} LeadingIcon={ArrowPathIcon}
text="Try again" text={m.video_overlay_try_again()}
size="SM" size="SM"
theme="light" theme="light"
/> />
@ -183,12 +184,12 @@ export function PeerConnectionDisconnectedOverlay({
<div className="text-left text-sm text-slate-700 dark:text-slate-300"> <div className="text-left text-sm text-slate-700 dark:text-slate-300">
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2 text-black dark:text-white"> <div className="space-y-2 text-black dark:text-white">
<h2 className="text-xl font-bold">Connection Issue Detected</h2> <h2 className="text-xl font-bold">{m.video_overlay_connection_issue_title()}</h2>
<ul className="list-disc space-y-2 pl-4 text-left"> <ul className="list-disc space-y-2 pl-4 text-left">
<li>Verify that the device is powered on and properly connected</li> <li>{m.video_overlay_conn_verify_power()}</li>
<li>Check all cable connections for any loose or damaged wires</li> <li>{m.video_overlay_conn_check_cables()}</li>
<li>Ensure your network connection is stable and active</li> <li>{m.video_overlay_conn_ensure_network()}</li>
<li>Try restarting both the device and your computer</li> <li>{m.video_overlay_conn_restart()}</li>
</ul> </ul>
</div> </div>
<div className="flex items-center gap-x-2"> <div className="flex items-center gap-x-2">
@ -196,7 +197,7 @@ export function PeerConnectionDisconnectedOverlay({
<div className="flex items-center gap-x-2 p-4"> <div className="flex items-center gap-x-2 p-4">
<LoadingSpinner className="h-4 w-4 text-blue-800 dark:text-blue-200" /> <LoadingSpinner className="h-4 w-4 text-blue-800 dark:text-blue-200" />
<p className="text-sm text-slate-700 dark:text-slate-300"> <p className="text-sm text-slate-700 dark:text-slate-300">
Retrying connection... {m.video_overlay_retrying_connection()}
</p> </p>
</div> </div>
</Card> </Card>
@ -240,23 +241,18 @@ export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) {
<div className="text-left text-sm text-slate-700 dark:text-slate-300"> <div className="text-left text-sm text-slate-700 dark:text-slate-300">
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2 text-black dark:text-white"> <div className="space-y-2 text-black dark:text-white">
<h2 className="text-xl font-bold">No HDMI signal detected.</h2> <h2 className="text-xl font-bold">{m.video_overlay_no_hdmi_signal()}</h2>
<ul className="list-disc space-y-2 pl-4 text-left"> <ul className="list-disc space-y-2 pl-4 text-left">
<li>Ensure the HDMI cable securely connected at both ends</li> <li>{m.video_overlay_no_hdmi_ensure_cable()}</li>
<li> <li>{m.video_overlay_no_hdmi_ensure_power()}</li>
Ensure source device is powered on and outputting a signal <li>{m.video_overlay_no_hdmi_adapter_compat()}</li>
</li>
<li>
If using an adapter, ensure it&apos;s compatible and functioning
correctly
</li>
</ul> </ul>
</div> </div>
<div> <div>
<LinkButton <LinkButton
to={"https://jetkvm.com/docs/getting-started/troubleshooting"} to={"https://jetkvm.com/docs/getting-started/troubleshooting"}
theme="light" theme="light"
text="Learn more" text={m.video_overlay_learn_more()}
TrailingIcon={ArrowRightIcon} TrailingIcon={ArrowRightIcon}
size="SM" size="SM"
/> />
@ -287,18 +283,18 @@ export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) {
<div className="text-left text-sm text-slate-700 dark:text-slate-300"> <div className="text-left text-sm text-slate-700 dark:text-slate-300">
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2 text-black dark:text-white"> <div className="space-y-2 text-black dark:text-white">
<h2 className="text-xl font-bold">HDMI signal error detected.</h2> <h2 className="text-xl font-bold">{m.video_overlay_hdmi_error_title()}</h2>
<ul className="list-disc space-y-2 pl-4 text-left"> <ul className="list-disc space-y-2 pl-4 text-left">
<li>A loose or faulty HDMI connection</li> <li>{m.video_overlay_hdmi_loose_faulty()}</li>
<li>Incompatible resolution or refresh rate settings</li> <li>{m.video_overlay_hdmi_incompatible_resolution()}</li>
<li>Issues with the source device&apos;s HDMI output</li> <li>{m.video_overlay_hdmi_source_issue()}</li>
</ul> </ul>
</div> </div>
<div> <div>
<LinkButton <LinkButton
to={"https://jetkvm.com/docs/getting-started/troubleshooting"} to={"https://jetkvm.com/docs/getting-started/troubleshooting"}
theme="light" theme="light"
text="Learn more" text={m.video_overlay_learn_more()}
TrailingIcon={ArrowRightIcon} TrailingIcon={ArrowRightIcon}
size="SM" size="SM"
/> />
@ -339,7 +335,7 @@ export function NoAutoplayPermissionsOverlay({
<OverlayContent> <OverlayContent>
<div className="space-y-4"> <div className="space-y-4">
<h2 className="text-2xl font-extrabold text-black dark:text-white"> <h2 className="text-2xl font-extrabold text-black dark:text-white">
Autoplay permissions required {m.video_overlay_autoplay_permissions_required()}
</h2> </h2>
<div className="space-y-2 text-center"> <div className="space-y-2 text-center">
@ -348,13 +344,13 @@ export function NoAutoplayPermissionsOverlay({
size="MD" size="MD"
theme="primary" theme="primary"
LeadingIcon={LuPlay} LeadingIcon={LuPlay}
text="Manually start stream" text={m.video_overlay_manually_start_stream()}
onClick={onPlayClick} onClick={onPlayClick}
/> />
</div> </div>
<div className="text-xs text-slate-600 dark:text-slate-400"> <div className="text-xs text-slate-600 dark:text-slate-400">
Please adjust browser settings to enable autoplay {m.video_overlay_enable_autoplay_settings()}
</div> </div>
</div> </div>
</div> </div>
@ -386,7 +382,7 @@ export function PointerLockBar({ show }: PointerLockBarProps) {
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<BsMouseFill className="h-4 w-4 text-blue-700 dark:text-blue-500" /> <BsMouseFill className="h-4 w-4 text-blue-700 dark:text-blue-500" />
<span className="text-sm text-black dark:text-white"> <span className="text-sm text-black dark:text-white">
Click on the video to enable mouse control {m.video_overlay_pointerlock_click_to_enable()}
</span> </span>
</div> </div>
</div> </div>
@ -480,6 +476,7 @@ export function RebootingOverlay({ show, postRebootAction }: RebootingOverlayPro
// Device is available, redirect to the specified URL // Device is available, redirect to the specified URL
console.log('Device is available, redirecting to:', postRebootAction.redirectUrl); console.log('Device is available, redirecting to:', postRebootAction.redirectUrl);
window.location.href = postRebootAction.redirectUrl; window.location.href = postRebootAction.redirectUrl;
window.location.reload();
} }
} catch (err) { } catch (err) {
// Ignore errors - they're expected while device is rebooting // Ignore errors - they're expected while device is rebooting

View File

@ -1,21 +1,19 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { ChevronDownIcon } from "@heroicons/react/16/solid"; import { ChevronDownIcon } from "@heroicons/react/16/solid";
import { AnimatePresence, motion } from "framer-motion"; import { AnimatePresence, motion } from "framer-motion";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import Keyboard from "react-simple-keyboard"; import Keyboard from "react-simple-keyboard";
import { LuKeyboard } from "react-icons/lu"; import { LuKeyboard } from "react-icons/lu";
import Card from "@components/Card";
// eslint-disable-next-line import/order
import { Button, LinkButton } from "@components/Button";
import "react-simple-keyboard/build/css/index.css"; import "react-simple-keyboard/build/css/index.css";
import DetachIconRaw from "@/assets/detach-icon.svg"; import DetachIconRaw from "@/assets/detach-icon.svg";
import { cx } from "@/cva.config"; import { cx } from "@/cva.config";
import { useHidStore, useUiStore } from "@/hooks/stores"; import { useHidStore, useUiStore } from "@hooks/stores";
import useKeyboard from "@/hooks/useKeyboard"; import useKeyboard from "@hooks/useKeyboard";
import useKeyboardLayout from "@/hooks/useKeyboardLayout"; import useKeyboardLayout from "@hooks/useKeyboardLayout";
import { Button, LinkButton } from "@components/Button";
import Card from "@components/Card";
import { decodeModifiers, keys, latchingKeys, modifiers } from "@/keyboardMappings"; import { decodeModifiers, keys, latchingKeys, modifiers } from "@/keyboardMappings";
import { m } from "@localizations/messages.js";
export const DetachIcon = ({ className }: { className?: string }) => { export const DetachIcon = ({ className }: { className?: string }) => {
return <img src={DetachIconRaw} alt="Detach Icon" className={className} />; return <img src={DetachIconRaw} alt="Detach Icon" className={className} />;
@ -244,20 +242,20 @@ function KeyboardWrapper() {
<Button <Button
size="XS" size="XS"
theme="light" theme="light"
text="Detach" text={m.detach()}
onClick={() => setAttachedVirtualKeyboardVisibility(false)} onClick={() => setAttachedVirtualKeyboardVisibility(false)}
/> />
) : ( ) : (
<Button <Button
size="XS" size="XS"
theme="light" theme="light"
text="Attach" text={m.attach()}
onClick={() => setAttachedVirtualKeyboardVisibility(true)} onClick={() => setAttachedVirtualKeyboardVisibility(true)}
/> />
)} )}
</div> </div>
<h2 className="self-center font-sans text-sm leading-none font-medium text-slate-700 select-none dark:text-slate-300"> <h2 className="self-center font-sans text-sm leading-none font-medium text-slate-700 select-none dark:text-slate-300">
Virtual Keyboard {m.virtual_keyboard_header()}
</h2> </h2>
<div className="absolute right-2 flex items-center gap-x-2"> <div className="absolute right-2 flex items-center gap-x-2">
<div className="hidden md:flex gap-x-2 items-center"> <div className="hidden md:flex gap-x-2 items-center">
@ -274,7 +272,7 @@ function KeyboardWrapper() {
<Button <Button
size="XS" size="XS"
theme="light" theme="light"
text="Hide" text={m.hide()}
LeadingIcon={ChevronDownIcon} LeadingIcon={ChevronDownIcon}
onClick={() => setVirtualKeyboardEnabled(false)} onClick={() => setVirtualKeyboardEnabled(false)}
/> />

View File

@ -1,27 +1,27 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useResizeObserver } from "usehooks-ts"; import { useResizeObserver } from "usehooks-ts";
import VirtualKeyboard from "@components/VirtualKeyboard";
import Actionbar from "@components/ActionBar";
import MacroBar from "@/components/MacroBar";
import InfoBar from "@components/InfoBar";
import notifications from "@/notifications";
import useKeyboard from "@/hooks/useKeyboard";
import { cx } from "@/cva.config"; import { cx } from "@/cva.config";
import { keys } from "@/keyboardMappings"; import useKeyboard from "@hooks/useKeyboard";
import useMouse from "@hooks/useMouse";
import { import {
useRTCStore, useRTCStore,
useSettingsStore, useSettingsStore,
useVideoStore, useVideoStore,
} from "@/hooks/stores"; } from "@hooks/stores";
import useMouse from "@/hooks/useMouse"; import VirtualKeyboard from "@components/VirtualKeyboard";
import Actionbar from "@components/ActionBar";
import MacroBar from "@components/MacroBar";
import InfoBar from "@components/InfoBar";
import { import {
HDMIErrorOverlay, HDMIErrorOverlay,
LoadingVideoOverlay, LoadingVideoOverlay,
NoAutoplayPermissionsOverlay, NoAutoplayPermissionsOverlay,
PointerLockBar, PointerLockBar,
} from "./VideoOverlay"; } from "@components/VideoOverlay";
import { keys } from "@/keyboardMappings";
import notifications from "@/notifications";
import { m } from "@localizations/messages.js";
export default function WebRTCVideo({ hasConnectionIssues }: { hasConnectionIssues: boolean }) { export default function WebRTCVideo({ hasConnectionIssues }: { hasConnectionIssues: boolean }) {
// Video and stream related refs and states // Video and stream related refs and states
@ -168,10 +168,10 @@ export default function WebRTCVideo({ hasConnectionIssues }: { hasConnectionIssu
const handlePointerLockChange = () => { const handlePointerLockChange = () => {
if (document.pointerLockElement) { if (document.pointerLockElement) {
notifications.success("Pointer lock Enabled, press escape to unlock"); notifications.success(m.video_pointer_lock_enabled());
setIsPointerLockActive(true); setIsPointerLockActive(true);
} else { } else {
notifications.success("Pointer lock Disabled"); notifications.success(m.video_pointer_lock_disabled());
setIsPointerLockActive(false); setIsPointerLockActive(false);
} }
}; };
@ -234,6 +234,18 @@ export default function WebRTCVideo({ hasConnectionIssues }: { hasConnectionIssu
[getMouseWheelHandler], [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( const keyDownHandler = useCallback(
(e: KeyboardEvent) => { (e: KeyboardEvent) => {
e.preventDefault(); e.preventDefault();
@ -468,17 +480,6 @@ export default function WebRTCVideo({ hasConnectionIssues }: { hasConnectionIssu
}; };
}, [videoSaturation, videoBrightness, videoContrast]); }, [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 ( return (
<div className="grid h-full w-full grid-rows-(--grid-layout)"> <div className="grid h-full w-full grid-rows-(--grid-layout)">

View File

@ -1,13 +1,13 @@
import { LuHardDrive, LuPower, LuRotateCcw } from "react-icons/lu";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { LuHardDrive, LuPower, LuRotateCcw } from "react-icons/lu";
import { m } from "@localizations/messages.js";
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import { Button } from "@components/Button"; import { Button } from "@components/Button";
import Card from "@components/Card"; import Card from "@components/Card";
import LoadingSpinner from "@components/LoadingSpinner";
import { SettingsPageHeader } from "@components/SettingsPageheader"; import { SettingsPageHeader } from "@components/SettingsPageheader";
import notifications from "@/notifications"; import notifications from "@/notifications";
import LoadingSpinner from "@/components/LoadingSpinner";
import { JsonRpcResponse, useJsonRpc } from "../../hooks/useJsonRpc";
const LONG_PRESS_DURATION = 3000; // 3 seconds for long press const LONG_PRESS_DURATION = 3000; // 3 seconds for long press
@ -33,9 +33,7 @@ export function ATXPowerControl() {
useEffect(() => { useEffect(() => {
send("getATXState", {}, (resp: JsonRpcResponse) => { send("getATXState", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(m.atx_power_control_get_state_error({ error: resp.error.data || m.unknown_error() }));
`Failed to get ATX state: ${resp.error.data || "Unknown error"}`,
);
return; return;
} }
setAtxState(resp.result as ATXState); setAtxState(resp.result as ATXState);
@ -56,9 +54,7 @@ export function ATXPowerControl() {
console.log("Sending long press ATX power action"); console.log("Sending long press ATX power action");
send("setATXPowerAction", { action: "power-long" }, (resp: JsonRpcResponse) => { send("setATXPowerAction", { action: "power-long" }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(m.atx_power_control_send_action_error({ action: m.atx_power_control_long_power_button(), error: resp.error.data || m.unknown_error() }));
`Failed to send ATX power action: ${resp.error.data || "Unknown error"}`,
);
} }
setIsPowerPressed(false); setIsPowerPressed(false);
}); });
@ -77,9 +73,7 @@ export function ATXPowerControl() {
console.log("Sending short press ATX power action"); console.log("Sending short press ATX power action");
send("setATXPowerAction", { action: "power-short" }, (resp: JsonRpcResponse) => { send("setATXPowerAction", { action: "power-short" }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(m.atx_power_control_send_action_error({ action: m.atx_power_control_short_power_button(), error: resp.error.data || m.unknown_error() }));
`Failed to send ATX power action: ${resp.error.data || "Unknown error"}`,
);
} }
}); });
} }
@ -98,8 +92,8 @@ export function ATXPowerControl() {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<SettingsPageHeader <SettingsPageHeader
title="ATX Power Control" title={m.extensions_atx_power_control()}
description="Control your ATX power settings" description={m.extensions_atx_power_control_description()}
/> />
{atxState === null ? ( {atxState === null ? (
@ -115,7 +109,7 @@ export function ATXPowerControl() {
size="SM" size="SM"
theme="light" theme="light"
LeadingIcon={LuPower} LeadingIcon={LuPower}
text="Power" text={m.atx_power_control_power_button()}
onMouseDown={() => handlePowerPress(true)} onMouseDown={() => handlePowerPress(true)}
onMouseUp={() => handlePowerPress(false)} onMouseUp={() => handlePowerPress(false)}
onMouseLeave={() => handlePowerPress(false)} onMouseLeave={() => handlePowerPress(false)}
@ -125,13 +119,11 @@ export function ATXPowerControl() {
size="SM" size="SM"
theme="light" theme="light"
LeadingIcon={LuRotateCcw} LeadingIcon={LuRotateCcw}
text="Reset" text={m.atx_power_control_reset_button()}
onClick={() => { onClick={() => {
send("setATXPowerAction", { action: "reset" }, (resp: JsonRpcResponse) => { send("setATXPowerAction", { action: "reset" }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(m.atx_power_control_send_action_error({ action: m.atx_power_control_reset_button(), error: resp.error.data || m.unknown_error() }));
`Failed to send ATX power action: ${resp.error.data || "Unknown error"}`,
);
return; return;
} }
}); });
@ -150,7 +142,7 @@ export function ATXPowerControl() {
atxState?.power ? "text-green-600" : "text-slate-300" atxState?.power ? "text-green-600" : "text-slate-300"
}`} }`}
/> />
Power LED {m.atx_power_control_power_led()}
</span> </span>
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
@ -161,7 +153,7 @@ export function ATXPowerControl() {
atxState?.hdd ? "text-blue-400" : "text-slate-300" atxState?.hdd ? "text-blue-400" : "text-slate-300"
}`} }`}
/> />
HDD LED {m.atx_power_control_hdd_led()}
</span> </span>
</div> </div>
</div> </div>

View File

@ -1,14 +1,15 @@
import { LuPower } from "react-icons/lu";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { LuPower } from "react-icons/lu";
import { m } from "@localizations/messages.js";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { Button } from "@components/Button"; import { Button } from "@components/Button";
import Card from "@components/Card"; import Card from "@components/Card";
import { SettingsPageHeader } from "@components/SettingsPageheader"; import { SettingsPageHeader } from "@components/SettingsPageheader";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import notifications from "@/notifications";
import FieldLabel from "@components/FieldLabel"; import FieldLabel from "@components/FieldLabel";
import LoadingSpinner from "@components/LoadingSpinner"; import LoadingSpinner from "@components/LoadingSpinner";
import {SelectMenuBasic} from "@components/SelectMenuBasic"; import {SelectMenuBasic} from "@components/SelectMenuBasic";
import notifications from "@/notifications";
interface DCPowerState { interface DCPowerState {
isOn: boolean; isOn: boolean;
@ -25,9 +26,7 @@ export function DCPowerControl() {
const getDCPowerState = useCallback(() => { const getDCPowerState = useCallback(() => {
send("getDCPowerState", {}, (resp: JsonRpcResponse) => { send("getDCPowerState", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(m.dc_power_control_get_state_error({ error: resp.error.data || m.unknown_error() }));
`Failed to get DC power state: ${resp.error.data || "Unknown error"}`,
);
return; return;
} }
setPowerState(resp.result as DCPowerState); setPowerState(resp.result as DCPowerState);
@ -37,9 +36,7 @@ export function DCPowerControl() {
const handlePowerToggle = (enabled: boolean) => { const handlePowerToggle = (enabled: boolean) => {
send("setDCPowerState", { enabled }, (resp: JsonRpcResponse) => { send("setDCPowerState", { enabled }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(m.dc_power_control_set_power_state_error({ enabled: enabled, error: resp.error.data || m.unknown_error() }));
`Failed to set DC power state: ${resp.error.data || "Unknown error"}`,
);
return; return;
} }
getDCPowerState(); // Refresh state after change getDCPowerState(); // Refresh state after change
@ -49,17 +46,13 @@ export function DCPowerControl() {
// const state = powerState?.restoreState === 0 ? 1 : powerState?.restoreState === 1 ? 2 : 0; // const state = powerState?.restoreState === 0 ? 1 : powerState?.restoreState === 1 ? 2 : 0;
send("setDCRestoreState", { state }, (resp: JsonRpcResponse) => { send("setDCRestoreState", { state }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(m.dc_power_control_set_restore_state_error({ state: state, error: resp.error.data || m.unknown_error() }));
`Failed to set DC power state: ${resp.error.data || "Unknown error"}`,
);
return; return;
} }
getDCPowerState(); // Refresh state after change getDCPowerState(); // Refresh state after change
}); });
}; };
useEffect(() => { useEffect(() => {
getDCPowerState(); getDCPowerState();
// Set up polling interval to update status // Set up polling interval to update status
@ -70,8 +63,8 @@ export function DCPowerControl() {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<SettingsPageHeader <SettingsPageHeader
title="DC Power Control" title={m.extensions_dc_power_control()}
description="Control your DC power settings" description={m.extensions_dc_power_control_description()}
/> />
{powerState === null ? ( {powerState === null ? (
@ -87,7 +80,7 @@ export function DCPowerControl() {
size="SM" size="SM"
theme="light" theme="light"
LeadingIcon={LuPower} LeadingIcon={LuPower}
text="Power On" text={m.dc_power_control_power_on_button()}
onClick={() => handlePowerToggle(true)} onClick={() => handlePowerToggle(true)}
disabled={powerState.isOn} disabled={powerState.isOn}
/> />
@ -95,7 +88,7 @@ export function DCPowerControl() {
size="SM" size="SM"
theme="light" theme="light"
LeadingIcon={LuPower} LeadingIcon={LuPower}
text="Power Off" text={m.dc_power_control_power_off_button()}
disabled={!powerState.isOn} disabled={!powerState.isOn}
onClick={() => handlePowerToggle(false)} onClick={() => handlePowerToggle(false)}
/> />
@ -104,13 +97,13 @@ export function DCPowerControl() {
<div className="flex items-center"> <div className="flex items-center">
<SelectMenuBasic <SelectMenuBasic
size="SM" size="SM"
label="Restore Power Loss" label={m.dc_power_control_restore_power_state()}
value={powerState.restoreState} value={powerState.restoreState}
onChange={e => handleRestoreChange(parseInt(e.target.value))} onChange={e => handleRestoreChange(parseInt(e.target.value))}
options={[ options={[
{ value: '0', label: "Power OFF" }, { value: '0', label: m.dc_power_control_power_off_state()},
{ value: '1', label: "Power ON" }, { value: '1', label: m.dc_power_control_power_on_state()},
{ value: '2', label: "Last State" }, { value: '2', label: m.dc_power_control_restore_last_state()},
]} ]}
/> />
</div> </div>
@ -120,21 +113,21 @@ export function DCPowerControl() {
{/* Status Display */} {/* Status Display */}
<div className="grid grid-cols-3 gap-4"> <div className="grid grid-cols-3 gap-4">
<div className="space-y-1"> <div className="space-y-1">
<FieldLabel label="Voltage" /> <FieldLabel label={m.dc_power_control_voltage()} />
<p className="text-sm font-medium text-slate-900 dark:text-slate-100"> <p className="text-sm font-medium text-slate-900 dark:text-slate-100">
{powerState.voltage.toFixed(1)}V {powerState.voltage.toFixed(1)}&nbsp;{m.dc_power_control_voltage_unit()}
</p> </p>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<FieldLabel label="Current" /> <FieldLabel label={m.dc_power_control_current()} />
<p className="text-sm font-medium text-slate-900 dark:text-slate-100"> <p className="text-sm font-medium text-slate-900 dark:text-slate-100">
{powerState.current.toFixed(1)}A {powerState.current.toFixed(1)}&nbsp;{m.dc_power_control_current_unit()}
</p> </p>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<FieldLabel label="Power" /> <FieldLabel label={m.dc_power_control_power()}/>
<p className="text-sm font-medium text-slate-900 dark:text-slate-100"> <p className="text-sm font-medium text-slate-900 dark:text-slate-100">
{powerState.power.toFixed(1)}W {powerState.power.toFixed(1)}&nbsp;{m.dc_power_control_power_unit()}
</p> </p>
</div> </div>
</div> </div>

View File

@ -1,13 +1,14 @@
import { LuTerminal } from "react-icons/lu";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { LuTerminal } from "react-icons/lu";
import { m } from "@localizations/messages.js";
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import { useUiStore } from "@hooks/stores";
import { Button } from "@components/Button"; import { Button } from "@components/Button";
import Card from "@components/Card"; import Card from "@components/Card";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import notifications from "@/notifications";
import { useUiStore } from "@/hooks/stores";
import { SelectMenuBasic } from "@components/SelectMenuBasic"; import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import notifications from "@/notifications";
interface SerialSettings { interface SerialSettings {
baudRate: string; baudRate: string;
@ -28,9 +29,7 @@ export function SerialConsole() {
useEffect(() => { useEffect(() => {
send("getSerialSettings", {}, (resp: JsonRpcResponse) => { send("getSerialSettings", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(m.serial_console_get_settings_error({ error: resp.error.data || m.unknown_error() }));
`Failed to get serial settings: ${resp.error.data || "Unknown error"}`,
);
return; return;
} }
setSettings(resp.result as SerialSettings); setSettings(resp.result as SerialSettings);
@ -41,9 +40,7 @@ export function SerialConsole() {
const newSettings = { ...settings, [setting]: value }; const newSettings = { ...settings, [setting]: value };
send("setSerialSettings", { settings: newSettings }, (resp: JsonRpcResponse) => { send("setSerialSettings", { settings: newSettings }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(m.serial_console_set_settings_error({ settings: setting, error: resp.error.data || m.unknown_error() }));
`Failed to update serial settings: ${resp.error.data || "Unknown error"}`,
);
return; return;
} }
setSettings(newSettings); setSettings(newSettings);
@ -54,8 +51,8 @@ export function SerialConsole() {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<SettingsPageHeader <SettingsPageHeader
title="Serial Console" title={m.extension_serial_console()}
description="Configure your serial console settings" description={m.serial_console_configure_description()}
/> />
<Card className="animate-fadeIn opacity-0"> <Card className="animate-fadeIn opacity-0">
@ -66,10 +63,10 @@ export function SerialConsole() {
size="SM" size="SM"
theme="primary" theme="primary"
LeadingIcon={LuTerminal} LeadingIcon={LuTerminal}
text="Open Console" text={m.serial_console_open_console()}
onClick={() => { onClick={() => {
setTerminalType("serial");
console.log("Opening serial console with settings: ", settings); console.log("Opening serial console with settings: ", settings);
setTerminalType("serial");
}} }}
/> />
</div> </div>
@ -77,7 +74,7 @@ export function SerialConsole() {
{/* Settings */} {/* Settings */}
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<SelectMenuBasic <SelectMenuBasic
label="Baud Rate" label={m.serial_console_baud_rate()}
options={[ options={[
{ label: "1200", value: "1200" }, { label: "1200", value: "1200" },
{ label: "2400", value: "2400" }, { label: "2400", value: "2400" },
@ -93,7 +90,7 @@ export function SerialConsole() {
/> />
<SelectMenuBasic <SelectMenuBasic
label="Data Bits" label={m.serial_console_data_bits()}
options={[ options={[
{ label: "8", value: "8" }, { label: "8", value: "8" },
{ label: "7", value: "7" }, { label: "7", value: "7" },
@ -103,7 +100,7 @@ export function SerialConsole() {
/> />
<SelectMenuBasic <SelectMenuBasic
label="Stop Bits" label={m.serial_console_stop_bits()}
options={[ options={[
{ label: "1", value: "1" }, { label: "1", value: "1" },
{ label: "1.5", value: "1.5" }, { label: "1.5", value: "1.5" },
@ -114,11 +111,13 @@ export function SerialConsole() {
/> />
<SelectMenuBasic <SelectMenuBasic
label="Parity" label={m.serial_console_parity()}
options={[ options={[
{ label: "None", value: "none" }, { label: m.serial_console_parity_none(), value: "none" },
{ label: "Even", value: "even" }, { label: m.serial_console_parity_even(), value: "even" },
{ label: "Odd", value: "odd" }, { label: m.serial_console_parity_odd(), value: "odd" },
{ label: m.serial_console_parity_mark(), value: "mark" },
{ label: m.serial_console_parity_space(), value: "space" },
]} ]}
value={settings.parity} value={settings.parity}
onChange={e => handleSettingChange("parity", e.target.value)} onChange={e => handleSettingChange("parity", e.target.value)}

View File

@ -1,7 +1,8 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { LuPower, LuTerminal, LuPlugZap } from "react-icons/lu"; import { LuPower, LuTerminal, LuPlugZap } from "react-icons/lu";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc"; import { m } from "@localizations/messages.js";
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import Card, { GridCard } from "@components/Card"; import Card, { GridCard } from "@components/Card";
import { SettingsPageHeader } from "@components/SettingsPageheader"; import { SettingsPageHeader } from "@components/SettingsPageheader";
import { ATXPowerControl } from "@components/extensions/ATXPowerControl"; import { ATXPowerControl } from "@components/extensions/ATXPowerControl";
@ -20,20 +21,20 @@ interface Extension {
const AVAILABLE_EXTENSIONS: Extension[] = [ const AVAILABLE_EXTENSIONS: Extension[] = [
{ {
id: "atx-power", id: "atx-power",
name: "ATX Power Control", name: m.extensions_atx_power_control(),
description: "Control your ATX Power extension", description: m.extensions_atx_power_control_description(),
icon: LuPower, icon: LuPower,
}, },
{ {
id: "dc-power", id: "dc-power",
name: "DC Power Control", name: m.extensions_dc_power_control(),
description: "Control your DC Power extension", description: m.extensions_dc_power_control(),
icon: LuPlugZap, icon: LuPlugZap,
}, },
{ {
id: "serial-console", id: "serial-console",
name: "Serial Console", name: m.extension_serial_console(),
description: "Access your serial console extension", description: m.extension_serial_console_description(),
icon: LuTerminal, icon: LuTerminal,
}, },
]; ];
@ -60,7 +61,7 @@ export default function ExtensionPopover() {
send("setActiveExtension", { extensionId: extension?.id || "" }, (resp: JsonRpcResponse) => { send("setActiveExtension", { extensionId: extension?.id || "" }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(
`Failed to set active extension: ${resp.error.data || "Unknown error"}`, m.extension_popover_set_error_notification({ error: resp.error.data || m.unknown_error() }),
); );
return; return;
} }
@ -101,7 +102,7 @@ export default function ExtensionPopover() {
<Button <Button
size="SM" size="SM"
theme="light" theme="light"
text="Unload Extension" text={m.extension_popover_unload_extension()}
onClick={() => handleSetActiveExtension(null)} onClick={() => handleSetActiveExtension(null)}
/> />
</div> </div>
@ -110,8 +111,8 @@ export default function ExtensionPopover() {
// Extensions List View // Extensions List View
<div className="space-y-4"> <div className="space-y-4">
<SettingsPageHeader <SettingsPageHeader
title="Extensions" title={m.extensions_popover_extensions()}
description="Load and manage your extensions" description={m.extension_popover_load_and_manage_extensions()}
/> />
<Card className="animate-fadeIn opacity-0" > <Card className="animate-fadeIn opacity-0" >
<div className="w-full divide-y divide-slate-700/30 dark:divide-slate-600/30"> <div className="w-full divide-y divide-slate-700/30 dark:divide-slate-600/30">
@ -131,7 +132,7 @@ export default function ExtensionPopover() {
<Button <Button
size="XS" size="XS"
theme="light" theme="light"
text="Load" text={m.load()}
onClick={() => handleSetActiveExtension(extension)} onClick={() => handleSetActiveExtension(extension)}
/> />
</div> </div>

View File

@ -1,20 +1,17 @@
import { PlusCircleIcon } from "@heroicons/react/20/solid"; import { PlusCircleIcon } from "@heroicons/react/20/solid";
import { forwardRef, useEffect, useCallback } from "react"; import { forwardRef, useEffect, useCallback } from "react";
import { import { LuLink, LuPlus, LuRadioReceiver } from "react-icons/lu";
LuLink,
LuPlus,
LuRadioReceiver,
} from "react-icons/lu";
import { useClose } from "@headlessui/react"; import { useClose } from "@headlessui/react";
import { useLocation } from "react-router"; import { useLocation } from "react-router";
import { m } from "@localizations/messages.js";
import { Button } from "@components/Button"; import { Button } from "@components/Button";
import Card, { GridCard } from "@components/Card"; import Card, { GridCard } from "@components/Card";
import { formatters } from "@/utils"; import { formatters } from "@/utils";
import { RemoteVirtualMediaState, useMountMediaStore } from "@/hooks/stores"; import { RemoteVirtualMediaState, useMountMediaStore } from "@hooks/stores";
import { SettingsPageHeader } from "@components/SettingsPageheader"; import { SettingsPageHeader } from "@components/SettingsPageheader";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc"; import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation"; import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
import notifications from "@/notifications"; import notifications from "@/notifications";
const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => { const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
@ -25,9 +22,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
const syncRemoteVirtualMediaState = useCallback(() => { const syncRemoteVirtualMediaState = useCallback(() => {
send("getVirtualMediaState", {}, (response: JsonRpcResponse) => { send("getVirtualMediaState", {}, (response: JsonRpcResponse) => {
if ("error" in response) { if ("error" in response) {
notifications.error( notifications.error(m.mount_get_state_error({ error: response.error.message }));
`Failed to get virtual media state: ${response.error.message}`,
);
} else { } else {
setRemoteVirtualMediaState(response.result as unknown as RemoteVirtualMediaState); setRemoteVirtualMediaState(response.result as unknown as RemoteVirtualMediaState);
} }
@ -37,7 +32,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
const handleUnmount = () => { const handleUnmount = () => {
send("unmountImage", {}, (response: JsonRpcResponse) => { send("unmountImage", {}, (response: JsonRpcResponse) => {
if ("error" in response) { if ("error" in response) {
notifications.error(`Failed to unmount image: ${response.error.message}`); notifications.error(m.mount_unmount_error({ error: response.error.message }));
} else { } else {
syncRemoteVirtualMediaState(); syncRemoteVirtualMediaState();
} }
@ -57,10 +52,10 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<h3 className="text-sm font-semibold leading-none text-black dark:text-white"> <h3 className="text-sm font-semibold leading-none text-black dark:text-white">
No mounted media {m.mount_no_mounted_media()}
</h3> </h3>
<p className="text-xs leading-none text-slate-700 dark:text-slate-300"> <p className="text-xs leading-none text-slate-700 dark:text-slate-300">
Add a file to get started {m.mount_add_file_to_get_started()}
</p> </p>
</div> </div>
</div> </div>
@ -81,7 +76,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
</Card> </Card>
</div> </div>
<h3 className="text-base font-semibold text-black dark:text-white"> <h3 className="text-base font-semibold text-black dark:text-white">
Streaming from URL {m.mount_streaming_from_url()}
</h3> </h3>
<p className="truncate text-sm text-slate-900 dark:text-slate-100"> <p className="truncate text-sm text-slate-900 dark:text-slate-100">
{formatters.truncateMiddle(url, 55)} {formatters.truncateMiddle(url, 55)}
@ -105,7 +100,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
</Card> </Card>
</div> </div>
<h3 className="text-base font-semibold text-black dark:text-white"> <h3 className="text-base font-semibold text-black dark:text-white">
Mounted from JetKVM Storage {m.mount_mounted_from_storage()}
</h3> </h3>
<p className="text-sm text-slate-900 dark:text-slate-100"> <p className="text-sm text-slate-900 dark:text-slate-100">
{formatters.truncateMiddle(path, 50)} {formatters.truncateMiddle(path, 50)}
@ -138,8 +133,8 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
<div className="h-full space-y-4"> <div className="h-full space-y-4">
<div className="space-y-4"> <div className="space-y-4">
<SettingsPageHeader <SettingsPageHeader
title="Virtual Media" title={m.mount_virtual_media()}
description="Mount an image to boot from or install an operating system." description={m.mount_virtual_media_description()}
/> />
<div <div
@ -163,9 +158,9 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
{remoteVirtualMediaState ? ( {remoteVirtualMediaState ? (
<div className="flex select-none items-center justify-between text-xs"> <div className="flex select-none items-center justify-between text-xs">
<div className="select-none text-white dark:text-slate-300"> <div className="select-none text-white dark:text-slate-300">
<span>Mounted as</span>{" "} <span>{m.mount_mounted_as()}</span>{" "}
<span className="font-semibold"> <span className="font-semibold">
{remoteVirtualMediaState.mode === "Disk" ? "Disk" : "CD-ROM"} {remoteVirtualMediaState.mode === "Disk" ? m.mount_mode_disk() : m.mount_mode_cdrom()}
</span> </span>
</div> </div>
@ -173,7 +168,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
<Button <Button
size="SM" size="SM"
theme="blank" theme="blank"
text="Close" text={m.close()}
onClick={() => { onClick={() => {
close(); close();
}} }}
@ -181,7 +176,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
<Button <Button
size="SM" size="SM"
theme="light" theme="light"
text="Unmount" text={m.mount_unmount()}
LeadingIcon={({ className }) => ( LeadingIcon={({ className }) => (
<svg <svg
className={`${className} h-2.5 w-2.5 shrink-0`} className={`${className} h-2.5 w-2.5 shrink-0`}
@ -227,7 +222,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
<Button <Button
size="SM" size="SM"
theme="blank" theme="blank"
text="Close" text={m.close()}
onClick={() => { onClick={() => {
close(); close();
}} }}
@ -235,7 +230,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
<Button <Button
size="SM" size="SM"
theme="primary" theme="primary"
text="Add New Media" text={m.mount_add_new_media()}
onClick={() => { onClick={() => {
setModalView("mode"); setModalView("mode");
navigateTo("/mount"); navigateTo("/mount");

View File

@ -1,13 +1,14 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useClose } from "@headlessui/react"; import { useClose } from "@headlessui/react";
import { ExclamationCircleIcon } from "@heroicons/react/16/solid"; import { ExclamationCircleIcon } from "@heroicons/react/16/solid";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { LuCornerDownLeft } from "react-icons/lu"; import { LuCornerDownLeft } from "react-icons/lu";
import { cx } from "@/cva.config"; import { cx } from "@/cva.config";
import { useHidStore, useSettingsStore, useUiStore } from "@/hooks/stores"; import { m } from "@localizations/messages.js";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc"; import { useHidStore, useSettingsStore, useUiStore } from "@hooks/stores";
import useKeyboard, { type MacroStep } from "@/hooks/useKeyboard"; import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import useKeyboardLayout from "@/hooks/useKeyboardLayout"; import useKeyboard, { type MacroStep } from "@hooks/useKeyboard";
import useKeyboardLayout from "@hooks/useKeyboardLayout";
import notifications from "@/notifications"; import notifications from "@/notifications";
import { Button } from "@components/Button"; import { Button } from "@components/Button";
import { GridCard } from "@components/Card"; import { GridCard } from "@components/Card";
@ -105,7 +106,7 @@ export default function PasteModal() {
} }
} catch (error) { } catch (error) {
console.error("Failed to paste text:", error); console.error("Failed to paste text:", error);
notifications.error("Failed to paste text"); notifications.error(m.paste_modal_failed_paste({ error: String(error) }));
} }
}, [selectedKeyboard, executeMacro, delay]); }, [selectedKeyboard, executeMacro, delay]);
@ -122,8 +123,8 @@ export default function PasteModal() {
<div className="h-full space-y-4"> <div className="h-full space-y-4">
<div className="space-y-4"> <div className="space-y-4">
<SettingsPageHeader <SettingsPageHeader
title="Paste text" title={m.paste_text()}
description="Paste text from your client to the remote host" description={m.paste_text_description()}
/> />
<div <div
@ -143,7 +144,7 @@ export default function PasteModal() {
> >
<TextAreaWithLabel <TextAreaWithLabel
ref={TextAreaRef} ref={TextAreaRef}
label="Paste from host" label={m.paste_modal_paste_from_host()}
rows={4} rows={4}
onKeyUp={e => e.stopPropagation()} onKeyUp={e => e.stopPropagation()}
maxLength={pasteMaxLength} maxLength={pasteMaxLength}
@ -176,7 +177,7 @@ export default function PasteModal() {
<div className="mt-2 flex items-center gap-x-2"> <div className="mt-2 flex items-center gap-x-2">
<ExclamationCircleIcon className="h-4 w-4 text-red-500 dark:text-red-400" /> <ExclamationCircleIcon className="h-4 w-4 text-red-500 dark:text-red-400" />
<span className="text-xs text-red-500 dark:text-red-400"> <span className="text-xs text-red-500 dark:text-red-400">
The following characters won&apos;t be pasted:{" "} {m.paste_modal_invalid_chars_intro()}{" "}
{invalidChars.join(", ")} {invalidChars.join(", ")}
</span> </span>
</div> </div>
@ -186,8 +187,8 @@ export default function PasteModal() {
<div className={cx("text-xs text-slate-600 dark:text-slate-400", delayClassName)}> <div className={cx("text-xs text-slate-600 dark:text-slate-400", delayClassName)}>
<InputFieldWithLabel <InputFieldWithLabel
type="number" type="number"
label="Delay between keys" label={m.paste_modal_delay_between_keys()}
placeholder="Delay between keys" placeholder={m.paste_modal_delay_between_keys()}
min={50} min={50}
max={65534} max={65534}
value={delayValue} value={delayValue}
@ -199,15 +200,14 @@ export default function PasteModal() {
<div className="mt-2 flex items-center gap-x-2"> <div className="mt-2 flex items-center gap-x-2">
<ExclamationCircleIcon className="h-4 w-4 text-red-500 dark:text-red-400" /> <ExclamationCircleIcon className="h-4 w-4 text-red-500 dark:text-red-400" />
<span className="text-xs text-red-500 dark:text-red-400"> <span className="text-xs text-red-500 dark:text-red-400">
Delay must be between 50 and 65534 {m.paste_modal_delay_out_of_range({ min: 50, max: 65534 })}
</span> </span>
</div> </div>
)} )}
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<p className="text-xs text-slate-600 dark:text-slate-400"> <p className="text-xs text-slate-600 dark:text-slate-400">
Sending text using keyboard layout: {selectedKeyboard.isoCode}- {m.paste_modal_sending_using_layout({ iso: selectedKeyboard.isoCode, name: selectedKeyboard.name })}
{selectedKeyboard.name}
</p> </p>
</div> </div>
</div> </div>
@ -224,7 +224,7 @@ export default function PasteModal() {
<Button <Button
size="SM" size="SM"
theme="blank" theme="blank"
text="Cancel" text={m.cancel()}
onClick={() => { onClick={() => {
onCancelPasteMode(); onCancelPasteMode();
close(); close();
@ -233,7 +233,7 @@ export default function PasteModal() {
<Button <Button
size="SM" size="SM"
theme="primary" theme="primary"
text="Confirm Paste" text={m.paste_modal_confirm_paste()}
disabled={isPasteInProgress} disabled={isPasteInProgress}
onClick={onConfirmPaste} onClick={onConfirmPaste}
LeadingIcon={LuCornerDownLeft} LeadingIcon={LuCornerDownLeft}

View File

@ -1,8 +1,9 @@
import { useState, useRef } from "react"; import { useState, useRef } from "react";
import { LuPlus, LuArrowLeft } from "react-icons/lu"; import { LuPlus, LuArrowLeft } from "react-icons/lu";
import { InputFieldWithLabel } from "@/components/InputField"; import { m } from "@localizations/messages.js";
import { Button } from "@/components/Button"; import { InputFieldWithLabel } from "@components/InputField";
import { Button } from "@components/Button";
interface AddDeviceFormProps { interface AddDeviceFormProps {
onAddDevice: (name: string, macAddress: string) => void; onAddDevice: (name: string, macAddress: string) => void;
@ -34,8 +35,8 @@ export default function AddDeviceForm({
> >
<InputFieldWithLabel <InputFieldWithLabel
ref={nameInputRef} ref={nameInputRef}
placeholder="Plex Media Server" placeholder={m.wake_on_lan_add_device_example_device_name()}
label="Device Name" label={m.wake_on_lan_add_device_device_name()}
required required
onChange={e => { onChange={e => {
setIsDeviceNameValid(e.target.validity.valid); setIsDeviceNameValid(e.target.validity.valid);
@ -46,7 +47,7 @@ export default function AddDeviceForm({
<InputFieldWithLabel <InputFieldWithLabel
ref={macInputRef} ref={macInputRef}
placeholder="00:b0:d0:63:c2:26" placeholder="00:b0:d0:63:c2:26"
label="MAC Address" label={m.wake_on_lan_add_device_mac_address()}
onKeyUp={e => e.stopPropagation()} onKeyUp={e => e.stopPropagation()}
required required
pattern="^([0-9a-fA-F][0-9a-fA-F]:){5}([0-9a-fA-F][0-9a-fA-F])$" pattern="^([0-9a-fA-F][0-9a-fA-F]:){5}([0-9a-fA-F][0-9a-fA-F])$"
@ -82,14 +83,14 @@ export default function AddDeviceForm({
<Button <Button
size="SM" size="SM"
theme="light" theme="light"
text="Back" text={m.back()}
LeadingIcon={LuArrowLeft} LeadingIcon={LuArrowLeft}
onClick={() => setShowAddForm(false)} onClick={() => setShowAddForm(false)}
/> />
<Button <Button
size="SM" size="SM"
theme="primary" theme="primary"
text="Save Device" text={m.wake_on_lan_add_device_save_device()}
disabled={!isDeviceNameValid || !isMacAddressValid} disabled={!isDeviceNameValid || !isMacAddressValid}
onClick={() => { onClick={() => {
const deviceName = nameInputRef.current?.value || ""; const deviceName = nameInputRef.current?.value || "";

View File

@ -1,8 +1,9 @@
import { LuPlus, LuSend, LuTrash2 } from "react-icons/lu"; import { LuPlus, LuSend, LuTrash2 } from "react-icons/lu";
import { Button } from "@/components/Button"; import { m } from "@localizations/messages.js";
import Card from "@/components/Card"; import { Button } from "@components/Button";
import { FieldError } from "@/components/InputField"; import Card from "@components/Card";
import { FieldError } from "@components/InputField";
export interface StoredDevice { export interface StoredDevice {
name: string; name: string;
@ -46,7 +47,7 @@ export default function DeviceList({
<Button <Button
size="XS" size="XS"
theme="light" theme="light"
text="Wake" text={m.wake_on_lan_device_list_wake()}
LeadingIcon={LuSend} LeadingIcon={LuSend}
onClick={() => onSendMagicPacket(device.macAddress)} onClick={() => onSendMagicPacket(device.macAddress)}
/> />
@ -55,7 +56,7 @@ export default function DeviceList({
theme="danger" theme="danger"
LeadingIcon={LuTrash2} LeadingIcon={LuTrash2}
onClick={() => onDeleteDevice(index)} onClick={() => onDeleteDevice(index)}
aria-label="Delete device" aria-label={m.wake_on_lan_device_list_delete_device()}
/> />
</div> </div>
</div> </div>
@ -69,11 +70,11 @@ export default function DeviceList({
animationDelay: "0.2s", animationDelay: "0.2s",
}} }}
> >
<Button size="SM" theme="blank" text="Close" onClick={onCancelWakeOnLanModal} /> <Button size="SM" theme="blank" text={m.close()} onClick={onCancelWakeOnLanModal} />
<Button <Button
size="SM" size="SM"
theme="primary" theme="primary"
text="Add New Device" text={m.wake_on_lan_device_list_add_new_device()}
onClick={() => setShowAddForm(true)} onClick={() => setShowAddForm(true)}
LeadingIcon={LuPlus} LeadingIcon={LuPlus}
/> />

View File

@ -1,8 +1,9 @@
import { PlusCircleIcon } from "@heroicons/react/16/solid"; import { PlusCircleIcon } from "@heroicons/react/16/solid";
import { LuPlus } from "react-icons/lu"; import { LuPlus } from "react-icons/lu";
import Card from "@/components/Card"; import { m } from "@localizations/messages.js";
import { Button } from "@/components/Button"; import Card from "@components/Card";
import { Button } from "@components/Button";
export default function EmptyStateCard({ export default function EmptyStateCard({
onCancelWakeOnLanModal, onCancelWakeOnLanModal,
@ -25,10 +26,10 @@ export default function EmptyStateCard({
</Card> </Card>
</div> </div>
<h3 className="text-sm font-semibold leading-none text-black dark:text-white"> <h3 className="text-sm font-semibold leading-none text-black dark:text-white">
No devices added {m.wake_on_lan_empty_no_devices_added()}
</h3> </h3>
<p className="text-xs leading-none text-slate-700 dark:text-slate-300"> <p className="text-xs leading-none text-slate-700 dark:text-slate-300">
Add a device to start using Wake-on-LAN {m.wake_on_lan_empty_add_device_to_start()}
</p> </p>
</div> </div>
</div> </div>
@ -41,11 +42,11 @@ export default function EmptyStateCard({
animationDelay: "0.2s", animationDelay: "0.2s",
}} }}
> >
<Button size="SM" theme="blank" text="Close" onClick={onCancelWakeOnLanModal} /> <Button size="SM" theme="blank" text={m.close()} onClick={onCancelWakeOnLanModal} />
<Button <Button
size="SM" size="SM"
theme="primary" theme="primary"
text="Add New Device" text={m.wake_on_lan_empty_add_new_device()}
onClick={() => setShowAddForm(true)} onClick={() => setShowAddForm(true)}
LeadingIcon={LuPlus} LeadingIcon={LuPlus}
/> />

View File

@ -1,10 +1,11 @@
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { useClose } from "@headlessui/react"; import { useClose } from "@headlessui/react";
import { m } from "@localizations/messages.js";
import { GridCard } from "@components/Card"; import { GridCard } from "@components/Card";
import { SettingsPageHeader } from "@components/SettingsPageheader"; import { SettingsPageHeader } from "@components/SettingsPageheader";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc"; import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import { useRTCStore, useUiStore } from "@/hooks/stores"; import { useRTCStore, useUiStore } from "@hooks/stores";
import notifications from "@/notifications"; import notifications from "@/notifications";
import EmptyStateCard from "./EmptyStateCard"; import EmptyStateCard from "./EmptyStateCard";
@ -35,12 +36,12 @@ export default function WakeOnLanModal() {
if ("error" in resp) { if ("error" in resp) {
const isInvalid = resp.error.data?.includes("invalid MAC address"); const isInvalid = resp.error.data?.includes("invalid MAC address");
if (isInvalid) { if (isInvalid) {
setErrorMessage("Invalid MAC address"); setErrorMessage(m.wake_on_lan_invalid_mac());
} else { } else {
setErrorMessage("Failed to send Magic Packet"); setErrorMessage(m.wake_on_lan_failed_send_magic());
} }
} else { } else {
notifications.success("Magic Packet sent successfully"); notifications.success(m.wake_on_lan_magic_sent_success());
setDisableVideoFocusTrap(false); setDisableVideoFocusTrap(false);
close(); close();
} }
@ -87,7 +88,7 @@ export default function WakeOnLanModal() {
send("setWakeOnLanDevices", { params: { devices: updatedDevices } }, (resp: JsonRpcResponse) => { send("setWakeOnLanDevices", { params: { devices: updatedDevices } }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
console.error("Failed to add Wake-on-LAN device:", resp.error); console.error("Failed to add Wake-on-LAN device:", resp.error);
setAddDeviceErrorMessage("Failed to add device"); setAddDeviceErrorMessage(m.wake_on_lan_failed_add_device());
} else { } else {
setShowAddForm(false); setShowAddForm(false);
syncStoredDevices(); syncStoredDevices();
@ -103,8 +104,8 @@ export default function WakeOnLanModal() {
<div className="grid h-full grid-rows-(--grid-headerBody)"> <div className="grid h-full grid-rows-(--grid-headerBody)">
<div className="space-y-4"> <div className="space-y-4">
<SettingsPageHeader <SettingsPageHeader
title="Wake On LAN" title={m.wake_on_lan()}
description="Send a Magic Packet to wake up a remote device." description={m.wake_on_lan_description()}
/> />
{showAddForm ? ( {showAddForm ? (

View File

@ -1,12 +1,12 @@
import { useInterval } from "usehooks-ts"; import { useInterval } from "usehooks-ts";
import SidebarHeader from "@/components/SidebarHeader"; import { m } from "@localizations/messages.js";
import { useRTCStore, useUiStore } from "@/hooks/stores"; import { useRTCStore, useUiStore } from "@hooks/stores";
import { createChartArray, Metric } from "@components/Metric";
import { SettingsSectionHeader } from "@components/SettingsSectionHeader";
import SidebarHeader from "@components/SidebarHeader";
import { someIterable } from "@/utils"; import { someIterable } from "@/utils";
import { createChartArray, Metric } from "../Metric";
import { SettingsSectionHeader } from "../SettingsSectionHeader";
export default function ConnectionStatsSidebar() { export default function ConnectionStatsSidebar() {
const { sidebarView, setSidebarView } = useUiStore(); const { sidebarView, setSidebarView } = useUiStore();
const { const {
@ -95,7 +95,7 @@ export default function ConnectionStatsSidebar() {
return ( return (
<div className="grid h-full grid-rows-(--grid-headerBody) shadow-xs"> <div className="grid h-full grid-rows-(--grid-headerBody) shadow-xs">
<SidebarHeader title="Connection Stats" setSidebarView={setSidebarView} /> <SidebarHeader title={m.connection_stats_sidebar()} setSidebarView={setSidebarView} />
<div className="h-full space-y-4 overflow-y-scroll bg-white px-4 py-2 pb-8 dark:bg-slate-900"> <div className="h-full space-y-4 overflow-y-scroll bg-white px-4 py-2 pb-8 dark:bg-slate-900">
<div className="space-y-4"> <div className="space-y-4">
{sidebarView === "connection-stats" && ( {sidebarView === "connection-stats" && (
@ -103,12 +103,12 @@ export default function ConnectionStatsSidebar() {
{/* Connection Group */} {/* Connection Group */}
<div className="space-y-3"> <div className="space-y-3">
<SettingsSectionHeader <SettingsSectionHeader
title="Connection" title={m.connection_stats_connection()}
description="The connection between the client and the JetKVM." description={m.connection_stats_connection_description()}
/> />
<Metric <Metric
title="Round-Trip Time" title={m.connection_stats_round_trip_time()}
description="Round-trip time for the active ICE candidate pair between peers." description={m.connection_stats_round_trip_time_description()}
stream={iceCandidatePairStats} stream={iceCandidatePairStats}
metric="currentRoundTripTime" metric="currentRoundTripTime"
map={x => ({ map={x => ({
@ -116,23 +116,23 @@ export default function ConnectionStatsSidebar() {
metric: x.metric != null ? Math.round(x.metric * 1000) : null, metric: x.metric != null ? Math.round(x.metric * 1000) : null,
})} })}
domain={[0, 600]} domain={[0, 600]}
unit=" ms" unit={m.connection_stats_unit_milliseconds()}
/> />
</div> </div>
{/* Video Group */} {/* Video Group */}
<div className="space-y-3"> <div className="space-y-3">
<SettingsSectionHeader <SettingsSectionHeader
title="Video" title={m.connection_stats_video()}
description="The video stream from the JetKVM to the client." description={m.connection_stats_video_description()}
/> />
{/* RTP Jitter */} {/* RTP Jitter */}
<Metric <Metric
title="Network Stability" title={m.connection_stats_network_stability()}
badge="Jitter" badge={m.connection_stats_badge_jitter()}
badgeTheme="light" badgeTheme="light"
description="How steady the flow of inbound video packets is across the network." description={m.connection_stats_network_stability_description()}
stream={inboundVideoRtpStats} stream={inboundVideoRtpStats}
metric="jitter" metric="jitter"
map={x => ({ map={x => ({
@ -140,14 +140,14 @@ export default function ConnectionStatsSidebar() {
metric: x.metric != null ? Math.round(x.metric * 1000) : null, metric: x.metric != null ? Math.round(x.metric * 1000) : null,
})} })}
domain={[0, 10]} domain={[0, 10]}
unit=" ms" unit={m.connection_stats_unit_milliseconds()}
/> />
{/* Playback Delay */} {/* Playback Delay */}
<Metric <Metric
title="Playback Delay" title={m.connection_stats_playback_delay()}
description="Delay added by the jitter buffer to smooth playback when frames arrive unevenly." description={m.connection_stats_playback_delay_description()}
badge="Jitter Buffer Avg. Delay" badge={m.connection_stats_badge_jitter_buffer_avg_delay()}
badgeTheme="light" badgeTheme="light"
data={jitterBufferAvgDelayData} data={jitterBufferAvgDelayData}
gate={inboundVideoRtpStats} gate={inboundVideoRtpStats}
@ -162,27 +162,27 @@ export default function ConnectionStatsSidebar() {
) )
} }
domain={[0, 30]} domain={[0, 30]}
unit=" ms" unit={m.connection_stats_unit_milliseconds()}
/> />
{/* Packets Lost */} {/* Packets Lost */}
<Metric <Metric
title="Packets Lost" title={m.connection_stats_packets_lost()}
description="Count of lost inbound video RTP packets." description={m.connection_stats_packets_lost_description()}
stream={inboundVideoRtpStats} stream={inboundVideoRtpStats}
metric="packetsLost" metric="packetsLost"
domain={[0, 100]} domain={[0, 100]}
unit=" packets" unit={m.connection_stats_unit_packets()}
/> />
{/* Frames Per Second */} {/* Frames Per Second */}
<Metric <Metric
title="Frames per second" title={m.connection_stats_frames_per_second()}
description="Number of inbound video frames displayed per second." description={m.connection_stats_frames_per_second_description()}
stream={inboundVideoRtpStats} stream={inboundVideoRtpStats}
metric="framesPerSecond" metric="framesPerSecond"
domain={[0, 80]} domain={[0, 80]}
unit=" fps" unit={m.connection_stats_unit_frames_per_second()}
/> />
</div> </div>
</div> </div>

View File

@ -245,6 +245,8 @@ export class KeyboardMacroReportMessage extends RpcMessage {
...fromUint32toUint8(this.stepCount), ...fromUint32toUint8(this.stepCount),
]), 0); ]), 0);
let offset = 6;
for (let i = 0; i < this.stepCount; i++) { for (let i = 0; i < this.stepCount; i++) {
const step = this.steps[i]; const step = this.steps[i];
if (!withinUint8Range(step.modifier)) { if (!withinUint8Range(step.modifier)) {
@ -270,10 +272,9 @@ export class KeyboardMacroReportMessage extends RpcMessage {
...keys, ...keys,
...fromUint16toUint8(step.delay), ...fromUint16toUint8(step.delay),
]); ]);
const offset = 6 + i * 9;
data.set(macroBinary, offset); data.set(macroBinary, offset);
offset += 9;
} }
return data; return data;

View File

@ -258,6 +258,7 @@ export interface MouseMove {
y: number; y: number;
buttons: number; buttons: number;
} }
export interface MouseState { export interface MouseState {
mouseX: number; mouseX: number;
mouseY: number; mouseY: number;
@ -361,8 +362,10 @@ export interface SettingsState {
// Video enhancement settings // Video enhancement settings
videoSaturation: number; videoSaturation: number;
setVideoSaturation: (value: number) => void; setVideoSaturation: (value: number) => void;
videoBrightness: number; videoBrightness: number;
setVideoBrightness: (value: number) => void; setVideoBrightness: (value: number) => void;
videoContrast: number; videoContrast: number;
setVideoContrast: (value: number) => void; setVideoContrast: (value: number) => void;
} }
@ -406,8 +409,10 @@ export const useSettingsStore = create(
// Video enhancement settings with default values (1.0 = normal) // Video enhancement settings with default values (1.0 = normal)
videoSaturation: 1.0, videoSaturation: 1.0,
setVideoSaturation: (value: number) => set({ videoSaturation: value }), setVideoSaturation: (value: number) => set({ videoSaturation: value }),
videoBrightness: 1.0, videoBrightness: 1.0,
setVideoBrightness: (value: number) => set({ videoBrightness: value }), setVideoBrightness: (value: number) => set({ videoBrightness: value }),
videoContrast: 1.0, videoContrast: 1.0,
setVideoContrast: (value: number) => set({ videoContrast: value }), setVideoContrast: (value: number) => set({ videoContrast: value }),
}), }),
@ -507,7 +512,7 @@ export const useHidStore = create<HidState>(set => ({
keyboardLedState: { num_lock: false, caps_lock: false, scroll_lock: false, compose: false, kana: false, shift: false } as KeyboardLedState, keyboardLedState: { num_lock: false, caps_lock: false, scroll_lock: false, compose: false, kana: false, shift: false } as KeyboardLedState,
setKeyboardLedState: (ledState: KeyboardLedState): void => set({ keyboardLedState: ledState }), setKeyboardLedState: (ledState: KeyboardLedState): void => set({ keyboardLedState: ledState }),
keysDownState: { modifier: 0, keys: [0,0,0,0,0,0] } as KeysDownState, keysDownState: { modifier: 0, keys: [0, 0, 0, 0, 0, 0] } as KeysDownState,
setKeysDownState: (state: KeysDownState): void => set({ keysDownState: state }), setKeysDownState: (state: KeysDownState): void => set({ keysDownState: state }),
isVirtualKeyboardEnabled: false, isVirtualKeyboardEnabled: false,
@ -617,7 +622,7 @@ export type UsbConfigModalViews =
| "updateUsbConfigSuccess"; | "updateUsbConfigSuccess";
export interface UsbConfigModalState { export interface UsbConfigModalState {
modalView: UsbConfigModalViews ; modalView: UsbConfigModalViews;
errorMessage: string | null; errorMessage: string | null;
setModalView: (view: UsbConfigModalViews) => void; setModalView: (view: UsbConfigModalViews) => void;
setErrorMessage: (message: string | null) => void; setErrorMessage: (message: string | null) => void;
@ -647,8 +652,8 @@ export type LocalAuthModalViews =
| "updateSuccess"; | "updateSuccess";
export interface LocalAuthModalState { export interface LocalAuthModalState {
modalView:LocalAuthModalViews; modalView: LocalAuthModalViews;
setModalView: (view:LocalAuthModalViews) => void; setModalView: (view: LocalAuthModalViews) => void;
} }
export const useLocalAuthModalStore = create<LocalAuthModalState>(set => ({ export const useLocalAuthModalStore = create<LocalAuthModalState>(set => ({

View File

@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo } from "react"; import { useCallback, useEffect, useMemo } from "react";
import { useRTCStore } from "@/hooks/stores"; import { useRTCStore } from "@hooks/stores";
import { import {
CancelKeyboardMacroReportMessage, CancelKeyboardMacroReportMessage,
@ -78,7 +78,7 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
try { try {
data = message.marshal(); data = message.marshal();
} catch (e) { } catch (e) {
console.error("Failed to send HID RPC message", e); console.error("Failed to marshal HID RPC message", e);
} }
if (!data) return; if (!data) return;
@ -223,13 +223,19 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
setRpcHidProtocolVersion(null); setRpcHidProtocolVersion(null);
}; };
const errorHandler = (e: Event) => {
console.error(`Error on rpcHidChannel '${rpcHidChannel.label}': ${e}`)
};
rpcHidChannel.addEventListener("message", messageHandler); rpcHidChannel.addEventListener("message", messageHandler);
rpcHidChannel.addEventListener("close", closeHandler); rpcHidChannel.addEventListener("close", closeHandler);
rpcHidChannel.addEventListener("error", errorHandler);
rpcHidChannel.addEventListener("open", openHandler); rpcHidChannel.addEventListener("open", openHandler);
return () => { return () => {
rpcHidChannel.removeEventListener("message", messageHandler); rpcHidChannel.removeEventListener("message", messageHandler);
rpcHidChannel.removeEventListener("close", closeHandler); rpcHidChannel.removeEventListener("close", closeHandler);
rpcHidChannel.removeEventListener("error", errorHandler);
rpcHidChannel.removeEventListener("open", openHandler); rpcHidChannel.removeEventListener("open", openHandler);
}; };
}, [ }, [

View File

@ -1,6 +1,6 @@
import { useCallback, useEffect } from "react"; import { useCallback, useEffect } from "react";
import { useRTCStore } from "@/hooks/stores"; import { useRTCStore } from "@hooks/stores";
export interface JsonRpcRequest { export interface JsonRpcRequest {
jsonrpc: string; jsonrpc: string;

View File

@ -54,7 +54,8 @@ export default function useKeyboard() {
// support the keyPressReport API. In that case, we need to handle the key presses locally // support the keyPressReport API. In that case, we need to handle the key presses locally
// and send the full state to the device, so it can behave like a real USB HID keyboard. // and send the full state to the device, so it can behave like a real USB HID keyboard.
// This flag indicates whether the keyPressReport API is available on the device which is // This flag indicates whether the keyPressReport API is available on the device which is
// dynamically set when the device responds to the first key press event or reports its // keysDownState when queried since the keyPressReport was introduced together with the // dynamically set when the device responds to the first key press event or reports its
// keysDownState when queried since the keyPressReport was introduced together with the
// getKeysDownState API. // getKeysDownState API.
// HidRPC is a binary format for exchanging keyboard and mouse events // HidRPC is a binary format for exchanging keyboard and mouse events
@ -148,66 +149,6 @@ export default function useKeyboard() {
} }
}, [rpcHidReady, sendKeyboardEventHidRpc, handleLegacyKeyboardReport, cancelKeepAlive]); }, [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 // IMPORTANT: See the keyPressReportApiAvailable comment above for the reason this exists
function simulateDeviceSideKeyHandlingForLegacyDevices( function simulateDeviceSideKeyHandlingForLegacyDevices(
@ -272,12 +213,71 @@ export default function useKeyboard() {
return { modifier: modifiers, keys }; 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 // Cleanup function to cancel keepalive timer
const cleanup = useCallback(() => { const cleanup = useCallback(() => {
cancelKeepAlive(); cancelKeepAlive();
}, [cancelKeepAlive]); }, [cancelKeepAlive]);
// executeMacro is used to execute a macro consisting of multiple steps. // executeMacro is used to execute a macro consisting of multiple steps.
// Each step can have multiple keys, multiple modifiers and a delay. // Each step can have multiple keys, multiple modifiers and a delay.
// The keys and modifiers are pressed together and held for the delay duration. // The keys and modifiers are pressed together and held for the delay duration.
@ -306,6 +306,7 @@ export default function useKeyboard() {
sendKeyboardMacroEventHidRpc(macro); sendKeyboardMacroEventHidRpc(macro);
}, [sendKeyboardMacroEventHidRpc]); }, [sendKeyboardMacroEventHidRpc]);
const executeMacroClientSide = useCallback(async (steps: MacroSteps) => { const executeMacroClientSide = useCallback(async (steps: MacroSteps) => {
const promises: (() => Promise<void>)[] = []; const promises: (() => Promise<void>)[] = [];
@ -355,6 +356,7 @@ export default function useKeyboard() {
}); });
}); });
}, [sendKeystrokeLegacy, resetKeyboardState, setAbortController]); }, [sendKeystrokeLegacy, resetKeyboardState, setAbortController]);
const executeMacro = useCallback(async (steps: MacroSteps) => { const executeMacro = useCallback(async (steps: MacroSteps) => {
if (rpcHidReady) { if (rpcHidReady) {
return executeMacroRemote(steps); return executeMacroRemote(steps);

View File

@ -3,6 +3,7 @@ import { useCallback } from "react";
import { useDeviceStore } from "@/hooks/stores"; import { useDeviceStore } from "@/hooks/stores";
import { type JsonRpcResponse, RpcMethodNotFound, useJsonRpc } from "@/hooks/useJsonRpc"; import { type JsonRpcResponse, RpcMethodNotFound, useJsonRpc } from "@/hooks/useJsonRpc";
import notifications from "@/notifications"; import notifications from "@/notifications";
import { m } from "@localizations/messages.js";
export interface VersionInfo { export interface VersionInfo {
appVersion: string; appVersion: string;
@ -29,7 +30,7 @@ export function useVersion() {
return new Promise<SystemVersionInfo>((resolve, reject) => { return new Promise<SystemVersionInfo>((resolve, reject) => {
send("getUpdateStatus", {}, (resp: JsonRpcResponse) => { send("getUpdateStatus", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error(`Failed to check for updates: ${resp.error}`); notifications.error(m.updates_failed_check({ error: String(resp.error) }));
reject(new Error("Failed to check for updates")); reject(new Error("Failed to check for updates"));
} else { } else {
const result = resp.result as SystemVersionInfo; const result = resp.result as SystemVersionInfo;
@ -37,7 +38,7 @@ export function useVersion() {
setSystemVersion(result.local.systemVersion); setSystemVersion(result.local.systemVersion);
if (result.error) { if (result.error) {
notifications.error(`Failed to check for updates: ${result.error}`); notifications.error(m.updates_failed_check({ error: String(result.error) }));
reject(new Error("Failed to check for updates")); reject(new Error("Failed to check for updates"));
} else { } else {
resolve(result); resolve(result);
@ -57,7 +58,7 @@ export function useVersion() {
return getVersionInfo().then(result => resolve(result.local)).catch(reject); return getVersionInfo().then(result => resolve(result.local)).catch(reject);
} }
console.error("Failed to get device version N", resp.error); console.error("Failed to get device version N", resp.error);
notifications.error(`Failed to get device version: ${resp.error}`); notifications.error(m.updates_failed_get_device_version({ error: String(resp.error) }));
reject(new Error("Failed to get device version")); reject(new Error("Failed to get device version"));
} else { } else {
const result = resp.result as VersionInfo; const result = resp.result as VersionInfo;

View File

@ -29,4 +29,4 @@ import { nb_NO } from "@/keyboardLayouts/nb_NO"
import { sv_SE } from "@/keyboardLayouts/sv_SE" import { sv_SE } from "@/keyboardLayouts/sv_SE"
import { da_DK } from "@/keyboardLayouts/da_DK" import { da_DK } from "@/keyboardLayouts/da_DK"
export const keyboards: KeyboardLayout[] = [ cs_CZ, de_CH, de_DE, en_UK, en_US, es_ES, fr_BE, fr_CH, fr_FR, it_IT, nb_NO, sv_SE, da_DK ]; export const keyboards: KeyboardLayout[] = [cs_CZ, de_CH, de_DE, en_UK, en_US, es_ES, fr_BE, fr_CH, fr_FR, it_IT, nb_NO, sv_SE, da_DK];

View File

@ -166,7 +166,7 @@ export const chars = {
"~": { key: "BracketRight", deadKey: true, altRight: true }, "~": { key: "BracketRight", deadKey: true, altRight: true },
"^": { key: "BracketRight", deadKey: true, shift: true }, "^": { key: "BracketRight", deadKey: true, shift: true },
"¨": { key: "BracketRight", deadKey: true, }, "¨": { key: "BracketRight", deadKey: true, },
"|": { key: "Equal", deadKey: true, altRight: true}, "|": { key: "Equal", deadKey: true, altRight: true },
"`": { key: "Equal", deadKey: true, shift: true, }, "`": { key: "Equal", deadKey: true, shift: true, },
"´": { key: "Equal", deadKey: true, }, "´": { key: "Equal", deadKey: true, },
" ": { key: "Space" }, " ": { key: "Space" },

View File

@ -121,7 +121,7 @@ export const keys = {
Hanja: 0x91, Hanja: 0x91,
Katakana: 0x92, Katakana: 0x92,
Hiragana: 0x93, Hiragana: 0x93,
ZenkakuHankaku:0x94, ZenkakuHankaku: 0x94,
LockingCapsLock: 0x82, LockingCapsLock: 0x82,
LockingNumLock: 0x83, LockingNumLock: 0x83,
LockingScrollLock: 0x84, LockingScrollLock: 0x84,

View File

@ -1,6 +1,5 @@
import { lazy } from "react"; import { lazy } from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import "./index.css";
import { import {
createBrowserRouter, createBrowserRouter,
isRouteErrorResponse, isRouteErrorResponse,
@ -8,11 +7,13 @@ import {
RouterProvider, RouterProvider,
useRouteError, useRouteError,
} from "react-router"; } from "react-router";
import "./index.css";
import { ExclamationTriangleIcon } from "@heroicons/react/16/solid"; import { ExclamationTriangleIcon } from "@heroicons/react/16/solid";
import { CLOUD_API, DEVICE_API } from "@/ui.config"; import { CLOUD_API, DEVICE_API } from "@/ui.config";
import api from "@/api"; import api from "@/api";
import Root from "@/root"; import Root from "@/root";
import { m } from "@localizations/messages.js";
import Card from "@components/Card"; import Card from "@components/Card";
import EmptyCard from "@components/EmptyCard"; import EmptyCard from "@components/EmptyCard";
import NotFoundPage from "@components/NotFoundPage"; import NotFoundPage from "@components/NotFoundPage";
@ -33,7 +34,7 @@ const SignupRoute = lazy(() => import("@routes/signup"));
const LoginRoute = lazy(() => import("@routes/login")); const LoginRoute = lazy(() => import("@routes/login"));
const DevicesAlreadyAdopted = lazy(() => import("@routes/devices.already-adopted")); const DevicesAlreadyAdopted = lazy(() => import("@routes/devices.already-adopted"));
const OtherSessionRoute = lazy(() => import("@routes/devices.$id.other-session")); const OtherSessionRoute = lazy(() => import("@routes/devices.$id.other-session"));
const MountRoute = lazy(() => import("./routes/devices.$id.mount")); const MountRoute = lazy(() => import("@routes/devices.$id.mount"));
const SettingsRoute = lazy(() => import("@routes/devices.$id.settings")); const SettingsRoute = lazy(() => import("@routes/devices.$id.settings"));
const SettingsMouseRoute = lazy(() => import("@routes/devices.$id.settings.mouse")); const SettingsMouseRoute = lazy(() => import("@routes/devices.$id.settings.mouse"));
const SettingsKeyboardRoute = lazy(() => import("@routes/devices.$id.settings.keyboard")); const SettingsKeyboardRoute = lazy(() => import("@routes/devices.$id.settings.keyboard"));
@ -116,7 +117,7 @@ if (isOnDevice) {
path: "/", path: "/",
errorElement: <ErrorBoundary />, errorElement: <ErrorBoundary />,
element: <DeviceRoute />, element: <DeviceRoute />,
HydrateFallback: () => <div className="p-4">Loading...</div>, HydrateFallback: () => <div className="p-4">{m.loading()}</div>,
loader: DeviceRoute.loader, loader: DeviceRoute.loader,
children: [ children: [
{ {
@ -390,22 +391,46 @@ document.addEventListener("DOMContentLoaded", () => {
// eslint-disable-next-line react-refresh/only-export-components // eslint-disable-next-line react-refresh/only-export-components
function ErrorBoundary() { function ErrorBoundary() {
const error = useRouteError(); const error = useRouteError();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
const errorMessage = error?.data?.error?.message || error?.message;
if (isRouteErrorResponse(error)) { if (isRouteErrorResponse(error)) {
if (error.status === 404) return <NotFoundPage />; if (error.status === 404) return <NotFoundPage />;
} }
const getErrorMessage = (err: unknown): string | null => {
// If it's a route error response, try to read a string at err.data.error.message or err.data.error safely
if (isRouteErrorResponse(err)) {
const data = (err as { data?: unknown }).data;
if (data && typeof data === "object") {
const maybeError = (data as Record<string, unknown>)["error"];
if (maybeError) {
if (typeof maybeError === "object") {
const msg = (maybeError as Record<string, unknown>)["message"];
if (typeof msg === "string") return msg;
} else if (typeof maybeError === "string") {
return maybeError;
}
}
}
}
// Fallback: check plain object message property
if (err && typeof err === "object") {
const maybeMsg = (err as Record<string, unknown>)["message"];
if (typeof maybeMsg === "string") return maybeMsg;
}
return null;
};
const errorMessage = getErrorMessage(error);
return ( return (
<div className="h-full w-full"> <div className="h-full w-full">
<div className="flex h-full items-center justify-center"> <div className="flex h-full items-center justify-center">
<div className="w-full max-w-2xl"> <div className="w-full max-w-2xl">
<EmptyCard <EmptyCard
IconElm={ExclamationTriangleIcon} IconElm={ExclamationTriangleIcon}
headline="Oh no!" headline={m.oh_no()}
description="Something went wrong. Please try again later or contact support" description={m.something_went_wrong()}
BtnElm={ BtnElm={
errorMessage && ( errorMessage && (
<Card> <Card>

View File

@ -1,9 +1,8 @@
import toast, { Toast, Toaster, useToasterStore } from "react-hot-toast";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import toast, { Toast, Toaster, useToasterStore } from "react-hot-toast";
import { CheckCircleIcon, XCircleIcon } from "@heroicons/react/20/solid"; import { CheckCircleIcon, XCircleIcon } from "@heroicons/react/20/solid";
import Card from "@/components/Card"; import Card from "@components/Card";
interface NotificationOptions { interface NotificationOptions {
duration?: number; duration?: number;
@ -20,8 +19,7 @@ const ToastContent = ({
t: Toast; t: Toast;
}) => ( }) => (
<Card <Card
className={`${ className={`${t.visible ? "animate-enter" : "animate-leave"
t.visible ? "animate-enter" : "animate-leave"
} pointer-events-auto z-30 w-full max-w-sm shadow-xl!`} } pointer-events-auto z-30 w-full max-w-sm shadow-xl!`}
> >
<div className="flex items-center gap-x-2 p-2.5 px-2"> <div className="flex items-center gap-x-2 p-2.5 px-2">
@ -34,7 +32,7 @@ const ToastContent = ({
const notifications = { const notifications = {
success: (message: string, options?: NotificationOptions) => { success: (message: string, options?: NotificationOptions) => {
return toast.custom( return toast.custom(
t => ( (t: Toast) => (
<ToastContent <ToastContent
icon={<CheckCircleIcon className="w-5 h-5 text-green-500 dark:text-green-400" />} icon={<CheckCircleIcon className="w-5 h-5 text-green-500 dark:text-green-400" />}
message={message} message={message}
@ -47,7 +45,7 @@ const notifications = {
error: (message: string, options?: NotificationOptions) => { error: (message: string, options?: NotificationOptions) => {
return toast.custom( return toast.custom(
t => ( (t: Toast) => (
<ToastContent <ToastContent
icon={<XCircleIcon className="w-5 h-5 text-red-500 dark:text-red-400" />} icon={<XCircleIcon className="w-5 h-5 text-red-500 dark:text-red-400" />}
message={message} message={message}
@ -64,9 +62,9 @@ function useMaxToasts(max: number) {
useEffect(() => { useEffect(() => {
toasts toasts
.filter(t => t.visible) // Only consider visible toasts .filter((t: Toast) => t.visible) // Only consider visible toasts
.filter((_, i) => i >= max) // Is toast index over limit? .filter((_: Toast, i: number) => i >= max) // Is toast index over limit?
.forEach(t => toast.dismiss(t.id)); // Dismiss Use toast.remove(t.id) for no exit animation .forEach((t: Toast) => toast.dismiss(t.id)); // Dismiss Use toast.remove(t.id) for no exit animation
}, [toasts, max]); }, [toasts, max]);
} }

View File

@ -2,14 +2,15 @@ import { Form, redirect, useActionData, useLoaderData } from "react-router";
import type { ActionFunction, ActionFunctionArgs, LoaderFunction, LoaderFunctionArgs } from "react-router"; import type { ActionFunction, ActionFunctionArgs, LoaderFunction, LoaderFunctionArgs } from "react-router";
import { ChevronLeftIcon } from "@heroicons/react/16/solid"; import { ChevronLeftIcon } from "@heroicons/react/16/solid";
import { User } from "@hooks/stores";
import { Button, LinkButton } from "@components/Button"; import { Button, LinkButton } from "@components/Button";
import Card from "@components/Card"; import Card from "@components/Card";
import { CardHeader } from "@components/CardHeader"; import { CardHeader } from "@components/CardHeader";
import DashboardNavbar from "@components/Header"; import DashboardNavbar from "@components/Header";
import { User } from "@/hooks/stores";
import { checkAuth } from "@/main";
import Fieldset from "@components/Fieldset"; import Fieldset from "@components/Fieldset";
import { checkAuth } from "@/main";
import { CLOUD_API } from "@/ui.config"; import { CLOUD_API } from "@/ui.config";
import { m } from "@localizations/messages.js";
interface LoaderData { interface LoaderData {
device: { id: string; name: string; user: { googleId: string } }; device: { id: string; name: string; user: { googleId: string } };
@ -28,11 +29,12 @@ const action: ActionFunction = async ({ request }: ActionFunctionArgs) => {
}); });
if (!res.ok) { 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) { } catch (e) {
console.error(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"); return redirect("/devices");
@ -68,7 +70,7 @@ export default function DevicesIdDeregister() {
<div className="grid min-h-screen grid-rows-(--grid-layout)"> <div className="grid min-h-screen grid-rows-(--grid-layout)">
<DashboardNavbar <DashboardNavbar
isLoggedIn={!!user} isLoggedIn={!!user}
primaryLinks={[{ title: "Cloud Devices", to: "/devices" }]} primaryLinks={[{ title: m.deregister_cloud_devices(), to: "/devices" }]}
userEmail={user?.email} userEmail={user?.email}
picture={user?.picture} picture={user?.picture}
kvmName={device?.name} kvmName={device?.name}
@ -82,21 +84,14 @@ export default function DevicesIdDeregister() {
size="SM" size="SM"
theme="blank" theme="blank"
LeadingIcon={ChevronLeftIcon} LeadingIcon={ChevronLeftIcon}
text="Back to Devices" text={m.back_to_devices()}
to="/devices" to="/devices"
/> />
<Card className="max-w-3xl p-6"> <Card className="max-w-3xl p-6">
<div className="max-w-xl space-y-4"> <div className="max-w-xl space-y-4">
<CardHeader <CardHeader
headline={`Deregister ${device.name || device.id} from your cloud account`} headline={m.deregister_headline({ device: device.name || device.id })}
description={ description={m.deregister_description()}
<>
This will remove the device from your cloud account and revoke
remote access to it.
<br />
Please note that local access will still be possible
</>
}
/> />
<Fieldset> <Fieldset>
@ -107,20 +102,20 @@ export default function DevicesIdDeregister() {
size="MD" size="MD"
theme="light" theme="light"
to="/devices" to="/devices"
text="Cancel" text={m.cancel()}
textAlign="center" textAlign="center"
/> />
<Button <Button
size="MD" size="MD"
theme="danger" theme="danger"
type="submit" type="submit"
text="Deregister from Cloud" text={m.deregister_from_cloud()}
textAlign="center" textAlign="center"
/> />
</div> </div>
{error?.message && ( {error?.message && (
<p className="text-sm text-red-500 dark:text-red-400"> <p className="text-sm text-red-500 dark:text-red-400">
{error?.message} {m.deregister_error({ status: error.message })}
</p> </p>
)} )}
</Form> </Form>

View File

@ -1,4 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useNavigate } from "react-router";
import { import {
LuLink, LuLink,
LuRadioReceiver, LuRadioReceiver,
@ -7,28 +8,28 @@ import {
} from "react-icons/lu"; } from "react-icons/lu";
import { PlusCircleIcon, ExclamationTriangleIcon } from "@heroicons/react/20/solid"; import { PlusCircleIcon, ExclamationTriangleIcon } from "@heroicons/react/20/solid";
import { TrashIcon } from "@heroicons/react/16/solid"; import { TrashIcon } from "@heroicons/react/16/solid";
import { useNavigate } from "react-router";
import Card, { GridCard } from "@/components/Card"; import DebianIcon from "@assets/debian-icon.png";
import { Button } from "@components/Button"; import UbuntuIcon from "@assets/ubuntu-icon.png";
import LogoBlueIcon from "@/assets/logo-blue.svg"; import FedoraIcon from "@assets/fedora-icon.png";
import LogoWhiteIcon from "@/assets/logo-white.svg"; import OpenSUSEIcon from "@assets/opensuse-icon.png";
import { formatters } from "@/utils"; import ArchIcon from "@assets/arch-icon.png";
import NetBootIcon from "@assets/netboot-icon.svg";
import LogoBlueIcon from "@assets/logo-blue.svg";
import LogoWhiteIcon from "@assets/logo-white.svg";
import { cx } from "@/cva.config";
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import AutoHeight from "@components/AutoHeight"; import AutoHeight from "@components/AutoHeight";
import { Button } from "@components/Button";
import Card, { GridCard } from "@components/Card";
import Fieldset from "@components/Fieldset";
import { InputFieldWithLabel } from "@/components/InputField"; import { InputFieldWithLabel } from "@/components/InputField";
import DebianIcon from "@/assets/debian-icon.png"; import { formatters } from "@/utils";
import UbuntuIcon from "@/assets/ubuntu-icon.png";
import FedoraIcon from "@/assets/fedora-icon.png";
import OpenSUSEIcon from "@/assets/opensuse-icon.png";
import ArchIcon from "@/assets/arch-icon.png";
import NetBootIcon from "@/assets/netboot-icon.svg";
import Fieldset from "@/components/Fieldset";
import { DEVICE_API } from "@/ui.config"; import { DEVICE_API } from "@/ui.config";
import { isOnDevice } from "@/main";
import notifications from "@/notifications";
import { m } from "@localizations/messages.js";
import { JsonRpcResponse, useJsonRpc } from "../hooks/useJsonRpc";
import notifications from "../notifications";
import { isOnDevice } from "../main";
import { cx } from "../cva.config";
import { import {
MountMediaState, MountMediaState,
RemoteVirtualMediaState, RemoteVirtualMediaState,
@ -38,13 +39,10 @@ import {
export default function MountRoute() { export default function MountRoute() {
const navigate = useNavigate(); 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("..")} />; return <Dialog onClose={() => navigate("..")} />;
} }
export function Dialog({ onClose }: { onClose: () => void }) { export function Dialog({ onClose }: Readonly<{ onClose: () => void }>) {
const { const {
modalView, modalView,
setModalView, setModalView,
@ -145,12 +143,12 @@ export function Dialog({ onClose }: { onClose: () => void }) {
<div className="flex flex-col items-start justify-start space-y-4 text-left"> <div className="flex flex-col items-start justify-start space-y-4 text-left">
<img <img
src={LogoBlueIcon} src={LogoBlueIcon}
alt="JetKVM Logo" alt={m.jetkvm_logo()}
className="block h-[24px] dark:hidden" className="block h-[24px] dark:hidden"
/> />
<img <img
src={LogoWhiteIcon} src={LogoWhiteIcon}
alt="JetKVM Logo" alt={m.jetkvm_logo()}
className="hidden h-[24px] dark:mt-0! dark:block" className="hidden h-[24px] dark:mt-0! dark:block"
/> />
{modalView === "mode" && ( {modalView === "mode" && (
@ -238,26 +236,26 @@ function ModeSelectionView({
<div className="w-full space-y-4"> <div className="w-full space-y-4">
<div className="animate-fadeIn space-y-0 opacity-0"> <div className="animate-fadeIn space-y-0 opacity-0">
<h2 className="text-lg leading-tight font-bold dark:text-white"> <h2 className="text-lg leading-tight font-bold dark:text-white">
Virtual Media Source {m.mount_virtual_media_source()}
</h2> </h2>
<div className="text-sm leading-snug text-slate-600 dark:text-slate-400"> <div className="text-sm leading-snug text-slate-600 dark:text-slate-400">
Choose how you want to mount your virtual media {m.mount_virtual_media_source_description()}
</div> </div>
</div> </div>
<div className="grid gap-4 md:grid-cols-3"> <div className="grid gap-4 md:grid-cols-3">
{[ {[
{ {
label: "URL Mount", label: m.mount_url_mount(),
value: "url", value: "url",
description: "Mount files from any public web address", description: m.mount_url_description(),
icon: LuLink, icon: LuLink,
tag: "Experimental", tag: m.experimental(),
disabled: false, disabled: false,
}, },
{ {
label: "JetKVM Storage Mount", label: m.mount_jetkvm_storage(),
value: "device", value: "device",
description: "Mount previously uploaded files from the JetKVM storage", description: m.mount_jetkvm_storage_description(),
icon: LuRadioReceiver, icon: LuRadioReceiver,
tag: null, tag: null,
disabled: false, disabled: false,
@ -332,7 +330,7 @@ function ModeSelectionView({
onClick={() => { onClick={() => {
setModalView(selectedMode); setModalView(selectedMode);
}} }}
text="Continue" text={m.continue()}
/> />
</div> </div>
</div> </div>
@ -351,6 +349,7 @@ function UrlView({
}) { }) {
const [usbMode, setUsbMode] = useState<RemoteVirtualMediaState["mode"]>("CDROM"); const [usbMode, setUsbMode] = useState<RemoteVirtualMediaState["mode"]>("CDROM");
const [url, setUrl] = useState<string>(""); const [url, setUrl] = useState<string>("");
const [isUrlValid, setIsUrlValid] = useState(false);
const popularImages = [ const popularImages = [
{ {
@ -398,6 +397,12 @@ function UrlView({
const urlRef = useRef<HTMLInputElement>(null); const urlRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (urlRef.current) {
setIsUrlValid(urlRef.current.validity.valid);
}
}, [url]);
function handleUrlChange(url: string) { function handleUrlChange(url: string) {
setUrl(url); setUrl(url);
if (url.endsWith(".iso")) { if (url.endsWith(".iso")) {
@ -410,8 +415,8 @@ function UrlView({
return ( return (
<div className="w-full space-y-4"> <div className="w-full space-y-4">
<ViewHeader <ViewHeader
title="Mount from URL" title={m.mount_view_url_title()}
description="Enter an URL to the image file to mount" description={m.mount_view_url_description()}
/> />
<div <div
@ -423,7 +428,7 @@ function UrlView({
<InputFieldWithLabel <InputFieldWithLabel
placeholder="https://example.com/image.iso" placeholder="https://example.com/image.iso"
type="url" type="url"
label="Image URL" label={m.mount_url_input_label()}
ref={urlRef} ref={urlRef}
value={url} value={url}
onChange={e => handleUrlChange(e.target.value)} onChange={e => handleUrlChange(e.target.value)}
@ -436,19 +441,19 @@ function UrlView({
animationDelay: "0.1s", animationDelay: "0.1s",
}} }}
> >
<Fieldset disabled={!urlRef.current?.validity.valid || url.length === 0}> <Fieldset disabled={!isUrlValid || url.length === 0}>
<UsbModeSelector usbMode={usbMode} setUsbMode={setUsbMode} /> <UsbModeSelector usbMode={usbMode} setUsbMode={setUsbMode} />
</Fieldset> </Fieldset>
<div className="flex space-x-2"> <div className="flex space-x-2">
<Button size="MD" theme="blank" text="Back" onClick={onBack} /> <Button size="MD" theme="blank" text={m.back()} onClick={onBack} />
<Button <Button
size="MD" size="MD"
theme="primary" theme="primary"
loading={mountInProgress} loading={mountInProgress}
text="Mount URL" text={m.mount_button_mount_url()}
onClick={() => onMount(url, usbMode)} onClick={() => onMount(url, usbMode)}
disabled={ disabled={
mountInProgress || !urlRef.current?.validity.valid || url.length === 0 mountInProgress || !isUrlValid || url.length === 0
} }
/> />
</div> </div>
@ -463,7 +468,7 @@ function UrlView({
}} }}
> >
<h2 className="mb-2 text-sm font-semibold text-black dark:text-white"> <h2 className="mb-2 text-sm font-semibold text-black dark:text-white">
Popular images {m.mount_popular_images()}
</h2> </h2>
<Card className="w-full divide-y divide-slate-800/20 dark:divide-slate-300/20"> <Card className="w-full divide-y divide-slate-800/20 dark:divide-slate-300/20">
{popularImages.map((image, index) => ( {popularImages.map((image, index) => (
@ -487,7 +492,7 @@ function UrlView({
<Button <Button
size="XS" size="XS"
theme="light" theme="light"
text="Select" text={m.mount_button_select()}
onClick={() => handleUrlChange(image.url)} onClick={() => handleUrlChange(image.url)}
/> />
</div> </div>
@ -553,7 +558,7 @@ function DeviceFileView({
const syncStorage = useCallback(() => { const syncStorage = useCallback(() => {
send("listStorageFiles", {}, (resp: JsonRpcResponse) => { send("listStorageFiles", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error(`Error listing storage files: ${resp.error}`); notifications.error(m.mount_error_list_storage({ error: resp.error }));
return; return;
} }
const { files } = resp.result as StorageFiles; const { files } = resp.result as StorageFiles;
@ -568,7 +573,7 @@ function DeviceFileView({
send("getStorageSpace", {}, (resp: JsonRpcResponse) => { send("getStorageSpace", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error(`Error getting storage space: ${resp.error}`); notifications.error(m.mount_error_get_storage_space({ error: resp.error }));
return; return;
} }
@ -597,7 +602,7 @@ function DeviceFileView({
console.log("Deleting file:", file); console.log("Deleting file:", file);
send("deleteStorageFile", { filename: file.name }, (resp: JsonRpcResponse) => { send("deleteStorageFile", { filename: file.name }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error(`Error deleting file: ${resp.error}`); notifications.error(m.mount_error_delete_file({ error: resp.error }));
return; return;
} }
@ -630,8 +635,8 @@ function DeviceFileView({
return ( return (
<div className="w-full space-y-4"> <div className="w-full space-y-4">
<ViewHeader <ViewHeader
title="Mount from JetKVM Storage" title={m.mount_view_device_title()}
description="Select an image to mount from the JetKVM storage" description={m.mount_view_device_description()}
/> />
<div <div
className="w-full animate-fadeIn opacity-0" className="w-full animate-fadeIn opacity-0"
@ -647,17 +652,17 @@ function DeviceFileView({
<div className="space-y-1"> <div className="space-y-1">
<PlusCircleIcon className="mx-auto h-6 w-6 text-blue-700 dark:text-blue-500" /> <PlusCircleIcon className="mx-auto h-6 w-6 text-blue-700 dark:text-blue-500" />
<h3 className="text-sm leading-none font-semibold text-black dark:text-white"> <h3 className="text-sm leading-none font-semibold text-black dark:text-white">
No images available {m.mount_no_images_title()}
</h3> </h3>
<p className="text-xs leading-none text-slate-700 dark:text-slate-300"> <p className="text-xs leading-none text-slate-700 dark:text-slate-300">
Upload an image to start virtual media mounting. {m.mount_no_images_description()}
</p> </p>
</div> </div>
<div> <div>
<Button <Button
size="SM" size="SM"
theme="primary" theme="primary"
text="Upload a new image" text={m.mount_upload_title()}
onClick={() => onNewImageClick()} onClick={() => onNewImageClick()}
/> />
</div> </div>
@ -677,9 +682,7 @@ function DeviceFileView({
const selectedFile = onStorageFiles.find(f => f.name === file.name); const selectedFile = onStorageFiles.find(f => f.name === file.name);
if (!selectedFile) return; if (!selectedFile) return;
if ( if (
window.confirm( window.confirm(m.mount_confirm_delete({ name: selectedFile.name }))
"Are you sure you want to delete " + selectedFile.name + "?",
)
) { ) {
handleDeleteFile(selectedFile); handleDeleteFile(selectedFile);
} }
@ -692,24 +695,24 @@ function DeviceFileView({
{onStorageFiles.length > filesPerPage && ( {onStorageFiles.length > filesPerPage && (
<div className="flex items-center justify-between px-3 py-2"> <div className="flex items-center justify-between px-3 py-2">
<p className="text-sm text-slate-700 dark:text-slate-300"> <p className="text-sm text-slate-700 dark:text-slate-300">
Showing <span className="font-bold">{indexOfFirstFile + 1}</span> to{" "} {m.mount_button_showing_results({
<span className="font-bold"> from: indexOfFirstFile + 1,
{Math.min(indexOfLastFile, onStorageFiles.length)} to: Math.min(indexOfLastFile, onStorageFiles.length),
</span>{" "} total: onStorageFiles.length
of <span className="font-bold">{onStorageFiles.length}</span> results })}
</p> </p>
<div className="flex items-center gap-x-2"> <div className="flex items-center gap-x-2">
<Button <Button
size="XS" size="XS"
theme="light" theme="light"
text="Previous" text={m.previous()}
onClick={handlePreviousPage} onClick={handlePreviousPage}
disabled={currentPage === 1} disabled={currentPage === 1}
/> />
<Button <Button
size="XS" size="XS"
theme="light" theme="light"
text="Next" text={m.next()}
onClick={handleNextPage} onClick={handleNextPage}
disabled={currentPage === totalPages} disabled={currentPage === totalPages}
/> />
@ -738,7 +741,7 @@ function DeviceFileView({
size="MD" size="MD"
disabled={selected === null || mountInProgress} disabled={selected === null || mountInProgress}
theme="primary" theme="primary"
text="Mount File" text={m.mount_button_mount_file()}
loading={mountInProgress} loading={mountInProgress}
onClick={() => onClick={() =>
onMountStorageFile( onMountStorageFile(
@ -772,10 +775,10 @@ function DeviceFileView({
> >
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="font-medium text-black dark:text-white"> <span className="font-medium text-black dark:text-white">
Available Storage {m.mount_available_storage()}
</span> </span>
<span className="text-slate-700 dark:text-slate-300"> <span className="text-slate-700 dark:text-slate-300">
{percentageUsed}% used {m.mount_percentage_used({ percentageUsed })}
</span> </span>
</div> </div>
<div className="h-3.5 w-full overflow-hidden rounded-xs bg-slate-200 dark:bg-slate-700"> <div className="h-3.5 w-full overflow-hidden rounded-xs bg-slate-200 dark:bg-slate-700">
@ -786,10 +789,10 @@ function DeviceFileView({
</div> </div>
<div className="flex justify-between text-sm text-slate-600"> <div className="flex justify-between text-sm text-slate-600">
<span className="text-slate-700 dark:text-slate-300"> <span className="text-slate-700 dark:text-slate-300">
{formatters.bytes(bytesUsed)} used {m.mount_bytes_used({ bytesUsed: formatters.bytes(bytesUsed) })}
</span> </span>
<span className="text-slate-700 dark:text-slate-300"> <span className="text-slate-700 dark:text-slate-300">
{formatters.bytes(bytesFree)} free {m.mount_bytes_free({ bytesFree: formatters.bytes(bytesFree) })}
</span> </span>
</div> </div>
</div> </div>
@ -806,7 +809,7 @@ function DeviceFileView({
size="MD" size="MD"
theme="light" theme="light"
fullWidth fullWidth
text="Upload a new image" text={m.mount_button_upload_new_image()}
onClick={() => onNewImageClick()} onClick={() => onNewImageClick()}
/> />
</div> </div>
@ -862,7 +865,7 @@ function UploadFileView({
if (!rtcDataChannel) { if (!rtcDataChannel) {
console.error("Failed to create data channel for file upload"); console.error("Failed to create data channel for file upload");
notifications.error("Failed to create data channel for file upload"); notifications.error(m.mount_upload_failed_datachannel());
setUploadState("idle"); setUploadState("idle");
console.log("Upload state set to 'idle'"); console.log("Upload state set to 'idle'");
@ -952,7 +955,7 @@ function UploadFileView({
rtcDataChannel.onerror = error => { rtcDataChannel.onerror = error => {
console.error("RTC Data channel error:", error); console.error("RTC Data channel error:", error);
notifications.error(`Upload failed: ${error}`); notifications.error(m.mount_upload_failed_rtc({ error: error }));
setUploadState("idle"); setUploadState("idle");
console.log("Upload state set to 'idle'"); console.log("Upload state set to 'idle'");
}; };
@ -1037,7 +1040,7 @@ function UploadFileView({
file.name !== incompleteFileName.replace(".incomplete", "") file.name !== incompleteFileName.replace(".incomplete", "")
) { ) {
setFileError( setFileError(
`Please select the file "${incompleteFileName.replace(".incomplete", "")}" to continue the upload.`, m.mount_please_select_file({ name: incompleteFileName.replace(".incomplete", "") }),
); );
return; return;
} }
@ -1080,11 +1083,11 @@ function UploadFileView({
return ( return (
<div className="w-full space-y-4"> <div className="w-full space-y-4">
<ViewHeader <ViewHeader
title="Upload New Image" title={m.mount_upload_title()}
description={ description={
incompleteFileName incompleteFileName
? `Continue uploading "${incompleteFileName}"` ? m.mount_continue_uploading_with_name({ name: incompleteFileName.replace(".incomplete", "") })
: "Select an image file to upload to JetKVM storage" : m.mount_upload_description()
} }
/> />
<div <div
@ -1121,11 +1124,11 @@ function UploadFileView({
</div> </div>
<h3 className="text-sm leading-none font-semibold text-black dark:text-white"> <h3 className="text-sm leading-none font-semibold text-black dark:text-white">
{incompleteFileName {incompleteFileName
? `Click to select "${incompleteFileName.replace(".incomplete", "")}"` ? m.mount_click_to_select_incomplete({ name: incompleteFileName.replace(".incomplete", "") })
: "Click to select a file"} : m.mount_click_to_select_file()}
</h3> </h3>
<p className="text-xs leading-none text-slate-700 dark:text-slate-300"> <p className="text-xs leading-none text-slate-700 dark:text-slate-300">
Supported formats: ISO, IMG {m.mount_supported_formats()}
</p> </p>
</div> </div>
)} )}
@ -1140,7 +1143,7 @@ function UploadFileView({
</Card> </Card>
</div> </div>
<h3 className="leading-non text-lg font-semibold text-black dark:text-white"> <h3 className="leading-non text-lg font-semibold text-black dark:text-white">
Uploading {formatters.truncateMiddle(uploadedFileName, 30)} {m.mount_uploading_with_name({ name: formatters.truncateMiddle(uploadedFileName, 30) })}
</h3> </h3>
<p className="text-xs leading-none text-slate-700 dark:text-slate-300"> <p className="text-xs leading-none text-slate-700 dark:text-slate-300">
{formatters.bytes(uploadedFileSize || 0)} {formatters.bytes(uploadedFileSize || 0)}
@ -1153,11 +1156,11 @@ function UploadFileView({
></div> ></div>
</div> </div>
<div className="flex justify-between text-xs text-slate-600 dark:text-slate-400"> <div className="flex justify-between text-xs text-slate-600 dark:text-slate-400">
<span>Uploading...</span> <span>{m.mount_uploading()}</span>{" "}
<span> <span>
{uploadSpeed !== null {uploadSpeed !== null
? `${formatters.bytes(uploadSpeed)}/s` ? `${formatters.bytes(uploadSpeed)}/s`
: "Calculating..."} : m.mount_calculating()}
</span> </span>
</div> </div>
</div> </div>
@ -1174,11 +1177,10 @@ function UploadFileView({
</Card> </Card>
</div> </div>
<h3 className="text-sm leading-none font-semibold text-black dark:text-white"> <h3 className="text-sm leading-none font-semibold text-black dark:text-white">
Upload successful {m.mount_upload_successful()}
</h3> </h3>
<p className="text-xs leading-none text-slate-700 dark:text-slate-300"> <p className="text-xs leading-none text-slate-700 dark:text-slate-300">
{formatters.truncateMiddle(uploadedFileName, 40)} has been {m.mount_uploaded_has_been_uploaded({ name: formatters.truncateMiddle(uploadedFileName, 40) })}
uploaded
</p> </p>
</div> </div>
)} )}
@ -1205,7 +1207,7 @@ function UploadFileView({
className="mt-2 animate-fadeIn truncate text-sm text-red-600 dark:text-red-400 opacity-0" className="mt-2 animate-fadeIn truncate text-sm text-red-600 dark:text-red-400 opacity-0"
style={{ animationDuration: "0.7s" }} style={{ animationDuration: "0.7s" }}
> >
Error: {uploadError} {m.mount_upload_error({ error: String(uploadError) })}
</div> </div>
)} )}
@ -1221,7 +1223,7 @@ function UploadFileView({
<Button <Button
size="MD" size="MD"
theme="light" theme="light"
text="Cancel Upload" text={m.mount_button_cancel_upload()}
onClick={() => { onClick={() => {
onCancelUpload(); onCancelUpload();
setUploadState("idle"); setUploadState("idle");
@ -1235,7 +1237,7 @@ function UploadFileView({
<Button <Button
size="MD" size="MD"
theme={uploadState === "success" ? "primary" : "light"} theme={uploadState === "success" ? "primary" : "light"}
text="Back to Overview" text={m.mount_button_back_to_overview()}
onClick={onBack} onClick={onBack}
/> />
)} )}
@ -1259,10 +1261,10 @@ function ErrorView({
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center space-x-2 text-red-600"> <div className="flex items-center space-x-2 text-red-600">
<ExclamationTriangleIcon className="h-6 w-6" /> <ExclamationTriangleIcon className="h-6 w-6" />
<h2 className="text-lg leading-tight font-bold">Mount Error</h2> <h2 className="text-lg leading-tight font-bold">{m.mount_error_title()}</h2>
</div> </div>
<p className="text-sm leading-snug text-slate-600"> <p className="text-sm leading-snug text-slate-600">
An error occurred while attempting to mount the media. Please try again. {m.mount_error_description()}
</p> </p>
</div> </div>
{errorMessage && ( {errorMessage && (
@ -1271,8 +1273,8 @@ function ErrorView({
</Card> </Card>
)} )}
<div className="flex justify-end space-x-2"> <div className="flex justify-end space-x-2">
<Button size="SM" theme="light" text="Close" onClick={onClose} /> <Button size="SM" theme="light" text={m.close()} onClick={onClose} />
<Button size="SM" theme="primary" text="Back to Overview" onClick={onRetry} /> <Button size="SM" theme="primary" text={m.mount_button_back_to_overview()} onClick={onRetry} />
</div> </div>
</div> </div>
); );
@ -1341,7 +1343,7 @@ function PreUploadedImageItem({
size="XS" size="XS"
theme="light" theme="light"
LeadingIcon={TrashIcon} LeadingIcon={TrashIcon}
text="Delete" text={m.delete()}
onClick={e => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
onDelete(); onDelete();
@ -1362,7 +1364,7 @@ function PreUploadedImageItem({
<Button <Button
size="XS" size="XS"
theme="light" theme="light"
text="Continue uploading" text={m.mount_button_continue_upload()}
onClick={e => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
onContinueUpload(); onContinueUpload();
@ -1408,7 +1410,7 @@ function UsbModeSelector({
className="form-radio h-3 w-3 rounded-full border-slate-800/30 bg-white text-blue-700 transition-opacity focus:ring-blue-500 disabled:opacity-30 dark:bg-slate-800" className="form-radio h-3 w-3 rounded-full border-slate-800/30 bg-white text-blue-700 transition-opacity focus:ring-blue-500 disabled:opacity-30 dark:bg-slate-800"
/> />
<span className="ml-2 text-sm font-medium text-slate-900 dark:text-white"> <span className="ml-2 text-sm font-medium text-slate-900 dark:text-white">
CD/DVD {m.mount_mode_cdrom()}
</span> </span>
</label> </label>
<label htmlFor="disk" className="flex items-center"> <label htmlFor="disk" className="flex items-center">
@ -1421,7 +1423,7 @@ function UsbModeSelector({
className="form-radio h-3 w-3 rounded-full border-slate-800/30 bg-white text-blue-700 transition-opacity focus:ring-blue-500 disabled:opacity-30 dark:bg-slate-800" className="form-radio h-3 w-3 rounded-full border-slate-800/30 bg-white text-blue-700 transition-opacity focus:ring-blue-500 disabled:opacity-30 dark:bg-slate-800"
/> />
<span className="ml-2 text-sm font-medium text-slate-900 dark:text-white"> <span className="ml-2 text-sm font-medium text-slate-900 dark:text-white">
Disk {m.mount_mode_disk()}
</span> </span>
</label> </label>
</div> </div>

View File

@ -1,9 +1,10 @@
import { useNavigate, useOutletContext } from "react-router"; import { useNavigate, useOutletContext } from "react-router";
import { GridCard } from "@/components/Card";
import { Button } from "@components/Button"; import { Button } from "@components/Button";
import LogoBlue from "@/assets/logo-blue.svg"; import { GridCard } from "@components/Card";
import LogoWhite from "@/assets/logo-white.svg"; import LogoBlue from "@assets/logo-blue.svg";
import LogoWhite from "@assets/logo-white.svg";
import { m } from "@localizations/messages";
interface ContextType { interface ContextType {
setupPeerConnection: () => Promise<void>; setupPeerConnection: () => Promise<void>;
@ -30,14 +31,13 @@ export default function OtherSessionRoute() {
<div className="text-left"> <div className="text-left">
<p className="text-base font-semibold dark:text-white"> <p className="text-base font-semibold dark:text-white">
Another Active Session Detected {m.other_session_detected()}
</p> </p>
<p className="mb-4 text-sm text-slate-600 dark:text-slate-400"> <p className="mb-4 text-sm text-slate-600 dark:text-slate-400">
Only one active session is supported at a time. Would you like to take over {m.other_session_take_over()}
this session?
</p> </p>
<div className="flex items-center justify-start space-x-4"> <div className="flex items-center justify-start space-x-4">
<Button size="SM" theme="primary" text="Use Here" onClick={handleClose} /> <Button size="SM" theme="primary" text={m.other_session_use_here_button()} onClick={handleClose} />
</div> </div>
</div> </div>
</div> </div>

View File

@ -5,26 +5,20 @@ import { ChevronLeftIcon } from "@heroicons/react/16/solid";
import { Button, LinkButton } from "@components/Button"; import { Button, LinkButton } from "@components/Button";
import Card from "@components/Card"; import Card from "@components/Card";
import { CardHeader } from "@components/CardHeader"; import { CardHeader } from "@components/CardHeader";
import { InputFieldWithLabel } from "@components/InputField";
import DashboardNavbar from "@components/Header"; import DashboardNavbar from "@components/Header";
import { User } from "@/hooks/stores";
import { checkAuth } from "@/main";
import Fieldset from "@components/Fieldset"; import Fieldset from "@components/Fieldset";
import { InputFieldWithLabel } from "@components/InputField";
import { checkAuth } from "@/main";
import { CLOUD_API } from "@/ui.config"; import { CLOUD_API } from "@/ui.config";
import api from "@/api";
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 action: ActionFunction = async ({ params, request }: ActionFunctionArgs) => {
const { id } = params; const { id } = params;
const { name } = Object.fromEntries(await request.formData()); const { name } = Object.fromEntries(await request.formData());
if (!name || name === "") { if (!name || name === "") {
return { message: "Please specify a name" }; return { message: m.rename_device_no_name() };
} }
try { try {
@ -32,11 +26,11 @@ const action: ActionFunction = async ({ params, request }: ActionFunctionArgs) =
name, name,
}); });
if (!res.ok) { if (!res.ok) {
return { message: "There was an error renaming your device. Please try again." }; return { message: m.rename_device_error({ error: res.statusText }) };
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);
return { message: "There was an error renaming your device. Please try again." }; return { message: m.rename_device_error({ error: String(e) }) };
} }
return redirect("/devices"); return redirect("/devices");
@ -65,7 +59,7 @@ const loader: LoaderFunction = async ({ params }: LoaderFunctionArgs) => {
}; };
export default function DeviceIdRename() { export default function DeviceIdRename() {
const { device, user } = useLoaderData() as LoaderData; const { device, user } = useLoaderData();
const error = useActionData() as { message: string }; const error = useActionData() as { message: string };
return ( return (
@ -86,24 +80,24 @@ export default function DeviceIdRename() {
size="SM" size="SM"
theme="blank" theme="blank"
LeadingIcon={ChevronLeftIcon} LeadingIcon={ChevronLeftIcon}
text="Back to Devices" text={m.back_to_devices()}
to="/devices" to="/devices"
/> />
<Card className="max-w-3xl p-6"> <Card className="max-w-3xl p-6">
<div className="space-y-4"> <div className="space-y-4">
<CardHeader <CardHeader
headline={`Rename ${device.name || device.id}`} headline={m.rename_device_headline({ name: device.name || device.id })}
description="Properly name your device to easily identify it." description={m.rename_device_description()}
/> />
<Fieldset> <Fieldset>
<Form method="POST" className="max-w-sm space-y-4"> <Form method="POST" className="max-w-sm space-y-4">
<div className="group relative"> <div className="group relative">
<InputFieldWithLabel <InputFieldWithLabel
label="New device name" label={m.rename_device_new_name_label()}
type="text" type="text"
name="name" name="name"
placeholder="Plex Media Server" placeholder={m.rename_device_new_name_placeholder()}
size="MD" size="MD"
autoFocus autoFocus
error={error?.message.toString()} error={error?.message.toString()}
@ -114,7 +108,7 @@ export default function DeviceIdRename() {
size="MD" size="MD"
theme="primary" theme="primary"
type="submit" type="submit"
text="Rename Device" text={m.rename_device()}
textAlign="center" textAlign="center"
/> />
</Form> </Form>

View File

@ -1,7 +1,7 @@
import { redirect } from "react-router"; import { redirect } from "react-router";
import type { LoaderFunction, LoaderFunctionArgs } from "react-router"; import type { LoaderFunction, LoaderFunctionArgs } from "react-router";
import { getDeviceUiPath } from "../hooks/useAppNavigation"; import { getDeviceUiPath } from "@hooks/useAppNavigation";
const loader: LoaderFunction = ({ params }: LoaderFunctionArgs) => { const loader: LoaderFunction = ({ params }: LoaderFunctionArgs) => {
return redirect(getDeviceUiPath("/settings/general", params.id)); return redirect(getDeviceUiPath("/settings/general", params.id));

View File

@ -1,22 +1,22 @@
import { useLoaderData, useNavigate } from "react-router";
import type { LoaderFunction } from "react-router";
import { ShieldCheckIcon } from "@heroicons/react/24/outline";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { useLoaderData, useNavigate, type LoaderFunction } from "react-router";
import { ShieldCheckIcon } from "@heroicons/react/24/outline";
import api from "@/api"; import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
import { SettingsPageHeader } from "@components/SettingsPageheader"; import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import { GridCard } from "@/components/Card"; import { GridCard } from "@components/Card";
import { Button, LinkButton } from "@/components/Button"; import { Button, LinkButton } from "@components/Button";
import { InputFieldWithLabel } from "@/components/InputField"; import { InputFieldWithLabel } from "@components/InputField";
import { SelectMenuBasic } from "@/components/SelectMenuBasic"; import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { SettingsItem } from "@components/SettingsItem"; import { SettingsItem } from "@components/SettingsItem";
import { SettingsSectionHeader } from "@/components/SettingsSectionHeader"; import { SettingsPageHeader } from "@components/SettingsPageheader";
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation"; import { SettingsSectionHeader } from "@components/SettingsSectionHeader";
import { TextAreaWithLabel } from "@components/TextArea";
import api from "@/api";
import notifications from "@/notifications"; import notifications from "@/notifications";
import { DEVICE_API } from "@/ui.config"; import { DEVICE_API } from "@/ui.config";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { isOnDevice } from "@/main"; import { isOnDevice } from "@/main";
import { TextAreaWithLabel } from "@components/TextArea"; import { m } from "@localizations/messages.js";
import { LocalDevice } from "./devices.$id"; import { LocalDevice } from "./devices.$id";
import { CloudState } from "./adopt"; import { CloudState } from "./adopt";
@ -92,7 +92,7 @@ export default function SettingsAccessIndexRoute() {
send("deregisterDevice", {}, (resp: JsonRpcResponse) => { send("deregisterDevice", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(
`Failed to de-register device: ${resp.error.data || "Unknown error"}`, m.access_failed_deregister({ error: resp.error.data || m.unknown_error() }),
); );
return; return;
} }
@ -107,14 +107,14 @@ export default function SettingsAccessIndexRoute() {
const onCloudAdoptClick = useCallback( const onCloudAdoptClick = useCallback(
(cloudApiUrl: string, cloudAppUrl: string) => { (cloudApiUrl: string, cloudAppUrl: string) => {
if (!deviceId) { if (!deviceId) {
notifications.error("No device ID available"); notifications.error(m.access_no_device_id());
return; return;
} }
send("setCloudUrl", { apiUrl: cloudApiUrl, appUrl: cloudAppUrl }, (resp: JsonRpcResponse) => { send("setCloudUrl", { apiUrl: cloudApiUrl, appUrl: cloudAppUrl }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(
`Failed to update cloud URL: ${resp.error.data || "Unknown error"}`, m.access_failed_update_cloud_url({ error: resp.error.data || m.unknown_error() }),
); );
return; return;
} }
@ -160,12 +160,12 @@ export default function SettingsAccessIndexRoute() {
send("setTLSState", { state }, (resp: JsonRpcResponse) => { send("setTLSState", { state }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(
`Failed to update TLS settings: ${resp.error.data || "Unknown error"}`, m.access_failed_update_tls({ error: resp.error.data || m.unknown_error() }),
); );
return; return;
} }
notifications.success("TLS settings updated successfully"); notifications.success(m.access_tls_updated());
}); });
}, [send]); }, [send]);
@ -206,22 +206,22 @@ export default function SettingsAccessIndexRoute() {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<SettingsPageHeader <SettingsPageHeader
title="Access" title={m.access_title()}
description="Manage the Access Control of the device" description={m.access_description()}
/> />
{loaderData?.authMode && ( {loaderData?.authMode && (
<> <>
<div className="space-y-4"> <div className="space-y-4">
<SettingsSectionHeader <SettingsSectionHeader
title="Local" title={m.access_local_title()}
description="Manage the mode of local access to the device" description={m.access_local_description()}
/> />
<> <>
<SettingsItem <SettingsItem
title="HTTPS Mode" title={m.access_https_mode_title()}
badge="Experimental" badge="Experimental"
description="Configure secure HTTPS access to your device" description={m.access_https_description()}
> >
<SelectMenuBasic <SelectMenuBasic
size="SM" size="SM"
@ -229,9 +229,9 @@ export default function SettingsAccessIndexRoute() {
onChange={e => handleTlsModeChange(e.target.value)} onChange={e => handleTlsModeChange(e.target.value)}
disabled={tlsMode === "unknown"} disabled={tlsMode === "unknown"}
options={[ options={[
{ value: "disabled", label: "Disabled" }, { value: "disabled", label: m.access_tls_disabled() },
{ value: "self-signed", label: "Self-signed" }, { value: "self-signed", label: m.access_tls_self_signed() },
{ value: "custom", label: "Custom" }, { value: "custom", label: m.access_tls_custom() },
]} ]}
/> />
</SettingsItem> </SettingsItem>
@ -240,12 +240,12 @@ export default function SettingsAccessIndexRoute() {
<div className="mt-4 space-y-4"> <div className="mt-4 space-y-4">
<div className="space-y-4"> <div className="space-y-4">
<SettingsItem <SettingsItem
title="TLS Certificate" title={m.access_tls_certificate_title()}
description="Paste your TLS certificate below. For certificate chains, include the entire chain (leaf, intermediate, and root certificates)." description={m.access_tls_certificate_description()}
/> />
<div className="space-y-4"> <div className="space-y-4">
<TextAreaWithLabel <TextAreaWithLabel
label="Certificate" label={m.access_certificate_label()}
rows={3} rows={3}
placeholder={ placeholder={
"-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----" "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
@ -258,8 +258,8 @@ export default function SettingsAccessIndexRoute() {
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-4"> <div className="space-y-4">
<TextAreaWithLabel <TextAreaWithLabel
label="Private Key" label={m.access_private_key_label()}
description="For security reasons, it will not be displayed after saving." description={m.access_private_key_description()}
rows={3} rows={3}
placeholder={ placeholder={
"-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----" "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
@ -274,7 +274,7 @@ export default function SettingsAccessIndexRoute() {
<Button <Button
size="SM" size="SM"
theme="primary" theme="primary"
text="Update TLS Settings" text={m.access_update_tls_settings()}
onClick={handleCustomTlsUpdate} onClick={handleCustomTlsUpdate}
/> />
</div> </div>
@ -282,14 +282,14 @@ export default function SettingsAccessIndexRoute() {
)} )}
<SettingsItem <SettingsItem
title="Authentication Mode" title={m.access_authentication_mode_title()}
description={`Current mode: ${loaderData.authMode === "password" ? "Password protected" : "No password"}`} description={loaderData.authMode === "password" ? m.access_auth_mode_password() : m.access_auth_mode_no_password()}
> >
{loaderData.authMode === "password" ? ( {loaderData.authMode === "password" ? (
<Button <Button
size="SM" size="SM"
theme="light" theme="light"
text="Disable Protection" text={m.access_disable_protection()}
onClick={() => { onClick={() => {
navigateTo("./local-auth", { state: { init: "deletePassword" } }); navigateTo("./local-auth", { state: { init: "deletePassword" } });
}} }}
@ -298,7 +298,7 @@ export default function SettingsAccessIndexRoute() {
<Button <Button
size="SM" size="SM"
theme="light" theme="light"
text="Enable Password" text={m.access_enable_password()}
onClick={() => { onClick={() => {
navigateTo("./local-auth", { state: { init: "createPassword" } }); navigateTo("./local-auth", { state: { init: "createPassword" } });
}} }}
@ -309,13 +309,13 @@ export default function SettingsAccessIndexRoute() {
{loaderData.authMode === "password" && ( {loaderData.authMode === "password" && (
<SettingsItem <SettingsItem
title="Change Password" title={m.access_change_password_title()}
description="Update your device access password" description={m.access_change_password_description()}
> >
<Button <Button
size="SM" size="SM"
theme="light" theme="light"
text="Change Password" text={m.access_change_password_button()}
onClick={() => { onClick={() => {
navigateTo("./local-auth", { state: { init: "updatePassword" } }); navigateTo("./local-auth", { state: { init: "updatePassword" } });
}} }}
@ -330,23 +330,23 @@ export default function SettingsAccessIndexRoute() {
<div className="space-y-4"> <div className="space-y-4">
<SettingsSectionHeader <SettingsSectionHeader
title="Remote" title="Remote"
description="Manage the mode of Remote access to the device" description={m.access_remote_description()}
/> />
<div className="space-y-4"> <div className="space-y-4">
{!isAdopted && ( {!isAdopted && (
<> <>
<SettingsItem <SettingsItem
title="Cloud Provider" title={m.access_cloud_provider_title()}
description="Select the cloud provider for your device" description={m.access_cloud_provider_description()}
> >
<SelectMenuBasic <SelectMenuBasic
size="SM" size="SM"
value={selectedProvider} value={selectedProvider}
onChange={e => handleProviderChange(e.target.value)} onChange={e => handleProviderChange(e.target.value)}
options={[ options={[
{ value: "jetkvm", label: "JetKVM Cloud" }, { value: "jetkvm", label: m.access_provider_jetkvm() },
{ value: "custom", label: "Custom" }, { value: "custom", label: m.access_provider_custom() },
]} ]}
/> />
</SettingsItem> </SettingsItem>
@ -356,7 +356,7 @@ export default function SettingsAccessIndexRoute() {
<div className="flex items-end gap-x-2"> <div className="flex items-end gap-x-2">
<InputFieldWithLabel <InputFieldWithLabel
size="SM" size="SM"
label="Cloud API URL" label={m.access_cloud_api_url_label()}
value={cloudApiUrl} value={cloudApiUrl}
onChange={e => setCloudApiUrl(e.target.value)} onChange={e => setCloudApiUrl(e.target.value)}
placeholder="https://api.example.com" placeholder="https://api.example.com"
@ -365,7 +365,7 @@ export default function SettingsAccessIndexRoute() {
<div className="flex items-end gap-x-2"> <div className="flex items-end gap-x-2">
<InputFieldWithLabel <InputFieldWithLabel
size="SM" size="SM"
label="Cloud App URL" label={m.access_cloud_app_url_label()}
value={cloudAppUrl} value={cloudAppUrl}
onChange={e => setCloudAppUrl(e.target.value)} onChange={e => setCloudAppUrl(e.target.value)}
placeholder="https://app.example.com" placeholder="https://app.example.com"
@ -384,26 +384,26 @@ export default function SettingsAccessIndexRoute() {
<div className="space-y-3"> <div className="space-y-3">
<div className="space-y-2"> <div className="space-y-2">
<h3 className="text-base font-bold text-slate-900 dark:text-white"> <h3 className="text-base font-bold text-slate-900 dark:text-white">
Cloud Security {m.access_cloud_security_title()}
</h3> </h3>
<div> <div>
<ul className="list-disc space-y-1 pl-5 text-xs text-slate-700 dark:text-slate-300"> <ul className="list-disc space-y-1 pl-5 text-xs text-slate-700 dark:text-slate-300">
<li>End-to-end encryption using WebRTC (DTLS and SRTP)</li> <li>{m.access_security_encryption()}</li>
<li>Zero Trust security model</li> <li>{m.access_security_zero_trust()}</li>
<li>OIDC (OpenID Connect) authentication</li> <li>{m.access_security_oidc()}</li>
<li>All streams encrypted in transit</li> <li>{m.access_security_streams()}</li>
</ul> </ul>
</div> </div>
<div className="text-xs text-slate-700 dark:text-slate-300"> <div className="text-xs text-slate-700 dark:text-slate-300">
All cloud components are open-source and available on{" "} {m.access_security_open_source()}{" "}
<a <a
href="https://github.com/jetkvm" href="https://github.com/jetkvm"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="font-medium text-blue-600 hover:text-blue-800 dark:text-blue-500 dark:hover:text-blue-400" className="font-medium text-blue-600 hover:text-blue-800 dark:text-blue-500 dark:hover:text-blue-400"
> >
GitHub {m.access_github_link()}
</a> </a>
. .
</div> </div>
@ -415,7 +415,7 @@ export default function SettingsAccessIndexRoute() {
to="https://jetkvm.com/docs/networking/remote-access" to="https://jetkvm.com/docs/networking/remote-access"
size="SM" size="SM"
theme="light" theme="light"
text="Learn about our cloud security" text={m.access_learn_security()}
/> />
</div> </div>
</div> </div>
@ -429,32 +429,32 @@ export default function SettingsAccessIndexRoute() {
onClick={() => onCloudAdoptClick(cloudApiUrl, cloudAppUrl)} onClick={() => onCloudAdoptClick(cloudApiUrl, cloudAppUrl)}
size="SM" size="SM"
theme="primary" theme="primary"
text="Adopt KVM to Cloud" text={m.access_adopt_kvm()}
/> />
</div> </div>
) : ( ) : (
<div> <div>
<div className="space-y-2"> <div className="space-y-2">
<p className="text-sm text-slate-600 dark:text-slate-300"> <p className="text-sm text-slate-600 dark:text-slate-300">
Your device is adopted to the Cloud {m.access_adopted_message()}
</p> </p>
<div> <div>
<Button <Button
size="SM" size="SM"
theme="light" theme="light"
text="De-register from Cloud" text={m.access_deregister()}
className="text-red-600" className="text-red-600"
onClick={() => { onClick={() => {
if (deviceId) { if (deviceId) {
if ( if (
window.confirm( window.confirm(
"Are you sure you want to de-register this device?", m.access_confirm_deregister(),
) )
) { ) {
deregisterDevice(); deregisterDevice();
} }
} else { } else {
notifications.error("No device ID available"); notifications.error(m.access_no_device_id());
} }
}} }}
/> />

View File

@ -1,11 +1,12 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useLocation, useRevalidator } from "react-router"; import { useLocation, useRevalidator } from "react-router";
import { useLocalAuthModalStore } from "@hooks/stores";
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
import { Button } from "@components/Button"; import { Button } from "@components/Button";
import { InputFieldWithLabel } from "@/components/InputField"; import { InputFieldWithLabel } from "@/components/InputField";
import api from "@/api"; import api from "@/api";
import { useLocalAuthModalStore } from "@/hooks/stores"; import { m } from "@localizations/messages.js";
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
export default function SecurityAccessLocalAuthRoute() { export default function SecurityAccessLocalAuthRoute() {
const { setModalView } = useLocalAuthModalStore(); const { setModalView } = useLocalAuthModalStore();
@ -21,25 +22,22 @@ export default function SecurityAccessLocalAuthRoute() {
} }
}, [init, navigateTo, setModalView]); }, [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("..")} />; return <Dialog onClose={() => navigateTo("..")} />;
} }
export function Dialog({ onClose }: { onClose: () => void }) { export function Dialog({ onClose }: Readonly<{ onClose: () => void }>) {
const { modalView, setModalView } = useLocalAuthModalStore(); const { modalView, setModalView } = useLocalAuthModalStore();
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const revalidator = useRevalidator(); const revalidator = useRevalidator();
const handleCreatePassword = async (password: string, confirmPassword: string) => { const handleCreatePassword = async (password: string, confirmPassword: string) => {
if (password === "") { if (password === "") {
setError("Please enter a password"); setError(m.local_auth_error_enter_password());
return; return;
} }
if (password !== confirmPassword) { if (password !== confirmPassword) {
setError("Passwords do not match"); setError(m.local_auth_error_passwords_not_match());
return; return;
} }
@ -51,11 +49,11 @@ export function Dialog({ onClose }: { onClose: () => void }) {
revalidator.revalidate(); revalidator.revalidate();
} else { } else {
const data = await res.json(); const data = await res.json();
setError(data.error || "An error occurred while setting the password"); setError(data.error || m.local_auth_error_setting_password());
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
setError("An error occurred while setting the password"); setError(m.local_auth_error_setting_password());
} }
}; };
@ -65,17 +63,17 @@ export function Dialog({ onClose }: { onClose: () => void }) {
confirmNewPassword: string, confirmNewPassword: string,
) => { ) => {
if (newPassword !== confirmNewPassword) { if (newPassword !== confirmNewPassword) {
setError("Passwords do not match"); setError(m.local_auth_error_passwords_not_match());
return; return;
} }
if (oldPassword === "") { if (oldPassword === "") {
setError("Please enter your old password"); setError(m.local_auth_error_enter_old_password());
return; return;
} }
if (newPassword === "") { if (newPassword === "") {
setError("Please enter a new password"); setError(m.local_auth_error_enter_new_password());
return; return;
} }
@ -91,17 +89,17 @@ export function Dialog({ onClose }: { onClose: () => void }) {
revalidator.revalidate(); revalidator.revalidate();
} else { } else {
const data = await res.json(); const data = await res.json();
setError(data.error || "An error occurred while changing the password"); setError(data.error || m.local_auth_error_changing_password());
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
setError("An error occurred while changing the password"); setError(m.local_auth_error_changing_password());
} }
}; };
const handleDeletePassword = async (password: string) => { const handleDeletePassword = async (password: string) => {
if (password === "") { if (password === "") {
setError("Please enter your current password"); setError(m.local_auth_error_enter_current_password());
return; return;
} }
@ -113,11 +111,11 @@ export function Dialog({ onClose }: { onClose: () => void }) {
revalidator.revalidate(); revalidator.revalidate();
} else { } else {
const data = await res.json(); const data = await res.json();
setError(data.error || "An error occurred while disabling the password"); setError(data.error || m.local_auth_error_disabling_password());
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
setError("An error occurred while disabling the password"); setError(m.local_auth_error_disabling_password());
} }
}; };
@ -150,24 +148,24 @@ export function Dialog({ onClose }: { onClose: () => void }) {
{modalView === "creationSuccess" && ( {modalView === "creationSuccess" && (
<SuccessModal <SuccessModal
headline="Password Set Successfully" headline={m.local_auth_success_password_set_title()}
description="You've successfully set up local device protection. Your device is now secure against unauthorized local access." description={m.local_auth_success_password_set_description()}
onClose={onClose} onClose={onClose}
/> />
)} )}
{modalView === "deleteSuccess" && ( {modalView === "deleteSuccess" && (
<SuccessModal <SuccessModal
headline="Password Protection Disabled" headline={m.local_auth_success_password_disabled_title()}
description="You've successfully disabled the password protection for local access. Remember, your device is now less secure." description={m.local_auth_success_password_disabled_description()}
onClose={onClose} onClose={onClose}
/> />
)} )}
{modalView === "updateSuccess" && ( {modalView === "updateSuccess" && (
<SuccessModal <SuccessModal
headline="Password Updated Successfully" headline={m.local_auth_success_password_updated_title()}
description="You've successfully changed your local device protection password. Make sure to remember your new password for future access." description={m.local_auth_success_password_updated_description()}
onClose={onClose} onClose={onClose}
/> />
)} )}
@ -198,24 +196,24 @@ function CreatePasswordModal({
> >
<div> <div>
<h2 className="text-lg font-semibold dark:text-white"> <h2 className="text-lg font-semibold dark:text-white">
Local Device Protection {m.local_auth_create_title()}
</h2> </h2>
<p className="text-sm text-slate-600 dark:text-slate-400"> <p className="text-sm text-slate-600 dark:text-slate-400">
Create a password to protect your device from unauthorized local access. {m.local_auth_create_description()}
</p> </p>
</div> </div>
<InputFieldWithLabel <InputFieldWithLabel
label="New Password" label={m.local_auth_create_new_password_label()}
type="password" type="password"
placeholder="Enter a strong password" placeholder={m.local_auth_create_new_password_placeholder()}
value={password} value={password}
autoFocus autoFocus
onChange={e => setPassword(e.target.value)} onChange={e => setPassword(e.target.value)}
/> />
<InputFieldWithLabel <InputFieldWithLabel
label="Confirm New Password" label={m.local_auth_confirm_new_password_label()}
type="password" type="password"
placeholder="Re-enter your password" placeholder={m.local_auth_create_confirm_password_placeholder()}
value={confirmPassword} value={confirmPassword}
onChange={e => setConfirmPassword(e.target.value)} onChange={e => setConfirmPassword(e.target.value)}
/> />
@ -224,10 +222,10 @@ function CreatePasswordModal({
<Button <Button
size="SM" size="SM"
theme="primary" theme="primary"
text="Secure Device" text={m.local_auth_create_secure_button()}
onClick={() => onSetPassword(password, confirmPassword)} onClick={() => onSetPassword(password, confirmPassword)}
/> />
<Button size="SM" theme="light" text="Not Now" onClick={onCancel} /> <Button size="SM" theme="light" text={m.local_auth_create_not_now_button()} onClick={onCancel} />
</div> </div>
{error && <p className="text-sm text-red-500">{error}</p>} {error && <p className="text-sm text-red-500">{error}</p>}
</form> </form>
@ -251,16 +249,16 @@ function DeletePasswordModal({
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<h2 className="text-lg font-semibold dark:text-white"> <h2 className="text-lg font-semibold dark:text-white">
Disable Local Device Protection {m.local_auth_disable_local_device_protection_title()}
</h2> </h2>
<p className="text-sm text-slate-600 dark:text-slate-400"> <p className="text-sm text-slate-600 dark:text-slate-400">
Enter your current password to disable local device protection. {m.local_auth_disable_local_device_protection_description()}
</p> </p>
</div> </div>
<InputFieldWithLabel <InputFieldWithLabel
label="Current Password" label={m.local_auth_current_password_label()}
type="password" type="password"
placeholder="Enter your current password" placeholder={m.local_auth_enter_current_password_placeholder()}
value={password} value={password}
onChange={e => setPassword(e.target.value)} onChange={e => setPassword(e.target.value)}
/> />
@ -268,10 +266,10 @@ function DeletePasswordModal({
<Button <Button
size="SM" size="SM"
theme="danger" theme="danger"
text="Disable Protection" text={m.local_auth_disable_protection_button()}
onClick={() => onDeletePassword(password)} onClick={() => onDeletePassword(password)}
/> />
<Button size="SM" theme="light" text="Cancel" onClick={onCancel} /> <Button size="SM" theme="light" text={m.cancel()} onClick={onCancel} />
</div> </div>
{error && <p className="text-sm text-red-500">{error}</p>} {error && <p className="text-sm text-red-500">{error}</p>}
</div> </div>
@ -306,31 +304,30 @@ function UpdatePasswordModal({
> >
<div> <div>
<h2 className="text-lg font-semibold dark:text-white"> <h2 className="text-lg font-semibold dark:text-white">
Change Local Device Password {m.local_auth_change_local_device_password_title()}
</h2> </h2>
<p className="text-sm text-slate-600 dark:text-slate-400"> <p className="text-sm text-slate-600 dark:text-slate-400">
Enter your current password and a new password to update your local device {m.local_auth_change_local_device_password_description()}
protection.
</p> </p>
</div> </div>
<InputFieldWithLabel <InputFieldWithLabel
label="Current Password" label={m.local_auth_current_password_label()}
type="password" type="password"
placeholder="Enter your current password" placeholder={m.local_auth_enter_current_password_placeholder()}
value={oldPassword} value={oldPassword}
onChange={e => setOldPassword(e.target.value)} onChange={e => setOldPassword(e.target.value)}
/> />
<InputFieldWithLabel <InputFieldWithLabel
label="New Password" label={m.local_auth_new_password_label()}
type="password" type="password"
placeholder="Enter a new strong password" placeholder={m.local_auth_enter_new_password_placeholder()}
value={newPassword} value={newPassword}
onChange={e => setNewPassword(e.target.value)} onChange={e => setNewPassword(e.target.value)}
/> />
<InputFieldWithLabel <InputFieldWithLabel
label="Confirm New Password" label={m.local_auth_confirm_new_password_label()}
type="password" type="password"
placeholder="Re-enter your new password" placeholder={m.local_auth_reenter_new_password_placeholder()}
value={confirmNewPassword} value={confirmNewPassword}
onChange={e => setConfirmNewPassword(e.target.value)} onChange={e => setConfirmNewPassword(e.target.value)}
/> />
@ -338,10 +335,10 @@ function UpdatePasswordModal({
<Button <Button
size="SM" size="SM"
theme="primary" theme="primary"
text="Update Password" text={m.local_auth_update_password_button()}
onClick={() => onUpdatePassword(oldPassword, newPassword, confirmNewPassword)} onClick={() => onUpdatePassword(oldPassword, newPassword, confirmNewPassword)}
/> />
<Button size="SM" theme="light" text="Cancel" onClick={onCancel} /> <Button size="SM" theme="light" text={m.cancel()} onClick={onCancel} />
</div> </div>
{error && <p className="text-sm text-red-500">{error}</p>} {error && <p className="text-sm text-red-500">{error}</p>}
</form> </form>
@ -365,7 +362,7 @@ function SuccessModal({
<h2 className="text-lg font-semibold dark:text-white">{headline}</h2> <h2 className="text-lg font-semibold dark:text-white">{headline}</h2>
<p className="text-sm text-slate-600 dark:text-slate-400">{description}</p> <p className="text-sm text-slate-600 dark:text-slate-400">{description}</p>
</div> </div>
<Button size="SM" theme="primary" text="Close" onClick={onClose} /> <Button size="SM" theme="primary" text={m.close()} onClick={onClose} />
</div> </div>
</div> </div>
); );

View File

@ -1,17 +1,17 @@
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { useSettingsStore } from "@hooks/stores";
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import { Button } from "@components/Button";
import Checkbox from "@components/Checkbox";
import { ConfirmDialog } from "@components/ConfirmDialog";
import { GridCard } from "@components/Card"; import { GridCard } from "@components/Card";
import { SettingsItem } from "@components/SettingsItem"; import { SettingsItem } from "@components/SettingsItem";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import { Button } from "../components/Button"; import { TextAreaWithLabel } from "@components/TextArea";
import Checkbox from "../components/Checkbox"; import { isOnDevice } from "@/main";
import { ConfirmDialog } from "../components/ConfirmDialog"; import notifications from "@/notifications";
import { SettingsPageHeader } from "../components/SettingsPageheader"; import { m } from "@localizations/messages.js";
import { TextAreaWithLabel } from "../components/TextArea";
import { useSettingsStore } from "../hooks/stores";
import { JsonRpcResponse, useJsonRpc } from "../hooks/useJsonRpc";
import { isOnDevice } from "../main";
import notifications from "../notifications";
export default function SettingsAdvancedRoute() { export default function SettingsAdvancedRoute() {
const { send } = useJsonRpc(); const { send } = useJsonRpc();
@ -65,7 +65,9 @@ export default function SettingsAdvancedRoute() {
send("setUsbEmulationState", { enabled: enabled }, (resp: JsonRpcResponse) => { send("setUsbEmulationState", { enabled: enabled }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(
`Failed to ${enabled ? "enable" : "disable"} USB emulation: ${resp.error.data || "Unknown error"}`, enabled
? m.advanced_error_usb_emulation_enable({ error: resp.error.data || m.unknown_error() })
: m.advanced_error_usb_emulation_disable({ error: resp.error.data || m.unknown_error() })
); );
return; return;
} }
@ -80,11 +82,11 @@ export default function SettingsAdvancedRoute() {
send("resetConfig", {}, (resp: JsonRpcResponse) => { send("resetConfig", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(
`Failed to reset configuration: ${resp.error.data || "Unknown error"}`, m.advanced_error_reset_config({ error: resp.error.data || m.unknown_error() })
); );
return; return;
} }
notifications.success("Configuration reset to default successfully"); notifications.success(m.advanced_success_reset_config());
}); });
}, [send]); }, [send]);
@ -92,11 +94,11 @@ export default function SettingsAdvancedRoute() {
send("setSSHKeyState", { sshKey }, (resp: JsonRpcResponse) => { send("setSSHKeyState", { sshKey }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(
`Failed to update SSH key: ${resp.error.data || "Unknown error"}`, m.advanced_error_update_ssh_key({ error: resp.error.data || m.unknown_error() })
); );
return; return;
} }
notifications.success("SSH key updated successfully"); notifications.success(m.advanced_success_update_ssh_key());
}); });
}, [send, sshKey]); }, [send, sshKey]);
@ -105,7 +107,7 @@ export default function SettingsAdvancedRoute() {
send("setDevModeState", { enabled: developerMode }, (resp: JsonRpcResponse) => { send("setDevModeState", { enabled: developerMode }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(
`Failed to set dev mode: ${resp.error.data || "Unknown error"}`, m.advanced_error_set_dev_mode({ error: resp.error.data || m.unknown_error() })
); );
return; return;
} }
@ -120,7 +122,7 @@ export default function SettingsAdvancedRoute() {
send("setDevChannelState", { enabled }, (resp: JsonRpcResponse) => { send("setDevChannelState", { enabled }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(
`Failed to set dev channel state: ${resp.error.data || "Unknown error"}`, m.advanced_error_set_dev_channel({ error: resp.error.data || m.unknown_error() })
); );
return; return;
} }
@ -135,19 +137,17 @@ export default function SettingsAdvancedRoute() {
send("setLocalLoopbackOnly", { enabled }, (resp: JsonRpcResponse) => { send("setLocalLoopbackOnly", { enabled }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(
`Failed to ${enabled ? "enable" : "disable"} loopback-only mode: ${resp.error.data || "Unknown error"}`, enabled
? m.advanced_error_loopback_enable({ error: resp.error.data || m.unknown_error() })
: m.advanced_error_loopback_disable({ error: resp.error.data || m.unknown_error() })
); );
return; return;
} }
setLocalLoopbackOnly(enabled); setLocalLoopbackOnly(enabled);
if (enabled) { if (enabled) {
notifications.success( notifications.success(m.advanced_success_loopback_enabled());
"Loopback-only mode enabled. Restart your device to apply.",
);
} else { } else {
notifications.success( notifications.success(m.advanced_success_loopback_disabled());
"Loopback-only mode disabled. Restart your device to apply.",
);
} }
}); });
}, },
@ -175,14 +175,14 @@ export default function SettingsAdvancedRoute() {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<SettingsPageHeader <SettingsPageHeader
title="Advanced" title={m.advanced_title()}
description="Access additional settings for troubleshooting and customization" description={m.advanced_description()}
/> />
<div className="space-y-4"> <div className="space-y-4">
<SettingsItem <SettingsItem
title="Dev Channel Updates" title={m.advanced_dev_channel_title()}
description="Receive early updates from the development channel" description={m.advanced_dev_channel_description()}
> >
<Checkbox <Checkbox
checked={devChannel} checked={devChannel}
@ -192,8 +192,8 @@ export default function SettingsAdvancedRoute() {
/> />
</SettingsItem> </SettingsItem>
<SettingsItem <SettingsItem
title="Developer Mode" title={m.advanced_developer_mode_title()}
description="Enable advanced features for developers" description={m.advanced_developer_mode_description()}
> >
<Checkbox <Checkbox
checked={settings.developerMode} checked={settings.developerMode}
@ -219,18 +219,17 @@ export default function SettingsAdvancedRoute() {
<div className="space-y-3"> <div className="space-y-3">
<div className="space-y-2"> <div className="space-y-2">
<h3 className="text-base font-bold text-slate-900 dark:text-white"> <h3 className="text-base font-bold text-slate-900 dark:text-white">
Developer Mode Enabled {m.advanced_developer_mode_enabled_title()}
</h3> </h3>
<div> <div>
<ul className="list-disc space-y-1 pl-5 text-xs text-slate-700 dark:text-slate-300"> <ul className="list-disc space-y-1 pl-5 text-xs text-slate-700 dark:text-slate-300">
<li>Security is weakened while active</li> <li>{m.advanced_developer_mode_warning_security()}</li>
<li>Only use if you understand the risks</li> <li>{m.advanced_developer_mode_warning_risks()}</li>
</ul> </ul>
</div> </div>
</div> </div>
<div className="text-xs text-slate-700 dark:text-slate-300"> <div className="text-xs text-slate-700 dark:text-slate-300">
For advanced users only. Not for production use. {m.advanced_developer_mode_warning_advanced()}
</div> </div>
</div> </div>
</div> </div>
@ -238,8 +237,8 @@ export default function SettingsAdvancedRoute() {
)} )}
<SettingsItem <SettingsItem
title="Loopback-Only Mode" title={m.advanced_loopback_only_title()}
description="Restrict web interface access to localhost only (127.0.0.1)" description={m.advanced_loopback_only_description()}
> >
<Checkbox <Checkbox
checked={localLoopbackOnly} checked={localLoopbackOnly}
@ -250,25 +249,25 @@ export default function SettingsAdvancedRoute() {
{isOnDevice && settings.developerMode && ( {isOnDevice && settings.developerMode && (
<div className="space-y-4"> <div className="space-y-4">
<SettingsItem <SettingsItem
title="SSH Access" title={m.advanced_ssh_access_title()}
description="Add your SSH public key to enable secure remote access to the device" description={m.advanced_ssh_access_description()}
/> />
<div className="space-y-4"> <div className="space-y-4">
<TextAreaWithLabel <TextAreaWithLabel
label="SSH Public Key" label={m.advanced_ssh_public_key_label()}
value={sshKey || ""} value={sshKey || ""}
rows={3} rows={3}
onChange={e => setSSHKey(e.target.value)} onChange={e => setSSHKey(e.target.value)}
placeholder="Enter your SSH public key" placeholder={m.advanced_ssh_public_key_placeholder()}
/> />
<p className="text-xs text-slate-600 dark:text-slate-400"> <p className="text-xs text-slate-600 dark:text-slate-400">
The default SSH user is <strong>root</strong>. {m.advanced_ssh_default_user()}<strong>root</strong>.
</p> </p>
<div className="flex items-center gap-x-2"> <div className="flex items-center gap-x-2">
<Button <Button
size="SM" size="SM"
theme="primary" theme="primary"
text="Update SSH Key" text={m.advanced_update_ssh_key_button()}
onClick={handleUpdateSSHKey} onClick={handleUpdateSSHKey}
/> />
</div> </div>
@ -277,8 +276,8 @@ export default function SettingsAdvancedRoute() {
)} )}
<SettingsItem <SettingsItem
title="Troubleshooting Mode" title={m.advanced_troubleshooting_mode_title()}
description="Diagnostic tools and additional controls for troubleshooting and development purposes" description={m.advanced_troubleshooting_mode_description()}
> >
<Checkbox <Checkbox
defaultChecked={settings.debugMode} defaultChecked={settings.debugMode}
@ -291,27 +290,27 @@ export default function SettingsAdvancedRoute() {
{settings.debugMode && ( {settings.debugMode && (
<> <>
<SettingsItem <SettingsItem
title="USB Emulation" title={m.advanced_usb_emulation_title()}
description="Control the USB emulation state" description={m.advanced_usb_emulation_description()}
> >
<Button <Button
size="SM" size="SM"
theme="light" theme="light"
text={ text={
usbEmulationEnabled ? "Disable USB Emulation" : "Enable USB Emulation" usbEmulationEnabled ? m.advanced_disable_usb_emulation() : m.advanced_enable_usb_emulation()
} }
onClick={() => handleUsbEmulationToggle(!usbEmulationEnabled)} onClick={() => handleUsbEmulationToggle(!usbEmulationEnabled)}
/> />
</SettingsItem> </SettingsItem>
<SettingsItem <SettingsItem
title="Reset Configuration" title={m.advanced_reset_config_title()}
description="Reset configuration to default. This will log you out." description={m.advanced_reset_config_description()}
> >
<Button <Button
size="SM" size="SM"
theme="light" theme="light"
text="Reset Config" text={m.advanced_reset_config_button()}
onClick={() => { onClick={() => {
handleResetConfig(); handleResetConfig();
window.location.reload(); window.location.reload();
@ -327,22 +326,23 @@ export default function SettingsAdvancedRoute() {
onClose={() => { onClose={() => {
setShowLoopbackWarning(false); setShowLoopbackWarning(false);
}} }}
title="Enable Loopback-Only Mode?" title={m.advanced_loopback_warning_title()}
description={ description={
<> <>
<p> <p>
WARNING: This will restrict web interface access to localhost (127.0.0.1) {m.advanced_loopback_warning_description()}
only. </p>
<p>
{m.advanced_loopback_warning_before()}
</p> </p>
<p>Before enabling this feature, make sure you have either:</p>
<ul className="list-disc space-y-1 pl-5 text-xs text-slate-700 dark:text-slate-300"> <ul className="list-disc space-y-1 pl-5 text-xs text-slate-700 dark:text-slate-300">
<li>SSH access configured and tested</li> <li>{m.advanced_loopback_warning_ssh()}</li>
<li>Cloud access enabled and working</li> <li>{m.advanced_loopback_warning_cloud()}</li>
</ul> </ul>
</> </>
} }
variant="warning" variant="warning"
confirmText="I Understand, Enable Anyway" confirmText={m.advanced_loopback_warning_confirm()}
onConfirm={confirmLoopbackModeEnable} onConfirm={confirmLoopbackModeEnable}
/> />
</div> </div>

View File

@ -1,9 +1,9 @@
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { SettingsItem } from "@components/SettingsItem"; import { SettingsItem } from "@components/SettingsItem";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import { SettingsPageHeader } from "../components/SettingsPageheader"; import { m } from "@localizations/messages.js";
import { SelectMenuBasic } from "../components/SelectMenuBasic";
export default function SettingsAppearanceRoute() { export default function SettingsAppearanceRoute() {
const [currentTheme, setCurrentTheme] = useState(() => { const [currentTheme, setCurrentTheme] = useState(() => {
@ -28,22 +28,24 @@ export default function SettingsAppearanceRoute() {
} }
}, []); }, []);
const themeOptions = [
{ value: "system", label: m.appearance_theme_system() },
{ value: "light", label: m.appearance_theme_light() },
{ value: "dark", label: m.appearance_theme_dark() },
];
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<SettingsPageHeader <SettingsPageHeader
title="Appearance" title={m.appearance_title()}
description="Customize the look and feel of your JetKVM interface" description={m.appearance_page_description()}
/> />
<SettingsItem title="Theme" description="Choose your preferred color theme"> <SettingsItem title={m.appearance_theme()} description={m.appearance_description()}>
<SelectMenuBasic <SelectMenuBasic
size="SM" size="SM"
label="" label=""
value={currentTheme} value={currentTheme}
options={[ options={themeOptions}
{ value: "system", label: "System" },
{ value: "light", label: "Light" },
{ value: "dark", label: "Dark" },
]}
onChange={e => { onChange={e => {
setCurrentTheme(e.target.value); setCurrentTheme(e.target.value);
handleThemeChange(e.target.value); handleThemeChange(e.target.value);

View File

@ -1,16 +1,17 @@
import { useState, useEffect, useMemo } from "react";
import { useState , useEffect } from "react"; import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
import { useDeviceStore } from "@hooks/stores";
import { Button } from "@components/Button";
import Checkbox from "@components/Checkbox";
import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { SettingsItem } from "@components/SettingsItem"; import { SettingsItem } from "@components/SettingsItem";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc"; import { SettingsPageHeader } from "@components/SettingsPageheader";
import notifications from "@/notifications";
import { SettingsPageHeader } from "../components/SettingsPageheader"; import { getLocale, setLocale, locales, baseLocale } from '@localizations/runtime.js';
import { Button } from "../components/Button"; import { m } from "@localizations/messages.js";
import notifications from "../notifications"; import { deleteCookie, map_locale_code_to_name } from "@/utils";
import Checkbox from "../components/Checkbox";
import { useDeviceUiNavigation } from "../hooks/useAppNavigation";
import { useDeviceStore } from "../hooks/stores";
export default function SettingsGeneralRoute() { export default function SettingsGeneralRoute() {
const { send } = useJsonRpc(); const { send } = useJsonRpc();
@ -34,7 +35,7 @@ export default function SettingsGeneralRoute() {
send("setAutoUpdateState", { enabled }, (resp: JsonRpcResponse) => { send("setAutoUpdateState", { enabled }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(
`Failed to set auto-update: ${resp.error.data || "Unknown error"}`, m.general_auto_update_error({ error: resp.error.data || m.unknown_error() }),
); );
return; return;
} }
@ -42,47 +43,84 @@ export default function SettingsGeneralRoute() {
}); });
}; };
const [currentLocale, setCurrentLocale] = useState(getLocale());
const localeOptions = useMemo(() => {
return ["", ...locales]
.map((code) => {
const [localizedName, nativeName] = map_locale_code_to_name(currentLocale, code);
// don't repeat the name if it's the same in both locales (or blank)
const label = nativeName && nativeName !== localizedName ? `${localizedName} - ${nativeName}` : localizedName;
return { value: code, label: label }
});
}, [currentLocale]);
const handleLocaleChange = (newLocale: string) => {
if (newLocale === currentLocale) return;
let validLocale = newLocale as typeof locales[number];
if (newLocale !== "") {
if (!locales.includes(validLocale)) {
validLocale = baseLocale;
}
setLocale(validLocale); // tell the i18n system to change locale
} else {
deleteCookie("JETKVM_LOCALE", "", "/"); // delete the cookie that the i18n system uses to store the locale
}
setCurrentLocale(validLocale);
notifications.success(m.locale_change_success({ locale: validLocale || m.locale_auto() }));
};
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<SettingsPageHeader <SettingsPageHeader
title="General" title={m.general_title()}
description="Configure device settings and update preferences" description={m.general_page_description()}
/> />
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-4 pb-2"> <div className="space-y-4 pb-2">
<div className="space-y-4">
<SettingsItem
title={m.user_interface_language_title()}
description={m.user_interface_language_description()}
>
<SelectMenuBasic
size="SM"
label=""
value={currentLocale}
options={localeOptions}
onChange={e => { handleLocaleChange(e.target.value); }}
/>
</SettingsItem>
</div>
<div className="mt-2 flex items-center justify-between gap-x-2"> <div className="mt-2 flex items-center justify-between gap-x-2">
<SettingsItem <SettingsItem
title="Check for Updates" title={m.general_check_for_updates()}
description={ description={
currentVersions ? (
<> <>
App: {currentVersions.appVersion} {m.general_app_version({ version: currentVersions ? currentVersions.appVersion : m.loading() })}
<br /> <br />
System: {currentVersions.systemVersion} {m.general_system_version({ version: currentVersions ? currentVersions.systemVersion : m.loading() })}
</> </>
) : (
<>
App: Loading...
<br />
System: Loading...
</>
)
} }
/> />
<div> <div>
<Button <Button
size="SM" size="SM"
theme="light" theme="light"
text="Check for Updates" text={m.general_check_for_updates()}
onClick={() => navigateTo("./update")} onClick={() => navigateTo("./update")}
/> />
</div> </div>
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<SettingsItem <SettingsItem
title="Auto Update" title={m.general_auto_update_title()}
description="Automatically update the device to the latest version" description={m.general_auto_update_description()}
> >
<Checkbox <Checkbox
checked={autoUpdate} checked={autoUpdate}
@ -92,17 +130,16 @@ export default function SettingsGeneralRoute() {
/> />
</SettingsItem> </SettingsItem>
</div> </div>
<div className="mt-2 flex items-center justify-between gap-x-2"> <div className="mt-2 flex items-center justify-between gap-x-2">
<SettingsItem <SettingsItem
title="Reboot Device" title={m.general_reboot_device()}
description="Power cycle the JetKVM" description={m.general_reboot_device_description()}
/> />
<div> <div>
<Button <Button
size="SM" size="SM"
theme="light" theme="light"
text="Reboot Device" text={m.general_reboot_device()}
onClick={() => navigateTo("./reboot")} onClick={() => navigateTo("./reboot")}
/> />
</div> </div>

View File

@ -1,8 +1,9 @@
import { useNavigate } from "react-router";
import { useCallback } from "react"; import { useCallback } from "react";
import { useNavigate } from "react-router";
import { useJsonRpc } from "@/hooks/useJsonRpc"; import { useJsonRpc } from "@hooks/useJsonRpc";
import { Button } from "@components/Button"; import { Button } from "@components/Button";
import { m } from "@localizations/messages.js";
export default function SettingsGeneralRebootRoute() { export default function SettingsGeneralRebootRoute() {
const navigate = useNavigate(); const navigate = useNavigate();
@ -18,10 +19,10 @@ export default function SettingsGeneralRebootRoute() {
export function Dialog({ export function Dialog({
onClose, onClose,
onConfirmUpdate, onConfirmUpdate,
}: { }: Readonly<{
onClose: () => void; onClose: () => void;
onConfirmUpdate: () => void; onConfirmUpdate: () => void;
}) { }>) {
return ( return (
<div className="pointer-events-auto relative mx-auto text-left"> <div className="pointer-events-auto relative mx-auto text-left">
@ -46,15 +47,15 @@ function ConfirmationBox({
<div className="flex flex-col items-start justify-start space-y-4 text-left"> <div className="flex flex-col items-start justify-start space-y-4 text-left">
<div className="text-left"> <div className="text-left">
<p className="text-base font-semibold text-black dark:text-white"> <p className="text-base font-semibold text-black dark:text-white">
Reboot JetKVM {m.general_reboot_title()}
</p> </p>
<p className="text-sm text-slate-600 dark:text-slate-300"> <p className="text-sm text-slate-600 dark:text-slate-300">
Do you want to proceed with rebooting the system? {m.general_reboot_description()}
</p> </p>
<div className="mt-4 flex gap-x-2"> <div className="mt-4 flex gap-x-2">
<Button size="SM" theme="light" text="Yes" onClick={onYes} /> <Button size="SM" theme="light" text={m.general_reboot_yes_button()} onClick={onYes} />
<Button size="SM" theme="blank" text="No" onClick={onNo} /> <Button size="SM" theme="blank" text={m.general_reboot_no_button()} onClick={onNo} />
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,14 +1,15 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useLocation, useNavigate } from "react-router"; import { useLocation, useNavigate } from "react-router";
import { useCallback, useEffect, useRef, useState } from "react";
import { CheckCircleIcon } from "@heroicons/react/20/solid";
import Card from "@/components/Card"; import { useJsonRpc } from "@hooks/useJsonRpc";
import { useJsonRpc } from "@/hooks/useJsonRpc"; import { UpdateState, useUpdateStore } from "@hooks/stores";
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
import { SystemVersionInfo, useVersion } from "@hooks/useVersion";
import { Button } from "@components/Button"; import { Button } from "@components/Button";
import { UpdateState, useUpdateStore } from "@/hooks/stores"; import Card from "@components/Card";
import LoadingSpinner from "@/components/LoadingSpinner"; import LoadingSpinner from "@components/LoadingSpinner";
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation"; import UpdatingStatusCard, { type UpdatePart} from "@components/UpdatingStatusCard";
import { SystemVersionInfo, useVersion } from "@/hooks/useVersion"; import { m } from "@localizations/messages.js";
export default function SettingsGeneralUpdateRoute() { export default function SettingsGeneralUpdateRoute() {
const navigate = useNavigate(); const navigate = useNavigate();
@ -35,21 +36,16 @@ export default function SettingsGeneralUpdateRoute() {
} }
}, [otaState.updating, otaState.error, setModalView, updateSuccess]); }, [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} />; return <Dialog onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />;
} }
export function Dialog({ export function Dialog({
onClose, onClose,
onConfirmUpdate, onConfirmUpdate,
}: { }: Readonly<{
onClose: () => void; onClose: () => void;
onConfirmUpdate: () => void; onConfirmUpdate: () => void;
}) { }>) {
const { navigateTo } = useDeviceUiNavigation(); const { navigateTo } = useDeviceUiNavigation();
const [versionInfo, setVersionInfo] = useState<null | SystemVersionInfo>(null); const [versionInfo, setVersionInfo] = useState<null | SystemVersionInfo>(null);
@ -71,11 +67,6 @@ export function Dialog({
[setModalView], [setModalView],
); );
// Reset modal view when dialog is opened
useEffect(() => {
setVersionInfo(null);
}, [setModalView]);
return ( return (
<div className="pointer-events-auto relative mx-auto text-left"> <div className="pointer-events-auto relative mx-auto text-left">
<div> <div>
@ -130,15 +121,15 @@ function LoadingState({
const abortControllerRef = useRef<AbortController | null>(null); const abortControllerRef = useRef<AbortController | null>(null);
const { getVersionInfo } = useVersion(); const { getVersionInfo } = useVersion();
const { setModalView } = useUpdateStore();
const progressBarRef = useRef<HTMLDivElement>(null); const progressBarRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
setProgressWidth("0%");
abortControllerRef.current = new AbortController(); abortControllerRef.current = new AbortController();
const signal = abortControllerRef.current.signal; const signal = abortControllerRef.current.signal;
const animationTimer = setTimeout(() => { const animationTimer = setTimeout(() => {
// we start the progress bar animation after a tiny delay to avoid react warnings
setProgressWidth("100%"); setProgressWidth("100%");
}, 0); }, 0);
@ -155,6 +146,7 @@ function LoadingState({
.catch(error => { .catch(error => {
if (!signal.aborted) { if (!signal.aborted) {
console.error("LoadingState: Error fetching version info", error); console.error("LoadingState: Error fetching version info", error);
setModalView("error");
} }
}); });
@ -162,17 +154,17 @@ function LoadingState({
clearTimeout(animationTimer); clearTimeout(animationTimer);
abortControllerRef.current?.abort(); abortControllerRef.current?.abort();
}; };
}, [getVersionInfo, onFinished]); }, [getVersionInfo, onFinished, setModalView]);
return ( return (
<div className="flex flex-col items-start justify-start space-y-4 text-left"> <div className="flex flex-col items-start justify-start space-y-4 text-left">
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-0"> <div className="space-y-0">
<p className="text-base font-semibold text-black dark:text-white"> <p className="text-base font-semibold text-black dark:text-white">
Checking for updates... {m.general_update_checking_title()}
</p> </p>
<p className="text-sm text-slate-600 dark:text-slate-300"> <p className="text-sm text-slate-600 dark:text-slate-300">
We{"'"}re ensuring your device has the latest features and improvements. {m.general_update_checking_description()}
</p> </p>
</div> </div>
<div className="h-2.5 w-full overflow-hidden rounded-full bg-slate-300"> <div className="h-2.5 w-full overflow-hidden rounded-full bg-slate-300">
@ -183,7 +175,7 @@ function LoadingState({
></div> ></div>
</div> </div>
<div className="mt-4"> <div className="mt-4">
<Button size="SM" theme="light" text="Cancel" onClick={onCancelCheck} /> <Button size="SM" theme="light" text={m.cancel()} onClick={onCancelCheck} />
</div> </div>
</div> </div>
</div> </div>
@ -197,8 +189,13 @@ function UpdatingDeviceState({
otaState: UpdateState["otaState"]; otaState: UpdateState["otaState"];
onMinimizeUpgradeDialog: () => void; onMinimizeUpgradeDialog: () => void;
}) { }) {
const formatProgress = (progress: number) => `${Math.round(progress)}%`; interface ProgressSummary {
system: UpdatePart;
app: UpdatePart;
areAllUpdatesComplete: boolean;
};
const progress = useMemo<ProgressSummary>(() => {
const calculateOverallProgress = (type: "system" | "app") => { const calculateOverallProgress = (type: "system" | "app") => {
const downloadProgress = Math.round((otaState[`${type}DownloadProgress`] || 0) * 100); const downloadProgress = Math.round((otaState[`${type}DownloadProgress`] || 0) * 100);
const updateProgress = Math.round((otaState[`${type}UpdateProgress`] || 0) * 100); const updateProgress = Math.round((otaState[`${type}UpdateProgress`] || 0) * 100);
@ -210,43 +207,38 @@ function UpdatingDeviceState({
return 0; return 0;
} }
console.log(
`For ${type}:\n` +
` Download Progress: ${downloadProgress}% (${otaState[`${type}DownloadProgress`]})\n` +
` Update Progress: ${updateProgress}% (${otaState[`${type}UpdateProgress`]})\n` +
` Verification Progress: ${verificationProgress}% (${otaState[`${type}VerificationProgress`]})`,
);
if (type === "app") { if (type === "app") {
// App: 65% download, 34% verification, 1% update(There is no "real" update for the app) // App: 55% download, 54% verification, 1% update(There is no "real" update for the app)
return Math.min( return Math.round(Math.min(
downloadProgress * 0.55 + verificationProgress * 0.54 + updateProgress * 0.01, downloadProgress * 0.55 + verificationProgress * 0.54 + updateProgress * 0.01,
100, 100,
); ));
} else { } else {
// System: 10% download, 90% update // System: 10% download, 10% verification, 80% update
return Math.min( return Math.round(Math.min(
downloadProgress * 0.1 + verificationProgress * 0.1 + updateProgress * 0.8, downloadProgress * 0.1 + verificationProgress * 0.1 + updateProgress * 0.8,
100, 100,
); ));
} }
}; };
const getUpdateStatus = (type: "system" | "app") => { const getUpdateStatus = (type: "system" | "app") => {
const downloadFinishedAt = otaState[`${type}DownloadFinishedAt`]; const downloadFinishedAt = otaState[`${type}DownloadFinishedAt`];
const verfiedAt = otaState[`${type}VerifiedAt`]; const verifiedAt = otaState[`${type}VerifiedAt`];
const updatedAt = otaState[`${type}UpdatedAt`]; const updatedAt = otaState[`${type}UpdatedAt`];
const update_type = () => (type === "system" ? m.general_update_system_type() : m.general_update_application_type());
if (!otaState.metadataFetchedAt) { if (!otaState.metadataFetchedAt) {
return "Fetching update information..."; return m.general_update_status_fetching();
} else if (!downloadFinishedAt) { } else if (!downloadFinishedAt) {
return `Downloading ${type} update...`; return m.general_update_status_downloading({ update_type: update_type() });
} else if (!verfiedAt) { } else if (!verifiedAt) {
return `Verifying ${type} update...`; return m.general_update_status_verifying({ update_type: update_type() });
} else if (!updatedAt) { } else if (!updatedAt) {
return `Installing ${type} update...`; return m.general_update_status_installing({ update_type: update_type() });
} else { } else {
return `Awaiting reboot`; return m.general_update_status_awaiting_reboot();
} }
}; };
@ -254,105 +246,77 @@ function UpdatingDeviceState({
return !!otaState[`${type}UpdatedAt`]; return !!otaState[`${type}UpdatedAt`];
}; };
const areAllUpdatesComplete = () => { const systemUpdatePending = otaState.systemUpdatePending
if (otaState.systemUpdatePending && otaState.appUpdatePending) { const systemUpdateComplete = isUpdateComplete("system");
return isUpdateComplete("system") && isUpdateComplete("app");
const appUpdatePending = otaState.appUpdatePending
const appUpdateComplete = isUpdateComplete("app");
let areAllUpdatesComplete: boolean;
if (!systemUpdatePending && !appUpdatePending) {
areAllUpdatesComplete = false;
} else if (systemUpdatePending && appUpdatePending) {
areAllUpdatesComplete = systemUpdateComplete && appUpdateComplete;
} else {
areAllUpdatesComplete = systemUpdatePending ? systemUpdateComplete : appUpdateComplete;
} }
return (
(otaState.systemUpdatePending && isUpdateComplete("system")) || return {
(otaState.appUpdatePending && isUpdateComplete("app")) system: {
); pending: systemUpdatePending,
status: getUpdateStatus("system"),
progress: calculateOverallProgress("system"),
complete: systemUpdateComplete,
},
app: {
pending: appUpdatePending,
status: getUpdateStatus("app"),
progress: calculateOverallProgress("app"),
complete: appUpdateComplete,
},
areAllUpdatesComplete,
}; };
}, [otaState]);
return ( return (
<div className="flex flex-col items-start justify-start space-y-4 text-left"> <div className="flex flex-col items-start justify-start space-y-4 text-left">
<div className="w-full max-w-sm space-y-4"> <div className="w-full max-w-sm space-y-4">
<div className="space-y-0"> <div className="space-y-0">
<p className="text-base font-semibold text-black dark:text-white"> <p className="text-base font-semibold text-black dark:text-white">
Updating your device {m.general_update_updating_title()}
</p> </p>
<p className="text-sm text-slate-600 dark:text-slate-300"> <p className="text-sm text-slate-600 dark:text-slate-300">
Please don{"'"}t turn off your device. This process may take a few minutes. {m.general_update_updating_description()}
</p> </p>
</div> </div>
<Card className="space-y-4 p-4"> <Card className="space-y-4 p-4">
{areAllUpdatesComplete() ? ( {progress.areAllUpdatesComplete ? (
<div className="my-2 flex flex-col items-center space-y-2 text-center"> <div className="my-2 flex flex-col items-center space-y-2 text-center">
<LoadingSpinner className="h-6 w-6 text-blue-700 dark:text-blue-500" /> <LoadingSpinner className="h-6 w-6 text-blue-700 dark:text-blue-500" />
<div className="flex justify-between text-sm text-slate-600 dark:text-slate-300"> <div className="flex justify-between text-sm text-slate-600 dark:text-slate-300">
<span className="font-medium text-black dark:text-white"> <span className="font-medium text-black dark:text-white">
Rebooting to complete the update... {m.general_update_rebooting()}
</span> </span>
</div> </div>
</div> </div>
) : ( ) : (
<> <>
{!(otaState.systemUpdatePending || otaState.appUpdatePending) && ( {!(progress.system.pending || progress.app.pending) && (
<div className="my-2 flex flex-col items-center space-y-2 text-center"> <div className="my-2 flex flex-col items-center space-y-2 text-center">
<LoadingSpinner className="h-6 w-6 text-blue-700 dark:text-blue-500" /> <LoadingSpinner className="h-6 w-6 text-blue-700 dark:text-blue-500" />
</div> </div>
)} )}
{otaState.systemUpdatePending && ( {progress.system.pending && (
<div className="space-y-2"> <UpdatingStatusCard label={m.general_update_system_update_title()} part={progress.system} />
<div className="flex items-center justify-between">
<p className="text-sm font-semibold text-black dark:text-white">
Linux System Update
</p>
{calculateOverallProgress("system") < 100 ? (
<LoadingSpinner className="h-4 w-4 text-blue-700 dark:text-blue-500" />
) : (
<CheckCircleIcon className="h-4 w-4 text-blue-700 dark:text-blue-500" />
)} )}
</div>
<div className="h-2.5 w-full overflow-hidden rounded-full bg-slate-300 dark:bg-slate-600"> {progress.system.pending && progress.app.pending && (
<div
className="h-2.5 rounded-full bg-blue-700 transition-all duration-500 ease-linear dark:bg-blue-500"
style={{
width: formatProgress(calculateOverallProgress("system")),
}}
></div>
</div>
<div className="flex justify-between text-sm text-slate-600 dark:text-slate-300">
<span>{getUpdateStatus("system")}</span>
{calculateOverallProgress("system") < 100 ? (
<span>{formatProgress(calculateOverallProgress("system"))}</span>
) : null}
</div>
</div>
)}
{otaState.appUpdatePending && (
<>
{otaState.systemUpdatePending && (
<hr className="dark:border-slate-600" /> <hr className="dark:border-slate-600" />
)} )}
<div className="space-y-2">
<div className="flex items-center justify-between"> {progress.app.pending && (
<p className="text-sm font-semibold text-black dark:text-white"> <UpdatingStatusCard label={m.general_update_app_update_title()} part={progress.app} />
App Update
</p>
{calculateOverallProgress("app") < 100 ? (
<LoadingSpinner className="h-4 w-4 text-blue-700 dark:text-blue-500" />
) : (
<CheckCircleIcon className="h-4 w-4 text-blue-700 dark:text-blue-500" />
)}
</div>
<div className="h-2.5 w-full overflow-hidden rounded-full bg-slate-300 dark:bg-slate-600">
<div
className="h-2.5 rounded-full bg-blue-700 transition-all duration-500 ease-linear dark:bg-blue-500"
style={{
width: formatProgress(calculateOverallProgress("app")),
}}
></div>
</div>
<div className="flex justify-between text-sm text-slate-600 dark:text-slate-300">
<span>{getUpdateStatus("app")}</span>
{calculateOverallProgress("system") < 100 ? (
<span>{formatProgress(calculateOverallProgress("app"))}</span>
) : null}
</div>
</div>
</>
)} )}
</> </>
)} )}
@ -361,7 +325,7 @@ function UpdatingDeviceState({
<Button <Button
size="XS" size="XS"
theme="light" theme="light"
text="Update in Background" text={m.general_update_background_button()}
onClick={onMinimizeUpgradeDialog} onClick={onMinimizeUpgradeDialog}
/> />
</div> </div>
@ -381,15 +345,15 @@ function SystemUpToDateState({
<div className="flex flex-col items-start justify-start space-y-4 text-left"> <div className="flex flex-col items-start justify-start space-y-4 text-left">
<div className="text-left"> <div className="text-left">
<p className="text-base font-semibold text-black dark:text-white"> <p className="text-base font-semibold text-black dark:text-white">
System is up to date {m.general_update_up_to_date_title()}
</p> </p>
<p className="text-sm text-slate-600 dark:text-slate-300"> <p className="text-sm text-slate-600 dark:text-slate-300">
Your system is running the latest version. No updates are currently available. {m.general_update_up_to_date_description()}
</p> </p>
<div className="mt-4 flex gap-x-2"> <div className="mt-4 flex gap-x-2">
<Button size="SM" theme="light" text="Check Again" onClick={checkUpdate} /> <Button size="SM" theme="light" text={m.general_update_check_again_button()} onClick={checkUpdate} />
<Button size="SM" theme="blank" text="Back" onClick={onClose} /> <Button size="SM" theme="blank" text={m.back()} onClick={onClose} />
</div> </div>
</div> </div>
</div> </div>
@ -409,30 +373,27 @@ function UpdateAvailableState({
<div className="flex flex-col items-start justify-start space-y-4 text-left"> <div className="flex flex-col items-start justify-start space-y-4 text-left">
<div className="text-left"> <div className="text-left">
<p className="text-base font-semibold text-black dark:text-white"> <p className="text-base font-semibold text-black dark:text-white">
Update available {m.general_update_available_title()}
</p> </p>
<p className="mb-2 text-sm text-slate-600 dark:text-slate-300"> <p className="mb-2 text-sm text-slate-600 dark:text-slate-300">
A new update is available to enhance system performance and improve {m.general_update_available_description()}
compatibility. We recommend updating to ensure everything runs smoothly.
</p> </p>
<p className="mb-4 text-sm text-slate-600 dark:text-slate-300"> <p className="mb-4 text-sm text-slate-600 dark:text-slate-300">
{versionInfo?.systemUpdateAvailable ? ( {versionInfo?.systemUpdateAvailable ? (
<> <>
<span className="font-semibold">System:</span>{" "} <span className="font-semibold">{m.general_update_system_type()}</span>: {versionInfo?.remote?.systemVersion}
{versionInfo?.remote?.systemVersion}
<br /> <br />
</> </>
) : null} ) : null}
{versionInfo?.appUpdateAvailable ? ( {versionInfo?.appUpdateAvailable ? (
<> <>
<span className="font-semibold">App:</span>{" "} <span className="font-semibold">{m.general_update_application_type()}</span>: {versionInfo?.remote?.appVersion}
{versionInfo?.remote?.appVersion}
</> </>
) : null} ) : null}
</p> </p>
<div className="flex items-center justify-start gap-x-2"> <div className="flex items-center justify-start gap-x-2">
<Button size="SM" theme="primary" text="Update Now" onClick={onConfirmUpdate} /> <Button size="SM" theme="primary" text={m.general_update_now_button()} onClick={onConfirmUpdate} />
<Button size="SM" theme="light" text="Do it later" onClick={onClose} /> <Button size="SM" theme="light" text={m.general_update_later_button()} onClick={onClose} />
</div> </div>
</div> </div>
</div> </div>
@ -444,14 +405,13 @@ function UpdateCompletedState({ onClose }: { onClose: () => void }) {
<div className="flex flex-col items-start justify-start space-y-4 text-left"> <div className="flex flex-col items-start justify-start space-y-4 text-left">
<div className="text-left"> <div className="text-left">
<p className="text-base font-semibold dark:text-white"> <p className="text-base font-semibold dark:text-white">
Update Completed Successfully {m.general_update_completed_title()}
</p> </p>
<p className="mb-4 text-sm text-slate-600 dark:text-slate-400"> <p className="mb-4 text-sm text-slate-600 dark:text-slate-400">
Your device has been successfully updated to the latest version. Enjoy the new {m.general_update_completed_description()}
features and improvements!
</p> </p>
<div className="flex items-center justify-start"> <div className="flex items-center justify-start">
<Button size="SM" theme="primary" text="Back" onClick={onClose} /> <Button size="SM" theme="primary" text={m.back()} onClick={onClose} />
</div> </div>
</div> </div>
</div> </div>
@ -470,18 +430,18 @@ function UpdateErrorState({
return ( return (
<div className="flex flex-col items-start justify-start space-y-4 text-left"> <div className="flex flex-col items-start justify-start space-y-4 text-left">
<div className="text-left"> <div className="text-left">
<p className="text-base font-semibold dark:text-white">Update Error</p> <p className="text-base font-semibold dark:text-white">{m.general_update_error_title()}</p>
<p className="mb-4 text-sm text-slate-600 dark:text-slate-400"> <p className="mb-4 text-sm text-slate-600 dark:text-slate-400">
An error occurred while updating your device. Please try again later. {m.general_update_error_description()}
</p> </p>
{errorMessage && ( {errorMessage && (
<p className="mb-4 text-sm font-medium text-red-600 dark:text-red-400"> <p className="mb-4 text-sm font-medium text-red-600 dark:text-red-400">
Error details: {errorMessage} {m.general_update_error_details({ errorMessage })}
</p> </p>
)} )}
<div className="flex items-center justify-start gap-x-2"> <div className="flex items-center justify-start gap-x-2">
<Button size="SM" theme="light" text="Back" onClick={onClose} /> <Button size="SM" theme="light" text={m.back()} onClick={onClose} />
<Button size="SM" theme="blank" text="Retry" onClick={onRetryUpdate} /> <Button size="SM" theme="blank" text={m.retry()} onClick={onRetryUpdate} />
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,22 +1,22 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { BacklightSettings, useSettingsStore } from "@hooks/stores";
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import { Checkbox } from "@components/Checkbox";
import { FeatureFlag } from "@components/FeatureFlag";
import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { SettingsItem } from "@components/SettingsItem"; import { SettingsItem } from "@components/SettingsItem";
import { SettingsPageHeader } from "@components/SettingsPageheader"; import { SettingsPageHeader } from "@components/SettingsPageheader";
import { SettingsSectionHeader } from "@components/SettingsSectionHeader"; import { SettingsSectionHeader } from "@components/SettingsSectionHeader";
import { BacklightSettings, useSettingsStore } from "@/hooks/stores";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { UsbDeviceSetting } from "@components/UsbDeviceSetting"; import { UsbDeviceSetting } from "@components/UsbDeviceSetting";
import { Checkbox } from "@components/Checkbox"; import { UsbInfoSetting } from "@components/UsbInfoSetting";
import notifications from "@/notifications";
import notifications from "../notifications"; import { m } from "@localizations/messages.js";
import { UsbInfoSetting } from "../components/UsbInfoSetting";
import { FeatureFlag } from "../components/FeatureFlag";
export default function SettingsHardwareRoute() { export default function SettingsHardwareRoute() {
const { send } = useJsonRpc(); const { send } = useJsonRpc();
const settings = useSettingsStore(); const settings = useSettingsStore();
const { setDisplayRotation } = useSettingsStore(); const { displayRotation, setDisplayRotation } = useSettingsStore();
const [powerSavingEnabled, setPowerSavingEnabled] = useState(false); const [powerSavingEnabled, setPowerSavingEnabled] = useState(false);
const handleDisplayRotationChange = (rotation: string) => { const handleDisplayRotationChange = (rotation: string) => {
@ -25,18 +25,18 @@ export default function SettingsHardwareRoute() {
}; };
const handleDisplayRotationSave = () => { const handleDisplayRotationSave = () => {
send("setDisplayRotation", { params: { rotation: settings.displayRotation } }, (resp: JsonRpcResponse) => { send("setDisplayRotation", { params: { rotation: displayRotation } }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(
`Failed to set display orientation: ${resp.error.data || "Unknown error"}`, m.hardware_display_orientation_error({ error: resp.error.data || m.unknown_error() }),
); );
return; return;
} }
notifications.success("Display orientation updated successfully"); notifications.success(m.hardware_display_orientation_success());
}); });
}; };
const { setBacklightSettings } = useSettingsStore(); const { backlightSettings, setBacklightSettings } = useSettingsStore();
const handleBacklightSettingsChange = (settings: BacklightSettings) => { const handleBacklightSettingsChange = (settings: BacklightSettings) => {
// If the user has set the display to dim after it turns off, set the dim_after // If the user has set the display to dim after it turns off, set the dim_after
@ -50,29 +50,42 @@ export default function SettingsHardwareRoute() {
}; };
const handleBacklightSettingsSave = () => { const handleBacklightSettingsSave = () => {
send("setBacklightSettings", { params: settings.backlightSettings }, (resp: JsonRpcResponse) => { send("setBacklightSettings", { params: backlightSettings }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(
`Failed to set backlight settings: ${resp.error.data || "Unknown error"}`, m.hardware_backlight_settings_error({ error: resp.error.data || m.unknown_error() }),
); );
return; return;
} }
notifications.success("Backlight settings updated successfully"); notifications.success(m.hardware_backlight_settings_success());
}); });
}; };
const handleBacklightMaxBrightnessChange = (max_brightness: number) => {
const settings = { ...backlightSettings, max_brightness };
handleBacklightSettingsChange(settings);
};
const handleBacklightDimAfterChange = (dim_after: number) => {
const settings = { ...backlightSettings, dim_after };
handleBacklightSettingsChange(settings);
};
const handleBacklightOffAfterChange = (off_after: number) => {
const settings = { ...backlightSettings, off_after };
handleBacklightSettingsChange(settings);
};
const handlePowerSavingChange = (enabled: boolean) => { const handlePowerSavingChange = (enabled: boolean) => {
setPowerSavingEnabled(enabled); setPowerSavingEnabled(enabled);
const duration = enabled ? 90 : -1; const duration = enabled ? 90 : -1;
send("setVideoSleepMode", { duration }, (resp: JsonRpcResponse) => { send("setVideoSleepMode", { duration }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(m.hardware_power_saving_failed_error({ error: resp.error.data ||m.unknown_error() }));
`Failed to set power saving mode: ${resp.error.data || "Unknown error"}`, setPowerSavingEnabled(!enabled); // Attempt to revert on error
);
setPowerSavingEnabled(!enabled); // Revert on error
return; return;
} }
notifications.success(`Power saving mode ${enabled ? "enabled" : "disabled"}`); notifications.success(enabled ? m.hardware_power_saving_enabled() : m.hardware_power_saving_disabled());
}); });
}; };
@ -80,7 +93,7 @@ export default function SettingsHardwareRoute() {
send("getBacklightSettings", {}, (resp: JsonRpcResponse) => { send("getBacklightSettings", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
return notifications.error( return notifications.error(
`Failed to get backlight settings: ${resp.error.data || "Unknown error"}`, m.hardware_backlight_settings_get_error({ error: resp.error.data || m.unknown_error() }),
); );
} }
const result = resp.result as BacklightSettings; const result = resp.result as BacklightSettings;
@ -102,97 +115,93 @@ export default function SettingsHardwareRoute() {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<SettingsPageHeader <SettingsPageHeader
title="Hardware" title={m.hardware_title()}
description="Configure display settings and hardware options for your JetKVM device" description={m.hardware_page_description()}
/> />
<div className="space-y-4"> <div className="space-y-4">
<SettingsItem <SettingsItem
title="Display Orientation" title={m.hardware_display_orientation_title()}
description="Set the orientation of the display" description={m.hardware_display_orientation_description()}
> >
<SelectMenuBasic <SelectMenuBasic
size="SM" size="SM"
label="" label=""
value={settings.displayRotation.toString()} value={settings.displayRotation.toString()}
options={[ options={[
{ value: "270", label: "Normal" }, { value: "270", label: m.hardware_display_orientation_normal() },
{ value: "90", label: "Inverted" }, { value: "90", label: m.hardware_display_orientation_inverted() },
]} ]}
onChange={e => { onChange={e => {
settings.displayRotation = e.target.value; handleDisplayRotationChange(e.target.value);
handleDisplayRotationChange(settings.displayRotation);
}} }}
/> />
</SettingsItem> </SettingsItem>
<SettingsItem <SettingsItem
title="Display Brightness" title={m.hardware_display_brightness_title()}
description="Set the brightness of the display" description={m.hardware_display_brightness_description()}
> >
<SelectMenuBasic <SelectMenuBasic
size="SM" size="SM"
label="" label=""
value={settings.backlightSettings.max_brightness.toString()} value={backlightSettings.max_brightness.toString()}
options={[ options={[
{ value: "0", label: "Off" }, { value: "0", label: m.hardware_display_brightness_off() },
{ value: "10", label: "Low" }, { value: "10", label: m.hardware_display_brightness_low() },
{ value: "35", label: "Medium" }, { value: "35", label: m.hardware_display_brightness_medium() },
{ value: "64", label: "High" }, { value: "64", label: m.hardware_display_brightness_high() },
]} ]}
onChange={e => { onChange={e => {
settings.backlightSettings.max_brightness = parseInt(e.target.value); handleBacklightMaxBrightnessChange(Number.parseInt(e.target.value));
handleBacklightSettingsChange(settings.backlightSettings);
}} }}
/> />
</SettingsItem> </SettingsItem>
{settings.backlightSettings.max_brightness != 0 && ( {backlightSettings.max_brightness != 0 && (
<> <>
<SettingsItem <SettingsItem
title="Dim Display After" title={m.hardware_dim_display_after_title()}
description="Set how long to wait before dimming the display" description={m.hardware_dim_display_after_description()}
> >
<SelectMenuBasic <SelectMenuBasic
size="SM" size="SM"
label="" label=""
value={settings.backlightSettings.dim_after.toString()} value={backlightSettings.dim_after.toString()}
options={[ options={[
{ value: "0", label: "Never" }, { value: "0", label: m.hardware_time_never() },
{ value: "60", label: "1 Minute" }, { value: "60", label: m.hardware_time_1_minute() },
{ value: "300", label: "5 Minutes" }, { value: "300", label: m.hardware_time_5_minutes() },
{ value: "600", label: "10 Minutes" }, { value: "600", label: m.hardware_time_10_minutes() },
{ value: "1800", label: "30 Minutes" }, { value: "1800", label: m.hardware_time_30_minutes() },
{ value: "3600", label: "1 Hour" }, { value: "3600", label: m.hardware_time_1_hour() },
]} ]}
onChange={e => { onChange={e => {
settings.backlightSettings.dim_after = parseInt(e.target.value); handleBacklightDimAfterChange(Number.parseInt(e.target.value));
handleBacklightSettingsChange(settings.backlightSettings);
}} }}
/> />
</SettingsItem> </SettingsItem>
<SettingsItem <SettingsItem
title="Turn off Display After" title={m.hardware_turn_off_display_after_title()}
description="Period of inactivity before display automatically turns off" description={m.hardware_turn_off_display_after_description()}
> >
<SelectMenuBasic <SelectMenuBasic
size="SM" size="SM"
label="" label=""
value={settings.backlightSettings.off_after.toString()} value={backlightSettings.off_after.toString()}
options={[ options={[
{ value: "0", label: "Never" }, { value: "0", label: m.hardware_time_never() },
{ value: "300", label: "5 Minutes" }, { value: "300", label: m.hardware_time_5_minutes() },
{ value: "600", label: "10 Minutes" }, { value: "600", label: m.hardware_time_10_minutes() },
{ value: "1800", label: "30 Minutes" }, { value: "1800", label: m.hardware_time_30_minutes() },
{ value: "3600", label: "1 Hour" }, { value: "3600", label: m.hardware_time_1_hour() },
]} ]}
onChange={e => { onChange={e => {
settings.backlightSettings.off_after = parseInt(e.target.value); handleBacklightOffAfterChange(Number.parseInt(e.target.value));
handleBacklightSettingsChange(settings.backlightSettings);
}} }}
/> />
</SettingsItem> </SettingsItem>
</> </>
)} )}
<p className="text-xs text-slate-600 dark:text-slate-400"> <p className="text-xs text-slate-600 dark:text-slate-400">
The display will wake up when the connection state changes, or when touched. {m.hardware_display_wake_up_note()}
</p> </p>
</div> </div>
@ -200,13 +209,13 @@ export default function SettingsHardwareRoute() {
<div className="space-y-4"> <div className="space-y-4">
<div className="h-px w-full bg-slate-800/10 dark:bg-slate-300/20" /> <div className="h-px w-full bg-slate-800/10 dark:bg-slate-300/20" />
<SettingsSectionHeader <SettingsSectionHeader
title="Power Saving" title={m.hardware_power_saving_title()}
description="Reduce power consumption when not in use" description={m.hardware_power_saving_description()}
/> />
<SettingsItem <SettingsItem
badge="Experimental" badge={m.experimental()}
title="HDMI Sleep Mode" title={m.hardware_power_saving_hdmi_sleep_title()}
description="Turn off capture after 90 seconds of inactivity" description={m.hardware_power_saving_hdmi_sleep_description()}
> >
<Checkbox <Checkbox
checked={powerSavingEnabled} checked={powerSavingEnabled}

View File

@ -1,13 +1,14 @@
import { useCallback, useEffect } from "react"; import { useCallback, useEffect } from "react";
import { useSettingsStore } from "@/hooks/stores"; import { useSettingsStore } from "@hooks/stores";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc"; import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import useKeyboardLayout from "@/hooks/useKeyboardLayout"; import useKeyboardLayout from "@hooks/useKeyboardLayout";
import { Checkbox } from "@components/Checkbox";
import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { SettingsItem } from "@components/SettingsItem"; import { SettingsItem } from "@components/SettingsItem";
import { SettingsPageHeader } from "@components/SettingsPageheader"; import { SettingsPageHeader } from "@components/SettingsPageheader";
import { Checkbox } from "@/components/Checkbox";
import { SelectMenuBasic } from "@/components/SelectMenuBasic";
import notifications from "@/notifications"; import notifications from "@/notifications";
import { m } from "@localizations/messages.js";
export default function SettingsKeyboardRoute() { export default function SettingsKeyboardRoute() {
const { setKeyboardLayout } = useSettingsStore(); const { setKeyboardLayout } = useSettingsStore();
@ -33,10 +34,10 @@ export default function SettingsKeyboardRoute() {
send("setKeyboardLayout", { layout: isoCode }, resp => { send("setKeyboardLayout", { layout: isoCode }, resp => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(
`Failed to set keyboard layout: ${resp.error.data || "Unknown error"}`, m.keyboard_layout_error({ error: resp.error.data || m.unknown_error() }),
); );
} }
notifications.success("Keyboard layout set successfully to " + isoCode); notifications.success(m.keyboard_layout_success({ layout: isoCode }));
setKeyboardLayout(isoCode); setKeyboardLayout(isoCode);
}); });
}, },
@ -46,14 +47,14 @@ export default function SettingsKeyboardRoute() {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<SettingsPageHeader <SettingsPageHeader
title="Keyboard" title={m.keyboard_title()}
description="Configure keyboard settings for your device" description={m.keyboard_description()}
/> />
<div className="space-y-4"> <div className="space-y-4">
<SettingsItem <SettingsItem
title="Keyboard Layout" title={m.keyboard_layout_title()}
description="Keyboard layout of target operating system" description={m.keyboard_layout_description()}
> >
<SelectMenuBasic <SelectMenuBasic
size="SM" size="SM"
@ -65,14 +66,14 @@ export default function SettingsKeyboardRoute() {
/> />
</SettingsItem> </SettingsItem>
<p className="text-xs text-slate-600 dark:text-slate-400"> <p className="text-xs text-slate-600 dark:text-slate-400">
The virtual keyboard, paste text, and keyboard macros send individual key strokes to the target device. The keyboard layout determines which key codes are being sent. Ensure that the keyboard layout in JetKVM matches the settings in the operating system. {m.keyboard_layout_long_description()}
</p> </p>
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<SettingsItem <SettingsItem
title="Show Pressed Keys" title={m.keyboard_show_pressed_keys_title()}
description="Display currently pressed keys in the status bar" description={m.keyboard_show_pressed_keys_description()}
> >
<Checkbox <Checkbox
checked={showPressedKeys} checked={showPressedKeys}

Some files were not shown because too many files have changed in this diff Show More