release-2.5.1 (#1067)

* chore(deps): bump node from 24-slim to 26-slim in /docker in the docker-major-updates group (#1021)

* chore: fix release workflow to merge docs branch

* fix: svg donation generator push fail

* fix: svg donation generator push fail

* Update termix.rb

* fix: svg donation generator push fail

* chore: move donation badge to badges branch to avoid ruleset conflicts

* chore: remove unneeded token from donation badge workflow

* chore: debug donation badge commit step

* fix: escape < character in donation SVG

* fix: point donation badge to badges branch

* chore: remove unused donation badge svg from main

* Add Rack Genius logo to README

Added Rack Genius logo to the README.

* chore: improve donation goal svg generator to include stablecoins

* chore: donation goal generator syntax error

* chore: donation goal generator incorrect docs url usage

* chore(deps): bump node in /docker in the docker-major-updates group

Bumps the docker-major-updates group in /docker with 1 update: node.


Updates `node` from 24-slim to 26-slim

---
updated-dependencies:
- dependency-name: node
  dependency-version: 26-slim
  dependency-type: direct:production
  dependency-group: docker-major-updates
...

Signed-off-by: dependabot[bot] <support@github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps-dev): bump the dev-patch-updates group with 24 updates (#1023)

* chore: fix release workflow to merge docs branch

* fix: svg donation generator push fail

* fix: svg donation generator push fail

* Update termix.rb

* fix: svg donation generator push fail

* chore: move donation badge to badges branch to avoid ruleset conflicts

* chore: remove unneeded token from donation badge workflow

* chore: debug donation badge commit step

* fix: escape < character in donation SVG

* fix: point donation badge to badges branch

* chore: remove unused donation badge svg from main

* Add Rack Genius logo to README

Added Rack Genius logo to the README.

* chore: improve donation goal svg generator to include stablecoins

* chore: donation goal generator syntax error

* chore: donation goal generator incorrect docs url usage

* chore(deps-dev): bump the dev-patch-updates group with 24 updates

Bumps the dev-patch-updates group with 24 updates:

| Package | From | To |
| --- | --- | --- |
| [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.5.1` | `2.5.2` |
| [@codemirror/commands](https://github.com/codemirror/commands) | `6.10.3` | `6.10.4` |
| [@codemirror/view](https://github.com/codemirror/view) | `6.43.1` | `6.43.5` |
| [@radix-ui/react-accordion](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/accordion) | `1.2.14` | `1.2.15` |
| [@radix-ui/react-alert-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/alert-dialog) | `1.1.17` | `1.1.18` |
| [@radix-ui/react-checkbox](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/checkbox) | `1.3.5` | `1.3.6` |
| [@radix-ui/react-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dialog) | `1.1.17` | `1.1.18` |
| [@radix-ui/react-dropdown-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dropdown-menu) | `2.1.18` | `2.1.19` |
| [@radix-ui/react-label](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/label) | `2.1.10` | `2.1.11` |
| [@radix-ui/react-popover](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/popover) | `1.1.17` | `1.1.18` |
| [@radix-ui/react-progress](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/progress) | `1.1.10` | `1.1.11` |
| [@radix-ui/react-scroll-area](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/scroll-area) | `1.2.12` | `1.2.13` |
| [@radix-ui/react-select](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/select) | `2.3.1` | `2.3.2` |
| [@radix-ui/react-separator](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/separator) | `1.1.10` | `1.1.11` |
| [@radix-ui/react-slider](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slider) | `1.4.1` | `1.4.2` |
| [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) | `1.3.1` | `1.3.2` |
| [@radix-ui/react-tabs](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tabs) | `1.1.15` | `1.1.16` |
| [@radix-ui/react-tooltip](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tooltip) | `1.2.10` | `1.2.11` |
| [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) | `4.3.1` | `4.3.2` |
| [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `6.0.2` | `6.0.3` |
| [i18next](https://github.com/i18next/i18next) | `26.3.1` | `26.3.4` |
| [radix-ui](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/radix-ui) | `1.6.0` | `1.6.1` |
| [sharp](https://github.com/lovell/sharp) | `0.35.2` | `0.35.3` |
| [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `4.3.1` | `4.3.2` |


Updates `@biomejs/biome` from 2.5.1 to 2.5.2
- [Release notes](https://github.com/biomejs/biome/releases)
- [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md)
- [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.5.2/packages/@biomejs/biome)

Updates `@codemirror/commands` from 6.10.3 to 6.10.4
- [Changelog](https://github.com/codemirror/commands/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codemirror/commands/commits)

Updates `@codemirror/view` from 6.43.1 to 6.43.5
- [Changelog](https://github.com/codemirror/view/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codemirror/view/commits)

Updates `@radix-ui/react-accordion` from 1.2.14 to 1.2.15
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/accordion/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/accordion)

Updates `@radix-ui/react-alert-dialog` from 1.1.17 to 1.1.18
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/alert-dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/alert-dialog)

Updates `@radix-ui/react-checkbox` from 1.3.5 to 1.3.6
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/checkbox/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/checkbox)

Updates `@radix-ui/react-dialog` from 1.1.17 to 1.1.18
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dialog)

Updates `@radix-ui/react-dropdown-menu` from 2.1.18 to 2.1.19
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dropdown-menu/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dropdown-menu)

Updates `@radix-ui/react-label` from 2.1.10 to 2.1.11
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/label/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/label)

Updates `@radix-ui/react-popover` from 1.1.17 to 1.1.18
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/popover/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/popover)

Updates `@radix-ui/react-progress` from 1.1.10 to 1.1.11
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/progress/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/progress)

Updates `@radix-ui/react-scroll-area` from 1.2.12 to 1.2.13
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/scroll-area/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/scroll-area)

Updates `@radix-ui/react-select` from 2.3.1 to 2.3.2
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/select/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/select)

Updates `@radix-ui/react-separator` from 1.1.10 to 1.1.11
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/separator/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/separator)

Updates `@radix-ui/react-slider` from 1.4.1 to 1.4.2
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slider/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slider)

Updates `@radix-ui/react-switch` from 1.3.1 to 1.3.2
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/switch/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/switch)

Updates `@radix-ui/react-tabs` from 1.1.15 to 1.1.16
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tabs/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tabs)

Updates `@radix-ui/react-tooltip` from 1.2.10 to 1.2.11
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tooltip/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tooltip)

Updates `@tailwindcss/vite` from 4.3.1 to 4.3.2
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.2/packages/@tailwindcss-vite)

Updates `@vitejs/plugin-react` from 6.0.2 to 6.0.3
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.0.3/packages/plugin-react)

Updates `i18next` from 26.3.1 to 26.3.4
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v26.3.1...v26.3.4)

Updates `radix-ui` from 1.6.0 to 1.6.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/radix-ui/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/radix-ui)

Updates `sharp` from 0.35.2 to 0.35.3
- [Release notes](https://github.com/lovell/sharp/releases)
- [Commits](https://github.com/lovell/sharp/compare/v0.35.2...v0.35.3)

Updates `tailwindcss` from 4.3.1 to 4.3.2
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.2/packages/tailwindcss)

---
updated-dependencies:
- dependency-name: "@biomejs/biome"
  dependency-version: 2.5.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@codemirror/commands"
  dependency-version: 6.10.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@codemirror/view"
  dependency-version: 6.43.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@radix-ui/react-accordion"
  dependency-version: 1.2.15
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@radix-ui/react-alert-dialog"
  dependency-version: 1.1.18
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@radix-ui/react-checkbox"
  dependency-version: 1.3.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@radix-ui/react-dialog"
  dependency-version: 1.1.18
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@radix-ui/react-dropdown-menu"
  dependency-version: 2.1.19
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@radix-ui/react-label"
  dependency-version: 2.1.11
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@radix-ui/react-popover"
  dependency-version: 1.1.18
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@radix-ui/react-progress"
  dependency-version: 1.1.11
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@radix-ui/react-scroll-area"
  dependency-version: 1.2.13
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@radix-ui/react-select"
  dependency-version: 2.3.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@radix-ui/react-separator"
  dependency-version: 1.1.11
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@radix-ui/react-slider"
  dependency-version: 1.4.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@radix-ui/react-switch"
  dependency-version: 1.3.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@radix-ui/react-tabs"
  dependency-version: 1.1.16
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@radix-ui/react-tooltip"
  dependency-version: 1.2.11
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.3.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 6.0.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: i18next
  dependency-version: 26.3.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: radix-ui
  dependency-version: 1.6.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: sharp
  dependency-version: 0.35.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: tailwindcss
  dependency-version: 4.3.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump the prod-patch-updates group with 2 updates (#1025)

* chore: fix release workflow to merge docs branch

* fix: svg donation generator push fail

* fix: svg donation generator push fail

* Update termix.rb

* fix: svg donation generator push fail

* chore: move donation badge to badges branch to avoid ruleset conflicts

* chore: remove unneeded token from donation badge workflow

* chore: debug donation badge commit step

* fix: escape < character in donation SVG

* fix: point donation badge to badges branch

* chore: remove unused donation badge svg from main

* Add Rack Genius logo to README

Added Rack Genius logo to the README.

* chore: improve donation goal svg generator to include stablecoins

* chore: donation goal generator syntax error

* chore: donation goal generator incorrect docs url usage

* chore(deps): bump the prod-patch-updates group with 2 updates

Bumps the prod-patch-updates group with 2 updates: [axios](https://github.com/axios/axios) and [nanoid](https://github.com/ai/nanoid).


Updates `axios` from 1.18.0 to 1.18.1
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.18.0...v1.18.1)

Updates `nanoid` from 5.1.15 to 5.1.16
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/5.1.15...5.1.16)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.18.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: nanoid
  dependency-version: 5.1.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump the prod-minor-updates group with 3 updates (#1026)

* chore: fix release workflow to merge docs branch

* fix: svg donation generator push fail

* fix: svg donation generator push fail

* Update termix.rb

* fix: svg donation generator push fail

* chore: move donation badge to badges branch to avoid ruleset conflicts

* chore: remove unneeded token from donation badge workflow

* chore: debug donation badge commit step

* fix: escape < character in donation SVG

* fix: point donation badge to badges branch

* chore: remove unused donation badge svg from main

* Add Rack Genius logo to README

Added Rack Genius logo to the README.

* chore: improve donation goal svg generator to include stablecoins

* chore: donation goal generator syntax error

* chore: donation goal generator incorrect docs url usage

* chore(deps): bump the prod-minor-updates group with 3 updates

Bumps the prod-minor-updates group with 3 updates: [js-yaml](https://github.com/nodeca/js-yaml), [motion](https://github.com/motiondivision/motion) and [undici](https://github.com/nodejs/undici).


Updates `js-yaml` from 5.0.0 to 5.2.1
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/5.0.0...5.2.1)

Updates `motion` from 12.40.0 to 12.42.2
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v12.40.0...v12.42.2)

Updates `undici` from 8.5.0 to 8.7.0
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v8.5.0...v8.7.0)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 5.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: motion
  dependency-version: 12.42.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: undici
  dependency-version: 8.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps-dev): bump electron from 42.4.1 to 43.0.0 in the major-updates group (#1027)

* chore: fix release workflow to merge docs branch

* fix: svg donation generator push fail

* fix: svg donation generator push fail

* Update termix.rb

* fix: svg donation generator push fail

* chore: move donation badge to badges branch to avoid ruleset conflicts

* chore: remove unneeded token from donation badge workflow

* chore: debug donation badge commit step

* fix: escape < character in donation SVG

* fix: point donation badge to badges branch

* chore: remove unused donation badge svg from main

* Add Rack Genius logo to README

Added Rack Genius logo to the README.

* chore: improve donation goal svg generator to include stablecoins

* chore: donation goal generator syntax error

* chore: donation goal generator incorrect docs url usage

* chore(deps-dev): bump electron in the major-updates group

Bumps the major-updates group with 1 update: [electron](https://github.com/electron/electron).


Updates `electron` from 42.4.1 to 43.0.0
- [Release notes](https://github.com/electron/electron/releases)
- [Commits](https://github.com/electron/electron/compare/v42.4.1...v43.0.0)

---
updated-dependencies:
- dependency-name: electron
  dependency-version: 43.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: major-updates
...

Signed-off-by: dependabot[bot] <support@github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Fix MC syntax highlighting artifacts (#996)

* Filter dashboard status hosts (#997)

* Persist dashboard service link changes (#999)

* Fix snippet text overflow (#1000)

* Persist remote desktop credential auth (#1001)

* Guard language switching failures (#1002)

* Resolve tunnel source credentials (#1003)

* Support Vault auth for monitors (#1004)

* Fix Windows file delete command (#1005)

* Fix release artifact checkout ref (#1006)

* Fix command palette escape in fullscreen (#1008)

* Fix alerts and audit log data normalization (#1010)

* Fix macOS VNC protocol negotiation (#1012)

* Fix port knocking before SSH connect (#1013)

* Allow Escape to close link confirmation (#1014)

* Prevent Electron modifier wheel zoom (#1016)

* Fix credential auth optional password (#1009)

* Retry transient terminal DNS lookups (#1011)

* Retry transient terminal DNS lookups

* Apply DNS retry to SSH entry points

* Fix OIDC redirect forwarded port handling (#1007)

* Preserve recent open tabs on startup (#1015)

* Fix fish prompt OSC highlighting (#998)

* Fix terminal font selection (#1018)

* fix: font legibility (#1019)

* chore: fix release workflow to merge docs branch

* fix: svg donation generator push fail

* fix: svg donation generator push fail

* Update termix.rb

* fix: svg donation generator push fail

* chore: move donation badge to badges branch to avoid ruleset conflicts

* chore: remove unneeded token from donation badge workflow

* chore: debug donation badge commit step

* fix: escape < character in donation SVG

* fix: point donation badge to badges branch

* chore: remove unused donation badge svg from main

* Add Rack Genius logo to README

Added Rack Genius logo to the README.

* chore: improve donation goal svg generator to include stablecoins

* chore: donation goal generator syntax error

* chore: donation goal generator incorrect docs url usage

* fix: font legibility

Text was entirely unreadable in places for me. Especially with themes
like Catppuccin. The muted-foreground text and the tags too similiar to
the background.

---------

Co-authored-by: LukeGus <bugattiguy527@gmail.com>
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: russell <git@0896c69e.com>

* fix(file-manager): chunked uploads fail with 'Expected multipart/form-data request' (#1020)

* chore: fix release workflow to merge docs branch

* fix: svg donation generator push fail

* fix: svg donation generator push fail

* Update termix.rb

* fix: svg donation generator push fail

* chore: move donation badge to badges branch to avoid ruleset conflicts

* chore: remove unneeded token from donation badge workflow

* chore: debug donation badge commit step

* fix: escape < character in donation SVG

* fix: point donation badge to badges branch

* chore: remove unused donation badge svg from main

* Add Rack Genius logo to README

Added Rack Genius logo to the README.

* chore: improve donation goal svg generator to include stablecoins

* chore: donation goal generator syntax error

* chore: donation goal generator incorrect docs url usage

* fix(file-manager): use postForm for chunked uploads so multipart content-type is sent

The fileManagerApi axios instance defaults to Content-Type:
application/json. Axios 1.x's default transformRequest converts a
FormData body to JSON whenever the request content type is
application/json, so every chunk POSTed to /ssh/uploadFileChunk
arrived as a JSON body like {"chunk":{}} and the backend rejected
it with 400 'Expected multipart/form-data request'. This breaks all
uploads of files larger than the 1.5 GiB chunking threshold.

The non-chunked path already uses postForm for /ssh/uploadFileStream;
use it for the chunk path too so axios keeps the FormData intact and
the browser sets the multipart boundary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: LukeGus <bugattiguy527@gmail.com>
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* feat: implement OIDC back-channel logout support with session management (#1028)

* feat: implement OIDC back-channel logout support with session management

* Fix OIDC back-channel logout handling

* Require logout token replay identifiers

---------

Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com>

* Add API key host enrollment endpoint (#1029)

* Fix tmux detection for non-POSIX shells (#1030)

* Fix OPKSSH js-yaml ESM import (#1031)

* Fix Android Vietnamese IME input (#1032)

* Fix Firefox RDP clipboard paste (#1033)

* Fix Proxmox discovery over HTTPS (#1041)

* Fix external editor actions in file preview (#1042)

* Allow pinned hosts with name sorting (#1043)

* Fix Firefox desktop OIDC callback (#1044)

* feat(session): add recording and replay (#1049)

* Fix status checks through jump hosts (#1045)

* Add terminal font size shortcuts (#1047)

* feat: add Open File Manager to tab right-click menu (#1051)

Co-authored-by: SankeerthNara <sankeerthnara@gmail.com>

* perf: frontend request cache, poll pause, and code-split shell (#1052)

Host/status caching, shell code-split, SSH pool waits, host-metrics concurrency, background-tab idle, per-host status subscriptions, homepage poll quieting, and virtualized host sidebar + file manager lists.

* Merge commit from fork

* Merge commit from fork

* Merge commit from fork

* Merge commit from fork

* Merge commit from fork

* Merge commit from fork

* Merge commit from fork

* Merge commit from fork

* Merge commit from fork

* Merge commit from fork

* Merge commit from fork

* Merge commit from fork

* feat: save quick connect sessions as hosts (#1055)

* fix: restore sudo password autofill settings (#1056)

* fix: preserve file editor position on save (#1057)

* fix: sync cloud preference storage mode (#1058)

* fix: render RDP sessions at native pixel density (#1059)

* fix: restore database import in embedded desktop mode (#1060)

* Update Auto-complete.tsx (#1061)

* chore: fix release workflow to merge docs branch

* fix: svg donation generator push fail

* fix: svg donation generator push fail

* Update termix.rb

* fix: svg donation generator push fail

* chore: move donation badge to badges branch to avoid ruleset conflicts

* chore: remove unneeded token from donation badge workflow

* chore: debug donation badge commit step

* fix: escape < character in donation SVG

* fix: point donation badge to badges branch

* chore: remove unused donation badge svg from main

* Add Rack Genius logo to README

Added Rack Genius logo to the README.

* chore: improve donation goal svg generator to include stablecoins

* chore: donation goal generator syntax error

* chore: donation goal generator incorrect docs url usage

* chore: donation bar reporting wrong result

* feat: add Open File Manager to tab right-click menu (#1046)

* Revert "feat: add Open File Manager to tab right-click menu (#1046)" (#1050)

This reverts commit 0712fdd731.

* Remove donation badge from README

Removed donation badge from README.

* Delete .github/workflows/donation-goal.yml

* Update Auto-complete.tsx

---------

Co-authored-by: LukeGus <bugattiguy527@gmail.com>
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com>

* feat(auth): opt-in OIDC DEK unlock for API-key requests (ALLOW_APIKEY_DATA_UNLOCK) (#1064)

* chore: fix release workflow to merge docs branch

* fix: svg donation generator push fail

* fix: svg donation generator push fail

* Update termix.rb

* fix: svg donation generator push fail

* chore: move donation badge to badges branch to avoid ruleset conflicts

* chore: remove unneeded token from donation badge workflow

* chore: debug donation badge commit step

* fix: escape < character in donation SVG

* fix: point donation badge to badges branch

* chore: remove unused donation badge svg from main

* Add Rack Genius logo to README

Added Rack Genius logo to the README.

* chore: improve donation goal svg generator to include stablecoins

* chore: donation goal generator syntax error

* chore: donation goal generator incorrect docs url usage

* chore: donation bar reporting wrong result

* feat: add Open File Manager to tab right-click menu (#1046)

* Revert "feat: add Open File Manager to tab right-click menu (#1046)" (#1050)

This reverts commit 0712fdd731.

* Remove donation badge from README

Removed donation badge from README.

* Delete .github/workflows/donation-goal.yml

* feat(auth): opt-in OIDC DEK unlock for API-key requests

API keys authenticate but cannot touch the encrypted credential/host store
('User data not unlocked') unless the user has a live interactive session,
making them unusable for headless automation. For OIDC users the DEK is
server-derivable (deriveOIDCSystemKey), so handleApiKeyAuth can unlock it
without a password.

Gated behind ALLOW_APIKEY_DATA_UNLOCK (default off) because enabling it widens
the blast radius of a leaked API key. OIDC-only; password users are untouched.

Refs #1063

---------

Co-authored-by: LukeGus <bugattiguy527@gmail.com>
Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com>
Co-authored-by: Sankeerth Nara <sankeerthnara@gmail.com>
Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com>

* chore: package lock sync

* Add Proxmox guest auto sync (#1053)

* draft: database layer refactor (#1054)

* feat(sshid) - sshid.io equivalent for termix (#919)

* feat(ssh-id): database schema, migrations and field encryption

Adds ssh_identities, ssh_identity_keys and ssh_identity_ca tables (public keys
stored plaintext for the unauthenticated resolver; CA private key registered
for per-user field encryption), with UNIQUE(user_id), an index on
ssh_identity_keys(identity_id), and idempotent CREATE TABLE migrations.

* feat(ssh-id): backend API — resolver, key management, CA and certificates

Mounts /sshid (nginx route added). Public text/plain authorized_keys resolver
(+ exact /:algo filter, HTML viewer) and CA public-key endpoint; no-store +
noindex headers on every resolver response including early 404s. Authenticated
management: claim/rename/delete handle, add/import/generate/enable/delete keys,
and a per-user CA (create/rotate/delete) with pure-Node OpenSSH certificate
issuance. Audit logging on all mutations; UNIQUE races map to a precise 409.
Unit tests for key parsing and certificate signing (ssh-keygen-validated).

* feat(ssh-id): frontend panel, API client and i18n

SSH ID panel wired into the app rail and AppShell: claim handle, resolver URL +
curl one-liner, key list, generate, paste/import, CA enable/rotate/remove with
server trust command, and per-key certificate issuance. API client re-exported
through main-axios.ts; all strings i18n'd.

* style(ssh-id): align panel and resolver page with Termix theme

- Rebuild the SSH ID sidebar panel with the theme's square components
  (SectionCard / SettingRow / FakeSwitch) instead of rounded ad-hoc cards;
  use accent-brand and destructive tokens rather than raw red/green.
- Fix panel scrolling: move overflow to a block scroll container so the
  cards keep their natural height instead of being clipped.
- Restyle the public resolver HTML page (/sshid/u/:handle) to the Termix
  dark theme: square corners, #18181b/#303032 palette, #f59145 accent,
  uppercase section labels.
- Tidy copy: 'Save To Credentials' label, drop the redundant generate intro,
  and correct the generate tooltip (the key is stored when saving to vault).

* feat: rename to Termix ID, improve UI, backend inconsistencies, and general bug fixes

---------

Co-authored-by: LukeGus <bugattiguy527@gmail.com>

* ci(deps): bump actions/checkout from 6 to 7 in the github-actions group (#922)

Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 6 to 7
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps-dev): bump the dev-patch-updates group with 11 updates (#923)

Bumps the dev-patch-updates group with 11 updates:

| Package | From | To |
| --- | --- | --- |
| [@codemirror/search](https://github.com/codemirror/search) | `6.7.0` | `6.7.1` |
| [@codemirror/view](https://github.com/codemirror/view) | `6.43.0` | `6.43.1` |
| [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) | `4.3.0` | `4.3.1` |
| [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) | `4.1.8` | `4.1.9` |
| [@vitest/ui](https://github.com/vitest-dev/vitest/tree/HEAD/packages/ui) | `4.1.8` | `4.1.9` |
| [eslint-plugin-react-refresh](https://github.com/ArnaudBarre/eslint-plugin-react-refresh) | `0.5.2` | `0.5.3` |
| [lint-staged](https://github.com/lint-staged/lint-staged) | `17.0.7` | `17.0.8` |
| [prettier](https://github.com/prettier/prettier) | `3.8.3` | `3.8.4` |
| [sharp](https://github.com/lovell/sharp) | `0.35.1` | `0.35.2` |
| [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `4.3.0` | `4.3.1` |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.8` | `4.1.9` |


Updates `@codemirror/search` from 6.7.0 to 6.7.1
- [Changelog](https://github.com/codemirror/search/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codemirror/search/commits)

Updates `@codemirror/view` from 6.43.0 to 6.43.1
- [Changelog](https://github.com/codemirror/view/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codemirror/view/commits)

Updates `@tailwindcss/vite` from 4.3.0 to 4.3.1
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.1/packages/@tailwindcss-vite)

Updates `@vitest/coverage-v8` from 4.1.8 to 4.1.9
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.9/packages/coverage-v8)

Updates `@vitest/ui` from 4.1.8 to 4.1.9
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.9/packages/ui)

Updates `eslint-plugin-react-refresh` from 0.5.2 to 0.5.3
- [Release notes](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/releases)
- [Changelog](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ArnaudBarre/eslint-plugin-react-refresh/compare/v0.5.2...v0.5.3)

Updates `lint-staged` from 17.0.7 to 17.0.8
- [Release notes](https://github.com/lint-staged/lint-staged/releases)
- [Changelog](https://github.com/lint-staged/lint-staged/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lint-staged/lint-staged/compare/v17.0.7...v17.0.8)

Updates `prettier` from 3.8.3 to 3.8.4
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.8.3...3.8.4)

Updates `sharp` from 0.35.1 to 0.35.2
- [Release notes](https://github.com/lovell/sharp/releases)
- [Commits](https://github.com/lovell/sharp/compare/v0.35.1...v0.35.2)

Updates `tailwindcss` from 4.3.0 to 4.3.1
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.1/packages/tailwindcss)

Updates `vitest` from 4.1.8 to 4.1.9
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.9/packages/vitest)

---
updated-dependencies:
- dependency-name: "@codemirror/search"
  dependency-version: 6.7.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@codemirror/view"
  dependency-version: 6.43.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@vitest/coverage-v8"
  dependency-version: 4.1.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: "@vitest/ui"
  dependency-version: 4.1.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: eslint-plugin-react-refresh
  dependency-version: 0.5.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: lint-staged
  dependency-version: 17.0.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: prettier
  dependency-version: 3.8.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: sharp
  dependency-version: 0.35.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: tailwindcss
  dependency-version: 4.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: vitest
  dependency-version: 4.1.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump nanoid in the prod-patch-updates group (#925)

Bumps the prod-patch-updates group with 1 update: [nanoid](https://github.com/ai/nanoid).


Updates `nanoid` from 5.1.11 to 5.1.15
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/5.1.11...5.1.15)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-version: 5.1.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump the major-updates group with 5 updates (#926)

Bumps the major-updates group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [js-yaml](https://github.com/nodeca/js-yaml) | `4.2.0` | `5.0.0` |
| [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) | `9.39.4` | `10.0.1` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.9.2` | `26.0.0` |
| [concurrently](https://github.com/open-cli-tools/concurrently) | `9.2.1` | `10.0.3` |
| [eslint](https://github.com/eslint/eslint) | `9.39.4` | `10.5.0` |


Updates `js-yaml` from 4.2.0 to 5.0.0
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.2.0...5.0.0)

Updates `@eslint/js` from 9.39.4 to 10.0.1
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/commits/v10.0.1/packages/js)

Updates `@types/node` from 25.9.2 to 26.0.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `concurrently` from 9.2.1 to 10.0.3
- [Release notes](https://github.com/open-cli-tools/concurrently/releases)
- [Commits](https://github.com/open-cli-tools/concurrently/compare/v9.2.1...v10.0.3)

Updates `eslint` from 9.39.4 to 10.5.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.4...v10.5.0)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: major-updates
- dependency-name: "@eslint/js"
  dependency-version: 10.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: major-updates
- dependency-name: "@types/node"
  dependency-version: 26.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: major-updates
- dependency-name: concurrently
  dependency-version: 10.0.3
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: major-updates
- dependency-name: eslint
  dependency-version: 10.5.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: major-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* feat(ssh): add HashiCorp Vault SSH signer authentication

* fix: small fixes to vault feature to align with Termix codebase

* chore: add view docs links for vault/termix id

* fix: file upload fails with 400 and missing schema migrations on upgrade (#929)

Two bugs introduced in v2.4.1:

1. uploadFileStream uses fileManagerApi.post() which triggers axios's
   transformRequest to JSON-serialize the FormData because the instance
   default Content-Type is application/json. Change to postForm() which
   sets Content-Type: multipart/form-data so the browser XHR sends the
   correct multipart body with boundary.

2. Two schema items added to schema.ts were not included in migrateSchema()
   in db/index.ts, causing 500 errors on existing installations upgrading
   from v2.4.0:
   - user_preferences.status_color_scheme (no such column)
   - dashboard_service_links table (no such table)

Fixes #928

Co-authored-by: sash <sash@fominykh.io>

* fix: support PuTTY PPK ssh keys (#930)

* fix: chunk large file manager uploads (#932)

* fix: route dashboard hosts by protocol (#934)

* fix: resolve tunnel endpoints reliably (#935)

* Fix Electron OIDC browser auth failures (#936)

* Allow RDP connections without stored credentials (#937)

* Sync role credential shares for OIDC users (#938)

* Fix terminal link dialog layering (#940)

* Confirm large files before opening editor (#942)

* Confirm closing active host connections (#943)

* Preserve file path case in file manager UI (#941)

* fix: preserve unicode guacamole tokens (#933)

* Persist VNC authentication settings (#944)

* Fix Guacamole websocket base path (#946)

* Promote file manager terminals to tabs (#939)

* Guard Guacamole disconnect during startup (#945)

* chore: increment ver

* feat: bitwarden ssh agent integration

* feat: serial connections support

* fix: various small bug fixes

* feat: open all sessions in a folder and terminal custom theme color support

* feat: cross host file manager clipboard and several small bug fixes

* feat: tailscale/wireguard support and added a new status state for when backend is checking status

* feat: grafana like server stats history, new alert system, ntfy/webhook support

* feat: new grid and widget based homepage function

* feat: new donate button in dashboard

* fix: alert ui incorrectly using termix css and fixed issue with alert system not loading

* chore: start database layer refactor

* docs: plan database layer refactor

* docs: audit database layer refactor phase zero

* chore: add database runtime adapter skeleton

* chore: add settings repository skeleton

* chore: add user session repository skeleton

* chore: add host credential repository skeleton

* chore: add field encryption boundary

* chore: migrate settings route slice

* chore: migrate user settings routes

* chore: migrate host metrics settings routes

* chore: migrate acme settings route

* chore: migrate terminal settings route

* chore: migrate tailscale settings read

* chore: migrate guacamole settings reads

* chore: migrate session timeout settings reads

* chore: migrate auth route settings reads

* chore: migrate host metrics settings reads

* chore: migrate startup settings reads

* chore: migrate user settings cleanup

* chore: migrate password reset settings

* chore: migrate oidc legacy settings read

* chore: migrate user route settings slice

* chore: migrate oidc state settings

* chore: migrate user login settings reads

* chore: migrate user crypto settings

* chore: consolidate startup settings defaults

* chore: consolidate database settings import export

* chore: migrate core session auth paths

* chore: migrate remaining session auth paths

* chore: migrate admin user routes

* chore: migrate user route admin checks

* chore: migrate user lifecycle routes

* chore: migrate auth user lookups

* chore: migrate oidc user routes

* chore: migrate api key repository paths

* docs: add database gray rollout guide

* chore: migrate trusted device paths

* chore: migrate user session route user lookups

* chore: add database repository rollout guard

* chore: expose repository rollout status

* chore: warn on repository rollout misconfiguration

* chore: migrate remaining user lookup helpers

* chore: migrate ssh user lookups

* chore: migrate user settings admin lookups

* chore: migrate acme ssl user lookups

* chore: migrate audit log admin checks

* chore: migrate oidc account user updates

* chore: migrate password reset user updates

* chore: migrate user deletion core records

* chore: migrate snippet audit user lookups

* chore: migrate ldap user sync paths

* chore: migrate totp user updates

* chore: migrate rbac user checks

* chore: migrate rbac role paths

* chore: migrate permission role lookups

* chore: migrate rbac access list reads

* chore: migrate shared rbac reads

* chore: migrate rbac access writes

* chore: migrate permission host access

* chore: migrate role host access lookup

* chore: migrate snippet access lookup

* chore: migrate shared credential access lookups

* chore: migrate host access cleanup writes

* chore: migrate host list access checks

* chore: migrate host access cleanup routes

* chore: migrate shared credential role lookups

* chore: migrate user role cleanup

* chore: migrate admin role sync

* chore: migrate ldap role sync

* chore: migrate user role assignment

* chore: migrate sso provider access

* chore: migrate audit log access

* chore: migrate user preference access

* chore: migrate open tab access

* chore: migrate dismissed alert access

* chore: migrate homepage layout access

* chore: migrate network topology access

* chore: migrate dashboard service link access

* chore: migrate command history access

* chore: migrate recent activity cleanup

* chore: migrate ssh credential usage access

* chore: migrate transfer recent access

* chore: migrate file manager bookmark access

* chore: migrate c2s tunnel preset access

* chore: migrate homepage item access

* chore: migrate session recording access

* chore: migrate tmux session tag access

* chore: migrate opkssh token access

* chore: migrate vault token access

* chore: migrate vault profile access

* chore: migrate host metrics preference access

* chore: migrate host health access

* chore: migrate host metrics history access

* chore: migrate alert persistence access

* chore: route alert host lookup through repository

* chore: migrate user data export reads

* chore: route host metrics stats sync through repository

* chore: migrate host folder persistence

* chore: migrate host resolution reads

* chore: route jump host resolution reads

* chore: route docker console jump host reads

* chore: route docker ssh resolution reads

* chore: route proxmox discovery resolution reads

* chore: route file manager activity host reads

* chore: route host metrics resolution reads

* chore: route ssh auth credential reads

* chore: route tunnel endpoint credential reads

* chore: route credential deployment resolution reads

* chore: route command history host flag reads

* chore: route snippet execution resolution reads

* chore: route terminal host resolution reads

* chore: route vault oidc host resolution reads

* chore: route wake on lan host reads

* chore: route internal host list reads

* chore: route host key verification persistence

* chore: route credential read paths

* chore: route credential host usage reads

* chore: route credential folder rename

* chore: route host owner access checks

* chore: route shared credential source reads

* chore: route user host credential cleanup

* chore: route credential delete reads

* chore: route credential update reads

* chore: route host credential reads

* chore: route host read paths

* chore: route host projection reads

* chore: route host list reads

* chore: route snippet read paths

* chore: route snippet folder writes

* chore: route snippet crud paths

* chore: route snippet bulk import

* chore: route rbac ownership reads

* chore: route user count reads

* chore: route cleanup snippets folders

* chore: route shared credential persistence

* chore: route dashboard activity

* chore: route guacamole host reads

* chore: route host bulk lookups

* chore: remove unlock-only simple db ops

* chore: route host autostart persistence

* chore: route ldap provisioning through users

* chore: route credential encrypted writes

* chore: route host encrypted writes

* chore: route bulk host encrypted writes

* chore: route termix id credentials

* chore: route termix id ca persistence

* chore: route termix identity persistence

* chore: route credential system migration

* chore: isolate user encryption migration storage

* chore: remove legacy simple db ops

* chore: isolate legacy sqlite migration copy

* chore: route database settings import export

* chore: route database host credential export

* chore: route database host credential import

* chore: route database file-manager import export

* chore: route database alert usage import export

* chore: route database user checks

* chore: isolate auth lazy migration storage

* chore: route explicit database saves

* chore: initialize database save boundary

* chore: route migration snapshot saves

* chore: isolate sqlite import constraints

* chore: route import sqlite boundary

* chore: route user encryption migration store

* chore: centralize current repository runtime

* chore: route more current repositories

* chore: route activity repository runtimes

* chore: route token repository runtimes

* chore: route health repository runtimes

* chore: route identity repository runtimes

* chore: route rbac repository runtime

* chore: centralize current sqlite runtime access

* chore: route user deletion key cleanup

* chore: route user deletion vault cleanup

* chore: route user deletion homepage cleanup

* chore: route user deletion health cleanup

* chore: route user deletion alert cleanup

* chore: route user deletion identity cleanup

* chore: add database layer preupgrade backup

* Fix database repository type errors

* fix: complete post-merge compile fixes for database refactor

Restore missing DatabaseSaveTrigger/getDb imports, session log format
fallback, OIDC provider resolution, guacamole recording insert, and
passwordFallbackOnly typing after merging current dev.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: DivByZero <mr.oplus@yahoo.fr>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: devdanetra <46488477+devdanetra@users.noreply.github.com>
Co-authored-by: Aleksandr Fominykh <neoformalex@users.noreply.github.com>
Co-authored-by: sash <sash@fominykh.io>

* refactor(db): collapse repository rollout scaffolding into single factory

Repositories are now the only data path. Replaces the 41 current-*-repository
wrapper files, the DATABASE_LAYER_REPOSITORY_ROLLOUT flag/alias map and the
unused database/runtime adapter with repositories/factory.ts, a plain
DatabaseContext type and an in-memory TestSqliteDatabase test harness.

* refactor(db): route remaining raw DB access through repositories

proxmox, session-log, oidc-utils, webauthn and guacamole recording now use
repositories (new WebauthnCredentialRepository; SsoProviderRepository
listEnabled; HostRepository findDecryptedByIdAs/listProxmoxEnabled).
Remaining raw access: db boot code, simple-db-ops and docker.ts, which are
removed/restructured in later phases.

* feat(crypto): add UserKeyManager with system-wrapped per-user DEKs

New utils/user-keys.ts: one random 32-byte DEK per user, wrapped
AES-256-GCM under an HKDF key derived from the system ENCRYPTION_KEY
(per-user info string + AAD binding, versioned v3 wrap format stored in
settings). Synchronous unwrap-on-demand with a 15-minute cache so the
existing DataCrypto facade keeps its sync call sites. Not wired up yet.

* feat(crypto): boot-time DEK migration to system-wrapped v3 format

utils/crypto-migration/dek-migration.ts carries the legacy unwrap paths
(PBKDF2 password KEK, OIDC/WebAuthn system keys, hardcoded-default
fallback) and migrates every server-unwrappable DEK to the v3 wrap at
startup. Password-wrapped DEKs migrate at next login or from a live
session via adoptRecoveredDEK. Legacy rows are kept for now; cleanup
flips on once the new path is authoritative.

* refactor(crypto): make system-wrapped DEKs the authoritative key path

DataCrypto and AuthManager now read keys through UserKeyManager: DEKs are
always unwrappable server-side, so the in-memory unlock session, DEK-in-JWT
wrapping, session-expiry data locks and ALLOW_APIKEY_DATA_UNLOCK are gone.
utils/user-crypto.ts is deleted; boot migration now cleans legacy wraps.
A one-release shim adopts DEKs from legacy dataKeyWrap tokens so active
password users migrate without re-login. Password login migrates legacy
password-wrapped DEKs via migratePasswordUserAtLogin.

* refactor(crypto): remove pending share queue and credential sharing key

With server-unwrappable DEKs both sides of a share are always available,
so the needsReEncryption queue, CREDENTIAL_SHARING_KEY and the system_*
shadow columns on ssh_credentials are gone. A one-time boot cleanup
re-creates legacy pending share copies where possible (dropping
unresolvable ones with a warning) and drops the legacy columns.

* feat(auth): non-destructive password resets and admin reset endpoint

Password resets no longer destroy user data: the DEK is system-wrapped, so
forgot-password and admin resets are just a hash update plus session revoke.
The wipe branch survives only for accounts that never logged in since the
encryption upgrade and now requires explicit confirmDataWipe (surfaced as a
409 DATA_WIPE_REQUIRED; the reset UI asks for confirmation). Adds
POST /users/admin/reset-password and removes the dead re-encryption paths.

* refactor(ssh): consolidate four jump-host chain copies into one module

terminal, host-metrics and docker now use ssh/jump-host-chain.ts (already
shared by file-manager, tmux-monitor and docker-console); docker's inline
copy also drops its raw SimpleDBOps host/credential lookups in favor of
repositories.

* refactor(ssh): single shared createConnectionLog helper

file-manager-log.ts becomes ssh/connection-log.ts; the copies in docker.ts
and host-metrics-helpers.ts are gone.

* refactor(ssh): split docker module into layered directory

ssh/docker/{index,routes,session-manager,container-routes,console}.ts:
server boot and wiring in index, HTTP handlers in routes, SSH session
registry and command execution in session-manager. Code motion only;
port 30007/30009 and endpoints unchanged. Swagger now scans ssh
subdirectories.

* refactor(ssh): split tunnel module into layered directory

ssh/tunnel/{index,routes,manager}.ts: server boot in index, HTTP handlers
in routes, tunnel state and engine (connect/retry/autostart) in manager.
Code motion only; port 30003 and endpoints unchanged.

* refactor(backend): reorganize top-level layout

- ssh/ renamed to hosts/ (it covers SSH, RDP, VNC, Telnet, Docker, metrics)
- serial/serial.ts and guacamole/ moved inside hosts/
- dashboard.ts and homepage.ts moved to services/
- swagger.ts moved to utils/ with adjusted scan globs

Import paths and the generate:openapi script updated; ports and endpoints
unchanged.

* refactor(tests): move backend tests into src/backend/tests mirror tree

Backend *.test.ts files (and the test-support harness) no longer sit next
to source files; they live under src/backend/tests/ mirroring the source
layout. Imports rewritten accordingly; CLAUDE.md convention updated.

* refactor(hosts): group host modules into per-feature directories

file-manager/, metrics/ (incl. widgets, managers, alert-engine),
terminal/, tmux/ and tunnel/ each own their files; docker/ gains
container-runtime. Genuinely shared helpers (jump-host chain, host
resolver, connection pool, opkssh, vault, serial) stay at hosts/ root.
Pure file moves with import path updates; mirrored test paths follow.

* refactor(backend): final cleanup pass

- re-register WebAuthn passkey routes (registration was dropped in the
  #1054 merge, breaking passkey login) and document all six endpoints
- delete utils/simple-db-ops.ts (last caller migrated to DataCrypto)
- starter: use the typed serverReady export, collapse the four-way
  version lookup to env then package.json candidates
- add OpenAPI JSDoc to c2s-tunnel-presets endpoints
- strip block-divider comment banners

* feat: remove legacy "data_unlocked" field

* feat: refactor rbac/sharing to support new permissions and auth types

* feat: refactor rbac/sharing to support new permissions and auth types

* feat: add "id" to user profile hide list

* chore: root cleanup

* feat: add more donation references and a 30-day donation reminder

* chore: update readme

* feat: automate beta tests

* feat: add links to milestones

* fix: hoist github/google SSO defaults to module scope (#1065)

* fix(ssh-tools): allow clipboard paste in key recording field (#1066)

The broadcast key-recording input was marked readOnly, which makes
browsers block paste entirely (no context-menu Paste, Ctrl+V does
nothing). handleKeyDown also called preventDefault() unconditionally,
swallowing the Ctrl+V shortcut before a paste event could even fire.

Let Ctrl/Cmd+V pass through in handleKeyDown, drop readOnly, and add
an onPaste handler that reads the clipboard text and broadcasts it to
the selected terminals like any other captured keystroke.

Signed-off-by: emreumar <emreumar@users.noreply.github.com>
Co-authored-by: emreumar <emreumar@users.noreply.github.com>

* chore: write release notes

* chore: update release notes

* chore: update readmes

* chore: add crypto only reminder in en.json

* fix: macOS and cask errors on release workflow

* chore: sync Crowdin translations for 2.5.1

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: emreumar <emreumar@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com>
Co-authored-by: Russell Poovey <09.our_seekers@icloud.com>
Co-authored-by: russell <git@0896c69e.com>
Co-authored-by: Subedi Bibek <77529535+questbibek@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Alexander Elsner <101340634+Bensonheimer992@users.noreply.github.com>
Co-authored-by: SankeerthNara <sankeerthnara@gmail.com>
Co-authored-by: Stephan Groth <96803994+Kalvalax@users.noreply.github.com>
Co-authored-by: DivByZero <mr.oplus@yahoo.fr>
Co-authored-by: devdanetra <46488477+devdanetra@users.noreply.github.com>
Co-authored-by: Aleksandr Fominykh <neoformalex@users.noreply.github.com>
Co-authored-by: sash <sash@fominykh.io>
Co-authored-by: lhojun <ldgs3324@gmail.com>
Co-authored-by: Yunus Emre Umar <77045015+emre155@users.noreply.github.com>
Co-authored-by: emreumar <emreumar@users.noreply.github.com>
This commit is contained in:
Luke Gustafson
2026-07-19 12:29:52 -05:00
committed by GitHub
co-authored by emreumar dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ZacharyZcR Russell Poovey russell Subedi Bibek Claude Fable 5 Alexander Elsner SankeerthNara Stephan Groth DivByZero devdanetra Aleksandr Fominykh sash lhojun Yunus Emre Umar
parent fba645e92e
commit ddbdd5c437
562 changed files with 52303 additions and 23645 deletions
+342 -209
View File
@@ -6,30 +6,101 @@ import { Separator } from "@/components/separator";
import { Button } from "@/components/button";
import { Sheet, SheetContent } from "@/components/sheet";
import { ChevronLeft, ChevronRight, Maximize2 } from "lucide-react";
import { useState, useRef, useCallback, useEffect, createRef } from "react";
import {
useState,
useRef,
useCallback,
useEffect,
createRef,
lazy,
Suspense,
} from "react";
import { createPortal } from "react-dom";
import { useIsMobile } from "@/hooks/use-mobile";
import { MobileBottomBar } from "@/shell/MobileBottomBar";
import { CommandPalette } from "@/shell/CommandPalette";
import { AppRail } from "@/sidebar/AppRail";
import type { RailView } from "@/sidebar/AppRail";
import { HostsPanel } from "@/sidebar/HostsPanel";
import { QuickConnectPanel } from "@/sidebar/QuickConnectPanel";
import { SerialPanel } from "@/sidebar/SerialPanel";
import { SshToolsPanel } from "@/sidebar/SshToolsPanel";
import { SnippetsPanel } from "@/sidebar/SnippetsPanel";
import { HistoryPanel } from "@/sidebar/HistoryPanel";
import { SessionLogsPanel } from "@/sidebar/SessionLogsPanel";
import { SplitScreenPanel } from "@/sidebar/SplitScreenPanel";
import { UserProfilePanel } from "@/sidebar/UserProfilePanel";
import { AdminSettingsPanel } from "@/sidebar/AdminSettingsPanel";
import { AlertsPanel } from "@/sidebar/AlertsPanel";
import { CredentialsPanel } from "@/sidebar/CredentialsPanel";
import { TermixIdPanel } from "@/sidebar/TermixIdPanel";
import { SplitView } from "@/shell/SplitView";
import { renderTabContent } from "@/shell/tabUtils";
import { AlertManager } from "@/dashboard/panels/alerts/AlertManager";
import { TabBar } from "@/shell/TabBar";
// Shell surfaces that are not needed for first paint.
const CommandPalette = lazy(() =>
import("@/shell/CommandPalette").then((m) => ({
default: m.CommandPalette,
})),
);
const HostsPanel = lazy(() =>
import("@/sidebar/HostsPanel").then((m) => ({ default: m.HostsPanel })),
);
const QuickConnectPanel = lazy(() =>
import("@/sidebar/QuickConnectPanel").then((m) => ({
default: m.QuickConnectPanel,
})),
);
const SerialPanel = lazy(() =>
import("@/sidebar/SerialPanel").then((m) => ({ default: m.SerialPanel })),
);
const SplitScreenPanel = lazy(() =>
import("@/sidebar/SplitScreenPanel").then((m) => ({
default: m.SplitScreenPanel,
})),
);
const AlertManager = lazy(() =>
import("@/dashboard/panels/alerts/AlertManager").then((m) => ({
default: m.AlertManager,
})),
);
// Secondary rail panels — load on first open, not with the shell critical path.
const SshToolsPanel = lazy(() =>
import("@/sidebar/SshToolsPanel").then((m) => ({ default: m.SshToolsPanel })),
);
const SnippetsPanel = lazy(() =>
import("@/sidebar/SnippetsPanel").then((m) => ({ default: m.SnippetsPanel })),
);
const HistoryPanel = lazy(() =>
import("@/sidebar/HistoryPanel").then((m) => ({ default: m.HistoryPanel })),
);
const SessionLogsPanel = lazy(() =>
import("@/sidebar/SessionLogsPanel").then((m) => ({
default: m.SessionLogsPanel,
})),
);
const UserProfilePanel = lazy(() =>
import("@/sidebar/UserProfilePanel").then((m) => ({
default: m.UserProfilePanel,
})),
);
const AdminSettingsPanel = lazy(() =>
import("@/sidebar/AdminSettingsPanel").then((m) => ({
default: m.AdminSettingsPanel,
})),
);
const AlertsPanel = lazy(() =>
import("@/sidebar/AlertsPanel").then((m) => ({ default: m.AlertsPanel })),
);
const CredentialsPanel = lazy(() =>
import("@/sidebar/CredentialsPanel").then((m) => ({
default: m.CredentialsPanel,
})),
);
const TermixIdPanel = lazy(() =>
import("@/sidebar/TermixIdPanel").then((m) => ({ default: m.TermixIdPanel })),
);
const ConnectionsPanel = lazy(() =>
import("@/sidebar/ConnectionsPanel").then((m) => ({
default: m.ConnectionsPanel,
})),
);
function SidebarPanelFallback() {
return (
<div className="flex flex-1 items-center justify-center p-6">
<div className="size-5 rounded-full border-2 border-muted-foreground/30 border-t-muted-foreground/70 animate-spin" />
</div>
);
}
import type {
Tab,
TabType,
@@ -51,18 +122,22 @@ import {
addOpenTab,
deleteOpenTab,
patchOpenTab,
createSSHHost,
getActiveSessions,
getUserPreferences,
dismissDonationModal,
type UserPreferences,
type OpenTabRecord,
} from "@/main-axios";
import { DonationReminderModal } from "@/user/DonationReminderModal.tsx";
import { dbHealthMonitor } from "@/lib/db-health-monitor";
import type { SSHHostWithStatus } from "@/main-axios";
import { ServerStatusProvider } from "@/lib/ServerStatusContext";
import { ConnectionsPanel } from "@/sidebar/ConnectionsPanel";
import { TransferMonitor } from "@/features/file-manager/TransferMonitor.tsx";
import { sshHostToHost } from "@/sidebar/HostManagerData";
import { resolveHostTabType } from "@/lib/host-connection-tabs";
import { changeAppLanguage } from "@/i18n/i18n";
import { quickConnectHostToPayload } from "@/sidebar/quick-connect-host";
function buildHostTree(
hosts: SSHHostWithStatus[],
@@ -157,6 +232,7 @@ export function AppShell({
const [allHosts, setAllHosts] = useState<Host[]>([]);
const [isAdmin, setIsAdmin] = useState(false);
const [userId, setUserId] = useState<string | null>(null);
const [showDonationModal, setShowDonationModal] = useState(false);
const [backgroundTabRecords, setBackgroundTabRecords] = useState<
OpenTabRecord[]
>([]);
@@ -202,10 +278,16 @@ export function AppShell({
.then((info) => {
setIsAdmin(info.is_admin);
setUserId(info.userId);
setShowDonationModal(!!info.show_donation_modal);
})
.catch(() => setIsAdmin(false));
}, []);
const handleDismissDonationModal = useCallback(() => {
setShowDonationModal(false);
dismissDonationModal().catch(() => {});
}, []);
const toggleAppFullscreen = useCallback(async () => {
try {
if (document.fullscreenElement) {
@@ -618,8 +700,7 @@ export function AppShell({
applyAccentColor(prefs.accentColor);
}
if (prefs.language && prefs.language !== i18n.language) {
localStorage.setItem("i18nextLng", prefs.language);
void i18n.changeLanguage(prefs.language);
void changeAppLanguage(prefs.language);
}
if (
prefs.commandAutocomplete !== null &&
@@ -738,8 +819,17 @@ export function AppShell({
}, [loadHosts]);
useEffect(() => {
window.addEventListener("termix:hosts-changed", loadHosts);
return () => window.removeEventListener("termix:hosts-changed", loadHosts);
const onHostsChanged = () => {
void loadHosts();
};
window.addEventListener("termix:hosts-changed", onHostsChanged);
window.addEventListener("ssh-hosts:changed", onHostsChanged);
window.addEventListener("hosts:refresh", onHostsChanged);
return () => {
window.removeEventListener("termix:hosts-changed", onHostsChanged);
window.removeEventListener("ssh-hosts:changed", onHostsChanged);
window.removeEventListener("hosts:refresh", onHostsChanged);
};
}, [loadHosts]);
// Sync tab host data when allHosts updates (e.g. after editing terminal theme in host settings)
@@ -1013,6 +1103,21 @@ export function AppShell({
openTab(host, type);
}
const saveQuickConnectHost = useCallback(
async (tab: Tab, host: Host) => {
try {
const savedHost = await createSSHHost(quickConnectHostToPayload(host));
await patchOpenTab(tab.instanceId, { hostId: savedHost.id });
await loadHosts();
toast.success(t("hosts.hostCreated"));
} catch (error) {
toast.error(t("hosts.failedToSave"));
throw error;
}
},
[loadHosts, t],
);
function openSerialTab(config: SerialConfig) {
const pseudoHost: Host = {
id: `serial-${Date.now()}`,
@@ -1437,169 +1542,182 @@ export function AppShell({
// Sidebar panel content — shared between desktop inline sidebar and mobile sheet
const sidebarPanelContent = (
<div className="flex flex-col flex-1 min-h-0 overflow-hidden">
<div
className={`flex flex-col flex-1 min-h-0 ${railView === "hosts" ? "" : "hidden"}`}
>
<HostsPanel
onOpenTab={(host, type) => {
connectHost(host, type);
if (isMobile) setSidebarOpen(false);
}}
onEditHost={editHostInManager}
hostTree={realHostTree ?? undefined}
loading={hostsLoading}
onEditingChange={setSidebarEditing}
active={railView === "hosts"}
/>
</div>
<div
className={`flex flex-col flex-1 min-h-0 ${railView === "credentials" ? "" : "hidden"}`}
>
<CredentialsPanel
onEditingChange={setSidebarEditing}
active={railView === "credentials"}
/>
</div>
{railView === "termix-id" && (
<div className="flex flex-col flex-1 min-h-0">
<TermixIdPanel />
</div>
)}
{railView === "serial" && (
<SerialPanel
onConnect={(config) => {
openSerialTab(config);
if (isMobile) setSidebarOpen(false);
}}
/>
)}
{railView === "quick-connect" && (
<QuickConnectPanel
onConnect={(host, type) => {
openTab(host, type);
if (isMobile) setSidebarOpen(false);
}}
/>
)}
{railView === "ssh-tools" && (
<div className="flex-1 min-h-0 overflow-y-auto">
<SshToolsPanel
terminalTabs={terminalTabs}
activeTabId={activeTabId}
/>
</div>
)}
{railView === "snippets" && (
<div className="flex-1 min-h-0 overflow-y-auto">
<SnippetsPanel
terminalTabs={terminalTabs}
activeTabId={activeTabId}
/>
</div>
)}
{railView === "history" && (
<div className="flex flex-col flex-1 min-h-0 overflow-y-auto">
<HistoryPanel terminalTabs={terminalTabs} activeTabId={activeTabId} />
</div>
)}
{railView === "split-screen" && (
<div className="flex-1 min-h-0 overflow-y-auto">
<SplitScreenPanel
tabs={tabs}
splitMode={splitMode}
setSplitMode={setSplitMode}
paneTabIds={paneTabIds}
setPaneTabIds={setPaneTabIds}
onAssignPane={assignPane}
/>
</div>
)}
{railView === "connections" && (
<div className="flex-1 min-h-0 overflow-y-auto">
<ConnectionsPanel
tabs={tabs}
activeTabId={activeTabId}
allHosts={allHosts}
backgroundTabRecords={backgroundTabRecords}
onSwitchToTab={(tabId) => {
setActiveTabId(tabId);
<Suspense fallback={<SidebarPanelFallback />}>
<div className="flex flex-col flex-1 min-h-0 overflow-hidden">
<div
className={`flex flex-col flex-1 min-h-0 ${railView === "hosts" ? "" : "hidden"}`}
>
<HostsPanel
onOpenTab={(host, type) => {
connectHost(host, type);
if (isMobile) setSidebarOpen(false);
}}
onCloseTab={closeTab}
onReopenTab={(record, restoredSessionId) => {
const host = record.hostId
? allHosts.find((h) => h.id === String(record.hostId))
: undefined;
const hostlessTypes: TabType[] = ["tunnel"];
if (!host && !hostlessTypes.includes(record.tabType as TabType))
return;
setBackgroundTabRecords((prev) =>
prev.filter((r) => r.id !== record.id),
);
if (host) {
const effectiveSessionId =
restoredSessionId ?? record.backendSessionId ?? null;
openTab(host, record.tabType as TabType, {
instanceId: record.id,
restoredSessionId: effectiveSessionId,
savedLabel: record.label,
});
} else {
openSingletonTab(record.tabType as TabType);
onEditHost={editHostInManager}
hostTree={realHostTree ?? undefined}
loading={hostsLoading}
onEditingChange={setSidebarEditing}
active={railView === "hosts"}
/>
</div>
<div
className={`flex flex-col flex-1 min-h-0 ${railView === "credentials" ? "" : "hidden"}`}
>
<CredentialsPanel
onEditingChange={setSidebarEditing}
active={railView === "credentials"}
/>
</div>
{railView === "termix-id" && (
<div className="flex flex-col flex-1 min-h-0">
<TermixIdPanel />
</div>
)}
{railView === "serial" && (
<SerialPanel
onConnect={(config) => {
openSerialTab(config);
if (isMobile) setSidebarOpen(false);
}}
/>
)}
{railView === "quick-connect" && (
<QuickConnectPanel
onConnect={(host, type) => {
openTab(host, type);
if (isMobile) setSidebarOpen(false);
}}
/>
)}
{railView === "ssh-tools" && (
<div className="flex-1 min-h-0 overflow-y-auto">
<SshToolsPanel
terminalTabs={terminalTabs}
activeTabId={activeTabId}
/>
</div>
)}
{railView === "snippets" && (
<div className="flex-1 min-h-0 overflow-y-auto">
<SnippetsPanel
terminalTabs={terminalTabs}
activeTabId={activeTabId}
/>
</div>
)}
{railView === "history" && (
<div className="flex flex-col flex-1 min-h-0 overflow-y-auto">
<HistoryPanel
terminalTabs={terminalTabs}
activeTabId={activeTabId}
/>
</div>
)}
{railView === "split-screen" && (
<div className="flex-1 min-h-0 overflow-y-auto">
<SplitScreenPanel
tabs={tabs}
splitMode={splitMode}
setSplitMode={setSplitMode}
paneTabIds={paneTabIds}
setPaneTabIds={setPaneTabIds}
onAssignPane={assignPane}
/>
</div>
)}
{railView === "connections" && (
<div className="flex-1 min-h-0 overflow-y-auto">
<ConnectionsPanel
tabs={tabs}
activeTabId={activeTabId}
allHosts={allHosts}
backgroundTabRecords={backgroundTabRecords}
onSwitchToTab={(tabId) => {
setActiveTabId(tabId);
if (isMobile) setSidebarOpen(false);
}}
onCloseTab={closeTab}
onReopenTab={(record, restoredSessionId) => {
const host = record.hostId
? allHosts.find((h) => h.id === String(record.hostId))
: undefined;
const hostlessTypes: TabType[] = ["tunnel"];
if (!host && !hostlessTypes.includes(record.tabType as TabType))
return;
setBackgroundTabRecords((prev) =>
prev.filter((r) => r.id !== record.id),
);
if (host) {
const effectiveSessionId =
restoredSessionId ?? record.backendSessionId ?? null;
openTab(host, record.tabType as TabType, {
instanceId: record.id,
restoredSessionId: effectiveSessionId,
savedLabel: record.label,
});
} else {
openSingletonTab(record.tabType as TabType);
}
if (isMobile) setSidebarOpen(false);
}}
onForgetBackground={(recordId) => {
setBackgroundTabRecords((prev) =>
prev.filter((r) => r.id !== recordId),
);
}}
onRenameTab={renameTab}
onReorderTabs={setTabs}
/>
</div>
)}
{railView === "session-logs" && (
<div className="relative flex-1 min-h-0 flex flex-col">
<SessionLogsPanel />
</div>
)}
{railView === "user-profile" && (
<div className="flex-1 min-h-0 overflow-y-auto">
<UserProfilePanel
username={username}
onLogout={onLogout}
onChangeServer={onChangeServer}
userPrefs={userPrefs}
onPrefsChange={(updates) =>
setUserPrefs((current) => ({ ...current, ...updates }))
}
if (isMobile) setSidebarOpen(false);
}}
onForgetBackground={(recordId) => {
setBackgroundTabRecords((prev) =>
prev.filter((r) => r.id !== recordId),
);
}}
onRenameTab={renameTab}
onReorderTabs={setTabs}
/>
</div>
)}
/>
</div>
)}
{railView === "session-logs" && (
<div className="relative flex-1 min-h-0 flex flex-col">
<SessionLogsPanel />
</div>
)}
{railView === "admin-settings" && isAdmin && (
<div className="flex flex-col flex-1 min-h-0 overflow-y-auto">
<AdminSettingsPanel
onEditingChange={setSidebarEditing}
onOpenHostTab={(host) => {
connectHost(host);
if (isMobile) setSidebarOpen(false);
}}
/>
</div>
)}
{railView === "user-profile" && (
<div className="flex-1 min-h-0 overflow-y-auto">
<UserProfilePanel
username={username}
onLogout={onLogout}
onChangeServer={onChangeServer}
userPrefs={userPrefs}
onPrefsChange={setUserPrefs}
/>
</div>
)}
{railView === "admin-settings" && isAdmin && (
<div className="flex-1 min-h-0 overflow-y-auto">
<AdminSettingsPanel />
</div>
)}
{railView === "alerts" && (
<div className="flex-1 min-h-0 overflow-hidden flex flex-col">
<AlertsPanel />
</div>
)}
</div>
{railView === "alerts" && (
<div className="flex-1 min-h-0 overflow-hidden flex flex-col">
<AlertsPanel />
</div>
)}
</div>
</Suspense>
);
// Sidebar header — shared
@@ -1652,7 +1770,7 @@ export function AppShell({
{/* Desktop: inline resizable sidebar */}
{!isMobile && (
<div
className={`relative flex flex-col bg-sidebar shrink-0 overflow-hidden ${sidebarOpen ? `border-r transition-colors ${sidebarDragging ? "border-accent-brand/60" : "border-border"}` : ""}`}
className={`relative flex flex-col min-h-0 bg-sidebar shrink-0 overflow-hidden ${sidebarOpen ? `border-r transition-colors ${sidebarDragging ? "border-accent-brand/60" : "border-border"}` : ""}`}
style={{
width: sidebarOpen ? (sidebarEditing ? 560 : sidebarWidth) : 0,
transition: sidebarDragging ? "none" : "width 0.2s",
@@ -1676,7 +1794,7 @@ export function AppShell({
<SheetContent
side="left"
showCloseButton={false}
className="p-0 flex flex-col w-[min(85vw,360px)] max-w-full bg-sidebar border-r border-border gap-0"
className="p-0 flex flex-col min-h-0 w-[min(85vw,360px)] max-w-full bg-sidebar border-r border-border gap-0"
style={{ height: "100dvh" }}
>
{sidebarHeader}
@@ -1713,6 +1831,10 @@ export function AppShell({
onAddToSplit={addTabToSplit}
onRemoveFromSplit={removeTabFromSplit}
onRenameTab={renameTab}
onOpenFileManager={(tabId) => {
const targetTab = tabs.find((t) => t.id === tabId);
if (targetTab?.host) openTab(targetTab.host, "files");
}}
isAppFullscreen={isAppFullscreen}
onToggleAppFullscreen={toggleAppFullscreen}
/>
@@ -1789,6 +1911,7 @@ export function AppShell({
initialFilePath: path,
}),
renameTab,
saveQuickConnectHost,
),
tabNode,
tab.id,
@@ -1808,35 +1931,45 @@ export function AppShell({
</div>
</div>
<CommandPalette
isOpen={commandPaletteOpen}
setIsOpen={setCommandPaletteOpen}
hosts={allHosts}
onOpenTab={(type, label, pendingEvent) => {
if (
[
"dashboard",
"host-manager",
"user-profile",
"admin-settings",
].includes(type)
) {
openSingletonTab(type, pendingEvent);
} else if (type === "tmux_monitor") {
// --- tmux-monitor --- singleton tab, optionally preselecting a host
openSingletonTab(
type,
undefined,
label ? allHosts.find((h) => h.name === label) : undefined,
);
} else if (label) {
const host = allHosts.find((h) => h.name === label);
if (host) openTab(host, type);
}
}}
/>
{commandPaletteOpen && (
<Suspense fallback={null}>
<CommandPalette
isOpen={commandPaletteOpen}
setIsOpen={setCommandPaletteOpen}
hosts={allHosts}
onOpenTab={(type, label, pendingEvent) => {
if (
[
"dashboard",
"host-manager",
"user-profile",
"admin-settings",
].includes(type)
) {
openSingletonTab(type, pendingEvent);
} else if (type === "tmux_monitor") {
// --- tmux-monitor --- singleton tab, optionally preselecting a host
openSingletonTab(
type,
undefined,
label ? allHosts.find((h) => h.name === label) : undefined,
);
} else if (label) {
const host = allHosts.find((h) => h.name === label);
if (host) openTab(host, type);
}
}}
/>
</Suspense>
)}
<TransferMonitor />
<AlertManager userId={userId} loggedIn={!!username} />
<Suspense fallback={null}>
<AlertManager userId={userId} loggedIn={!!username} />
</Suspense>
<DonationReminderModal
open={showDonationModal}
onDismiss={handleDismissDonationModal}
/>
</ServerStatusProvider>
);
}
+241
View File
@@ -0,0 +1,241 @@
import { authApi, handleApiError, sshHostApi } from "@/main-axios";
import type { SSHHost, SSHHostData } from "@/types/index";
// ADMIN USER DATA MANAGEMENT
// ============================================================================
// Wrappers over the regular data-plane endpoints that act on another user's
// data via the X-Admin-Target-User header (admin only, audited server-side).
// They intentionally bypass the host request caches: the data belongs to the
// target user, not the signed-in admin.
const ADMIN_TARGET_USER_HEADER = "X-Admin-Target-User";
function adminHeaders(targetUserId: string): Record<string, string> {
return { [ADMIN_TARGET_USER_HEADER]: targetUserId };
}
export async function adminGetUserHosts(
targetUserId: string,
): Promise<SSHHost[]> {
try {
const response = await sshHostApi.get("/db/host", {
headers: adminHeaders(targetUserId),
});
return Array.isArray(response.data) ? response.data : [];
} catch (error) {
throw handleApiError(error, "fetch user's hosts");
}
}
export async function adminCreateUserHost(
targetUserId: string,
hostData: SSHHostData,
): Promise<SSHHost> {
try {
if (hostData.authType === "key" && hostData.key instanceof File) {
const formData = new FormData();
formData.append("key", hostData.key);
const dataWithoutFile = { ...hostData, key: undefined };
formData.append("data", JSON.stringify(dataWithoutFile));
const response = await sshHostApi.post("/db/host", formData, {
headers: {
"Content-Type": "multipart/form-data",
...adminHeaders(targetUserId),
},
});
return response.data;
}
const response = await sshHostApi.post("/db/host", hostData, {
headers: adminHeaders(targetUserId),
});
return response.data;
} catch (error) {
throw handleApiError(error, "create host for user");
}
}
export async function adminUpdateUserHost(
targetUserId: string,
hostId: number,
hostData: SSHHostData,
): Promise<SSHHost> {
try {
if (hostData.authType === "key" && hostData.key instanceof File) {
const formData = new FormData();
formData.append("key", hostData.key);
const dataWithoutFile = { ...hostData, key: undefined };
formData.append("data", JSON.stringify(dataWithoutFile));
const response = await sshHostApi.put(`/db/host/${hostId}`, formData, {
headers: {
"Content-Type": "multipart/form-data",
...adminHeaders(targetUserId),
},
});
return response.data;
}
const response = await sshHostApi.put(`/db/host/${hostId}`, hostData, {
headers: adminHeaders(targetUserId),
});
return response.data;
} catch (error) {
throw handleApiError(error, "update user's host");
}
}
export async function adminDeleteUserHost(
targetUserId: string,
hostId: number,
): Promise<Record<string, unknown>> {
try {
const response = await sshHostApi.delete(`/db/host/${hostId}`, {
headers: adminHeaders(targetUserId),
});
return response.data;
} catch (error) {
throw handleApiError(error, "delete user's host");
}
}
export async function adminGetHostPassword(
targetUserId: string,
hostId: number,
field: "password" | "sudoPassword" | "vncPassword" = "password",
): Promise<string | null> {
try {
const response = await sshHostApi.get(
`/db/host/${hostId}/password?field=${field}`,
{ headers: adminHeaders(targetUserId) },
);
return response.data?.value || null;
} catch {
return null;
}
}
export async function adminGetUserCredentials(
targetUserId: string,
): Promise<Record<string, unknown>[] | Record<string, unknown>> {
try {
const response = await authApi.get("/credentials", {
headers: adminHeaders(targetUserId),
});
return response.data;
} catch (error) {
throw handleApiError(error, "fetch user's credentials");
}
}
export async function adminGetUserCredentialDetails(
targetUserId: string,
credentialId: number,
): Promise<Record<string, unknown>> {
try {
const response = await authApi.get(`/credentials/${credentialId}`, {
headers: adminHeaders(targetUserId),
});
return response.data;
} catch (error) {
throw handleApiError(error, "fetch user's credential details");
}
}
export async function adminCreateUserCredential(
targetUserId: string,
credentialData: Record<string, unknown>,
): Promise<Record<string, unknown>> {
try {
const response = await authApi.post("/credentials", credentialData, {
headers: adminHeaders(targetUserId),
});
return response.data;
} catch (error) {
throw handleApiError(error, "create credential for user");
}
}
export async function adminUpdateUserCredential(
targetUserId: string,
credentialId: number,
credentialData: Record<string, unknown>,
): Promise<Record<string, unknown>> {
try {
const response = await authApi.put(
`/credentials/${credentialId}`,
credentialData,
{ headers: adminHeaders(targetUserId) },
);
return response.data;
} catch (error) {
throw handleApiError(error, "update user's credential");
}
}
export async function adminDeleteUserCredential(
targetUserId: string,
credentialId: number,
): Promise<Record<string, unknown>> {
try {
const response = await authApi.delete(`/credentials/${credentialId}`, {
headers: adminHeaders(targetUserId),
});
return response.data;
} catch (error) {
throw handleApiError(error, "delete user's credential");
}
}
export async function adminGetUserSnippets(
targetUserId: string,
): Promise<Record<string, unknown>> {
try {
const response = await authApi.get("/snippets", {
headers: adminHeaders(targetUserId),
});
return response.data;
} catch (error) {
throw handleApiError(error, "fetch user's snippets");
}
}
export async function adminCreateUserSnippet(
targetUserId: string,
snippetData: Record<string, unknown>,
): Promise<Record<string, unknown>> {
try {
const response = await authApi.post("/snippets", snippetData, {
headers: adminHeaders(targetUserId),
});
return response.data;
} catch (error) {
throw handleApiError(error, "create snippet for user");
}
}
export async function adminUpdateUserSnippet(
targetUserId: string,
snippetId: number,
snippetData: Record<string, unknown>,
): Promise<Record<string, unknown>> {
try {
const response = await authApi.put(`/snippets/${snippetId}`, snippetData, {
headers: adminHeaders(targetUserId),
});
return response.data;
} catch (error) {
throw handleApiError(error, "update user's snippet");
}
}
export async function adminDeleteUserSnippet(
targetUserId: string,
snippetId: number,
): Promise<Record<string, unknown>> {
try {
const response = await authApi.delete(`/snippets/${snippetId}`, {
headers: adminHeaders(targetUserId),
});
return response.data;
} catch (error) {
throw handleApiError(error, "delete user's snippet");
}
}
+46 -1
View File
@@ -40,6 +40,28 @@ export interface AlertFiring {
ruleName?: string;
}
function stringValue(value: unknown, fallback = ""): string {
return typeof value === "string" ? value : fallback;
}
function numberValue(value: unknown, fallback = 0): number {
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
}
function nullableNumberValue(value: unknown): number | null {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
function boolValue(value: unknown): boolean {
return value === true || value === 1 || value === "1";
}
function severityValue(value: unknown): AlertFiring["severity"] {
return value === "info" || value === "critical" || value === "warning"
? value
: "warning";
}
export async function getNotificationChannels(): Promise<
NotificationChannel[]
> {
@@ -91,6 +113,23 @@ function mapRule(r: Record<string, unknown>): AlertRule {
};
}
export function mapAlertFiring(r: Record<string, unknown>): AlertFiring {
return {
id: numberValue(r.id),
userId: stringValue(r.userId ?? r.user_id),
ruleId: numberValue(r.ruleId ?? r.rule_id),
hostId: numberValue(r.hostId ?? r.host_id),
hostName: stringValue(r.hostName ?? r.host_name),
firedAt: stringValue(r.firedAt ?? r.fired_at, new Date(0).toISOString()),
resolvedAt: stringValue(r.resolvedAt ?? r.resolved_at) || null,
value: nullableNumberValue(r.value),
message: stringValue(r.message),
severity: severityValue(r.severity),
acknowledged: boolValue(r.acknowledged),
ruleName: stringValue(r.ruleName ?? r.rule_name) || undefined,
};
}
export async function getAlertRules(): Promise<AlertRule[]> {
const res = await rbacApi.get("/alert-rules");
return (res.data as Record<string, unknown>[]).map(mapRule);
@@ -121,7 +160,13 @@ export async function getAlertFirings(opts?: {
acknowledged?: boolean;
}): Promise<AlertFiring[]> {
const res = await rbacApi.get("/alert-firings", { params: opts });
return (res.data as { firings: AlertFiring[] }).firings ?? res.data;
const data = res.data as { firings?: unknown } | unknown[];
const rows = Array.isArray(data)
? data
: Array.isArray(data.firings)
? data.firings
: [];
return rows.map((row) => mapAlertFiring(row as Record<string, unknown>));
}
export async function acknowledgeAlertFiring(id: number): Promise<void> {
+42 -1
View File
@@ -34,6 +34,40 @@ export interface AuditLogResponse {
totalPages: number;
}
function stringValue(value: unknown, fallback = ""): string {
return typeof value === "string" ? value : fallback;
}
function numberValue(value: unknown, fallback = 0): number {
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
}
function nullableStringValue(value: unknown): string | null {
return typeof value === "string" && value ? value : null;
}
function boolValue(value: unknown): boolean {
return value === true || value === 1 || value === "1";
}
export function mapAuditLog(row: Record<string, unknown>): AuditLog {
return {
id: numberValue(row.id),
userId: stringValue(row.userId ?? row.user_id),
username: stringValue(row.username),
action: stringValue(row.action, "unknown"),
resourceType: stringValue(row.resourceType ?? row.resource_type, "unknown"),
resourceId: nullableStringValue(row.resourceId ?? row.resource_id),
resourceName: nullableStringValue(row.resourceName ?? row.resource_name),
details: nullableStringValue(row.details),
ipAddress: nullableStringValue(row.ipAddress ?? row.ip_address),
userAgent: nullableStringValue(row.userAgent ?? row.user_agent),
success: boolValue(row.success),
errorMessage: nullableStringValue(row.errorMessage ?? row.error_message),
timestamp: stringValue(row.timestamp, new Date(0).toISOString()),
};
}
export async function getAuditLogs(
filters: AuditLogFilters = {},
): Promise<AuditLogResponse> {
@@ -50,7 +84,14 @@ export async function getAuditLogs(
if (filters.endDate) params.set("endDate", filters.endDate);
const response = await authApi.get(`/audit-logs?${params.toString()}`);
return response.data;
const data = response.data as Partial<AuditLogResponse>;
const rows = Array.isArray(data.logs) ? data.logs : [];
return {
logs: rows.map((row) => mapAuditLog(row as Record<string, unknown>)),
total: numberValue(data.total),
page: numberValue(data.page, filters.page ?? 1),
totalPages: numberValue(data.totalPages, 1),
};
} catch (error) {
handleApiError(error, "fetch audit logs");
}
+27 -23
View File
@@ -1,6 +1,7 @@
import axios, { type AxiosRequestConfig } from "axios";
import { handleApiError, statsApi } from "@/main-axios";
import type { ServerMetrics, ServerStatus } from "@/main-axios";
import { getCachedServerStatuses } from "@/lib/hosts-request-cache";
type ApiConnectionLog = {
type: "info" | "success" | "warning" | "error";
@@ -73,34 +74,37 @@ function isTransientStatusError(error: unknown): boolean {
export async function getAllServerStatuses(): Promise<
Record<number, ServerStatus>
> {
let lastError: unknown = null;
return getCachedServerStatuses(async () => {
let lastError: unknown = null;
for (let i = 0; i < STATUS_RETRY_SCHEDULE.length; i++) {
const { timeoutMs, pauseAfterMs } = STATUS_RETRY_SCHEDULE[i];
const isFinalAttempt = i === STATUS_RETRY_SCHEDULE.length - 1;
for (let i = 0; i < STATUS_RETRY_SCHEDULE.length; i++) {
const { timeoutMs, pauseAfterMs } = STATUS_RETRY_SCHEDULE[i];
const isFinalAttempt = i === STATUS_RETRY_SCHEDULE.length - 1;
try {
const response = await statsApi.get("/status", {
timeout: timeoutMs,
// Silence per-attempt interceptor logging & health-monitor side
// effects on all attempts except the final one, so background
// blips don't look like real outages.
__silentRetry: !isFinalAttempt,
} as AxiosRequestConfig & { __silentRetry?: boolean });
return response.data || {};
} catch (error) {
lastError = error;
if (!isTransientStatusError(error)) {
break;
try {
const response = await statsApi.get("/status", {
timeout: timeoutMs,
// Silence per-attempt interceptor logging & health-monitor side
// effects on all attempts except the final one, so background
// blips don't look like real outages.
__silentRetry: !isFinalAttempt,
} as AxiosRequestConfig & { __silentRetry?: boolean });
return response.data || {};
} catch (error) {
lastError = error;
if (!isTransientStatusError(error)) {
break;
}
if (pauseAfterMs === null) {
break;
}
await new Promise((resolve) => setTimeout(resolve, pauseAfterMs));
}
if (pauseAfterMs === null) {
break;
}
await new Promise((resolve) => setTimeout(resolve, pauseAfterMs));
}
}
handleApiError(lastError, "fetch server statuses");
handleApiError(lastError, "fetch server statuses");
return {};
});
}
export async function getServerStatusById(id: number): Promise<ServerStatus> {
+7 -2
View File
@@ -1,4 +1,5 @@
import { authApi } from "@/main-axios";
import { createTtlRequestCache } from "@/lib/ttl-request-cache";
// OPEN TABS API
// ============================================================================
@@ -42,6 +43,8 @@ export interface ActiveSessionInfo {
createdAt: number;
}
const activeSessionsCache = createTtlRequestCache<ActiveSessionInfo[]>(2_000);
export async function getOpenTabs(): Promise<OpenTabRecord[]> {
const response = await authApi.get("/open-tabs");
return response.data;
@@ -69,8 +72,10 @@ export async function addOpenTab(tab: OpenTabUpsertPayload): Promise<void> {
}
export async function getActiveSessions(): Promise<ActiveSessionInfo[]> {
const response = await authApi.get("/open-tabs/active-sessions");
return response.data;
return activeSessionsCache.get(async () => {
const response = await authApi.get("/open-tabs/active-sessions");
return Array.isArray(response.data) ? response.data : [];
});
}
// ============================================================================
+79 -6
View File
@@ -28,6 +28,7 @@ export async function updateRole(
roleData: {
displayName?: string;
description?: string | null;
permissions?: string[];
},
): Promise<{ role: Role }> {
try {
@@ -88,16 +89,30 @@ export async function removeRoleFromUser(
}
}
export type SharePermissionLevel = "connect" | "view" | "edit" | "manage";
export interface ShareTarget {
type: "user" | "role";
id: string | number;
}
export async function shareHost(
hostId: number,
shareData: {
targetType: "user" | "role";
targetUserId?: string;
targetRoleId?: number;
permissionLevel: "view";
targets: ShareTarget[];
permissionLevel: SharePermissionLevel;
durationHours?: number;
},
): Promise<{ success: boolean }> {
): Promise<{
success: boolean;
expiresAt: string | null;
results: Array<{
type: "user" | "role";
id: string | number;
accessId: number;
created: boolean;
}>;
}> {
try {
const response = await rbacApi.post(
`/rbac/host/${hostId}/share`,
@@ -109,9 +124,28 @@ export async function shareHost(
}
}
export async function updateHostAccess(
hostId: number,
accessId: number,
update: {
permissionLevel?: SharePermissionLevel;
durationHours?: number | null;
},
): Promise<{ success: boolean; expiresAt: string | null }> {
try {
const response = await rbacApi.patch(
`/rbac/host/${hostId}/access/${accessId}`,
update,
);
return response.data;
} catch (error) {
throw handleApiError(error, "update host access");
}
}
export async function getHostAccess(
hostId: number,
): Promise<{ accessList: AccessRecord[] }> {
): Promise<{ accessList: AccessRecord[]; isOwner?: boolean }> {
try {
const response = await rbacApi.get(`/rbac/host/${hostId}/access`);
return response.data;
@@ -120,6 +154,45 @@ export async function getHostAccess(
}
}
export interface PermissionCatalogEntry {
group: string;
permissions: string[];
}
export async function getPermissionsCatalog(): Promise<{
catalog: PermissionCatalogEntry[];
}> {
try {
const response = await rbacApi.get("/rbac/permissions/catalog");
return response.data;
} catch (error) {
throw handleApiError(error, "fetch permissions catalog");
}
}
export async function getSharedHosts(): Promise<{
sharedHosts: Array<{
id: number;
name: string | null;
ip: string;
port: number;
username: string;
folder: string | null;
tags: string | null;
permissionLevel: SharePermissionLevel;
expiresAt: string | null;
grantedBy: string;
ownerUsername: string;
}>;
}> {
try {
const response = await rbacApi.get("/rbac/shared-hosts");
return response.data;
} catch (error) {
throw handleApiError(error, "fetch shared hosts");
}
}
export async function revokeHostAccess(
hostId: number,
accessId: number,
+33
View File
@@ -11,6 +11,9 @@ export type SessionLogRecord = {
hostName: string | null;
hostIp: string | null;
sizeBytes: number | null;
protocol: "ssh" | "rdp" | "vnc" | "telnet";
format: "text" | "asciicast" | "guacamole";
username: string | null;
};
export async function getSessionLogs(): Promise<SessionLogRecord[]> {
@@ -22,6 +25,36 @@ export async function getSessionLogs(): Promise<SessionLogRecord[]> {
}
}
export async function getSessionLogBlob(id: number): Promise<Blob> {
try {
const response = await authApi.get(`/session_logs/${id}/content`, {
responseType: "blob",
});
return response.data;
} catch (error) {
throw handleApiError(error, "fetch session recording");
}
}
export async function getSessionRecordingRetention(): Promise<number> {
try {
const response = await authApi.get("/session_logs/retention");
return response.data.retentionDays;
} catch (error) {
throw handleApiError(error, "fetch session recording retention");
}
}
export async function setSessionRecordingRetention(
retentionDays: number,
): Promise<void> {
try {
await authApi.put("/session_logs/retention", { retentionDays });
} catch (error) {
throw handleApiError(error, "update session recording retention");
}
}
export async function getSessionLogContent(id: number): Promise<string> {
try {
const response = await authApi.get(`/session_logs/${id}/content`, {
+1 -1
View File
@@ -410,7 +410,7 @@ export async function uploadSSHFile(
form.append("totalSize", String(file.size));
form.append("chunk", chunkBlob, fileName);
const response = await fileManagerApi.post(
const response = await fileManagerApi.postForm(
"/ssh/uploadFileChunk",
form,
{ timeout: 0 },
+51 -6
View File
@@ -7,17 +7,39 @@ import {
} from "@/main-axios";
import type { SSHHost, SSHHostData, ProxyNode } from "@/types/index";
import type { ServerStatus, SSHHostWithStatus } from "@/main-axios";
import type { ProxmoxDiscoverResult } from "@/types/proxmox";
import type { ProxmoxDiscoverResult, ProxmoxSyncResult } from "@/types/proxmox";
import {
getCachedSSHHosts,
invalidateHostsAndStatusCaches,
} from "@/lib/hosts-request-cache";
// SSH HOST MANAGEMENT
// ============================================================================
export async function getSSHHosts(): Promise<SSHHostWithStatus[]> {
export type GetSSHHostsOptions = {
/** When false, skip the status service call (host config only). Default true. */
includeStatus?: boolean;
};
async function loadSSHHostsFromApi(): Promise<SSHHost[]> {
const hostsResponse = await sshHostApi.get("/db/host");
return Array.isArray(hostsResponse.data) ? hostsResponse.data : [];
}
export async function getSSHHosts(
options: GetSSHHostsOptions = {},
): Promise<SSHHostWithStatus[]> {
const includeStatus = options.includeStatus !== false;
try {
const hostsResponse = await sshHostApi.get("/db/host");
const hosts: SSHHost[] = Array.isArray(hostsResponse.data)
? hostsResponse.data
: [];
const hosts = await getCachedSSHHosts(loadSSHHostsFromApi);
if (!includeStatus) {
return hosts.map((host) => ({
...host,
status: "unknown",
}));
}
let statuses: Record<number, ServerStatus> = {};
try {
@@ -45,9 +67,11 @@ export async function createSSHHost(hostData: SSHHostData): Promise<SSHHost> {
const response = await sshHostApi.post("/db/host", formData, {
headers: { "Content-Type": "multipart/form-data" },
});
invalidateHostsAndStatusCaches();
return response.data;
}
const response = await sshHostApi.post("/db/host", hostData);
invalidateHostsAndStatusCaches();
return response.data;
} catch (error) {
throw handleApiError(error, "create SSH host");
@@ -67,9 +91,11 @@ export async function updateSSHHost(
const response = await sshHostApi.put(`/db/host/${hostId}`, formData, {
headers: { "Content-Type": "multipart/form-data" },
});
invalidateHostsAndStatusCaches();
return response.data;
}
const response = await sshHostApi.put(`/db/host/${hostId}`, hostData);
invalidateHostsAndStatusCaches();
return response.data;
} catch (error) {
throw handleApiError(error, "update SSH host");
@@ -103,6 +129,7 @@ export async function bulkImportSSHHosts(
overwrite,
...(credentials ? { credentials } : {}),
});
invalidateHostsAndStatusCaches();
return response.data;
} catch (error) {
handleApiError(error, "bulk import SSH hosts");
@@ -125,6 +152,7 @@ export async function importSSHConfigHosts(
content,
overwrite,
});
invalidateHostsAndStatusCaches();
return response.data;
} catch (error) {
handleApiError(error, "import SSH config hosts");
@@ -146,6 +174,21 @@ export async function discoverProxmoxGuests(
}
}
export async function syncProxmoxGuests(
hostId: number,
): Promise<ProxmoxSyncResult> {
try {
const response = await authApi.post(
"/proxmox/sync",
{ hostId },
{ timeout: 120000 },
);
return response.data;
} catch (error) {
handleApiError(error, "sync Proxmox guests");
}
}
export async function bulkUpdateSSHHosts(
hostIds: number[],
updates: Record<string, unknown>,
@@ -155,6 +198,7 @@ export async function bulkUpdateSSHHosts(
hostIds,
updates,
});
invalidateHostsAndStatusCaches();
return response.data;
} catch (error) {
handleApiError(error, "bulk update SSH hosts");
@@ -166,6 +210,7 @@ export async function deleteSSHHost(
): Promise<Record<string, unknown>> {
try {
const response = await sshHostApi.delete(`/db/host/${hostId}`);
invalidateHostsAndStatusCaches();
return response.data;
} catch (error) {
handleApiError(error, "delete SSH host");
+42
View File
@@ -161,6 +161,48 @@ export async function deleteAccount(
}
}
// Raw axios errors propagate here so callers can detect the 409
// DATA_WIPE_REQUIRED code and re-submit with confirmDataWipe.
export async function adminResetUserPassword(
userId: string,
newPassword: string,
confirmDataWipe = false,
): Promise<{ message: string; dataWiped?: boolean }> {
const response = await authApi.post("/users/admin/reset-password", {
userId,
newPassword,
confirmDataWipe,
});
return response.data;
}
export async function adminDisableUserTotp(
userId: string,
): Promise<{ message: string }> {
try {
const response = await authApi.post("/users/admin/totp/disable", {
userId,
});
return response.data;
} catch (error) {
handleApiError(error, "disable user TOTP");
}
}
export async function adminExportUserData(
userId: string,
): Promise<Record<string, unknown>> {
try {
const response = await authApi.get(
`/users/admin/export/${encodeURIComponent(userId)}`,
{ timeout: 120000 },
);
return response.data;
} catch (error) {
handleApiError(error, "export user data");
}
}
export async function updateRegistrationAllowed(
allowed: boolean,
): Promise<Record<string, unknown>> {
+6 -6
View File
@@ -41,7 +41,7 @@ import type { SSOProviderPublic } from "@/types/index";
import { ElectronServerConfig as ServerConfigComponent } from "@/auth/ElectronServerConfig";
import { ElectronLoginForm } from "@/auth/ElectronLoginForm";
import { Checkbox } from "@/components/checkbox";
import i18n from "@/i18n/i18n";
import { changeAppLanguage, normalizeLanguageCode } from "@/i18n/i18n";
import {
removeSilentSigninFromSearch,
shouldTriggerSilentSignin,
@@ -238,14 +238,14 @@ export function Auth({ onLogin }: AuthProps) {
const [confirmNewPassword, setConfirmNewPassword] = useState("");
const [resetTempToken, setResetTempToken] = useState("");
const [language, setLanguage] = useState(
() => localStorage.getItem("i18nextLng") ?? "en",
const [language, setLanguage] = useState(() =>
normalizeLanguageCode(localStorage.getItem("i18nextLng")),
);
function handleLanguageChange(code: string) {
setLanguage(code);
localStorage.setItem("i18nextLng", code);
i18n.changeLanguage(code);
void changeAppLanguage(code)
.then((language) => setLanguage(language))
.catch(() => {});
}
const [registrationAllowed, setRegistrationAllowed] = useState(true);
+20 -1
View File
@@ -511,7 +511,26 @@ export function Auth({
}
try {
await completePasswordReset(localUsername, tempToken, newPassword);
try {
await completePasswordReset(localUsername, tempToken, newPassword);
} catch (err: unknown) {
const error = err as {
response?: { data?: { code?: string } };
};
if (error?.response?.data?.code !== "DATA_WIPE_REQUIRED") {
throw err;
}
if (!window.confirm(t("auth.confirmResetDataWipe"))) {
setResetLoading(false);
return;
}
await completePasswordReset(
localUsername,
tempToken,
newPassword,
true,
);
}
setResetStep("initiate");
setResetCode("");
@@ -138,6 +138,18 @@ export function ProxmoxDiscoverDialog({
enableDocker: g.enableDocker,
connectionType: g.connectionType,
tags: ["proxmox", g.type, g.node],
proxmoxConfig: {
source: {
source: "proxmox",
sourceHostId: Number(effectiveHostId),
node: g.node,
vmid: g.vmid,
type: g.type,
lastSeenAt: new Date().toISOString(),
lastStatus: g.status,
missingSince: null,
},
},
}));
const result = await bulkImportSSHHosts(toImport, false);
+34 -6
View File
@@ -531,6 +531,10 @@ function HostStatusCard({
);
}
function isStatusCheckEnabled(host: Host): boolean {
return host.statsConfig?.statusCheckEnabled !== false;
}
function RecentActivityCard({
activity,
hosts,
@@ -797,6 +801,7 @@ function CardItem({
onAddServiceLink,
onDeleteServiceLink,
statusLoading,
isVisible = true,
}: {
slot: CardSlot;
editMode: boolean;
@@ -826,6 +831,7 @@ function CardItem({
onAddServiceLink: (label: string, url: string) => Promise<void>;
onDeleteServiceLink: (id: number) => Promise<void>;
statusLoading?: boolean;
isVisible?: boolean;
}) {
const cardRef = useRef<HTMLDivElement | null>(null);
@@ -922,6 +928,7 @@ function CardItem({
{slot.id === "network_graph" && (
<NetworkGraphCard
embedded={true}
isVisible={isVisible}
onOpenInNewTab={() => onOpenSingletonTab("network_graph")}
/>
)}
@@ -1051,6 +1058,7 @@ type PanelColumnProps = {
onAddServiceLink: (label: string, url: string) => Promise<void>;
onDeleteServiceLink: (id: number) => Promise<void>;
statusLoading: boolean;
isVisible?: boolean;
};
function PanelColumn({
@@ -1082,6 +1090,7 @@ function PanelColumn({
onAddServiceLink,
onDeleteServiceLink,
statusLoading,
isVisible = true,
}: PanelColumnProps) {
const { t } = useTranslation();
const sorted = [...slots].sort((a, b) => a.order - b.order);
@@ -1138,6 +1147,7 @@ function PanelColumn({
onAddServiceLink={onAddServiceLink}
onDeleteServiceLink={onDeleteServiceLink}
statusLoading={statusLoading}
isVisible={isVisible}
/>
</div>
))}
@@ -1188,9 +1198,12 @@ function ColumnDivider({
export function DashboardTab({
onOpenSingletonTab,
onOpenTab,
isVisible = true,
}: {
onOpenSingletonTab: (type: TabType, pendingEvent?: string) => void;
onOpenTab: (host: Host, type: TabType) => void;
/** When false, pause dashboard metrics refresh while the tab stays mounted. */
isVisible?: boolean;
}) {
const { t, i18n } = useTranslation();
const { initialLoadComplete } = useServerStatus();
@@ -1283,6 +1296,7 @@ export function DashboardTab({
Map<string, { cpu: number | null; ram: number | null; disk: number | null }>
>(new Map());
const viewerSessionsRef = useRef<Map<number, string>>(new Map());
const statusCheckHosts = hosts.filter(isStatusCheckEnabled);
const fetchMetrics = useCallback(async (hostList: Host[]) => {
let statuses: Record<number, { status?: string }> = {};
@@ -1342,8 +1356,11 @@ export function DashboardTab({
const load = async () => {
const raw = await getSSHHosts().catch(() => []);
const mapped = raw.map(sshHostToHost);
const statusHosts = mapped.filter(isStatusCheckEnabled);
if (mounted) setHosts(mapped);
fetchMetrics(mapped).catch(() => {});
if (isVisible) {
fetchMetrics(statusHosts).catch(() => {});
}
};
load();
@@ -1393,28 +1410,37 @@ export function DashboardTab({
})
.catch(() => {});
if (!isVisible) {
return () => {
mounted = false;
};
}
const metricsInterval = setInterval(async () => {
if (document.visibilityState === "hidden") return;
const raw = await getSSHHosts().catch(() => []);
const mapped = raw.map(sshHostToHost);
const statusHosts = mapped.filter(isStatusCheckEnabled);
if (mounted) setHosts(mapped);
fetchMetrics(mapped).catch(() => {});
fetchMetrics(statusHosts).catch(() => {});
}, 30000);
return () => {
mounted = false;
clearInterval(metricsInterval);
};
}, [fetchMetrics]);
}, [fetchMetrics, isVisible]);
useEffect(() => {
if (viewerSessionsRef.current.size === 0) return;
if (!isVisible || viewerSessionsRef.current.size === 0) return;
const heartbeat = setInterval(async () => {
if (document.visibilityState === "hidden") return;
for (const [, sessionId] of viewerSessionsRef.current) {
sendMetricsHeartbeat(sessionId).catch(() => {});
}
}, 30000);
return () => clearInterval(heartbeat);
}, [hostMetrics]);
}, [hostMetrics, isVisible]);
const handleClearActivity = async () => {
try {
@@ -1587,6 +1613,7 @@ export function DashboardTab({
onAddServiceLink: handleAddServiceLink,
onDeleteServiceLink: handleDeleteServiceLink,
statusLoading,
isVisible,
};
const isMobile = useIsMobile();
@@ -1707,7 +1734,7 @@ export function DashboardTab({
)}
{slot.id === "host_status" && (
<HostStatusCard
hosts={hosts}
hosts={statusCheckHosts}
hostMetrics={hostMetrics}
onOpenTab={onOpenTab}
isAdmin={isAdmin}
@@ -1726,6 +1753,7 @@ export function DashboardTab({
{slot.id === "network_graph" && (
<NetworkGraphCard
embedded={true}
isVisible={isVisible}
onOpenInNewTab={() => onOpenSingletonTab("network_graph")}
/>
)}
+16 -2
View File
@@ -92,6 +92,8 @@ interface NetworkGraphCardProps {
rightSidebarWidth?: number;
embedded?: boolean;
onOpenInNewTab?: () => void;
/** When false, pause status refresh while the surface stays mounted. */
isVisible?: boolean;
}
type NetworkElement = NetworkTopologyNode | NetworkTopologyEdge;
@@ -195,6 +197,7 @@ function buildNodeSvg(
export function NetworkGraphCard({
embedded = true,
onOpenInNewTab,
isVisible = true,
}: NetworkGraphCardProps): React.ReactElement {
const { t } = useTranslation();
const { addTab } = useTabsSafe();
@@ -269,8 +272,19 @@ export function NetworkGraphCard({
}, [hostMap]);
useEffect(() => {
if (!isVisible) {
if (statusIntervalRef.current) {
clearInterval(statusIntervalRef.current);
statusIntervalRef.current = null;
}
return;
}
loadData();
statusIntervalRef.current = setInterval(updateHostStatuses, 30000);
statusIntervalRef.current = setInterval(() => {
if (document.visibilityState === "hidden") return;
updateHostStatuses();
}, 30000);
const onClickOutside = (e: MouseEvent) => {
if (
contextMenuRef.current &&
@@ -293,7 +307,7 @@ export function NetworkGraphCard({
document.removeEventListener("mousedown", onClickOutside, true);
themeObserver.disconnect();
};
}, []);
}, [isVisible]);
const loadData = async () => {
setLoading(true);
+4 -1
View File
@@ -335,7 +335,10 @@ function DockerManagerInner({
};
pollContainers();
const interval = setInterval(pollContainers, 5000);
const interval = setInterval(() => {
if (document.visibilityState === "hidden") return;
void pollContainers();
}, 5000);
return () => {
cancelled = true;
@@ -50,7 +50,10 @@ export function ContainerStats({
React.useEffect(() => {
fetchStats();
const interval = setInterval(fetchStats, 2000);
const interval = setInterval(() => {
if (document.visibilityState === "hidden") return;
void fetchStats();
}, 2000);
return () => clearInterval(interval);
}, [fetchStats]);
@@ -90,7 +90,10 @@ export function LogViewer({
React.useEffect(() => {
if (!autoRefresh) return;
const interval = setInterval(fetchLogs, 3000);
const interval = setInterval(() => {
if (document.visibilityState === "hidden") return;
void fetchLogs();
}, 3000);
return () => clearInterval(interval);
}, [autoRefresh, fetchLogs]);
+5 -2
View File
@@ -87,6 +87,7 @@ function FileManagerContent({
initialPath,
onClose,
onOpenTerminalTab,
isVisible = true,
}: FileManagerProps) {
const { openWindow } = useWindowManager();
const { t } = useTranslation();
@@ -268,7 +269,7 @@ function FileManagerContent({
}, [currentHost]);
useEffect(() => {
if (sshSessionId) {
if (sshSessionId && isVisible) {
startKeepalive();
} else {
stopKeepalive();
@@ -277,7 +278,7 @@ function FileManagerContent({
return () => {
stopKeepalive();
};
}, [sshSessionId, startKeepalive, stopKeepalive]);
}, [sshSessionId, isVisible, startKeepalive, stopKeepalive]);
const initialFileOpenedRef = useRef(false);
useEffect(() => {
@@ -3105,6 +3106,7 @@ function FileManagerInner({
initialPath,
onClose,
onOpenTerminalTab,
isVisible = true,
}: FileManagerProps) {
return (
<WindowManager>
@@ -3114,6 +3116,7 @@ function FileManagerInner({
initialPath={initialPath}
onClose={onClose}
onOpenTerminalTab={onOpenTerminalTab}
isVisible={isVisible}
/>
</WindowManager>
);
+355 -170
View File
@@ -1,6 +1,14 @@
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useState, useRef, useCallback, useEffect } from "react";
import React, {
useState,
useRef,
useCallback,
useEffect,
useMemo,
useLayoutEffect,
} from "react";
import { createPortal } from "react-dom";
import { useVirtualizer } from "@tanstack/react-virtual";
import { cn } from "@/lib/utils.ts";
import {
Folder,
@@ -185,6 +193,12 @@ export function FileManagerGrid({
const { t } = useTranslation();
const gridRef = useRef<HTMLDivElement>(null);
const [editingName, setEditingName] = useState("");
const [gridCols, setGridCols] = useState(4);
const LIST_ROW_H = 41;
const GRID_ROW_H = 112;
const LIST_HEADER_H = 33;
const CONTENT_PAD = 16;
const [dragState, setDragState] = useState<DragState>({
type: "none",
@@ -192,6 +206,52 @@ export function FileManagerGrid({
counter: 0,
});
// Responsive column count for grid virtualization (matches Tailwind breakpoints roughly).
useEffect(() => {
if (viewMode !== "grid") return;
const el = gridRef.current;
if (!el) return;
const updateCols = () => {
const w = el.clientWidth - CONTENT_PAD * 2;
// gap-4 (16px) + ~min cell 96px
const n = Math.max(2, Math.min(8, Math.floor((w + 16) / 112)));
setGridCols(n);
};
updateCols();
const ro = new ResizeObserver(updateCols);
ro.observe(el);
return () => ro.disconnect();
}, [viewMode]);
const gridRowCount = useMemo(
() => (viewMode === "grid" ? Math.ceil(files.length / gridCols) : 0),
[viewMode, files.length, gridCols],
);
const listVirtualizer = useVirtualizer({
count: viewMode === "list" ? files.length : 0,
getScrollElement: () => gridRef.current,
estimateSize: () => LIST_ROW_H,
overscan: 16,
getItemKey: (index) => files[index]?.path ?? index,
enabled: viewMode === "list" && files.length > 0,
});
const gridVirtualizer = useVirtualizer({
count: gridRowCount,
getScrollElement: () => gridRef.current,
estimateSize: () => GRID_ROW_H,
overscan: 6,
getItemKey: (index) => `row-${index}`,
enabled: viewMode === "grid" && files.length > 0,
});
useLayoutEffect(() => {
if (viewMode === "list") listVirtualizer.measure();
else gridVirtualizer.measure();
}, [viewMode, files.length, editingFile?.path, createIntent, gridCols]);
useEffect(() => {
const handleGlobalMouseMove = (e: MouseEvent) => {
if (dragState.type === "internal" && dragState.files.length > 0) {
@@ -442,6 +502,77 @@ export function FileManagerGrid({
setSelectionRect({ x, y, width, height });
if (gridRef.current) {
const selectionBox = {
left: x,
top: y,
right: x + width,
bottom: y + height,
};
// Virtual list: only visible DOM nodes exist. For list mode compute
// selection from scroll position + fixed row height so drag-select
// still covers off-screen rows.
if (viewMode === "list" && files.length > 0) {
const scrollTop = gridRef.current.scrollTop;
const createExtra = createIntent ? LIST_ROW_H : 0;
const contentTop =
selectionBox.top +
scrollTop -
CONTENT_PAD -
LIST_HEADER_H -
createExtra;
const contentBottom =
selectionBox.bottom +
scrollTop -
CONTENT_PAD -
LIST_HEADER_H -
createExtra;
const startIdx = Math.max(0, Math.floor(contentTop / LIST_ROW_H));
const endIdx = Math.min(
files.length - 1,
Math.ceil(contentBottom / LIST_ROW_H),
);
if (endIdx >= startIdx) {
onSelectionChange(files.slice(startIdx, endIdx + 1));
} else {
onSelectionChange([]);
}
return;
}
if (viewMode === "grid" && files.length > 0) {
const scrollTop = gridRef.current.scrollTop;
const createExtra = createIntent ? GRID_ROW_H : 0;
const contentTop =
selectionBox.top + scrollTop - CONTENT_PAD - createExtra;
const contentBottom =
selectionBox.bottom + scrollTop - CONTENT_PAD - createExtra;
const startRow = Math.max(0, Math.floor(contentTop / GRID_ROW_H));
const endRow = Math.min(
gridRowCount - 1,
Math.ceil(contentBottom / GRID_ROW_H),
);
// Column range from X within padded content.
const contentLeft = selectionBox.left - CONTENT_PAD;
const contentRight = selectionBox.right - CONTENT_PAD;
const cellW =
(gridRef.current.clientWidth - CONTENT_PAD * 2 + 16) / gridCols;
const startCol = Math.max(0, Math.floor(contentLeft / cellW));
const endCol = Math.min(
gridCols - 1,
Math.ceil(contentRight / cellW),
);
const selected: FileItem[] = [];
for (let r = startRow; r <= endRow; r++) {
for (let c = startCol; c <= endCol; c++) {
const idx = r * gridCols + c;
if (idx >= 0 && idx < files.length) selected.push(files[idx]);
}
}
onSelectionChange(selected);
return;
}
const fileElements =
gridRef.current.querySelectorAll("[data-file-path]");
const selectedPaths: string[] = [];
@@ -457,13 +588,6 @@ export function FileManagerGrid({
bottom: elementRect.bottom - containerRect.top,
};
const selectionBox = {
left: x,
top: y,
right: x + width,
bottom: y + height,
};
const intersects = !(
relativeElementRect.right < selectionBox.left ||
relativeElementRect.left > selectionBox.right ||
@@ -486,7 +610,16 @@ export function FileManagerGrid({
}
}
},
[isSelecting, selectionStart, files, onSelectionChange],
[
isSelecting,
selectionStart,
files,
onSelectionChange,
viewMode,
createIntent,
gridCols,
gridRowCount,
],
);
const handleMouseUp = useCallback(
@@ -819,89 +952,126 @@ export function FileManagerGrid({
</span>
</div>
) : viewMode === "grid" ? (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4">
<div className="flex flex-col gap-4">
{createIntent && (
<CreateIntentGridItem
intent={createIntent}
onConfirm={onConfirmCreate}
onCancel={onCancelCreate}
/>
<div
className="grid gap-4"
style={{
gridTemplateColumns: `repeat(${gridCols}, minmax(0, 1fr))`,
}}
>
<CreateIntentGridItem
intent={createIntent}
onConfirm={onConfirmCreate}
onCancel={onCancelCreate}
/>
</div>
)}
{files.map((file) => {
const isSelected = selectedFiles.some(
(f) => f.path === file.path,
);
return (
<div
key={file.path}
data-file-path={file.path}
draggable={true}
className={cn(
"group flex flex-col items-center p-3 rounded-none border-2 border-transparent transition-all cursor-pointer hover:bg-muted/50 select-none",
isSelected && "bg-accent-brand/10 border-accent-brand/40",
dragState.target?.path === file.path &&
"bg-accent-brand/20 border-accent-brand border-dashed",
dragState.files.some((f) => f.path === file.path) &&
"opacity-50",
)}
title={file.name}
onClick={(e) => handleFileClick(file, e)}
onContextMenu={(e) => {
e.preventDefault();
e.stopPropagation();
onContextMenu?.(e, file);
}}
onDragStart={(e) => handleFileDragStart(e, file)}
onDragOver={(e) => handleFileDragOver(e, file)}
onDragLeave={(e) => handleFileDragLeave(e, file)}
onDrop={(e) => handleFileDrop(e, file)}
onDragEnd={handleFileDragEnd}
>
<div className="relative mb-2 pointer-events-none">
{getFileIcon(file, viewMode)}
<div
className="relative w-full"
style={{ height: gridVirtualizer.getTotalSize() }}
>
{gridVirtualizer.getVirtualItems().map((vRow) => {
const start = vRow.index * gridCols;
const rowFiles = files.slice(start, start + gridCols);
return (
<div
key={vRow.key}
data-index={vRow.index}
ref={gridVirtualizer.measureElement}
className="absolute top-0 left-0 w-full"
style={{
transform: `translateY(${vRow.start}px)`,
}}
>
<div
className="grid gap-4 pb-4"
style={{
gridTemplateColumns: `repeat(${gridCols}, minmax(0, 1fr))`,
}}
>
{rowFiles.map((file) => {
const isSelected = selectedFiles.some(
(f) => f.path === file.path,
);
return (
<div
key={file.path}
data-file-path={file.path}
draggable={true}
className={cn(
"group flex flex-col items-center p-3 rounded-none border-2 border-transparent transition-all cursor-pointer hover:bg-muted/50 select-none",
isSelected &&
"bg-accent-brand/10 border-accent-brand/40",
dragState.target?.path === file.path &&
"bg-accent-brand/20 border-accent-brand border-dashed",
dragState.files.some(
(f) => f.path === file.path,
) && "opacity-50",
)}
title={file.name}
onClick={(e) => handleFileClick(file, e)}
onContextMenu={(e) => {
e.preventDefault();
e.stopPropagation();
onContextMenu?.(e, file);
}}
onDragStart={(e) => handleFileDragStart(e, file)}
onDragOver={(e) => handleFileDragOver(e, file)}
onDragLeave={(e) => handleFileDragLeave(e, file)}
onDrop={(e) => handleFileDrop(e, file)}
onDragEnd={handleFileDragEnd}
>
<div className="relative mb-2 pointer-events-none">
{getFileIcon(file, viewMode)}
</div>
<div className="w-full flex flex-col items-center pointer-events-none">
{editingFile?.path === file.path ? (
<input
ref={editInputRef}
type="text"
value={editingName}
onChange={(e) =>
setEditingName(e.target.value)
}
onKeyDown={handleEditKeyDown}
onBlur={handleEditConfirm}
className="max-w-[120px] min-w-[60px] w-fit border border-accent-brand/60 bg-card px-2 py-1 text-xs rounded-none outline-none focus:ring-1 focus:ring-accent-brand/50 text-center pointer-events-auto"
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
/>
) : (
<p
className="text-[11px] font-bold tracking-tight text-center truncate w-full px-1"
title={file.name}
>
{file.name}
</p>
)}
{file.type === "file" &&
file.size !== undefined &&
file.size !== null && (
<p className="text-[10px] font-medium text-muted-foreground/60 mt-0.5">
{formatFileSize(file.size)}
</p>
)}
{file.type === "link" && file.linkTarget && (
<p
className="text-[10px] text-accent-brand mt-0.5 truncate w-full text-center"
title={file.linkTarget}
>
{file.linkTarget}
</p>
)}
</div>
</div>
);
})}
</div>
</div>
<div className="w-full flex flex-col items-center pointer-events-none">
{editingFile?.path === file.path ? (
<input
ref={editInputRef}
type="text"
value={editingName}
onChange={(e) => setEditingName(e.target.value)}
onKeyDown={handleEditKeyDown}
onBlur={handleEditConfirm}
className="max-w-[120px] min-w-[60px] w-fit border border-accent-brand/60 bg-card px-2 py-1 text-xs rounded-none outline-none focus:ring-1 focus:ring-accent-brand/50 text-center pointer-events-auto"
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
/>
) : (
<p
className="text-[11px] font-bold tracking-tight text-center truncate w-full px-1"
title={file.name}
>
{file.name}
</p>
)}
{file.type === "file" &&
file.size !== undefined &&
file.size !== null && (
<p className="text-[10px] font-medium text-muted-foreground/60 mt-0.5">
{formatFileSize(file.size)}
</p>
)}
{file.type === "link" && file.linkTarget && (
<p
className="text-[10px] text-accent-brand mt-0.5 truncate w-full text-center"
title={file.linkTarget}
>
{file.linkTarget}
</p>
)}
</div>
</div>
);
})}
);
})}
</div>
</div>
) : (
<div className="flex flex-col">
@@ -952,91 +1122,106 @@ export function FileManagerGrid({
onCancel={onCancelCreate}
/>
)}
{files.map((file) => {
const isSelected = selectedFiles.some(
(f) => f.path === file.path,
);
return (
<div
key={file.path}
data-file-path={file.path}
draggable={true}
className={cn(
"grid grid-cols-[1fr_120px_150px_80px_90px] gap-2 px-4 py-2 items-center text-xs cursor-pointer border-b border-border hover:bg-muted/50 last:border-0 rounded-none select-none transition-colors",
isSelected && "bg-accent-brand/10",
dragState.target?.path === file.path &&
"bg-accent-brand/20 border-accent-brand border-dashed",
dragState.files.some((f) => f.path === file.path) &&
"opacity-50",
)}
onClick={(e) => handleFileClick(file, e)}
onContextMenu={(e) => {
e.preventDefault();
e.stopPropagation();
onContextMenu?.(e, file);
}}
onDragStart={(e) => handleFileDragStart(e, file)}
onDragOver={(e) => handleFileDragOver(e, file)}
onDragLeave={(e) => handleFileDragLeave(e, file)}
onDrop={(e) => handleFileDrop(e, file)}
onDragEnd={handleFileDragEnd}
>
<div className="flex items-center gap-3 overflow-hidden pointer-events-none">
<div className="shrink-0">
{getFileIcon(file, viewMode)}
</div>
{editingFile?.path === file.path ? (
<input
ref={editInputRef}
type="text"
value={editingName}
onChange={(e) => setEditingName(e.target.value)}
onKeyDown={handleEditKeyDown}
onBlur={handleEditConfirm}
className="flex-1 min-w-0 max-w-[200px] border border-accent-brand/60 bg-card px-2 py-1 text-xs rounded-none outline-none focus:ring-1 focus:ring-accent-brand/50 pointer-events-auto"
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
/>
) : (
<span
className="font-bold truncate tracking-tight"
title={file.name}
>
{file.name}
{file.type === "link" && file.linkTarget && (
<span className="text-accent-brand ml-1 normal-case font-normal">
{file.linkTarget}
<div
className="relative w-full"
style={{ height: listVirtualizer.getTotalSize() }}
>
{listVirtualizer.getVirtualItems().map((vItem) => {
const file = files[vItem.index];
if (!file) return null;
const isSelected = selectedFiles.some(
(f) => f.path === file.path,
);
return (
<div
key={vItem.key}
data-index={vItem.index}
ref={listVirtualizer.measureElement}
className="absolute top-0 left-0 w-full"
style={{
transform: `translateY(${vItem.start}px)`,
}}
>
<div
data-file-path={file.path}
draggable={true}
className={cn(
"grid grid-cols-[1fr_120px_150px_80px_90px] gap-2 px-4 py-2 items-center text-xs cursor-pointer border-b border-border hover:bg-muted/50 rounded-none select-none transition-colors",
isSelected && "bg-accent-brand/10",
dragState.target?.path === file.path &&
"bg-accent-brand/20 border-accent-brand border-dashed",
dragState.files.some((f) => f.path === file.path) &&
"opacity-50",
)}
onClick={(e) => handleFileClick(file, e)}
onContextMenu={(e) => {
e.preventDefault();
e.stopPropagation();
onContextMenu?.(e, file);
}}
onDragStart={(e) => handleFileDragStart(e, file)}
onDragOver={(e) => handleFileDragOver(e, file)}
onDragLeave={(e) => handleFileDragLeave(e, file)}
onDrop={(e) => handleFileDrop(e, file)}
onDragEnd={handleFileDragEnd}
>
<div className="flex items-center gap-3 overflow-hidden pointer-events-none">
<div className="shrink-0">
{getFileIcon(file, viewMode)}
</div>
{editingFile?.path === file.path ? (
<input
ref={editInputRef}
type="text"
value={editingName}
onChange={(e) => setEditingName(e.target.value)}
onKeyDown={handleEditKeyDown}
onBlur={handleEditConfirm}
className="flex-1 min-w-0 max-w-[200px] border border-accent-brand/60 bg-card px-2 py-1 text-xs rounded-none outline-none focus:ring-1 focus:ring-accent-brand/50 pointer-events-auto"
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
/>
) : (
<span
className="font-bold truncate tracking-tight"
title={file.name}
>
{file.name}
{file.type === "link" && file.linkTarget && (
<span className="text-accent-brand ml-1 normal-case font-normal">
{file.linkTarget}
</span>
)}
</span>
)}
</div>
<span className="text-[10px] text-muted-foreground pointer-events-none">
{file.modified || "—"}
</span>
)}
<span className="text-[10px] text-muted-foreground truncate hidden md:block pointer-events-none">
{file.owner
? `${file.owner}${file.group ? `:${file.group}` : ""}`
: "—"}
</span>
<span className="text-[10px] text-right text-muted-foreground tabular-nums pointer-events-none">
{file.type === "file" &&
file.size !== undefined &&
file.size !== null
? formatFileSize(file.size)
: "—"}
</span>
<span className="text-[10px] text-right font-mono text-muted-foreground/60 pointer-events-none">
{file.permissions || "—"}
</span>
</div>
</div>
<span className="text-[10px] text-muted-foreground pointer-events-none">
{file.modified || "—"}
</span>
<span className="text-[10px] text-muted-foreground truncate hidden md:block pointer-events-none">
{file.owner
? `${file.owner}${file.group ? `:${file.group}` : ""}`
: "—"}
</span>
<span className="text-[10px] text-right text-muted-foreground tabular-nums pointer-events-none">
{file.type === "file" &&
file.size !== undefined &&
file.size !== null
? formatFileSize(file.size)
: "—"}
</span>
<span className="text-[10px] text-right font-mono text-muted-foreground/60 pointer-events-none">
{file.permissions || "—"}
</span>
</div>
);
})}
);
})}
</div>
</div>
)}
@@ -71,9 +71,34 @@ export function TransferMonitor() {
}
};
let intervalId: ReturnType<typeof setInterval> | null = null;
const start = () => {
if (intervalId !== null) return;
intervalId = setInterval(reconcileTransfers, POLL_INTERVAL_MS);
};
const stop = () => {
if (intervalId === null) return;
clearInterval(intervalId);
intervalId = null;
};
const onVisibility = () => {
if (document.visibilityState === "hidden") {
stop();
return;
}
void reconcileTransfers();
start();
};
void reconcileTransfers();
const interval = setInterval(reconcileTransfers, POLL_INTERVAL_MS);
return () => clearInterval(interval);
if (document.visibilityState !== "hidden") start();
document.addEventListener("visibilitychange", onVisibility);
return () => {
stop();
document.removeEventListener("visibilitychange", onVisibility);
};
}, [t, formatTransferMetrics]);
return null;
@@ -88,6 +88,7 @@ interface FileViewerProps {
content?: string;
savedContent?: string;
isLoading?: boolean;
isSaving?: boolean;
isEditable?: boolean;
resetKey?: number;
onContentChange?: (content: string) => void;
@@ -271,6 +272,7 @@ export function FileViewer({
content = "",
savedContent = "",
isLoading = false,
isSaving = false,
isEditable = false,
resetKey,
onContentChange,
@@ -353,6 +355,7 @@ export function FileViewer({
};
const handleSave = () => {
if (isSaving) return;
onSave?.(editedContent);
};
@@ -407,7 +410,7 @@ export function FileViewer({
return (
<div className="h-full flex flex-col bg-background">
<div className="flex-shrink-0 bg-card border-b border-border p-4">
<div className="flex items-center justify-between">
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-3">
<div className={cn("p-2 rounded-lg bg-muted", fileTypeInfo.color)}>
{fileTypeInfo.icon}
@@ -434,7 +437,7 @@ export function FileViewer({
</div>
</div>
<div className="flex items-center gap-2">
<div className="ml-auto flex min-w-0 max-w-full flex-wrap items-center justify-end gap-2">
{isEditable && (
<Button
variant="ghost"
@@ -490,6 +493,7 @@ export function FileViewer({
variant="outline"
size="sm"
onClick={handleRevert}
disabled={isSaving}
className="flex items-center gap-2"
>
<RotateCcw className="w-4 h-4" />
@@ -499,6 +503,7 @@ export function FileViewer({
variant="default"
size="sm"
onClick={handleSave}
disabled={isSaving}
className="flex items-center gap-2"
>
<Save className="w-4 h-4" />
@@ -81,6 +81,7 @@ export function FileWindow({
const [content, setContent] = useState<string>("");
const [isLoading, setIsLoading] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [isEditable, setIsEditable] = useState(false);
const [pendingContent, setPendingContent] = useState<string>("");
const [resetKey, setResetKey] = useState(0);
@@ -309,7 +310,7 @@ export function FileWindow({
const handleSave = async (newContent: string) => {
try {
setIsLoading(true);
setIsSaving(true);
await ensureSSHConnection();
@@ -340,7 +341,7 @@ export function FileWindow({
);
}
} finally {
setIsLoading(false);
setIsSaving(false);
}
};
@@ -553,6 +554,7 @@ export function FileWindow({
content={pendingContent || content}
savedContent={content}
isLoading={isLoading}
isSaving={isSaving}
resetKey={resetKey}
onRevert={handleRevert}
isEditable={isEditable}
@@ -7,6 +7,8 @@ export interface FileManagerProps {
initialPath?: string;
onClose?: () => void;
onOpenTerminalTab?: (path?: string) => void;
/** When false, pause keepalive while the tab stays mounted in the background. */
isVisible?: boolean;
}
export type ConnectionLogPayload = Omit<LogEntry, "id" | "timestamp">;
+6 -1
View File
@@ -96,7 +96,7 @@ const GuacamoleApp = React.forwardRef<GuacamoleAppHandle, GuacamoleAppProps>(
interface GuacamoleAppInnerProps {
hostId: number;
hostConfig: Pick<SSHHost, "connectionType">;
hostConfig: Pick<SSHHost, "connectionType" | "guacamoleConfig">;
hostName: string;
tabId?: string;
protocol?: "rdp" | "vnc" | "telnet";
@@ -213,6 +213,7 @@ const GuacamoleAppInner = React.forwardRef<
| "rdp"
| "vnc"
| "telnet";
const configuredDpi = Number(hostConfig.guacamoleConfig?.dpi);
return (
<div className="relative w-full h-full">
@@ -250,6 +251,10 @@ const GuacamoleAppInner = React.forwardRef<
token,
protocol: resolvedProtocol,
type: resolvedProtocol,
dpi:
Number.isFinite(configuredDpi) && configuredDpi > 0
? configuredDpi
: undefined,
}}
isVisible={true}
onError={(err) => setConnectionError(err)}
+70 -15
View File
@@ -12,6 +12,12 @@ import { getGuacamoleToken, isElectron, isEmbeddedMode } from "@/main-axios.ts";
import { SimpleLoader } from "@/lib/SimpleLoader.tsx";
import { getBasePath } from "@/lib/base-path.ts";
import { buildGuacamoleWebSocketBaseUrl } from "./guacamole-websocket-url.ts";
import {
isFirefoxBrowser,
isPasteShortcut,
pasteTextToRemote,
} from "./guacamole-clipboard.ts";
import { getGuacamoleDisplaySize } from "./guacamole-display-size.ts";
export type GuacamoleConnectionType = "rdp" | "vnc" | "telnet";
@@ -124,13 +130,14 @@ export const GuacamoleDisplay = forwardRef<
): Promise<string | null> => {
try {
let token: string;
const protocol = connectionConfig.protocol ?? connectionConfig.type;
const connectionProtocol =
connectionConfig.protocol ?? connectionConfig.type;
if (connectionConfig.token) {
token = connectionConfig.token;
} else {
const data = await getGuacamoleToken({
protocol: protocol ?? "rdp",
protocol: connectionProtocol ?? "rdp",
hostname: String(connectionConfig.hostname ?? ""),
port: connectionConfig.port,
username: connectionConfig.username,
@@ -151,8 +158,13 @@ export const GuacamoleDisplay = forwardRef<
token = data.token;
}
const width = connectionConfig.width ?? containerWidth ?? 1280;
const height = connectionConfig.height ?? containerHeight ?? 720;
const displaySize = getGuacamoleDisplaySize(
connectionConfig.width ?? containerWidth ?? 1280,
connectionConfig.height ?? containerHeight ?? 720,
connectionProtocol,
window.devicePixelRatio,
connectionConfig.dpi,
);
const wsBase = buildGuacamoleWebSocketBaseUrl({
isDev,
@@ -166,9 +178,10 @@ export const GuacamoleDisplay = forwardRef<
const params = new URLSearchParams({
token,
width: String(width),
height: String(height),
width: String(displaySize.width),
height: String(displaySize.height),
});
if (displaySize.dpi) params.set("dpi", String(displaySize.dpi));
return `${wsBase}?${params.toString()}`;
} catch (error) {
const errorMessage =
@@ -338,6 +351,32 @@ export const GuacamoleDisplay = forwardRef<
displayElement.setAttribute("tabindex", "0");
displayElement.style.outline = "none";
const useNativePasteFallback = isFirefoxBrowser();
if (useNativePasteFallback) {
displayElement.addEventListener(
"keydown",
(event) => {
if (isPasteShortcut(event)) {
event.stopImmediatePropagation();
}
},
true,
);
displayElement.addEventListener(
"paste",
(event) => {
if (clientRef.current !== client) return;
const text = event.clipboardData?.getData("text/plain");
if (!text) return;
event.preventDefault();
event.stopImmediatePropagation();
pasteTextToRemote(client, text);
},
true,
);
}
display.onresize = () => {
if (!isMountedRef.current) return;
rescaleDisplay(true);
@@ -403,9 +442,14 @@ export const GuacamoleDisplay = forwardRef<
onConnect?.();
if (containerRef.current) {
const rect = containerRef.current.getBoundingClientRect();
const w = Math.round(rect.width);
const h = Math.round(rect.height);
if (w > 0 && h > 0) client.sendSize(w, h);
const size = getGuacamoleDisplaySize(
rect.width,
rect.height,
protocol,
window.devicePixelRatio,
connectionConfig.dpi,
);
client.sendSize(size.width, size.height);
}
rescaleDisplay(false);
break;
@@ -484,6 +528,7 @@ export const GuacamoleDisplay = forwardRef<
rescaleDisplay,
connectionConfig.protocol,
connectionConfig.type,
connectionConfig.dpi,
t,
]);
@@ -557,10 +602,15 @@ export const GuacamoleDisplay = forwardRef<
resizeTimeoutRef.current = setTimeout(() => {
if (clientRef.current && containerRef.current) {
const rect = containerRef.current.getBoundingClientRect();
const w = Math.round(rect.width);
const h = Math.round(rect.height);
if (w > 0 && h > 0) {
clientRef.current.sendSize(w, h);
const size = getGuacamoleDisplaySize(
rect.width,
rect.height,
connectionConfig.protocol ?? connectionConfig.type,
window.devicePixelRatio,
connectionConfig.dpi,
);
if (rect.width > 0 && rect.height > 0) {
clientRef.current.sendSize(size.width, size.height);
rescaleDisplay(true);
}
}
@@ -572,11 +622,16 @@ export const GuacamoleDisplay = forwardRef<
return () => {
resizeObserver.disconnect();
};
}, [rescaleDisplay]);
}, [
connectionConfig.dpi,
connectionConfig.protocol,
connectionConfig.type,
rescaleDisplay,
]);
const syncClipboard = useCallback(() => {
const client = clientRef.current;
if (!client || !navigator.clipboard?.readText) return;
if (!client || isFirefoxBrowser() || !navigator.clipboard?.readText) return;
navigator.clipboard
.readText()
.then((text) => {
@@ -0,0 +1,43 @@
import Guacamole from "guacamole-common-js";
const CONTROL_LEFT_KEYSYM = 0xffe3;
const V_KEYSYM = 0x76;
interface ClipboardOutputStream {
sendBlob(data: string): void;
sendEnd(): void;
}
export interface GuacamoleClipboardClient {
createClipboardStream(mimetype: string): ClipboardOutputStream;
sendKeyEvent(pressed: number, keysym: number): void;
}
export function isFirefoxBrowser(userAgent = navigator.userAgent): boolean {
return /(?:Firefox|FxiOS)\//.test(userAgent);
}
export function isPasteShortcut(
event: Pick<KeyboardEvent, "altKey" | "ctrlKey" | "key" | "metaKey">,
): boolean {
return (
!event.altKey &&
(event.ctrlKey || event.metaKey) &&
event.key.toLowerCase() === "v"
);
}
export function pasteTextToRemote(
client: GuacamoleClipboardClient,
text: string,
): void {
const stream = client.createClipboardStream("text/plain");
const writer = new Guacamole.StringWriter(stream);
writer.sendText(text);
writer.sendEnd();
client.sendKeyEvent(1, CONTROL_LEFT_KEYSYM);
client.sendKeyEvent(1, V_KEYSYM);
client.sendKeyEvent(0, V_KEYSYM);
client.sendKeyEvent(0, CONTROL_LEFT_KEYSYM);
}
@@ -0,0 +1,39 @@
const DEFAULT_RDP_DPI = 96;
const MAX_DEVICE_PIXEL_RATIO = 3;
export interface GuacamoleDisplaySize {
width: number;
height: number;
dpi?: number;
pixelRatio: number;
}
export function getGuacamoleDisplaySize(
cssWidth: number,
cssHeight: number,
protocol: string | undefined,
devicePixelRatio: number,
configuredDpi?: number,
): GuacamoleDisplaySize {
const isRdp = protocol === "rdp";
const pixelRatio = isRdp
? Math.min(
MAX_DEVICE_PIXEL_RATIO,
Math.max(1, Number.isFinite(devicePixelRatio) ? devicePixelRatio : 1),
)
: 1;
const size = {
width: Math.max(1, Math.round(cssWidth * pixelRatio)),
height: Math.max(1, Math.round(cssHeight * pixelRatio)),
pixelRatio,
};
if (!isRdp) return size;
const baseDpi =
configuredDpi && Number.isFinite(configuredDpi) && configuredDpi > 0
? configuredDpi
: DEFAULT_RDP_DPI;
return { ...size, dpi: Math.round(baseDpi * pixelRatio) };
}
@@ -0,0 +1,50 @@
/**
* setInterval that pauses while the document is hidden and fires once on resume.
* Returns a cleanup function for useEffect.
*/
export function runVisibleInterval(
tick: () => void,
intervalMs: number,
): () => void {
let intervalId: ReturnType<typeof setInterval> | null = null;
const start = () => {
if (intervalId !== null || intervalMs <= 0) return;
intervalId = setInterval(() => {
if (document.visibilityState === "hidden") return;
tick();
}, intervalMs);
};
const stop = () => {
if (intervalId === null) return;
clearInterval(intervalId);
intervalId = null;
};
const onVisibility = () => {
if (document.visibilityState === "hidden") {
stop();
return;
}
tick();
start();
};
if (
typeof document === "undefined" ||
document.visibilityState !== "hidden"
) {
start();
}
if (typeof document !== "undefined") {
document.addEventListener("visibilitychange", onVisibility);
}
return () => {
stop();
if (typeof document !== "undefined") {
document.removeEventListener("visibilitychange", onVisibility);
}
};
}
@@ -61,8 +61,35 @@ function AlertFeedWidget({
useEffect(() => {
fetchData();
const iv = setInterval(fetchData, 30_000);
return () => clearInterval(iv);
let intervalId: ReturnType<typeof setInterval> | null = null;
const start = () => {
if (intervalId !== null) return;
intervalId = setInterval(() => {
if (document.visibilityState === "hidden") return;
void fetchData();
}, 30_000);
};
const stop = () => {
if (intervalId === null) return;
clearInterval(intervalId);
intervalId = null;
};
const onVisibility = () => {
if (document.visibilityState === "hidden") {
stop();
return;
}
void fetchData();
start();
};
if (document.visibilityState !== "hidden") start();
document.addEventListener("visibilitychange", onVisibility);
return () => {
stop();
document.removeEventListener("visibilitychange", onVisibility);
};
}, [maxItems, showAcknowledged]);
const handleAck = async (id: number) => {
@@ -8,6 +8,7 @@ import type {
} from "@/types/homepage-types";
import { GRID_SIZE } from "@/types/homepage-types";
import { WidgetTitle } from "./WidgetTitle";
import { runVisibleInterval } from "../use-visible-interval";
function getMonthData(year: number, month: number, startOnMonday: boolean) {
const firstDay = new Date(year, month, 1).getDay();
@@ -27,8 +28,7 @@ function CalendarWidget({
const [viewMonth, setViewMonth] = useState(now.getMonth());
useEffect(() => {
const iv = setInterval(() => setNow(new Date()), 60_000);
return () => clearInterval(iv);
return runVisibleInterval(() => setNow(new Date()), 60_000);
}, []);
const { daysInMonth, offset } = getMonthData(
@@ -1,18 +1,20 @@
import { useEffect, useState } from "react";
import { useState } from "react";
import { Clock } from "lucide-react";
import { registerWidget } from "./WidgetRegistry";
import type { ClockConfig, WidgetComponentProps } from "@/types/homepage-types";
import { GRID_SIZE } from "@/types/homepage-types";
import { WidgetTitle } from "./WidgetTitle";
import { usePageVisibleInterval } from "@/hooks/use-page-visible-interval";
function ClockWidget({ widget, config }: WidgetComponentProps<ClockConfig>) {
const { timezone, showSeconds, format } = config;
const [now, setNow] = useState(new Date());
useEffect(() => {
const iv = setInterval(() => setNow(new Date()), 1000);
return () => clearInterval(iv);
}, []);
// Minute-level is enough without seconds; pause when the tab is backgrounded.
usePageVisibleInterval(
() => setNow(new Date()),
showSeconds ? 1_000 : 30_000,
);
const opts: Intl.DateTimeFormatOptions = {
hour: "2-digit",
@@ -69,8 +69,32 @@ function CountdownWidget({
}
};
update();
const iv = setInterval(update, 1000);
return () => clearInterval(iv);
let intervalId: ReturnType<typeof setInterval> | null = null;
const start = () => {
if (intervalId !== null) return;
intervalId = setInterval(update, 1000);
};
const stop = () => {
if (intervalId === null) return;
clearInterval(intervalId);
intervalId = null;
};
const onVisibility = () => {
if (document.visibilityState === "hidden") {
stop();
return;
}
update();
start();
};
if (document.visibilityState !== "hidden") start();
document.addEventListener("visibilitychange", onVisibility);
return () => {
stop();
document.removeEventListener("visibilitychange", onVisibility);
};
}, [targetDate]);
const titleBar = (
@@ -9,6 +9,7 @@ import type {
import { GRID_SIZE } from "@/types/homepage-types";
import { homepageApi } from "@/main-axios";
import { WidgetTitle } from "./WidgetTitle";
import { runVisibleInterval } from "../use-visible-interval";
function resolvePath(obj: unknown, path: string): unknown {
return path.split(".").reduce((acc: unknown, key) => {
@@ -70,8 +71,9 @@ function CustomApiWidget({
useEffect(() => {
fetchData();
const iv = setInterval(fetchData, interval * 1000);
return () => clearInterval(iv);
return runVisibleInterval(() => {
void fetchData();
}, interval * 1000);
}, [url, interval]);
const accent =
@@ -10,6 +10,7 @@ import { GRID_SIZE } from "@/types/homepage-types";
import { getRecentActivity } from "@/api/dashboard-api";
import type { RecentActivityItem } from "@/api/dashboard-api";
import { WidgetTitle } from "./WidgetTitle";
import { runVisibleInterval } from "../use-visible-interval";
function relativeTime(ts: string): string {
const diff = Date.now() - new Date(ts).getTime();
@@ -43,8 +44,9 @@ function DockerActivityWidget({
useEffect(() => {
fetchData();
const iv = setInterval(fetchData, 30_000);
return () => clearInterval(iv);
return runVisibleInterval(() => {
void fetchData();
}, 30_000);
}, [maxItems]);
if (loading) {
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useCallback, useState } from "react";
import { LayoutGrid } from "lucide-react";
import { useTranslation } from "react-i18next";
import { registerWidget } from "./WidgetRegistry";
@@ -8,9 +8,9 @@ import type {
} from "@/types/homepage-types";
import { GRID_SIZE } from "@/types/homepage-types";
import { getSSHHosts } from "@/api/ssh-host-management-api";
import { getAllServerStatuses } from "@/api/host-metrics-status-api";
import type { SSHHostWithStatus } from "@/main-axios";
import { WidgetTitle } from "./WidgetTitle";
import { usePageVisibleInterval } from "@/hooks/use-page-visible-interval";
function getAccentColor(): string {
return (
@@ -44,39 +44,27 @@ function HostGridWidget({
const [hosts, setHosts] = useState<SSHHostWithStatus[]>([]);
const [loading, setLoading] = useState(true);
const fetchData = async () => {
// getSSHHosts already attaches cached server status — no second /status call.
const fetchData = useCallback(async () => {
try {
const [allHosts, statuses] = await Promise.all([
getSSHHosts(),
getAllServerStatuses().catch(
() => ({}) as Record<number, { status: string }>,
),
]);
const allHosts = await getSSHHosts();
const filtered =
hostIds.length > 0
? allHosts.filter((h) => hostIds.includes(h.id))
: allHosts;
const withStatus = filtered.map((h) => ({
...h,
status:
(statuses as Record<number, { status: string }>)[h.id]?.status ??
h.status ??
"unknown",
}));
setHosts(withStatus);
setHosts(filtered);
} catch {
/* ignore */
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
const iv = setInterval(fetchData, 10_000);
return () => clearInterval(iv);
}, [hostIds.join(",")]);
// Align with global status cadence; pause when the tab is hidden.
usePageVisibleInterval(() => {
void fetchData();
}, 30_000);
if (loading) {
return (
<div className="flex items-center justify-center w-full h-full text-xs text-muted-foreground">
@@ -130,10 +130,37 @@ function HostStatusWidget({
};
poll();
const iv = setInterval(poll, 10000);
let intervalId: ReturnType<typeof setInterval> | null = null;
const start = () => {
if (intervalId !== null) return;
// Align with global status cadence; metrics do not need sub-10s when hidden.
intervalId = setInterval(() => {
if (document.visibilityState === "hidden") return;
void poll();
}, 30_000);
};
const stop = () => {
if (intervalId === null) return;
clearInterval(intervalId);
intervalId = null;
};
const onVisibility = () => {
if (document.visibilityState === "hidden") {
stop();
return;
}
void poll();
start();
};
if (document.visibilityState !== "hidden") start();
document.addEventListener("visibilitychange", onVisibility);
return () => {
cancelled = true;
clearInterval(iv);
stop();
document.removeEventListener("visibilitychange", onVisibility);
};
}, [hostId, needsMetrics]);
@@ -12,6 +12,7 @@ import { getMetricsHistory } from "@/api/host-metrics-api";
import { getSSHHosts } from "@/api/ssh-host-management-api";
import type { MetricsHistoryRow } from "@/api/host-metrics-api";
import { WidgetTitle } from "./WidgetTitle";
import { runVisibleInterval } from "../use-visible-interval";
function getAccentColor(): string {
return (
@@ -161,8 +162,9 @@ function MetricsChartWidget({
useEffect(() => {
fetchData();
const iv = setInterval(fetchData, 60_000);
return () => clearInterval(iv);
return runVisibleInterval(() => {
void fetchData();
}, 60_000);
}, [hostId, metric, range]);
const accent = getAccentColor();
@@ -9,6 +9,7 @@ import type {
import { GRID_SIZE } from "@/types/homepage-types";
import { homepageApi } from "@/main-axios";
import { WidgetTitle } from "./WidgetTitle";
import { runVisibleInterval } from "../use-visible-interval";
interface PingResult {
ok: boolean;
@@ -72,8 +73,9 @@ function PingStatusWidget({
useEffect(() => {
fetchAll();
const iv = setInterval(fetchAll, interval * 1000);
return () => clearInterval(iv);
return runVisibleInterval(() => {
void fetchAll();
}, interval * 1000);
}, [JSON.stringify(urls), interval]);
if (urls.length === 0) {
@@ -18,6 +18,7 @@ import { GRID_SIZE } from "@/types/homepage-types";
import { getRecentActivity } from "@/api/dashboard-api";
import type { RecentActivityItem } from "@/api/dashboard-api";
import { WidgetTitle } from "./WidgetTitle";
import { runVisibleInterval } from "../use-visible-interval";
function relativeTime(ts: string): string {
const diff = Date.now() - new Date(ts).getTime();
@@ -75,8 +76,9 @@ function RecentActivityWidget({
useEffect(() => {
fetchData();
const iv = setInterval(fetchData, 60_000);
return () => clearInterval(iv);
return runVisibleInterval(() => {
void fetchData();
}, 60_000);
}, [maxItems, filterTypes.join(",")]);
if (loading) {
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useCallback, useState } from "react";
import { Zap, Terminal, FolderOpen, Container } from "lucide-react";
import { useTranslation } from "react-i18next";
import { registerWidget } from "./WidgetRegistry";
@@ -8,9 +8,9 @@ import type {
} from "@/types/homepage-types";
import { GRID_SIZE } from "@/types/homepage-types";
import { getSSHHosts } from "@/api/ssh-host-management-api";
import { getAllServerStatuses } from "@/api/host-metrics-status-api";
import type { SSHHostWithStatus } from "@/main-axios";
import { WidgetTitle } from "./WidgetTitle";
import { usePageVisibleInterval } from "@/hooks/use-page-visible-interval";
function getAccentColor(): string {
return (
@@ -51,40 +51,25 @@ function SshQuickConnectWidget({
const [hosts, setHosts] = useState<SSHHostWithStatus[]>([]);
const [loading, setLoading] = useState(true);
const fetchData = async () => {
const fetchData = useCallback(async () => {
try {
const [allHosts, statuses] = await Promise.all([
getSSHHosts(),
getAllServerStatuses().catch(
() => ({}) as Record<number, { status: string }>,
),
]);
const allHosts = await getSSHHosts();
const filtered =
hostIds.length > 0
? allHosts.filter((h) => hostIds.includes(h.id))
: allHosts;
setHosts(
filtered.map((h) => ({
...h,
status:
(statuses as Record<number, { status: string }>)[h.id]?.status ??
h.status ??
"unknown",
})),
);
setHosts(filtered);
} catch {
/* ignore */
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
const iv = setInterval(fetchData, 10_000);
return () => clearInterval(iv);
}, [hostIds.join(",")]);
usePageVisibleInterval(() => {
void fetchData();
}, 30_000);
if (loading) {
return (
<div className="flex items-center justify-center w-full h-full text-xs text-muted-foreground">
@@ -10,6 +10,7 @@ import { GRID_SIZE } from "@/types/homepage-types";
import { getVersionInfo, getDatabaseHealth } from "@/api/system-status-api";
import { getUptime } from "@/api/dashboard-api";
import { WidgetTitle } from "./WidgetTitle";
import { runVisibleInterval } from "../use-visible-interval";
interface InfoRowProps {
label: string;
@@ -65,8 +66,9 @@ function SystemOverviewWidget({
useEffect(() => {
fetchData();
const iv = setInterval(fetchData, 300_000);
return () => clearInterval(iv);
return runVisibleInterval(() => {
void fetchData();
}, 300_000);
}, [showVersion, showDbHealth, showUptime]);
const accent =
@@ -9,6 +9,7 @@ import type {
import { GRID_SIZE } from "@/types/homepage-types";
import { getUptime } from "@/api/dashboard-api";
import { WidgetTitle } from "./WidgetTitle";
import { runVisibleInterval } from "../use-visible-interval";
function formatUptime(seconds: number): {
days: number;
@@ -43,14 +44,12 @@ function TermixUptimeWidget({
})
.catch(() => setError(true));
const iv = setInterval(() => {
return runVisibleInterval(() => {
if (baseRef.current !== null) {
const elapsed = (Date.now() - startRef.current) / 1000;
setSeconds(Math.floor(baseRef.current + elapsed));
}
}, 1000);
return () => clearInterval(iv);
}, []);
if (error) {
@@ -14,6 +14,7 @@ import type {
} from "@/types/homepage-types";
import { GRID_SIZE } from "@/types/homepage-types";
import { WidgetTitle } from "./WidgetTitle";
import { runVisibleInterval } from "../use-visible-interval";
interface WttrCurrent {
temp_C: string;
@@ -77,10 +78,15 @@ function WeatherWidget({
}
};
fetchWeather();
const interval = setInterval(fetchWeather, 10 * 60 * 1000);
const stop = runVisibleInterval(
() => {
void fetchWeather();
},
10 * 60 * 1000,
);
return () => {
cancelled = true;
clearInterval(interval);
stop();
};
}, [location]);
@@ -286,8 +286,8 @@ function HostMetricsInner({
}, [hostConfig?.id]);
React.useEffect(() => {
if (!statusCheckEnabled || !currentHostConfig?.id) {
setServerStatus("offline");
if (!statusCheckEnabled || !currentHostConfig?.id || !isActuallyVisible) {
if (!statusCheckEnabled) setServerStatus("offline");
return;
}
let cancelled = false;
@@ -314,6 +314,7 @@ function HostMetricsInner({
currentHostConfig?.id,
statusCheckEnabled,
statsConfig.statusCheckInterval,
isActuallyVisible,
]);
React.useEffect(() => {
@@ -0,0 +1,293 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { Pause, Play } from "lucide-react";
import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import Guacamole from "guacamole-common-js";
import type { SessionLogRecord } from "@/api/session-log-api";
import { parseAsciicast, type Asciicast } from "./asciicast";
import "@xterm/xterm/css/xterm.css";
const SPEEDS = [0.5, 1, 2, 4];
function formatPosition(seconds: number) {
const minutes = Math.floor(seconds / 60);
return `${minutes}:${String(Math.floor(seconds % 60)).padStart(2, "0")}`;
}
function PlaybackControls({
playing,
position,
duration,
speed,
onToggle,
onSeek,
onSpeed,
}: {
playing: boolean;
position: number;
duration: number;
speed: number;
onToggle: () => void;
onSeek: (position: number) => void;
onSpeed: (speed: number) => void;
}) {
return (
<div className="flex items-center gap-2 border-t border-border/60 bg-muted/20 px-3 py-2">
<button
type="button"
onClick={onToggle}
className="flex size-7 items-center justify-center hover:bg-muted"
aria-label={playing ? "Pause recording" : "Play recording"}
>
{playing ? (
<Pause className="size-3.5" />
) : (
<Play className="size-3.5" />
)}
</button>
<span className="w-10 text-[10px] tabular-nums text-muted-foreground">
{formatPosition(position)}
</span>
<input
type="range"
min={0}
max={Math.max(duration, 0.01)}
step={0.01}
value={Math.min(position, duration)}
onChange={(event) => onSeek(Number(event.target.value))}
className="min-w-0 flex-1 accent-primary"
aria-label="Recording timeline"
/>
<span className="w-10 text-right text-[10px] tabular-nums text-muted-foreground">
{formatPosition(duration)}
</span>
<select
value={speed}
onChange={(event) => onSpeed(Number(event.target.value))}
className="h-7 border border-border bg-background px-1 text-[10px]"
aria-label="Playback speed"
>
{SPEEDS.map((value) => (
<option key={value} value={value}>
{value}×
</option>
))}
</select>
</div>
);
}
function AsciicastPlayer({ blob }: { blob: Blob }) {
const containerRef = useRef<HTMLDivElement>(null);
const terminalRef = useRef<Terminal | null>(null);
const recordingRef = useRef<Asciicast | null>(null);
const positionRef = useRef(0);
const eventIndexRef = useRef(0);
const [position, setPosition] = useState(0);
const [duration, setDuration] = useState(0);
const [speed, setSpeed] = useState(1);
const [playing, setPlaying] = useState(false);
const [error, setError] = useState("");
const renderAt = useCallback((nextPosition: number) => {
const terminal = terminalRef.current;
const recording = recordingRef.current;
if (!terminal || !recording) return;
if (nextPosition < positionRef.current) {
terminal.reset();
terminal.resize(recording.width, recording.height);
eventIndexRef.current = 0;
}
while (eventIndexRef.current < recording.events.length) {
const [time, type, data] = recording.events[eventIndexRef.current];
if (time > nextPosition) break;
if (type === "o") terminal.write(data);
if (type === "r") {
const [cols, rows] = data.split("x").map(Number);
if (cols > 0 && rows > 0) terminal.resize(cols, rows);
}
eventIndexRef.current++;
}
positionRef.current = nextPosition;
setPosition(nextPosition);
}, []);
useEffect(() => {
if (!containerRef.current) return;
const terminal = new Terminal({
cursorBlink: false,
disableStdin: true,
convertEol: false,
fontSize: 12,
theme: { background: "#09090b" },
});
const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);
terminal.open(containerRef.current);
terminalRef.current = terminal;
const observer = new ResizeObserver(() => fitAddon.fit());
observer.observe(containerRef.current);
fitAddon.fit();
blob
.text()
.then((source) => {
const recording = parseAsciicast(source);
recordingRef.current = recording;
setDuration(recording.duration);
terminal.resize(recording.width, recording.height);
renderAt(0);
})
.catch((reason) =>
setError(reason instanceof Error ? reason.message : String(reason)),
);
return () => {
observer.disconnect();
terminal.dispose();
terminalRef.current = null;
};
}, [blob, renderAt]);
useEffect(() => {
if (!playing || !recordingRef.current) return;
let frame = 0;
let previous = performance.now();
const tick = (now: number) => {
const next = Math.min(
duration,
positionRef.current + ((now - previous) / 1000) * speed,
);
previous = now;
renderAt(next);
if (next >= duration) setPlaying(false);
else frame = requestAnimationFrame(tick);
};
frame = requestAnimationFrame(tick);
return () => cancelAnimationFrame(frame);
}, [duration, playing, renderAt, speed]);
if (error) return <div className="p-4 text-xs text-destructive">{error}</div>;
return (
<div className="flex h-full min-h-0 flex-col">
<div ref={containerRef} className="min-h-0 flex-1 bg-[#09090b] p-2" />
<PlaybackControls
playing={playing}
position={position}
duration={duration}
speed={speed}
onToggle={() => {
if (!playing && position >= duration) renderAt(0);
setPlaying((value) => !value);
}}
onSeek={renderAt}
onSpeed={setSpeed}
/>
</div>
);
}
function GuacamolePlayer({ blob }: { blob: Blob }) {
const containerRef = useRef<HTMLDivElement>(null);
const recordingRef = useRef<Guacamole.SessionRecording | null>(null);
const positionRef = useRef(0);
const [position, setPosition] = useState(0);
const [duration, setDuration] = useState(0);
const [speed, setSpeed] = useState(1);
const [playing, setPlaying] = useState(false);
const [error, setError] = useState("");
useEffect(() => {
if (!containerRef.current) return;
const recording = new Guacamole.SessionRecording(blob, 100);
recordingRef.current = recording;
const display = recording.getDisplay();
const element = display.getElement();
containerRef.current.replaceChildren(element);
const fit = () => {
const width = display.getWidth();
if (width && containerRef.current) {
display.scale(containerRef.current.clientWidth / width);
}
};
const observer = new ResizeObserver(fit);
observer.observe(containerRef.current);
recording.onload = () => {
setDuration(recording.getDuration() / 1000);
fit();
};
recording.onprogress = (nextDuration) => setDuration(nextDuration / 1000);
recording.onseek = (nextPosition) => {
positionRef.current = nextPosition / 1000;
setPosition(positionRef.current);
};
recording.onerror = setError;
return () => {
observer.disconnect();
recording.abort();
recordingRef.current = null;
};
}, [blob]);
useEffect(() => {
const recording = recordingRef.current;
if (!recording || !playing) return;
recording.pause();
let previous = performance.now();
let nextPosition = positionRef.current;
const interval = window.setInterval(() => {
const now = performance.now();
nextPosition = Math.min(
duration,
nextPosition + ((now - previous) / 1000) * speed,
);
previous = now;
recording.seek(nextPosition * 1000);
if (nextPosition >= duration) setPlaying(false);
}, 100);
return () => clearInterval(interval);
}, [duration, playing, speed]);
if (error) return <div className="p-4 text-xs text-destructive">{error}</div>;
return (
<div className="flex h-full min-h-0 flex-col">
<div
ref={containerRef}
className="min-h-0 flex-1 overflow-hidden bg-black"
/>
<PlaybackControls
playing={playing}
position={position}
duration={duration}
speed={speed}
onToggle={() => {
const recording = recordingRef.current;
if (!recording) return;
if (!playing && position >= duration) recording.seek(0);
setPlaying((value) => !value);
}}
onSeek={(nextPosition) => {
recordingRef.current?.seek(nextPosition * 1000);
positionRef.current = nextPosition;
setPosition(nextPosition);
}}
onSpeed={setSpeed}
/>
</div>
);
}
export function SessionRecordingPlayer({
log,
blob,
}: {
log: SessionLogRecord;
blob: Blob;
}) {
return log.format === "guacamole" ? (
<GuacamolePlayer blob={blob} />
) : (
<AsciicastPlayer blob={blob} />
);
}
@@ -0,0 +1,39 @@
export type AsciicastEvent = [
time: number,
type: "i" | "o" | "r",
data: string,
];
export type Asciicast = {
width: number;
height: number;
duration: number;
events: AsciicastEvent[];
};
export function parseAsciicast(source: string): Asciicast {
const lines = source.trim().split("\n");
const header = JSON.parse(lines.shift() || "{}") as {
version?: number;
width?: number;
height?: number;
};
if (header.version !== 2) throw new Error("Unsupported asciicast version");
const events = lines
.filter(Boolean)
.map((line) => JSON.parse(line) as AsciicastEvent)
.filter(
([time, type, data]) =>
Number.isFinite(time) &&
(type === "i" || type === "o" || type === "r") &&
typeof data === "string",
);
return {
width: header.width || 80,
height: header.height || 24,
duration: events.at(-1)?.[0] || 0,
events,
};
}
+69 -24
View File
@@ -55,8 +55,13 @@ import {
import { ConnectionLog } from "@/ssh/connection-log/ConnectionLog.tsx";
import { toast } from "sonner";
import { Button } from "@/components/button";
import { Save } from "lucide-react";
import { resolveTermixThemeColors } from "./terminal-theme.ts";
import type { TerminalHandle, TerminalHostConfig } from "./terminal-types.ts";
import {
getNextTerminalFontSize,
getTerminalFontZoomDirection,
} from "./terminal-font-zoom.ts";
export type { TerminalHandle, TerminalHostConfig } from "./terminal-types.ts";
type HostKeyVerificationData = Omit<
@@ -81,10 +86,10 @@ interface SSHTerminalProps {
previewTheme?: string | null;
/** When true, suppress automatic focus on connect/visibility change. */
disableAutoFocus?: boolean;
isQuickConnect?: boolean;
onSaveQuickConnect?: () => Promise<void>;
}
const TERMINAL_FONT_ZOOM_MIN = 8;
const TERMINAL_FONT_ZOOM_MAX = 36;
const ALTERNATE_SCREEN_SEQUENCE = /\x1b\[\?(47|1047|1049)([hl])/g;
function updateAlternateScreenMode(output: string, currentMode: boolean) {
@@ -116,6 +121,8 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
onOpenFileInEditor,
previewTheme,
disableAutoFocus = false,
isQuickConnect = false,
onSaveQuickConnect,
},
ref,
) {
@@ -157,6 +164,8 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
const pongReceivedRef = useRef(true);
const pongTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const [isConnected, setIsConnected] = useState(false);
const [isSavingQuickConnect, setIsSavingQuickConnect] = useState(false);
const [isQuickConnectSaved, setIsQuickConnectSaved] = useState(false);
const [isConnecting, setIsConnecting] = useState(false);
const [isFitted, setIsFitted] = useState(false);
const [connectionError, setConnectionError] = useState<string | null>(null);
@@ -223,6 +232,20 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
url: string;
} | null>(null);
useEffect(() => {
if (!linkClickDialog) return;
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key !== "Escape") return;
event.preventDefault();
event.stopPropagation();
setLinkClickDialog(null);
};
window.addEventListener("keydown", handleKeyDown, true);
return () => window.removeEventListener("keydown", handleKeyDown, true);
}, [linkClickDialog]);
const [tmuxSessionPicker, setTmuxSessionPicker] = useState<{
sessions: Array<{
name: string;
@@ -416,20 +439,8 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
}, [isVisible]);
useEffect(() => {
const checkAuth = () => {
setIsAuthenticated((prev) => {
if (!prev) {
return true;
}
return prev;
});
};
checkAuth();
const authCheckInterval = setInterval(checkAuth, 5000);
return () => clearInterval(authCheckInterval);
// One-shot: historical code polled every 5s but only ever flipped false→true.
setIsAuthenticated(true);
}, []);
function hardRefresh() {
@@ -483,16 +494,12 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
}
}
function zoomTerminalFont(deltaY: number) {
const direction = deltaY < 0 ? 1 : -1;
function changeTerminalFontSize(direction: -1 | 1) {
const currentFontSize =
terminal.options.fontSize ??
terminalFontSizeRef.current ??
DEFAULT_TERMINAL_CONFIG.fontSize;
const nextFontSize = Math.min(
TERMINAL_FONT_ZOOM_MAX,
Math.max(TERMINAL_FONT_ZOOM_MIN, currentFontSize + direction),
);
const nextFontSize = getNextTerminalFontSize(currentFontSize, direction);
if (nextFontSize === currentFontSize) {
return;
@@ -2153,7 +2160,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
terminal.attachCustomWheelEventHandler((ev) => {
if (ev.ctrlKey || ev.metaKey) {
zoomTerminalFont(ev.deltaY);
changeTerminalFontSize(ev.deltaY < 0 ? 1 : -1);
return false;
}
@@ -2276,9 +2283,11 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
element?.addEventListener("keydown", handleTabCapture, true);
const resizeObserver = new ResizeObserver(() => {
// Background keep-alive tabs still observe layout; skip fit work.
if (!isVisibleRef.current) return;
if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
resizeTimeout.current = setTimeout(() => {
if (isVisible) {
if (isVisibleRef.current) {
performFit();
}
}, 50);
@@ -2384,6 +2393,14 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
}
}
const fontZoomDirection = getTerminalFontZoomDirection(e);
if (fontZoomDirection !== 0) {
e.preventDefault();
e.stopPropagation();
changeTerminalFontSize(fontZoomDirection);
return false;
}
if (
e.ctrlKey &&
!e.shiftKey &&
@@ -2800,6 +2817,32 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
</button>
)}
{isQuickConnect &&
isConnected &&
!isQuickConnectSaved &&
onSaveQuickConnect && (
<Button
size="sm"
variant="secondary"
disabled={isSavingQuickConnect}
onClick={async () => {
setIsSavingQuickConnect(true);
try {
await onSaveQuickConnect();
setIsQuickConnectSaved(true);
} catch {
// The shell reports the failure with a toast.
} finally {
setIsSavingQuickConnect(false);
}
}}
className="absolute top-2 left-2 z-[110] h-7 gap-1.5 bg-black/60 text-white/80 hover:bg-black/80 hover:text-white"
>
<Save className="size-3.5" />
{t("hosts.addHost")}
</Button>
)}
<SimpleLoader
visible={isConnecting && !isConnectionLogExpanded}
message={t("terminal.connecting")}
@@ -3092,10 +3135,12 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
<div
className="fixed inset-0 flex items-center justify-center z-[10000]"
style={{ backgroundColor: "rgba(0,0,0,0.5)" }}
onClick={() => setLinkClickDialog(null)}
>
<div
className="flex flex-col gap-3 p-4 rounded shadow-lg max-w-sm w-full mx-4"
style={{ backgroundColor }}
onClick={(event) => event.stopPropagation()}
>
<p className="text-xs font-bold uppercase tracking-widest text-muted-foreground">
{t("terminal.linkDialogTitle")}
+1 -1
View File
@@ -45,7 +45,7 @@ export function TerminalPreview({
return (
<div className="border border-input overflow-hidden">
<div
className="p-3 font-mono"
className="p-3"
style={{
fontSize: `${fontSize}px`,
fontFamily: fontFallback,
@@ -54,9 +54,9 @@ export function CommandAutocomplete({
key={index}
ref={index === selectedIndex ? selectedRef : null}
className={cn(
"px-3 py-1.5 text-sm font-mono cursor-pointer transition-colors",
"px-3 py-1.5 text-sm font-mono cursor-pointer transition-colors text-foreground",
"hover:bg-hover",
index === selectedIndex && "bg-surface text-muted-foreground",
index === selectedIndex && "bg-accent text-accent-foreground",
)}
onClick={() => onSelect(suggestion)}
onMouseEnter={() => {}}
@@ -0,0 +1,37 @@
export const TERMINAL_FONT_ZOOM_MIN = 8;
export const TERMINAL_FONT_ZOOM_MAX = 36;
type ZoomKeyEvent = Pick<
KeyboardEvent,
"altKey" | "code" | "ctrlKey" | "key" | "metaKey"
>;
export function getTerminalFontZoomDirection(event: ZoomKeyEvent): -1 | 0 | 1 {
if ((!event.ctrlKey && !event.metaKey) || event.altKey) return 0;
if (
event.key === "+" ||
event.code === "Equal" ||
event.code === "NumpadAdd"
) {
return 1;
}
if (
event.key === "-" ||
event.code === "Minus" ||
event.code === "NumpadSubtract"
) {
return -1;
}
return 0;
}
export function getNextTerminalFontSize(
currentFontSize: number,
direction: -1 | 1,
): number {
return Math.min(
TERMINAL_FONT_ZOOM_MAX,
Math.max(TERMINAL_FONT_ZOOM_MIN, currentFontSize + direction),
);
}
@@ -66,7 +66,6 @@ style.innerHTML = `
}
.xterm .xterm-screen {
font-family: 'Caskaydia Cove Nerd Font Mono', 'SF Mono', Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace !important;
font-variant-ligatures: none;
}
+2 -1
View File
@@ -241,9 +241,10 @@ export function TmuxMonitor({
// -- relative time refresh --------------------------------------------------
useEffect(() => {
if (!isVisible) return;
const interval = setInterval(() => setNow(Date.now()), TIME_TICK_MS);
return () => clearInterval(interval);
}, []);
}, [isVisible]);
// -- overview polling -----------------------------------------------------
const loadOverview = useCallback(
+37 -4
View File
@@ -239,7 +239,14 @@ function TunnelCard({
);
}
export function TunnelTab({ host }: { label: string; host?: DemoHost }) {
export function TunnelTab({
host,
isVisible = true,
}: {
label: string;
host?: DemoHost;
isVisible?: boolean;
}) {
const { t } = useTranslation();
const [sshHost, setSshHost] = useState<SSHHost | null>(null);
const [tunnelStatuses, setTunnelStatuses] = useState<
@@ -264,14 +271,40 @@ export function TunnelTab({ host }: { label: string; host?: DemoHost }) {
}, [host]);
useEffect(() => {
if (!isVisible) return;
fetchHost();
const interval = setInterval(fetchHost, 5000);
window.addEventListener("ssh-hosts:changed", fetchHost);
let intervalId: ReturnType<typeof setInterval> | null = null;
const start = () => {
if (intervalId !== null) return;
// Host config rarely changes; tunnel runtime status uses subscribeTunnelStatuses.
intervalId = setInterval(fetchHost, 30_000);
};
const stop = () => {
if (intervalId === null) return;
clearInterval(intervalId);
intervalId = null;
};
const onVisibility = () => {
if (document.visibilityState === "hidden") {
stop();
return;
}
void fetchHost();
start();
};
if (document.visibilityState !== "hidden") start();
document.addEventListener("visibilitychange", onVisibility);
return () => {
clearInterval(interval);
stop();
document.removeEventListener("visibilitychange", onVisibility);
window.removeEventListener("ssh-hosts:changed", fetchHost);
};
}, [fetchHost]);
}, [fetchHost, isVisible]);
useEffect(() => {
return subscribeTunnelStatuses(setTunnelStatuses, () => {});
+67
View File
@@ -0,0 +1,67 @@
import { useEffect, useRef } from "react";
function isDocumentVisible(): boolean {
if (typeof document === "undefined") return true;
return document.visibilityState !== "hidden";
}
type Options = {
/** Run callback once when the effect mounts (if visible). Default true. */
runOnMount?: boolean;
};
/**
* Runs `callback` on `intervalMs` while the page is visible.
* Pauses when the tab is backgrounded; fires once on resume.
*/
export function usePageVisibleInterval(
callback: () => void,
intervalMs: number,
enabled = true,
options: Options = {},
): void {
const { runOnMount = true } = options;
const callbackRef = useRef(callback);
callbackRef.current = callback;
useEffect(() => {
if (!enabled || intervalMs <= 0) return;
let intervalId: ReturnType<typeof setInterval> | null = null;
const tick = () => {
callbackRef.current();
};
const start = () => {
if (intervalId !== null) return;
intervalId = setInterval(tick, intervalMs);
};
const stop = () => {
if (intervalId === null) return;
clearInterval(intervalId);
intervalId = null;
};
const onVisibility = () => {
if (isDocumentVisible()) {
tick();
start();
} else {
stop();
}
};
if (isDocumentVisible()) {
if (runOnMount) tick();
start();
}
document.addEventListener("visibilitychange", onVisibility);
return () => {
stop();
document.removeEventListener("visibilitychange", onVisibility);
};
}, [intervalMs, enabled, runOnMount]);
}
+29 -10
View File
@@ -43,31 +43,43 @@ const localeLoaders = {
"zh-TW": () => import("../locales/translated/zh_TW.json"),
} satisfies Record<string, () => Promise<LocaleModule>>;
const supportedLngs = ["en", ...Object.keys(localeLoaders)];
export const supportedLngs = ["en", ...Object.keys(localeLoaders)];
export function normalizeLanguageCode(language?: string | null): string {
if (!language) return "en";
const normalized = language.replaceAll("_", "-");
if (supportedLngs.includes(normalized)) return normalized;
const exactMatch = supportedLngs.find(
(supported) => supported.toLowerCase() === normalized.toLowerCase(),
);
if (exactMatch) return exactMatch;
const baseLanguage = normalized.split("-")[0];
return supportedLngs.includes(baseLanguage) ? baseLanguage : "en";
}
const localeBackend: BackendModule = {
type: "backend",
init: () => {},
read: (language, _namespace, callback) => {
if (language === "en") {
const normalizedLanguage = normalizeLanguageCode(language);
if (normalizedLanguage === "en") {
callback(null, enTranslation);
return;
}
const loadLocale = localeLoaders[language];
const loadLocale = localeLoaders[normalizedLanguage];
if (!loadLocale) {
callback(new Error(`Unsupported language: ${language}`), false);
callback(null, enTranslation);
return;
}
loadLocale()
.then((module) => callback(null, module.default))
.catch((error: unknown) => {
callback(
error instanceof Error ? error : new Error(String(error)),
false,
);
});
.catch(() => callback(null, enTranslation));
},
};
@@ -104,4 +116,11 @@ i18n
},
});
export async function changeAppLanguage(language: string): Promise<string> {
const normalizedLanguage = normalizeLanguageCode(language);
await i18n.changeLanguage(normalizedLanguage);
localStorage.setItem("i18nextLng", normalizedLanguage);
return normalizedLanguage;
}
export default i18n;
+124 -122
View File
@@ -176,24 +176,24 @@
.dracula {
--background: #282a36;
--foreground: #f8f8f2;
--card: #21222c;
--card: #2d303d;
--card-foreground: #f8f8f2;
--popover: #21222c;
--popover: #2d303d;
--popover-foreground: #f8f8f2;
--primary: #f8f8f2;
--primary: #8be9fd;
--primary-foreground: #282a36;
--secondary: #44475a;
--secondary: #50536b;
--secondary-foreground: #f8f8f2;
--surface: #21222c;
--surface-dim: #191a21;
--muted: #44475a;
--muted-foreground: #6272a4;
--accent: #44475a;
--surface: #2d303d;
--surface-dim: #21222c;
--muted: #50536b;
--muted-foreground: #a8b3d9;
--accent: #5a5e7a;
--accent-foreground: #f8f8f2;
--destructive: #ff5555;
--border: rgba(98, 114, 164, 0.25);
--input: rgba(98, 114, 164, 0.2);
--ring: #6272a4;
--border: rgba(168, 179, 217, 0.18);
--input: rgba(168, 179, 217, 0.14);
--ring: #8be9fd;
--chart-1: #ff79c6;
--chart-2: #8be9fd;
--chart-3: #50fa7b;
@@ -202,36 +202,36 @@
--radius: 0.625rem;
--sidebar: #21222c;
--sidebar-foreground: #f8f8f2;
--sidebar-primary: #bd93f9;
--sidebar-primary: #8be9fd;
--sidebar-primary-foreground: #282a36;
--sidebar-accent: #44475a;
--sidebar-accent: #5a5e7a;
--sidebar-accent-foreground: #f8f8f2;
--sidebar-border: rgba(98, 114, 164, 0.25);
--sidebar-ring: #6272a4;
--sidebar-border: rgba(168, 179, 217, 0.18);
--sidebar-ring: #8be9fd;
}
/* ── Catppuccin Mocha ───────────────────────────────────────── */
.catppuccin {
--background: #1e1e2e;
--foreground: #cdd6f4;
--card: #181825;
--card: #202235;
--card-foreground: #cdd6f4;
--popover: #181825;
--popover: #202235;
--popover-foreground: #cdd6f4;
--primary: #cdd6f4;
--primary: #89b4fa;
--primary-foreground: #1e1e2e;
--secondary: #313244;
--secondary: #3b3d52;
--secondary-foreground: #cdd6f4;
--surface: #181825;
--surface-dim: #11111b;
--muted: #313244;
--muted-foreground: #6c7086;
--accent: #313244;
--surface: #202235;
--surface-dim: #181825;
--muted: #3b3d52;
--muted-foreground: #b8c0e0;
--accent: #45475a;
--accent-foreground: #cdd6f4;
--destructive: #f38ba8;
--border: rgba(108, 112, 134, 0.25);
--input: rgba(108, 112, 134, 0.2);
--ring: #6c7086;
--border: rgba(186, 194, 222, 0.18);
--input: rgba(186, 194, 222, 0.14);
--ring: #89b4fa;
--chart-1: #f38ba8;
--chart-2: #89dceb;
--chart-3: #a6e3a1;
@@ -240,35 +240,35 @@
--radius: 0.625rem;
--sidebar: #181825;
--sidebar-foreground: #cdd6f4;
--sidebar-primary: #cba6f7;
--sidebar-primary: #89b4fa;
--sidebar-primary-foreground: #1e1e2e;
--sidebar-accent: #313244;
--sidebar-accent: #45475a;
--sidebar-accent-foreground: #cdd6f4;
--sidebar-border: rgba(108, 112, 134, 0.25);
--sidebar-ring: #6c7086;
--sidebar-border: rgba(186, 194, 222, 0.18);
--sidebar-ring: #89b4fa;
}
/* ── Nord ───────────────────────────────────────────────────── */
.nord {
--background: #2e3440;
--foreground: #eceff4;
--card: #303642;
--card: #353c4a;
--card-foreground: #eceff4;
--popover: #303642;
--popover: #353c4a;
--popover-foreground: #eceff4;
--primary: #eceff4;
--primary: #88c0d0;
--primary-foreground: #2e3440;
--secondary: #3b4252;
--secondary: #434c5e;
--secondary-foreground: #eceff4;
--surface: #303642;
--surface-dim: #262b35;
--muted: #3b4252;
--muted-foreground: #8899b0;
--accent: #3b4252;
--surface: #353c4a;
--surface-dim: #2a303b;
--muted: #434c5e;
--muted-foreground: #b4c0d4;
--accent: #4c566a;
--accent-foreground: #eceff4;
--destructive: #bf616a;
--border: rgba(136, 192, 208, 0.2);
--input: rgba(136, 192, 208, 0.15);
--border: rgba(180, 192, 212, 0.18);
--input: rgba(180, 192, 212, 0.14);
--ring: #88c0d0;
--chart-1: #bf616a;
--chart-2: #88c0d0;
@@ -276,38 +276,38 @@
--chart-4: #ebcb8b;
--chart-5: #b48ead;
--radius: 0.625rem;
--sidebar: #282d39;
--sidebar: #2a303b;
--sidebar-foreground: #eceff4;
--sidebar-primary: #88c0d0;
--sidebar-primary-foreground: #2e3440;
--sidebar-accent: #3b4252;
--sidebar-accent: #4c566a;
--sidebar-accent-foreground: #eceff4;
--sidebar-border: rgba(136, 192, 208, 0.2);
--sidebar-border: rgba(180, 192, 212, 0.18);
--sidebar-ring: #88c0d0;
}
/* ── Solarized Dark ─────────────────────────────────────────── */
.solarized {
--background: #002b36;
--foreground: #839496;
--foreground: #b7c4c4;
--card: #073642;
--card-foreground: #93a1a1;
--card-foreground: #b7c4c4;
--popover: #073642;
--popover-foreground: #93a1a1;
--primary: #93a1a1;
--popover-foreground: #b7c4c4;
--primary: #268bd2;
--primary-foreground: #002b36;
--secondary: #073642;
--secondary-foreground: #839496;
--secondary: #0b4654;
--secondary-foreground: #b7c4c4;
--surface: #073642;
--surface-dim: #00212b;
--muted: #073642;
--muted-foreground: #586e75;
--accent: #073642;
--accent-foreground: #93a1a1;
--muted: #0b4654;
--muted-foreground: #9fb3b3;
--accent: #105667;
--accent-foreground: #d5dddd;
--destructive: #dc322f;
--border: rgba(88, 110, 117, 0.3);
--input: rgba(88, 110, 117, 0.25);
--ring: #586e75;
--border: rgba(159, 179, 179, 0.2);
--input: rgba(159, 179, 179, 0.16);
--ring: #268bd2;
--chart-1: #dc322f;
--chart-2: #2aa198;
--chart-3: #859900;
@@ -315,37 +315,38 @@
--chart-5: #6c71c4;
--radius: 0.625rem;
--sidebar: #073642;
--sidebar-foreground: #93a1a1;
--sidebar-foreground: #b7c4c4;
--sidebar-primary: #268bd2;
--sidebar-primary-foreground: #002b36;
--sidebar-accent: #073642;
--sidebar-accent-foreground: #93a1a1;
--sidebar-border: rgba(88, 110, 117, 0.3);
--sidebar-ring: #586e75;
--sidebar-accent: #105667;
--sidebar-accent-foreground: #d5dddd;
--sidebar-border: rgba(159, 179, 179, 0.2);
--sidebar-ring: #268bd2;
}
/* ── Tokyo Night ────────────────────────────────────────────── */
.tokyo-night {
--background: #1a1b26;
--foreground: #a9b1d6;
--card: #16161e;
--card-foreground: #a9b1d6;
--popover: #16161e;
--popover-foreground: #a9b1d6;
--primary: #a9b1d6;
--foreground: #c0caf5;
--card: #1f2335;
--card-foreground: #c0caf5;
--popover: #1f2335;
--popover-foreground: #c0caf5;
--primary: #7aa2f7;
--primary-foreground: #1a1b26;
--secondary: #24283b;
--secondary-foreground: #a9b1d6;
--surface: #16161e;
--surface-dim: #13131a;
--muted: #24283b;
--muted-foreground: #565f89;
--accent: #24283b;
--accent-foreground: #a9b1d6;
--secondary: #2a2f47;
--secondary-foreground: #c0caf5;
--surface: #1f2335;
--surface-dim: #16161e;
--muted: #2a2f47;
--muted-foreground: #8b93bd;
--accent: #2f3654;
--accent-foreground: #c0caf5;
--destructive: #f7768e;
--border: rgba(86, 95, 137, 0.25);
--input: rgba(86, 95, 137, 0.2);
--ring: #565f89;
--border: rgba(139, 147, 189, 0.2);
--input: rgba(139, 147, 189, 0.16);
--ring: #7aa2f7;
--chart-1: #f7768e;
--chart-2: #7dcfff;
--chart-3: #9ece6a;
@@ -353,37 +354,37 @@
--chart-5: #bb9af7;
--radius: 0.625rem;
--sidebar: #16161e;
--sidebar-foreground: #a9b1d6;
--sidebar-foreground: #c0caf5;
--sidebar-primary: #7aa2f7;
--sidebar-primary-foreground: #1a1b26;
--sidebar-accent: #24283b;
--sidebar-accent-foreground: #a9b1d6;
--sidebar-border: rgba(86, 95, 137, 0.25);
--sidebar-ring: #565f89;
--sidebar-accent: #2f3654;
--sidebar-accent-foreground: #c0caf5;
--sidebar-border: rgba(139, 147, 189, 0.2);
--sidebar-ring: #7aa2f7;
}
/* ── One Dark ───────────────────────────────────────────────── */
.one-dark {
--background: #282c34;
--foreground: #abb2bf;
--card: #21252b;
--card: #2f343d;
--card-foreground: #abb2bf;
--popover: #21252b;
--popover: #2f343d;
--popover-foreground: #abb2bf;
--primary: #abb2bf;
--primary: #61afef;
--primary-foreground: #282c34;
--secondary: #2c313a;
--secondary: #3a404b;
--secondary-foreground: #abb2bf;
--surface: #21252b;
--surface-dim: #1b1f23;
--muted: #2c313a;
--muted-foreground: #5c6370;
--accent: #2c313a;
--surface: #2f343d;
--surface-dim: #21252b;
--muted: #3a404b;
--muted-foreground: #9aa3b2;
--accent: #424957;
--accent-foreground: #abb2bf;
--destructive: #e06c75;
--border: rgba(92, 99, 112, 0.25);
--input: rgba(92, 99, 112, 0.2);
--ring: #5c6370;
--border: rgba(154, 163, 178, 0.18);
--input: rgba(154, 163, 178, 0.14);
--ring: #61afef;
--chart-1: #e06c75;
--chart-2: #56b6c2;
--chart-3: #98c379;
@@ -394,48 +395,49 @@
--sidebar-foreground: #abb2bf;
--sidebar-primary: #61afef;
--sidebar-primary-foreground: #282c34;
--sidebar-accent: #2c313a;
--sidebar-accent: #424957;
--sidebar-accent-foreground: #abb2bf;
--sidebar-border: rgba(92, 99, 112, 0.25);
--sidebar-ring: #5c6370;
--sidebar-border: rgba(154, 163, 178, 0.18);
--sidebar-ring: #61afef;
}
/* ── Gruvbox Dark ───────────────────────────────────────────── */
.gruvbox {
--background: #282828;
--foreground: #ebdbb2;
--card: #1d2021;
--card: #32302f;
--card-foreground: #ebdbb2;
--popover: #1d2021;
--popover: #32302f;
--popover-foreground: #ebdbb2;
--primary: #ebdbb2;
--primary: #fabd2f;
--primary-foreground: #282828;
--secondary: #3c3836;
--secondary: #45403d;
--secondary-foreground: #ebdbb2;
--surface: #1d2021;
--surface-dim: #181a1a;
--muted: #3c3836;
--muted-foreground: #928374;
--accent: #3c3836;
--surface: #32302f;
--surface-dim: #1d2021;
--muted: #45403d;
--muted-foreground: #c4b59d;
--accent: #504945;
--accent-foreground: #ebdbb2;
--destructive: #cc241d;
--border: rgba(146, 131, 116, 0.2);
--input: rgba(146, 131, 116, 0.15);
--ring: #928374;
--chart-1: #cc241d;
--chart-2: #689d6a;
--chart-3: #98971a;
--chart-4: #d79921;
--chart-5: #b16286;
--destructive: #fb4934;
--border: rgba(196, 181, 157, 0.18);
--input: rgba(196, 181, 157, 0.14);
--ring: #fabd2f;
--chart-1: #fb4934;
--chart-2: #8ec07c;
--chart-3: #b8bb26;
--chart-4: #fabd2f;
--chart-5: #d3869b;
--radius: 0.625rem;
--sidebar: #1d2021;
--sidebar-foreground: #ebdbb2;
--sidebar-primary: #fabd2f;
--sidebar-primary-foreground: #282828;
--sidebar-accent: #3c3836;
--sidebar-accent: #504945;
--sidebar-accent-foreground: #ebdbb2;
--sidebar-border: rgba(146, 131, 116, 0.2);
--sidebar-ring: #928374;
--sidebar-border: rgba(196, 181, 157, 0.18);
--sidebar-ring: #fabd2f;
}
@layer base {
+187 -101
View File
@@ -2,21 +2,20 @@
import React, {
createContext,
useContext,
useState,
useEffect,
useCallback,
useRef,
useMemo,
useSyncExternalStore,
useState,
} from "react";
import { getAllServerStatuses, getSSHHosts } from "@/main-axios";
import { DEFAULT_STATS_CONFIG } from "@/types/stats-widgets";
type StatusValue = "online" | "offline" | "degraded";
interface ServerStatusEntry {
status: StatusValue;
lastChecked: string;
}
import {
ServerStatusStore,
type ServerStatusEntry,
type StatusValue,
} from "./server-status-store";
interface ServerStatusContextType {
statuses: Map<number, ServerStatusEntry>;
@@ -26,6 +25,8 @@ interface ServerStatusContextType {
getStatus: (hostId: number) => StatusValue;
}
/** Stable for the provider lifetime — fine-grained hooks only need this. */
const StatusStoreContext = createContext<ServerStatusStore | null>(null);
const ServerStatusContext = createContext<ServerStatusContextType | null>(null);
const POLL_INTERVAL = 30000;
@@ -37,34 +38,47 @@ export function ServerStatusProvider({
children: React.ReactNode;
isAuthenticated?: boolean;
}) {
const [statuses, setStatuses] = useState<Map<number, ServerStatusEntry>>(
new Map(),
);
const [isLoading, setIsLoading] = useState(false);
const [initialLoadComplete, setInitialLoadComplete] = useState(false);
const [enabledHostIds, setEnabledHostIds] = useState<Set<number>>(new Set());
const storeRef = useRef<ServerStatusStore | null>(null);
if (!storeRef.current) {
storeRef.current = new ServerStatusStore();
}
const store = storeRef.current;
// Bumps only full-context consumers (dashboard, folder counts, etc.).
const [version, setVersion] = useState(0);
const mountedRef = useRef(true);
const enabledHostIdsRef = useRef(enabledHostIds);
const refreshInFlightRef = useRef<Promise<void> | null>(null);
useEffect(() => {
enabledHostIdsRef.current = enabledHostIds;
}, [enabledHostIds]);
return store.subscribeAll(() => {
setVersion((v) => v + 1);
});
}, [store]);
useEffect(() => {
return store.subscribeMeta(() => {
setVersion((v) => v + 1);
});
}, [store]);
const fetchEnabledHosts = useCallback(async () => {
if (!isAuthenticated) {
store.setEnabledHostIds(new Set());
return new Set<number>();
}
try {
const hosts = await getSSHHosts();
const hosts = await getSSHHosts({ includeStatus: false });
const enabled = new Set<number>();
hosts.forEach((host) => {
const statsConfig = (() => {
try {
return host.statsConfig
? JSON.parse(host.statsConfig)
: DEFAULT_STATS_CONFIG;
if (!host.statsConfig) return DEFAULT_STATS_CONFIG;
if (typeof host.statsConfig === "string") {
return JSON.parse(host.statsConfig);
}
return host.statsConfig;
} catch {
return DEFAULT_STATS_CONFIG;
}
@@ -75,94 +89,122 @@ export function ServerStatusProvider({
}
});
setEnabledHostIds((prev) => {
if (prev.size !== enabled.size) return enabled;
for (const id of enabled) {
if (!prev.has(id)) return enabled;
}
return prev;
});
store.setEnabledHostIds(enabled);
return enabled;
} catch {
return new Set<number>();
return store.getEnabledHostIds();
}
}, [isAuthenticated]);
}, [isAuthenticated, store]);
const refreshStatuses = useCallback(async () => {
if (!mountedRef.current || !isAuthenticated) return;
setIsLoading(true);
try {
const data = await getAllServerStatuses();
if (!mountedRef.current) return;
const newStatuses = new Map<number, ServerStatusEntry>();
const now = new Date().toISOString();
if (data && typeof data === "object") {
Object.entries(data).forEach(([idStr, statusData]) => {
const id = parseInt(idStr, 10);
if (!isNaN(id)) {
const status =
statusData?.status === "online" ? "online" : "offline";
newStatuses.set(id, {
status,
lastChecked: statusData?.lastChecked || now,
});
}
});
}
setStatuses(newStatuses);
} catch {
if (mountedRef.current) {
setStatuses((prev) => {
const updated = new Map(prev);
enabledHostIdsRef.current.forEach((id) => {
const existing = updated.get(id);
updated.set(id, {
status: "degraded",
lastChecked: existing?.lastChecked || new Date().toISOString(),
});
});
return updated;
});
}
} finally {
if (mountedRef.current) {
setIsLoading(false);
setInitialLoadComplete(true);
}
if (
typeof document !== "undefined" &&
document.visibilityState === "hidden"
) {
return;
}
}, [isAuthenticated]);
const stableEnabledHostIds = useMemo(() => enabledHostIds, [enabledHostIds]);
if (refreshInFlightRef.current) {
return refreshInFlightRef.current;
}
const showLoading = !store.getInitialLoadComplete();
if (showLoading) store.setLoading(true);
const run = (async () => {
try {
const data = await getAllServerStatuses();
if (!mountedRef.current) return;
const newStatuses = new Map<number, ServerStatusEntry>();
const now = new Date().toISOString();
if (data && typeof data === "object") {
Object.entries(data).forEach(([idStr, statusData]) => {
const id = parseInt(idStr, 10);
if (!isNaN(id)) {
const status =
statusData?.status === "online" ? "online" : "offline";
newStatuses.set(id, {
status,
lastChecked: statusData?.lastChecked || now,
});
}
});
}
store.applyStatuses(newStatuses);
} catch {
if (mountedRef.current) {
store.markDegraded(store.getEnabledHostIds());
}
} finally {
if (mountedRef.current) {
if (showLoading) store.setLoading(false);
store.setInitialLoadComplete(true);
}
}
})();
refreshInFlightRef.current = run.finally(() => {
refreshInFlightRef.current = null;
});
return refreshInFlightRef.current;
}, [isAuthenticated, store]);
const getStatus = useCallback(
(hostId: number): StatusValue => {
if (!stableEnabledHostIds.has(hostId)) {
return "offline";
}
return statuses.get(hostId)?.status || "degraded";
},
[statuses, stableEnabledHostIds],
(hostId: number): StatusValue => store.getStatus(hostId),
[store],
);
useEffect(() => {
mountedRef.current = true;
let intervalId: ReturnType<typeof setInterval> | null = null;
const startPolling = () => {
if (intervalId !== null) return;
intervalId = setInterval(() => {
void refreshStatuses();
}, POLL_INTERVAL);
};
const stopPolling = () => {
if (intervalId === null) return;
clearInterval(intervalId);
intervalId = null;
};
const init = async () => {
await fetchEnabledHosts();
await refreshStatuses();
if (
mountedRef.current &&
(typeof document === "undefined" ||
document.visibilityState !== "hidden")
) {
startPolling();
}
};
init();
void init();
const intervalId = setInterval(refreshStatuses, POLL_INTERVAL);
const onVisibility = () => {
if (document.visibilityState === "hidden") {
stopPolling();
return;
}
void refreshStatuses();
startPolling();
};
document.addEventListener("visibilitychange", onVisibility);
return () => {
mountedRef.current = false;
clearInterval(intervalId);
stopPolling();
document.removeEventListener("visibilitychange", onVisibility);
};
}, [fetchEnabledHosts, refreshStatuses]);
@@ -181,19 +223,34 @@ export function ServerStatusProvider({
};
}, [fetchEnabledHosts, refreshStatuses]);
return (
<ServerStatusContext.Provider
value={{
statuses,
isLoading,
initialLoadComplete,
refreshStatuses,
getStatus,
}}
>
{children}
</ServerStatusContext.Provider>
const contextValue = useMemo(
() => ({
statuses: store.getStatuses(),
isLoading: store.getIsLoading(),
initialLoadComplete: store.getInitialLoadComplete(),
refreshStatuses,
getStatus,
}),
// version refreshes statuses/isLoading/initialLoadComplete snapshots
// eslint-disable-next-line react-hooks/exhaustive-deps
[version, refreshStatuses, getStatus],
);
return (
<StatusStoreContext.Provider value={store}>
<ServerStatusContext.Provider value={contextValue}>
{children}
</ServerStatusContext.Provider>
</StatusStoreContext.Provider>
);
}
function useStatusStore(): ServerStatusStore {
const store = useContext(StatusStoreContext);
if (!store) {
throw new Error("Server status store hooks require ServerStatusProvider");
}
return store;
}
export function useServerStatus() {
@@ -206,15 +263,44 @@ export function useServerStatus() {
return context;
}
/**
* Subscribe to a single host's status. Only re-renders when that host's
* status value changes (or its enabled flag flips). Does not re-render when
* other hosts update.
*/
export function useHostStatus(
hostId: number,
statusCheckEnabled: boolean = true,
) {
const { getStatus } = useServerStatus();
): StatusValue | null {
const store = useStatusStore();
const status = useSyncExternalStore(
(onChange) => store.subscribeHost(hostId, onChange),
() => store.getHostSnapshot(hostId),
() => store.getHostSnapshot(hostId),
);
if (!statusCheckEnabled) {
return "offline" as StatusValue;
return null;
}
return status;
}
return getStatus(hostId);
/** Meta flags without depending on the full status map. */
export function useServerStatusMeta(): {
initialLoadComplete: boolean;
isLoading: boolean;
} {
const store = useStatusStore();
useSyncExternalStore(
(onChange) => store.subscribeMeta(onChange),
() => store.getMetaSnapshot(),
() => store.getMetaSnapshot(),
);
return {
initialLoadComplete: store.getInitialLoadComplete(),
isLoading: store.getIsLoading(),
};
}
+33
View File
@@ -0,0 +1,33 @@
import { getBasePath } from "./base-path";
type BrowserLocation = Pick<
Location,
"protocol" | "host" | "hostname" | "port"
>;
interface DatabaseTransferUrlOptions {
electron: boolean;
configuredServerUrl?: string | null;
location: BrowserLocation;
}
export function getDatabaseTransferUrl(
operation: "export" | "import",
{ electron, configuredServerUrl, location }: DatabaseTransferUrlOptions,
): string {
if (electron) {
const serverUrl = configuredServerUrl || "http://localhost:30001";
return `${serverUrl.replace(/\/$/, "")}/database/${operation}`;
}
const development =
location.port === "5173" ||
location.hostname === "localhost" ||
location.hostname === "127.0.0.1";
if (development) {
return `http://localhost:30001/database/${operation}`;
}
return `${location.protocol}//${location.host}${getBasePath()}/database/${operation}`;
}
+20
View File
@@ -0,0 +1,20 @@
import { isElectron } from "./electron";
export function installElectronWheelZoomGuard(): () => void {
if (!isElectron()) return () => {};
const preventModifierWheelZoom = (event: WheelEvent) => {
if (!event.metaKey && !event.ctrlKey) return;
event.preventDefault();
};
window.addEventListener("wheel", preventModifierWheelZoom, {
capture: true,
passive: false,
});
return () =>
window.removeEventListener("wheel", preventModifierWheelZoom, {
capture: true,
});
}
+53
View File
@@ -0,0 +1,53 @@
import type { SSHHost } from "@/types/index";
import type { ServerStatus } from "@/main-axios";
import { createTtlRequestCache } from "./ttl-request-cache";
/** Host list changes less often than status; keep a short shared window. */
const HOSTS_TTL_MS = 10_000;
/** Status polls stack from many UI surfaces; short TTL + inflight dedupe. */
const STATUS_TTL_MS = 3_000;
const hostsCache = createTtlRequestCache<SSHHost[]>(HOSTS_TTL_MS);
const statusCache =
createTtlRequestCache<Record<number, ServerStatus>>(STATUS_TTL_MS);
let listenersBound = false;
function bindInvalidationListeners(): void {
if (listenersBound || typeof window === "undefined") return;
listenersBound = true;
const invalidateHosts = () => {
hostsCache.invalidate();
};
window.addEventListener("ssh-hosts:changed", invalidateHosts);
window.addEventListener("hosts:refresh", invalidateHosts);
}
export function getCachedSSHHosts(
loader: () => Promise<SSHHost[]>,
): Promise<SSHHost[]> {
bindInvalidationListeners();
return hostsCache.get(loader);
}
export function getCachedServerStatuses(
loader: () => Promise<Record<number, ServerStatus>>,
): Promise<Record<number, ServerStatus>> {
return statusCache.get(loader);
}
export function invalidateSSHHostsCache(): void {
hostsCache.invalidate();
}
export function invalidateServerStatusCache(): void {
statusCache.invalidate();
}
/** Drop both caches after host mutations so the next read is fresh. */
export function invalidateHostsAndStatusCaches(): void {
hostsCache.invalidate();
statusCache.invalidate();
}
+184
View File
@@ -0,0 +1,184 @@
export type StatusValue = "online" | "offline" | "degraded";
export interface ServerStatusEntry {
status: StatusValue;
lastChecked: string;
}
type Listener = () => void;
/**
* Mutable store for per-host status so list rows can subscribe to a single host
* via useSyncExternalStore instead of re-rendering the whole tree on every poll.
*/
export class ServerStatusStore {
private statuses = new Map<number, ServerStatusEntry>();
private enabledHostIds = new Set<number>();
private initialLoadComplete = false;
private isLoading = false;
private readonly hostListeners = new Map<number, Set<Listener>>();
private readonly allListeners = new Set<Listener>();
private readonly metaListeners = new Set<Listener>();
getStatus(hostId: number): StatusValue {
if (!this.enabledHostIds.has(hostId)) {
return "offline";
}
return this.statuses.get(hostId)?.status || "degraded";
}
getStatuses(): Map<number, ServerStatusEntry> {
return this.statuses;
}
getEnabledHostIds(): Set<number> {
return this.enabledHostIds;
}
getInitialLoadComplete(): boolean {
return this.initialLoadComplete;
}
getIsLoading(): boolean {
return this.isLoading;
}
/** Snapshot string for a host — used as useSyncExternalStore getSnapshot. */
getHostSnapshot(hostId: number): StatusValue {
return this.getStatus(hostId);
}
getMetaSnapshot(): string {
return `${this.initialLoadComplete ? 1 : 0}:${this.isLoading ? 1 : 0}:${this.enabledHostIds.size}`;
}
subscribeHost(hostId: number, listener: Listener): () => void {
let set = this.hostListeners.get(hostId);
if (!set) {
set = new Set();
this.hostListeners.set(hostId, set);
}
set.add(listener);
return () => {
set!.delete(listener);
if (set!.size === 0) {
this.hostListeners.delete(hostId);
}
};
}
subscribeAll(listener: Listener): () => void {
this.allListeners.add(listener);
return () => {
this.allListeners.delete(listener);
};
}
subscribeMeta(listener: Listener): () => void {
this.metaListeners.add(listener);
return () => {
this.metaListeners.delete(listener);
};
}
setEnabledHostIds(next: Set<number>): void {
if (setsEqual(this.enabledHostIds, next)) return;
const prev = this.enabledHostIds;
this.enabledHostIds = next;
this.emitMeta();
// Hosts that entered/left the enabled set need a status re-read.
for (const id of next) {
if (!prev.has(id)) this.emitHost(id);
}
for (const id of prev) {
if (!next.has(id)) this.emitHost(id);
}
this.emitAll();
}
setLoading(loading: boolean): void {
if (this.isLoading === loading) return;
this.isLoading = loading;
this.emitMeta();
this.emitAll();
}
setInitialLoadComplete(complete: boolean): void {
if (this.initialLoadComplete === complete) return;
this.initialLoadComplete = complete;
this.emitMeta();
this.emitAll();
}
/**
* Replace status map. Notifies only hosts whose status value changed,
* plus allListeners when any change occurred.
*/
applyStatuses(next: Map<number, ServerStatusEntry>): void {
const changedIds: number[] = [];
for (const [id, entry] of next) {
const prev = this.statuses.get(id);
if (!prev || prev.status !== entry.status) {
changedIds.push(id);
}
}
for (const id of this.statuses.keys()) {
if (!next.has(id)) {
changedIds.push(id);
}
}
if (changedIds.length === 0) {
// Still refresh lastChecked silently without notifying.
this.statuses = next;
return;
}
this.statuses = next;
for (const id of changedIds) {
this.emitHost(id);
}
this.emitAll();
}
markDegraded(enabledIds: Iterable<number>): void {
const next = new Map(this.statuses);
let changed = false;
const now = new Date().toISOString();
for (const id of enabledIds) {
const existing = next.get(id);
if (existing?.status === "degraded") continue;
changed = true;
next.set(id, {
status: "degraded",
lastChecked: existing?.lastChecked || now,
});
}
if (changed) {
this.applyStatuses(next);
}
}
private emitHost(hostId: number): void {
const set = this.hostListeners.get(hostId);
if (!set) return;
for (const listener of set) listener();
}
private emitAll(): void {
for (const listener of this.allListeners) listener();
}
private emitMeta(): void {
for (const listener of this.metaListeners) listener();
}
}
function setsEqual(a: Set<number>, b: Set<number>): boolean {
if (a.size !== b.size) return false;
for (const id of a) {
if (!b.has(id)) return false;
}
return true;
}
@@ -61,6 +61,8 @@ const MAX_LINE_LENGTH = 2000;
// (mc, nano, vim, htop). If a chunk contains these, highlighting can inject
// extra SGR bytes into a full-screen redraw and corrupt xterm's cursor state.
const TUI_SEQUENCE = /\x1b\[(?:[\d;]*[ABCDEFGHJKST]|\?[\d;]*[hl])/;
const TUI_FRAME_SEQUENCE = /[\u2500-\u257f]|\x1b[()][0B]|\x1b\[[0-9;]*[~`]/;
const CONTROL_STRING_SEQUENCE = /\x1b[\]P^_]/;
// A bare \r (not immediately followed by \n) means the terminal is overwriting
// the current line (shell prompts, progress bars). Highlighting mid-rewrite
@@ -376,6 +378,7 @@ function highlightLine(
const bare = cr ? line.slice(0, -1) : line;
if (!bare.trim()) return line;
if (bare.length > MAX_LINE_LENGTH) return line;
if (isShellPromptLine(bare)) return line;
const segments = parseAnsiSegments(bare);
@@ -398,6 +401,8 @@ export function highlightTerminalOutput(
if (hasIncompleteAnsiSequence(text)) return text;
if (TUI_SEQUENCE.test(text)) return text;
if (TUI_FRAME_SEQUENCE.test(text)) return text;
if (CONTROL_STRING_SEQUENCE.test(text)) return text;
if (MID_LINE_CR.test(text)) return text;
const activePatterns = buildActivePatterns(options);
+48
View File
@@ -0,0 +1,48 @@
/**
* Short-lived in-memory cache with in-flight request deduplication.
* Concurrent callers share one loader promise; failures are not cached.
*/
export type TtlRequestCache<T> = {
get(loader: () => Promise<T>): Promise<T>;
invalidate(): void;
peek(): T | null;
};
export function createTtlRequestCache<T>(ttlMs: number): TtlRequestCache<T> {
let cached: { value: T; expiresAt: number } | null = null;
let inflight: Promise<T> | null = null;
return {
get(loader: () => Promise<T>): Promise<T> {
if (cached && Date.now() < cached.expiresAt) {
return Promise.resolve(cached.value);
}
if (inflight) {
return inflight;
}
inflight = (async () => {
try {
const value = await loader();
cached = { value, expiresAt: Date.now() + ttlMs };
return value;
} finally {
inflight = null;
}
})();
return inflight;
},
invalidate(): void {
cached = null;
},
peek(): T | null {
if (cached && Date.now() < cached.expiresAt) {
return cached.value;
}
return null;
},
};
}
+418 -287
View File
@@ -106,8 +106,265 @@
"filterTagsGroup": "Tags"
},
"homepage": {
"failedToLoadAlerts": "Failed to load alerts",
"failedToDismissAlert": "Failed to dismiss alert"
"title": "Homepage",
"addWidget": "Add Widget",
"editWidget": "Edit Widget",
"deleteWidget": "Delete",
"widgetTypes": "Widget Types",
"serviceLink": "Service Link",
"folder": "Folder",
"clock": "Clock",
"notes": "Notes",
"hostStatus": "Host Status",
"bookmarkList": "Bookmarks",
"zoomIn": "Zoom In",
"zoomOut": "Zoom Out",
"resetView": "Reset View",
"lockLayout": "Lock Layout",
"unlockLayout": "Unlock Layout",
"noWidgets": "Right-click or click + to add your first widget",
"openFullView": "Open Full View",
"previewTitle": "Homepage Preview",
"cancel": "Cancel",
"save": "Save",
"title_label": "Title",
"widgetTitlePlaceholder": "Widget title (optional)",
"url": "URL",
"imageUrl": "Custom Image URL",
"imageUrlHint": "Leave blank to use the site favicon automatically",
"showImage": "Show Image",
"description": "Description",
"color": "Color",
"icon": "Icon",
"expanded": "Expanded by default",
"timezone": "Timezone",
"showSeconds": "Show seconds",
"format12h": "12-hour",
"format24h": "24-hour",
"content": "Content",
"backgroundColor": "Background Color",
"host": "Host",
"showMetrics": "Show Metrics",
"links": "Links",
"addLink": "Add Link",
"linkLabel": "Label",
"linkUrl": "URL",
"removeLink": "Remove",
"categoryLinks": "Links",
"categoryInfo": "Info",
"categorySystem": "System",
"widgetServiceLinkDesc": "A clickable tile linking to a service URL",
"widgetFolderDesc": "A container to group related widgets",
"widgetClockDesc": "A live clock with configurable timezone",
"widgetNotesDesc": "A markdown notes widget",
"widgetHostStatusDesc": "Shows live CPU, memory and disk for an SSH host",
"widgetBookmarkListDesc": "A list of quick links",
"copyLink": "Copy Link",
"linkCopied": "Link copied!",
"location": "Location",
"temperatureUnit": "Temperature Unit",
"showForecast": "Show 3-day forecast",
"scrolling": "Allow Scrolling",
"feedUrl": "Feed URL",
"maxItems": "Max Items",
"showDescription": "Show Description",
"widgetWeatherName": "Weather",
"widgetWeatherDesc": "Live weather for any location",
"widgetIframeName": "iFrame Embed",
"widgetIframeDesc": "Embed any URL in an iframe",
"widgetRssName": "RSS Feed",
"widgetRssDesc": "Display items from an RSS or Atom feed",
"dragToFolder": "Drag widgets here or use + to add",
"showDisk": "Show Disk Usage",
"addToFolder": "Add widget to folder",
"noHostSelected": "No host selected",
"metricsNotAvailable": "Metrics not available",
"selectHost": "Select a host...",
"displayedMetrics": "Displayed Metrics",
"metricCpu": "CPU",
"metricMemory": "Memory",
"metricDisk": "Disk",
"metricUptime": "Uptime",
"metricSystem": "System",
"metricOs": "OS",
"metricKernel": "Kernel",
"metricHostname": "Hostname",
"metricNetwork": "Network",
"metricProcesses": "Processes",
"metricProcessesTotal": "total",
"metricProcessesRunning": "running",
"categoryMonitoring": "Monitoring",
"loading": "Loading...",
"noData": "No data available",
"allClear": "All clear",
"acknowledgeAlert": "Acknowledge",
"noPingUrls": "No URLs configured",
"pingLabel": "Label",
"addPingUrl": "Add URL",
"showLatency": "Show Latency",
"refreshInterval": "Refresh Interval",
"seconds": "seconds",
"filterActivityTypes": "Filter Types",
"filterTypesHint": "Leave empty to show all",
"showTimestamp": "Show Timestamp",
"uptimeUnavailable": "Uptime unavailable",
"uptimeLabel": "Uptime",
"overviewVersion": "Version",
"overviewUpdate": "Up to date",
"overviewUpdateAvailable": "Update available",
"overviewDatabase": "Database",
"overviewUptime": "Uptime",
"noHosts": "No hosts configured",
"hostGridHosts": "Hosts",
"hostGridAllHint": "Leave empty to show all hosts",
"columns": "Columns",
"showIp": "Show IP Address",
"connectionType": "Connection Type",
"layout": "Layout",
"showStatus": "Show Status",
"showHostName": "Show Host Name",
"noDockerActivity": "No Docker activity",
"noActivity": "No activity",
"showAcknowledged": "Show Acknowledged",
"showCurrentValue": "Show Current Value",
"chartMetric": "Metric",
"metricRange": "Range",
"widgetMetricsChartName": "Metrics Chart",
"widgetMetricsChartDesc": "Historical CPU, memory, disk or network chart for a host",
"widgetHostGridName": "Host Grid",
"widgetHostGridDesc": "Grid view of SSH host statuses",
"widgetAlertFeedName": "Alert Feed",
"widgetAlertFeedDesc": "Live alert firings with acknowledge support",
"widgetPingStatusName": "Ping Status",
"widgetPingStatusDesc": "HTTP ping status for one or more URLs",
"widgetRecentActivityName": "Recent Activity",
"widgetRecentActivityDesc": "Scrollable feed of recent Termix activity",
"widgetTermixUptimeName": "Termix Uptime",
"widgetTermixUptimeDesc": "Live uptime counter for the Termix server",
"widgetSystemOverviewName": "System Overview",
"widgetSystemOverviewDesc": "Termix version, database health and uptime at a glance",
"widgetSshQuickConnectName": "SSH Quick Connect",
"widgetSshQuickConnectDesc": "One-click buttons to open SSH sessions",
"widgetDockerActivityName": "Docker Activity",
"widgetDockerActivityDesc": "Recent Docker container events across all hosts",
"widgetCalendarName": "Calendar",
"widgetCalendarDesc": "A monthly calendar with today highlighted",
"widgetCountdownName": "Countdown",
"widgetCountdownDesc": "Countdown timer to a target date",
"widgetSearchBarName": "Search Bar",
"widgetSearchBarDesc": "Quick web search widget",
"widgetTextBannerName": "Text Banner",
"widgetTextBannerDesc": "A bold label or section header for the canvas",
"widgetImageWidgetName": "Image",
"widgetImageWidgetDesc": "Display an image from a URL",
"widgetMarkdownNotesName": "Markdown Notes",
"widgetMarkdownNotesDesc": "Rich notes with inline markdown rendering",
"widgetCustomApiName": "Custom API",
"widgetCustomApiDesc": "Fetch and display data from any JSON API",
"widgetServiceGridName": "Service Grid",
"widgetServiceGridDesc": "A configurable grid of service tile links",
"widgetDashboardLinksName": "Dashboard Links",
"widgetDashboardLinksDesc": "Display your configured service links from the dashboard",
"widgetSearchLinksName": "Search Shortcuts",
"widgetSearchLinksDesc": "Quick search shortcut buttons with inline input",
"widgetLinkTreeName": "Link Tree",
"widgetLinkTreeDesc": "Grouped sections of links with headings",
"calMon": "Mo",
"calTue": "Tu",
"calWed": "We",
"calThu": "Th",
"calFri": "Fr",
"calSat": "Sa",
"calSun": "Su",
"startOnMonday": "Start week on Monday",
"countdownNoDate": "No target date set",
"countdownPast": "Event has passed",
"countdownDays": "days",
"countdownHours": "hrs",
"countdownMinutes": "min",
"countdownSeconds": "sec",
"countdownLabel": "Label",
"countdownLabelPlaceholder": "e.g. Launch Day",
"countdownShowDays": "Show Days",
"countdownShowHours": "Show Hours",
"targetDate": "Target Date",
"searchEngine": "Search Engine",
"customSearchUrl": "Custom Search URL",
"searchPlaceholder": "Search...",
"searchGo": "Go",
"searchPlaceholderLabel": "Placeholder Text",
"searchPlaceholderHint": "Text shown inside the search input",
"openInNewTab": "Open in New Tab",
"searchQueryPlaceholder": "Enter query...",
"noSearchShortcuts": "No shortcuts configured",
"addSearchShortcut": "Add Shortcut",
"fontSize": "Font Size",
"textAlign": "Text Align",
"fontWeight": "Font Weight",
"clearColor": "Clear Color",
"imageFit": "Image Fit",
"imageLinkUrl": "Link URL",
"noImage": "No image URL set",
"altText": "Alt Text",
"altTextPlaceholder": "Describe the image",
"renderMarkdown": "Render Markdown",
"displayMode": "Display Mode",
"displayField": "Display Field",
"jsonPath": "JSON Path",
"customApiLabel": "Label",
"customApiLabelPlaceholder": "e.g. Temperature",
"customApiUnit": "Unit",
"customApiNoUrl": "No API URL configured",
"customApiError": "Failed to fetch",
"customApiNotArray": "Response is not an array",
"addService": "Add Service",
"showLabels": "Show Labels",
"iconSize": "Icon Size",
"noDashboardLinks": "No dashboard links configured",
"noLimit": "No limit",
"sectionHeading": "Section Heading",
"addSection": "Add Section",
"compactMode": "Compact Mode",
"showDetailed": "Show Detailed",
"selectHosts": "Select Hosts",
"allHosts": "All hosts",
"listLayout": "List",
"gridLayout": "Grid",
"terminal": "Terminal",
"files": "Files",
"docker": "Docker",
"range15m": "15 minutes",
"range1h": "1 hour",
"range6h": "6 hours",
"range24h": "24 hours",
"metricNetRx": "Net Download",
"metricNetTx": "Net Upload",
"severityFilter": "Severity Filter",
"filterAll": "All",
"accentColor": "Accent Color",
"widgetSshTerminalName": "SSH Terminal",
"widgetSshTerminalDesc": "An inline SSH terminal connected to a configured host",
"sshTerminalNoHost": "No host configured",
"sshTerminalConnect": "Connect",
"sshTerminalAutoConnect": "Auto-connect on load",
"widgetQuickConnectName": "Quick Connect",
"widgetQuickConnectDesc": "One-click launch buttons for any connection type across your hosts",
"connectionTypes": "Connection Types",
"connType_terminal": "Terminal",
"connType_files": "File Manager",
"connType_docker": "Docker",
"connType_tunnel": "Tunnel",
"connType_host-metrics": "Host Metrics",
"connType_rdp": "RDP",
"connType_vnc": "VNC",
"connType_telnet": "Telnet",
"widgetFileManagerName": "File Manager",
"widgetFileManagerDesc": "Embedded SFTP file manager for a configured host",
"widgetDockerName": "Docker Manager",
"widgetDockerDesc": "Embedded Docker container manager for a configured host",
"widgetTunnelName": "Tunnel Manager",
"widgetTunnelDesc": "Embedded SSH tunnel manager for a configured host",
"widgetNoHostSelected": "No host configured"
},
"serverConfig": {
"title": "Server Configuration",
@@ -317,7 +574,7 @@
"status": "Status",
"folderRenamed": "Folder \"{{oldName}}\" renamed to \"{{newName}}\" successfully",
"failedToRenameFolder": "Failed to rename folder",
"movedToFolder": "Moved to \"{{folder}}\"",
"movedToFolder": "Moved {{count}} host(s) to \"{{folder}}\"",
"editHostTooltip": "Edit host",
"statusChecks": "Status Checks",
"metricsCollection": "Metrics Collection",
@@ -607,6 +864,21 @@
"proxmoxDockerDetectionDesc": "Comma-separated name patterns that enable Docker for matching guests",
"proxmoxPreferredRanges": "Preferred IP ranges",
"proxmoxPreferredRangesDesc": "Comma-separated prefixes in priority order for IP selection when a guest has multiple interfaces",
"proxmoxAutoSync": "Auto sync guests",
"proxmoxAutoSyncDesc": "Periodically discover this Proxmox node and create or update imported guest hosts while your session is unlocked.",
"proxmoxSyncInterval": "Sync interval (minutes)",
"proxmoxSyncIntervalDesc": "Minimum 5 minutes. The scheduler skips locked user sessions.",
"proxmoxMarkMissing": "Mark missing guests",
"proxmoxMarkMissingDesc": "Add a proxmox-missing tag when a previously imported guest disappears instead of deleting it.",
"proxmoxLastSync": "Last sync",
"proxmoxLastSyncNever": "Not synced yet",
"proxmoxLastSyncNoResult": "No result details",
"proxmoxLastSyncSummary": "{{created}} created, {{updated}} updated, {{markedMissing}} missing, {{skipped}} skipped",
"proxmoxLastSyncStatus": {
"success": "Success",
"error": "Failed",
"pending": "Pending"
},
"proxmoxDiscoverAction": "Discover & import Proxmox guests",
"proxmoxImportTitle": "Import from Proxmox",
"proxmoxSelectHost": "Select a Proxmox host…",
@@ -741,7 +1013,6 @@
"failedToSaveFolder": "Failed to save folder",
"folderDeleted": "Deleted folder \"{{name}}\"",
"deleteFolderConfirm": "Delete \"{{name}}\" and its {{count}} host(s)? This cannot be undone.",
"movedToFolder": "Moved {{count}} host(s) to \"{{folder}}\"",
"failedToMoveHosts": "Failed to move hosts",
"expandAll": "Expand all folders",
"collapseAll": "Collapse all folders",
@@ -791,7 +1062,6 @@
"failedToDeployKey": "Failed to deploy key",
"deleteHostsConfirm": "Delete {{count}} host{{plural}}? This cannot be undone.",
"movedToRoot": "Moved to root",
"failedToMoveHosts": "Failed to move hosts",
"enableTerminalFeature": "Enable Terminal",
"disableTerminalFeature": "Disable Terminal",
"enableFilesFeature": "Enable Files",
@@ -912,7 +1182,57 @@
"shareHost": "Share Host",
"shareHostTitle": "Share: {{name}}",
"sharing": {
"requiresCredential": "This host must use a credential to enable sharing. Edit the host and assign a credential first."
"loadError": "Failed to load sharing data. Please try again.",
"shareWithSection": "Share with",
"usersTab": "Users",
"rolesTab": "Roles",
"searchPlaceholder": "Search users or roles...",
"noMatches": "No matches found",
"permissionLevelLabel": "Permission level",
"levels": {
"connect": {
"label": "Connect",
"description": "Open sessions only: terminal, remote desktop, file manager, tunnels and Docker. No access to the host configuration."
},
"view": {
"label": "View",
"description": "Connect, plus see the host configuration. Secrets are never shown."
},
"edit": {
"label": "Edit",
"description": "View, plus modify the host. Secrets can be replaced but never read; credential assignments stay owner-only."
},
"manage": {
"label": "Manage",
"description": "Edit, plus share the host with others, change permission levels and revoke access."
}
},
"expiryLabel": "Access expiry",
"expiry": {
"never": "Never",
"oneHour": "1 hour",
"oneDay": "24 hours",
"sevenDays": "7 days",
"thirtyDays": "30 days",
"custom": "Custom"
},
"customHoursPlaceholder": "Hours until access expires",
"shareButton": "Share",
"shareWithCount": "Share ({{count}})",
"currentAccess": "Current access",
"noAccessEntries": "This host has not been shared yet",
"grantedBy": "Granted by",
"expires": "Expires",
"expired": "Expired",
"never": "Never",
"revoke": "Revoke",
"accessUpdated": "Access updated",
"accessUpdateFailed": "Failed to update access",
"sharedBadge": "Shared",
"sharedBadgeTooltip": "Shared by {{owner}} ({{level}} access)",
"viewOnlyBanner": "This host is shared with you by {{owner}} with view access. The configuration is read-only.",
"sharedEditBanner": "This host is shared with you by {{owner}} with edit access. Changes apply to the real host; authentication references can only be changed by the owner.",
"ownerOnlyControl": "Only the host owner can change this"
},
"guac": {
"connection": "Connection",
@@ -1047,28 +1367,10 @@
"backspaceKey": "Backspace Key",
"saveHostFirst": "Save the host first.",
"sharingOptionsAfterSave": "Sharing options are available after the host has been saved.",
"sharingLoadError": "Failed to load sharing data. Check your connection and try again.",
"shareHostSection": "Share Host",
"shareWithUser": "Share with User",
"shareWithRole": "Share with Role",
"selectUser": "Select User",
"selectRole": "Select Role",
"selectUserOption": "Select a user...",
"selectRoleOption": "Select a role...",
"permissionLevel": "Permission Level",
"expiresInHours": "Expires in (hours)",
"noExpiryPlaceholder": "Leave empty for no expiry",
"shareBtn": "Share",
"currentAccess": "Current Access",
"typeHeader": "Type",
"targetHeader": "Target",
"permissionHeader": "Permission",
"grantedByHeader": "Granted By",
"expiresHeader": "Expires",
"noAccessEntries": "No access entries yet.",
"expiredLabel": "Expired",
"neverLabel": "Never",
"revokeBtn": "Revoke",
"cancelBtn": "Cancel",
"savingBtn": "Saving...",
"updateHostBtn": "Update Host",
@@ -1935,7 +2237,8 @@
"passwordLoginDisabledDesc": "Password login is disabled. Use a passkey or an external authentication provider.",
"signInWithPasskey": "Sign in with passkey",
"passkeyLoginFailed": "Passkey login failed",
"attemptsRemaining": "{{count}} attempts remaining"
"attemptsRemaining": "{{count}} attempts remaining",
"confirmResetDataWipe": "This account has not logged in since the encryption upgrade, so its stored data cannot be recovered without the old password. Resetting will permanently delete its hosts, credentials and snippets. Continue?"
},
"hostKey": {
"verifyNewHost": "Verify SSH Host Key",
@@ -2068,6 +2371,13 @@
"serviceLinks": "Service Links",
"homepagePreview": "Homepage Preview"
},
"donation": {
"title": "Enjoying Termix?",
"body": "Termix is free and open source, built and maintained by a two-person team in our spare time. If it's replaced a commercial tool you'd otherwise be paying for, a donation helps cover hosting costs and keeps development going. As of now, donations are crypto only.",
"milestones": "Donations also help fund the time to research and learn what's needed to build features like SAML and Kubernetes support. See the progress on the donate page.",
"cta": "Donate",
"dismiss": "Maybe later"
},
"dashboardTab": {
"stable": "STABLE",
"hostsOnline": "Hosts Online",
@@ -2535,6 +2845,71 @@
"updateAdminStatusFailed": "Failed to update admin status",
"allSessionsRevoked": "All sessions revoked",
"revokeSessionsFailed": "Failed to revoke sessions",
"manageUserData": "Manage user data",
"backToUsers": "Back to users",
"manageTabAccount": "Account",
"manageTabHosts": "Hosts",
"manageTabCredentials": "Credentials",
"manageTabSnippets": "Snippets",
"manageTabSessions": "Sessions",
"manageTabDanger": "Danger",
"manageEditorBack": "Back to {{username}}'s data",
"dataLockedBadge": "LOCKED",
"dataLockedNotice": "{{username}}'s data stays locked until they next log in. Their hosts, credentials and snippets cannot be viewed or edited until then.",
"resetPasswordTitle": "Reset Password",
"resetPasswordOidcOnly": "This user signs in through an external provider and has no password.",
"resetPasswordPlaceholder": "New password",
"resetPasswordBtn": "Reset",
"resetPasswordWorking": "Resetting...",
"resetPasswordSuccess": "Password reset",
"resetPasswordSuccessWiped": "Password reset. The user's encrypted data was wiped.",
"resetPasswordFailed": "Failed to reset password",
"resetPasswordConfirmWipe": "{{username}} has not logged in since the encryption upgrade, so their data cannot be recovered. Resetting now will delete their hosts, credentials and snippets. Continue?",
"totpSectionTitle": "Two-Factor Authentication",
"totpStatusEnabled": "TOTP is enabled for this user",
"totpStatusDisabled": "TOTP is not enabled for this user",
"disableTotp": "Disable",
"disableTotpConfirm": "Disable two-factor authentication for {{username}}? They will be able to log in with only their password.",
"totpDisabledSuccess": "TOTP disabled",
"totpDisableFailed": "Failed to disable TOTP",
"manageApiKeys": "API Keys",
"apiKeyNamePlaceholder": "Key name",
"apiKeyCopyNotice": "Copy this key now, it won't be shown again.",
"noApiKeysForUser": "No API keys",
"apiKeyDeleteFailed": "Failed to delete API key",
"exportUserData": "Data Export",
"exportUserDataDesc": "Download this user's hosts, credentials and file manager data as JSON. Secrets are decrypted.",
"exportUserDataSuccess": "User data exported",
"exportUserDataFailed": "Failed to export user data",
"hostsCount": "{{count}} hosts",
"addHostForUser": "Add Host",
"noHostsForUser": "This user has no hosts",
"connectToHost": "Connect",
"deleteHostConfirm": "Delete host \"{{name}}\" belonging to {{username}}?",
"hostDeletedSuccess": "Host deleted",
"hostDeleteFailed": "Failed to delete host",
"credentialsCount": "{{count}} credentials",
"addCredentialForUser": "Add Credential",
"noCredentialsForUser": "This user has no credentials",
"deleteCredentialConfirm": "Delete credential \"{{name}}\" belonging to {{username}}?",
"credentialDeletedSuccess": "Credential deleted",
"credentialDeleteFailed": "Failed to delete credential",
"snippetsCount": "{{count}} snippets",
"addSnippetForUser": "Add Snippet",
"noSnippetsForUser": "This user has no snippets",
"snippetRequiredFields": "Snippet name and content are required",
"snippetNamePlaceholder": "Snippet name",
"snippetContentPlaceholder": "Command content",
"snippetFolderPlaceholder": "Folder (optional)",
"snippetSaved": "Snippet saved",
"snippetSaveFailed": "Failed to save snippet",
"deleteSnippetConfirm": "Delete snippet \"{{name}}\" belonging to {{username}}?",
"snippetDeletedSuccess": "Snippet deleted",
"snippetDeleteFailed": "Failed to delete snippet",
"noSessionsForUser": "No active sessions",
"deleteUserDangerDesc": "Permanently delete {{username}} and all of their data (hosts, credentials, snippets, history). This cannot be undone.",
"deleteUserConfirm": "Permanently delete {{username}} and all of their data?",
"deleteUserAdminBlocked": "Remove admin status before deleting this user.",
"createRoleRequired": "Name and display name are required",
"createRoleSuccess": "Role \"{{name}}\" created",
"createRoleFailed": "Failed to create role",
@@ -2567,7 +2942,16 @@
"hostDefaultsCommandHistory": "Command History",
"hostDefaultsCommandHistoryDesc": "Track command history on new hosts by default",
"hostDefaultsSaved": "Host defaults saved",
"hostDefaultsSaveFailed": "Failed to save host defaults"
"hostDefaultsSaveFailed": "Failed to save host defaults",
"rolePermissions": {
"count": "{{count}} permissions",
"editAction": "Edit permissions",
"loadError": "Failed to load the permissions catalog",
"saved": "Role permissions saved",
"saveError": "Failed to save role permissions",
"save": "Save",
"saving": "Saving..."
}
},
"newUi": {
"sidebar": {
@@ -2736,6 +3120,10 @@
"executionFailed": "Failed"
},
"userProfile": {
"donateTitle": "Keep Termix alive",
"donateDescription": "Termix is free, self-hosted, and built by only a few people in their spare time. If it's saved you time or money, a crypto donation helps keep it going.",
"donateMilestones": "Donations help fund the time to research and learn what's needed to build SAML, Kubernetes, and Agent support. See the progress and donate.",
"donateButton": "Donate crypto",
"storageModeLocal": "Browser",
"storageModeCloud": "Database",
"storageModeDescription": "Browser stores settings in this browser only. Database syncs to the server and loads on any device.",
@@ -2756,6 +3144,10 @@
"twoFaOn": "On",
"twoFaOff": "Off",
"versionLabel": "Version",
"betaProgramTitle": "Beta Program",
"betaProgramDescription": "Try upcoming features early with the weekly rolling :beta Docker tag. Unstable, not for production.",
"betaProgramFeedback": "Found a bug? Report it here.",
"betaProgramLearnMore": "Learn More",
"deleteAccount": "Delete Account",
"deleteAccountDescription": "Permanently delete your account",
"changeServerDescription": "Switch to a different Termix backend server",
@@ -3076,266 +3468,5 @@
"channels": "Notification Channels",
"noChannelsHint": "Add channels in the Channels tab first",
"ruleSaveFailed": "Failed to save rule"
},
"homepage": {
"title": "Homepage",
"addWidget": "Add Widget",
"editWidget": "Edit Widget",
"deleteWidget": "Delete",
"widgetTypes": "Widget Types",
"serviceLink": "Service Link",
"folder": "Folder",
"clock": "Clock",
"notes": "Notes",
"hostStatus": "Host Status",
"bookmarkList": "Bookmarks",
"zoomIn": "Zoom In",
"zoomOut": "Zoom Out",
"resetView": "Reset View",
"lockLayout": "Lock Layout",
"unlockLayout": "Unlock Layout",
"noWidgets": "Right-click or click + to add your first widget",
"openFullView": "Open Full View",
"previewTitle": "Homepage Preview",
"cancel": "Cancel",
"save": "Save",
"title_label": "Title",
"widgetTitlePlaceholder": "Widget title (optional)",
"url": "URL",
"imageUrl": "Custom Image URL",
"imageUrlHint": "Leave blank to use the site favicon automatically",
"showImage": "Show Image",
"description": "Description",
"color": "Color",
"icon": "Icon",
"expanded": "Expanded by default",
"timezone": "Timezone",
"showSeconds": "Show seconds",
"format12h": "12-hour",
"format24h": "24-hour",
"content": "Content",
"backgroundColor": "Background Color",
"host": "Host",
"showMetrics": "Show Metrics",
"links": "Links",
"addLink": "Add Link",
"linkLabel": "Label",
"linkUrl": "URL",
"removeLink": "Remove",
"categoryLinks": "Links",
"categoryInfo": "Info",
"categorySystem": "System",
"widgetServiceLinkDesc": "A clickable tile linking to a service URL",
"widgetFolderDesc": "A container to group related widgets",
"widgetClockDesc": "A live clock with configurable timezone",
"widgetNotesDesc": "A markdown notes widget",
"widgetHostStatusDesc": "Shows live CPU, memory and disk for an SSH host",
"widgetBookmarkListDesc": "A list of quick links",
"copyLink": "Copy Link",
"linkCopied": "Link copied!",
"location": "Location",
"temperatureUnit": "Temperature Unit",
"showForecast": "Show 3-day forecast",
"scrolling": "Allow Scrolling",
"feedUrl": "Feed URL",
"maxItems": "Max Items",
"showDescription": "Show Description",
"widgetWeatherName": "Weather",
"widgetWeatherDesc": "Live weather for any location",
"widgetIframeName": "iFrame Embed",
"widgetIframeDesc": "Embed any URL in an iframe",
"widgetRssName": "RSS Feed",
"widgetRssDesc": "Display items from an RSS or Atom feed",
"dragToFolder": "Drag widgets here or use + to add",
"showDisk": "Show Disk Usage",
"addToFolder": "Add widget to folder",
"noHostSelected": "No host selected",
"metricsNotAvailable": "Metrics not available",
"selectHost": "Select a host...",
"displayedMetrics": "Displayed Metrics",
"metricCpu": "CPU",
"metricMemory": "Memory",
"metricDisk": "Disk",
"metricUptime": "Uptime",
"metricSystem": "System",
"metricOs": "OS",
"metricKernel": "Kernel",
"metricHostname": "Hostname",
"metricNetwork": "Network",
"metricProcesses": "Processes",
"metricProcessesTotal": "total",
"metricProcessesRunning": "running",
"categoryMonitoring": "Monitoring",
"loading": "Loading...",
"noData": "No data available",
"allClear": "All clear",
"acknowledgeAlert": "Acknowledge",
"noPingUrls": "No URLs configured",
"pingLabel": "Label",
"addPingUrl": "Add URL",
"showLatency": "Show Latency",
"refreshInterval": "Refresh Interval",
"seconds": "seconds",
"filterActivityTypes": "Filter Types",
"filterTypesHint": "Leave empty to show all",
"showTimestamp": "Show Timestamp",
"uptimeUnavailable": "Uptime unavailable",
"uptimeLabel": "Uptime",
"overviewVersion": "Version",
"overviewUpdate": "Up to date",
"overviewUpdateAvailable": "Update available",
"overviewDatabase": "Database",
"overviewUptime": "Uptime",
"noHosts": "No hosts configured",
"hostGridHosts": "Hosts",
"hostGridAllHint": "Leave empty to show all hosts",
"columns": "Columns",
"showIp": "Show IP Address",
"connectionType": "Connection Type",
"layout": "Layout",
"showStatus": "Show Status",
"showHostName": "Show Host Name",
"noDockerActivity": "No Docker activity",
"noActivity": "No activity",
"showAcknowledged": "Show Acknowledged",
"showCurrentValue": "Show Current Value",
"chartMetric": "Metric",
"metricRange": "Range",
"widgetMetricsChartName": "Metrics Chart",
"widgetMetricsChartDesc": "Historical CPU, memory, disk or network chart for a host",
"widgetHostGridName": "Host Grid",
"widgetHostGridDesc": "Grid view of SSH host statuses",
"widgetAlertFeedName": "Alert Feed",
"widgetAlertFeedDesc": "Live alert firings with acknowledge support",
"widgetPingStatusName": "Ping Status",
"widgetPingStatusDesc": "HTTP ping status for one or more URLs",
"widgetRecentActivityName": "Recent Activity",
"widgetRecentActivityDesc": "Scrollable feed of recent Termix activity",
"widgetTermixUptimeName": "Termix Uptime",
"widgetTermixUptimeDesc": "Live uptime counter for the Termix server",
"widgetSystemOverviewName": "System Overview",
"widgetSystemOverviewDesc": "Termix version, database health and uptime at a glance",
"widgetSshQuickConnectName": "SSH Quick Connect",
"widgetSshQuickConnectDesc": "One-click buttons to open SSH sessions",
"widgetDockerActivityName": "Docker Activity",
"widgetDockerActivityDesc": "Recent Docker container events across all hosts",
"widgetCalendarName": "Calendar",
"widgetCalendarDesc": "A monthly calendar with today highlighted",
"widgetCountdownName": "Countdown",
"widgetCountdownDesc": "Countdown timer to a target date",
"widgetSearchBarName": "Search Bar",
"widgetSearchBarDesc": "Quick web search widget",
"widgetTextBannerName": "Text Banner",
"widgetTextBannerDesc": "A bold label or section header for the canvas",
"widgetImageWidgetName": "Image",
"widgetImageWidgetDesc": "Display an image from a URL",
"widgetMarkdownNotesName": "Markdown Notes",
"widgetMarkdownNotesDesc": "Rich notes with inline markdown rendering",
"widgetCustomApiName": "Custom API",
"widgetCustomApiDesc": "Fetch and display data from any JSON API",
"widgetServiceGridName": "Service Grid",
"widgetServiceGridDesc": "A configurable grid of service tile links",
"widgetDashboardLinksName": "Dashboard Links",
"widgetDashboardLinksDesc": "Display your configured service links from the dashboard",
"widgetSearchLinksName": "Search Shortcuts",
"widgetSearchLinksDesc": "Quick search shortcut buttons with inline input",
"widgetLinkTreeName": "Link Tree",
"widgetLinkTreeDesc": "Grouped sections of links with headings",
"calMon": "Mo",
"calTue": "Tu",
"calWed": "We",
"calThu": "Th",
"calFri": "Fr",
"calSat": "Sa",
"calSun": "Su",
"startOnMonday": "Start week on Monday",
"countdownNoDate": "No target date set",
"countdownPast": "Event has passed",
"countdownDays": "days",
"countdownHours": "hrs",
"countdownMinutes": "min",
"countdownSeconds": "sec",
"countdownLabel": "Label",
"countdownLabelPlaceholder": "e.g. Launch Day",
"countdownShowDays": "Show Days",
"countdownShowHours": "Show Hours",
"targetDate": "Target Date",
"searchEngine": "Search Engine",
"customSearchUrl": "Custom Search URL",
"searchPlaceholder": "Search...",
"searchGo": "Go",
"searchPlaceholderLabel": "Placeholder Text",
"searchPlaceholderHint": "Text shown inside the search input",
"openInNewTab": "Open in New Tab",
"searchQueryPlaceholder": "Enter query...",
"noSearchShortcuts": "No shortcuts configured",
"addSearchShortcut": "Add Shortcut",
"fontSize": "Font Size",
"textAlign": "Text Align",
"fontWeight": "Font Weight",
"clearColor": "Clear Color",
"imageFit": "Image Fit",
"imageLinkUrl": "Link URL",
"noImage": "No image URL set",
"altText": "Alt Text",
"altTextPlaceholder": "Describe the image",
"renderMarkdown": "Render Markdown",
"displayMode": "Display Mode",
"displayField": "Display Field",
"jsonPath": "JSON Path",
"customApiLabel": "Label",
"customApiLabelPlaceholder": "e.g. Temperature",
"customApiUnit": "Unit",
"customApiNoUrl": "No API URL configured",
"customApiError": "Failed to fetch",
"customApiNotArray": "Response is not an array",
"addService": "Add Service",
"showLabels": "Show Labels",
"iconSize": "Icon Size",
"noDashboardLinks": "No dashboard links configured",
"noLimit": "No limit",
"sectionHeading": "Section Heading",
"addSection": "Add Section",
"compactMode": "Compact Mode",
"showDetailed": "Show Detailed",
"selectHosts": "Select Hosts",
"allHosts": "All hosts",
"listLayout": "List",
"gridLayout": "Grid",
"terminal": "Terminal",
"files": "Files",
"docker": "Docker",
"range15m": "15 minutes",
"range1h": "1 hour",
"range6h": "6 hours",
"range24h": "24 hours",
"metricNetRx": "Net Download",
"metricNetTx": "Net Upload",
"severityFilter": "Severity Filter",
"filterAll": "All",
"accentColor": "Accent Color",
"widgetSshTerminalName": "SSH Terminal",
"widgetSshTerminalDesc": "An inline SSH terminal connected to a configured host",
"sshTerminalNoHost": "No host configured",
"sshTerminalConnect": "Connect",
"sshTerminalAutoConnect": "Auto-connect on load",
"widgetQuickConnectName": "Quick Connect",
"widgetQuickConnectDesc": "One-click launch buttons for any connection type across your hosts",
"connectionTypes": "Connection Types",
"connType_terminal": "Terminal",
"connType_files": "File Manager",
"connType_docker": "Docker",
"connType_tunnel": "Tunnel",
"connType_host-metrics": "Host Metrics",
"connType_rdp": "RDP",
"connType_vnc": "VNC",
"connType_telnet": "Telnet",
"widgetFileManagerName": "File Manager",
"widgetFileManagerDesc": "Embedded SFTP file manager for a configured host",
"widgetDockerName": "Docker Manager",
"widgetDockerDesc": "Embedded Docker container manager for a configured host",
"widgetTunnelName": "Tunnel Manager",
"widgetTunnelDesc": "Embedded SSH tunnel manager for a configured host",
"widgetNoHostSelected": "No host configured"
}
}
+158 -21
View File
@@ -864,6 +864,21 @@
"proxmoxDockerDetectionDesc": "Komma-geskeide naampatrone wat Docker in staat stel om gaste te pas",
"proxmoxPreferredRanges": "Voorkeur-IP-reekse",
"proxmoxPreferredRangesDesc": "Komma-geskeide voorvoegsels in prioriteitsvolgorde vir IP-keuse wanneer 'n gas verskeie koppelvlakke het",
"proxmoxAutoSync": "Outomatiese sinkronisering van gaste",
"proxmoxAutoSyncDesc": "Ontdek hierdie Proxmox-nodus gereeld en skep of werk ingevoerde gashere op terwyl jou sessie ontsluit is.",
"proxmoxSyncInterval": "Sinkronisasie-interval (minute)",
"proxmoxSyncIntervalDesc": "Minimum 5 minute. Die skeduleerder slaan geslote gebruikerssessies oor.",
"proxmoxMarkMissing": "Merk ontbrekende gaste",
"proxmoxMarkMissingDesc": "Voeg 'n proxmox-missing-etiket by wanneer 'n voorheen ingevoerde gas verdwyn in plaas daarvan om dit te verwyder.",
"proxmoxLastSync": "Laaste sinkronisasie",
"proxmoxLastSyncNever": "Nog nie gesinkroniseer nie",
"proxmoxLastSyncNoResult": "Geen resultaatbesonderhede nie",
"proxmoxLastSyncSummary": "{{created}} geskep, {{updated}} opgedateer, {{markedMissing}} ontbreek, {{skipped}} oorgeslaan",
"proxmoxLastSyncStatus": {
"success": "Sukses",
"error": "Misluk",
"pending": "Hangende"
},
"proxmoxDiscoverAction": "Ontdek en invoer Proxmox-gaste",
"proxmoxImportTitle": "Invoer vanaf Proxmox",
"proxmoxSelectHost": "Kies 'n Proxmox-gasheer…",
@@ -1167,7 +1182,57 @@
"shareHost": "Deel gasheer",
"shareHostTitle": "Deel: {{name}}",
"sharing": {
"requiresCredential": "Hierdie gasheer moet 'n geloofsbrief gebruik om deling te aktiveer. Wysig die gasheer en ken eers 'n geloofsbrief toe."
"loadError": "Kon nie deeldata laai nie. Probeer asseblief weer.",
"shareWithSection": "Deel met",
"usersTab": "Gebruikers",
"rolesTab": "Rolle",
"searchPlaceholder": "Soek gebruikers of rolle...",
"noMatches": "Geen ooreenstemmings gevind nie",
"permissionLevelLabel": "Toestemmingsvlak",
"levels": {
"connect": {
"label": "Verbind",
"description": "Slegs oop sessies: terminaal, afstandwerkskerm, lêerbestuurder, tonnels en Docker. Geen toegang tot die gasheerkonfigurasie nie."
},
"view": {
"label": "Bekyk",
"description": "Verbind, plus sien die gasheerkonfigurasie. Geheime word nooit gewys nie."
},
"edit": {
"label": "Wysig",
"description": "Bekyk, plus wysig die gasheer. Geheime kan vervang word, maar nooit gelees word nie; geloofsbriewe-toewysings bly slegs vir die eienaar."
},
"manage": {
"label": "Bestuur",
"description": "Wysig, plus deel die gasheer met ander, verander toestemmingsvlakke en herroep toegang."
}
},
"expiryLabel": "Toegang verval",
"expiry": {
"never": "Nooit",
"oneHour": "1 uur",
"oneDay": "24 uur",
"sevenDays": "7 dae",
"thirtyDays": "30 dae",
"custom": "Pasgemaak"
},
"customHoursPlaceholder": "Ure totdat toegang verval",
"shareButton": "Deel",
"shareWithCount": "Deel ({{count}})",
"currentAccess": "Huidige toegang",
"noAccessEntries": "Hierdie gasheer is nog nie gedeel nie",
"grantedBy": "Toegeken deur",
"expires": "Vervaldatum",
"expired": "Verstryk",
"never": "Nooit",
"revoke": "Herroep",
"accessUpdated": "Toegang opgedateer",
"accessUpdateFailed": "Kon nie toegang opdateer nie",
"sharedBadge": "Gedeel",
"sharedBadgeTooltip": "Gedeel deur {{owner}} ({{level}} toegang)",
"viewOnlyBanner": "Hierdie gasheer word met jou gedeel deur {{owner}} met kyktoegang. Die konfigurasie is slegs leesbaar.",
"sharedEditBanner": "Hierdie gasheer word met jou gedeel deur {{owner}} met wysigingstoegang. Veranderinge is van toepassing op die werklike gasheer; verifikasieverwysings kan slegs deur die eienaar verander word.",
"ownerOnlyControl": "Slegs die gasheer-eienaar kan dit verander"
},
"guac": {
"connection": "Verbinding",
@@ -1302,28 +1367,10 @@
"backspaceKey": "Terugspasie-sleutel",
"saveHostFirst": "Stoor eers die gasheer.",
"sharingOptionsAfterSave": "Deelopsies is beskikbaar nadat die gasheer gestoor is.",
"sharingLoadError": "Kon nie deeldata laai nie. Gaan jou verbinding na en probeer weer.",
"shareHostSection": "Deel gasheer",
"shareWithUser": "Deel met Gebruiker",
"shareWithRole": "Deel met Rol",
"selectUser": "Kies Gebruiker",
"selectRole": "Kies Rol",
"selectUserOption": "Kies 'n gebruiker...",
"selectRoleOption": "Kies 'n rol...",
"permissionLevel": "Toestemmingsvlak",
"expiresInHours": "Verval oor (ure)",
"noExpiryPlaceholder": "Los leeg vir geen vervaldatum",
"shareBtn": "Deel",
"currentAccess": "Huidige Toegang",
"typeHeader": "Tipe",
"targetHeader": "Teiken",
"permissionHeader": "Toestemming",
"grantedByHeader": "Toegeken deur",
"expiresHeader": "Vervaldatum",
"noAccessEntries": "Geen toegangsinskrywings nog nie.",
"expiredLabel": "Verstryk",
"neverLabel": "Nooit",
"revokeBtn": "Herroep",
"cancelBtn": "Kanselleer",
"savingBtn": "Stoor...",
"updateHostBtn": "Opdateer gasheer",
@@ -2190,7 +2237,8 @@
"passwordLoginDisabledDesc": "Wagwoordaanmelding is gedeaktiveer. Gebruik 'n wagwoordsleutel of 'n eksterne verifikasieverskaffer.",
"signInWithPasskey": "Teken aan met wagwoord",
"passkeyLoginFailed": "Wagwoord-aanmelding het misluk",
"attemptsRemaining": "{{count}} pogings oor"
"attemptsRemaining": "{{count}} pogings oor",
"confirmResetDataWipe": "Hierdie rekening het nie aangemeld sedert die enkripsie-opgradering nie, dus kan die gestoorde data nie sonder die ou wagwoord herstel word nie. Terugstelling sal die gashere, geloofsbriewe en brokkies permanent verwyder. Gaan voort?"
},
"hostKey": {
"verifyNewHost": "Verifieer SSH-gasheersleutel",
@@ -2323,6 +2371,13 @@
"serviceLinks": "Diens skakels",
"homepagePreview": "Tuisbladvoorskou"
},
"donation": {
"title": "Geniet jy Termix?",
"body": "Termix is gratis en oopbron, gebou en onderhou deur 'n tweepersoonspan in ons vrye tyd. As dit 'n kommersiële hulpmiddel vervang waarvoor jy andersins sou betaal, help 'n skenking om gasheerkoste te dek en ontwikkeling aan die gang te hou. Van nou af is skenkings slegs kripto.",
"milestones": "Donasies help ook om die tyd te befonds om navorsing te doen en te leer wat nodig is om funksies soos SAML en Kubernetes-ondersteuning te bou. Sien die vordering op die skenkingsbladsy.",
"cta": "Skenk",
"dismiss": "Miskien later"
},
"dashboardTab": {
"stable": "STABIEL",
"hostsOnline": "Aanlyn gashere",
@@ -2790,6 +2845,71 @@
"updateAdminStatusFailed": "Kon nie administrateurstatus opdateer nie",
"allSessionsRevoked": "Alle sessies herroep",
"revokeSessionsFailed": "Kon nie sessies herroep nie",
"manageUserData": "Bestuur gebruikersdata",
"backToUsers": "Terug na gebruikers",
"manageTabAccount": "Rekening",
"manageTabHosts": "Gashere",
"manageTabCredentials": "Geloofsbriewe",
"manageTabSnippets": "Brokkies",
"manageTabSessions": "Sessies",
"manageTabDanger": "Gevaar",
"manageEditorBack": "Terug na {{username}}se data",
"dataLockedBadge": "GESLUIT",
"dataLockedNotice": "{{username}}se data bly gesluit totdat hulle weer aanmeld. Hul gashere, geloofsbriewe en brokkies kan nie tot dan besigtig of geredigeer word nie.",
"resetPasswordTitle": "Herstel wagwoord",
"resetPasswordOidcOnly": "Hierdie gebruiker meld aan deur 'n eksterne verskaffer en het geen wagwoord nie.",
"resetPasswordPlaceholder": "Nuwe wagwoord",
"resetPasswordBtn": "Herstel",
"resetPasswordWorking": "Herstel tans...",
"resetPasswordSuccess": "Wagwoord herstel",
"resetPasswordSuccessWiped": "Wagwoord herstel. Die gebruiker se geïnkripteerde data is uitgevee.",
"resetPasswordFailed": "Kon nie wagwoord terugstel nie",
"resetPasswordConfirmWipe": "{{username}} het nie aangemeld sedert die enkripsie-opgradering nie, dus kan hul data nie herwin word nie. As u nou terugstel, sal hul gashere, geloofsbriewe en brokkies uitgevee word. Gaan voort?",
"totpSectionTitle": "Twee-faktor-verifikasie",
"totpStatusEnabled": "TOTP is vir hierdie gebruiker geaktiveer",
"totpStatusDisabled": "TOTP is nie vir hierdie gebruiker geaktiveer nie",
"disableTotp": "Deaktiveer",
"disableTotpConfirm": "Deaktiveer tweefaktor-verifikasie vir {{username}}? Hulle sal slegs met hul wagwoord kan aanmeld.",
"totpDisabledSuccess": "TOTP gedeaktiveer",
"totpDisableFailed": "Kon nie TOTP deaktiveer nie",
"manageApiKeys": "API-sleutels",
"apiKeyNamePlaceholder": "Sleutelnaam",
"apiKeyCopyNotice": "Kopieer hierdie sleutel nou, dit sal nie weer gewys word nie.",
"noApiKeysForUser": "Geen API-sleutels nie",
"apiKeyDeleteFailed": "Kon nie API-sleutel verwyder nie",
"exportUserData": "Data-uitvoer",
"exportUserDataDesc": "Laai hierdie gebruiker se gashere, geloofsbriewe en lêerbestuurderdata as JSON af. Geheime word ontsyfer.",
"exportUserDataSuccess": "Gebruikersdata uitgevoer",
"exportUserDataFailed": "Kon nie gebruikersdata uitvoer nie",
"hostsCount": "{{count}} gashere",
"addHostForUser": "Voeg gasheer by",
"noHostsForUser": "Hierdie gebruiker het geen gashere nie",
"connectToHost": "Verbind",
"deleteHostConfirm": "Vee gasheer \"{{name}}\" uit wat aan {{username}} behoort?",
"hostDeletedSuccess": "Gasheer uitgevee",
"hostDeleteFailed": "Kon nie gasheer verwyder nie",
"credentialsCount": "{{count}} geloofsbriewe",
"addCredentialForUser": "Voeg geloofsbriewe by",
"noCredentialsForUser": "Hierdie gebruiker het geen geloofsbriewe nie",
"deleteCredentialConfirm": "Vee die geloofsbrief \"{{name}}\" wat aan {{username}} behoort, uit?",
"credentialDeletedSuccess": "Geloofsbrief uitgevee",
"credentialDeleteFailed": "Kon nie geloofsbriewe verwyder nie",
"snippetsCount": "{{count}} brokkies",
"addSnippetForUser": "Voeg brokkie by",
"noSnippetsForUser": "Hierdie gebruiker het geen brokkies nie",
"snippetRequiredFields": "Brokkienaam en inhoud word vereis",
"snippetNamePlaceholder": "Brokkie naam",
"snippetContentPlaceholder": "Opdraginhoud",
"snippetFolderPlaceholder": "Vouer (opsioneel)",
"snippetSaved": "Brokkie gestoor",
"snippetSaveFailed": "Kon nie die brokkie stoor nie",
"deleteSnippetConfirm": "Vee die brokkie \"{{name}}\" uit wat aan {{username}} behoort?",
"snippetDeletedSuccess": "Brokkie uitgevee",
"snippetDeleteFailed": "Kon nie fragment uitvee nie",
"noSessionsForUser": "Geen aktiewe sessies nie",
"deleteUserDangerDesc": "Vee {{username}} en al hul data (gashere, geloofsbriewe, brokkies, geskiedenis) permanent uit. Dit kan nie ongedaan gemaak word nie.",
"deleteUserConfirm": "Verwyder {{username}} en al hul data permanent?",
"deleteUserAdminBlocked": "Verwyder administrateurstatus voordat hierdie gebruiker uitgevee word.",
"createRoleRequired": "Naam en vertoonnaam word vereis",
"createRoleSuccess": "Rol \"{{name}}\" geskep",
"createRoleFailed": "Kon nie rol skep nie",
@@ -2822,7 +2942,16 @@
"hostDefaultsCommandHistory": "Opdraggeskiedenis",
"hostDefaultsCommandHistoryDesc": "Volg standaard opdraggeskiedenis op nuwe gashere",
"hostDefaultsSaved": "Gasheerverstekwaardes gestoor",
"hostDefaultsSaveFailed": "Kon nie gasheerverstekwaardes stoor nie"
"hostDefaultsSaveFailed": "Kon nie gasheerverstekwaardes stoor nie",
"rolePermissions": {
"count": "{{count}} toestemmings",
"editAction": "Wysig toestemmings",
"loadError": "Kon nie die toestemmingskatalogus laai nie",
"saved": "Roltoestemmings gestoor",
"saveError": "Kon nie roltoestemmings stoor nie",
"save": "Stoor",
"saving": "Stoor..."
}
},
"newUi": {
"sidebar": {
@@ -2991,6 +3120,10 @@
"executionFailed": "Misluk"
},
"userProfile": {
"donateTitle": "Hou Termix lewendig",
"donateDescription": "Termix is gratis, self-gehost en gebou deur slegs 'n paar mense in hul vrye tyd. As dit jou tyd of geld bespaar het, help 'n kripto-skenking om dit aan die gang te hou.",
"donateMilestones": "Donasies help om die tyd te befonds om navorsing te doen en te leer wat nodig is om SAML-, Kubernetes- en Agent-ondersteuning te bou. Sien die vordering en skenk.",
"donateButton": "Skenk kripto",
"storageModeLocal": "Blaaier",
"storageModeCloud": "Databasis",
"storageModeDescription": "Die blaaier stoor instellings slegs in hierdie blaaier. Die databasis sinkroniseer met die bediener en laai op enige toestel.",
@@ -3011,6 +3144,10 @@
"twoFaOn": "Aan",
"twoFaOff": "Af",
"versionLabel": "Weergawe",
"betaProgramTitle": "Beta-program",
"betaProgramDescription": "Probeer komende funksies vroegtydig met die weeklikse rollende :beta Docker-etiket. Onstabiel, nie vir produksie nie.",
"betaProgramFeedback": "Het jy 'n fout gevind? Rapporteer dit hier.",
"betaProgramLearnMore": "Leer Meer",
"deleteAccount": "Vee rekening uit",
"deleteAccountDescription": "Vee jou rekening permanent uit",
"changeServerDescription": "Skakel oor na 'n ander Termix-agterbediener",
+158 -21
View File
@@ -864,6 +864,21 @@
"proxmoxDockerDetectionDesc": "أنماط أسماء مفصولة بفواصل تُمكّن Docker من مطابقة الضيوف",
"proxmoxPreferredRanges": "نطاقات IP المفضلة",
"proxmoxPreferredRangesDesc": "بادئات مفصولة بفواصل مرتبة حسب الأولوية لاختيار عنوان IP عندما يكون لدى جهاز ضيف عدة واجهات",
"proxmoxAutoSync": "مزامنة الضيوف تلقائيًا",
"proxmoxAutoSyncDesc": "قم باكتشاف عقدة Proxmox هذه بشكل دوري وقم بإنشاء أو تحديث مضيفي الضيوف المستوردين أثناء فتح جلستك.",
"proxmoxSyncInterval": "فاصل المزامنة (بالدقائق)",
"proxmoxSyncIntervalDesc": "الحد الأدنى 5 دقائق. يتخطى المجدول جلسات المستخدمين المقفلة.",
"proxmoxMarkMissing": "مارك الضيوف المفقودين",
"proxmoxMarkMissingDesc": "أضف علامة proxmox-missing عندما يختفي ضيف تم استيراده مسبقًا بدلاً من حذفه.",
"proxmoxLastSync": "آخر مزامنة",
"proxmoxLastSyncNever": "لم تتم المزامنة بعد",
"proxmoxLastSyncNoResult": "لا توجد تفاصيل عن النتائج",
"proxmoxLastSyncSummary": "تم إنشاء {{created}} ، تم تحديث {{updated}} ، مفقود {{markedMissing}} ، تم تخطي {{skipped}}",
"proxmoxLastSyncStatus": {
"success": "نجاح",
"error": "فشل",
"pending": "قيد الانتظار"
},
"proxmoxDiscoverAction": "اكتشف واستورد ضيوف Proxmox",
"proxmoxImportTitle": "الاستيراد من Proxmox",
"proxmoxSelectHost": "حدد مضيف Proxmox…",
@@ -1167,7 +1182,57 @@
"shareHost": "مشاركة الاستضافة",
"shareHostTitle": "مشاركة: {{name}}",
"sharing": {
"requiresCredential": "يجب أن يستخدم هذا المضيف بيانات اعتماد لتمكين المشاركة. قم بتحرير المضيف وتعيين بيانات اعتماد له أولاً."
"loadError": "فشل تحميل بيانات المشاركة. يرجى المحاولة مرة أخرى.",
"shareWithSection": "شارك مع",
"usersTab": "المستخدمون",
"rolesTab": "الأدوار",
"searchPlaceholder": "ابحث عن المستخدمين أو الأدوار...",
"noMatches": "لم يتم العثور على نتائج",
"permissionLevelLabel": "مستوى الإذن",
"levels": {
"connect": {
"label": "يتصل",
"description": "الجلسات المفتوحة فقط: الطرفية، سطح المكتب البعيد، مدير الملفات، الأنفاق، وDocker. لا يُسمح بالوصول إلى إعدادات المضيف."
},
"view": {
"label": "منظر",
"description": "اتصل، واطلع على إعدادات المضيف. لا يتم عرض أي بيانات سرية."
},
"edit": {
"label": "يحرر",
"description": "يمكنك عرض المضيف وتعديله. يمكن استبدال البيانات السرية ولكن لا يمكن قراءتها مطلقًا؛ وتبقى بيانات الاعتماد خاصة بالمالك فقط."
},
"manage": {
"label": "يدير",
"description": "قم بالتحرير، بالإضافة إلى مشاركة المضيف مع الآخرين، وتغيير مستويات الأذونات وإلغاء الوصول."
}
},
"expiryLabel": "انتهاء صلاحية الوصول",
"expiry": {
"never": "أبداً",
"oneHour": "ساعة واحدة",
"oneDay": "24 ساعة",
"sevenDays": "7 أيام",
"thirtyDays": "30 يومًا",
"custom": "مخصص"
},
"customHoursPlaceholder": "عدد الساعات المتبقية حتى انتهاء صلاحية الوصول",
"shareButton": "يشارك",
"shareWithCount": "مشاركة ({{count}})",
"currentAccess": "الوصول الحالي",
"noAccessEntries": "لم تتم مشاركة هذا المضيف بعد",
"grantedBy": "مُنِحَتْ بِـ",
"expires": "تاريخ الانتهاء",
"expired": "منتهي الصلاحية",
"never": "أبداً",
"revoke": "إلغاء",
"accessUpdated": "تم تحديث الوصول",
"accessUpdateFailed": "فشل تحديث الوصول",
"sharedBadge": "مشترك",
"sharedBadgeTooltip": "تمت المشاركة بواسطة {{owner}} ({{level}} الوصول)",
"viewOnlyBanner": "هذا المضيف مُشترك معك بواسطة {{owner}} مع إمكانية عرض الملف. الإعدادات للقراءة فقط.",
"sharedEditBanner": "هذا المضيف مُشترك معك بواسطة {{owner}} مع إمكانية التعديل. تُطبّق التغييرات على المضيف الفعلي؛ ولا يُمكن تغيير بيانات المصادقة إلا من قِبل المالك.",
"ownerOnlyControl": "لا يمكن تغيير هذا إلا من قبل مالك المضيف"
},
"guac": {
"connection": "اتصال",
@@ -1302,28 +1367,10 @@
"backspaceKey": "مفتاح الحذف",
"saveHostFirst": "احفظ المضيف أولاً.",
"sharingOptionsAfterSave": "تتوفر خيارات المشاركة بعد حفظ المضيف.",
"sharingLoadError": "فشل تحميل بيانات المشاركة. تحقق من اتصالك وحاول مرة أخرى.",
"shareHostSection": "مشاركة الاستضافة",
"shareWithUser": "مشاركة مع المستخدم",
"shareWithRole": "مشاركة مع الدور",
"selectUser": "حدد المستخدم",
"selectRole": "اختر الدور",
"selectUserOption": "اختر مستخدمًا...",
"selectRoleOption": "اختر دورًا...",
"permissionLevel": "مستوى الإذن",
"expiresInHours": "ينتهي خلال (ساعات)",
"noExpiryPlaceholder": "اتركه فارغًا لعدم وجود تاريخ انتهاء صلاحية",
"shareBtn": "يشارك",
"currentAccess": "الوصول الحالي",
"typeHeader": "يكتب",
"targetHeader": "هدف",
"permissionHeader": "إذن",
"grantedByHeader": "مُنِحَتْ بِجِهِ",
"expiresHeader": "تاريخ الانتهاء",
"noAccessEntries": "لا توجد إدخالات وصول حتى الآن.",
"expiredLabel": "منتهي الصلاحية",
"neverLabel": "أبداً",
"revokeBtn": "إلغاء",
"cancelBtn": "يلغي",
"savingBtn": "توفير...",
"updateHostBtn": "تحديث المضيف",
@@ -2190,7 +2237,8 @@
"passwordLoginDisabledDesc": "تم تعطيل تسجيل الدخول باستخدام كلمة المرور. استخدم رمز مرور أو موفر مصادقة خارجي.",
"signInWithPasskey": "سجّل الدخول باستخدام رمز المرور",
"passkeyLoginFailed": "فشل تسجيل الدخول باستخدام مفتاح المرور",
"attemptsRemaining": "عدد المحاولات المتبقية: {{count}}"
"attemptsRemaining": "عدد المحاولات المتبقية: {{count}}",
"confirmResetDataWipe": "لم يتم تسجيل الدخول إلى هذا الحساب منذ تحديث التشفير، لذا لا يمكن استعادة بياناته المخزنة بدون كلمة المرور القديمة. ستؤدي إعادة التعيين إلى حذف مضيفيه وبيانات اعتماده ومقتطفاته نهائيًا. هل تريد المتابعة؟"
},
"hostKey": {
"verifyNewHost": "تحقق من مفتاح مضيف SSH",
@@ -2323,6 +2371,13 @@
"serviceLinks": "روابط الخدمة",
"homepagePreview": "معاينة الصفحة الرئيسية"
},
"donation": {
"title": "هل تستمتع باستخدام Termix؟",
"body": "Termix برنامج مجاني ومفتوح المصدر، تم تطويره وصيانته بواسطة فريق مكون من شخصين في أوقات فراغهم. إذا حلّ Termix محل أداة تجارية كنت ستدفع ثمنها، فإن تبرعك يساعد في تغطية تكاليف الاستضافة واستمرار التطوير. حاليًا، التبرعات متاحة بالعملات الرقمية فقط.",
"milestones": "تساهم التبرعات أيضاً في تمويل الوقت اللازم للبحث والتعلم اللازمين لتطوير ميزات مثل دعم SAML وKubernetes. تابعوا التقدم المحرز على صفحة التبرعات.",
"cta": "يتبرع",
"dismiss": "ربما لاحقاً"
},
"dashboardTab": {
"stable": "مستقر",
"hostsOnline": "المضيفون عبر الإنترنت",
@@ -2790,6 +2845,71 @@
"updateAdminStatusFailed": "فشل تحديث حالة المسؤول",
"allSessionsRevoked": "تم إلغاء جميع الجلسات",
"revokeSessionsFailed": "فشل في إلغاء الجلسات",
"manageUserData": "إدارة بيانات المستخدم",
"backToUsers": "العودة إلى المستخدمين",
"manageTabAccount": "حساب",
"manageTabHosts": "المضيفون",
"manageTabCredentials": "أوراق اعتماد",
"manageTabSnippets": "مقتطفات",
"manageTabSessions": "الجلسات",
"manageTabDanger": "خطر",
"manageEditorBack": "العودة إلى بيانات {{username}}",
"dataLockedBadge": "مغلق",
"dataLockedNotice": "تبقى بيانات المستخدم {{username}}مقفلة حتى يقوم بتسجيل الدخول مرة أخرى. ولا يمكن عرض أو تعديل مضيفيه وبيانات اعتماده ومقتطفاته حتى ذلك الحين.",
"resetPasswordTitle": "إعادة تعيين كلمة المرور",
"resetPasswordOidcOnly": "يقوم هذا المستخدم بتسجيل الدخول من خلال مزود خدمة خارجي وليس لديه كلمة مرور.",
"resetPasswordPlaceholder": "كلمة المرور الجديدة",
"resetPasswordBtn": "إعادة ضبط",
"resetPasswordWorking": "جارٍ إعادة الضبط...",
"resetPasswordSuccess": "إعادة تعيين كلمة المرور",
"resetPasswordSuccessWiped": "تمت إعادة تعيين كلمة المرور. تم مسح بيانات المستخدم المشفرة.",
"resetPasswordFailed": "فشل إعادة تعيين كلمة المرور",
"resetPasswordConfirmWipe": "لم يسجّل المستخدم {{username}} دخوله منذ تحديث التشفير، لذا لا يمكن استعادة بياناته. إعادة التعيين الآن ستحذف مضيفيه وبيانات اعتماده وبياناته الشخصية. هل تريد المتابعة؟",
"totpSectionTitle": "المصادقة الثنائية",
"totpStatusEnabled": "تم تفعيل رمز TOTP لهذا المستخدم",
"totpStatusDisabled": "لم يتم تفعيل رمز التحقق من المصدر (TOTP) لهذا المستخدم",
"disableTotp": "إبطال",
"disableTotpConfirm": "هل يجب تعطيل المصادقة الثنائية للمستخدم {{username}}؟ سيتمكنون من تسجيل الدخول باستخدام كلمة المرور فقط.",
"totpDisabledSuccess": "تم تعطيل رمز التحقق من الهاتف",
"totpDisableFailed": "فشل تعطيل بروتوكول TOTP",
"manageApiKeys": "مفاتيح واجهة برمجة التطبيقات",
"apiKeyNamePlaceholder": "الاسم الرئيسي",
"apiKeyCopyNotice": "انسخ هذا المفتاح الآن، فلن يظهر مرة أخرى.",
"noApiKeysForUser": "لا توجد مفاتيح API",
"apiKeyDeleteFailed": "فشل حذف مفتاح API",
"exportUserData": "تصدير البيانات",
"exportUserDataDesc": "قم بتنزيل بيانات المضيفين وبيانات الاعتماد وبيانات مدير الملفات الخاصة بهذا المستخدم بصيغة JSON. يتم فك تشفير البيانات السرية.",
"exportUserDataSuccess": "تم تصدير بيانات المستخدم",
"exportUserDataFailed": "فشل تصدير بيانات المستخدم",
"hostsCount": "{{count}} المضيفون",
"addHostForUser": "إضافة مضيف",
"noHostsForUser": "لا يملك هذا المستخدم أي مضيفين",
"connectToHost": "يتصل",
"deleteHostConfirm": "حذف المضيف \"{{name}}\" التابع لـ \" {{username}}\"؟",
"hostDeletedSuccess": "تم حذف المضيف",
"hostDeleteFailed": "فشل حذف المضيف",
"credentialsCount": "بيانات الاعتماد {{count}}",
"addCredentialForUser": "إضافة بيانات اعتماد",
"noCredentialsForUser": "لا يملك هذا المستخدم بيانات اعتماد",
"deleteCredentialConfirm": "حذف بيانات الاعتماد \"{{name}}\" التابعة لـ \" {{username}}\"؟",
"credentialDeletedSuccess": "تم حذف بيانات الاعتماد",
"credentialDeleteFailed": "فشل حذف بيانات الاعتماد",
"snippetsCount": "مقتطفات {{count}}",
"addSnippetForUser": "إضافة مقتطف",
"noSnippetsForUser": "لا يملك هذا المستخدم أي مقتطفات",
"snippetRequiredFields": "اسم المقتطف ومحتواه مطلوبان",
"snippetNamePlaceholder": "اسم المقتطف",
"snippetContentPlaceholder": "محتوى الأمر",
"snippetFolderPlaceholder": "مجلد (اختياري)",
"snippetSaved": "تم حفظ المقتطف",
"snippetSaveFailed": "فشل حفظ المقتطف",
"deleteSnippetConfirm": "حذف المقتطف \"{{name}}\" التابع لـ {{username}}؟",
"snippetDeletedSuccess": "تم حذف المقتطف",
"snippetDeleteFailed": "فشل حذف المقتطف",
"noSessionsForUser": "لا توجد جلسات نشطة",
"deleteUserDangerDesc": "احذف نهائيًا {{username}} وجميع بياناتها (المضيفين، بيانات الاعتماد، المقتطفات، السجل). لا يمكن التراجع عن هذا الإجراء.",
"deleteUserConfirm": "هل تريد حذف {{username}} وجميع بياناتهم بشكل دائم؟",
"deleteUserAdminBlocked": "قم بإزالة صلاحيات المسؤول قبل حذف هذا المستخدم.",
"createRoleRequired": "الاسم واسم العرض مطلوبان",
"createRoleSuccess": "تم إنشاء الدور \"{{name}}\"",
"createRoleFailed": "فشل إنشاء الدور",
@@ -2822,7 +2942,16 @@
"hostDefaultsCommandHistory": "سجل الأوامر",
"hostDefaultsCommandHistoryDesc": "يتم تتبع سجل الأوامر على الأجهزة المضيفة الجديدة افتراضيًا",
"hostDefaultsSaved": "تم حفظ الإعدادات الافتراضية للمضيف",
"hostDefaultsSaveFailed": "فشل حفظ الإعدادات الافتراضية للمضيف"
"hostDefaultsSaveFailed": "فشل حفظ الإعدادات الافتراضية للمضيف",
"rolePermissions": {
"count": "{{count}} الأذونات",
"editAction": "تعديل الأذونات",
"loadError": "فشل تحميل كتالوج الأذونات",
"saved": "تم حفظ أذونات الدور",
"saveError": "فشل حفظ أذونات الدور",
"save": "يحفظ",
"saving": "توفير..."
}
},
"newUi": {
"sidebar": {
@@ -2991,6 +3120,10 @@
"executionFailed": "فشل"
},
"userProfile": {
"donateTitle": "حافظ على حياة Termix",
"donateDescription": "Termix مجاني، ويُستضاف ذاتيًا، وقد طُوّر بواسطة عدد قليل من الأشخاص في أوقات فراغهم. إذا وفّر لك الوقت أو المال، فإن التبرع بالعملات الرقمية يُساعد في استمراره.",
"donateMilestones": "تساهم التبرعات في تمويل الوقت اللازم للبحث والتعلم حول ما يلزم لبناء دعم SAML وKubernetes وAgent. تابعوا التقدم وتبرعوا.",
"donateButton": "تبرع بالعملات الرقمية",
"storageModeLocal": "المتصفح",
"storageModeCloud": "قاعدة البيانات",
"storageModeDescription": "يخزن المتصفح الإعدادات في هذا المتصفح فقط. تتم مزامنة قاعدة البيانات مع الخادم ويتم تحميلها على أي جهاز.",
@@ -3011,6 +3144,10 @@
"twoFaOn": "على",
"twoFaOff": "عن",
"versionLabel": "إصدار",
"betaProgramTitle": "برنامج تجريبي",
"betaProgramDescription": "جرّب الميزات القادمة مبكراً باستخدام علامة Docker :beta الأسبوعية المتجددة. غير مستقر، غير مناسب للاستخدام في بيئات الإنتاج.",
"betaProgramFeedback": "هل وجدت خطأً برمجياً؟ أبلغ عنه هنا.",
"betaProgramLearnMore": "يتعلم أكثر",
"deleteAccount": "حذف الحساب",
"deleteAccountDescription": "احذف حسابك نهائياً",
"changeServerDescription": "قم بالتبديل إلى خادم Termix خلفي مختلف",
+158 -21
View File
@@ -864,6 +864,21 @@
"proxmoxDockerDetectionDesc": "Модели за имена, разделени със запетаи, които позволяват на Docker да съпоставя гости",
"proxmoxPreferredRanges": "Предпочитани IP диапазони",
"proxmoxPreferredRangesDesc": "Префикси, разделени със запетаи, подредени по приоритет за избор на IP адрес, когато гост има множество интерфейси",
"proxmoxAutoSync": "Автоматично синхронизиране на гости",
"proxmoxAutoSyncDesc": "Периодично откривайте този Proxmox възел и създавайте или актуализирайте импортирани гост хостове, докато сесията ви е отключена.",
"proxmoxSyncInterval": "Интервал на синхронизиране (минути)",
"proxmoxSyncIntervalDesc": "Минимум 5 минути. Планировчикът пропуска заключените потребителски сесии.",
"proxmoxMarkMissing": "Маркиране на липсващи гости",
"proxmoxMarkMissingDesc": "Добавете етикет proxmox-missing, когато предварително импортиран гост изчезне, вместо да го изтриете.",
"proxmoxLastSync": "Последна синхронизация",
"proxmoxLastSyncNever": "Все още не е синхронизирано",
"proxmoxLastSyncNoResult": "Няма подробности за резултата",
"proxmoxLastSyncSummary": "{{created}} създадено, {{updated}} актуализирано, {{markedMissing}} липсващо, {{skipped}} пропуснато",
"proxmoxLastSyncStatus": {
"success": "Успех",
"error": "Неуспешно",
"pending": "В очакване"
},
"proxmoxDiscoverAction": "Откриване и импортиране на гости на Proxmox",
"proxmoxImportTitle": "Импортиране от Proxmox",
"proxmoxSelectHost": "Изберете Proxmox хост…",
@@ -1167,7 +1182,57 @@
"shareHost": "Споделяне на хост",
"shareHostTitle": "Споделяне: {{name}}",
"sharing": {
"requiresCredential": "Този хост трябва да използва идентификационни данни, за да активира споделянето. Първо редактирайте хоста и задайте идентификационни данни."
"loadError": "Зареждането на данните за споделяне не бе успешно. Моля, опитайте отново.",
"shareWithSection": "Сподели с",
"usersTab": "Потребители",
"rolesTab": "Роли",
"searchPlaceholder": "Търсене на потребители или роли...",
"noMatches": "Няма намерени съвпадения",
"permissionLevelLabel": "Ниво на разрешение",
"levels": {
"connect": {
"label": "Свържете се",
"description": "Само отворени сесии: терминал, отдалечен работен плот, файлов мениджър, тунели и Docker. Няма достъп до конфигурацията на хоста."
},
"view": {
"label": "Преглед",
"description": "Свържете се, плюс вижте конфигурацията на хоста. Тайните никога не се показват."
},
"edit": {
"label": "Редактиране",
"description": "Преглеждайте и променяйте хоста. Тайните могат да бъдат заменени, но никога прочетени; присвояванията на идентификационни данни остават само за собственика."
},
"manage": {
"label": "Управление",
"description": "Редактирайте, плюс споделяйте хоста с други, променяйте нивата на разрешения и отменяйте достъпа."
}
},
"expiryLabel": "Изтичане на достъпа",
"expiry": {
"never": "Никога",
"oneHour": "1 час",
"oneDay": "24 часа",
"sevenDays": "7 дни",
"thirtyDays": "30 дни",
"custom": "Персонализирано"
},
"customHoursPlaceholder": "Часове до изтичане на срока на достъп",
"shareButton": "Споделяне",
"shareWithCount": "Споделяне ({{count}})",
"currentAccess": "Текущ достъп",
"noAccessEntries": "Този хост все още не е споделен",
"grantedBy": "Предоставено от",
"expires": "Изтича",
"expired": "Изтекъл",
"never": "Никога",
"revoke": "Отмяна",
"accessUpdated": "Достъпът е актуализиран",
"accessUpdateFailed": "Актуализирането на достъпа не бе успешно",
"sharedBadge": "Споделено",
"sharedBadgeTooltip": "Споделено от {{owner}} (достъп{{level}})",
"viewOnlyBanner": "Този хост е споделен с вас от {{owner}} с достъп за преглед. Конфигурацията е само за четене.",
"sharedEditBanner": "Този хост е споделен с вас от {{owner}} с достъп за редактиране. Промените се отнасят за истинския хост; референтните данни за удостоверяване могат да бъдат променяни само от собственика.",
"ownerOnlyControl": "Само собственикът на хоста може да промени това"
},
"guac": {
"connection": "Връзка",
@@ -1302,28 +1367,10 @@
"backspaceKey": "Клавиш Backspace",
"saveHostFirst": "Първо запазете хоста.",
"sharingOptionsAfterSave": "Опциите за споделяне са налични след запазване на хоста.",
"sharingLoadError": "Зареждането на данните за споделяне не бе успешно. Проверете връзката си и опитайте отново.",
"shareHostSection": "Споделяне на хост",
"shareWithUser": "Споделяне с потребителя",
"shareWithRole": "Споделяне с роля",
"selectUser": "Изберете потребител",
"selectRole": "Изберете роля",
"selectUserOption": "Изберете потребител...",
"selectRoleOption": "Изберете роля...",
"permissionLevel": "Ниво на разрешение",
"expiresInHours": "Изтича след (часове)",
"noExpiryPlaceholder": "Оставете празно за дата без срок на валидност",
"shareBtn": "Споделяне",
"currentAccess": "Текущ достъп",
"typeHeader": "Тип",
"targetHeader": "Цел",
"permissionHeader": "Разрешение",
"grantedByHeader": "Предоставено от",
"expiresHeader": "Изтича",
"noAccessEntries": "Все още няма записи за достъп.",
"expiredLabel": "Изтекъл",
"neverLabel": "Никога",
"revokeBtn": "Отмяна",
"cancelBtn": "Отказ",
"savingBtn": "Запазване...",
"updateHostBtn": "Актуализиране на хоста",
@@ -2190,7 +2237,8 @@
"passwordLoginDisabledDesc": "Входът с парола е деактивиран. Използвайте ключ за достъп или външен доставчик на удостоверяване.",
"signInWithPasskey": "Влезте с парола",
"passkeyLoginFailed": "Влизането с парола не бе успешно",
"attemptsRemaining": "{{count}} attempts remaining"
"attemptsRemaining": "{{count}} attempts remaining",
"confirmResetDataWipe": "Този акаунт не е влизал след надстройката на криптирането, така че съхранените в него данни не могат да бъдат възстановени без старата парола. Нулирането ще изтрие окончателно неговите хостове, идентификационни данни и фрагменти. Искате ли да продължите?"
},
"hostKey": {
"verifyNewHost": "Проверете SSH хост ключа",
@@ -2323,6 +2371,13 @@
"serviceLinks": "Връзки към услуги",
"homepagePreview": "Преглед на началната страница"
},
"donation": {
"title": "Харесва ли ви Termix?",
"body": "Termix е безплатен и с отворен код, разработен и поддържан от екип от двама души в свободното ни време. Ако замести търговски инструмент, за който иначе бихте платили, дарението помага за покриване на разходите за хостинг и поддържа разработката. Към момента даренията са само в криптовалута.",
"milestones": "Даренията също помагат за финансиране на времето за проучване и изучаване на необходимото за изграждане на функции като поддръжка на SAML и Kubernetes. Вижте напредъка на страницата за дарения.",
"cta": "Дарете",
"dismiss": "Може би по-късно"
},
"dashboardTab": {
"stable": "СТАБИЛЕН",
"hostsOnline": "Домакините онлайн",
@@ -2790,6 +2845,71 @@
"updateAdminStatusFailed": "Актуализирането на администраторския статус не бе успешно",
"allSessionsRevoked": "Всички сесии са отменени",
"revokeSessionsFailed": "Неуспешно отменяне на сесиите",
"manageUserData": "Управление на потребителските данни",
"backToUsers": "Обратно към потребителите",
"manageTabAccount": "Профил",
"manageTabHosts": "Домакини",
"manageTabCredentials": "Пълномощия",
"manageTabSnippets": "Откъси",
"manageTabSessions": "Сесии",
"manageTabDanger": "Опасност",
"manageEditorBack": "Обратно към данните на {{username}}",
"dataLockedBadge": "ЗАКЛЮЧЕНО",
"dataLockedNotice": "Данните на {{username}}остават заключени до следващото им влизане. Техните хостове, идентификационни данни и фрагменти не могат да бъдат преглеждани или редактирани дотогава.",
"resetPasswordTitle": "Нулиране на парола",
"resetPasswordOidcOnly": "Този потребител влиза чрез външен доставчик и няма парола.",
"resetPasswordPlaceholder": "Нова парола",
"resetPasswordBtn": "Нулиране",
"resetPasswordWorking": "Нулиране...",
"resetPasswordSuccess": "Възстановяване на паролата",
"resetPasswordSuccessWiped": "Нулиране на паролата. Криптираните данни на потребителя бяха изтрити.",
"resetPasswordFailed": "Неуспешно нулиране на паролата",
"resetPasswordConfirmWipe": "{{username}} не е влизал в системата след надстройката на криптирането, така че данните му не могат да бъдат възстановени. Нулирането сега ще изтрие неговите хостове, идентификационни данни и фрагменти. Искате ли да продължите?",
"totpSectionTitle": "Двуфакторно удостоверяване",
"totpStatusEnabled": "TOTP е активиран за този потребител",
"totpStatusDisabled": "TOTP не е активиран за този потребител",
"disableTotp": "Деактивиране",
"disableTotpConfirm": "Деактивиране на двуфакторното удостоверяване за {{username}}? Те ще могат да влизат само с паролата си.",
"totpDisabledSuccess": "TOTP е деактивиран",
"totpDisableFailed": "Деактивирането на TOTP не бе успешно",
"manageApiKeys": "API ключове",
"apiKeyNamePlaceholder": "Име на ключа",
"apiKeyCopyNotice": "Копирайте този ключ сега, той няма да се показва отново.",
"noApiKeysForUser": "Няма API ключове",
"apiKeyDeleteFailed": "Изтриването на API ключа не бе успешно",
"exportUserData": "Експортиране на данни",
"exportUserDataDesc": "Изтеглете хостовете, идентификационните данни и данните за файловия мениджър на този потребител като JSON. Тайните данни са дешифрирани.",
"exportUserDataSuccess": "Експортирани потребителски данни",
"exportUserDataFailed": "Експортирането на потребителските данни не бе успешно",
"hostsCount": "{{count}} хостове",
"addHostForUser": "Добавяне на хост",
"noHostsForUser": "Този потребител няма хостове",
"connectToHost": "Свържете се",
"deleteHostConfirm": "Да се изтрие ли хостът „{{name}}“, принадлежащ на {{username}}?",
"hostDeletedSuccess": "Хостът е изтрит",
"hostDeleteFailed": "Изтриването на хоста не бе успешно",
"credentialsCount": "{{count}} идентификационни данни",
"addCredentialForUser": "Добавяне на идентификационни данни",
"noCredentialsForUser": "Този потребител няма идентификационни данни",
"deleteCredentialConfirm": "Да се изтрият ли идентификационните данни „{{name}}“, принадлежащи на {{username}}?",
"credentialDeletedSuccess": "Идентификационните данни са изтрити",
"credentialDeleteFailed": "Неуспешно изтриване на идентификационните данни",
"snippetsCount": "{{count}} фрагменти",
"addSnippetForUser": "Добавяне на фрагмент",
"noSnippetsForUser": "Този потребител няма фрагменти",
"snippetRequiredFields": "Името и съдържанието на фрагмента са задължителни",
"snippetNamePlaceholder": "Име на фрагмента",
"snippetContentPlaceholder": "Съдържание на командата",
"snippetFolderPlaceholder": "Папка (по избор)",
"snippetSaved": "Фрагментът е запазен",
"snippetSaveFailed": "Запазването на фрагмента не бе успешно",
"deleteSnippetConfirm": "Да се изтрие ли фрагментът „{{name}}“, принадлежащ на {{username}}?",
"snippetDeletedSuccess": "Фрагментът е изтрит",
"snippetDeleteFailed": "Изтриването на фрагмента не бе успешно",
"noSessionsForUser": "Няма активни сесии",
"deleteUserDangerDesc": "Изтриване за постоянно на {{username}} и всички негови данни (хостове, идентификационни данни, фрагменти, история). Това не може да бъде отменено.",
"deleteUserConfirm": "Да се изтрие ли окончателно {{username}} и всичките му данни?",
"deleteUserAdminBlocked": "Премахнете администраторския статус, преди да изтриете този потребител.",
"createRoleRequired": "Името и показваното име са задължителни",
"createRoleSuccess": "Ролята „{{name}}“ е създадена",
"createRoleFailed": "Създаването на роля не бе успешно",
@@ -2822,7 +2942,16 @@
"hostDefaultsCommandHistory": "История на командите",
"hostDefaultsCommandHistoryDesc": "Проследяване на историята на командите на нови хостове по подразбиране",
"hostDefaultsSaved": "Настройките по подразбиране на хоста са запазени",
"hostDefaultsSaveFailed": "Запазването на настройките по подразбиране на хоста не бе успешно"
"hostDefaultsSaveFailed": "Запазването на настройките по подразбиране на хоста не бе успешно",
"rolePermissions": {
"count": "{{count}} permissions",
"editAction": "Редактиране на разрешенията",
"loadError": "Зареждането на каталога с разрешения не бе успешно",
"saved": "Разрешенията за роли са запазени",
"saveError": "Запазването на разрешенията за ролята не бе успешно",
"save": "Запазване",
"saving": "Запазване..."
}
},
"newUi": {
"sidebar": {
@@ -2991,6 +3120,10 @@
"executionFailed": "Неуспешно"
},
"userProfile": {
"donateTitle": "Пази Термикс жив",
"donateDescription": "Termix е безплатен, самостоятелно хостван и изграден само от няколко души в свободното им време. Ако ви е спестил време или пари, дарението от крипто помага да продължи да функционира.",
"donateMilestones": "Даренията помагат да се финансира времето за проучване и изучаване на необходимото за изграждане на поддръжка за SAML, Kubernetes и Agent. Вижте напредъка и дарете.",
"donateButton": "Дарете криптовалута",
"storageModeLocal": "Браузър",
"storageModeCloud": "База данни",
"storageModeDescription": "Браузърът съхранява настройките само в този браузър. Базата данни се синхронизира със сървъра и се зарежда на всяко устройство.",
@@ -3011,6 +3144,10 @@
"twoFaOn": "Включено",
"twoFaOff": "Изключено",
"versionLabel": "Версия",
"betaProgramTitle": "Бета програма",
"betaProgramDescription": "Изпробвайте предстоящите функции рано със седмичния подвижен таг :beta на Docker. Нестабилна версия, не е за производство.",
"betaProgramFeedback": "Открихте грешка? Докладвайте я тук.",
"betaProgramLearnMore": "Научете повече",
"deleteAccount": "Изтриване на акаунт",
"deleteAccountDescription": "Изтрийте профила си завинаги",
"changeServerDescription": "Преминете към друг Termix backend сървър",
+158 -21
View File
@@ -864,6 +864,21 @@
"proxmoxDockerDetectionDesc": "কমা দ্বারা পৃথক করা নামের প্যাটার্ন যা ডকারকে গেস্টদের মেলানোর জন্য সক্ষম করে",
"proxmoxPreferredRanges": "পছন্দের আইপি রেঞ্জ",
"proxmoxPreferredRangesDesc": "যখন কোনো গেস্টের একাধিক ইন্টারফেস থাকে, তখন আইপি নির্বাচনের জন্য অগ্রাধিকার ক্রমে কমা দ্বারা পৃথক করা প্রিফিক্সগুলো ব্যবহৃত হয়।",
"proxmoxAutoSync": "অতিথিদের স্বয়ংক্রিয়ভাবে সিঙ্ক করুন",
"proxmoxAutoSyncDesc": "আপনার সেশন আনলক থাকা অবস্থায় পর্যায়ক্রমে এই প্রক্সমক্স নোডটি আবিষ্কার করুন এবং ইম্পোর্ট করা গেস্ট হোস্টগুলি তৈরি বা আপডেট করুন।",
"proxmoxSyncInterval": "সিঙ্ক ব্যবধান (মিনিট)",
"proxmoxSyncIntervalDesc": "সর্বনিম্ন ৫ মিনিট। শিডিউলারটি লক করা ব্যবহারকারীর সেশনগুলো এড়িয়ে যায়।",
"proxmoxMarkMissing": "অনুপস্থিত অতিথিদের চিহ্নিত করুন",
"proxmoxMarkMissingDesc": "পূর্বে ইম্পোর্ট করা কোনো গেস্ট অদৃশ্য হয়ে গেলে, সেটিকে ডিলিট না করে একটি proxmox-missing ট্যাগ যোগ করুন।",
"proxmoxLastSync": "শেষ সিঙ্ক",
"proxmoxLastSyncNever": "এখনো সিঙ্ক হয়নি",
"proxmoxLastSyncNoResult": "ফলাফলের কোনো বিবরণ নেই",
"proxmoxLastSyncSummary": "{{created}} তৈরি করা হয়েছে, {{updated}} আপডেট করা হয়েছে, {{markedMissing}} অনুপস্থিত, {{skipped}} বাদ দেওয়া হয়েছে",
"proxmoxLastSyncStatus": {
"success": "সাফল্য",
"error": "ব্যর্থ",
"pending": "বিচারাধীন"
},
"proxmoxDiscoverAction": "Proxmox গেস্টদের আবিষ্কার ও আমদানি করুন",
"proxmoxImportTitle": "প্রক্সমক্স থেকে আমদানি করুন",
"proxmoxSelectHost": "একটি প্রক্সমক্স হোস্ট নির্বাচন করুন…",
@@ -1167,7 +1182,57 @@
"shareHost": "হোস্ট শেয়ার করুন",
"shareHostTitle": "শেয়ার করুন: {{name}}",
"sharing": {
"requiresCredential": "শেয়ারিং সক্ষম করতে এই হোস্টকে অবশ্যই একটি ক্রেডেনশিয়াল ব্যবহার করতে হবে। প্রথমে হোস্টটি সম্পাদনা করুন এবং একটি ক্রেডেনশিয়াল নির্ধারণ করুন।"
"loadError": "শেয়ারিং ডেটা লোড করা সম্ভব হয়নি। অনুগ্রহ করে আবার চেষ্টা করুন।",
"shareWithSection": "শেয়ার করুন",
"usersTab": "ব্যবহারকারীরা",
"rolesTab": "ভূমিকা",
"searchPlaceholder": "ব্যবহারকারী বা ভূমিকা অনুসন্ধান করুন...",
"noMatches": "কোনো মিল খুঁজে পাওয়া যায়নি",
"permissionLevelLabel": "অনুমতি স্তর",
"levels": {
"connect": {
"label": "সংযোগ করুন",
"description": "শুধুমাত্র টার্মিনাল, রিমোট ডেস্কটপ, ফাইল ম্যানেজার, টানেল এবং ডকার সেশনগুলো খোলা থাকবে। হোস্ট কনফিগারেশনে কোনো অ্যাক্সেস নেই।"
},
"view": {
"label": "দেখুন",
"description": "সংযোগ করুন, এবং হোস্ট কনফিগারেশন দেখুন। গোপনীয় তথ্য কখনোই দেখানো হয় না।"
},
"edit": {
"label": "সম্পাদনা",
"description": "হোস্টটি দেখুন এবং পরিবর্তন করুন। গোপনীয় তথ্য প্রতিস্থাপন করা গেলেও পড়া যাবে না; ক্রেডেনশিয়াল অ্যাসাইনমেন্ট শুধুমাত্র মালিকের কাছেই থাকবে।"
},
"manage": {
"label": "পরিচালনা করুন",
"description": "সম্পাদনা করুন, সেইসাথে অন্যদের সাথে হোস্টটি শেয়ার করুন, অনুমতির স্তর পরিবর্তন করুন এবং অ্যাক্সেস প্রত্যাহার করুন।"
}
},
"expiryLabel": "অ্যাক্সেসের মেয়াদ শেষ",
"expiry": {
"never": "কখনো না",
"oneHour": "১ ঘন্টা",
"oneDay": "২৪ ঘন্টা",
"sevenDays": " দিন",
"thirtyDays": "৩০ দিন",
"custom": "কাস্টম"
},
"customHoursPlaceholder": "অ্যাক্সেসের মেয়াদ শেষ হতে বাকি ঘন্টা",
"shareButton": "শেয়ার",
"shareWithCount": "শেয়ার করুন ({{count}})",
"currentAccess": "বর্তমান অ্যাক্সেস",
"noAccessEntries": "এই হোস্টটি এখনও শেয়ার করা হয়নি।",
"grantedBy": "মঞ্জুর করেছেন",
"expires": "মেয়াদ শেষ",
"expired": "মেয়াদোত্তীর্ণ",
"never": "কখনো না",
"revoke": "বাতিল করুন",
"accessUpdated": "অ্যাক্সেস আপডেট করা হয়েছে",
"accessUpdateFailed": "অ্যাক্সেস আপডেট করতে ব্যর্থ হয়েছে",
"sharedBadge": "শেয়ার করা",
"sharedBadgeTooltip": "{{owner}} ({{level}} অ্যাক্সেস) দ্বারা শেয়ার করা হয়েছে",
"viewOnlyBanner": "এই হোস্টটি {{owner}} আপনার সাথে দেখার অ্যাক্সেস সহ শেয়ার করেছে। কনফিগারেশনটি শুধুমাত্র পঠনযোগ্য।",
"sharedEditBanner": "এই হোস্টটি {{owner}} দ্বারা আপনার সাথে সম্পাদনার অ্যাক্সেস সহ শেয়ার করা হয়েছে। পরিবর্তনগুলি আসল হোস্টে প্রযোজ্য হবে; প্রমাণীকরণ রেফারেন্স শুধুমাত্র মালিক দ্বারা পরিবর্তন করা যাবে।",
"ownerOnlyControl": "শুধুমাত্র হোস্টের মালিকই এটি পরিবর্তন করতে পারেন।"
},
"guac": {
"connection": "সংযোগ",
@@ -1302,28 +1367,10 @@
"backspaceKey": "ব্যাকস্পেস কী",
"saveHostFirst": "প্রথমে হোস্টটি সংরক্ষণ করুন।",
"sharingOptionsAfterSave": "হোস্ট সংরক্ষণ করার পর শেয়ার করার বিকল্পগুলো পাওয়া যাবে।",
"sharingLoadError": "শেয়ারিং ডেটা লোড করা সম্ভব হয়নি। আপনার সংযোগ পরীক্ষা করে আবার চেষ্টা করুন।",
"shareHostSection": "হোস্ট শেয়ার করুন",
"shareWithUser": "ব্যবহারকারীর সাথে শেয়ার করুন",
"shareWithRole": "ভূমিকার সাথে শেয়ার করুন",
"selectUser": "ব্যবহারকারী নির্বাচন করুন",
"selectRole": "ভূমিকা নির্বাচন করুন",
"selectUserOption": "একজন ব্যবহারকারী নির্বাচন করুন...",
"selectRoleOption": "একটি ভূমিকা নির্বাচন করুন...",
"permissionLevel": "অনুমতি স্তর",
"expiresInHours": "(ঘন্টায়) মেয়াদ শেষ হবে",
"noExpiryPlaceholder": "মেয়াদ শেষ না হওয়ার জন্য খালি রাখুন।",
"shareBtn": "শেয়ার",
"currentAccess": "বর্তমান অ্যাক্সেস",
"typeHeader": "প্রকার",
"targetHeader": "লক্ষ্য",
"permissionHeader": "অনুমতি",
"grantedByHeader": "মঞ্জুর করেছেন",
"expiresHeader": "মেয়াদ শেষ",
"noAccessEntries": "এখনো কোনো প্রবেশাধিকার দেওয়া হয়নি।",
"expiredLabel": "মেয়াদোত্তীর্ণ",
"neverLabel": "কখনো না",
"revokeBtn": "বাতিল করুন",
"cancelBtn": "বাতিল করুন",
"savingBtn": "সঞ্চয়...",
"updateHostBtn": "হোস্ট আপডেট করুন",
@@ -2190,7 +2237,8 @@
"passwordLoginDisabledDesc": "পাসওয়ার্ড দিয়ে লগইন করা বন্ধ আছে। একটি পাসকি অথবা বাহ্যিক প্রমাণীকরণ প্রদানকারী ব্যবহার করুন।",
"signInWithPasskey": "পাসকি দিয়ে সাইন ইন করুন",
"passkeyLoginFailed": "পাসকি লগইন ব্যর্থ হয়েছে",
"attemptsRemaining": "{{count}} টি প্রচেষ্টা বাকি"
"attemptsRemaining": "{{count}} টি প্রচেষ্টা বাকি",
"confirmResetDataWipe": "এনক্রিপশন আপগ্রেডের পর থেকে এই অ্যাকাউন্টটি লগ ইন করা হয়নি, তাই পুরোনো পাসওয়ার্ড ছাড়া এর সংরক্ষিত ডেটা পুনরুদ্ধার করা যাবে না। রিসেট করলে এর হোস্ট, ক্রেডেনশিয়াল এবং স্নিপেট স্থায়ীভাবে মুছে যাবে। চালিয়ে যাবেন?"
},
"hostKey": {
"verifyNewHost": "SSH হোস্ট কী যাচাই করুন",
@@ -2323,6 +2371,13 @@
"serviceLinks": "পরিষেবা লিঙ্ক",
"homepagePreview": "হোমপেজ প্রিভিউ"
},
"donation": {
"title": "টারমিক্স উপভোগ করছেন?",
"body": "টারমিক্স একটি ফ্রি এবং ওপেন সোর্স টুল, যা আমাদের দুইজনের একটি দল অবসর সময়ে তৈরি ও রক্ষণাবেক্ষণ করে। যদি এটি আপনার কেনা কোনো বাণিজ্যিক টুলের বিকল্প হয়ে ওঠে, তবে আপনার অনুদান হোস্টিং খরচ মেটাতে এবং এর উন্নয়ন চালিয়ে যেতে সাহায্য করে। আপাতত, অনুদান শুধুমাত্র ক্রিপ্টোকারেন্সির মাধ্যমে গ্রহণ করা হয়।",
"milestones": "অনুদান SAML এবং Kubernetes সাপোর্টের মতো ফিচারগুলো তৈরি করার জন্য প্রয়োজনীয় গবেষণা ও শেখার সময় জোগাতেও সাহায্য করে। অনুদান পেজে এর অগ্রগতি দেখুন।",
"cta": "দান করুন",
"dismiss": "হয়তো পরে"
},
"dashboardTab": {
"stable": "স্থিতিশীল",
"hostsOnline": "হোস্ট অনলাইন",
@@ -2790,6 +2845,71 @@
"updateAdminStatusFailed": "অ্যাডমিন স্ট্যাটাস আপডেট করতে ব্যর্থ হয়েছে",
"allSessionsRevoked": "সকল অধিবেশন বাতিল করা হয়েছে",
"revokeSessionsFailed": "সেশন বাতিল করতে ব্যর্থ হয়েছে",
"manageUserData": "ব্যবহারকারীর ডেটা পরিচালনা করুন",
"backToUsers": "ব্যবহারকারীদের কাছে ফিরে যান",
"manageTabAccount": "অ্যাকাউন্ট",
"manageTabHosts": "হোস্টরা",
"manageTabCredentials": "যোগ্যতা",
"manageTabSnippets": "খণ্ডাংশ",
"manageTabSessions": "অধিবেশন",
"manageTabDanger": "বিপদ",
"manageEditorBack": "{{username}}এর ডেটাতে ফিরে যান",
"dataLockedBadge": "লক করা",
"dataLockedNotice": "{{username}}-এর ডেটা পরবর্তী লগইন পর্যন্ত লক করা থাকে। ততক্ষণ পর্যন্ত তাদের হোস্ট, ক্রেডেনশিয়াল এবং স্নিপেট দেখা বা সম্পাদনা করা যায় না।",
"resetPasswordTitle": "পাসওয়ার্ড রিসেট করুন",
"resetPasswordOidcOnly": "এই ব্যবহারকারী একটি বাহ্যিক প্রদানকারীর মাধ্যমে সাইন ইন করেন এবং তার কোনো পাসওয়ার্ড নেই।",
"resetPasswordPlaceholder": "নতুন পাসওয়ার্ড",
"resetPasswordBtn": "রিসেট",
"resetPasswordWorking": "রিসেট করা হচ্ছে...",
"resetPasswordSuccess": "পাসওয়ার্ড রিসেট",
"resetPasswordSuccessWiped": "পাসওয়ার্ড রিসেট করা হয়েছে। ব্যবহারকারীর এনক্রিপ্টেড ডেটা মুছে ফেলা হয়েছে।",
"resetPasswordFailed": "পাসওয়ার্ড রিসেট করতে ব্যর্থ হয়েছে",
"resetPasswordConfirmWipe": "এনক্রিপশন আপগ্রেডের পর থেকে {{username}} লগ ইন করেননি, তাই তাদের ডেটা পুনরুদ্ধার করা সম্ভব নয়। এখন রিসেট করলে তাদের হোস্ট, ক্রেডেনশিয়াল এবং স্নিপেট মুছে যাবে। চালিয়ে যাবেন?",
"totpSectionTitle": "দ্বি-স্তরীয় প্রমাণীকরণ",
"totpStatusEnabled": "এই ব্যবহারকারীর জন্য TOTP সক্রিয় করা হয়েছে।",
"totpStatusDisabled": "এই ব্যবহারকারীর জন্য TOTP সক্রিয় করা নেই।",
"disableTotp": "অক্ষম করুন",
"disableTotpConfirm": "{{username}}এর জন্য দ্বি-স্তরীয় প্রমাণীকরণ নিষ্ক্রিয় করবেন? তারা শুধুমাত্র তাদের পাসওয়ার্ড দিয়ে লগ ইন করতে পারবে।",
"totpDisabledSuccess": "TOTP নিষ্ক্রিয়",
"totpDisableFailed": "TOTP নিষ্ক্রিয় করতে ব্যর্থ হয়েছে",
"manageApiKeys": "এপিআই কী",
"apiKeyNamePlaceholder": "মূল নাম",
"apiKeyCopyNotice": "এই কী-টি এখন কপি করুন, এটি আর দেখানো হবে না।",
"noApiKeysForUser": "কোন এপিআই কী নেই",
"apiKeyDeleteFailed": "এপিআই কী মুছে ফেলতে ব্যর্থ হয়েছে",
"exportUserData": "ডেটা রপ্তানি",
"exportUserDataDesc": "এই ব্যবহারকারীর হোস্ট, ক্রেডেনশিয়াল এবং ফাইল ম্যানেজার ডেটা JSON ফরম্যাটে ডাউনলোড করুন। গোপনীয় তথ্য ডিক্রিপ্ট করা আছে।",
"exportUserDataSuccess": "ব্যবহারকারীর ডেটা রপ্তানি করা হয়েছে",
"exportUserDataFailed": "ব্যবহারকারীর ডেটা রপ্তানি করতে ব্যর্থ হয়েছে",
"hostsCount": "{{count}} হোস্ট",
"addHostForUser": "হোস্ট যোগ করুন",
"noHostsForUser": "এই ব্যবহারকারীর কোনো হোস্ট নেই",
"connectToHost": "সংযোগ করুন",
"deleteHostConfirm": "{{username}} এর অন্তর্গত হোস্ট \"{{name}}\" মুছে ফেলুন?",
"hostDeletedSuccess": "হোস্ট মুছে ফেলা হয়েছে",
"hostDeleteFailed": "হোস্ট মুছে ফেলতে ব্যর্থ হয়েছে",
"credentialsCount": "{{count}} পরিচয়পত্র",
"addCredentialForUser": "পরিচয়পত্র যোগ করুন",
"noCredentialsForUser": "এই ব্যবহারকারীর কোনো পরিচয়পত্র নেই।",
"deleteCredentialConfirm": "{{username}} এর অন্তর্গত পরিচয়পত্র \"{{name}}\" মুছে ফেলবেন?",
"credentialDeletedSuccess": "পরিচয়পত্র মুছে ফেলা হয়েছে",
"credentialDeleteFailed": "ক্রেডেনশিয়াল মুছে ফেলতে ব্যর্থ হয়েছে",
"snippetsCount": "{{count}} খণ্ডাংশ",
"addSnippetForUser": "স্নিপেট যোগ করুন",
"noSnippetsForUser": "এই ব্যবহারকারীর কোনো স্নিপেট নেই",
"snippetRequiredFields": "স্নিপেটের নাম এবং বিষয়বস্তু আবশ্যক।",
"snippetNamePlaceholder": "স্নিপেট নাম",
"snippetContentPlaceholder": "কমান্ডের বিষয়বস্তু",
"snippetFolderPlaceholder": "ফোল্ডার (ঐচ্ছিক)",
"snippetSaved": "স্নিপেট সংরক্ষিত",
"snippetSaveFailed": "স্নিপেট সংরক্ষণ করতে ব্যর্থ হয়েছে",
"deleteSnippetConfirm": "{{username}} এর অন্তর্গত স্নিপেট \"{{name}}\" মুছে ফেলবেন?",
"snippetDeletedSuccess": "স্নিপেট মুছে ফেলা হয়েছে",
"snippetDeleteFailed": "স্নিপেট মুছে ফেলা ব্যর্থ হয়েছে",
"noSessionsForUser": "কোন সক্রিয় সেশন নেই",
"deleteUserDangerDesc": "{{username}} এবং তাদের সমস্ত ডেটা (হোস্ট, ক্রেডেনশিয়াল, স্নিপেট, হিস্ট্রি) স্থায়ীভাবে মুছে ফেলা হলো। এটি আর পূর্বাবস্থায় ফেরানো যাবে না।",
"deleteUserConfirm": "{{username}} এবং তাদের সমস্ত ডেটা স্থায়ীভাবে মুছে ফেলা হবে?",
"deleteUserAdminBlocked": "এই ব্যবহারকারীকে মুছে ফেলার আগে অ্যাডমিন স্ট্যাটাস অপসারণ করুন।",
"createRoleRequired": "নাম এবং প্রদর্শিত নাম আবশ্যক",
"createRoleSuccess": "ভূমিকা \"{{name}}\" তৈরি করা হয়েছে",
"createRoleFailed": "ভূমিকা তৈরি করতে ব্যর্থ হয়েছে",
@@ -2822,7 +2942,16 @@
"hostDefaultsCommandHistory": "কমান্ডের ইতিহাস",
"hostDefaultsCommandHistoryDesc": "ডিফল্টরূপে নতুন হোস্টগুলিতে কমান্ড হিস্ট্রি ট্র্যাক করুন।",
"hostDefaultsSaved": "হোস্ট ডিফল্ট সংরক্ষিত",
"hostDefaultsSaveFailed": "হোস্ট ডিফল্ট সংরক্ষণ করতে ব্যর্থ হয়েছে"
"hostDefaultsSaveFailed": "হোস্ট ডিফল্ট সংরক্ষণ করতে ব্যর্থ হয়েছে",
"rolePermissions": {
"count": "{{count}} অনুমতি",
"editAction": "সম্পাদনার অনুমতি",
"loadError": "অনুমতি ক্যাটালগ লোড করতে ব্যর্থ হয়েছে",
"saved": "ভূমিকার অনুমতি সংরক্ষিত হয়েছে",
"saveError": "ভূমিকার অনুমতি সংরক্ষণ করতে ব্যর্থ হয়েছে",
"save": "সংরক্ষণ করুন",
"saving": "সঞ্চয়..."
}
},
"newUi": {
"sidebar": {
@@ -2991,6 +3120,10 @@
"executionFailed": "ব্যর্থ"
},
"userProfile": {
"donateTitle": "টারমিক্সকে বাঁচিয়ে রাখুন",
"donateDescription": "টারমিক্স বিনামূল্যে, সেলফ-হোস্টেড এবং মাত্র কয়েকজন ব্যক্তি তাদের অবসর সময়ে এটি তৈরি করেছেন। যদি এটি আপনার সময় বা অর্থ বাঁচিয়ে থাকে, তবে একটি ক্রিপ্টো অনুদান এটিকে চালিয়ে যেতে সাহায্য করে।",
"donateMilestones": "SAML, Kubernetes, এবং Agent সাপোর্ট তৈরির জন্য প্রয়োজনীয় গবেষণা ও শেখার কাজে অর্থায়নে অনুদান সাহায্য করে। অগ্রগতি দেখুন এবং অনুদান দিন।",
"donateButton": "ক্রিপ্টো দান করুন",
"storageModeLocal": "ব্রাউজার",
"storageModeCloud": "ডাটাবেস",
"storageModeDescription": "ব্রাউজার শুধুমাত্র এই ব্রাউজারেই সেটিংস সংরক্ষণ করে। ডাটাবেস সার্ভারের সাথে সিঙ্ক হয় এবং যেকোনো ডিভাইসে লোড হয়।",
@@ -3011,6 +3144,10 @@
"twoFaOn": "চালু",
"twoFaOff": "বন্ধ",
"versionLabel": "সংস্করণ",
"betaProgramTitle": "বিটা প্রোগ্রাম",
"betaProgramDescription": "সাপ্তাহিক রোলিং :beta ডকার ট্যাগ ব্যবহার করে আসন্ন ফিচারগুলো আগেভাগেই ব্যবহার করে দেখুন। এটি অস্থিতিশীল, প্রোডাকশনের জন্য নয়।",
"betaProgramFeedback": "কোনো ত্রুটি খুঁজে পেয়েছেন? এখানে জানান।",
"betaProgramLearnMore": "আরও জানুন",
"deleteAccount": "অ্যাকাউন্ট মুছে ফেলুন",
"deleteAccountDescription": "আপনার অ্যাকাউন্ট স্থায়ীভাবে মুছে ফেলুন",
"changeServerDescription": "অন্য একটি টারমিক্স ব্যাকএন্ড সার্ভারে স্যুইচ করুন",
+158 -21
View File
@@ -864,6 +864,21 @@
"proxmoxDockerDetectionDesc": "Patrons de noms separats per comes que permeten a Docker utilitzar convidats coincidents",
"proxmoxPreferredRanges": "Intervals d'IP preferits",
"proxmoxPreferredRangesDesc": "Prefixos separats per comes en ordre de prioritat per a la selecció d'IP quan un convidat té diverses interfícies",
"proxmoxAutoSync": "Sincronitza automàticament els convidats",
"proxmoxAutoSyncDesc": "Descobreix periòdicament aquest node de Proxmox i crea o actualitza els hosts convidats importats mentre la sessió està desbloquejada.",
"proxmoxSyncInterval": "Interval de sincronització (minuts)",
"proxmoxSyncIntervalDesc": "Mínim 5 minuts. El planificador omet les sessions d'usuari bloquejades.",
"proxmoxMarkMissing": "Marca els convidats que falten",
"proxmoxMarkMissingDesc": "Afegeix una etiqueta proxmox-missing quan un convidat importat prèviament desapareix en comptes d'eliminar-lo.",
"proxmoxLastSync": "Darrera sincronització",
"proxmoxLastSyncNever": "Encara no sincronitzat",
"proxmoxLastSyncNoResult": "Sense detalls del resultat",
"proxmoxLastSyncSummary": "{{created}} creat, {{updated}} actualitzat, {{markedMissing}} no hi és, {{skipped}} omès",
"proxmoxLastSyncStatus": {
"success": "Èxit",
"error": "Fallat",
"pending": "Pendent"
},
"proxmoxDiscoverAction": "Descobreix i importa convidats de Proxmox",
"proxmoxImportTitle": "Importar des de Proxmox",
"proxmoxSelectHost": "Seleccioneu un amfitrió de Proxmox…",
@@ -1167,7 +1182,57 @@
"shareHost": "Comparteix l'amfitrió",
"shareHostTitle": "Comparteix: {{name}}",
"sharing": {
"requiresCredential": "Aquest amfitrió ha d'utilitzar una credencial per habilitar la compartició. Editeu l'amfitrió i assigneu-li primer una credencial."
"loadError": "No s'han pogut carregar les dades de compartició. Torna-ho a provar.",
"shareWithSection": "Comparteix amb",
"usersTab": "Usuaris",
"rolesTab": "Rols",
"searchPlaceholder": "Cerca usuaris o rols...",
"noMatches": "No s'han trobat coincidències",
"permissionLevelLabel": "Nivell de permís",
"levels": {
"connect": {
"label": "Connecta",
"description": "Només sessions obertes: terminal, escriptori remot, gestor de fitxers, túnels i Docker. No hi ha accés a la configuració de l'amfitrió."
},
"view": {
"label": "Vista",
"description": "Connecta't i consulta la configuració de l'amfitrió. Els secrets no es mostren mai."
},
"edit": {
"label": "Edita",
"description": "Visualitza i modifica l'amfitrió. Els secrets es poden substituir però mai es poden llegir; les assignacions de credencials només són del propietari."
},
"manage": {
"label": "Gestiona",
"description": "Edita i comparteix l'amfitrió amb altres persones, canvia els nivells de permisos i revoca l'accés."
}
},
"expiryLabel": "Caducitat de l'accés",
"expiry": {
"never": "Mai",
"oneHour": "1 hora",
"oneDay": "24 hores",
"sevenDays": "7 dies",
"thirtyDays": "30 dies",
"custom": "Personalitzat"
},
"customHoursPlaceholder": "Hores fins que caduca l'accés",
"shareButton": "Comparteix",
"shareWithCount": "Comparteix ({{count}})",
"currentAccess": "Accés actual",
"noAccessEntries": "Aquest amfitrió encara no s'ha compartit",
"grantedBy": "Atorgat per",
"expires": "Caduca",
"expired": "Caducat",
"never": "Mai",
"revoke": "Revocar",
"accessUpdated": "Accés actualitzat",
"accessUpdateFailed": "No s'ha pogut actualitzar l'accés",
"sharedBadge": "Compartit",
"sharedBadgeTooltip": "Compartit per {{owner}} ( accés{{level}})",
"viewOnlyBanner": "Aquest amfitrió està compartit amb tu per {{owner}} amb accés de visualització. La configuració és de només lectura.",
"sharedEditBanner": "Aquest amfitrió està compartit amb tu per {{owner}} amb accés d'edició. Els canvis s'apliquen a l'amfitrió real; les referències d'autenticació només les pot canviar el propietari.",
"ownerOnlyControl": "Només el propietari de l'amfitrió pot canviar això"
},
"guac": {
"connection": "Connexió",
@@ -1302,28 +1367,10 @@
"backspaceKey": "Tecla Retrocés",
"saveHostFirst": "Primer desa l'amfitrió.",
"sharingOptionsAfterSave": "Les opcions de compartició estan disponibles després que s'hagi desat l'amfitrió.",
"sharingLoadError": "No s'han pogut carregar les dades de compartició. Comprova la connexió i torna-ho a provar.",
"shareHostSection": "Comparteix l'amfitrió",
"shareWithUser": "Comparteix amb l'usuari",
"shareWithRole": "Comparteix amb el rol",
"selectUser": "Selecciona l'usuari",
"selectRole": "Selecciona un rol",
"selectUserOption": "Selecciona un usuari...",
"selectRoleOption": "Selecciona un rol...",
"permissionLevel": "Nivell de permís",
"expiresInHours": "Caduca en (hores)",
"noExpiryPlaceholder": "Deixeu-ho en blanc si no hi ha caducitat",
"shareBtn": "Comparteix",
"currentAccess": "Accés actual",
"typeHeader": "Tipus",
"targetHeader": "Objectiu",
"permissionHeader": "Permís",
"grantedByHeader": "Atorgat per",
"expiresHeader": "Caduca",
"noAccessEntries": "Encara no hi ha entrades d'accés.",
"expiredLabel": "Caducat",
"neverLabel": "Mai",
"revokeBtn": "Revocar",
"cancelBtn": "Cancel·la",
"savingBtn": "Desant...",
"updateHostBtn": "Actualitza l'amfitrió",
@@ -2190,7 +2237,8 @@
"passwordLoginDisabledDesc": "L'inici de sessió amb contrasenya està desactivat. Feu servir una contrasenya o un proveïdor d'autenticació extern.",
"signInWithPasskey": "Inicia la sessió amb la clau d'accés",
"passkeyLoginFailed": "Error d'inici de sessió amb contrasenya",
"attemptsRemaining": "{{count}} intents restants"
"attemptsRemaining": "{{count}} intents restants",
"confirmResetDataWipe": "Aquest compte no ha iniciat sessió des de l'actualització del xifratge, de manera que les dades emmagatzemades no es poden recuperar sense la contrasenya antiga. Si el restableixes, se suprimiran permanentment els seus amfitrions, credencials i fragments. Vols continuar?"
},
"hostKey": {
"verifyNewHost": "Verifica la clau d'amfitrió SSH",
@@ -2323,6 +2371,13 @@
"serviceLinks": "Enllaços de servei",
"homepagePreview": "Vista prèvia de la pàgina d'inici"
},
"donation": {
"title": "T'agrada Termix?",
"body": "Termix és gratuït i de codi obert, construït i mantingut per un equip de dues persones en el nostre temps lliure. Si substitueix una eina comercial per la qual altrament estaríeu pagant, una donació ajuda a cobrir els costos d'allotjament i manté el desenvolupament en marxa. De moment, les donacions són només de criptomoneda.",
"milestones": "Les donacions també ajuden a finançar el temps per investigar i aprendre què cal per crear funcions com ara la compatibilitat amb SAML i Kubernetes. Vegeu el progrés a la pàgina de donacions.",
"cta": "Donar",
"dismiss": "Potser més tard"
},
"dashboardTab": {
"stable": "ESTABLE",
"hostsOnline": "Amfitrions en línia",
@@ -2790,6 +2845,71 @@
"updateAdminStatusFailed": "No s'ha pogut actualitzar l'estat de l'administrador",
"allSessionsRevoked": "Totes les sessions revocades",
"revokeSessionsFailed": "No s'han pogut revocar les sessions",
"manageUserData": "Gestiona les dades de l'usuari",
"backToUsers": "Torna als usuaris",
"manageTabAccount": "Compte",
"manageTabHosts": "Amfitrions",
"manageTabCredentials": "Credencials",
"manageTabSnippets": "Fragments",
"manageTabSessions": "Sessions",
"manageTabDanger": "Perill",
"manageEditorBack": "Torna a les dades de {{username}}",
"dataLockedBadge": "BLOQUEJAT",
"dataLockedNotice": "Les dades de {{username}}romanen bloquejades fins que inicien la sessió de nou. Els seus amfitrions, credencials i fragments no es poden veure ni editar fins aleshores.",
"resetPasswordTitle": "Restablir contrasenya",
"resetPasswordOidcOnly": "Aquest usuari inicia la sessió a través d'un proveïdor extern i no té contrasenya.",
"resetPasswordPlaceholder": "Nova contrasenya",
"resetPasswordBtn": "Restablir",
"resetPasswordWorking": "Restableix...",
"resetPasswordSuccess": "Restabliment de contrasenya",
"resetPasswordSuccessWiped": "Restabliment de la contrasenya. S'han esborrat les dades xifrades de l'usuari.",
"resetPasswordFailed": "No s'ha pogut restablir la contrasenya",
"resetPasswordConfirmWipe": "{{username}} no ha iniciat sessió des de l'actualització del xifratge, de manera que les seves dades no es poden recuperar. Si restableixes ara, se suprimiran els seus amfitrions, credencials i fragments. Vols continuar?",
"totpSectionTitle": "Autenticació de dos factors",
"totpStatusEnabled": "TOTP està habilitat per a aquest usuari",
"totpStatusDisabled": "El TOTP no està habilitat per a aquest usuari",
"disableTotp": "Desactiva",
"disableTotpConfirm": "Voleu desactivar l'autenticació de dos factors per a {{username}}? Només podran iniciar sessió amb la seva contrasenya.",
"totpDisabledSuccess": "TOTP desactivat",
"totpDisableFailed": "No s'ha pogut desactivar TOTP",
"manageApiKeys": "Claus API",
"apiKeyNamePlaceholder": "Nom de la clau",
"apiKeyCopyNotice": "Copia aquesta clau ara, no es tornarà a mostrar.",
"noApiKeysForUser": "Sense claus API",
"apiKeyDeleteFailed": "No s'ha pogut suprimir la clau de l'API",
"exportUserData": "Exportació de dades",
"exportUserDataDesc": "Baixa els hosts, les credencials i les dades del gestor de fitxers d'aquest usuari com a JSON. Els secrets es desencripten.",
"exportUserDataSuccess": "Dades d'usuari exportades",
"exportUserDataFailed": "No s'han pogut exportar les dades de l'usuari",
"hostsCount": "amfitrions {{count}}",
"addHostForUser": "Afegeix amfitrió",
"noHostsForUser": "Aquest usuari no té amfitrions",
"connectToHost": "Connecta",
"deleteHostConfirm": "Voleu suprimir l'amfitrió \"{{name}}\" que pertany a {{username}}?",
"hostDeletedSuccess": "L'amfitrió s'ha suprimit",
"hostDeleteFailed": "No s'ha pogut suprimir l'amfitrió",
"credentialsCount": "credencials {{count}}",
"addCredentialForUser": "Afegeix credencials",
"noCredentialsForUser": "Aquest usuari no té credencials",
"deleteCredentialConfirm": "Voleu suprimir la credencial \"{{name}}\" que pertany a {{username}}?",
"credentialDeletedSuccess": "Credencial suprimida",
"credentialDeleteFailed": "No s'ha pogut suprimir la credencial",
"snippetsCount": "{{count}} fragments",
"addSnippetForUser": "Afegeix un fragment",
"noSnippetsForUser": "Aquest usuari no té fragments",
"snippetRequiredFields": "El nom i el contingut del fragment són obligatoris",
"snippetNamePlaceholder": "Nom del fragment",
"snippetContentPlaceholder": "Contingut de l'ordre",
"snippetFolderPlaceholder": "Carpeta (opcional)",
"snippetSaved": "Fragment desat",
"snippetSaveFailed": "No s'ha pogut desar el fragment",
"deleteSnippetConfirm": "Voleu suprimir el fragment \"{{name}}\" pertanyent a {{username}}?",
"snippetDeletedSuccess": "Fragment suprimit",
"snippetDeleteFailed": "No s'ha pogut suprimir el fragment",
"noSessionsForUser": "No hi ha sessions actives",
"deleteUserDangerDesc": "Suprimeix permanentment {{username}} i totes les seves dades (amfitrions, credencials, fragments, historial). Aquesta acció no es pot desfer.",
"deleteUserConfirm": "Voleu suprimir permanentment {{username}} i totes les seves dades?",
"deleteUserAdminBlocked": "Treu l'estatus d'administrador abans de suprimir aquest usuari.",
"createRoleRequired": "El nom i el nom de visualització són obligatoris",
"createRoleSuccess": "S'ha creat el rol \"{{name}}\"",
"createRoleFailed": "No s'ha pogut crear el rol",
@@ -2822,7 +2942,16 @@
"hostDefaultsCommandHistory": "Historial de comandaments",
"hostDefaultsCommandHistoryDesc": "Fes un seguiment de l'historial d'ordres en els nous amfitrions per defecte",
"hostDefaultsSaved": "Els valors predeterminats de l'amfitrió s'han desat",
"hostDefaultsSaveFailed": "No s'han pogut desar els valors predeterminats de l'amfitrió"
"hostDefaultsSaveFailed": "No s'han pogut desar els valors predeterminats de l'amfitrió",
"rolePermissions": {
"count": "permisos {{count}}",
"editAction": "Edita els permisos",
"loadError": "No s'ha pogut carregar el catàleg de permisos",
"saved": "Permisos de rol desats",
"saveError": "No s'han pogut desar els permisos de rol",
"save": "Desa",
"saving": "Desant..."
}
},
"newUi": {
"sidebar": {
@@ -2991,6 +3120,10 @@
"executionFailed": "Fallat"
},
"userProfile": {
"donateTitle": "Mantingueu viva la Termix",
"donateDescription": "Termix és gratuït, s'allotja per si mateix i només l'han creat unes poques persones en el seu temps lliure. Si t'ha estalviat temps o diners, una donació de criptomonedes t'ajudarà a mantenir-lo en marxa.",
"donateMilestones": "Els donatius ajuden a finançar el temps per investigar i aprendre què cal per construir suport per a SAML, Kubernetes i Agents. Vegeu el progrés i feu una donació.",
"donateButton": "Dona criptomonedes",
"storageModeLocal": "Navegador",
"storageModeCloud": "Base de dades",
"storageModeDescription": "El navegador només emmagatzema la configuració en aquest navegador. La base de dades es sincronitza amb el servidor i es carrega en qualsevol dispositiu.",
@@ -3011,6 +3144,10 @@
"twoFaOn": "Activat",
"twoFaOff": "Desactivat",
"versionLabel": "Versió",
"betaProgramTitle": "Programa Beta",
"betaProgramDescription": "Prova les properes funcions aviat amb l'etiqueta Docker setmanalment disponible :beta. Inestable, no per a producció.",
"betaProgramFeedback": "Has trobat un error? Informa'n aquí.",
"betaProgramLearnMore": "Més informació",
"deleteAccount": "Suprimeix el compte",
"deleteAccountDescription": "Elimina el teu compte permanentment",
"changeServerDescription": "Canvia a un altre servidor de backend de Termix",
+158 -21
View File
@@ -864,6 +864,21 @@
"proxmoxDockerDetectionDesc": "Vzory názvů oddělených čárkami, které Dockeru umožňují vyhledávání hostů",
"proxmoxPreferredRanges": "Preferované rozsahy IP adres",
"proxmoxPreferredRangesDesc": "Prefixy oddělené čárkami v pořadí priority pro výběr IP adresy, když má host více rozhraní",
"proxmoxAutoSync": "Automatická synchronizace hostů",
"proxmoxAutoSyncDesc": "Pravidelně vyhledávejte tento uzel Proxmox a vytvářejte nebo aktualizujte importované hostující hostitele, zatímco je vaše relace odemčená.",
"proxmoxSyncInterval": "Interval synchronizace (minuty)",
"proxmoxSyncIntervalDesc": "Minimálně 5 minut. Plánovač přeskakuje uzamčené uživatelské relace.",
"proxmoxMarkMissing": "Označte chybějící hosty",
"proxmoxMarkMissingDesc": "Přidat tag proxmox-missing, když dříve importovaný host zmizí, místo aby byl smazán.",
"proxmoxLastSync": "Poslední synchronizace",
"proxmoxLastSyncNever": "Zatím nesynchronizováno",
"proxmoxLastSyncNoResult": "Žádné podrobnosti o výsledcích",
"proxmoxLastSyncSummary": "{{created}} vytvořeno, {{updated}} aktualizováno, {{markedMissing}} chybí, {{skipped}} přeskočeno",
"proxmoxLastSyncStatus": {
"success": "Úspěch",
"error": "Selhalo",
"pending": "Čeká na vyřízení"
},
"proxmoxDiscoverAction": "Objevte a importujte hosty Proxmoxu",
"proxmoxImportTitle": "Import z Proxmoxu",
"proxmoxSelectHost": "Vyberte hostitele Proxmox…",
@@ -1167,7 +1182,57 @@
"shareHost": "Sdílení hostitele",
"shareHostTitle": "Sdílet: {{name}}",
"sharing": {
"requiresCredential": "Tento hostitel musí pro sdílení používat přihlašovací údaje. Nejprve upravte hostitele a přiřaďte mu přihlašovací údaje."
"loadError": "Nepodařilo se načíst data sdílení. Zkuste to prosím znovu.",
"shareWithSection": "Sdílet s",
"usersTab": "Uživatelé",
"rolesTab": "Role",
"searchPlaceholder": "Hledat uživatele nebo role...",
"noMatches": "Nenalezeny žádné shody",
"permissionLevelLabel": "Úroveň oprávnění",
"levels": {
"connect": {
"label": "Připojit",
"description": "Pouze otevřené relace: terminál, vzdálená plocha, správce souborů, tunely a Docker. Bez přístupu ke konfiguraci hostitele."
},
"view": {
"label": "Pohled",
"description": "Připojte se a podívejte se na konfiguraci hostitele. Tajné kódy se nikdy nezobrazují."
},
"edit": {
"label": "Upravit",
"description": "Zobrazení a úprava hostitele. Tajné údaje lze nahradit, ale nikdy nečíst; přiřazení pověření zůstává pouze pro vlastníka."
},
"manage": {
"label": "Spravovat",
"description": "Upravovat a sdílet hostitele s ostatními, měnit úrovně oprávnění a odebírat přístup."
}
},
"expiryLabel": "Vypršení přístupu",
"expiry": {
"never": "Nikdy",
"oneHour": "1 hodina",
"oneDay": "24 hodin",
"sevenDays": "7 dní",
"thirtyDays": "30 dní",
"custom": "Zvyk"
},
"customHoursPlaceholder": "Hodiny do vypršení přístupu",
"shareButton": "Podíl",
"shareWithCount": "Sdílet ({{count}})",
"currentAccess": "Aktuální přístup",
"noAccessEntries": "Tento hostitel ještě nebyl sdílen",
"grantedBy": "Uděleno",
"expires": "Platnost vyprší",
"expired": "Platnost vypršela",
"never": "Nikdy",
"revoke": "Zrušit",
"accessUpdated": "Přístup aktualizován",
"accessUpdateFailed": "Aktualizace přístupu se nezdařilo",
"sharedBadge": "Sdílené",
"sharedBadgeTooltip": "Sdíleno uživatelem {{owner}} ( přístup{{level}})",
"viewOnlyBanner": "Tento hostitel je s vámi sdílen uživatelem {{owner}} s oprávněním pro zobrazení. Konfigurace je pouze pro čtení.",
"sharedEditBanner": "Tento hostitel je s vámi sdílen uživatelem {{owner}} s oprávněním k úpravám. Změny se vztahují na skutečného hostitele; ověřovací reference může změnit pouze vlastník.",
"ownerOnlyControl": "Toto může změnit pouze majitel hostitele"
},
"guac": {
"connection": "Spojení",
@@ -1302,28 +1367,10 @@
"backspaceKey": "Klávesa Backspace",
"saveHostFirst": "Nejprve uložte hostitele.",
"sharingOptionsAfterSave": "Možnosti sdílení jsou k dispozici po uložení hostitele.",
"sharingLoadError": "Nepodařilo se načíst data sdílení. Zkontrolujte připojení a zkuste to znovu.",
"shareHostSection": "Sdílení hostitele",
"shareWithUser": "Sdílet s uživatelem",
"shareWithRole": "Sdílet s rolí",
"selectUser": "Vyberte uživatele",
"selectRole": "Vyberte roli",
"selectUserOption": "Vyberte uživatele...",
"selectRoleOption": "Vyberte roli...",
"permissionLevel": "Úroveň oprávnění",
"expiresInHours": "Platnost vyprší za (hodiny)",
"noExpiryPlaceholder": "Pro neomezenou platnost ponechte prázdné.",
"shareBtn": "Podíl",
"currentAccess": "Aktuální přístup",
"typeHeader": "Typ",
"targetHeader": "Cíl",
"permissionHeader": "Povolení",
"grantedByHeader": "Uděleno kým",
"expiresHeader": "Platnost vyprší",
"noAccessEntries": "Zatím žádné přístupové položky.",
"expiredLabel": "Platnost vypršela",
"neverLabel": "Nikdy",
"revokeBtn": "Zrušit",
"cancelBtn": "Zrušit",
"savingBtn": "Ukládání...",
"updateHostBtn": "Aktualizovat hostitele",
@@ -2190,7 +2237,8 @@
"passwordLoginDisabledDesc": "Přihlášení heslem je zakázáno. Použijte přístupový klíč nebo externího poskytovatele ověřování.",
"signInWithPasskey": "Přihlásit se pomocí hesla",
"passkeyLoginFailed": "Přihlášení k heslu se nezdařilo",
"attemptsRemaining": "Zbývá {{count}} pokusů"
"attemptsRemaining": "Zbývá {{count}} pokusů",
"confirmResetDataWipe": "Tento účet se od aktualizace šifrování nepřihlásil, takže jeho uložená data nelze obnovit bez starého hesla. Resetováním se trvale smažou jeho hostitelé, přihlašovací údaje a úryvky. Pokračovat?"
},
"hostKey": {
"verifyNewHost": "Ověření klíče hostitele SSH",
@@ -2323,6 +2371,13 @@
"serviceLinks": "Odkazy na služby",
"homepagePreview": "Náhled domovské stránky"
},
"donation": {
"title": "Líbí se vám Termix?",
"body": "Termix je bezplatný a open source nástroj, který vytváří a spravuje dvoučlenný tým ve svém volném čase. Pokud nahradí komerční nástroj, za který byste jinak platili, dar pomůže pokrýt náklady na hosting a udrží vývoj v chodu. V současné době jsou dary pouze v kryptoměnách.",
"milestones": "Dary také pomáhají financovat čas na výzkum a zjištění, co je potřeba k vytvoření funkcí, jako je podpora SAML a Kubernetes. Sledujte průběh na stránce pro darování.",
"cta": "Darovat",
"dismiss": "Možná později"
},
"dashboardTab": {
"stable": "STABILNÍ",
"hostsOnline": "Hostitelé online",
@@ -2790,6 +2845,71 @@
"updateAdminStatusFailed": "Nepodařilo se aktualizovat stav administrátora",
"allSessionsRevoked": "Všechny relace zrušeny",
"revokeSessionsFailed": "Nepodařilo se zrušit relace",
"manageUserData": "Správa uživatelských dat",
"backToUsers": "Zpět k uživatelům",
"manageTabAccount": "Účet",
"manageTabHosts": "Hostitelé",
"manageTabCredentials": "Pověření",
"manageTabSnippets": "Úryvky",
"manageTabSessions": "Relace",
"manageTabDanger": "Nebezpečí",
"manageEditorBack": "Zpět k datům uživatele {{username}}",
"dataLockedBadge": "ZAMČENO",
"dataLockedNotice": "Data uživatele {{username}}zůstanou uzamčena až do jeho dalšího přihlášení. Do té doby nelze jeho hostitele, přihlašovací údaje a úryvky zobrazit ani upravovat.",
"resetPasswordTitle": "Obnovit heslo",
"resetPasswordOidcOnly": "Tento uživatel se přihlašuje přes externího poskytovatele a nemá heslo.",
"resetPasswordPlaceholder": "Nové heslo",
"resetPasswordBtn": "Obnovit",
"resetPasswordWorking": "Resetování...",
"resetPasswordSuccess": "Obnovení hesla",
"resetPasswordSuccessWiped": "Reset hesla. Šifrovaná data uživatele byla vymazána.",
"resetPasswordFailed": "Obnovení hesla se nezdařilo",
"resetPasswordConfirmWipe": "{{username}} se od aktualizace šifrování nepřihlásil, takže jeho data nelze obnovit. Resetováním nyní smažete jeho hostitele, přihlašovací údaje a úryvky. Pokračovat?",
"totpSectionTitle": "Dvoufaktorové ověřování",
"totpStatusEnabled": "TOTP je pro tohoto uživatele povolen",
"totpStatusDisabled": "TOTP není pro tohoto uživatele povoleno",
"disableTotp": "Zakázat",
"disableTotpConfirm": "Zakázat dvoufaktorové ověřování pro {{username}}? Budou se moci přihlásit pouze pomocí hesla.",
"totpDisabledSuccess": "TOTP zakázán",
"totpDisableFailed": "Nepodařilo se zakázat TOTP",
"manageApiKeys": "Klíče API",
"apiKeyNamePlaceholder": "Název klíče",
"apiKeyCopyNotice": "Zkopírujte tento klíč nyní, už se vám nezobrazí.",
"noApiKeysForUser": "Žádné API klíče",
"apiKeyDeleteFailed": "Nepodařilo se smazat klíč API",
"exportUserData": "Export dat",
"exportUserDataDesc": "Stáhnout hostitele, přihlašovací údaje a data správce souborů tohoto uživatele ve formátu JSON. Tajné kódy jsou dešifrovány.",
"exportUserDataSuccess": "Exportovaná uživatelská data",
"exportUserDataFailed": "Nepodařilo se exportovat uživatelská data",
"hostsCount": "{{count}} hostitelé",
"addHostForUser": "Přidat hostitele",
"noHostsForUser": "Tento uživatel nemá žádné hostitele",
"connectToHost": "Připojit",
"deleteHostConfirm": "Smazat hostitele „{{name}}“ patřícího do {{username}}?",
"hostDeletedSuccess": "Hostitel smazán",
"hostDeleteFailed": "Nepodařilo se smazat hostitele",
"credentialsCount": "{{count}} pověření",
"addCredentialForUser": "Přidat přihlašovací údaje",
"noCredentialsForUser": "Tento uživatel nemá žádné přihlašovací údaje",
"deleteCredentialConfirm": "Smazat přihlašovací údaje „{{name}}“ patřící uživateli {{username}}?",
"credentialDeletedSuccess": "Přihlašovací údaje smazány",
"credentialDeleteFailed": "Nepodařilo se smazat přihlašovací údaje",
"snippetsCount": "úryvky {{count}}",
"addSnippetForUser": "Přidat úryvek",
"noSnippetsForUser": "Tento uživatel nemá žádné úryvky",
"snippetRequiredFields": "Název a obsah úryvku jsou povinné",
"snippetNamePlaceholder": "Název úryvku",
"snippetContentPlaceholder": "Obsah příkazu",
"snippetFolderPlaceholder": "Složka (volitelné)",
"snippetSaved": "Úryvek uložen",
"snippetSaveFailed": "Uložení úryvku se nezdařilo",
"deleteSnippetConfirm": "Smazat úryvek „{{name}}“ patřící do {{username}}?",
"snippetDeletedSuccess": "Úryvek smazán",
"snippetDeleteFailed": "Nepodařilo se smazat úryvek",
"noSessionsForUser": "Žádné aktivní relace",
"deleteUserDangerDesc": "Trvale smazat {{username}} a všechna jeho data (hostitele, přihlašovací údaje, úryvky, historii). Tuto akci nelze vrátit zpět.",
"deleteUserConfirm": "Trvale smazat {{username}} a všechna jeho data?",
"deleteUserAdminBlocked": "Před smazáním tohoto uživatele odeberte status administrátora.",
"createRoleRequired": "Jméno a zobrazované jméno jsou povinné",
"createRoleSuccess": "Role „{{name}}“ byla vytvořena",
"createRoleFailed": "Vytvoření role se nezdařilo",
@@ -2822,7 +2942,16 @@
"hostDefaultsCommandHistory": "Historie příkazů",
"hostDefaultsCommandHistoryDesc": "Sledování historie příkazů na nových hostitelích ve výchozím nastavení",
"hostDefaultsSaved": "Výchozí nastavení hostitele uloženo",
"hostDefaultsSaveFailed": "Nepodařilo se uložit výchozí nastavení hostitele"
"hostDefaultsSaveFailed": "Nepodařilo se uložit výchozí nastavení hostitele",
"rolePermissions": {
"count": "{{count}} oprávnění",
"editAction": "Upravit oprávnění",
"loadError": "Nepodařilo se načíst katalog oprávnění",
"saved": "Oprávnění role uložena",
"saveError": "Uložení oprávnění role se nezdařilo",
"save": "Uložit",
"saving": "Ukládání..."
}
},
"newUi": {
"sidebar": {
@@ -2991,6 +3120,10 @@
"executionFailed": "Selhalo"
},
"userProfile": {
"donateTitle": "Udrž Termixe naživu",
"donateDescription": "Termix je zdarma, hostovaný na vlastních platformách a vyvíjí ho jen několik lidí ve svém volném čase. Pokud vám to ušetřilo čas nebo peníze, dar kryptoměn vám pomůže udržet ho v chodu.",
"donateMilestones": "Dary pomáhají financovat čas na výzkum a zjištění, co je potřeba k vytvoření podpory SAML, Kubernetes a Agentů. Sledujte pokrok a darujte.",
"donateButton": "Darujte kryptoměny",
"storageModeLocal": "Prohlížeč",
"storageModeCloud": "Databáze",
"storageModeDescription": "Prohlížeč ukládá nastavení pouze v tomto prohlížeči. Databáze se synchronizuje se serverem a načítá se na jakémkoli zařízení.",
@@ -3011,6 +3144,10 @@
"twoFaOn": "Na",
"twoFaOff": "Vypnuto",
"versionLabel": "Verze",
"betaProgramTitle": "Beta program",
"betaProgramDescription": "Vyzkoušejte připravované funkce včas s týdenním aktualizačním tagem :beta v Dockeru. Nestabilní, není určeno pro produkční prostředí.",
"betaProgramFeedback": "Našli jste chybu? Nahlaste ji zde.",
"betaProgramLearnMore": "Zjistěte více",
"deleteAccount": "Smazat účet",
"deleteAccountDescription": "Trvale smazat váš účet",
"changeServerDescription": "Přepněte na jiný backendový server Termix",
+158 -21
View File
@@ -864,6 +864,21 @@
"proxmoxDockerDetectionDesc": "Kommaseparerede navnemønstre, der gør det muligt for Docker at matche gæster",
"proxmoxPreferredRanges": "Foretrukne IP-intervaller",
"proxmoxPreferredRangesDesc": "Kommaseparerede præfikser i prioriteret rækkefølge for IP-valg, når en gæst har flere grænseflader",
"proxmoxAutoSync": "Automatisk synkronisering af gæster",
"proxmoxAutoSyncDesc": "Opdag periodisk denne Proxmox-node og opret eller opdater importerede gæsteværter, mens din session er låst op.",
"proxmoxSyncInterval": "Synkroniseringsinterval (minutter)",
"proxmoxSyncIntervalDesc": "Minimum 5 minutter. Planlæggeren springer låste brugersessioner over.",
"proxmoxMarkMissing": "Markér manglende gæster",
"proxmoxMarkMissingDesc": "Tilføj et proxmox-missing-tag, når en tidligere importeret gæst forsvinder, i stedet for at slette den.",
"proxmoxLastSync": "Sidste synkronisering",
"proxmoxLastSyncNever": "Ikke synkroniseret endnu",
"proxmoxLastSyncNoResult": "Ingen resultatdetaljer",
"proxmoxLastSyncSummary": "{{created}} oprettet, {{updated}} opdateret, {{markedMissing}} mangler, {{skipped}} sprunget over",
"proxmoxLastSyncStatus": {
"success": "Succes",
"error": "Mislykkedes",
"pending": "Indtil"
},
"proxmoxDiscoverAction": "Opdag og importer Proxmox-gæster",
"proxmoxImportTitle": "Importer fra Proxmox",
"proxmoxSelectHost": "Vælg en Proxmox-vært…",
@@ -1167,7 +1182,57 @@
"shareHost": "Del vært",
"shareHostTitle": "Del: {{name}}",
"sharing": {
"requiresCredential": "Denne vært skal bruge legitimationsoplysninger for at aktivere deling. Rediger værten, og tildel først legitimationsoplysninger."
"loadError": "Delingsdataene kunne ikke indlæses. Prøv igen.",
"shareWithSection": "Del med",
"usersTab": "Brugere",
"rolesTab": "Roller",
"searchPlaceholder": "Søg efter brugere eller roller...",
"noMatches": "Ingen matches fundet",
"permissionLevelLabel": "Tilladelsesniveau",
"levels": {
"connect": {
"label": "Forbinde",
"description": "Kun åbne sessioner: terminal, fjernskrivebord, filhåndtering, tunneler og Docker. Ingen adgang til værtkonfigurationen."
},
"view": {
"label": "Udsigt",
"description": "Opret forbindelse, plus se værtkonfigurationen. Hemmeligheder vises aldrig."
},
"edit": {
"label": "Redigere",
"description": "Vis og rediger værten. Hemmeligheder kan erstattes, men aldrig læses; tildeling af legitimationsoplysninger forbliver kun for ejeren."
},
"manage": {
"label": "Styre",
"description": "Rediger, plus del værten med andre, skift tilladelsesniveauer og tilbagekald adgang."
}
},
"expiryLabel": "Udløb af adgang",
"expiry": {
"never": "Aldrig",
"oneHour": "1 time",
"oneDay": "24 timer",
"sevenDays": "7 dage",
"thirtyDays": "30 dage",
"custom": "Skik"
},
"customHoursPlaceholder": "Timer indtil adgangen udløber",
"shareButton": "Dele",
"shareWithCount": "Del ({{count}})",
"currentAccess": "Nuværende adgang",
"noAccessEntries": "Denne vært er ikke blevet delt endnu",
"grantedBy": "Tildelt af",
"expires": "Udløber",
"expired": "Udløbet",
"never": "Aldrig",
"revoke": "Tilbagekald",
"accessUpdated": "Adgang opdateret",
"accessUpdateFailed": "Adgang kunne ikke opdateres",
"sharedBadge": "Delt",
"sharedBadgeTooltip": "Delt af {{owner}} ({{level}} adgang)",
"viewOnlyBanner": "Denne vært deles med dig af {{owner}} med læseadgang. Konfigurationen er skrivebeskyttet.",
"sharedEditBanner": "Denne vært deles med dig af {{owner}} med redigeringsadgang. Ændringer gælder for den rigtige vært; godkendelsesreferencer kan kun ændres af ejeren.",
"ownerOnlyControl": "Kun værtsejeren kan ændre dette"
},
"guac": {
"connection": "Forbindelse",
@@ -1302,28 +1367,10 @@
"backspaceKey": "Tilbagetast",
"saveHostFirst": "Gem værten først.",
"sharingOptionsAfterSave": "Delingsmuligheder er tilgængelige, når værten er blevet gemt.",
"sharingLoadError": "Kunne ikke indlæse delingsdata. Tjek din forbindelse, og prøv igen.",
"shareHostSection": "Del vært",
"shareWithUser": "Del med bruger",
"shareWithRole": "Del med rolle",
"selectUser": "Vælg bruger",
"selectRole": "Vælg rolle",
"selectUserOption": "Vælg en bruger...",
"selectRoleOption": "Vælg en rolle...",
"permissionLevel": "Tilladelsesniveau",
"expiresInHours": "Udløber om (timer)",
"noExpiryPlaceholder": "Lad være tomt for ingen udløbsdato",
"shareBtn": "Dele",
"currentAccess": "Nuværende adgang",
"typeHeader": "Type",
"targetHeader": "Mål",
"permissionHeader": "Tilladelse",
"grantedByHeader": "Tildelt af",
"expiresHeader": "Udløber",
"noAccessEntries": "Ingen adgangsposter endnu.",
"expiredLabel": "Udløbet",
"neverLabel": "Aldrig",
"revokeBtn": "Tilbagekald",
"cancelBtn": "Ophæve",
"savingBtn": "Gemmer...",
"updateHostBtn": "Opdater vært",
@@ -2190,7 +2237,8 @@
"passwordLoginDisabledDesc": "Adgangskodelogin er deaktiveret. Brug en adgangsnøgle eller en ekstern godkendelsesudbyder.",
"signInWithPasskey": "Log ind med adgangsnøgle",
"passkeyLoginFailed": "Login med adgangsnøgle mislykkedes",
"attemptsRemaining": "{{count}} forsøg tilbage"
"attemptsRemaining": "{{count}} forsøg tilbage",
"confirmResetDataWipe": "Denne konto er ikke logget ind siden krypteringsopgraderingen, så dens gemte data kan ikke gendannes uden den gamle adgangskode. Nulstilling vil permanent slette dens værter, legitimationsoplysninger og kodestykker. Vil du fortsætte?"
},
"hostKey": {
"verifyNewHost": "Bekræft SSH-værtsnøgle",
@@ -2323,6 +2371,13 @@
"serviceLinks": "Servicelinks",
"homepagePreview": "Forhåndsvisning af hjemmeside"
},
"donation": {
"title": "Kan du lide Termix?",
"body": "Termix er gratis og open source, bygget og vedligeholdt af et team på to personer i vores fritid. Hvis det erstatter et kommercielt værktøj, du ellers skulle betale for, hjælper en donation med at dække hostingomkostninger og holde udviklingen i gang. I øjeblikket er donationer kun krypto.",
"milestones": "Donationer hjælper også med at finansiere den tid, der skal bruges til at undersøge og lære, hvad der er nødvendigt for at udvikle funktioner som SAML og Kubernetes-support. Se status på donationssiden.",
"cta": "Donér",
"dismiss": "Måske senere"
},
"dashboardTab": {
"stable": "STABIL",
"hostsOnline": "Værter online",
@@ -2790,6 +2845,71 @@
"updateAdminStatusFailed": "Kunne ikke opdatere administratorstatus",
"allSessionsRevoked": "Alle sessioner tilbagekaldt",
"revokeSessionsFailed": "Kunne ikke tilbagekalde sessioner",
"manageUserData": "Administrer brugerdata",
"backToUsers": "Tilbage til brugerne",
"manageTabAccount": "Konto",
"manageTabHosts": "Værter",
"manageTabCredentials": "Akkreditiver",
"manageTabSnippets": "Uddrag",
"manageTabSessions": "Sessioner",
"manageTabDanger": "Fare",
"manageEditorBack": "Tilbage til {{username}}s data",
"dataLockedBadge": "LÅST",
"dataLockedNotice": "{{username}}s data forbliver låst, indtil de logger ind næste gang. Deres værter, legitimationsoplysninger og kodestykker kan ikke ses eller redigeres indtil da.",
"resetPasswordTitle": "Nulstil adgangskode",
"resetPasswordOidcOnly": "Denne bruger logger ind via en ekstern udbyder og har ingen adgangskode.",
"resetPasswordPlaceholder": "Ny adgangskode",
"resetPasswordBtn": "Nulstil",
"resetPasswordWorking": "Nulstiller...",
"resetPasswordSuccess": "Nulstilling af adgangskode",
"resetPasswordSuccessWiped": "Nulstilling af adgangskode. Brugerens krypterede data blev slettet.",
"resetPasswordFailed": "Kunne ikke nulstille adgangskoden",
"resetPasswordConfirmWipe": "{{username}} har ikke logget ind siden krypteringsopgraderingen, så deres data kan ikke gendannes. Nulstilling nu vil slette deres værter, legitimationsoplysninger og kodestykker. Vil du fortsætte?",
"totpSectionTitle": "Tofaktorgodkendelse",
"totpStatusEnabled": "TOTP er aktiveret for denne bruger",
"totpStatusDisabled": "TOTP er ikke aktiveret for denne bruger",
"disableTotp": "Deaktiver",
"disableTotpConfirm": "Deaktiver tofaktorgodkendelse for {{username}}? De vil kun kunne logge ind med deres adgangskode.",
"totpDisabledSuccess": "TOTP deaktiveret",
"totpDisableFailed": "Kunne ikke deaktivere TOTP",
"manageApiKeys": "API-nøgler",
"apiKeyNamePlaceholder": "Nøglenavn",
"apiKeyCopyNotice": "Kopier denne nøgle nu, den vil ikke blive vist igen.",
"noApiKeysForUser": "Ingen API-nøgler",
"apiKeyDeleteFailed": "Kunne ikke slette API-nøglen",
"exportUserData": "Dataeksport",
"exportUserDataDesc": "Download denne brugers værter, legitimationsoplysninger og filhåndteringsdata som JSON. Hemmelighederne dekrypteres.",
"exportUserDataSuccess": "Brugerdata eksporteret",
"exportUserDataFailed": "Kunne ikke eksportere brugerdata",
"hostsCount": "{{count}} værter",
"addHostForUser": "Tilføj vært",
"noHostsForUser": "Denne bruger har ingen værter",
"connectToHost": "Forbinde",
"deleteHostConfirm": "Slet værten \"{{name}}\" der tilhører {{username}}?",
"hostDeletedSuccess": "Vært slettet",
"hostDeleteFailed": "Kunne ikke slette værten",
"credentialsCount": "{{count}} legitimationsoplysninger",
"addCredentialForUser": "Tilføj legitimationsoplysninger",
"noCredentialsForUser": "Denne bruger har ingen loginoplysninger",
"deleteCredentialConfirm": "Slet legitimationsoplysningerne \"{{name}}\", der tilhører {{username}}?",
"credentialDeletedSuccess": "Legitimationsoplysninger slettet",
"credentialDeleteFailed": "Kunne ikke slette legitimationsoplysninger",
"snippetsCount": "{{count}} uddrag",
"addSnippetForUser": "Tilføj uddrag",
"noSnippetsForUser": "Denne bruger har ingen uddrag",
"snippetRequiredFields": "Navn og indhold på uddrag er påkrævet",
"snippetNamePlaceholder": "Snippetnavn",
"snippetContentPlaceholder": "Kommandoindhold",
"snippetFolderPlaceholder": "Mappe (valgfrit)",
"snippetSaved": "Uddrag gemt",
"snippetSaveFailed": "Kunne ikke gemme uddraget",
"deleteSnippetConfirm": "Slet uddraget \"{{name}}\" der tilhører {{username}}?",
"snippetDeletedSuccess": "Uddrag slettet",
"snippetDeleteFailed": "Kunne ikke slette uddraget",
"noSessionsForUser": "Ingen aktive sessioner",
"deleteUserDangerDesc": "Slet permanent {{username}} og alle deres data (hosts, legitimationsoplysninger, kodesnippets, historik). Dette kan ikke fortrydes.",
"deleteUserConfirm": "Vil du slette {{username}} og alle deres data permanent?",
"deleteUserAdminBlocked": "Fjern administratorstatus, før du sletter denne bruger.",
"createRoleRequired": "Navn og visningsnavn er påkrævet",
"createRoleSuccess": "Rollen \"{{name}}\" blev oprettet",
"createRoleFailed": "Kunne ikke oprette rollen",
@@ -2822,7 +2942,16 @@
"hostDefaultsCommandHistory": "Kommandohistorik",
"hostDefaultsCommandHistoryDesc": "Spor kommandohistorik på nye værter som standard",
"hostDefaultsSaved": "Værtsstandarder gemt",
"hostDefaultsSaveFailed": "Kunne ikke gemme værtens standardindstillinger"
"hostDefaultsSaveFailed": "Kunne ikke gemme værtens standardindstillinger",
"rolePermissions": {
"count": "{{count}} tilladelser",
"editAction": "Rediger tilladelser",
"loadError": "Kunne ikke indlæse tilladelseskataloget",
"saved": "Rolletilladelser gemt",
"saveError": "Rolletilladelser kunne ikke gemmes",
"save": "Spare",
"saving": "Gemmer..."
}
},
"newUi": {
"sidebar": {
@@ -2991,6 +3120,10 @@
"executionFailed": "Mislykkedes"
},
"userProfile": {
"donateTitle": "Hold Termix i live",
"donateDescription": "Termix er gratis, selvhostet og bygget af kun få personer i deres fritid. Hvis det har sparet dig tid eller penge, kan en kryptodonation hjælpe med at holde det kørende.",
"donateMilestones": "Donationer hjælper med at finansiere tiden til at undersøge og lære, hvad der er nødvendigt for at opbygge SAML-, Kubernetes- og agentsupport. Se fremskridtene, og donér.",
"donateButton": "Donér krypto",
"storageModeLocal": "Browser",
"storageModeCloud": "Database",
"storageModeDescription": "Browseren gemmer kun indstillinger i denne browser. Databasen synkroniseres med serveren og indlæses på enhver enhed.",
@@ -3011,6 +3144,10 @@
"twoFaOn": "På",
"twoFaOff": "Slukket",
"versionLabel": "Version",
"betaProgramTitle": "Betaprogram",
"betaProgramDescription": "Prøv kommende funktioner tidligt med det ugentlige rullende :beta Docker-tag. Ustabil, ikke til produktion.",
"betaProgramFeedback": "Har du fundet en fejl? Rapportér den her.",
"betaProgramLearnMore": "Lær mere",
"deleteAccount": "Slet konto",
"deleteAccountDescription": "Slet din konto permanent",
"changeServerDescription": "Skift til en anden Termix backend-server",
File diff suppressed because it is too large Load Diff
+158 -21
View File
@@ -864,6 +864,21 @@
"proxmoxDockerDetectionDesc": "Μοτίβα ονομάτων διαχωρισμένα με κόμματα που επιτρέπουν στο Docker να αντιστοιχίζει επισκέπτες",
"proxmoxPreferredRanges": "Προτιμώμενα εύρη IP",
"proxmoxPreferredRangesDesc": "Προθέματα διαχωρισμένα με κόμμα σε σειρά προτεραιότητας για την επιλογή IP όταν ένας επισκέπτης έχει πολλαπλές διεπαφές",
"proxmoxAutoSync": "Αυτόματος συγχρονισμός επισκεπτών",
"proxmoxAutoSyncDesc": "Εντοπίζετε περιοδικά αυτόν τον κόμβο Proxmox και δημιουργείτε ή ενημερώνετε εισαγόμενους φιλοξενούμενους υπολογιστές όσο η συνεδρία σας είναι ξεκλείδωτη.",
"proxmoxSyncInterval": "Διάστημα συγχρονισμού (λεπτά)",
"proxmoxSyncIntervalDesc": "Ελάχιστο 5 λεπτά. Ο προγραμματιστής παραλείπει τις κλειδωμένες συνεδρίες χρήστη.",
"proxmoxMarkMissing": "Ο Μαρκ λείπουν επισκέπτες.",
"proxmoxMarkMissingDesc": "Προσθέστε μια ετικέτα proxmox-missing όταν εξαφανιστεί ένας προηγουμένως εισαγόμενος επισκέπτης αντί να τον διαγράψετε.",
"proxmoxLastSync": "Τελευταίος συγχρονισμός",
"proxmoxLastSyncNever": "Δεν έχει συγχρονιστεί ακόμα",
"proxmoxLastSyncNoResult": "Δεν υπάρχουν λεπτομέρειες για το αποτέλεσμα",
"proxmoxLastSyncSummary": "{{created}} δημιουργήθηκε, {{updated}} ενημερώθηκε, {{markedMissing}} λείπει, {{skipped}} παραλείφθηκε",
"proxmoxLastSyncStatus": {
"success": "Επιτυχία",
"error": "Αποτυχημένος",
"pending": "Εκκρεμής"
},
"proxmoxDiscoverAction": "Ανακάλυψη και εισαγωγή επισκεπτών Proxmox",
"proxmoxImportTitle": "Εισαγωγή από το Proxmox",
"proxmoxSelectHost": "Επιλέξτε έναν κεντρικό υπολογιστή Proxmox…",
@@ -1167,7 +1182,57 @@
"shareHost": "Κοινή χρήση κεντρικού υπολογιστή",
"shareHostTitle": "Κοινή χρήση: {{name}}",
"sharing": {
"requiresCredential": "Αυτός ο κεντρικός υπολογιστής πρέπει να χρησιμοποιήσει ένα διαπιστευτήριο για να ενεργοποιήσει την κοινή χρήση. Επεξεργαστείτε τον κεντρικό υπολογιστή και εκχωρήστε πρώτα ένα διαπιστευτήριο."
"loadError": "Αποτυχία φόρτωσης δεδομένων κοινής χρήσης. Δοκιμάστε ξανά.",
"shareWithSection": "Κοινοποίηση με",
"usersTab": "Χρήστες",
"rolesTab": "Ρόλοι",
"searchPlaceholder": "Αναζήτηση χρηστών ή ρόλων...",
"noMatches": "Δεν βρέθηκαν αντιστοιχίσεις",
"permissionLevelLabel": "Επίπεδο δικαιωμάτων",
"levels": {
"connect": {
"label": "Συνδέω",
"description": "Μόνο ανοιχτές συνεδρίες: τερματικό, απομακρυσμένη επιφάνεια εργασίας, διαχειριστής αρχείων, σήραγγες και Docker. Δεν υπάρχει πρόσβαση στη διαμόρφωση του κεντρικού υπολογιστή."
},
"view": {
"label": "Θέα",
"description": "Συνδεθείτε και δείτε τη διαμόρφωση του κεντρικού υπολογιστή. Τα μυστικά δεν εμφανίζονται ποτέ."
},
"edit": {
"label": "Εκδίδω",
"description": "Προβολή και τροποποίηση του κεντρικού υπολογιστή. Τα μυστικά μπορούν να αντικατασταθούν αλλά ποτέ να διαβαστούν. Οι εκχωρήσεις διαπιστευτηρίων παραμένουν μόνο για τον κάτοχο."
},
"manage": {
"label": "Διαχειρίζομαι",
"description": "Επεξεργαστείτε και μοιραστείτε τον κεντρικό υπολογιστή με άλλους, αλλάξτε τα επίπεδα δικαιωμάτων και ανακαλέστε την πρόσβαση."
}
},
"expiryLabel": "Λήξη πρόσβασης",
"expiry": {
"never": "Ποτέ",
"oneHour": "1 ώρα",
"oneDay": "24 ώρες",
"sevenDays": "7 ημέρες",
"thirtyDays": "30 ημέρες",
"custom": "Εθιμο"
},
"customHoursPlaceholder": "Ώρες μέχρι τη λήξη της πρόσβασης",
"shareButton": "Μερίδιο",
"shareWithCount": "Κοινή χρήση ({{count}})",
"currentAccess": "Τρέχουσα πρόσβαση",
"noAccessEntries": "Αυτός ο κεντρικός υπολογιστής δεν έχει κοινοποιηθεί ακόμη",
"grantedBy": "Χορηγήθηκε από",
"expires": "Λήγει",
"expired": "Λήξη",
"never": "Ποτέ",
"revoke": "Ανακαλώ",
"accessUpdated": "Η πρόσβαση ενημερώθηκε",
"accessUpdateFailed": "Η ενημέρωση της πρόσβασης απέτυχε",
"sharedBadge": "Κοινόχρηστο",
"sharedBadgeTooltip": "Κοινή χρήση από {{owner}} ( πρόσβαση{{level}})",
"viewOnlyBanner": "Αυτός ο κεντρικός υπολογιστής είναι κοινόχρηστος μαζί σας από τον χρήστη {{owner}} με πρόσβαση προβολής. Η διαμόρφωση είναι μόνο για ανάγνωση.",
"sharedEditBanner": "Αυτός ο κεντρικός υπολογιστής είναι κοινόχρηστος μαζί σας από τον χρήστη {{owner}} με πρόσβαση επεξεργασίας. Οι αλλαγές ισχύουν για τον πραγματικό κεντρικό υπολογιστή. Οι αναφορές ελέγχου ταυτότητας μπορούν να αλλάξουν μόνο από τον κάτοχο.",
"ownerOnlyControl": "Μόνο ο κάτοχος του κεντρικού υπολογιστή μπορεί να το αλλάξει αυτό"
},
"guac": {
"connection": "Σύνδεση",
@@ -1302,28 +1367,10 @@
"backspaceKey": "Πλήκτρο Backspace",
"saveHostFirst": "Αποθηκεύστε πρώτα τον οικοδεσπότη.",
"sharingOptionsAfterSave": "Οι επιλογές κοινής χρήσης είναι διαθέσιμες μετά την αποθήκευση του κεντρικού υπολογιστή.",
"sharingLoadError": "Αποτυχία φόρτωσης δεδομένων κοινής χρήσης. Ελέγξτε τη σύνδεσή σας και δοκιμάστε ξανά.",
"shareHostSection": "Κοινή χρήση κεντρικού υπολογιστή",
"shareWithUser": "Κοινή χρήση με χρήστη",
"shareWithRole": "Κοινή χρήση με ρόλο",
"selectUser": "Επιλογή χρήστη",
"selectRole": "Επιλογή Ρόλου",
"selectUserOption": "Επιλέξτε έναν χρήστη...",
"selectRoleOption": "Επιλέξτε έναν ρόλο...",
"permissionLevel": "Επίπεδο δικαιωμάτων",
"expiresInHours": "Λήγει σε (ώρες)",
"noExpiryPlaceholder": "Αφήστε κενό για να μην λήξει",
"shareBtn": "Μερίδιο",
"currentAccess": "Τρέχουσα πρόσβαση",
"typeHeader": "Τύπος",
"targetHeader": "Στόχος",
"permissionHeader": "Αδεια",
"grantedByHeader": "Χορηγήθηκε από",
"expiresHeader": "Λήγει",
"noAccessEntries": "Δεν υπάρχουν ακόμη καταχωρίσεις πρόσβασης.",
"expiredLabel": "Λήξη",
"neverLabel": "Ποτέ",
"revokeBtn": "Ανακαλώ",
"cancelBtn": "Ματαίωση",
"savingBtn": "Οικονομία...",
"updateHostBtn": "Ενημέρωση κεντρικού υπολογιστή",
@@ -2190,7 +2237,8 @@
"passwordLoginDisabledDesc": "Η σύνδεση με κωδικό πρόσβασης είναι απενεργοποιημένη. Χρησιμοποιήστε κλειδί πρόσβασης ή εξωτερικό πάροχο ελέγχου ταυτότητας.",
"signInWithPasskey": "Σύνδεση με κλειδί πρόσβασης",
"passkeyLoginFailed": "Η σύνδεση με κωδικό πρόσβασης απέτυχε",
"attemptsRemaining": "{{count}} προσπάθειες απομένουν"
"attemptsRemaining": "{{count}} προσπάθειες απομένουν",
"confirmResetDataWipe": "Αυτός ο λογαριασμός δεν έχει συνδεθεί από την αναβάθμιση κρυπτογράφησης, επομένως τα αποθηκευμένα δεδομένα του δεν μπορούν να ανακτηθούν χωρίς τον παλιό κωδικό πρόσβασης. Η επαναφορά θα διαγράψει οριστικά τους κεντρικούς υπολογιστές, τα διαπιστευτήρια και τα αποσπάσματα κώδικα. Θέλετε να συνεχίσετε;"
},
"hostKey": {
"verifyNewHost": "Επαλήθευση κλειδιού κεντρικού υπολογιστή SSH",
@@ -2323,6 +2371,13 @@
"serviceLinks": "Σύνδεσμοι υπηρεσιών",
"homepagePreview": "Προεπισκόπηση αρχικής σελίδας"
},
"donation": {
"title": "Σας αρέσει το Termix;",
"body": "Το Termix είναι δωρεάν και ανοιχτού κώδικα, κατασκευασμένο και συντηρείται από μια ομάδα δύο ατόμων στον ελεύθερο χρόνο μας. Εάν αντικαταστήσει ένα εμπορικό εργαλείο για το οποίο διαφορετικά θα πληρώνατε, μια δωρεά βοηθά στην κάλυψη του κόστους φιλοξενίας και διατηρεί την ανάπτυξη σε εξέλιξη. Προς το παρόν, οι δωρεές είναι μόνο για κρυπτονομίσματα.",
"milestones": "Οι δωρεές βοηθούν επίσης στη χρηματοδότηση του χρόνου για έρευνα και εκμάθηση των απαραίτητων λειτουργιών για τη δημιουργία λειτουργιών όπως η υποστήριξη SAML και Kubernetes. Δείτε την πρόοδο στη σελίδα δωρεών.",
"cta": "Προσφέρω",
"dismiss": "Ίσως αργότερα"
},
"dashboardTab": {
"stable": "ΣΤΑΘΕΡΟΣ",
"hostsOnline": "Οικοδεσπότες στο Διαδίκτυο",
@@ -2790,6 +2845,71 @@
"updateAdminStatusFailed": "Η ενημέρωση της κατάστασης διαχειριστή απέτυχε",
"allSessionsRevoked": "Όλες οι συνεδρίες ανακλήθηκαν",
"revokeSessionsFailed": "Η ανάκληση των συνεδριών απέτυχε",
"manageUserData": "Διαχείριση δεδομένων χρήστη",
"backToUsers": "Πίσω στους χρήστες",
"manageTabAccount": "Λογαριασμός",
"manageTabHosts": "Οικοδεσπότες",
"manageTabCredentials": "Διαπιστευτήρια",
"manageTabSnippets": "Αποσπάσματα",
"manageTabSessions": "Συνεδρίες",
"manageTabDanger": "Κίνδυνος",
"manageEditorBack": "Πίσω στα δεδομένα του {{username}}",
"dataLockedBadge": "ΚΛΕΙΔΩΜΕΝΟ",
"dataLockedNotice": "Τα δεδομένα του χρήστη {{username}}παραμένουν κλειδωμένα μέχρι την επόμενη σύνδεσή τους. Οι κεντρικοί υπολογιστές, τα διαπιστευτήρια και τα αποσπάσματα δεδομένων τους δεν μπορούν να προβληθούν ή να επεξεργαστούν μέχρι τότε.",
"resetPasswordTitle": "Επαναφορά κωδικού πρόσβασης",
"resetPasswordOidcOnly": "Αυτός ο χρήστης συνδέεται μέσω εξωτερικού παρόχου και δεν έχει κωδικό πρόσβασης.",
"resetPasswordPlaceholder": "Νέος κωδικός πρόσβασης",
"resetPasswordBtn": "Επαναφορά",
"resetPasswordWorking": "Επαναφορά...",
"resetPasswordSuccess": "Επαναφορά κωδικού πρόσβασης",
"resetPasswordSuccessWiped": "Επαναφορά κωδικού πρόσβασης. Τα κρυπτογραφημένα δεδομένα του χρήστη διαγράφηκαν.",
"resetPasswordFailed": "Η επαναφορά του κωδικού πρόσβασης απέτυχε",
"resetPasswordConfirmWipe": "Ο χρήστης {{username}} δεν έχει συνδεθεί από την αναβάθμιση κρυπτογράφησης, επομένως τα δεδομένα του δεν μπορούν να ανακτηθούν. Η επαναφορά τώρα θα διαγράψει τους κεντρικούς υπολογιστές, τα διαπιστευτήρια και τα αποσπάσματα κώδικα. Θέλετε να συνεχίσετε;",
"totpSectionTitle": "Έλεγχος ταυτότητας δύο παραγόντων",
"totpStatusEnabled": "Το TOTP είναι ενεργοποιημένο για αυτόν τον χρήστη",
"totpStatusDisabled": "Το TOTP δεν είναι ενεργοποιημένο για αυτόν τον χρήστη",
"disableTotp": "Καθιστώ ανίκανο",
"disableTotpConfirm": "Απενεργοποίηση ελέγχου ταυτότητας δύο παραγόντων για {{username}}; Θα μπορούν να συνδεθούν μόνο με τον κωδικό πρόσβασής τους.",
"totpDisabledSuccess": "Το TOTP απενεργοποιήθηκε",
"totpDisableFailed": "Αποτυχία απενεργοποίησης του TOTP",
"manageApiKeys": "Κλειδιά API",
"apiKeyNamePlaceholder": "Όνομα κλειδιού",
"apiKeyCopyNotice": "Αντιγράψτε αυτό το κλειδί τώρα, δεν θα εμφανιστεί ξανά.",
"noApiKeysForUser": "Δεν υπάρχουν κλειδιά API",
"apiKeyDeleteFailed": "Η διαγραφή του κλειδιού API απέτυχε",
"exportUserData": "Εξαγωγή δεδομένων",
"exportUserDataDesc": "Λήψη των κεντρικών υπολογιστών, των διαπιστευτηρίων και των δεδομένων διαχειριστή αρχείων αυτού του χρήστη ως JSON. Τα μυστικά αποκρυπτογραφούνται.",
"exportUserDataSuccess": "Εξαγωγή δεδομένων χρήστη",
"exportUserDataFailed": "Η εξαγωγή δεδομένων χρήστη απέτυχε",
"hostsCount": "{{count}} κεντρικοί υπολογιστές",
"addHostForUser": "Προσθήκη κεντρικού υπολογιστή",
"noHostsForUser": "Αυτός ο χρήστης δεν έχει κεντρικούς υπολογιστές",
"connectToHost": "Συνδέω",
"deleteHostConfirm": "Διαγραφή κεντρικού υπολογιστή \"{{name}}\" που ανήκει στον {{username}};",
"hostDeletedSuccess": "Ο κεντρικός υπολογιστής διαγράφηκε.",
"hostDeleteFailed": "Η διαγραφή του κεντρικού υπολογιστή απέτυχε",
"credentialsCount": "{{count}} διαπιστευτήρια",
"addCredentialForUser": "Προσθήκη διαπιστευτηρίων",
"noCredentialsForUser": "Αυτός ο χρήστης δεν έχει διαπιστευτήρια",
"deleteCredentialConfirm": "Διαγραφή διαπιστευτηρίου \"{{name}}\" που ανήκει στο {{username}};",
"credentialDeletedSuccess": "Τα διαπιστευτήρια διαγράφηκαν",
"credentialDeleteFailed": "Η διαγραφή των διαπιστευτηρίων απέτυχε",
"snippetsCount": "{{count}} αποσπάσματα",
"addSnippetForUser": "Προσθήκη αποσπάσματος",
"noSnippetsForUser": "Αυτός ο χρήστης δεν έχει αποσπάσματα",
"snippetRequiredFields": "Απαιτούνται το όνομα και το περιεχόμενο του αποσπάσματος",
"snippetNamePlaceholder": "Όνομα αποσπάσματος",
"snippetContentPlaceholder": "Περιεχόμενο εντολών",
"snippetFolderPlaceholder": "Φάκελος (προαιρετικό)",
"snippetSaved": "Το απόσπασμα αποθηκεύτηκε",
"snippetSaveFailed": "Η αποθήκευση του αποσπάσματος απέτυχε",
"deleteSnippetConfirm": "Διαγραφή αποσπάσματος \"{{name}}\" που ανήκει στο {{username}};",
"snippetDeletedSuccess": "Το απόσπασμα διαγράφηκε",
"snippetDeleteFailed": "Η διαγραφή του αποσπάσματος απέτυχε",
"noSessionsForUser": "Δεν υπάρχουν ενεργές συνεδρίες",
"deleteUserDangerDesc": "Διαγράψτε οριστικά το {{username}} και όλα τα δεδομένα του (κεντρικούς υπολογιστές, διαπιστευτήρια, τμήματα κώδικα, ιστορικό). Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.",
"deleteUserConfirm": "Να διαγραφεί οριστικά ο χρήστης {{username}} και όλα τα δεδομένα του;",
"deleteUserAdminBlocked": "Καταργήστε την κατάσταση διαχειριστή πριν διαγράψετε αυτόν τον χρήστη.",
"createRoleRequired": "Απαιτούνται το όνομα και το εμφανιζόμενο όνομα",
"createRoleSuccess": "Ο ρόλος \"{{name}}\" δημιουργήθηκε",
"createRoleFailed": "Η δημιουργία ρόλου απέτυχε",
@@ -2822,7 +2942,16 @@
"hostDefaultsCommandHistory": "Ιστορικό εντολών",
"hostDefaultsCommandHistoryDesc": "Παρακολούθηση ιστορικού εντολών σε νέους κεντρικούς υπολογιστές από προεπιλογή",
"hostDefaultsSaved": "Οι προεπιλογές κεντρικού υπολογιστή αποθηκεύτηκαν",
"hostDefaultsSaveFailed": "Η αποθήκευση των προεπιλογών του κεντρικού υπολογιστή απέτυχε"
"hostDefaultsSaveFailed": "Η αποθήκευση των προεπιλογών του κεντρικού υπολογιστή απέτυχε",
"rolePermissions": {
"count": "{{count}} δικαιώματα",
"editAction": "Επεξεργασία δικαιωμάτων",
"loadError": "Αποτυχία φόρτωσης του καταλόγου δικαιωμάτων",
"saved": "Τα δικαιώματα ρόλου αποθηκεύτηκαν",
"saveError": "Αποτυχία αποθήκευσης δικαιωμάτων ρόλου",
"save": "Εκτός",
"saving": "Οικονομία..."
}
},
"newUi": {
"sidebar": {
@@ -2991,6 +3120,10 @@
"executionFailed": "Αποτυχημένος"
},
"userProfile": {
"donateTitle": "Κρατήστε το Termix ζωντανό",
"donateDescription": "Το Termix είναι δωρεάν, αυτο-φιλοξενείται και δημιουργείται από λίγα μόνο άτομα στον ελεύθερο χρόνο τους. Αν σας έχει εξοικονομήσει χρόνο ή χρήματα, μια δωρεά κρυπτονομισμάτων βοηθάει στη συνέχιση της λειτουργίας του.",
"donateMilestones": "Οι δωρεές βοηθούν στη χρηματοδότηση του χρόνου για έρευνα και εκμάθηση των απαραίτητων για την ανάπτυξη υποστήριξης SAML, Kubernetes και Agent. Δείτε την πρόοδο και κάντε μια δωρεά.",
"donateButton": "Δωρίστε κρυπτονομίσματα",
"storageModeLocal": "Πρόγραμμα περιήγησης",
"storageModeCloud": "Βάση δεδομένων",
"storageModeDescription": "Το πρόγραμμα περιήγησης αποθηκεύει τις ρυθμίσεις μόνο σε αυτό το πρόγραμμα περιήγησης. Η βάση δεδομένων συγχρονίζεται με τον διακομιστή και φορτώνει σε οποιαδήποτε συσκευή.",
@@ -3011,6 +3144,10 @@
"twoFaOn": "Επί",
"twoFaOff": "Μακριά από",
"versionLabel": "Εκδοχή",
"betaProgramTitle": "Πρόγραμμα Beta",
"betaProgramDescription": "Δοκιμάστε νωρίς τις επερχόμενες λειτουργίες με την εβδομαδιαία κυλιόμενη ετικέτα Docker :beta. Ασταθές, δεν είναι για παραγωγή.",
"betaProgramFeedback": "Βρήκατε κάποιο σφάλμα; Αναφέρετέ το εδώ.",
"betaProgramLearnMore": "Μάθετε περισσότερα",
"deleteAccount": "Διαγραφή λογαριασμού",
"deleteAccountDescription": "Οριστική διαγραφή του λογαριασμού σας",
"changeServerDescription": "Μετάβαση σε διαφορετικό διακομιστή backend Termix",
+158 -21
View File
@@ -864,6 +864,21 @@
"proxmoxDockerDetectionDesc": "Patrones de nombres separados por comas que permiten a Docker hacer coincidir los invitados.",
"proxmoxPreferredRanges": "Rangos de IP preferidos",
"proxmoxPreferredRangesDesc": "Prefijos separados por comas en orden de prioridad para la selección de IP cuando un invitado tiene múltiples interfaces.",
"proxmoxAutoSync": "Sincronización automática de invitados",
"proxmoxAutoSyncDesc": "Descubre periódicamente este nodo Proxmox y crea o actualiza los hosts invitados importados mientras tu sesión esté desbloqueada.",
"proxmoxSyncInterval": "Intervalo de sincronización (minutos)",
"proxmoxSyncIntervalDesc": "Mínimo 5 minutos. El programador omite las sesiones de usuario bloqueadas.",
"proxmoxMarkMissing": "Mark, huéspedes desaparecidos",
"proxmoxMarkMissingDesc": "Cuando un invitado importado previamente desaparece, se añade la etiqueta proxmox-missing en lugar de eliminarlo.",
"proxmoxLastSync": "Última sincronización",
"proxmoxLastSyncNever": "Aún no sincronizado",
"proxmoxLastSyncNoResult": "No hay detalles del resultado",
"proxmoxLastSyncSummary": "{{created}} creado, {{updated}} actualizado, {{markedMissing}} faltante, {{skipped}} omitido",
"proxmoxLastSyncStatus": {
"success": "Éxito",
"error": "Fallido",
"pending": "Pendiente"
},
"proxmoxDiscoverAction": "Descubre e importa invitados de Proxmox",
"proxmoxImportTitle": "Importar desde Proxmox",
"proxmoxSelectHost": "Seleccione un host Proxmox…",
@@ -1167,7 +1182,57 @@
"shareHost": "Compartir anfitrión",
"shareHostTitle": "Compartir: {{name}}",
"sharing": {
"requiresCredential": "Este host debe usar una credencial para habilitar el uso compartido. Edite el host y asigne una credencial primero."
"loadError": "No se pudieron cargar los datos para compartir. Inténtelo de nuevo.",
"shareWithSection": "Compartir con",
"usersTab": "Usuarios",
"rolesTab": "Roles",
"searchPlaceholder": "Buscar usuarios o roles...",
"noMatches": "No se encontraron coincidencias",
"permissionLevelLabel": "Nivel de permiso",
"levels": {
"connect": {
"label": "Conectar",
"description": "Solo se permiten sesiones abiertas: terminal, escritorio remoto, administrador de archivos, túneles y Docker. No se permite el acceso a la configuración del host."
},
"view": {
"label": "Vista",
"description": "Conéctate y consulta la configuración del host. Las claves secretas nunca se muestran."
},
"edit": {
"label": "Editar",
"description": "Visualice y modifique el host. Los secretos se pueden reemplazar, pero nunca leer; las asignaciones de credenciales permanecen solo para el propietario."
},
"manage": {
"label": "Administrar",
"description": "Edita, comparte el host con otros, cambia los niveles de permisos y revoca el acceso."
}
},
"expiryLabel": "Caducidad del acceso",
"expiry": {
"never": "Nunca",
"oneHour": "1 hora",
"oneDay": "24 horas",
"sevenDays": "7 días",
"thirtyDays": "30 días",
"custom": "Costumbre"
},
"customHoursPlaceholder": "Horas hasta que caduque el acceso",
"shareButton": "Compartir",
"shareWithCount": "Compartir ({{count}})",
"currentAccess": "Acceso actual",
"noAccessEntries": "Este host aún no se ha compartido.",
"grantedBy": "Concedido por",
"expires": "Caduca",
"expired": "Venció",
"never": "Nunca",
"revoke": "Revocar",
"accessUpdated": "Acceso actualizado",
"accessUpdateFailed": "No se pudo actualizar el acceso.",
"sharedBadge": "Compartido",
"sharedBadgeTooltip": "Compartido por {{owner}} ({{level}} acceso)",
"viewOnlyBanner": "Este host es compartido contigo por {{owner}} con acceso de visualización. La configuración es de solo lectura.",
"sharedEditBanner": "Este host es compartido contigo por {{owner}} con permisos de edición. Los cambios se aplican al host real; las referencias de autenticación solo pueden ser modificadas por el propietario.",
"ownerOnlyControl": "Solo el propietario del host puede cambiar esto."
},
"guac": {
"connection": "Conexión",
@@ -1302,28 +1367,10 @@
"backspaceKey": "Tecla de retroceso",
"saveHostFirst": "Primero, salva al anfitrión.",
"sharingOptionsAfterSave": "Las opciones para compartir estarán disponibles una vez que se haya guardado el host.",
"sharingLoadError": "No se pudieron cargar los datos para compartir. Compruebe su conexión e inténtelo de nuevo.",
"shareHostSection": "Compartir anfitrión",
"shareWithUser": "Compartir con el usuario",
"shareWithRole": "Compartir con rol",
"selectUser": "Seleccionar usuario",
"selectRole": "Seleccionar rol",
"selectUserOption": "Seleccione un usuario...",
"selectRoleOption": "Selecciona un rol...",
"permissionLevel": "Nivel de permiso",
"expiresInHours": "Caduca en (horas)",
"noExpiryPlaceholder": "Dejar vacío para que no caduque.",
"shareBtn": "Compartir",
"currentAccess": "Acceso actual",
"typeHeader": "Tipo",
"targetHeader": "Objetivo",
"permissionHeader": "Permiso",
"grantedByHeader": "Otorgado por",
"expiresHeader": "Caduca",
"noAccessEntries": "Aún no hay entradas de acceso.",
"expiredLabel": "Venció",
"neverLabel": "Nunca",
"revokeBtn": "Revocar",
"cancelBtn": "Cancelar",
"savingBtn": "Ahorro...",
"updateHostBtn": "Host de actualización",
@@ -2190,7 +2237,8 @@
"passwordLoginDisabledDesc": "El inicio de sesión con contraseña está deshabilitado. Utilice una clave de acceso o un proveedor de autenticación externo.",
"signInWithPasskey": "Iniciar sesión con la clave de acceso",
"passkeyLoginFailed": "Falló el inicio de sesión con clave de acceso.",
"attemptsRemaining": "{{count}} intentos restantes"
"attemptsRemaining": "{{count}} intentos restantes",
"confirmResetDataWipe": "Esta cuenta no ha iniciado sesión desde la actualización del cifrado, por lo que sus datos almacenados no se pueden recuperar sin la contraseña anterior. Restablecerla eliminará permanentemente sus hosts, credenciales y fragmentos de código. ¿Desea continuar?"
},
"hostKey": {
"verifyNewHost": "Verificar la clave de host SSH",
@@ -2323,6 +2371,13 @@
"serviceLinks": "Enlaces de servicio",
"homepagePreview": "Vista previa de la página de inicio"
},
"donation": {
"title": "¿Te gusta Termix?",
"body": "Termix es gratuito y de código abierto, desarrollado y mantenido por un equipo de dos personas en nuestro tiempo libre. Si ha reemplazado una herramienta comercial por la que de otro modo pagarías, una donación ayuda a cubrir los costos de alojamiento y a mantener el desarrollo. Por el momento, solo se aceptan donaciones en criptomonedas.",
"milestones": "Las donaciones también ayudan a financiar el tiempo necesario para investigar y aprender lo que se requiere para desarrollar funciones como la compatibilidad con SAML y Kubernetes. Consulta el progreso en la página de donaciones.",
"cta": "Donar",
"dismiss": "Quizás más tarde"
},
"dashboardTab": {
"stable": "ESTABLE",
"hostsOnline": "Anfitriones en línea",
@@ -2790,6 +2845,71 @@
"updateAdminStatusFailed": "No se pudo actualizar el estado del administrador.",
"allSessionsRevoked": "Todas las sesiones revocadas",
"revokeSessionsFailed": "No se pudieron revocar las sesiones.",
"manageUserData": "Gestionar datos de usuario",
"backToUsers": "Volver a los usuarios",
"manageTabAccount": "Cuenta",
"manageTabHosts": "Anfitriones",
"manageTabCredentials": "Cartas credenciales",
"manageTabSnippets": "Fragmentos",
"manageTabSessions": "Sesiones",
"manageTabDanger": "Peligro",
"manageEditorBack": "Volver a los datos de {{username}}",
"dataLockedBadge": "BLOQUEADO",
"dataLockedNotice": "Los datos de {{username}}permanecen bloqueados hasta que inicien sesión nuevamente. Sus hosts, credenciales y fragmentos no se pueden ver ni editar hasta entonces.",
"resetPasswordTitle": "Restablecer contraseña",
"resetPasswordOidcOnly": "Este usuario inicia sesión a través de un proveedor externo y no tiene contraseña.",
"resetPasswordPlaceholder": "Nueva contraseña",
"resetPasswordBtn": "Reiniciar",
"resetPasswordWorking": "Reiniciando...",
"resetPasswordSuccess": "Restablecimiento de contraseña",
"resetPasswordSuccessWiped": "Se ha restablecido la contraseña. Se han borrado los datos cifrados del usuario.",
"resetPasswordFailed": "No se pudo restablecer la contraseña.",
"resetPasswordConfirmWipe": "{{username}} no ha iniciado sesión desde la actualización del cifrado, por lo que sus datos no se pueden recuperar. Reiniciar ahora eliminará sus hosts, credenciales y fragmentos. ¿Continuar?",
"totpSectionTitle": "Autenticación de dos factores",
"totpStatusEnabled": "TOTP está habilitado para este usuario.",
"totpStatusDisabled": "TOTP no está habilitado para este usuario.",
"disableTotp": "Desactivar",
"disableTotpConfirm": "Deshabilitar la autenticación de dos factores para {{username}}? Podrán iniciar sesión solo con su contraseña.",
"totpDisabledSuccess": "TOTP deshabilitado",
"totpDisableFailed": "No se pudo desactivar TOTP.",
"manageApiKeys": "Claves API",
"apiKeyNamePlaceholder": "Nombre de la clave",
"apiKeyCopyNotice": "Copia esta clave ahora, no volverá a aparecer.",
"noApiKeysForUser": "Sin claves API",
"apiKeyDeleteFailed": "Error al eliminar la clave API",
"exportUserData": "Exportación de datos",
"exportUserDataDesc": "Descarga los hosts, las credenciales y los datos del administrador de archivos de este usuario en formato JSON. Los secretos están descifrados.",
"exportUserDataSuccess": "Datos de usuario exportados",
"exportUserDataFailed": "Error al exportar los datos del usuario.",
"hostsCount": "{{count}} anfitriones",
"addHostForUser": "Agregar host",
"noHostsForUser": "Este usuario no tiene hosts.",
"connectToHost": "Conectar",
"deleteHostConfirm": "¿Eliminar el host \"{{name}}\" perteneciente a {{username}}?",
"hostDeletedSuccess": "Host eliminado",
"hostDeleteFailed": "No se pudo eliminar el host",
"credentialsCount": "{{count}} credenciales",
"addCredentialForUser": "Agregar credencial",
"noCredentialsForUser": "Este usuario no tiene credenciales.",
"deleteCredentialConfirm": "¿Eliminar la credencial \"{{name}}\" perteneciente a {{username}}?",
"credentialDeletedSuccess": "Credencial eliminada",
"credentialDeleteFailed": "Error al eliminar las credenciales",
"snippetsCount": "{{count}} fragmentos",
"addSnippetForUser": "Agregar fragmento",
"noSnippetsForUser": "Este usuario no tiene fragmentos",
"snippetRequiredFields": "Se requiere el nombre y el contenido del fragmento.",
"snippetNamePlaceholder": "Nombre del fragmento",
"snippetContentPlaceholder": "Contenido del comando",
"snippetFolderPlaceholder": "Carpeta (opcional)",
"snippetSaved": "Fragmento guardado",
"snippetSaveFailed": "No se pudo guardar el fragmento.",
"deleteSnippetConfirm": "¿Eliminar el fragmento \"{{name}}\" perteneciente a {{username}}?",
"snippetDeletedSuccess": "Fragmento eliminado",
"snippetDeleteFailed": "No se pudo eliminar el fragmento.",
"noSessionsForUser": "No hay sesiones activas",
"deleteUserDangerDesc": "Eliminar permanentemente {{username}} y todos sus datos (hosts, credenciales, fragmentos, historial). Esta acción es irreversible.",
"deleteUserConfirm": "¿Eliminar permanentemente {{username}} y todos sus datos?",
"deleteUserAdminBlocked": "Elimine el estado de administrador antes de borrar a este usuario.",
"createRoleRequired": "Se requiere nombre y nombre para mostrar.",
"createRoleSuccess": "Rol \"{{name}}\" creado",
"createRoleFailed": "No se pudo crear el rol.",
@@ -2822,7 +2942,16 @@
"hostDefaultsCommandHistory": "Historial de comandos",
"hostDefaultsCommandHistoryDesc": "Registrar el historial de comandos en los nuevos hosts de forma predeterminada",
"hostDefaultsSaved": "Configuración predeterminada del host guardada",
"hostDefaultsSaveFailed": "No se pudieron guardar los valores predeterminados del host."
"hostDefaultsSaveFailed": "No se pudieron guardar los valores predeterminados del host.",
"rolePermissions": {
"count": "{{count}} permisos",
"editAction": "Modificar permisos",
"loadError": "No se pudo cargar el catálogo de permisos.",
"saved": "Permisos de rol guardados",
"saveError": "No se pudieron guardar los permisos de rol.",
"save": "Ahorrar",
"saving": "Ahorro..."
}
},
"newUi": {
"sidebar": {
@@ -2991,6 +3120,10 @@
"executionFailed": "Fallido"
},
"userProfile": {
"donateTitle": "Mantén Termix con vida",
"donateDescription": "Termix es gratuito, autoalojado y desarrollado por un pequeño grupo de personas en su tiempo libre. Si te ha ahorrado tiempo o dinero, una donación en criptomonedas ayuda a que siga funcionando.",
"donateMilestones": "Las donaciones ayudan a financiar el tiempo necesario para investigar y aprender lo que se requiere para desarrollar compatibilidad con SAML, Kubernetes y agentes. Sigue el progreso y dona.",
"donateButton": "Dona criptomonedas",
"storageModeLocal": "Navegador",
"storageModeCloud": "Base de datos",
"storageModeDescription": "El navegador guarda la configuración únicamente en este navegador. La base de datos se sincroniza con el servidor y se carga en cualquier dispositivo.",
@@ -3011,6 +3144,10 @@
"twoFaOn": "En",
"twoFaOff": "Apagado",
"versionLabel": "Versión",
"betaProgramTitle": "Programa Beta",
"betaProgramDescription": "Prueba las próximas funciones antes que nadie con la etiqueta Docker :beta, que se actualiza semanalmente. Es inestable y no apta para producción.",
"betaProgramFeedback": "¿Has encontrado un error? Infórmanos aquí.",
"betaProgramLearnMore": "Más información",
"deleteAccount": "Eliminar cuenta",
"deleteAccountDescription": "Eliminar permanentemente tu cuenta",
"changeServerDescription": "Cambia a un servidor backend de Termix diferente.",
+158 -21
View File
@@ -864,6 +864,21 @@
"proxmoxDockerDetectionDesc": "Pilkuilla erotetut nimimallit, joiden avulla Docker löytää vieraat yhteensopiviksi",
"proxmoxPreferredRanges": "Suositellut IP-alueet",
"proxmoxPreferredRangesDesc": "Pilkuilla erotetut etuliitteet prioriteettijärjestyksessä IP-osoitteen valinnassa, kun vieraalla on useita rajapintoja",
"proxmoxAutoSync": "Synkronoi vieraat automaattisesti",
"proxmoxAutoSyncDesc": "Etsi tämä Proxmox-solmu säännöllisesti ja luo tai päivitä tuotuja vieraskomanteja istunnon ollessa lukitsematon.",
"proxmoxSyncInterval": "Synkronointiväli (minuuttia)",
"proxmoxSyncIntervalDesc": "Vähintään 5 minuuttia. Ajastin ohittaa lukitut käyttäjäistunnot.",
"proxmoxMarkMissing": "Merkitse puuttuvat vieraat",
"proxmoxMarkMissingDesc": "Lisää proxmox-missing-tunniste, kun aiemmin tuotu vieras katoaa sen poistamisen sijaan.",
"proxmoxLastSync": "Viimeisin synkronointi",
"proxmoxLastSyncNever": "Ei vielä synkronoitu",
"proxmoxLastSyncNoResult": "Ei tulostietoja",
"proxmoxLastSyncSummary": "{{created}} luotu, {{updated}} päivitetty, {{markedMissing}} puuttuu, {{skipped}} ohitettu",
"proxmoxLastSyncStatus": {
"success": "Menestys",
"error": "Epäonnistui",
"pending": "Odottaa"
},
"proxmoxDiscoverAction": "Löydä ja tuo Proxmox-vieraita",
"proxmoxImportTitle": "Tuo Proxmoxista",
"proxmoxSelectHost": "Valitse Proxmox-isäntä…",
@@ -1167,7 +1182,57 @@
"shareHost": "Jaa isäntä",
"shareHostTitle": "Jaa: {{name}}",
"sharing": {
"requiresCredential": "Tämän isännän on käytettävä tunnistetietoja jakamisen mahdollistamiseksi. Muokkaa ensin isäntää ja määritä tunnistetiedot."
"loadError": "Jakamistietojen lataaminen epäonnistui. Yritä uudelleen.",
"shareWithSection": "Jaa kanssa",
"usersTab": "Käyttäjät",
"rolesTab": "Roolit",
"searchPlaceholder": "Hae käyttäjiä tai rooleja...",
"noMatches": "Ei osumia",
"permissionLevelLabel": "Käyttöoikeustaso",
"levels": {
"connect": {
"label": "Yhdistä",
"description": "Vain avoimet istunnot: pääte, etätyöpöytä, tiedostonhallinta, tunnelit ja Docker. Ei pääsyä isäntäkokoonpanoon."
},
"view": {
"label": "Näytä",
"description": "Yhdistä ja katso isäntäkokoonpano. Salaisuuksia ei koskaan näytetä."
},
"edit": {
"label": "Muokata",
"description": "Näytä ja muokkaa isäntää. Salaisuuksia voidaan korvata, mutta niitä ei voida koskaan lukea; tunnistetietojen määritykset pysyvät vain omistajalla."
},
"manage": {
"label": "Hallitse",
"description": "Muokkaa ja jaa isäntä muiden kanssa, muuta käyttöoikeustasoja ja peruuta käyttöoikeuksia."
}
},
"expiryLabel": "Käyttöoikeuden päättyminen",
"expiry": {
"never": "Ei koskaan",
"oneHour": "1 tunti",
"oneDay": "24 tuntia",
"sevenDays": "7 päivää",
"thirtyDays": "30 päivää",
"custom": "Mukautettu"
},
"customHoursPlaceholder": "Tuntia käyttöoikeuden vanhenemiseen",
"shareButton": "Jakaa",
"shareWithCount": "Jaa ({{count}})",
"currentAccess": "Nykyinen käyttöoikeus",
"noAccessEntries": "Tätä isäntää ei ole vielä jaettu",
"grantedBy": "Myöntänyt",
"expires": "Vanhenee",
"expired": "Vanhentunut",
"never": "Ei koskaan",
"revoke": "Peruuttaa",
"accessUpdated": "Käyttöoikeus päivitetty",
"accessUpdateFailed": "Käyttöoikeuksien päivittäminen epäonnistui",
"sharedBadge": "Jaettu",
"sharedBadgeTooltip": "Jaettu käyttäjän {{owner}} toimesta ({{level}} pääsy)",
"viewOnlyBanner": "{{owner}} jakaa tämän isännän kanssasi ja hänellä on katseluoikeudet. Määritykset ovat vain luku -tilassa.",
"sharedEditBanner": "{{owner}} jakaa tämän isännän kanssasi muokkausoikeuksin. Muutokset koskevat varsinaista isäntää; vain omistaja voi muuttaa todennusviitteitä.",
"ownerOnlyControl": "Vain isännän omistaja voi muuttaa tätä"
},
"guac": {
"connection": "Yhteys",
@@ -1302,28 +1367,10 @@
"backspaceKey": "Askelpalautin-näppäin",
"saveHostFirst": "Tallenna ensin isäntä.",
"sharingOptionsAfterSave": "Jakamisvaihtoehdot ovat käytettävissä isännän tallentamisen jälkeen.",
"sharingLoadError": "Jakamistietojen lataus epäonnistui. Tarkista yhteytesi ja yritä uudelleen.",
"shareHostSection": "Jaa isäntä",
"shareWithUser": "Jaa käyttäjän kanssa",
"shareWithRole": "Jaa roolin kanssa",
"selectUser": "Valitse käyttäjä",
"selectRole": "Valitse rooli",
"selectUserOption": "Valitse käyttäjä...",
"selectRoleOption": "Valitse rooli...",
"permissionLevel": "Käyttöoikeustaso",
"expiresInHours": "Vanhenee (tuntia) kuluttua",
"noExpiryPlaceholder": "Jätä tyhjäksi, jos et halua vanhenevan",
"shareBtn": "Jakaa",
"currentAccess": "Nykyinen käyttöoikeus",
"typeHeader": "Tyyppi",
"targetHeader": "Kohde",
"permissionHeader": "Lupa",
"grantedByHeader": "Myöntäjä",
"expiresHeader": "Vanhenee",
"noAccessEntries": "Ei vielä käyttöoikeusmerkintöjä.",
"expiredLabel": "Vanhentunut",
"neverLabel": "Ei koskaan",
"revokeBtn": "Peruuttaa",
"cancelBtn": "Peruuttaa",
"savingBtn": "Tallennetaan...",
"updateHostBtn": "Päivitä isäntä",
@@ -2190,7 +2237,8 @@
"passwordLoginDisabledDesc": "Salasanalla kirjautuminen on poistettu käytöstä. Käytä salasanaa tai ulkoista todennuspalveluntarjoajaa.",
"signInWithPasskey": "Kirjaudu sisään salasanalla",
"passkeyLoginFailed": "Salasanan kirjautuminen epäonnistui",
"attemptsRemaining": "{{count}} yritystä jäljellä"
"attemptsRemaining": "{{count}} yritystä jäljellä",
"confirmResetDataWipe": "Tälle tilille ei ole kirjautunut sisään salauspäivityksen jälkeen, joten sen tallennettuja tietoja ei voida palauttaa ilman vanhaa salasanaa. Nollaaminen poistaa pysyvästi sen isännät, tunnistetiedot ja koodinpätkät. Jatketaanko?"
},
"hostKey": {
"verifyNewHost": "Vahvista SSH-isäntäavain",
@@ -2323,6 +2371,13 @@
"serviceLinks": "Palvelulinkit",
"homepagePreview": "Etusivun esikatselu"
},
"donation": {
"title": "Pidätkö Termixistä?",
"body": "Termix on ilmainen ja avoimen lähdekoodin ohjelmisto, jota kahden hengen tiimi rakentaa ja ylläpitää vapaa-ajallaan. Jos se korvaa kaupallisen työkalun, josta muuten maksaisit, lahjoitus auttaa kattamaan ylläpitokulut ja pitämään kehityksen käynnissä. Tällä hetkellä lahjoitukset ovat vain kryptovaluuttoja.",
"milestones": "Lahjoitukset auttavat myös rahoittamaan aikaa tutkimukseen ja oppimaan, mitä tarvitaan ominaisuuksien, kuten SAML:n ja Kubernetes-tuen, rakentamiseen. Katso edistyminen lahjoitussivulta.",
"cta": "Lahjoittaa",
"dismiss": "Ehkä myöhemmin"
},
"dashboardTab": {
"stable": "VAKAA",
"hostsOnline": "Isännät verkossa",
@@ -2790,6 +2845,71 @@
"updateAdminStatusFailed": "Ylläpitäjän tilan päivittäminen epäonnistui",
"allSessionsRevoked": "Kaikki istunnot peruutettu",
"revokeSessionsFailed": "Istuntojen peruuttaminen epäonnistui",
"manageUserData": "Hallinnoi käyttäjätietoja",
"backToUsers": "Takaisin käyttäjiin",
"manageTabAccount": "Tili",
"manageTabHosts": "Isännät",
"manageTabCredentials": "Valtakirjat",
"manageTabSnippets": "Katkelmat",
"manageTabSessions": "Istunnot",
"manageTabDanger": "Vaara",
"manageEditorBack": "Takaisin käyttäjän {{username}}tietoihin",
"dataLockedBadge": "LUKITTU",
"dataLockedNotice": "{{username}}:n tiedot pysyvät lukittuina, kunnes he kirjautuvat sisään seuraavalla kerralla. Heidän palvelimiaan, tunnistetietojaan ja koodinpätkiään ei voida tarkastella tai muokata ennen sitä.",
"resetPasswordTitle": "Palauta salasana",
"resetPasswordOidcOnly": "Tämä käyttäjä kirjautuu sisään ulkoisen palveluntarjoajan kautta eikä hänellä ole salasanaa.",
"resetPasswordPlaceholder": "Uusi salasana",
"resetPasswordBtn": "Nollaa",
"resetPasswordWorking": "Nollataan...",
"resetPasswordSuccess": "Salasanan palautus",
"resetPasswordSuccessWiped": "Salasanan palautus. Käyttäjän salatut tiedot on pyyhitty.",
"resetPasswordFailed": "Salasanan nollaaminen epäonnistui",
"resetPasswordConfirmWipe": "{{username}} ei ole kirjautunut sisään salauspäivityksen jälkeen, joten hänen tietojaan ei voida palauttaa. Nollaaminen nyt poistaa hänen palvelimensa, tunnistetietonsa ja koodinpätkänsä. Jatketaanko?",
"totpSectionTitle": "Kaksivaiheinen todennus",
"totpStatusEnabled": "TOTP on käytössä tälle käyttäjälle",
"totpStatusDisabled": "TOTP ei ole käytössä tälle käyttäjälle",
"disableTotp": "Poista käytöstä",
"disableTotpConfirm": "Poista kaksivaiheinen todennus käytöstä käyttäjälle {{username}}? He voivat kirjautua sisään pelkällä salasanallaan.",
"totpDisabledSuccess": "TOTP poistettu käytöstä",
"totpDisableFailed": "TOTP:n poistaminen käytöstä epäonnistui",
"manageApiKeys": "API-avaimet",
"apiKeyNamePlaceholder": "Avaimen nimi",
"apiKeyCopyNotice": "Kopioi tämä avain nyt, sitä ei näytetä enää uudelleen.",
"noApiKeysForUser": "Ei API-avaimia",
"apiKeyDeleteFailed": "API-avaimen poistaminen epäonnistui",
"exportUserData": "Tietojen vienti",
"exportUserDataDesc": "Lataa tämän käyttäjän isännät, tunnistetiedot ja tiedostonhallintadata JSON-muodossa. Salaisuudet on purettu.",
"exportUserDataSuccess": "Käyttäjätiedot viety",
"exportUserDataFailed": "Käyttäjätietojen vienti epäonnistui",
"hostsCount": "{{count}} isännät",
"addHostForUser": "Lisää isäntä",
"noHostsForUser": "Tällä käyttäjällä ei ole isäntiä",
"connectToHost": "Yhdistä",
"deleteHostConfirm": "Poistetaanko isäntä \"{{name}}\", joka kuuluu {{username}}:lle?",
"hostDeletedSuccess": "Isäntä poistettu",
"hostDeleteFailed": "Isännän poistaminen epäonnistui",
"credentialsCount": "{{count}} tunnistetiedot",
"addCredentialForUser": "Lisää tunnistetiedot",
"noCredentialsForUser": "Tällä käyttäjällä ei ole tunnuksia",
"deleteCredentialConfirm": "Poistetaanko tunniste \"{{name}}\", joka kuuluu käyttäjälle {{username}}?",
"credentialDeletedSuccess": "Tunnistetiedot poistettu",
"credentialDeleteFailed": "Tunnuksen poistaminen epäonnistui",
"snippetsCount": "{{count}} katkelmia",
"addSnippetForUser": "Lisää katkelma",
"noSnippetsForUser": "Tällä käyttäjällä ei ole katkelmia",
"snippetRequiredFields": "Katkelman nimi ja sisältö vaaditaan",
"snippetNamePlaceholder": "Katkelman nimi",
"snippetContentPlaceholder": "Komennon sisältö",
"snippetFolderPlaceholder": "Kansio (valinnainen)",
"snippetSaved": "Katkelma tallennettu",
"snippetSaveFailed": "Katkelman tallentaminen epäonnistui",
"deleteSnippetConfirm": "Poistetaanko koodinpätkä \"{{name}}\", joka kuuluu käyttäjälle {{username}}?",
"snippetDeletedSuccess": "Katkelma poistettu",
"snippetDeleteFailed": "Katkelman poistaminen epäonnistui",
"noSessionsForUser": "Ei aktiivisia istuntoja",
"deleteUserDangerDesc": "Poista pysyvästi {{username}} ja kaikki siihen liittyvät tiedot (isännät, tunnistetiedot, koodinpätkät, historia). Tätä ei voi perua.",
"deleteUserConfirm": "Poistetaanko pysyvästi {{username}} ja kaikki heidän datansa?",
"deleteUserAdminBlocked": "Poista järjestelmänvalvojan tila ennen tämän käyttäjän poistamista.",
"createRoleRequired": "Nimi ja näyttönimi vaaditaan",
"createRoleSuccess": "Rooli \"{{name}}\" luotu",
"createRoleFailed": "Roolin luominen epäonnistui",
@@ -2822,7 +2942,16 @@
"hostDefaultsCommandHistory": "Komentohistoria",
"hostDefaultsCommandHistoryDesc": "Seuraa komentohistoriaa uusilla isännillä oletuksena",
"hostDefaultsSaved": "Isännän oletusasetukset tallennettu",
"hostDefaultsSaveFailed": "Isännän oletusasetusten tallentaminen epäonnistui"
"hostDefaultsSaveFailed": "Isännän oletusasetusten tallentaminen epäonnistui",
"rolePermissions": {
"count": "{{count}} käyttöoikeudet",
"editAction": "Muokkaa käyttöoikeuksia",
"loadError": "Käyttöoikeusluettelon lataaminen epäonnistui",
"saved": "Roolien käyttöoikeudet tallennettu",
"saveError": "Roolien käyttöoikeuksien tallentaminen epäonnistui",
"save": "Tallentaa",
"saving": "Tallennetaan..."
}
},
"newUi": {
"sidebar": {
@@ -2991,6 +3120,10 @@
"executionFailed": "Epäonnistui"
},
"userProfile": {
"donateTitle": "Pidä Termix hengissä",
"donateDescription": "Termix on ilmainen, itse isännöity ja vain muutaman ihmisen vapaa-ajallaan rakentama palvelu. Jos se on säästänyt aikaasi tai rahaasi, kryptovaluutan lahjoitus auttaa pitämään sen toiminnassa.",
"donateMilestones": "Lahjoitukset auttavat rahoittamaan aikaa SAML:n, Kubernetesin ja agenttituen rakentamiseen tarvittavien resurssien tutkimiseen ja oppimiseen. Katso edistymistä ja lahjoita.",
"donateButton": "Lahjoita kryptovaluuttaa",
"storageModeLocal": "Selain",
"storageModeCloud": "Tietokanta",
"storageModeDescription": "Selain tallentaa asetukset vain tähän selaimeen. Tietokanta synkronoituu palvelimelle ja latautuu mille tahansa laitteelle.",
@@ -3011,6 +3144,10 @@
"twoFaOn": "Päällä",
"twoFaOff": "Pois",
"versionLabel": "Versio",
"betaProgramTitle": "Beetaohjelma",
"betaProgramDescription": "Kokeile tulevia ominaisuuksia etukäteen viikoittain julkaistavan :beta Docker -tunnisteen avulla. Epävakaa, ei tuotantokäyttöön.",
"betaProgramFeedback": "Löysitkö virheen? Ilmoita siitä täällä.",
"betaProgramLearnMore": "Lue lisää",
"deleteAccount": "Poista tili",
"deleteAccountDescription": "Poista tilisi pysyvästi",
"changeServerDescription": "Vaihda toiseen Termix-taustapalvelimeen",
+188 -51
View File
@@ -1,15 +1,15 @@
{
"termixId": {
"title": "Identifiant Termix",
"title": "ID Termix",
"loadFailed": "Échec du chargement de l'identifiant Termix",
"claimTitle": "Réclamez votre identifiant Termix",
"claimIntro": "Choisissez un identifiant unique. Vos clés publiques SSH seront publiées à une URL publique que vous pourrez ajouter au fichier authorized_keys de n'importe quel serveur.",
"handleLabel": "Poignée",
"handlePlaceholder": "Alice",
"handlePlaceholder": "alice",
"checking": "Vérification…",
"available": "Disponible",
"taken": "Déjà pris",
"invalidHandle": "Lettres minuscules, chiffres et tirets (-) uniquement.",
"invalidHandle": "Lettres minuscules, chiffres, - et _ seulement",
"descriptionLabel": "Description (facultatif)",
"descriptionPlaceholder": "Clés de l'ordinateur portable et du téléphone professionnel",
"create": "Créer un identifiant Termix",
@@ -47,12 +47,12 @@
"linkedToTermixId": "Publié via Termix ID",
"selectCredential": "Sélectionnez une qualification...",
"import": "Importer",
"caTitle": "autorité de certification",
"caTitle": "Autorité de certification",
"caIntro": "Faites confiance à cette autorité de certification sur un serveur : elle acceptera tout certificat que vous signerez. Révoquez-les tous en une seule fois ; les certificats expirent automatiquement.",
"caEnable": "Activer l'autorité de certification",
"caEnabled": "CA activé",
"caCreateFailed": "Échec de l'activation de l'autorité de certification",
"caPublicKeyLabel": "clé publique de l'autorité de certification",
"caPublicKeyLabel": "Clé publique de l'autorité de certification",
"caTrustLabel": "Faire confiance à un serveur (exécuté en tant que root)",
"caRotate": "Tourner",
"caRotateConfirm": "Faut-il faire tourner l'autorité de certification ? Tous les certificats qu'elle a signés cesseront d'être acceptés et devront être réémis.",
@@ -64,7 +64,7 @@
"caDeleteFailed": "Échec de la suppression de l'autorité de certification",
"caValidityLabel": "Validité du certificat (jours)",
"issueCert": "Certificat",
"issueCertTooltip": "Émettez un certificat SSH pour cette clé, signé par votre autorité de certification.",
"issueCertTooltip": "Émettez un certificat SSH pour cette clé, signé par votre autorité de certification",
"certIssued": "Certificat délivré et téléchargé",
"certIssueFailed": "Échec de l'émission du certificat"
},
@@ -111,7 +111,7 @@
"editWidget": "Modifier le widget",
"deleteWidget": "Supprimer",
"widgetTypes": "Types de widgets",
"serviceLink": "Service Link",
"serviceLink": "Lien vers les services",
"folder": "Dossier",
"clock": "Horloge",
"notes": "Notes",
@@ -131,7 +131,7 @@
"widgetTitlePlaceholder": "Titre du widget (facultatif)",
"url": "URL",
"imageUrl": "URL de l'image personnalisée",
"imageUrlHint": "Laissez ce champ vide pour utiliser automatiquement le favicon du site.",
"imageUrlHint": "Laissez ce champ vide pour utiliser automatiquement le favicon du site",
"showImage": "Afficher l'image",
"description": "Description",
"color": "Couleur",
@@ -157,7 +157,7 @@
"widgetFolderDesc": "Un conteneur pour regrouper les widgets associés",
"widgetClockDesc": "Une horloge en temps réel avec fuseau horaire configurable",
"widgetNotesDesc": "Un widget de notes Markdown",
"widgetHostStatusDesc": "Affiche en temps réel le processeur, la mémoire et le disque d'un hôte SSH.",
"widgetHostStatusDesc": "Affiche en temps réel le processeur, la mémoire et le disque d'un hôte SSH",
"widgetBookmarkListDesc": "Liste de liens rapides",
"copyLink": "Copier le lien",
"linkCopied": "Lien copié !",
@@ -181,7 +181,7 @@
"metricsNotAvailable": "Métriques non disponibles",
"selectHost": "Sélectionnez un hôte...",
"displayedMetrics": "Métriques affichées",
"metricCpu": "processeur",
"metricCpu": "Processeur",
"metricMemory": "Mémoire",
"metricDisk": "Disque",
"metricUptime": "Disponibilité",
@@ -269,22 +269,22 @@
"widgetSearchLinksDesc": "Boutons de raccourci de recherche rapide avec saisie intégrée",
"widgetLinkTreeName": "Arbre de liens",
"widgetLinkTreeDesc": "Sections de liens regroupées avec des titres",
"calMon": "Mo",
"calTue": "Tu",
"calWed": "Nous",
"calThu": "Ème",
"calFri": "Fr",
"calMon": "Lu",
"calTue": "Ma",
"calWed": "Me",
"calThu": "Jeu",
"calFri": "Ve",
"calSat": "Sa",
"calSun": "Su",
"calSun": "Di",
"startOnMonday": "Début de la semaine lundi",
"countdownNoDate": "Aucune date cible n'a été fixée.",
"countdownPast": "L'événement est passé.",
"countdownNoDate": "Aucune date cible n'a été fixée",
"countdownPast": "L'événement est passé",
"countdownDays": "jours",
"countdownHours": "heures",
"countdownMinutes": "min",
"countdownSeconds": "seconde",
"countdownLabel": "Étiquette",
"countdownLabelPlaceholder": "par exemple, le jour du lancement",
"countdownLabelPlaceholder": "ex: Le jour du lancement",
"countdownShowDays": "Journées d'exposition",
"countdownShowHours": "Horaires d'ouverture",
"targetDate": "Date cible",
@@ -312,7 +312,7 @@
"displayField": "Champ d'affichage",
"jsonPath": "Chemin JSON",
"customApiLabel": "Étiquette",
"customApiLabelPlaceholder": "par exemple la température",
"customApiLabelPlaceholder": "ex: La température",
"customApiUnit": "Unité",
"customApiNoUrl": "Aucune URL d'API configurée",
"customApiError": "Échec de la récupération",
@@ -320,7 +320,7 @@
"addService": "Ajouter un service",
"showLabels": "Afficher les étiquettes",
"iconSize": "Taille de l'icône",
"noDashboardLinks": "Aucun lien vers le tableau de bord n'est configuré.",
"noDashboardLinks": "Aucun lien vers le tableau de bord n'est configuré",
"noLimit": "Aucune limite",
"sectionHeading": "Titre de section",
"addSection": "Ajouter une section",
@@ -368,20 +368,20 @@
},
"serverConfig": {
"title": "Configuration du serveur",
"description": "Configurez l'URL du serveur Termix pour vous connecter à vos services backend.",
"description": "Configurez l'URL du serveur Termix pour vous connecter à vos services backend",
"serverUrl": "URL du serveur",
"enterServerUrl": "Veuillez saisir une URL de serveur",
"saveFailed": "Échec de l'enregistrement de la configuration",
"saveError": "Erreur lors de l'enregistrement de la configuration",
"saving": "Économie...",
"saveConfig": "Enregistrer la configuration",
"helpText": "Saisissez l'URL où votre serveur Termix est exécuté (par exemple, http://localhost:30001 ou https://votre-serveur.com).",
"helpText": "Saisissez l'URL où votre serveur Termix est exécuté (par exemple, http://localhost:30001 ou https://votre-serveur.com)",
"changeServer": "Changer de serveur",
"mustIncludeProtocol": "L'URL du serveur doit commencer par http:// ou https://",
"allowInvalidCertificate": "Autoriser un certificat invalide",
"allowInvalidCertificateDesc": "À utiliser uniquement pour des serveurs auto-hébergés de confiance avec des certificats auto-signés ou des certificats d'adresse IP.",
"useEmbedded": "Utiliser le serveur local",
"embeddedDesc": "Exécutez Termix avec le serveur local intégré (aucun serveur distant n'est nécessaire).",
"embeddedDesc": "Exécutez Termix avec le serveur local intégré (aucun serveur distant n'est nécessaire)",
"embeddedConnecting": "Connexion au serveur local...",
"embeddedNotReady": "Le serveur local n'est pas encore prêt. Veuillez patienter quelques instants et réessayer.",
"localServer": "Serveur local",
@@ -600,17 +600,17 @@
"selectSnippet": "Sélectionner un extrait",
"forceKeyboardInteractive": "Forcer l'interaction au clavier",
"overrideCredentialUsername": "Remplacer le nom d'utilisateur d'identification",
"overrideCredentialUsernameDesc": "Utilisez le nom d'utilisateur indiqué ci-dessus au lieu du nom d'utilisateur des identifiants.",
"overrideCredentialUsernameDesc": "Utilisez le nom d'utilisateur indiqué ci-dessus au lieu du nom d'utilisateur des identifiants",
"oidcUsernameHint": "Utilisez $oidc.preferred_username pour remplacer votre nom de connexion OIDC.",
"jumpHostChain": "Chaîne d'hôtes de saut",
"portKnocking": "Port Knocking",
"portKnocking": "Tocage à la Porte",
"addKnock": "Ajouter un port",
"addProxyNode": "Ajouter un nœud",
"proxyNode": "Nœud proxy",
"proxyType": "Type de proxy",
"quickActions": "Actions rapides",
"sudoPasswordAutoFill": "Remplissage automatique du mot de passe Sudo",
"sudoPassword": "Mot de passe Sudo",
"sudoPasswordAutoFill": "Remplissage automatique du mot de passe sudo",
"sudoPassword": "Mot de passe sudo",
"keepaliveInterval": "Intervalle de maintien de connexion (ms)",
"moshCommand": "Commandement MOSH",
"environmentVariables": "Variables environnementales",
@@ -864,6 +864,21 @@
"proxmoxDockerDetectionDesc": "Modèles de noms séparés par des virgules permettant à Docker d'identifier les invités",
"proxmoxPreferredRanges": "Plages d'adresses IP préférées",
"proxmoxPreferredRangesDesc": "Préfixes séparés par des virgules, classés par ordre de priorité pour la sélection d'adresses IP lorsqu'un invité possède plusieurs interfaces.",
"proxmoxAutoSync": "Synchronisation automatique des invités",
"proxmoxAutoSyncDesc": "Découvrez périodiquement ce nœud Proxmox et créez ou mettez à jour les hôtes invités importés pendant que votre session est déverrouillée.",
"proxmoxSyncInterval": "Intervalle de synchronisation (minutes)",
"proxmoxSyncIntervalDesc": "Minimum 5 minutes. Le planificateur ignore les sessions utilisateur verrouillées.",
"proxmoxMarkMissing": "Marquer les invités manquants",
"proxmoxMarkMissingDesc": "Ajoutez une étiquette proxmox-missing lorsqu'un invité précédemment importé disparaît, au lieu de le supprimer.",
"proxmoxLastSync": "Dernière synchronisation",
"proxmoxLastSyncNever": "Pas encore synchronisé",
"proxmoxLastSyncNoResult": "Aucun résultat détaillé",
"proxmoxLastSyncSummary": "{{created}} créé, {{updated}} mis à jour, {{markedMissing}} manquant, {{skipped}} ignoré",
"proxmoxLastSyncStatus": {
"success": "Succès",
"error": "Échoué",
"pending": "En attente"
},
"proxmoxDiscoverAction": "Découvrir et importer des invités Proxmox",
"proxmoxImportTitle": "Importer depuis Proxmox",
"proxmoxSelectHost": "Sélectionnez un hôte Proxmox…",
@@ -1045,7 +1060,7 @@
"deployingBtn": "Déploiement...",
"deployBtn": "Déployer",
"failedToDeployKey": "Échec du déploiement de la clé",
"deleteHostsConfirm": "Delete {{count}} host{{plural}}? This cannot be undone.",
"deleteHostsConfirm": "Supprimer {{count}} hôte{{plural}} ? Cette opération est irréversible.",
"movedToRoot": "Déplacé vers la racine",
"enableTerminalFeature": "Activer le terminal",
"disableTerminalFeature": "Désactiver le terminal",
@@ -1167,7 +1182,57 @@
"shareHost": "Hôte partagé",
"shareHostTitle": "Partager : {{name}}",
"sharing": {
"requiresCredential": "Cet hôte doit utiliser des identifiants pour activer le partage. Modifiez l'hôte et attribuez-lui d'abord des identifiants."
"loadError": "Impossible de charger les données de partage. Veuillez réessayer.",
"shareWithSection": "Partager avec",
"usersTab": "Utilisateurs",
"rolesTab": "Rôles",
"searchPlaceholder": "Rechercher des utilisateurs ou des rôles...",
"noMatches": "Aucun résultat trouvé",
"permissionLevelLabel": "Niveau d'autorisation",
"levels": {
"connect": {
"label": "Connecter",
"description": "Seules les sessions ouvertes (terminal, bureau à distance, gestionnaire de fichiers, tunnels et Docker) sont autorisées. Aucun accès à la configuration de l'hôte n'est autorisé."
},
"view": {
"label": "Voir",
"description": "Connectez-vous et consultez la configuration de l'hôte. Les secrets ne sont jamais divulgués."
},
"edit": {
"label": "Modifier",
"description": "Consultez et modifiez l'hôte. Les secrets peuvent être remplacés mais jamais lus ; les identifiants restent réservés à leur propriétaire."
},
"manage": {
"label": "Gérer",
"description": "Modifiez, partagez l'hôte avec d'autres, changez les niveaux d'autorisation et révoquer l'accès."
}
},
"expiryLabel": "Expiration de l'accès",
"expiry": {
"never": "Jamais",
"oneHour": "1 heure",
"oneDay": "24 heures",
"sevenDays": "7 jours",
"thirtyDays": "30 jours",
"custom": "Coutume"
},
"customHoursPlaceholder": "Heures restantes avant l'expiration de l'accès",
"shareButton": "Partager",
"shareWithCount": "Partager ({{count}})",
"currentAccess": "Accès actuel",
"noAccessEntries": "Cet hôte n'a pas encore été partagé.",
"grantedBy": "Accordé par",
"expires": "Expire",
"expired": "Expiré",
"never": "Jamais",
"revoke": "Révoquer",
"accessUpdated": "Accès mis à jour",
"accessUpdateFailed": "Échec de la mise à jour de l'accès",
"sharedBadge": "Commun",
"sharedBadgeTooltip": "Partagé par {{owner}} ({{level}} accès)",
"viewOnlyBanner": "Cet hôte est partagé avec vous par {{owner}} qui dispose d'un accès en lecture seule. La configuration est en lecture seule.",
"sharedEditBanner": "Cet hôte est partagé avec vous par {{owner}} qui dispose d'un accès en modification. Les modifications s'appliquent à l'hôte réel ; les références d'authentification ne peuvent être modifiées que par le propriétaire.",
"ownerOnlyControl": "Seul le propriétaire de l'hôte peut modifier cela."
},
"guac": {
"connection": "Connexion",
@@ -1302,28 +1367,10 @@
"backspaceKey": "Touche Retour arrière",
"saveHostFirst": "Sauvegardez d'abord l'hôte.",
"sharingOptionsAfterSave": "Les options de partage sont disponibles une fois l'hôte enregistré.",
"sharingLoadError": "Impossible de charger les données de partage. Vérifiez votre connexion et réessayez.",
"shareHostSection": "Hôte partagé",
"shareWithUser": "Partager avec l'utilisateur",
"shareWithRole": "Partager avec le rôle",
"selectUser": "Sélectionner l'utilisateur",
"selectRole": "Sélectionner un rôle",
"selectUserOption": "Sélectionnez un utilisateur...",
"selectRoleOption": "Choisissez un rôle...",
"permissionLevel": "Niveau d'autorisation",
"expiresInHours": "Expire dans (heures)",
"noExpiryPlaceholder": "Laisser vide pour une durée de conservation illimitée.",
"shareBtn": "Partager",
"currentAccess": "Accès actuel",
"typeHeader": "Taper",
"targetHeader": "Cible",
"permissionHeader": "Autorisation",
"grantedByHeader": "Accordé par",
"expiresHeader": "Expire",
"noAccessEntries": "Aucune entrée d'accès pour le moment.",
"expiredLabel": "Expiré",
"neverLabel": "Jamais",
"revokeBtn": "Révoquer",
"cancelBtn": "Annuler",
"savingBtn": "Économie...",
"updateHostBtn": "Mettre à jour l'hôte",
@@ -1693,7 +1740,7 @@
"dragOutsideToDownload": "Faites glisser en dehors de la fenêtre pour télécharger ({{count}} fichiers)",
"newFolderDefault": "Nouveau dossier",
"newFileDefault": "NouveauFichier.txt",
"successfullyMovedItems": "Successfully moved {{count}} items to {{target}}",
"successfullyMovedItems": "Les objets {{count}} ont été déplacés vers {{target}} avec succès",
"move": "Se déplacer",
"searchInFile": "Rechercher dans un fichier (Ctrl+F)",
"showKeyboardShortcuts": "Afficher les raccourcis clavier",
@@ -2190,7 +2237,8 @@
"passwordLoginDisabledDesc": "La connexion par mot de passe est désactivée. Veuillez utiliser une clé d'accès ou un fournisseur d'authentification externe.",
"signInWithPasskey": "Connectez-vous avec votre clé d'accès",
"passkeyLoginFailed": "Échec de la connexion par mot de passe",
"attemptsRemaining": "{{count}} tentatives restantes"
"attemptsRemaining": "{{count}} tentatives restantes",
"confirmResetDataWipe": "Ce compte n'a pas été utilisé depuis la mise à jour du chiffrement ; ses données ne peuvent donc pas être récupérées sans l'ancien mot de passe. La réinitialisation supprimera définitivement ses hôtes, identifiants et extraits de code. Continuer ?"
},
"hostKey": {
"verifyNewHost": "Vérifier la clé d'hôte SSH",
@@ -2323,6 +2371,13 @@
"serviceLinks": "Liens de service",
"homepagePreview": "Aperçu de la page d'accueil"
},
"donation": {
"title": "Vous appréciez Termix ?",
"body": "Termix est un logiciel libre et gratuit, développé et maintenu par une équipe de deux personnes sur leur temps libre. S'il remplace un outil commercial que vous auriez autrement acheté, un don contribue à couvrir les frais d'hébergement et à soutenir son développement. Pour l'instant, les dons sont uniquement acceptés en cryptomonnaie.",
"milestones": "Les dons permettent également de financer le temps consacré à la recherche et à l'apprentissage des compétences nécessaires au développement de fonctionnalités telles que la prise en charge de SAML et de Kubernetes. Suivez l'avancement du projet sur la page de don.",
"cta": "Faire un don",
"dismiss": "Peut-être plus tard"
},
"dashboardTab": {
"stable": "ÉCURIE",
"hostsOnline": "Hôtes en ligne",
@@ -2790,6 +2845,71 @@
"updateAdminStatusFailed": "Échec de la mise à jour du statut d'administrateur",
"allSessionsRevoked": "Toutes les sessions ont été révoquées.",
"revokeSessionsFailed": "Échec de la révocation des sessions",
"manageUserData": "Gérer les données utilisateur",
"backToUsers": "Retour aux utilisateurs",
"manageTabAccount": "Compte",
"manageTabHosts": "Hôtes",
"manageTabCredentials": "Informations d'identification",
"manageTabSnippets": "Extraits",
"manageTabSessions": "Séances",
"manageTabDanger": "Danger",
"manageEditorBack": "Retour aux données de {{username}}",
"dataLockedBadge": "FERMÉ",
"dataLockedNotice": "Les données de {{username}}restent verrouillées jusqu'à sa prochaine connexion. Ses hôtes, identifiants et extraits de code ne peuvent être consultés ni modifiés d'ici là.",
"resetPasswordTitle": "Réinitialiser le mot de passe",
"resetPasswordOidcOnly": "Cet utilisateur se connecte via un fournisseur externe et n'a pas de mot de passe.",
"resetPasswordPlaceholder": "Nouveau mot de passe",
"resetPasswordBtn": "Réinitialiser",
"resetPasswordWorking": "Réinitialisation...",
"resetPasswordSuccess": "Réinitialisation du mot de passe",
"resetPasswordSuccessWiped": "Réinitialisation du mot de passe. Les données chiffrées de l'utilisateur ont été effacées.",
"resetPasswordFailed": "Échec de la réinitialisation du mot de passe",
"resetPasswordConfirmWipe": "L'utilisateur {{username}} ne s'est pas connecté depuis la mise à jour du chiffrement ; ses données sont donc irrécupérables. La réinitialisation effacera ses hôtes, ses identifiants et ses extraits de code. Continuer ?",
"totpSectionTitle": "Authentification à deux facteurs",
"totpStatusEnabled": "Le protocole TOTP est activé pour cet utilisateur.",
"totpStatusDisabled": "Le protocole TOTP n'est pas activé pour cet utilisateur.",
"disableTotp": "Désactiver",
"disableTotpConfirm": "Désactiver l'authentification à deux facteurs pour {{username}}? Ils pourront se connecter avec leur seul mot de passe.",
"totpDisabledSuccess": "TOTP désactivé",
"totpDisableFailed": "Échec de la désactivation du TOTP",
"manageApiKeys": "Clés API",
"apiKeyNamePlaceholder": "Nom de la clé",
"apiKeyCopyNotice": "Copiez cette clé maintenant, elle ne sera plus affichée.",
"noApiKeysForUser": "Aucune clé API",
"apiKeyDeleteFailed": "Échec de la suppression de la clé API",
"exportUserData": "Exportation de données",
"exportUserDataDesc": "Téléchargez les données d'hôtes, d'identifiants et de gestionnaire de fichiers de cet utilisateur au format JSON. Les secrets sont déchiffrés.",
"exportUserDataSuccess": "Données utilisateur exportées",
"exportUserDataFailed": "Échec de l'exportation des données utilisateur",
"hostsCount": "{{count}} hôtes",
"addHostForUser": "Ajouter un hôte",
"noHostsForUser": "Cet utilisateur n'a pas d'hôtes.",
"connectToHost": "Connecter",
"deleteHostConfirm": "Supprimer l'hôte \"{{name}}\" appartenant à {{username}}?",
"hostDeletedSuccess": "Hôte supprimé",
"hostDeleteFailed": "Échec de la suppression de l'hôte",
"credentialsCount": "{{count}} identifiants",
"addCredentialForUser": "Ajouter un identifiant",
"noCredentialsForUser": "Cet utilisateur ne possède aucun identifiant",
"deleteCredentialConfirm": "Supprimer l'identifiant «{{name}}» appartenant à {{username}}?",
"credentialDeletedSuccess": "Identifiants supprimés",
"credentialDeleteFailed": "Échec de la suppression des identifiants",
"snippetsCount": "{{count}} extraits",
"addSnippetForUser": "Ajouter un extrait",
"noSnippetsForUser": "Cet utilisateur n'a aucun extrait.",
"snippetRequiredFields": "Le nom et le contenu de l'extrait sont obligatoires.",
"snippetNamePlaceholder": "Nom de l'extrait",
"snippetContentPlaceholder": "Contenu de la commande",
"snippetFolderPlaceholder": "Dossier (facultatif)",
"snippetSaved": "Extrait enregistré",
"snippetSaveFailed": "Impossible d'enregistrer l'extrait.",
"deleteSnippetConfirm": "Supprimer l'extrait \"{{name}}\" appartenant à {{username}}?",
"snippetDeletedSuccess": "Extrait supprimé",
"snippetDeleteFailed": "Échec de la suppression de l'extrait",
"noSessionsForUser": "Aucune session active",
"deleteUserDangerDesc": "Supprimer définitivement {{username}} et toutes ses données (hôtes, identifiants, extraits, historique). Cette action est irréversible.",
"deleteUserConfirm": "Supprimer définitivement {{username}} et toutes ses données ?",
"deleteUserAdminBlocked": "Supprimez le statut d'administrateur avant de supprimer cet utilisateur.",
"createRoleRequired": "Le nom et le nom d'affichage sont obligatoires.",
"createRoleSuccess": "Rôle «{{name}}» créé",
"createRoleFailed": "Échec de la création du rôle",
@@ -2822,7 +2942,16 @@
"hostDefaultsCommandHistory": "Historique des commandes",
"hostDefaultsCommandHistoryDesc": "Suivi de l'historique des commandes sur les nouveaux hôtes par défaut",
"hostDefaultsSaved": "Paramètres par défaut de l'hôte enregistrés",
"hostDefaultsSaveFailed": "Impossible d'enregistrer les paramètres par défaut de l'hôte."
"hostDefaultsSaveFailed": "Impossible d'enregistrer les paramètres par défaut de l'hôte.",
"rolePermissions": {
"count": "{{count}} permissions",
"editAction": "Modifier les autorisations",
"loadError": "Échec du chargement du catalogue des autorisations",
"saved": "Les autorisations de rôle ont été enregistrées",
"saveError": "Impossible d'enregistrer les autorisations de rôle.",
"save": "Sauvegarder",
"saving": "Économie..."
}
},
"newUi": {
"sidebar": {
@@ -2991,6 +3120,10 @@
"executionFailed": "Échoué"
},
"userProfile": {
"donateTitle": "Maintenez Termix en vie",
"donateDescription": "Termix est gratuit, auto-hébergé et développé par une poignée de personnes sur leur temps libre. Si ce logiciel vous a permis d'économiser du temps ou de l'argent, un don en cryptomonnaie nous aidera à poursuivre son développement.",
"donateMilestones": "Vos dons nous permettent de financer le temps consacré à la recherche et à l'apprentissage des compétences nécessaires au développement de la prise en charge de SAML, Kubernetes et des agents. Suivez l'avancement du projet et faites un don.",
"donateButton": "Faire un don en cryptomonnaie",
"storageModeLocal": "Navigateur",
"storageModeCloud": "Base de données",
"storageModeDescription": "Le navigateur enregistre les paramètres uniquement dans ce navigateur. La base de données est synchronisée avec le serveur et accessible depuis n'importe quel appareil.",
@@ -3011,6 +3144,10 @@
"twoFaOn": "Sur",
"twoFaOff": "Désactivé",
"versionLabel": "Version",
"betaProgramTitle": "Programme bêta",
"betaProgramDescription": "Testez les nouvelles fonctionnalités en avant-première grâce à la balise Docker bêta (mise à jour hebdomadaire). Instable, ne convient pas à la production.",
"betaProgramFeedback": "Vous avez trouvé un bug ? Signalez-le ici.",
"betaProgramLearnMore": "Apprendre encore plus",
"deleteAccount": "Supprimer le compte",
"deleteAccountDescription": "Supprimer définitivement votre compte",
"changeServerDescription": "Passer à un autre serveur backend Termix",
+158 -21
View File
@@ -864,6 +864,21 @@
"proxmoxDockerDetectionDesc": "תבניות שמות מופרדות בפסיקים המאפשרות ל-Docker להתאים אורחים",
"proxmoxPreferredRanges": "טווחי IP מועדפים",
"proxmoxPreferredRangesDesc": "קידומות מופרדות בפסיקים בסדר עדיפות לבחירת IP כאשר לאורח יש ממשקים מרובים",
"proxmoxAutoSync": "סנכרון אוטומטי של אורחים",
"proxmoxAutoSyncDesc": "גלה מעת לעת את צומת Proxmox הזה וצור או עדכן מארחי אורחים מיובאים בזמן שההפעלה שלך אינה נעולה.",
"proxmoxSyncInterval": "מרווח סנכרון (דקות)",
"proxmoxSyncIntervalDesc": "מינימום 5 דקות. המתזמן מדלג על סשנים נעולים של משתמשים.",
"proxmoxMarkMissing": "סמן אורחים חסרים",
"proxmoxMarkMissingDesc": "הוסף תג proxmox-missing כאשר אורח שיובא בעבר נעלם במקום למחוק אותו.",
"proxmoxLastSync": "סנכרון אחרון",
"proxmoxLastSyncNever": "עדיין לא מסונכרן",
"proxmoxLastSyncNoResult": "אין פרטי תוצאה",
"proxmoxLastSyncSummary": "{{created}} נוצר, {{updated}} עודכן, {{markedMissing}} חסר, {{skipped}} דילג",
"proxmoxLastSyncStatus": {
"success": "הַצלָחָה",
"error": "נִכשָׁל",
"pending": "תָלוּי וְעוֹמֵד"
},
"proxmoxDiscoverAction": "גלה וייבא אורחי Proxmox",
"proxmoxImportTitle": "ייבוא מ-Proxmox",
"proxmoxSelectHost": "בחר מארח Proxmox…",
@@ -1167,7 +1182,57 @@
"shareHost": "שתף מארח",
"shareHostTitle": "שתף: {{name}}",
"sharing": {
"requiresCredential": "מארח זה חייב להשתמש באישור כדי לאפשר שיתוף. ערוך את המארח והקצה אישור תחילה."
"loadError": "טעינת נתוני השיתוף נכשלה. אנא נסה שוב.",
"shareWithSection": "שתף עם",
"usersTab": "משתמשים",
"rolesTab": "תפקידים",
"searchPlaceholder": "חיפוש משתמשים או תפקידים...",
"noMatches": "לא נמצאו התאמות",
"permissionLevelLabel": "רמת הרשאה",
"levels": {
"connect": {
"label": "לְחַבֵּר",
"description": "הפעלות פתוחות בלבד: טרמינל, שולחן עבודה מרוחק, מנהל קבצים, מנהרות ו-Docker. אין גישה לתצורת המארח."
},
"view": {
"label": "נוֹף",
"description": "התחבר, בנוסף ראה את תצורת המארח. סודות לעולם אינם מוצגים."
},
"edit": {
"label": "לַעֲרוֹך",
"description": "הצג ושנה את המארח. ניתן להחליף סודות אך לעולם לא לקרוא אותם; הקצאות אישורים נשארות לבעלים בלבד."
},
"manage": {
"label": "לְנַהֵל",
"description": "עריכה, בנוסף לשיתוף המארח עם אחרים, שינוי רמות הרשאה וביטול גישה."
}
},
"expiryLabel": "פקיעת גישה",
"expiry": {
"never": "לְעוֹלָם לֹא",
"oneHour": "שעה אחת",
"oneDay": "24 שעות",
"sevenDays": "7 ימים",
"thirtyDays": "30 ימים",
"custom": "מִנְהָג"
},
"customHoursPlaceholder": "שעות עד לפקיעת הגישה",
"shareButton": "לַחֲלוֹק",
"shareWithCount": "שתף ({{count}})",
"currentAccess": "גישה נוכחית",
"noAccessEntries": "מארח זה עדיין לא שותף",
"grantedBy": "הוענק על ידי",
"expires": "פג תוקף",
"expired": "פג תוקף",
"never": "לְעוֹלָם לֹא",
"revoke": "לְבַטֵל",
"accessUpdated": "הגישה עודכנה",
"accessUpdateFailed": "עדכון הגישה נכשל",
"sharedBadge": "מְשׁוּתָף",
"sharedBadgeTooltip": "משותף על ידי {{owner}} (גישה ל-{{level}})",
"viewOnlyBanner": "מארח זה משותף איתך על ידי {{owner}} עם גישת צפייה. התצורה היא לקריאה בלבד.",
"sharedEditBanner": "מארח זה משותף איתך על ידי {{owner}} עם גישת עריכה. השינויים חלים על המארח האמיתי; רק הבעלים יכול לשנות הפניות לאימות.",
"ownerOnlyControl": "רק בעל המארח יכול לשנות זאת"
},
"guac": {
"connection": "קֶשֶׁר",
@@ -1302,28 +1367,10 @@
"backspaceKey": "מקש Backspace",
"saveHostFirst": "שמור את המארח קודם.",
"sharingOptionsAfterSave": "אפשרויות שיתוף זמינות לאחר שמירת המארח.",
"sharingLoadError": "טעינת נתוני השיתוף נכשלה. בדוק את החיבור ונסה שוב.",
"shareHostSection": "שתף מארח",
"shareWithUser": "שתף עם משתמש",
"shareWithRole": "שתף עם תפקיד",
"selectUser": "בחר משתמש",
"selectRole": "בחר תפקיד",
"selectUserOption": "בחר משתמש...",
"selectRoleOption": "בחר תפקיד...",
"permissionLevel": "רמת הרשאה",
"expiresInHours": "פג תוקף בעוד (שעות)",
"noExpiryPlaceholder": "השאר ריק ללא תפוגה",
"shareBtn": "לַחֲלוֹק",
"currentAccess": "גישה נוכחית",
"typeHeader": "סוּג",
"targetHeader": "יַעַד",
"permissionHeader": "רְשׁוּת",
"grantedByHeader": "הוענק על ידי",
"expiresHeader": "פג תוקף",
"noAccessEntries": "עדיין אין רשומות גישה.",
"expiredLabel": "פג תוקף",
"neverLabel": "לְעוֹלָם לֹא",
"revokeBtn": "לְבַטֵל",
"cancelBtn": "לְבַטֵל",
"savingBtn": "חִסָכוֹן...",
"updateHostBtn": "עדכון מארח",
@@ -2190,7 +2237,8 @@
"passwordLoginDisabledDesc": "כניסה באמצעות סיסמה מושבתת. השתמש בסיסמה או בספק אימות חיצוני.",
"signInWithPasskey": "כניסה עם סיסמה",
"passkeyLoginFailed": "כניסה עם סיסמה נכשלה",
"attemptsRemaining": "{{count}} ניסיונות נותרו"
"attemptsRemaining": "{{count}} ניסיונות נותרו",
"confirmResetDataWipe": "חשבון זה לא התחבר מאז שדרוג ההצפנה, כך שלא ניתן לשחזר את הנתונים המאוחסנים בו ללא הסיסמה הישנה. איפוס ימחק לצמיתות את המארחים, האישורים והקטעים שלו. האם להמשיך?"
},
"hostKey": {
"verifyNewHost": "אימות מפתח מארח SSH",
@@ -2323,6 +2371,13 @@
"serviceLinks": "קישורי שירות",
"homepagePreview": "תצוגה מקדימה של דף הבית"
},
"donation": {
"title": "נהנים מ-Termix?",
"body": "Termix הוא כלי חינמי וקוד פתוח, שנבנה ומתוחזק על ידי צוות של שני אנשים בזמננו הפנוי. אם הוא מחליף כלי מסחרי שהייתם משלמים עליו אחרת, תרומה מסייעת לכסות את עלויות האירוח ולשמור על הפיתוח מתמשך. נכון לעכשיו, תרומות הן קריפטו בלבד.",
"milestones": "תרומות גם עוזרות לממן את הזמן הנדרש למחקר וללמידה של מה שנדרש כדי לבנות תכונות כמו SAML ותמיכה ב-Kubernetes. ראו את ההתקדמות בדף התרומות.",
"cta": "לִתְרוֹם",
"dismiss": "אולי מאוחר יותר"
},
"dashboardTab": {
"stable": "יַצִיב",
"hostsOnline": "מארחים אונליין",
@@ -2790,6 +2845,71 @@
"updateAdminStatusFailed": "נכשל עדכון סטטוס מנהל המערכת",
"allSessionsRevoked": "כל הפעילויות בוטלו",
"revokeSessionsFailed": "נכשלה ביטול ההפעלות",
"manageUserData": "ניהול נתוני משתמש",
"backToUsers": "חזרה למשתמשים",
"manageTabAccount": "חֶשְׁבּוֹן",
"manageTabHosts": "מארחים",
"manageTabCredentials": "אישורים",
"manageTabSnippets": "קטעי טקסט",
"manageTabSessions": "מפגשים",
"manageTabDanger": "סַכָּנָה",
"manageEditorBack": "חזרה לנתונים של {{username}}",
"dataLockedBadge": "נָעוּל",
"dataLockedNotice": "הנתונים של {{username}}נשארים נעולים עד לכניסה הבאה שלהם. לא ניתן יהיה לצפות או לערוך את המארחים, האישורים והקטעים שלהם עד אז.",
"resetPasswordTitle": "איפוס סיסמה",
"resetPasswordOidcOnly": "משתמש זה מתחבר דרך ספק חיצוני ואין לו סיסמה.",
"resetPasswordPlaceholder": "סיסמה חדשה",
"resetPasswordBtn": "אִתחוּל",
"resetPasswordWorking": "איפוס...",
"resetPasswordSuccess": "איפוס סיסמה",
"resetPasswordSuccessWiped": "איפוס סיסמה. הנתונים המוצפנים של המשתמש נמחקו.",
"resetPasswordFailed": "איפוס הסיסמה נכשל",
"resetPasswordConfirmWipe": "{{username}} לא התחבר מאז שדרוג ההצפנה, כך שלא ניתן לשחזר את הנתונים שלו. איפוס כעת ימחק את המארחים, האישורים והקטעים שלו. להמשיך?",
"totpSectionTitle": "אימות דו-שלבי",
"totpStatusEnabled": "TOTP מופעל עבור משתמש זה",
"totpStatusDisabled": "TOTP אינו מופעל עבור משתמש זה",
"disableTotp": "השבת",
"disableTotpConfirm": "להשבית אימות דו-שלבי עבור {{username}}? הם יוכלו להתחבר רק עם הסיסמה שלהם.",
"totpDisabledSuccess": "TOTP מושבת",
"totpDisableFailed": "נכשל בהשבתת TOTP",
"manageApiKeys": "מפתחות API",
"apiKeyNamePlaceholder": "שם המפתח",
"apiKeyCopyNotice": "העתק את המפתח הזה עכשיו, הוא לא יוצג שוב.",
"noApiKeysForUser": "אין מפתחות API",
"apiKeyDeleteFailed": "נכשלה מחיקת מפתח ה-API",
"exportUserData": "ייצוא נתונים",
"exportUserDataDesc": "הורד את המארחים, האישורים ונתוני מנהל הקבצים של משתמש זה כ-JSON. הסודות מפוענחים.",
"exportUserDataSuccess": "נתוני משתמש יוצאו",
"exportUserDataFailed": "ייצוא נתוני המשתמש נכשל",
"hostsCount": "{{count}} hosts",
"addHostForUser": "הוסף מארח",
"noHostsForUser": "למשתמש זה אין מארחים",
"connectToHost": "לְחַבֵּר",
"deleteHostConfirm": "למחוק את המארח \"{{name}}\" השייך ל- {{username}}?",
"hostDeletedSuccess": "המארח נמחק",
"hostDeleteFailed": "מחיקת המארח נכשלה",
"credentialsCount": "אישורים {{count}}",
"addCredentialForUser": "הוסף אישור",
"noCredentialsForUser": "למשתמש זה אין אישורים",
"deleteCredentialConfirm": "למחוק את פרטי הכניסה \"{{name}}\" השייכים ל- {{username}}?",
"credentialDeletedSuccess": "פרטי הכניסה נמחקו",
"credentialDeleteFailed": "מחיקת האישורים נכשלה",
"snippetsCount": "קטעי טקסט {{count}}",
"addSnippetForUser": "הוסף קטע",
"noSnippetsForUser": "למשתמש זה אין קטעי טקסט",
"snippetRequiredFields": "שם ותוכן הקטע נדרשים",
"snippetNamePlaceholder": "שם קטע",
"snippetContentPlaceholder": "תוכן הפקודה",
"snippetFolderPlaceholder": "תיקייה (אופציונלי)",
"snippetSaved": "קטע הטקסט נשמר",
"snippetSaveFailed": "שמירת קטע הקוד נכשלה",
"deleteSnippetConfirm": "למחוק את קטע הקוד \"{{name}}\" השייך ל- {{username}}?",
"snippetDeletedSuccess": "קטע הטקסט נמחק",
"snippetDeleteFailed": "מחיקת קטע הקוד נכשלה",
"noSessionsForUser": "אין סשנים פעילים",
"deleteUserDangerDesc": "מחק לצמיתות את {{username}} ואת כל הנתונים שלו (מארחים, אישורים, קטעי קוד, היסטוריה). לא ניתן לבטל פעולה זו.",
"deleteUserConfirm": "למחוק לצמיתות את {{username}} ואת כל הנתונים שלו?",
"deleteUserAdminBlocked": "הסר את סטטוס המנהל לפני מחיקת משתמש זה.",
"createRoleRequired": "חובה להזין שם ושם תצוגה",
"createRoleSuccess": "התפקיד \"{{name}}\" נוצר",
"createRoleFailed": "יצירת התפקיד נכשלה",
@@ -2822,7 +2942,16 @@
"hostDefaultsCommandHistory": "היסטוריית פקודות",
"hostDefaultsCommandHistoryDesc": "מעקב אחר היסטוריית פקודות במחשבים חדשים כברירת מחדל",
"hostDefaultsSaved": "ברירות מחדל של המארח נשמרו",
"hostDefaultsSaveFailed": "נכשלה שמירת ברירות המחדל של המארח"
"hostDefaultsSaveFailed": "נכשלה שמירת ברירות המחדל של המארח",
"rolePermissions": {
"count": "הרשאות {{count}}",
"editAction": "עריכת הרשאות",
"loadError": "נכשלה טעינת קטלוג ההרשאות",
"saved": "הרשאות התפקיד נשמרו",
"saveError": "שמירת הרשאות התפקיד נכשלה",
"save": "לְהַצִיל",
"saving": "חִסָכוֹן..."
}
},
"newUi": {
"sidebar": {
@@ -2991,6 +3120,10 @@
"executionFailed": "נִכשָׁל"
},
"userProfile": {
"donateTitle": "לשמור על טרמיקס בחיים",
"donateDescription": "Termix הוא שירות חינמי, מתארח בעצמו, ונבנה על ידי מספר אנשים בלבד בזמנם הפנוי. אם זה חסך לכם זמן או כסף, תרומת קריפטו תעזור לשמור עליו.",
"donateMilestones": "תרומות עוזרות לממן את הזמן הנדרש למחקר וללמוד מה נדרש כדי לבנות תמיכה ב-SAML, Kubernetes ו-Agent. צפו בהתקדמות ותרמו.",
"donateButton": "תרום קריפטו",
"storageModeLocal": "דפדפן",
"storageModeCloud": "מסד נתונים",
"storageModeDescription": "הדפדפן שומר הגדרות בדפדפן זה בלבד. מסד הנתונים מסתנכרן עם השרת ונטען בכל מכשיר.",
@@ -3011,6 +3144,10 @@
"twoFaOn": "עַל",
"twoFaOff": "כבוי",
"versionLabel": "גִרְסָה",
"betaProgramTitle": "תוכנית בטא",
"betaProgramDescription": "נסה תכונות עתידיות מוקדם עם תגית Docker המתגלגלת השבועית :beta. לא יציב, לא מיועד לייצור.",
"betaProgramFeedback": "מצאת באג? דווח על זה כאן.",
"betaProgramLearnMore": "למידע נוסף",
"deleteAccount": "מחיקת חשבון",
"deleteAccountDescription": "מחיקת החשבון שלך לצמיתות",
"changeServerDescription": "מעבר לשרת אחורי אחר של Termix",
+158 -21
View File
@@ -864,6 +864,21 @@
"proxmoxDockerDetectionDesc": "कॉमा-सेपरेटेड नाम पैटर्न जो डॉकर को गेस्ट्स से मिलान करने में सक्षम बनाते हैं",
"proxmoxPreferredRanges": "पसंदीदा आईपी रेंज",
"proxmoxPreferredRangesDesc": "अतिथि कंप्यूटर में एकाधिक इंटरफेस होने पर आईपी चयन के लिए अल्पविराम से अलग किए गए उपसर्गों को प्राथमिकता क्रम में रखा जाता है।",
"proxmoxAutoSync": "अतिथियों के साथ स्वतः सिंक",
"proxmoxAutoSyncDesc": "समय-समय पर इस प्रॉक्समॉक्स नोड का पता लगाएं और जब आपका सत्र अनलॉक हो, तब आयातित अतिथि होस्ट बनाएं या अपडेट करें।",
"proxmoxSyncInterval": "सिंक्रोनाइज़ेशन अंतराल (मिनटों में)",
"proxmoxSyncIntervalDesc": "कम से कम 5 मिनट। शेड्यूलर लॉक किए गए उपयोगकर्ता सत्रों को छोड़ देता है।",
"proxmoxMarkMissing": "अनुपस्थित मेहमानों को चिह्नित करें",
"proxmoxMarkMissingDesc": "पहले से आयात किए गए गेस्ट के गायब हो जाने पर, उसे हटाने के बजाय, एक proxmox-missing टैग जोड़ें।",
"proxmoxLastSync": "अंतिम सिंक",
"proxmoxLastSyncNever": "अभी तक सिंक नहीं हुआ है",
"proxmoxLastSyncNoResult": "कोई परिणाम विवरण उपलब्ध नहीं है",
"proxmoxLastSyncSummary": "{{created}} बनाया गया, {{updated}} अपडेट किया गया, {{markedMissing}} अनुपस्थित, {{skipped}} छोड़ा गया",
"proxmoxLastSyncStatus": {
"success": "सफलता",
"error": "असफल",
"pending": "लंबित"
},
"proxmoxDiscoverAction": "Proxmox गेस्ट्स को खोजें और इंपोर्ट करें",
"proxmoxImportTitle": "Proxmox से आयात करें",
"proxmoxSelectHost": "एक Proxmox होस्ट चुनें…",
@@ -1167,7 +1182,57 @@
"shareHost": "होस्ट साझा करें",
"shareHostTitle": "शेयर करें: {{name}}",
"sharing": {
"requiresCredential": "इस होस्ट को शेयरिंग सक्षम करने के लिए क्रेडेंशियल का उपयोग करना होगा। पहले होस्ट को संपादित करें और एक क्रेडेंशियल असाइन करें।"
"loadError": "शेयरिंग डेटा लोड करने में विफल। कृपया पुनः प्रयास करें।",
"shareWithSection": "के साथ शेयर करें",
"usersTab": "उपयोगकर्ताओं",
"rolesTab": "भूमिकाएँ",
"searchPlaceholder": "उपयोगकर्ताओं या भूमिकाओं को खोजें...",
"noMatches": "कोई मेल नहीं मिले",
"permissionLevelLabel": "अनुमति स्तर",
"levels": {
"connect": {
"label": "जोड़ना",
"description": "केवल टर्मिनल, रिमोट डेस्कटॉप, फ़ाइल मैनेजर, टनल और डॉकर के लिए ही ओपन सेशन उपलब्ध हैं। होस्ट कॉन्फ़िगरेशन तक कोई पहुंच नहीं है।"
},
"view": {
"label": "देखना",
"description": "कनेक्ट करें और होस्ट कॉन्फ़िगरेशन देखें। गुप्त जानकारी कभी प्रदर्शित नहीं की जाती है।"
},
"edit": {
"label": "संपादन करना",
"description": "होस्ट को देखें और संशोधित करें। गुप्त जानकारी को बदला जा सकता है लेकिन पढ़ा नहीं जा सकता; क्रेडेंशियल असाइनमेंट केवल मालिक के पास ही रहते हैं।"
},
"manage": {
"label": "प्रबंधित करना",
"description": "होस्ट को संपादित करें, साथ ही दूसरों के साथ साझा करें, अनुमति स्तर बदलें और पहुंच रद्द करें।"
}
},
"expiryLabel": "पहुँच समाप्ति",
"expiry": {
"never": "कभी नहीं",
"oneHour": "1 घंटा",
"oneDay": "24 घंटे",
"sevenDays": "7 दिन",
"thirtyDays": "30 दिन",
"custom": "रिवाज़"
},
"customHoursPlaceholder": "पहुँच समाप्त होने में लगने वाले घंटे",
"shareButton": "शेयर करना",
"shareWithCount": "शेयर ({{count}})",
"currentAccess": "वर्तमान पहुंच",
"noAccessEntries": "इस होस्ट को अभी तक साझा नहीं किया गया है",
"grantedBy": "अनुमती देना",
"expires": "समय-सीमा समाप्त",
"expired": "खत्म हो चुका",
"never": "कभी नहीं",
"revoke": "रद्द करना",
"accessUpdated": "अपडेट एक्सेस करें",
"accessUpdateFailed": "एक्सेस अपडेट करने में विफल",
"sharedBadge": "साझा",
"sharedBadgeTooltip": "{{owner}} द्वारा साझा किया गया ({{level}} पहुंच)",
"viewOnlyBanner": "यह होस्ट {{owner}} द्वारा आपके साथ साझा किया गया है और इसे देखने की अनुमति है। कॉन्फ़िगरेशन केवल पढ़ने योग्य है।",
"sharedEditBanner": "यह होस्ट {{owner}} द्वारा संपादन अनुमति के साथ आपके साथ साझा किया गया है। परिवर्तन वास्तविक होस्ट पर लागू होते हैं; प्रमाणीकरण संदर्भों को केवल स्वामी द्वारा ही बदला जा सकता है।",
"ownerOnlyControl": "इसे केवल होस्ट का मालिक ही बदल सकता है।"
},
"guac": {
"connection": "संबंध",
@@ -1302,28 +1367,10 @@
"backspaceKey": "बैकस्पेस कुंजी",
"saveHostFirst": "पहले होस्ट को सेव करें।",
"sharingOptionsAfterSave": "होस्ट को सेव करने के बाद शेयरिंग के विकल्प उपलब्ध हो जाते हैं।",
"sharingLoadError": "शेयरिंग डेटा लोड करने में विफल। कृपया अपना कनेक्शन जांचें और पुनः प्रयास करें।",
"shareHostSection": "होस्ट साझा करें",
"shareWithUser": "उपयोगकर्ता के साथ साझा करें",
"shareWithRole": "भूमिका के साथ साझा करें",
"selectUser": "उपयोगकर्ता का चयन करें",
"selectRole": "भूमिका का चयन करें",
"selectUserOption": "किसी उपयोगकर्ता का चयन करें...",
"selectRoleOption": "एक भूमिका चुनें...",
"permissionLevel": "अनुमति स्तर",
"expiresInHours": "समाप्ति तिथि (घंटे)",
"noExpiryPlaceholder": "समाप्ति तिथि से पहले खाली छोड़ दें",
"shareBtn": "शेयर करना",
"currentAccess": "वर्तमान पहुंच",
"typeHeader": "प्रकार",
"targetHeader": "लक्ष्य",
"permissionHeader": "अनुमति",
"grantedByHeader": "अनुमती देना",
"expiresHeader": "समय-सीमा समाप्त",
"noAccessEntries": "अभी तक प्रवेश के लिए कोई प्रविष्टियाँ नहीं हैं।",
"expiredLabel": "खत्म हो चुका",
"neverLabel": "कभी नहीं",
"revokeBtn": "रद्द करना",
"cancelBtn": "रद्द करना",
"savingBtn": "सहेजा जा रहा है...",
"updateHostBtn": "होस्ट को अपडेट करें",
@@ -2190,7 +2237,8 @@
"passwordLoginDisabledDesc": "पासवर्ड से लॉगिन करना अक्षम है। कृपया पासकी या किसी बाहरी प्रमाणीकरण प्रदाता का उपयोग करें।",
"signInWithPasskey": "पासकी से लॉग इन करें",
"passkeyLoginFailed": "पासकी लॉगिन विफल",
"attemptsRemaining": "{{count}} attempts remaining"
"attemptsRemaining": "{{count}} attempts remaining",
"confirmResetDataWipe": "इस खाते ने एन्क्रिप्शन अपग्रेड के बाद से लॉग इन नहीं किया है, इसलिए पुराने पासवर्ड के बिना इसका संग्रहित डेटा पुनर्प्राप्त नहीं किया जा सकता है। रीसेट करने से इसके होस्ट, क्रेडेंशियल और स्निपेट स्थायी रूप से हटा दिए जाएंगे। जारी रखें?"
},
"hostKey": {
"verifyNewHost": "SSH होस्ट कुंजी सत्यापित करें",
@@ -2323,6 +2371,13 @@
"serviceLinks": "सेवा लिंक",
"homepagePreview": "होमपेज का पूर्वावलोकन"
},
"donation": {
"title": "क्या आप टर्मिक्स का आनंद ले रहे हैं?",
"body": "टर्मिक्स एक मुफ़्त और ओपन सोर्स सॉफ़्टवेयर है, जिसे दो लोगों की टीम ने अपने खाली समय में बनाया और रखरखाव किया है। अगर इसने आपके किसी ऐसे व्यावसायिक टूल की जगह ले ली है जिसके लिए आप भुगतान कर रहे होते, तो आपका दान होस्टिंग लागत को कवर करने और विकास को जारी रखने में मदद करता है। फिलहाल, दान केवल क्रिप्टोकरेंसी में ही स्वीकार किए जाते हैं।",
"milestones": "दान से SAML और Kubernetes जैसी सुविधाओं को विकसित करने के लिए आवश्यक शोध और सीखने में लगने वाले समय को भी वित्त पोषित करने में मदद मिलती है। प्रगति देखने के लिए दान पृष्ठ पर जाएँ।",
"cta": "दान करें",
"dismiss": "शायद बाद में"
},
"dashboardTab": {
"stable": "स्थिर",
"hostsOnline": "ऑनलाइन होस्ट",
@@ -2790,6 +2845,71 @@
"updateAdminStatusFailed": "व्यवस्थापक की स्थिति अपडेट करने में विफल",
"allSessionsRevoked": "सभी सत्र रद्द कर दिए गए",
"revokeSessionsFailed": "सत्र रद्द करने में विफल",
"manageUserData": "उपयोगकर्ता डेटा प्रबंधित करें",
"backToUsers": "उपयोगकर्ताओं पर वापस जाएँ",
"manageTabAccount": "खाता",
"manageTabHosts": "मेजबान",
"manageTabCredentials": "साख",
"manageTabSnippets": "स्निपेट्स",
"manageTabSessions": "सत्रों",
"manageTabDanger": "खतरा",
"manageEditorBack": "{{username}}के डेटा पर वापस जाएँ",
"dataLockedBadge": "बंद",
"dataLockedNotice": "{{username}}का डेटा तब तक लॉक रहता है जब तक वे अगली बार लॉग इन नहीं करते। तब तक उनके होस्ट, क्रेडेंशियल और स्निपेट देखे या संपादित नहीं किए जा सकते।",
"resetPasswordTitle": "पासवर्ड रीसेट",
"resetPasswordOidcOnly": "यह उपयोगकर्ता किसी बाहरी प्रदाता के माध्यम से लॉग इन करता है और उसके पास कोई पासवर्ड नहीं है।",
"resetPasswordPlaceholder": "नया पासवर्ड",
"resetPasswordBtn": "रीसेट करें",
"resetPasswordWorking": "रीसेट हो रहा है...",
"resetPasswordSuccess": "पासवर्ड रीसेट",
"resetPasswordSuccessWiped": "पासवर्ड रीसेट कर दिया गया है। उपयोगकर्ता का एन्क्रिप्टेड डेटा मिटा दिया गया है।",
"resetPasswordFailed": "पासवर्ड रीसेट करने में विफल",
"resetPasswordConfirmWipe": "{{username}} ने एन्क्रिप्शन अपग्रेड के बाद से लॉग इन नहीं किया है, इसलिए उनका डेटा रिकवर नहीं किया जा सकता है। अभी रीसेट करने से उनके होस्ट, क्रेडेंशियल और स्निपेट डिलीट हो जाएंगे। जारी रखें?",
"totpSectionTitle": "दो-कारक प्रमाणीकरण",
"totpStatusEnabled": "इस उपयोगकर्ता के लिए TOTP सक्षम है।",
"totpStatusDisabled": "इस उपयोगकर्ता के लिए TOTP सक्षम नहीं है।",
"disableTotp": "अक्षम करना",
"disableTotpConfirm": "{{username}}के लिए दो-कारक प्रमाणीकरण अक्षम करें? वे केवल अपने पासवर्ड से लॉग इन कर सकेंगे।",
"totpDisabledSuccess": "टीओटीपी अक्षम",
"totpDisableFailed": "TOTP को निष्क्रिय करने में विफल",
"manageApiKeys": "एपीआई कुंजी",
"apiKeyNamePlaceholder": "कुंजी नाम",
"apiKeyCopyNotice": "इस कुंजी को अभी कॉपी कर लें, यह दोबारा नहीं दिखाई देगी।",
"noApiKeysForUser": "कोई एपीआई कुंजी नहीं",
"apiKeyDeleteFailed": "एपीआई कुंजी को हटाने में विफल रहा",
"exportUserData": "डेटा निर्यात",
"exportUserDataDesc": "इस उपयोगकर्ता के होस्ट, क्रेडेंशियल और फ़ाइल मैनेजर डेटा को JSON फॉर्मेट में डाउनलोड करें। गुप्त जानकारी को डिक्रिप्ट कर दिया गया है।",
"exportUserDataSuccess": "उपयोगकर्ता डेटा निर्यात किया गया",
"exportUserDataFailed": "उपयोगकर्ता डेटा निर्यात करने में विफल",
"hostsCount": "{{count}} मेजबान",
"addHostForUser": "होस्ट जोड़ें",
"noHostsForUser": "इस उपयोगकर्ता के पास कोई होस्ट नहीं है",
"connectToHost": "जोड़ना",
"deleteHostConfirm": "{{username}} से संबंधित होस्ट \"{{name}}\" को हटाएँ?",
"hostDeletedSuccess": "होस्ट हटा दिया गया",
"hostDeleteFailed": "होस्ट को हटाने में विफल",
"credentialsCount": "{{count}} क्रेडेंशियल",
"addCredentialForUser": "क्रेडेंशियल जोड़ें",
"noCredentialsForUser": "इस उपयोगकर्ता के पास कोई क्रेडेंशियल नहीं है",
"deleteCredentialConfirm": "{{username}} से संबंधित क्रेडेंशियल \"{{name}}\" को हटाएँ?",
"credentialDeletedSuccess": "क्रेडेंशियल हटा दिया गया",
"credentialDeleteFailed": "क्रेडेंशियल हटाने में विफल",
"snippetsCount": "{{count}} स्निपेट्स",
"addSnippetForUser": "स्निपेट जोड़ें",
"noSnippetsForUser": "इस उपयोगकर्ता के पास कोई स्निपेट नहीं है",
"snippetRequiredFields": "स्निपेट का नाम और सामग्री आवश्यक हैं",
"snippetNamePlaceholder": "स्निपेट नाम",
"snippetContentPlaceholder": "कमांड सामग्री",
"snippetFolderPlaceholder": "फ़ोल्डर (वैकल्पिक)",
"snippetSaved": "अंश सहेजा गया",
"snippetSaveFailed": "स्निपेट सहेजने में विफल",
"deleteSnippetConfirm": "{{username}} से संबंधित अंश \"{{name}}\" को हटाएँ?",
"snippetDeletedSuccess": "अंश हटा दिया गया",
"snippetDeleteFailed": "स्निपेट को हटाने में विफल",
"noSessionsForUser": "कोई सक्रिय सत्र नहीं",
"deleteUserDangerDesc": "{{username}} और उनके सभी डेटा (होस्ट, क्रेडेंशियल, स्निपेट, इतिहास) को स्थायी रूप से हटा दें। इसे पूर्ववत नहीं किया जा सकता है।",
"deleteUserConfirm": "{{username}} और उनके सभी डेटा को स्थायी रूप से हटा दें?",
"deleteUserAdminBlocked": "इस उपयोगकर्ता को हटाने से पहले उसका व्यवस्थापक दर्जा हटा दें।",
"createRoleRequired": "नाम और डिस्प्ले नाम आवश्यक हैं",
"createRoleSuccess": "भूमिका \"{{name}}\" बनाई गई",
"createRoleFailed": "भूमिका बनाने में विफल",
@@ -2822,7 +2942,16 @@
"hostDefaultsCommandHistory": "कमान का इतिहास",
"hostDefaultsCommandHistoryDesc": "नए होस्ट पर कमांड इतिहास को डिफ़ॉल्ट रूप से ट्रैक करें",
"hostDefaultsSaved": "होस्ट डिफ़ॉल्ट सहेजे गए",
"hostDefaultsSaveFailed": "होस्ट डिफ़ॉल्ट सहेजने में विफल"
"hostDefaultsSaveFailed": "होस्ट डिफ़ॉल्ट सहेजने में विफल",
"rolePermissions": {
"count": "{{count}} अनुमतियाँ",
"editAction": "संपादन अनुमतियाँ",
"loadError": "अनुमतियों की सूची लोड करने में विफल।",
"saved": "भूमिका अनुमतियाँ सहेजी गईं",
"saveError": "भूमिका अनुमतियों को सहेजने में विफल",
"save": "बचाना",
"saving": "सहेजा जा रहा है..."
}
},
"newUi": {
"sidebar": {
@@ -2991,6 +3120,10 @@
"executionFailed": "असफल"
},
"userProfile": {
"donateTitle": "टर्मिक्स को जीवित रखें",
"donateDescription": "टर्मिक्स मुफ़्त है, सेल्फ-होस्टेड है और इसे कुछ ही लोगों ने अपने खाली समय में बनाया है। अगर इससे आपका समय या पैसा बचा है, तो क्रिप्टोकरेंसी में दान करने से इसे चालू रखने में मदद मिलेगी।",
"donateMilestones": "दान से SAML, Kubernetes और एजेंट सपोर्ट बनाने के लिए आवश्यक शोध और सीखने में लगने वाले समय को वित्त पोषित करने में मदद मिलती है। प्रगति देखें और दान करें।",
"donateButton": "क्रिप्टोकरेंसी दान करें",
"storageModeLocal": "ब्राउज़र",
"storageModeCloud": "डेटाबेस",
"storageModeDescription": "ब्राउज़र सेटिंग्स को केवल इसी ब्राउज़र में स्टोर करता है। डेटाबेस सर्वर से सिंक्रोनाइज़ होता है और किसी भी डिवाइस पर लोड हो जाता है।",
@@ -3011,6 +3144,10 @@
"twoFaOn": "पर",
"twoFaOff": "बंद",
"versionLabel": "संस्करण",
"betaProgramTitle": "बीटा प्रोग्राम",
"betaProgramDescription": "साप्ताहिक रूप से जारी होने वाले :beta Docker टैग के साथ आगामी सुविधाओं को पहले ही आजमाएं। यह अस्थिर है और उत्पादन के लिए उपयुक्त नहीं है।",
"betaProgramFeedback": "कोई बग मिला? इसकी रिपोर्ट यहां करें।",
"betaProgramLearnMore": "और अधिक जानें",
"deleteAccount": "खाता हटा दो",
"deleteAccountDescription": "अपना खाता स्थायी रूप से हटाएँ",
"changeServerDescription": "किसी दूसरे टर्मिक्स बैकएंड सर्वर पर स्विच करें",
+158 -21
View File
@@ -864,6 +864,21 @@
"proxmoxDockerDetectionDesc": "Vesszővel elválasztott névminták, amelyek lehetővé teszik a Docker számára a vendégek egyezését",
"proxmoxPreferredRanges": "Előnyben részesített IP-tartományok",
"proxmoxPreferredRangesDesc": "Vesszővel elválasztott előtagok prioritási sorrendben IP-cím kiválasztásához, ha egy vendégnek több interfésze van",
"proxmoxAutoSync": "Vendégek automatikus szinkronizálása",
"proxmoxAutoSyncDesc": "Rendszeresen fedezze fel ezt a Proxmox csomópontot, és hozzon létre vagy frissítsen importált vendéggazdagépeket, miközben a munkamenete fel van oldva.",
"proxmoxSyncInterval": "Szinkronizálási időköz (perc)",
"proxmoxSyncIntervalDesc": "Minimum 5 perc. Az ütemező kihagyja a zárolt felhasználói munkameneteket.",
"proxmoxMarkMissing": "Jelöld meg az eltűnt vendégeket",
"proxmoxMarkMissingDesc": "Hozzáad egy proxmox-missing címkét, amikor egy korábban importált vendég eltűnik, ahelyett, hogy törölné azt.",
"proxmoxLastSync": "Utolsó szinkronizálás",
"proxmoxLastSyncNever": "Még nincs szinkronizálva",
"proxmoxLastSyncNoResult": "Nincsenek találati részletek",
"proxmoxLastSyncSummary": "{{created}} létrehozva, {{updated}} frissítve, {{markedMissing}} hiányzik, {{skipped}} kihagyva",
"proxmoxLastSyncStatus": {
"success": "Siker",
"error": "Sikertelen",
"pending": "Függőben levő"
},
"proxmoxDiscoverAction": "Proxmox vendégek felfedezése és importálása",
"proxmoxImportTitle": "Importálás a Proxmoxból",
"proxmoxSelectHost": "Válasszon egy Proxmox hosztot…",
@@ -1167,7 +1182,57 @@
"shareHost": "Megosztási házigazda",
"shareHostTitle": "Megosztás: {{name}}",
"sharing": {
"requiresCredential": "Ennek a gazdagépnek hitelesítő adatot kell használnia a megosztás engedélyezéséhez. Először szerkessze a gazdagépet, és rendeljen hozzá egy hitelesítő adatot."
"loadError": "Nem sikerült betölteni a megosztási adatokat. Próbáld újra.",
"shareWithSection": "Megosztás",
"usersTab": "Felhasználók",
"rolesTab": "Szerepkörök",
"searchPlaceholder": "Felhasználók vagy szerepkörök keresése...",
"noMatches": "Nincs találat",
"permissionLevelLabel": "Jogosultsági szint",
"levels": {
"connect": {
"label": "Csatlakozás",
"description": "Csak nyitott munkamenetek: terminál, távoli asztal, fájlkezelő, alagutak és Docker. Nincs hozzáférés a gazdagép konfigurációjához."
},
"view": {
"label": "Kilátás",
"description": "Csatlakozás, valamint a gazdagép konfigurációjának megtekintése. A titkos kódok soha nem jelennek meg."
},
"edit": {
"label": "Szerkesztés",
"description": "A gazdagép megtekintése és módosítása. A titkos kódok lecserélhetők, de soha nem olvashatók; a hitelesítő adatok hozzárendelése csak a tulajdonos számára marad."
},
"manage": {
"label": "Kezelés",
"description": "Szerkeszd, és oszd meg a gazdagépet másokkal, módosítsd a jogosultsági szinteket és vond vissza a hozzáférést."
}
},
"expiryLabel": "Hozzáférés lejárata",
"expiry": {
"never": "Soha",
"oneHour": "1 óra",
"oneDay": "24 óra",
"sevenDays": "7 nap",
"thirtyDays": "30 nap",
"custom": "Szokás"
},
"customHoursPlaceholder": "Órák a hozzáférés lejártáig",
"shareButton": "Részesedés",
"shareWithCount": "Megosztás ({{count}})",
"currentAccess": "Jelenlegi hozzáférés",
"noAccessEntries": "Ez a hoszt még nincs megosztva",
"grantedBy": "Megadta",
"expires": "Lejár",
"expired": "Lejárt",
"never": "Soha",
"revoke": "Visszavonás",
"accessUpdated": "Hozzáférés frissítve",
"accessUpdateFailed": "Nem sikerült frissíteni a hozzáférést",
"sharedBadge": "Megosztott",
"sharedBadgeTooltip": "Megosztotta {{owner}} ({{level}} hozzáférés)",
"viewOnlyBanner": "Ezt a hosztot {{owner}} osztotta meg veled megtekintési hozzáféréssel. A konfiguráció csak olvasható.",
"sharedEditBanner": "Ezt a hosztot {{owner}} osztotta meg veled szerkesztési hozzáféréssel. A változtatások a valódi hosztra vonatkoznak; a hitelesítési hivatkozásokat csak a tulajdonos módosíthatja.",
"ownerOnlyControl": "Csak a tárhely tulajdonosa módosíthatja ezt"
},
"guac": {
"connection": "Kapcsolat",
@@ -1302,28 +1367,10 @@
"backspaceKey": "Visszatörlés billentyű",
"saveHostFirst": "Először mentsd el a gazdagépet.",
"sharingOptionsAfterSave": "A megosztási beállítások a gazdagép mentése után érhetők el.",
"sharingLoadError": "Nem sikerült betölteni a megosztási adatokat. Ellenőrizd a kapcsolatot, és próbáld újra.",
"shareHostSection": "Megosztási házigazda",
"shareWithUser": "Megosztás a felhasználóval",
"shareWithRole": "Megosztás szerepkörrel",
"selectUser": "Felhasználó kiválasztása",
"selectRole": "Szerepkör kiválasztása",
"selectUserOption": "Válasszon ki egy felhasználót...",
"selectRoleOption": "Válasszon egy szerepet...",
"permissionLevel": "Engedélyszint",
"expiresInHours": "Lejár (órán belül)",
"noExpiryPlaceholder": "Hagyja üresen, ha nincs lejárati idő",
"shareBtn": "Részesedés",
"currentAccess": "Jelenlegi hozzáférés",
"typeHeader": "Típus",
"targetHeader": "Cél",
"permissionHeader": "Engedély",
"grantedByHeader": "Megadta",
"expiresHeader": "Lejár",
"noAccessEntries": "Még nincsenek hozzáférési bejegyzések.",
"expiredLabel": "Lejárt",
"neverLabel": "Soha",
"revokeBtn": "Visszavonás",
"cancelBtn": "Mégsem",
"savingBtn": "Megtakarítás...",
"updateHostBtn": "Frissítse a gazdagépet",
@@ -2190,7 +2237,8 @@
"passwordLoginDisabledDesc": "A jelszóval történő bejelentkezés le van tiltva. Használjon jelszót vagy külső hitelesítési szolgáltatót.",
"signInWithPasskey": "Bejelentkezés jelszóval",
"passkeyLoginFailed": "Jelszóval történő bejelentkezés sikertelen",
"attemptsRemaining": "{{count}} próbálkozás van hátra"
"attemptsRemaining": "{{count}} próbálkozás van hátra",
"confirmResetDataWipe": "Ez a fiók a titkosítás frissítése óta nem jelentkezett be, így a tárolt adatok nem állíthatók vissza a régi jelszó nélkül. A visszaállítás véglegesen törli a gazdagépeket, a hitelesítő adatokat és a kódrészleteket. Folytatja?"
},
"hostKey": {
"verifyNewHost": "SSH host kulcs ellenőrzése",
@@ -2323,6 +2371,13 @@
"serviceLinks": "Szolgáltatási linkek",
"homepagePreview": "Kezdőlap előnézete"
},
"donation": {
"title": "Élvezed a Termixet?",
"body": "A Termix ingyenes és nyílt forráskódú, egy kétfős csapat fejleszti és tartja karban a szabadidőnkben. Ha egy olyan kereskedelmi eszközt vált ki, amelyért egyébként fizetne, egy adomány segít fedezni a tárhelyköltségeket és fenntartja a fejlesztést. Jelenleg az adományok kizárólag kriptovalutákban fogadhatók.",
"milestones": "Az adományok segítenek finanszírozni az olyan funkciók kutatását és megismerését is, mint a SAML és a Kubernetes támogatása. A folyamat előrehaladását az adományozási oldalon tekintheti meg.",
"cta": "Adományoz",
"dismiss": "Talán később"
},
"dashboardTab": {
"stable": "STABIL",
"hostsOnline": "Online házigazdák",
@@ -2790,6 +2845,71 @@
"updateAdminStatusFailed": "Nem sikerült frissíteni az adminisztrátori állapotot",
"allSessionsRevoked": "Minden munkamenet visszavonva",
"revokeSessionsFailed": "Nem sikerült visszavonni a munkameneteket",
"manageUserData": "Felhasználói adatok kezelése",
"backToUsers": "Vissza a felhasználókhoz",
"manageTabAccount": "Fiók",
"manageTabHosts": "Házigazdák",
"manageTabCredentials": "Hitelesítő adatok",
"manageTabSnippets": "Kódrészletek",
"manageTabSessions": "Munkamenetek",
"manageTabDanger": "Veszély",
"manageEditorBack": "Vissza {{username}}adataihoz",
"dataLockedBadge": "BEZÁRT",
"dataLockedNotice": "{{username}}adatai zárolva maradnak a következő bejelentkezésig. A hosztjaik, hitelesítő adataik és kódrészleteik addig nem tekinthetők meg és nem szerkeszthetők.",
"resetPasswordTitle": "Jelszó visszaállítása",
"resetPasswordOidcOnly": "Ez a felhasználó egy külső szolgáltatón keresztül jelentkezik be, és nincs jelszava.",
"resetPasswordPlaceholder": "Új jelszó",
"resetPasswordBtn": "Visszaállítás",
"resetPasswordWorking": "Visszaállítás...",
"resetPasswordSuccess": "Jelszó visszaállítása",
"resetPasswordSuccessWiped": "Jelszó visszaállítása. A felhasználó titkosított adatait törölték.",
"resetPasswordFailed": "Nem sikerült visszaállítani a jelszót",
"resetPasswordConfirmWipe": "{{username}} nem jelentkezett be a titkosítás frissítése óta, így az adatai nem állíthatók vissza. A visszaállítás most törli a gazdagépeit, hitelesítő adatait és kódrészleteit. Folytatja?",
"totpSectionTitle": "Kétfaktoros hitelesítés",
"totpStatusEnabled": "A TOTP engedélyezve van ehhez a felhasználóhoz.",
"totpStatusDisabled": "A TOTP nincs engedélyezve ehhez a felhasználóhoz.",
"disableTotp": "Letiltás",
"disableTotpConfirm": "Letiltja a kétfaktoros hitelesítést a {{username}}felhasználó számára? Csak a jelszavával tudnak majd bejelentkezni.",
"totpDisabledSuccess": "TOTP letiltva",
"totpDisableFailed": "Nem sikerült letiltani a TOTP-t",
"manageApiKeys": "API-kulcsok",
"apiKeyNamePlaceholder": "Kulcs neve",
"apiKeyCopyNotice": "Másold le ezt a kulcsot most, nem fog újra megjelenni.",
"noApiKeysForUser": "Nincsenek API-kulcsok",
"apiKeyDeleteFailed": "Nem sikerült törölni az API-kulcsot",
"exportUserData": "Adatexportálás",
"exportUserDataDesc": "Töltse le a felhasználó hosztjait, hitelesítő adatait és fájlkezelői adatait JSON formátumban. A titkos kódok visszafejtése megtörtént.",
"exportUserDataSuccess": "Exportált felhasználói adatok",
"exportUserDataFailed": "Nem sikerült exportálni a felhasználói adatokat",
"hostsCount": "{{count}} gazdagépek",
"addHostForUser": "Gazdagép hozzáadása",
"noHostsForUser": "Ennek a felhasználónak nincsenek hosztjai",
"connectToHost": "Csatlakozás",
"deleteHostConfirm": "Törölni kell a(z) {{username}}-hez tartozó \"{{name}}\" hosztot?",
"hostDeletedSuccess": "Gazdagép törölve",
"hostDeleteFailed": "Nem sikerült törölni a gazdagépet",
"credentialsCount": "{{count}} hitelesítő adatok",
"addCredentialForUser": "Hitelesítő adat hozzáadása",
"noCredentialsForUser": "Ennek a felhasználónak nincsenek hitelesítő adatai",
"deleteCredentialConfirm": "Törölje a(z) {{username}} felhasználóhoz tartozó \"{{name}}\" hitelesítő adatot?",
"credentialDeletedSuccess": "Hitelesítő adat törölve",
"credentialDeleteFailed": "Nem sikerült törölni a hitelesítő adatokat",
"snippetsCount": "{{count}} kódrészletek",
"addSnippetForUser": "Kódrészlet hozzáadása",
"noSnippetsForUser": "Ennek a felhasználónak nincsenek kódrészletei",
"snippetRequiredFields": "A részlet nevének és tartalmának megadása kötelező",
"snippetNamePlaceholder": "Kódrészlet neve",
"snippetContentPlaceholder": "Parancs tartalma",
"snippetFolderPlaceholder": "Mappa (opcionális)",
"snippetSaved": "Kódrészlet mentve",
"snippetSaveFailed": "Nem sikerült menteni a részletet",
"deleteSnippetConfirm": "Törölni a(z) {{username}} felhasználóhoz tartozó \"{{name}}\" kódrészletet?",
"snippetDeletedSuccess": "Kódrészlet törölve",
"snippetDeleteFailed": "Nem sikerült törölni a részletet",
"noSessionsForUser": "Nincsenek aktív munkamenetek",
"deleteUserDangerDesc": "A(z) {{username}} és az összes hozzá tartozó adat (hosztok, hitelesítő adatok, kódrészletek, előzmények) végleges törlése. Ez a művelet nem vonható vissza.",
"deleteUserConfirm": "Véglegesen töröljem a(z) {{username}} felhasználót és az összes adatát?",
"deleteUserAdminBlocked": "A felhasználó törlése előtt távolítsa el az adminisztrátori státuszt.",
"createRoleRequired": "Név és megjelenített név megadása kötelező",
"createRoleSuccess": "\"{{name}}\" szerepkör létrehozva",
"createRoleFailed": "Nem sikerült létrehozni a szerepkört",
@@ -2822,7 +2942,16 @@
"hostDefaultsCommandHistory": "Parancsnoki előzmények",
"hostDefaultsCommandHistoryDesc": "Parancselőzmények követése új hosztokon alapértelmezés szerint",
"hostDefaultsSaved": "Gazdagép alapértelmezett beállításai mentve",
"hostDefaultsSaveFailed": "Nem sikerült menteni a gazdagép alapértelmezett beállításait"
"hostDefaultsSaveFailed": "Nem sikerült menteni a gazdagép alapértelmezett beállításait",
"rolePermissions": {
"count": "{{count}} jogosultságok",
"editAction": "Szerkesztési engedélyek",
"loadError": "Nem sikerült betölteni az engedélykatalógust",
"saved": "Szerepkör-engedélyek mentve",
"saveError": "Nem sikerült menteni a szerepkör-engedélyeket",
"save": "Megtakarítás",
"saving": "Megtakarítás..."
}
},
"newUi": {
"sidebar": {
@@ -2991,6 +3120,10 @@
"executionFailed": "Sikertelen"
},
"userProfile": {
"donateTitle": "Tartsd életben a Termixet",
"donateDescription": "A Termix ingyenes, saját tárhelyen üzemel, és csak néhány ember fejlesztette a szabadidejében. Ha időt vagy pénzt takarított meg vele, egy kriptovaluta-adomány segíthet a működésben.",
"donateMilestones": "Az adományok segítenek finanszírozni a kutatást és a SAML, a Kubernetes és az Agent-támogatás kiépítéséhez szükséges dolgok megismerését. Tekintse meg a haladást, és adományozzon.",
"donateButton": "Adományozz kriptovalutát",
"storageModeLocal": "Böngésző",
"storageModeCloud": "Adatbázis",
"storageModeDescription": "A böngésző csak ebben a böngészőben tárolja a beállításokat. Az adatbázis szinkronizálódik a szerverrel, és bármilyen eszközön betöltődik.",
@@ -3011,6 +3144,10 @@
"twoFaOn": "On",
"twoFaOff": "Le",
"versionLabel": "Változat",
"betaProgramTitle": "Béta program",
"betaProgramDescription": "Próbáld ki a legújabb funkciókat korán a heti :beta Docker címkével. Instabil, nem éles környezetben használható.",
"betaProgramFeedback": "Hibát találtál? Jelentsd itt.",
"betaProgramLearnMore": "Tudj meg többet",
"deleteAccount": "Fiók törlése",
"deleteAccountDescription": "Fiók végleges törlése",
"changeServerDescription": "Váltás másik Termix háttérkiszolgálóra",
+158 -21
View File
@@ -864,6 +864,21 @@
"proxmoxDockerDetectionDesc": "Pola nama yang dipisahkan koma yang memungkinkan Docker untuk mencocokkan mesin virtual.",
"proxmoxPreferredRanges": "Rentang IP yang disukai",
"proxmoxPreferredRangesDesc": "Awalan yang dipisahkan koma dalam urutan prioritas untuk pemilihan IP ketika tamu memiliki beberapa antarmuka.",
"proxmoxAutoSync": "Sinkronisasi otomatis tamu",
"proxmoxAutoSyncDesc": "Secara berkala, temukan node Proxmox ini dan buat atau perbarui host tamu yang diimpor saat sesi Anda tidak terkunci.",
"proxmoxSyncInterval": "Interval sinkronisasi (menit)",
"proxmoxSyncIntervalDesc": "Minimal 5 menit. Penjadwal akan melewati sesi pengguna yang terkunci.",
"proxmoxMarkMissing": "Tandai tamu yang hilang",
"proxmoxMarkMissingDesc": "Tambahkan tag proxmox-missing ketika tamu yang sebelumnya diimpor menghilang, alih-alih menghapusnya.",
"proxmoxLastSync": "Sinkronisasi terakhir",
"proxmoxLastSyncNever": "Belum disinkronkan",
"proxmoxLastSyncNoResult": "Tidak ada detail hasil.",
"proxmoxLastSyncSummary": "{{created}} dibuat, {{updated}} diperbarui, {{markedMissing}} hilang, {{skipped}} dilewati",
"proxmoxLastSyncStatus": {
"success": "Kesuksesan",
"error": "Gagal",
"pending": "Tertunda"
},
"proxmoxDiscoverAction": "Temukan & impor tamu Proxmox",
"proxmoxImportTitle": "Impor dari Proxmox",
"proxmoxSelectHost": "Pilih host Proxmox…",
@@ -1167,7 +1182,57 @@
"shareHost": "Bagikan Host",
"shareHostTitle": "Bagikan: {{name}}",
"sharing": {
"requiresCredential": "Host ini harus menggunakan kredensial agar dapat berbagi. Edit host dan tetapkan kredensial terlebih dahulu."
"loadError": "Gagal memuat data berbagi. Silakan coba lagi.",
"shareWithSection": "Bagikan dengan",
"usersTab": "Pengguna",
"rolesTab": "Peran",
"searchPlaceholder": "Cari pengguna atau peran...",
"noMatches": "Tidak ditemukan kecocokan.",
"permissionLevelLabel": "Tingkat izin",
"levels": {
"connect": {
"label": "Menghubungkan",
"description": "Hanya sesi yang diizinkan untuk dibuka: terminal, desktop jarak jauh, pengelola file, terowongan, dan Docker. Tidak ada akses ke konfigurasi host."
},
"view": {
"label": "Melihat",
"description": "Hubungkan, dan lihat konfigurasi host. Rahasia tidak akan pernah ditampilkan."
},
"edit": {
"label": "Edit",
"description": "Lihat, dan modifikasi host. Rahasia dapat diganti tetapi tidak pernah dibaca; penetapan kredensial tetap hanya untuk pemilik."
},
"manage": {
"label": "Mengelola",
"description": "Edit, bagikan host dengan orang lain, ubah tingkat izin, dan cabut akses."
}
},
"expiryLabel": "Kedaluwarsa akses",
"expiry": {
"never": "Tidak pernah",
"oneHour": "1 jam",
"oneDay": "24 jam",
"sevenDays": "7 hari",
"thirtyDays": "30 hari",
"custom": "Kebiasaan"
},
"customHoursPlaceholder": "Waktu hingga akses berakhir",
"shareButton": "Membagikan",
"shareWithCount": "Bagikan ({{count}})",
"currentAccess": "Akses saat ini",
"noAccessEntries": "Host ini belum dibagikan",
"grantedBy": "Diberikan oleh",
"expires": "Berakhir",
"expired": "Kedaluwarsa",
"never": "Tidak pernah",
"revoke": "Menarik kembali",
"accessUpdated": "Akses diperbarui",
"accessUpdateFailed": "Gagal memperbarui akses",
"sharedBadge": "Dibagikan",
"sharedBadgeTooltip": "Dibagikan oleh {{owner}} ( akses{{level}})",
"viewOnlyBanner": "Host ini dibagikan kepada Anda oleh {{owner}} dengan akses lihat. Konfigurasinya hanya baca.",
"sharedEditBanner": "Host ini dibagikan kepada Anda oleh {{owner}} dengan akses edit. Perubahan berlaku untuk host sebenarnya; referensi otentikasi hanya dapat diubah oleh pemilik.",
"ownerOnlyControl": "Hanya pemilik host yang dapat mengubah ini."
},
"guac": {
"connection": "Koneksi",
@@ -1302,28 +1367,10 @@
"backspaceKey": "Tombol Backspace",
"saveHostFirst": "Selamatkan tuan rumah terlebih dahulu.",
"sharingOptionsAfterSave": "Opsi berbagi tersedia setelah host disimpan.",
"sharingLoadError": "Gagal memuat data berbagi. Periksa koneksi Anda dan coba lagi.",
"shareHostSection": "Bagikan Host",
"shareWithUser": "Bagikan dengan Pengguna",
"shareWithRole": "Bagikan dengan Peran",
"selectUser": "Pilih Pengguna",
"selectRole": "Pilih Peran",
"selectUserOption": "Pilih pengguna...",
"selectRoleOption": "Pilih peran...",
"permissionLevel": "Tingkat Izin",
"expiresInHours": "Berakhir dalam (jam)",
"noExpiryPlaceholder": "Biarkan kosong agar tidak ada tanggal kedaluwarsa.",
"shareBtn": "Membagikan",
"currentAccess": "Akses Saat Ini",
"typeHeader": "Jenis",
"targetHeader": "Target",
"permissionHeader": "Izin",
"grantedByHeader": "Diberikan Oleh",
"expiresHeader": "Berakhir",
"noAccessEntries": "Belum ada entri akses.",
"expiredLabel": "Kedaluwarsa",
"neverLabel": "Tidak pernah",
"revokeBtn": "Menarik kembali",
"cancelBtn": "Membatalkan",
"savingBtn": "Penghematan...",
"updateHostBtn": "Perbarui Host",
@@ -2190,7 +2237,8 @@
"passwordLoginDisabledDesc": "Login dengan kata sandi dinonaktifkan. Gunakan kata sandi atau penyedia autentikasi eksternal.",
"signInWithPasskey": "Masuk dengan kata sandi",
"passkeyLoginFailed": "Login dengan kata sandi gagal.",
"attemptsRemaining": "{{count}} percobaan tersisa"
"attemptsRemaining": "{{count}} percobaan tersisa",
"confirmResetDataWipe": "Akun ini belum masuk sejak peningkatan enkripsi, jadi data yang tersimpan tidak dapat dipulihkan tanpa kata sandi lama. Mengatur ulang akan menghapus host, kredensial, dan cuplikan secara permanen. Lanjutkan?"
},
"hostKey": {
"verifyNewHost": "Verifikasi Kunci Host SSH",
@@ -2323,6 +2371,13 @@
"serviceLinks": "Tautan Layanan",
"homepagePreview": "Pratinjau Halaman Utama"
},
"donation": {
"title": "Apakah Anda menikmati Termix?",
"body": "Termix adalah perangkat lunak gratis dan sumber terbuka, yang dibangun dan dipelihara oleh tim beranggotakan dua orang di waktu luang kami. Jika Termix menggantikan alat komersial yang biasanya Anda bayar, donasi akan membantu menutupi biaya hosting dan menjaga pengembangan tetap berjalan. Saat ini, donasi hanya dalam bentuk kripto.",
"milestones": "Donasi juga membantu mendanai waktu untuk meneliti dan mempelajari apa yang dibutuhkan untuk membangun fitur-fitur seperti dukungan SAML dan Kubernetes. Lihat perkembangannya di halaman donasi.",
"cta": "Menyumbangkan",
"dismiss": "mungkin nanti"
},
"dashboardTab": {
"stable": "STABIL",
"hostsOnline": "Host Online",
@@ -2790,6 +2845,71 @@
"updateAdminStatusFailed": "Gagal memperbarui status admin",
"allSessionsRevoked": "Semua sesi dibatalkan",
"revokeSessionsFailed": "Gagal mencabut sesi",
"manageUserData": "Kelola data pengguna",
"backToUsers": "Kembali ke halaman pengguna",
"manageTabAccount": "Akun",
"manageTabHosts": "Tuan rumah",
"manageTabCredentials": "Kredensial",
"manageTabSnippets": "Cuplikan",
"manageTabSessions": "Sesi",
"manageTabDanger": "Bahaya",
"manageEditorBack": "Kembali ke data {{username}}",
"dataLockedBadge": "TERKUNCI",
"dataLockedNotice": "Data {{username}}tetap terkunci hingga mereka masuk kembali. Host, kredensial, dan cuplikan mereka tidak dapat dilihat atau diedit hingga saat itu.",
"resetPasswordTitle": "Atur Ulang Kata Sandi",
"resetPasswordOidcOnly": "Pengguna ini masuk melalui penyedia eksternal dan tidak memiliki kata sandi.",
"resetPasswordPlaceholder": "Kata sandi baru",
"resetPasswordBtn": "Mengatur ulang",
"resetPasswordWorking": "Mengatur ulang...",
"resetPasswordSuccess": "Reset kata sandi",
"resetPasswordSuccessWiped": "Kata sandi telah direset. Data terenkripsi pengguna telah dihapus.",
"resetPasswordFailed": "Gagal mengatur ulang kata sandi",
"resetPasswordConfirmWipe": "{{username}} belum masuk sejak peningkatan enkripsi, jadi data mereka tidak dapat dipulihkan. Mengatur ulang sekarang akan menghapus host, kredensial, dan cuplikan mereka. Lanjutkan?",
"totpSectionTitle": "Autentikasi Dua Faktor",
"totpStatusEnabled": "TOTP diaktifkan untuk pengguna ini.",
"totpStatusDisabled": "TOTP tidak diaktifkan untuk pengguna ini.",
"disableTotp": "Cacat",
"disableTotpConfirm": "Nonaktifkan otentikasi dua faktor untuk {{username}}? Mereka akan dapat masuk hanya dengan kata sandi mereka.",
"totpDisabledSuccess": "TOTP dinonaktifkan",
"totpDisableFailed": "Gagal menonaktifkan TOTP",
"manageApiKeys": "Kunci API",
"apiKeyNamePlaceholder": "Nama kunci",
"apiKeyCopyNotice": "Salin kunci ini sekarang, kunci ini tidak akan ditampilkan lagi.",
"noApiKeysForUser": "Tidak ada kunci API",
"apiKeyDeleteFailed": "Gagal menghapus kunci API",
"exportUserData": "Ekspor Data",
"exportUserDataDesc": "Unduh data host, kredensial, dan pengelola file pengguna ini sebagai JSON. Rahasia akan didekripsi.",
"exportUserDataSuccess": "Data pengguna diekspor",
"exportUserDataFailed": "Gagal mengekspor data pengguna",
"hostsCount": "{{count}} tuan rumah",
"addHostForUser": "Tambahkan Host",
"noHostsForUser": "Pengguna ini tidak memiliki host.",
"connectToHost": "Menghubungkan",
"deleteHostConfirm": "Hapus host \"{{name}}\" milik {{username}}?",
"hostDeletedSuccess": "Host dihapus",
"hostDeleteFailed": "Gagal menghapus host",
"credentialsCount": "{{count}} kredensial",
"addCredentialForUser": "Tambahkan Kredensial",
"noCredentialsForUser": "Pengguna ini tidak memiliki kredensial.",
"deleteCredentialConfirm": "Hapus kredensial \"{{name}}\" milik {{username}}?",
"credentialDeletedSuccess": "Kredensial dihapus",
"credentialDeleteFailed": "Gagal menghapus kredensial",
"snippetsCount": "{{count}} cuplikan",
"addSnippetForUser": "Tambahkan Cuplikan",
"noSnippetsForUser": "Pengguna ini tidak memiliki cuplikan.",
"snippetRequiredFields": "Nama cuplikan dan konten wajib diisi.",
"snippetNamePlaceholder": "Nama cuplikan",
"snippetContentPlaceholder": "Isi perintah",
"snippetFolderPlaceholder": "Folder (opsional)",
"snippetSaved": "Cuplikan tersimpan",
"snippetSaveFailed": "Gagal menyimpan cuplikan",
"deleteSnippetConfirm": "Hapus cuplikan \"{{name}}\" milik {{username}}?",
"snippetDeletedSuccess": "Cuplikan dihapus",
"snippetDeleteFailed": "Gagal menghapus cuplikan",
"noSessionsForUser": "Tidak ada sesi aktif",
"deleteUserDangerDesc": "Hapus secara permanen {{username}} dan semua datanya (host, kredensial, cuplikan, riwayat). Tindakan ini tidak dapat dibatalkan.",
"deleteUserConfirm": "Hapus secara permanen {{username}} dan semua datanya?",
"deleteUserAdminBlocked": "Hapus status admin sebelum menghapus pengguna ini.",
"createRoleRequired": "Nama dan nama tampilan wajib diisi.",
"createRoleSuccess": "Peran \"{{name}}\" dibuat",
"createRoleFailed": "Gagal membuat peran",
@@ -2822,7 +2942,16 @@
"hostDefaultsCommandHistory": "Riwayat Perintah",
"hostDefaultsCommandHistoryDesc": "Lacak riwayat perintah pada host baru secara default.",
"hostDefaultsSaved": "Pengaturan default host tersimpan.",
"hostDefaultsSaveFailed": "Gagal menyimpan pengaturan default host."
"hostDefaultsSaveFailed": "Gagal menyimpan pengaturan default host.",
"rolePermissions": {
"count": "{{count}} izin",
"editAction": "Edit izin",
"loadError": "Gagal memuat katalog izin.",
"saved": "Izin peran tersimpan",
"saveError": "Gagal menyimpan izin peran",
"save": "Menyimpan",
"saving": "Penghematan..."
}
},
"newUi": {
"sidebar": {
@@ -2991,6 +3120,10 @@
"executionFailed": "Gagal"
},
"userProfile": {
"donateTitle": "Jaga agar Termix tetap hidup.",
"donateDescription": "Termix gratis, dihosting sendiri, dan dibangun hanya oleh beberapa orang di waktu luang mereka. Jika Termix telah menghemat waktu atau uang Anda, donasi kripto akan membantu kelangsungannya.",
"donateMilestones": "Donasi membantu mendanai waktu yang dibutuhkan untuk meneliti dan mempelajari apa yang diperlukan untuk membangun dukungan SAML, Kubernetes, dan Agent. Lihat perkembangannya dan berikan donasi.",
"donateButton": "Donasikan kripto",
"storageModeLocal": "Browser",
"storageModeCloud": "Basis data",
"storageModeDescription": "Browser hanya menyimpan pengaturan di browser ini saja. Basis data disinkronkan ke server dan dimuat di perangkat apa pun.",
@@ -3011,6 +3144,10 @@
"twoFaOn": "Pada",
"twoFaOff": "Mati",
"versionLabel": "Versi",
"betaProgramTitle": "Program Beta",
"betaProgramDescription": "Coba fitur-fitur mendatang lebih awal dengan tag Docker :beta yang diperbarui setiap minggu. Tidak stabil, bukan untuk produksi.",
"betaProgramFeedback": "Menemukan bug? Laporkan di sini.",
"betaProgramLearnMore": "Pelajari Selengkapnya",
"deleteAccount": "Hapus Akun",
"deleteAccountDescription": "Hapus akun Anda secara permanen",
"changeServerDescription": "Beralihlah ke server backend Termix yang berbeda.",
+158 -21
View File
@@ -864,6 +864,21 @@
"proxmoxDockerDetectionDesc": "Modelli di nomi separati da virgole che consentono a Docker di abbinare gli ospiti",
"proxmoxPreferredRanges": "Intervalli IP preferiti",
"proxmoxPreferredRangesDesc": "Prefissi separati da virgole in ordine di priorità per la selezione dell'indirizzo IP quando una macchina virtuale dispone di più interfacce.",
"proxmoxAutoSync": "Sincronizzazione automatica degli ospiti",
"proxmoxAutoSyncDesc": "Rileva periodicamente questo nodo Proxmox e crea o aggiorna gli host guest importati mentre la tua sessione è sbloccata.",
"proxmoxSyncInterval": "Intervallo di sincronizzazione (minuti)",
"proxmoxSyncIntervalDesc": "Minimo 5 minuti. Lo scheduler salta le sessioni utente bloccate.",
"proxmoxMarkMissing": "Mark Ospiti mancanti",
"proxmoxMarkMissingDesc": "Aggiungi il tag proxmox-missing quando una macchina virtuale precedentemente importata scompare, invece di eliminarla.",
"proxmoxLastSync": "Ultima sincronizzazione",
"proxmoxLastSyncNever": "Non ancora sincronizzato",
"proxmoxLastSyncNoResult": "Nessun risultato disponibile",
"proxmoxLastSyncSummary": "{{created}} creato, {{updated}} aggiornato, {{markedMissing}} mancante, {{skipped}} saltato",
"proxmoxLastSyncStatus": {
"success": "Successo",
"error": "Fallito",
"pending": "In attesa di"
},
"proxmoxDiscoverAction": "Scopri e importa guest Proxmox",
"proxmoxImportTitle": "Importa da Proxmox",
"proxmoxSelectHost": "Seleziona un host Proxmox…",
@@ -1167,7 +1182,57 @@
"shareHost": "Condividi l'host",
"shareHostTitle": "Condividi: {{name}}",
"sharing": {
"requiresCredential": "Questo host deve utilizzare una credenziale per abilitare la condivisione. Modifica l'host e assegna prima una credenziale."
"loadError": "Impossibile caricare i dati di condivisione. Riprova.",
"shareWithSection": "Condividi con",
"usersTab": "Utenti",
"rolesTab": "Ruoli",
"searchPlaceholder": "Cerca utenti o ruoli...",
"noMatches": "Nessuna corrispondenza trovata",
"permissionLevelLabel": "Livello di autorizzazione",
"levels": {
"connect": {
"label": "Collegare",
"description": "Sono consentite solo le seguenti sessioni: terminale, desktop remoto, file manager, tunnel e Docker. Nessun accesso alla configurazione dell'host."
},
"view": {
"label": "Visualizzazione",
"description": "Connettiti e visualizza la configurazione dell'host. I segreti non vengono mai mostrati."
},
"edit": {
"label": "Modificare",
"description": "Visualizza e modifica l'host. I segreti possono essere sostituiti ma non letti; le credenziali assegnate rimangono riservate al proprietario."
},
"manage": {
"label": "Maneggio",
"description": "Modifica e condividi l'host con altri, cambia i livelli di autorizzazione e revoca l'accesso."
}
},
"expiryLabel": "Scadenza dell'accesso",
"expiry": {
"never": "Mai",
"oneHour": "1 ora",
"oneDay": "24 ore",
"sevenDays": "7 giorni",
"thirtyDays": "30 giorni",
"custom": "Costume"
},
"customHoursPlaceholder": "Ore rimanenti alla scadenza dell'accesso",
"shareButton": "Condividere",
"shareWithCount": "Condividi ({{count}})",
"currentAccess": "Accesso attuale",
"noAccessEntries": "Questo host non è stato ancora condiviso",
"grantedBy": "Concesso da",
"expires": "Scade",
"expired": "Scaduto",
"never": "Mai",
"revoke": "Revocare",
"accessUpdated": "Accesso aggiornato",
"accessUpdateFailed": "Impossibile aggiornare l'accesso",
"sharedBadge": "Condiviso",
"sharedBadgeTooltip": "Condiviso da {{owner}} ({{level}} accesso)",
"viewOnlyBanner": "Questo host è condiviso con te da {{owner}} con accesso in sola visualizzazione. La configurazione è di sola lettura.",
"sharedEditBanner": "Questo host è condiviso con te da {{owner}} con accesso in modifica. Le modifiche si applicano all'host reale; i riferimenti di autenticazione possono essere modificati solo dal proprietario.",
"ownerOnlyControl": "Solo il proprietario dell'host può modificare questo"
},
"guac": {
"connection": "Connessione",
@@ -1302,28 +1367,10 @@
"backspaceKey": "Tasto Backspace",
"saveHostFirst": "Salva prima l'host.",
"sharingOptionsAfterSave": "Le opzioni di condivisione saranno disponibili dopo aver salvato l'host.",
"sharingLoadError": "Impossibile caricare i dati di condivisione. Verifica la connessione e riprova.",
"shareHostSection": "Condividi l'host",
"shareWithUser": "Condividi con l'utente",
"shareWithRole": "Condividi con il ruolo",
"selectUser": "Seleziona utente",
"selectRole": "Seleziona il ruolo",
"selectUserOption": "Seleziona un utente...",
"selectRoleOption": "Seleziona un ruolo...",
"permissionLevel": "Livello di autorizzazione",
"expiresInHours": "Scade tra (ore)",
"noExpiryPlaceholder": "Lasciare vuoto per non avere scadenza",
"shareBtn": "Condividere",
"currentAccess": "Accesso attuale",
"typeHeader": "Tipo",
"targetHeader": "Bersaglio",
"permissionHeader": "Autorizzazione",
"grantedByHeader": "Concesso da",
"expiresHeader": "Scade",
"noAccessEntries": "Nessun accesso ancora registrato.",
"expiredLabel": "Scaduto",
"neverLabel": "Mai",
"revokeBtn": "Revocare",
"cancelBtn": "Cancellare",
"savingBtn": "Risparmio...",
"updateHostBtn": "Aggiorna host",
@@ -2190,7 +2237,8 @@
"passwordLoginDisabledDesc": "L'accesso tramite password è disabilitato. Utilizzare una chiave di accesso o un provider di autenticazione esterno.",
"signInWithPasskey": "Accedi con la password",
"passkeyLoginFailed": "Accesso con password non riuscito",
"attemptsRemaining": "{{count}} tentativi rimanenti"
"attemptsRemaining": "{{count}} tentativi rimanenti",
"confirmResetDataWipe": "Questo account non ha effettuato l'accesso dall'aggiornamento della crittografia, pertanto i dati memorizzati non possono essere recuperati senza la vecchia password. Il ripristino eliminerà definitivamente host, credenziali e snippet. Continuare?"
},
"hostKey": {
"verifyNewHost": "Verifica la chiave host SSH",
@@ -2323,6 +2371,13 @@
"serviceLinks": "Collegamenti ai servizi",
"homepagePreview": "Anteprima della homepage"
},
"donation": {
"title": "Ti piace Termix?",
"body": "Termix è gratuito e open source, sviluppato e gestito da un team di due persone nel nostro tempo libero. Se ha sostituito uno strumento commerciale che altrimenti avresti pagato, una donazione contribuisce a coprire i costi di hosting e a sostenere lo sviluppo. Al momento, le donazioni sono accettate solo in criptovalute.",
"milestones": "Le donazioni contribuiscono anche a finanziare il tempo dedicato alla ricerca e all'apprendimento di ciò che è necessario per sviluppare funzionalità come il supporto per SAML e Kubernetes. È possibile seguire gli aggiornamenti sulla pagina delle donazioni.",
"cta": "Donare",
"dismiss": "Forse più tardi"
},
"dashboardTab": {
"stable": "STABILE",
"hostsOnline": "Host online",
@@ -2790,6 +2845,71 @@
"updateAdminStatusFailed": "Impossibile aggiornare lo stato dell'amministratore.",
"allSessionsRevoked": "Tutte le sessioni sono state revocate.",
"revokeSessionsFailed": "Impossibile revocare le sessioni",
"manageUserData": "Gestire i dati utente",
"backToUsers": "Torna agli utenti",
"manageTabAccount": "Account",
"manageTabHosts": "Ospiti",
"manageTabCredentials": "Credenziali",
"manageTabSnippets": "Frammenti",
"manageTabSessions": "Sessioni",
"manageTabDanger": "Pericolo",
"manageEditorBack": "Torna ai dati di {{username}}",
"dataLockedBadge": "CHIUSO",
"dataLockedNotice": "I dati di {{username}}rimangono bloccati fino al loro prossimo accesso. I loro host, credenziali e snippet non possono essere visualizzati o modificati fino ad allora.",
"resetPasswordTitle": "Reimposta password",
"resetPasswordOidcOnly": "Questo utente effettua l'accesso tramite un provider esterno e non ha una password.",
"resetPasswordPlaceholder": "Nuova password",
"resetPasswordBtn": "Reset",
"resetPasswordWorking": "Ripristino in corso...",
"resetPasswordSuccess": "reimposta la password",
"resetPasswordSuccessWiped": "Reimpostazione della password. I dati crittografati dell'utente sono stati cancellati.",
"resetPasswordFailed": "Impossibile reimpostare la password",
"resetPasswordConfirmWipe": "{{username}} non ha effettuato l'accesso dall'aggiornamento della crittografia, pertanto i suoi dati non possono essere recuperati. Il ripristino ora eliminerà host, credenziali e snippet. Continuare?",
"totpSectionTitle": "Autenticazione a due fattori",
"totpStatusEnabled": "TOTP è abilitato per questo utente",
"totpStatusDisabled": "TOTP non è abilitato per questo utente",
"disableTotp": "Disabilita",
"disableTotpConfirm": "Disabilitare l'autenticazione a due fattori per {{username}}? Potranno accedere solo con la loro password.",
"totpDisabledSuccess": "TOTP disabilitato",
"totpDisableFailed": "Impossibile disattivare TOTP",
"manageApiKeys": "Chiavi API",
"apiKeyNamePlaceholder": "Nome chiave",
"apiKeyCopyNotice": "Copia subito questa chiave, non verrà più mostrata.",
"noApiKeysForUser": "Nessuna chiave API",
"apiKeyDeleteFailed": "Impossibile eliminare la chiave API",
"exportUserData": "Esportazione dei dati",
"exportUserDataDesc": "Scarica i dati relativi a host, credenziali e file manager di questo utente in formato JSON. I dati sensibili vengono decrittografati.",
"exportUserDataSuccess": "Dati utente esportati",
"exportUserDataFailed": "Impossibile esportare i dati utente",
"hostsCount": "{{count}} host",
"addHostForUser": "Aggiungi host",
"noHostsForUser": "Questo utente non ha host",
"connectToHost": "Collegare",
"deleteHostConfirm": "Eliminare l'host \"{{name}}\" appartenente a {{username}}?",
"hostDeletedSuccess": "Host eliminato",
"hostDeleteFailed": "Impossibile eliminare l'host",
"credentialsCount": "{{count}} credenziali",
"addCredentialForUser": "Aggiungi credenziali",
"noCredentialsForUser": "Questo utente non ha credenziali",
"deleteCredentialConfirm": "Eliminare le credenziali \"{{name}}\" appartenenti a {{username}}?",
"credentialDeletedSuccess": "Credenziali eliminate",
"credentialDeleteFailed": "Impossibile eliminare le credenziali",
"snippetsCount": "{{count}} frammenti",
"addSnippetForUser": "Aggiungi snippet",
"noSnippetsForUser": "Questo utente non ha snippet",
"snippetRequiredFields": "Il nome e il contenuto dello snippet sono obbligatori.",
"snippetNamePlaceholder": "Nome del frammento",
"snippetContentPlaceholder": "Contenuto del comando",
"snippetFolderPlaceholder": "Cartella (facoltativa)",
"snippetSaved": "Frammento salvato",
"snippetSaveFailed": "Impossibile salvare il frammento",
"deleteSnippetConfirm": "Eliminare il frammento \"{{name}}\" appartenente a {{username}}?",
"snippetDeletedSuccess": "Frammento eliminato",
"snippetDeleteFailed": "Impossibile eliminare il frammento",
"noSessionsForUser": "Nessuna sessione attiva",
"deleteUserDangerDesc": "Elimina definitivamente {{username}} e tutti i relativi dati (host, credenziali, snippet, cronologia). Questa operazione non può essere annullata.",
"deleteUserConfirm": "Eliminare definitivamente {{username}} e tutti i suoi dati?",
"deleteUserAdminBlocked": "Rimuovere i privilegi di amministratore prima di eliminare questo utente.",
"createRoleRequired": "Nome e nome visualizzato sono obbligatori.",
"createRoleSuccess": "Ruolo \"{{name}}\" creato",
"createRoleFailed": "Impossibile creare il ruolo",
@@ -2822,7 +2942,16 @@
"hostDefaultsCommandHistory": "Storia del comando",
"hostDefaultsCommandHistoryDesc": "Per impostazione predefinita, viene monitorata la cronologia dei comandi sui nuovi host.",
"hostDefaultsSaved": "Hostdefaults salvato",
"hostDefaultsSaveFailed": "Impossibile salvare le impostazioni predefinite dell'host"
"hostDefaultsSaveFailed": "Impossibile salvare le impostazioni predefinite dell'host",
"rolePermissions": {
"count": "{{count}} permessi",
"editAction": "Modifica autorizzazioni",
"loadError": "Impossibile caricare il catalogo delle autorizzazioni.",
"saved": "Permessi di ruolo salvati",
"saveError": "Impossibile salvare le autorizzazioni del ruolo.",
"save": "Salva",
"saving": "Risparmio..."
}
},
"newUi": {
"sidebar": {
@@ -2991,6 +3120,10 @@
"executionFailed": "Fallito"
},
"userProfile": {
"donateTitle": "Mantieni in vita Termix",
"donateDescription": "Termix è gratuito, autogestito e sviluppato da poche persone nel loro tempo libero. Se ti ha fatto risparmiare tempo o denaro, una donazione in criptovalute contribuisce a mantenerlo attivo.",
"donateMilestones": "Le donazioni contribuiscono a finanziare il tempo dedicato alla ricerca e all'apprendimento di ciò che è necessario per sviluppare il supporto per SAML, Kubernetes e Agent. Segui i progressi e dona.",
"donateButton": "Dona criptovalute",
"storageModeLocal": "Sito web",
"storageModeCloud": "Schermata",
"storageModeDescription": "Il browser memorizza le impostazioni solo in questo browser. Il database si sincronizza con il server e viene caricato su qualsiasi dispositivo.",
@@ -3011,6 +3144,10 @@
"twoFaOn": "SU",
"twoFaOff": "Spento",
"versionLabel": "Versione",
"betaProgramTitle": "Programma Beta",
"betaProgramDescription": "Prova in anteprima le nuove funzionalità con il tag Docker :beta, disponibile settimanalmente. Versione instabile, non adatta all'ambiente di produzione.",
"betaProgramFeedback": "Hai trovato un bug? Segnalalo qui.",
"betaProgramLearnMore": "Saperne di più",
"deleteAccount": "Elimina account",
"deleteAccountDescription": "Elimina definitivamente il tuo account",
"changeServerDescription": "Passa a un server backend Termix diverso",
+158 -21
View File
@@ -864,6 +864,21 @@
"proxmoxDockerDetectionDesc": "ゲストのマッチングを可能にする、カンマ区切りの名前パターン",
"proxmoxPreferredRanges": "推奨IPアドレス範囲",
"proxmoxPreferredRangesDesc": "ゲストが複数のインターフェースを持つ場合のIP選択の優先順位を示すカンマ区切りのプレフィックス",
"proxmoxAutoSync": "ゲストの自動同期",
"proxmoxAutoSyncDesc": "セッションがロック解除されている間に、定期的にこの Proxmox ノードを検出し、インポート済みのゲストホストを作成または更新します。",
"proxmoxSyncInterval": "同期間隔(分)",
"proxmoxSyncIntervalDesc": "最低5分。スケジューラはロックされたユーザーセッションをスキップします。",
"proxmoxMarkMissing": "行方不明のゲストをマークする",
"proxmoxMarkMissingDesc": "以前インポートしたゲストが消えた場合、削除する代わりにproxmox-missingタグを追加する。",
"proxmoxLastSync": "最終同期",
"proxmoxLastSyncNever": "まだ同期されていません",
"proxmoxLastSyncNoResult": "結果の詳細はありません",
"proxmoxLastSyncSummary": "{{created}} 作成、 {{updated}} 更新、 {{markedMissing}} 欠落、 {{skipped}} スキップ",
"proxmoxLastSyncStatus": {
"success": "成功",
"error": "失敗した",
"pending": "保留中"
},
"proxmoxDiscoverAction": "Proxmoxゲストの検出とインポート",
"proxmoxImportTitle": "Proxmoxからインポート",
"proxmoxSelectHost": "Proxmoxホストを選択してください…",
@@ -1167,7 +1182,57 @@
"shareHost": "共有ホスト",
"shareHostTitle": "共有: {{name}}",
"sharing": {
"requiresCredential": "このホストで共有を有効にするには、認証情報が必要です。まずホストを編集して認証情報を割り当ててください。"
"loadError": "共有データの読み込みに失敗しました。もう一度お試しください。",
"shareWithSection": "シェアする",
"usersTab": "ユーザー",
"rolesTab": "役割",
"searchPlaceholder": "ユーザーまたは役割を検索...",
"noMatches": "一致する項目が見つかりませんでした",
"permissionLevelLabel": "権限レベル",
"levels": {
"connect": {
"label": "接続する",
"description": "開いているセッションのみ:ターミナル、リモートデスクトップ、ファイルマネージャ、トンネル、およびDocker。ホスト構成へのアクセスはできません。"
},
"view": {
"label": "ビュー",
"description": "接続してホストの設定を確認できます。機密情報は一切表示されません。"
},
"edit": {
"label": "編集",
"description": "ホストの表示と変更が可能です。シークレットは置き換えることはできますが、読み取ることはできません。認証情報の割り当ては所有者のみが行えます。"
},
"manage": {
"label": "管理",
"description": "編集、ホストの共有、権限レベルの変更、アクセス権の取り消しが可能です。"
}
},
"expiryLabel": "アクセス期限切れ",
"expiry": {
"never": "一度もない",
"oneHour": "1時間",
"oneDay": "24時間",
"sevenDays": "7日間",
"thirtyDays": "30日間",
"custom": "カスタム"
},
"customHoursPlaceholder": "アクセス期限まであと数時間",
"shareButton": "共有",
"shareWithCount": "シェア({{count}}",
"currentAccess": "現在のアクセス状況",
"noAccessEntries": "このホストはまだ共有されていません",
"grantedBy": "許可された",
"expires": "有効期限",
"expired": "期限切れ",
"never": "一度もない",
"revoke": "取り消す",
"accessUpdated": "アクセス情報が更新されました",
"accessUpdateFailed": "アクセス情報の更新に失敗しました",
"sharedBadge": "共有",
"sharedBadgeTooltip": "{{owner}} ({{level}} アクセス) によって共有されています",
"viewOnlyBanner": "このホストは、 {{owner}} によって閲覧権限で共有されています。設定は読み取り専用です。",
"sharedEditBanner": "このホストは、編集権限を持つ {{owner}} によって共有されています。変更は実際のホストに適用されます。認証情報は所有者のみが変更できます。",
"ownerOnlyControl": "ホストの所有者のみがこれを変更できます"
},
"guac": {
"connection": "繋がり",
@@ -1302,28 +1367,10 @@
"backspaceKey": "バックスペースキー",
"saveHostFirst": "まずホストをセーブしてください。",
"sharingOptionsAfterSave": "ホストを保存すると、共有オプションが利用可能になります。",
"sharingLoadError": "共有データの読み込みに失敗しました。接続状況を確認して、もう一度お試しください。",
"shareHostSection": "共有ホスト",
"shareWithUser": "ユーザーと共有する",
"shareWithRole": "役割を共有",
"selectUser": "ユーザーを選択",
"selectRole": "役割を選択",
"selectUserOption": "ユーザーを選択してください...",
"selectRoleOption": "役割を選択してください...",
"permissionLevel": "権限レベル",
"expiresInHours": "有効期限(時間)",
"noExpiryPlaceholder": "有効期限がない場合は空欄のままにしてください。",
"shareBtn": "共有",
"currentAccess": "現在のアクセス状況",
"typeHeader": "タイプ",
"targetHeader": "ターゲット",
"permissionHeader": "許可",
"grantedByHeader": "許可者",
"expiresHeader": "有効期限",
"noAccessEntries": "アクセスエントリはまだありません。",
"expiredLabel": "期限切れ",
"neverLabel": "一度もない",
"revokeBtn": "取り消す",
"cancelBtn": "キャンセル",
"savingBtn": "保存中...",
"updateHostBtn": "ホストの更新",
@@ -2190,7 +2237,8 @@
"passwordLoginDisabledDesc": "パスワードによるログインは無効になっています。パスキーまたは外部認証プロバイダーをご利用ください。",
"signInWithPasskey": "パスキーでサインイン",
"passkeyLoginFailed": "パスキーログインに失敗しました",
"attemptsRemaining": "{{count}} 残り試行回数"
"attemptsRemaining": "{{count}} 残り試行回数",
"confirmResetDataWipe": "このアカウントは暗号化のアップグレード以降ログインしていないため、古いパスワードがないと保存されたデータを復元できません。リセットすると、ホスト、認証情報、スニペットが完全に削除されます。続行しますか?"
},
"hostKey": {
"verifyNewHost": "SSHホストキーを確認する",
@@ -2323,6 +2371,13 @@
"serviceLinks": "サービスリンク",
"homepagePreview": "ホームページのプレビュー"
},
"donation": {
"title": "Termixを楽しんでいますか?",
"body": "Termixは無料のオープンソースソフトウェアで、2人チームが空き時間を使って開発・保守しています。もしTermixが、本来有料で利用していた商用ツールの代替として使えるのであれば、ご寄付をいただけるとホスティング費用や開発継続に役立ちます。現時点では、寄付は仮想通貨のみ受け付けています。",
"milestones": "寄付金は、SAMLやKubernetesサポートといった機能の開発に必要な調査や学習に費やす時間にも役立てられます。寄付ページで進捗状況をご確認ください。",
"cta": "寄付する",
"dismiss": "後で"
},
"dashboardTab": {
"stable": "安定した",
"hostsOnline": "ホストがオンラインです",
@@ -2790,6 +2845,71 @@
"updateAdminStatusFailed": "管理者ステータスの更新に失敗しました",
"allSessionsRevoked": "すべてのセッションが取り消されました",
"revokeSessionsFailed": "セッションの取り消しに失敗しました",
"manageUserData": "ユーザーデータの管理",
"backToUsers": "ユーザー一覧に戻る",
"manageTabAccount": "アカウント",
"manageTabHosts": "主催者",
"manageTabCredentials": "資格",
"manageTabSnippets": "断片",
"manageTabSessions": "セッション",
"manageTabDanger": "危険",
"manageEditorBack": "{{username}}のデータに戻る",
"dataLockedBadge": "ロック済み",
"dataLockedNotice": "{{username}}のデータは、次回ログインするまでロックされたままです。それまでは、ホスト、認証情報、スニペットを表示したり編集したりすることはできません。",
"resetPasswordTitle": "パスワードをリセット",
"resetPasswordOidcOnly": "このユーザーは外部プロバイダー経由でログインしており、パスワードは設定されていません。",
"resetPasswordPlaceholder": "新しいパスワード",
"resetPasswordBtn": "リセット",
"resetPasswordWorking": "リセット中...",
"resetPasswordSuccess": "パスワードリセット",
"resetPasswordSuccessWiped": "パスワードがリセットされました。ユーザーの暗号化されたデータは消去されました。",
"resetPasswordFailed": "パスワードのリセットに失敗しました",
"resetPasswordConfirmWipe": "{{username}} は暗号化アップグレード以降ログインしていないため、データは復元できません。今すぐリセットすると、ホスト、認証情報、スニペットが削除されます。続行しますか?",
"totpSectionTitle": "二段階認証",
"totpStatusEnabled": "このユーザーに対してTOTPが有効になっています",
"totpStatusDisabled": "このユーザーに対してTOTPは有効になっていません",
"disableTotp": "無効にする",
"disableTotpConfirm": "{{username}}の二段階認証を無効にしてください。パスワードだけでログインできるようになります。",
"totpDisabledSuccess": "TOTPが無効になっています",
"totpDisableFailed": "TOTPの無効化に失敗しました",
"manageApiKeys": "APIキー",
"apiKeyNamePlaceholder": "キー名",
"apiKeyCopyNotice": "このキーを今すぐコピーしてください。二度と表示されません。",
"noApiKeysForUser": "APIキーなし",
"apiKeyDeleteFailed": "APIキーの削除に失敗しました",
"exportUserData": "データエクスポート",
"exportUserDataDesc": "このユーザーのホスト、認証情報、ファイルマネージャーのデータをJSON形式でダウンロードします。秘密情報は復号化されています。",
"exportUserDataSuccess": "ユーザーデータのエクスポート",
"exportUserDataFailed": "ユーザーデータのエクスポートに失敗しました",
"hostsCount": "{{count}} ホスト",
"addHostForUser": "ホストを追加",
"noHostsForUser": "このユーザーにはホストがありません",
"connectToHost": "接続する",
"deleteHostConfirm": "Delete host \"{{name}}\" belonging to {{username}}?",
"hostDeletedSuccess": "ホストが削除されました",
"hostDeleteFailed": "ホストの削除に失敗しました",
"credentialsCount": "{{count}} 認証情報",
"addCredentialForUser": "認証情報を追加",
"noCredentialsForUser": "このユーザーには認証情報がありません",
"deleteCredentialConfirm": "{{username}} に属する認証情報 \"{{name}}\" を削除しますか?",
"credentialDeletedSuccess": "認証情報が削除されました",
"credentialDeleteFailed": "認証情報の削除に失敗しました",
"snippetsCount": "{{count}} スニペット",
"addSnippetForUser": "スニペットを追加",
"noSnippetsForUser": "このユーザーにはスニペットがありません",
"snippetRequiredFields": "スニペット名とコンテンツは必須です。",
"snippetNamePlaceholder": "スニペット名",
"snippetContentPlaceholder": "コマンドの内容",
"snippetFolderPlaceholder": "フォルダ(オプション)",
"snippetSaved": "スニペットが保存されました",
"snippetSaveFailed": "スニペットの保存に失敗しました",
"deleteSnippetConfirm": "{{username}} に属するスニペット \"{{name}}\" を削除しますか?",
"snippetDeletedSuccess": "抜粋が削除されました",
"snippetDeleteFailed": "スニペットの削除に失敗しました",
"noSessionsForUser": "アクティブなセッションはありません",
"deleteUserDangerDesc": "{{username}} とそのすべてのデータ(ホスト、認証情報、スニペット、履歴)を完全に削除します。この操作は元に戻せません。",
"deleteUserConfirm": "{{username}} とそのすべてのデータを完全に削除しますか?",
"deleteUserAdminBlocked": "このユーザーを削除する前に、管理者権限を解除してください。",
"createRoleRequired": "名前と表示名は必須です。",
"createRoleSuccess": "ロール \"{{name}}\" が作成されました",
"createRoleFailed": "ロールの作成に失敗しました",
@@ -2822,7 +2942,16 @@
"hostDefaultsCommandHistory": "コマンド履歴",
"hostDefaultsCommandHistoryDesc": "デフォルトでは、新しいホストでコマンド履歴を追跡します。",
"hostDefaultsSaved": "ホストのデフォルト設定が保存されました",
"hostDefaultsSaveFailed": "ホストのデフォルト設定の保存に失敗しました"
"hostDefaultsSaveFailed": "ホストのデフォルト設定の保存に失敗しました",
"rolePermissions": {
"count": "{{count}} 権限",
"editAction": "編集権限",
"loadError": "権限カタログの読み込みに失敗しました",
"saved": "ロール権限が保存されました",
"saveError": "ロール権限の保存に失敗しました",
"save": "保存",
"saving": "保存中..."
}
},
"newUi": {
"sidebar": {
@@ -2991,6 +3120,10 @@
"executionFailed": "失敗した"
},
"userProfile": {
"donateTitle": "Termixを存続させよう",
"donateDescription": "Termixは無料で、セルフホスティング方式で、少数の開発者が余暇を利用して開発しています。もしTermixがあなたの時間や費用を節約できたなら、仮想通貨での寄付が開発継続の助けになります。",
"donateMilestones": "寄付金は、SAML、Kubernetes、およびエージェントのサポート構築に必要な調査や学習に費やす時間を支援するために活用されます。進捗状況を確認し、ぜひご寄付をお願いいたします。",
"donateButton": "仮想通貨を寄付する",
"storageModeLocal": "ブラウザ",
"storageModeCloud": "データベース",
"storageModeDescription": "ブラウザは設定をこのブラウザのみに保存します。データベースはサーバーと同期され、あらゆるデバイスに読み込まれます。",
@@ -3011,6 +3144,10 @@
"twoFaOn": "の上",
"twoFaOff": "オフ",
"versionLabel": "バージョン",
"betaProgramTitle": "ベータプログラム",
"betaProgramDescription": "毎週ローリングされる :beta Docker タグを使用して、今後の新機能をいち早くお試しください。不安定なため、本番環境での使用は推奨しません。",
"betaProgramFeedback": "バグを見つけましたか?こちらから報告してください。",
"betaProgramLearnMore": "もっと詳しく知る",
"deleteAccount": "アカウントを削除",
"deleteAccountDescription": "アカウントを完全に削除する",
"changeServerDescription": "別のTermixバックエンドサーバーに切り替える",
+158 -21
View File
@@ -864,6 +864,21 @@
"proxmoxDockerDetectionDesc": "Docker가 게스트를 일치시킬 수 있도록 쉼표로 구분된 이름 패턴입니다.",
"proxmoxPreferredRanges": "선호하는 IP 범위",
"proxmoxPreferredRangesDesc": "게스트 운영체제에 여러 인터페이스가 있는 경우 IP 주소 선택 우선순위를 쉼표로 구분하여 입력합니다.",
"proxmoxAutoSync": "게스트 자동 동기화",
"proxmoxAutoSyncDesc": "주기적으로 이 Proxmox 노드를 검색하고 세션이 잠금 해제된 동안 가져온 게스트 호스트를 생성하거나 업데이트하세요.",
"proxmoxSyncInterval": "동기화 간격(분)",
"proxmoxSyncIntervalDesc": "최소 5분. 스케줄러는 잠긴 사용자 세션을 건너뜁니다.",
"proxmoxMarkMissing": "누락된 손님을 표시하세요",
"proxmoxMarkMissingDesc": "이전에 불러온 게스트가 삭제되는 대신 사라질 경우 proxmox-missing 태그를 추가합니다.",
"proxmoxLastSync": "마지막 동기화",
"proxmoxLastSyncNever": "아직 동기화되지 않았습니다",
"proxmoxLastSyncNoResult": "결과 세부 정보 없음",
"proxmoxLastSyncSummary": "{{created}} 생성됨, {{updated}} 업데이트됨, {{markedMissing}} 누락됨, {{skipped}} 건너뛰어짐",
"proxmoxLastSyncStatus": {
"success": "성공",
"error": "실패한",
"pending": "보류 중"
},
"proxmoxDiscoverAction": "Proxmox 게스트를 검색하고 가져오세요.",
"proxmoxImportTitle": "Proxmox에서 가져오기",
"proxmoxSelectHost": "Proxmox 호스트를 선택하세요…",
@@ -1167,7 +1182,57 @@
"shareHost": "호스트 공유",
"shareHostTitle": "공유: {{name}}",
"sharing": {
"requiresCredential": "이 호스트에서 공유를 활성화하려면 자격 증명이 필요합니다. 먼저 호스트를 편집하고 자격 증명을 할당하십시오."
"loadError": "공유 데이터를 불러오는 데 실패했습니다. 다시 시도해 주세요.",
"shareWithSection": "공유하기",
"usersTab": "사용자",
"rolesTab": "역할",
"searchPlaceholder": "사용자 또는 역할을 검색하세요...",
"noMatches": "일치하는 항목이 없습니다.",
"permissionLevelLabel": "권한 수준",
"levels": {
"connect": {
"label": "연결하다",
"description": "터미널, 원격 데스크톱, 파일 관리자, 터널 및 Docker 세션만 사용 가능합니다. 호스트 구성에는 접근할 수 없습니다."
},
"view": {
"label": "보다",
"description": "연결하고 호스트 구성을 확인하세요. 비밀 키는 절대 표시되지 않습니다."
},
"edit": {
"label": "편집하다",
"description": "호스트를 보고 수정할 수 있습니다. 비밀 키는 교체할 수 있지만 읽을 수는 없으며, 자격 증명 할당은 소유자만 사용할 수 있습니다."
},
"manage": {
"label": "관리하다",
"description": "호스트를 편집하고, 다른 사람과 공유하고, 권한 수준을 변경하고, 액세스 권한을 취소할 수 있습니다."
}
},
"expiryLabel": "접근 만료",
"expiry": {
"never": "절대",
"oneHour": "1시간",
"oneDay": "24시간",
"sevenDays": "7일",
"thirtyDays": "30일",
"custom": "관습"
},
"customHoursPlaceholder": "접속 만료까지 남은 시간",
"shareButton": "공유하다",
"shareWithCount": "공유({{count}})",
"currentAccess": "현재 접근 권한",
"noAccessEntries": "이 호스트는 아직 공유되지 않았습니다.",
"grantedBy": "승인됨",
"expires": "만료됨",
"expired": "만료됨",
"never": "절대",
"revoke": "취소",
"accessUpdated": "액세스 업데이트됨",
"accessUpdateFailed": "접근 권한 업데이트에 실패했습니다.",
"sharedBadge": "공유됨",
"sharedBadgeTooltip": "{{owner}} 님이 공유함 ({{level}} 접근 권한)",
"viewOnlyBanner": "이 호스트는 {{owner}} 님과 보기 권한으로 공유됩니다. 구성은 읽기 전용입니다.",
"sharedEditBanner": "이 호스트는 {{owner}} 님이 편집 권한으로 공유하고 있습니다. 변경 사항은 실제 호스트에 적용되며, 인증 참조는 소유자만 변경할 수 있습니다.",
"ownerOnlyControl": "호스트 소유자만 이 설정을 변경할 수 있습니다."
},
"guac": {
"connection": "연결",
@@ -1302,28 +1367,10 @@
"backspaceKey": "백스페이스 키",
"saveHostFirst": "먼저 호스트를 저장하세요.",
"sharingOptionsAfterSave": "호스트를 저장한 후에는 공유 옵션을 사용할 수 있습니다.",
"sharingLoadError": "공유 데이터를 불러오는 데 실패했습니다. 연결 상태를 확인하고 다시 시도하세요.",
"shareHostSection": "호스트 공유",
"shareWithUser": "사용자와 공유",
"shareWithRole": "역할과 공유하기",
"selectUser": "사용자 선택",
"selectRole": "역할 선택",
"selectUserOption": "사용자를 선택하세요...",
"selectRoleOption": "역할을 선택하세요...",
"permissionLevel": "권한 수준",
"expiresInHours": "(시간) 후에 만료됩니다.",
"noExpiryPlaceholder": "유통기한이 없으려면 빈칸으로 두십시오.",
"shareBtn": "공유하다",
"currentAccess": "현재 액세스",
"typeHeader": "유형",
"targetHeader": "목표",
"permissionHeader": "허가",
"grantedByHeader": "승인자:",
"expiresHeader": "만료됨",
"noAccessEntries": "아직 접근 기록이 없습니다.",
"expiredLabel": "만료됨",
"neverLabel": "절대",
"revokeBtn": "취소",
"cancelBtn": "취소",
"savingBtn": "절약...",
"updateHostBtn": "호스트 업데이트",
@@ -2190,7 +2237,8 @@
"passwordLoginDisabledDesc": "비밀번호 로그인이 비활성화되었습니다. 패스키 또는 외부 인증 공급자를 사용하십시오.",
"signInWithPasskey": "패스키로 로그인하세요",
"passkeyLoginFailed": "암호키 로그인 실패",
"attemptsRemaining": "남은 시도 횟수 {{count}} 회"
"attemptsRemaining": "남은 시도 횟수 {{count}} 회",
"confirmResetDataWipe": "이 계정은 암호화 업그레이드 이후 로그인한 적이 없으므로 이전 비밀번호 없이는 저장된 데이터를 복구할 수 없습니다. 비밀번호를 재설정하면 호스트, 자격 증명 및 스니펫이 영구적으로 삭제됩니다. 계속하시겠습니까?"
},
"hostKey": {
"verifyNewHost": "SSH 호스트 키 확인",
@@ -2323,6 +2371,13 @@
"serviceLinks": "서비스 링크",
"homepagePreview": "홈페이지 미리보기"
},
"donation": {
"title": "Termix를 즐겁게 사용하고 계신가요?",
"body": "Termix는 무료 오픈 소스 소프트웨어로, 두 명의 팀원이 여가 시간을 활용하여 개발 및 유지 관리하고 있습니다. 만약 Termix를 통해 기존에 사용하던 유료 도구를 대체하게 되었다면, 기부를 통해 호스팅 비용을 충당하고 지속적인 개발을 지원할 수 있습니다. 현재는 암호화폐로만 기부가 가능합니다.",
"milestones": "기부금은 SAML 및 Kubernetes 지원과 같은 기능을 구축하는 데 필요한 연구 및 학습 시간을 지원하는 데에도 사용됩니다. 기부 페이지에서 진행 상황을 확인하세요.",
"cta": "기부하세요",
"dismiss": "나중에요"
},
"dashboardTab": {
"stable": "안정적인",
"hostsOnline": "호스트 온라인",
@@ -2790,6 +2845,71 @@
"updateAdminStatusFailed": "관리자 상태 업데이트에 실패했습니다.",
"allSessionsRevoked": "모든 세션이 취소되었습니다",
"revokeSessionsFailed": "세션 취소에 실패했습니다",
"manageUserData": "사용자 데이터 관리",
"backToUsers": "사용자 목록으로 돌아가기",
"manageTabAccount": "계정",
"manageTabHosts": "호스트",
"manageTabCredentials": "신임장",
"manageTabSnippets": "짧은 발췌",
"manageTabSessions": "세션",
"manageTabDanger": "위험",
"manageEditorBack": "{{username}}의 데이터로 돌아가기",
"dataLockedBadge": "잠김",
"dataLockedNotice": "{{username}}사용자의 데이터는 다음 로그인까지 잠금 상태로 유지됩니다. 그 전까지는 호스트, 자격 증명 및 스니펫을 보거나 편집할 수 없습니다.",
"resetPasswordTitle": "비밀번호 재설정",
"resetPasswordOidcOnly": "이 사용자는 외부 제공업체를 통해 로그인하며 비밀번호가 없습니다.",
"resetPasswordPlaceholder": "새 비밀번호",
"resetPasswordBtn": "다시 놓기",
"resetPasswordWorking": "재설정 중...",
"resetPasswordSuccess": "비밀번호 재설정",
"resetPasswordSuccessWiped": "비밀번호가 재설정되었습니다. 사용자의 암호화된 데이터가 삭제되었습니다.",
"resetPasswordFailed": "비밀번호 재설정에 실패했습니다",
"resetPasswordConfirmWipe": "{{username}} 님은 암호화 업그레이드 이후 로그인하지 않았으므로 데이터를 복구할 수 없습니다. 지금 초기화하면 호스트, 자격 증명 및 스니펫이 삭제됩니다. 계속하시겠습니까?",
"totpSectionTitle": "2단계 인증",
"totpStatusEnabled": "이 사용자에게 TOTP가 활성화되어 있습니다.",
"totpStatusDisabled": "이 사용자에게는 TOTP가 활성화되어 있지 않습니다.",
"disableTotp": "장애를 입히다",
"disableTotpConfirm": "{{username}}에 대한 2단계 인증을 비활성화하시겠습니까? 그러면 해당 사용자는 비밀번호만으로 로그인할 수 있습니다.",
"totpDisabledSuccess": "TOTP 비활성화됨",
"totpDisableFailed": "TOTP를 비활성화하는 데 실패했습니다.",
"manageApiKeys": "API 키",
"apiKeyNamePlaceholder": "키 이름",
"apiKeyCopyNotice": "이 키를 지금 복사하세요. 다시는 표시되지 않습니다.",
"noApiKeysForUser": "API 키 없음",
"apiKeyDeleteFailed": "API 키 삭제에 실패했습니다.",
"exportUserData": "데이터 내보내기",
"exportUserDataDesc": "이 사용자의 호스트, 자격 증명 및 파일 관리자 데이터를 JSON 형식으로 다운로드합니다. 비밀 키는 복호화됩니다.",
"exportUserDataSuccess": "사용자 데이터 내보내기",
"exportUserDataFailed": "사용자 데이터 내보내기에 실패했습니다.",
"hostsCount": "{{count}} 호스트",
"addHostForUser": "호스트 추가",
"noHostsForUser": "이 사용자는 호스트가 없습니다.",
"connectToHost": "연결하다",
"deleteHostConfirm": "{{username}}에 속한 호스트 \"{{name}}\"를 삭제하시겠습니까?",
"hostDeletedSuccess": "호스트가 삭제했습니다",
"hostDeleteFailed": "호스트 삭제에 실패했습니다",
"credentialsCount": "{{count}} 자격 증명",
"addCredentialForUser": "자격 증명 추가",
"noCredentialsForUser": "이 사용자는 자격 증명이 없습니다.",
"deleteCredentialConfirm": "{{username}}에 속한 자격 증명 \"{{name}}\"을 삭제하시겠습니까?",
"credentialDeletedSuccess": "자격 증명이 삭제되었습니다.",
"credentialDeleteFailed": "자격 증명 삭제에 실패했습니다.",
"snippetsCount": "{{count}} 스니펫",
"addSnippetForUser": "코드 조각 추가",
"noSnippetsForUser": "이 사용자는 스니펫이 없습니다.",
"snippetRequiredFields": "스니펫 이름과 내용은 필수 입력 사항입니다.",
"snippetNamePlaceholder": "스니펫 이름",
"snippetContentPlaceholder": "명령 내용",
"snippetFolderPlaceholder": "폴더 (선택 사항)",
"snippetSaved": "코드 조각이 저장되었습니다.",
"snippetSaveFailed": "코드 조각을 저장하는 데 실패했습니다.",
"deleteSnippetConfirm": "{{username}}에 속하는 코드 조각 \"{{name}}\"을 삭제하시겠습니까?",
"snippetDeletedSuccess": "일부 내용이 삭제되었습니다.",
"snippetDeleteFailed": "코드 조각을 삭제하는 데 실패했습니다.",
"noSessionsForUser": "활성 세션 없음",
"deleteUserDangerDesc": "{{username}} 및 해당 데이터(호스트, 자격 증명, 스니펫, 기록)를 영구적으로 삭제합니다. 이 작업은 되돌릴 수 없습니다.",
"deleteUserConfirm": "{{username}} 와 그 모든 데이터를 영구적으로 삭제하시겠습니까?",
"deleteUserAdminBlocked": "이 사용자를 삭제하기 전에 관리자 권한을 제거하십시오.",
"createRoleRequired": "이름과 표시 이름은 필수 입력 사항입니다.",
"createRoleSuccess": "역할 \"{{name}}\"이 생성되었습니다",
"createRoleFailed": "역할 생성에 실패했습니다",
@@ -2822,7 +2942,16 @@
"hostDefaultsCommandHistory": "명령 기록",
"hostDefaultsCommandHistoryDesc": "새 호스트에서 기본적으로 명령 기록을 추적합니다.",
"hostDefaultsSaved": "호스트 기본 설정이 저장되었습니다.",
"hostDefaultsSaveFailed": "호스트 기본 설정을 저장하는 데 실패했습니다."
"hostDefaultsSaveFailed": "호스트 기본 설정을 저장하는 데 실패했습니다.",
"rolePermissions": {
"count": "{{count}} 권한",
"editAction": "편집 권한",
"loadError": "권한 카탈로그를 불러오는 데 실패했습니다.",
"saved": "역할 권한이 저장되었습니다",
"saveError": "역할 권한 저장에 실패했습니다.",
"save": "구하다",
"saving": "절약..."
}
},
"newUi": {
"sidebar": {
@@ -2991,6 +3120,10 @@
"executionFailed": "실패한"
},
"userProfile": {
"donateTitle": "Termix를 계속 유지하세요",
"donateDescription": "Termix는 무료이며 자체 호스팅되고, 몇몇 사람들이 여가 시간을 활용하여 개발했습니다. Termix가 시간이나 비용을 절약하는 데 도움이 되었다면 암호화폐 기부를 통해 지속적인 운영을 지원해 주세요.",
"donateMilestones": "기부금은 SAML, Kubernetes 및 에이전트 지원 구축에 필요한 연구 및 학습 시간을 지원하는 데 사용됩니다. 진행 상황을 확인하고 기부해 주세요.",
"donateButton": "암호화폐 기부",
"storageModeLocal": "브라우저",
"storageModeCloud": "데이터 베이스",
"storageModeDescription": "브라우저는 설정을 해당 브라우저에만 저장합니다. 데이터베이스는 서버와 동기화되어 모든 기기에서 불러올 수 있습니다.",
@@ -3011,6 +3144,10 @@
"twoFaOn": "~에",
"twoFaOff": "끄다",
"versionLabel": "버전",
"betaProgramTitle": "베타 프로그램",
"betaProgramDescription": "매주 업데이트되는 `:beta` Docker 태그를 사용하여 출시 예정인 기능을 미리 사용해 보세요. 불안정한 환경이므로 프로덕션 환경에는 적합하지 않습니다.",
"betaProgramFeedback": "버그를 발견하셨나요? 여기에서 신고해 주세요.",
"betaProgramLearnMore": "더 알아보기",
"deleteAccount": "계정 삭제",
"deleteAccountDescription": "계정을 영구적으로 삭제하세요",
"changeServerDescription": "다른 Termix 백엔드 서버로 전환하세요",
+158 -21
View File
@@ -864,6 +864,21 @@
"proxmoxDockerDetectionDesc": "Door komma's gescheiden naampatronen waarmee Docker gastsystemen kan herkennen.",
"proxmoxPreferredRanges": "Voorkeurs-IP-bereiken",
"proxmoxPreferredRangesDesc": "Door komma's gescheiden voorvoegsels in prioriteitsvolgorde voor IP-selectie wanneer een gast meerdere interfaces heeft.",
"proxmoxAutoSync": "Automatische synchronisatie van gasten",
"proxmoxAutoSyncDesc": "Ontdek periodiek dit Proxmox-knooppunt en maak of update geïmporteerde gasthosts terwijl uw sessie ontgrendeld is.",
"proxmoxSyncInterval": "Synchronisatie-interval (minuten)",
"proxmoxSyncIntervalDesc": "Minimaal 5 minuten. De planner slaat vergrendelde gebruikerssessies over.",
"proxmoxMarkMissing": "Markeer ontbrekende gasten",
"proxmoxMarkMissingDesc": "Voeg een 'proxmox-missing'-tag toe wanneer een eerder geïmporteerde gast verdwijnt, in plaats van deze te verwijderen.",
"proxmoxLastSync": "Laatste synchronisatie",
"proxmoxLastSyncNever": "Nog niet gesynchroniseerd",
"proxmoxLastSyncNoResult": "Geen resultaatdetails",
"proxmoxLastSyncSummary": "{{created}} aangemaakt, {{updated}} bijgewerkt, {{markedMissing}} ontbreekt, {{skipped}} overgeslagen",
"proxmoxLastSyncStatus": {
"success": "Succes",
"error": "Mislukt",
"pending": "In behandeling"
},
"proxmoxDiscoverAction": "Ontdek en importeer Proxmox-gasten",
"proxmoxImportTitle": "Importeren vanuit Proxmox",
"proxmoxSelectHost": "Selecteer een Proxmox-host…",
@@ -1167,7 +1182,57 @@
"shareHost": "Deel host",
"shareHostTitle": "Delen: {{name}}",
"sharing": {
"requiresCredential": "Deze host moet een inloggegeven gebruiken om delen mogelijk te maken. Bewerk eerst de host en wijs een inloggegeven toe."
"loadError": "Het laden van de deelgegevens is mislukt. Probeer het opnieuw.",
"shareWithSection": "Delen met",
"usersTab": "Gebruikers",
"rolesTab": "Rollen",
"searchPlaceholder": "Gebruikers of rollen zoeken...",
"noMatches": "Geen overeenkomsten gevonden",
"permissionLevelLabel": "Toegangsniveau",
"levels": {
"connect": {
"label": "Verbinden",
"description": "Alleen open sessies: terminal, extern bureaublad, bestandsbeheer, tunnels en Docker. Geen toegang tot de hostconfiguratie."
},
"view": {
"label": "Weergave",
"description": "Maak verbinding en bekijk de hostconfiguratie. Geheime gegevens worden nooit weergegeven."
},
"edit": {
"label": "Bewerking",
"description": "Bekijk en wijzig de host. Geheimen kunnen worden vervangen, maar nooit gelezen; toegangsrechten blijven alleen voor de eigenaar."
},
"manage": {
"label": "Beheren",
"description": "Bewerken, de host delen met anderen, toegangsrechten wijzigen en toegang intrekken."
}
},
"expiryLabel": "Toegang verloopt",
"expiry": {
"never": "Nooit",
"oneHour": "1 uur",
"oneDay": "24 uur",
"sevenDays": "7 dagen",
"thirtyDays": "30 dagen",
"custom": "Aangepast"
},
"customHoursPlaceholder": "Aantal uren tot de toegang verloopt",
"shareButton": "Deel",
"shareWithCount": "Delen ({{count}})",
"currentAccess": "Huidige toegang",
"noAccessEntries": "Deze host is nog niet gedeeld.",
"grantedBy": "Toegekend door",
"expires": "Verloopt",
"expired": "Verlopen",
"never": "Nooit",
"revoke": "Herroepen",
"accessUpdated": "Toegang bijgewerkt",
"accessUpdateFailed": "Toegang kon niet worden bijgewerkt.",
"sharedBadge": "Gedeeld",
"sharedBadgeTooltip": "Gedeeld door {{owner}} ({{level}} toegang)",
"viewOnlyBanner": "Deze host wordt met u gedeeld door {{owner}} met leesrechten. De configuratie is alleen-lezen.",
"sharedEditBanner": "Deze host wordt met u gedeeld door {{owner}} met bewerkingsrechten. Wijzigingen zijn van toepassing op de daadwerkelijke host; authenticatiegegevens kunnen alleen door de eigenaar worden gewijzigd.",
"ownerOnlyControl": "Alleen de eigenaar van de host kan dit wijzigen."
},
"guac": {
"connection": "Verbinding",
@@ -1302,28 +1367,10 @@
"backspaceKey": "Backspace-toets",
"saveHostFirst": "Sla eerst de host op.",
"sharingOptionsAfterSave": "Deelopties zijn beschikbaar nadat de host is opgeslagen.",
"sharingLoadError": "Het laden van de deelgegevens is mislukt. Controleer uw verbinding en probeer het opnieuw.",
"shareHostSection": "Deel host",
"shareWithUser": "Delen met gebruiker",
"shareWithRole": "Delen met Role",
"selectUser": "Gebruiker selecteren",
"selectRole": "Selecteer rol",
"selectUserOption": "Selecteer een gebruiker...",
"selectRoleOption": "Kies een rol...",
"permissionLevel": "Toegangsniveau",
"expiresInHours": "Verloopt over (uren)",
"noExpiryPlaceholder": "Laat dit veld leeg voor onbeperkte houdbaarheid.",
"shareBtn": "Deel",
"currentAccess": "Huidige toegang",
"typeHeader": "Type",
"targetHeader": "Doel",
"permissionHeader": "Toestemming",
"grantedByHeader": "Toegekend door",
"expiresHeader": "Verloopt",
"noAccessEntries": "Nog geen toegangsgegevens beschikbaar.",
"expiredLabel": "Verlopen",
"neverLabel": "Nooit",
"revokeBtn": "Herroepen",
"cancelBtn": "Annuleren",
"savingBtn": "Besparing...",
"updateHostBtn": "Update Host",
@@ -2190,7 +2237,8 @@
"passwordLoginDisabledDesc": "Inloggen met wachtwoord is uitgeschakeld. Gebruik een toegangscode of een externe authenticatieprovider.",
"signInWithPasskey": "Aanmelden met wachtwoord",
"passkeyLoginFailed": "Aanmelden met wachtwoord mislukt",
"attemptsRemaining": "{{count}} resterende pogingen"
"attemptsRemaining": "{{count}} resterende pogingen",
"confirmResetDataWipe": "Dit account is sinds de upgrade van de encryptie niet meer gebruikt, waardoor de opgeslagen gegevens niet kunnen worden hersteld zonder het oude wachtwoord. Door het wachtwoord opnieuw in te stellen, worden de hosts, inloggegevens en codefragmenten permanent verwijderd. Doorgaan?"
},
"hostKey": {
"verifyNewHost": "Verifieer de SSH-hostsleutel",
@@ -2323,6 +2371,13 @@
"serviceLinks": "Servicelinks",
"homepagePreview": "Startpagina-voorbeeld"
},
"donation": {
"title": "Vind je Termix lekker?",
"body": "Termix is gratis en open source, ontwikkeld en onderhouden door een team van twee personen in hun vrije tijd. Als het een commerciële tool heeft vervangen waar je anders voor zou betalen, helpt een donatie om de hostingkosten te dekken en de ontwikkeling voort te zetten. Op dit moment accepteren we alleen donaties in cryptovaluta.",
"milestones": "Donaties helpen ook bij het financieren van de tijd die nodig is voor onderzoek en het leren van wat vereist is om functies zoals SAML- en Kubernetes-ondersteuning te ontwikkelen. Bekijk de voortgang op de donatiepagina.",
"cta": "Doneer",
"dismiss": "Misschien later"
},
"dashboardTab": {
"stable": "STABIEL",
"hostsOnline": "Hosts Online",
@@ -2790,6 +2845,71 @@
"updateAdminStatusFailed": "Het bijwerken van de beheerdersstatus is mislukt.",
"allSessionsRevoked": "Alle sessies zijn ingetrokken.",
"revokeSessionsFailed": "Sessies konden niet worden ingetrokken.",
"manageUserData": "Gebruikersgegevens beheren",
"backToUsers": "Terug naar gebruikers",
"manageTabAccount": "Rekening",
"manageTabHosts": "Gastheren",
"manageTabCredentials": "Referenties",
"manageTabSnippets": "Fragmenten",
"manageTabSessions": "Sessies",
"manageTabDanger": "Gevaar",
"manageEditorBack": "Terug naar de gegevens van {{username}}",
"dataLockedBadge": "VERGRENDELD",
"dataLockedNotice": "De gegevens van {{username}}blijven vergrendeld tot de volgende keer dat ze inloggen. Hun hosts, inloggegevens en codefragmenten kunnen tot die tijd niet worden bekeken of bewerkt.",
"resetPasswordTitle": "Wachtwoord opnieuw instellen",
"resetPasswordOidcOnly": "Deze gebruiker meldt zich aan via een externe provider en heeft geen wachtwoord.",
"resetPasswordPlaceholder": "Nieuw wachtwoord",
"resetPasswordBtn": "Reset",
"resetPasswordWorking": "Opnieuw instellen...",
"resetPasswordSuccess": "Wachtwoord opnieuw instellen",
"resetPasswordSuccessWiped": "Wachtwoord gereset. De versleutelde gegevens van de gebruiker zijn gewist.",
"resetPasswordFailed": "Wachtwoord opnieuw instellen mislukt",
"resetPasswordConfirmWipe": "{{username}} heeft niet meer ingelogd sinds de upgrade van de encryptie, waardoor hun gegevens niet kunnen worden hersteld. Door nu opnieuw in te stellen, worden hun hosts, inloggegevens en codefragmenten verwijderd. Doorgaan?",
"totpSectionTitle": "Tweefactorauthenticatie",
"totpStatusEnabled": "TOTP is ingeschakeld voor deze gebruiker.",
"totpStatusDisabled": "TOTP is niet ingeschakeld voor deze gebruiker.",
"disableTotp": "Uitzetten",
"disableTotpConfirm": "Schakel tweefactorauthenticatie uit voor {{username}}? Zij kunnen dan inloggen met alleen hun wachtwoord.",
"totpDisabledSuccess": "TOTP uitgeschakeld",
"totpDisableFailed": "TOTP uitschakelen is mislukt.",
"manageApiKeys": "API-sleutels",
"apiKeyNamePlaceholder": "Sleutelnaam",
"apiKeyCopyNotice": "Kopieer deze sleutel nu, hij wordt niet meer weergegeven.",
"noApiKeysForUser": "Geen API-sleutels",
"apiKeyDeleteFailed": "Het verwijderen van de API-sleutel is mislukt.",
"exportUserData": "Gegevens exporteren",
"exportUserDataDesc": "Download de hosts, inloggegevens en bestandsbeheergegevens van deze gebruiker als JSON. Geheimen zijn gedecodeerd.",
"exportUserDataSuccess": "Gebruikersgegevens geëxporteerd",
"exportUserDataFailed": "Exporteren van gebruikersgegevens is mislukt.",
"hostsCount": "{{count}} hosts",
"addHostForUser": "Voeg een host toe",
"noHostsForUser": "Deze gebruiker heeft geen hosts.",
"connectToHost": "Verbinden",
"deleteHostConfirm": "Host \"{{name}}\" verwijderen die bij {{username}} hoort?",
"hostDeletedSuccess": "Host verwijderd",
"hostDeleteFailed": "Het verwijderen van de host is mislukt.",
"credentialsCount": "{{count}} inloggegevens",
"addCredentialForUser": "Voeg inloggegevens toe",
"noCredentialsForUser": "Deze gebruiker heeft geen inloggegevens.",
"deleteCredentialConfirm": "Verwijder de inloggegevens \"{{name}}\" die bij {{username}} horen.",
"credentialDeletedSuccess": "Inloggegevens verwijderd",
"credentialDeleteFailed": "Het verwijderen van de inloggegevens is mislukt.",
"snippetsCount": "{{count}} fragmenten",
"addSnippetForUser": "Voeg codefragment toe",
"noSnippetsForUser": "Deze gebruiker heeft geen codefragmenten.",
"snippetRequiredFields": "De naam en inhoud van het codefragment zijn verplicht.",
"snippetNamePlaceholder": "Fragmentnaam",
"snippetContentPlaceholder": "Opdrachtinhoud",
"snippetFolderPlaceholder": "Map (optioneel)",
"snippetSaved": "Fragment opgeslagen",
"snippetSaveFailed": "Het opslaan van het codefragment is mislukt.",
"deleteSnippetConfirm": "Verwijder het fragment \"{{name}}\" dat bij {{username}} hoort.",
"snippetDeletedSuccess": "Fragment verwijderd",
"snippetDeleteFailed": "Het verwijderen van het fragment is mislukt.",
"noSessionsForUser": "Geen actieve sessies",
"deleteUserDangerDesc": "Verwijder {{username}} en alle bijbehorende gegevens (hosts, inloggegevens, codefragmenten, geschiedenis) permanent. Deze actie kan niet ongedaan worden gemaakt.",
"deleteUserConfirm": "Wil je {{username}} en al hun gegevens permanent verwijderen?",
"deleteUserAdminBlocked": "Verwijder de beheerdersstatus voordat u deze gebruiker verwijdert.",
"createRoleRequired": "Naam en weergavenaam zijn verplicht.",
"createRoleSuccess": "Rol \"{{name}}\" aangemaakt",
"createRoleFailed": "Het aanmaken van de rol is mislukt.",
@@ -2822,7 +2942,16 @@
"hostDefaultsCommandHistory": "Commandogeschiedenis",
"hostDefaultsCommandHistoryDesc": "Volg standaard de opdrachtgeschiedenis op nieuwe hosts.",
"hostDefaultsSaved": "Host-standaardinstellingen opgeslagen",
"hostDefaultsSaveFailed": "Het opslaan van de hoststandaardinstellingen is mislukt."
"hostDefaultsSaveFailed": "Het opslaan van de hoststandaardinstellingen is mislukt.",
"rolePermissions": {
"count": "{{count}} machtigingen",
"editAction": "Machtigingen bewerken",
"loadError": "Het laden van de machtigingencatalogus is mislukt.",
"saved": "Rolmachtigingen opgeslagen",
"saveError": "Het opslaan van rolmachtigingen is mislukt.",
"save": "Redden",
"saving": "Besparing..."
}
},
"newUi": {
"sidebar": {
@@ -2991,6 +3120,10 @@
"executionFailed": "Mislukt"
},
"userProfile": {
"donateTitle": "Houd Termix in leven",
"donateDescription": "Termix is gratis, zelf te hosten en is ontwikkeld door slechts een paar mensen in hun vrije tijd. Als het je tijd of geld heeft bespaard, helpt een cryptodonatie om het project draaiende te houden.",
"donateMilestones": "Donaties helpen ons de tijd te financieren die nodig is voor onderzoek en het leren van de benodigde kennis om SAML-, Kubernetes- en Agent-ondersteuning te ontwikkelen. Bekijk de voortgang en doneer.",
"donateButton": "Doneer crypto",
"storageModeLocal": "Browser",
"storageModeCloud": "Database",
"storageModeDescription": "De browser slaat instellingen alleen in deze browser op. De database synchroniseert met de server en laadt op elk apparaat.",
@@ -3011,6 +3144,10 @@
"twoFaOn": "Op",
"twoFaOff": "Uit",
"versionLabel": "Versie",
"betaProgramTitle": "Bèta-programma",
"betaProgramDescription": "Test aankomende functies vroegtijdig met de wekelijkse, doorlopende :beta Docker-tag. Deze is niet stabiel en niet geschikt voor productie.",
"betaProgramFeedback": "Een bug gevonden? Meld hem hier.",
"betaProgramLearnMore": "Meer informatie",
"deleteAccount": "Account verwijderen",
"deleteAccountDescription": "Verwijder je account permanent",
"changeServerDescription": "Schakel over naar een andere Termix-backendserver.",
+158 -21
View File
@@ -864,6 +864,21 @@
"proxmoxDockerDetectionDesc": "Kommaseparerte navnemønstre som gjør det mulig for Docker å matche gjester",
"proxmoxPreferredRanges": "Foretrukne IP-områder",
"proxmoxPreferredRangesDesc": "Kommaseparerte prefikser i prioritert rekkefølge for IP-valg når en gjest har flere grensesnitt",
"proxmoxAutoSync": "Automatisk synkronisering av gjester",
"proxmoxAutoSyncDesc": "Oppdag denne Proxmox-noden med jevne mellomrom og opprett eller oppdater importerte gjesteverter mens økten din er ulåst.",
"proxmoxSyncInterval": "Synkroniseringsintervall (minutter)",
"proxmoxSyncIntervalDesc": "Minimum 5 minutter. Planleggeren hopper over låste brukerøkter.",
"proxmoxMarkMissing": "Merk manglende gjester",
"proxmoxMarkMissingDesc": "Legg til en proxmox-missing-tag når en tidligere importert gjest forsvinner i stedet for å slette den.",
"proxmoxLastSync": "Siste synkronisering",
"proxmoxLastSyncNever": "Ikke synkronisert ennå",
"proxmoxLastSyncNoResult": "Ingen resultatdetaljer",
"proxmoxLastSyncSummary": "{{created}} opprettet, {{updated}} oppdatert, {{markedMissing}} mangler, {{skipped}} hoppet over",
"proxmoxLastSyncStatus": {
"success": "Suksess",
"error": "Mislyktes",
"pending": "I påvente av"
},
"proxmoxDiscoverAction": "Oppdag og importer Proxmox-gjester",
"proxmoxImportTitle": "Importer fra Proxmox",
"proxmoxSelectHost": "Velg en Proxmox-vert…",
@@ -1167,7 +1182,57 @@
"shareHost": "Del vert",
"shareHostTitle": "Del: {{name}}",
"sharing": {
"requiresCredential": "Denne verten må bruke en legitimasjon for å aktivere deling. Rediger verten og tilordne en legitimasjon først."
"loadError": "Kunne ikke laste inn delingsdataene. Prøv på nytt.",
"shareWithSection": "Del med",
"usersTab": "Brukere",
"rolesTab": "Roller",
"searchPlaceholder": "Søk etter brukere eller roller...",
"noMatches": "Ingen treff funnet",
"permissionLevelLabel": "Tillatelsesnivå",
"levels": {
"connect": {
"label": "Koble til",
"description": "Kun åpne økter: terminal, eksternt skrivebord, filbehandler, tunneler og Docker. Ingen tilgang til vertskonfigurasjonen."
},
"view": {
"label": "Utsikt",
"description": "Koble til, pluss se vertskonfigurasjonen. Hemmeligheter vises aldri."
},
"edit": {
"label": "Redigere",
"description": "Vis og endre verten. Hemmeligheter kan erstattes, men aldri leses; tildeling av legitimasjon forblir kun for eieren."
},
"manage": {
"label": "Administrer",
"description": "Rediger, pluss del verten med andre, endre tillatelsesnivåer og tilbakekall tilgang."
}
},
"expiryLabel": "Utløpsdato for tilgang",
"expiry": {
"never": "Aldri",
"oneHour": "1 time",
"oneDay": "24 timer",
"sevenDays": "7 dager",
"thirtyDays": "30 dager",
"custom": "Skikk"
},
"customHoursPlaceholder": "Timer til tilgangen utløper",
"shareButton": "Dele",
"shareWithCount": "Del ({{count}})",
"currentAccess": "Nåværende tilgang",
"noAccessEntries": "Denne verten har ikke blitt delt ennå",
"grantedBy": "Tildelt av",
"expires": "Utløper",
"expired": "Utløpt",
"never": "Aldri",
"revoke": "Tilbakekalling",
"accessUpdated": "Tilgang oppdatert",
"accessUpdateFailed": "Kunne ikke oppdatere tilgangen",
"sharedBadge": "Delt",
"sharedBadgeTooltip": "Delt av {{owner}} ({{level}} tilgang)",
"viewOnlyBanner": "Denne verten deles med deg av {{owner}} med lesetilgang. Konfigurasjonen er skrivebeskyttet.",
"sharedEditBanner": "Denne verten deles med deg av {{owner}} med redigeringstilgang. Endringene gjelder for den virkelige verten; autentiseringsreferanser kan bare endres av eieren.",
"ownerOnlyControl": "Bare vertseieren kan endre dette"
},
"guac": {
"connection": "Forbindelse",
@@ -1302,28 +1367,10 @@
"backspaceKey": "Tilbaketast",
"saveHostFirst": "Lagre verten først.",
"sharingOptionsAfterSave": "Delingsalternativer er tilgjengelige etter at verten er lagret.",
"sharingLoadError": "Kunne ikke laste inn delingsdataene. Sjekk tilkoblingen din og prøv på nytt.",
"shareHostSection": "Del vert",
"shareWithUser": "Del med bruker",
"shareWithRole": "Del med rolle",
"selectUser": "Velg bruker",
"selectRole": "Velg rolle",
"selectUserOption": "Velg en bruker...",
"selectRoleOption": "Velg en rolle...",
"permissionLevel": "Tillatelsesnivå",
"expiresInHours": "Utløper om (timer)",
"noExpiryPlaceholder": "La stå tomt hvis det ikke er utløpt",
"shareBtn": "Dele",
"currentAccess": "Nåværende tilgang",
"typeHeader": "Type",
"targetHeader": "Mål",
"permissionHeader": "Tillatelse",
"grantedByHeader": "Tildelt av",
"expiresHeader": "Utløper",
"noAccessEntries": "Ingen tilgangsoppføringer ennå.",
"expiredLabel": "Utløpt",
"neverLabel": "Aldri",
"revokeBtn": "Tilbakekalling",
"cancelBtn": "Kansellere",
"savingBtn": "Lagrer...",
"updateHostBtn": "Oppdater vert",
@@ -2190,7 +2237,8 @@
"passwordLoginDisabledDesc": "Passordpålogging er deaktivert. Bruk en passordnøkkel eller en ekstern autentiseringsleverandør.",
"signInWithPasskey": "Logg inn med passord",
"passkeyLoginFailed": "Innlogging med passord mislyktes",
"attemptsRemaining": "{{count}} forsøk igjen"
"attemptsRemaining": "{{count}} forsøk igjen",
"confirmResetDataWipe": "Denne kontoen har ikke vært logget inn siden krypteringsoppgraderingen, så de lagrede dataene kan ikke gjenopprettes uten det gamle passordet. Tilbakestilling vil slette vertene, påloggingsinformasjonen og kodebitene permanent. Vil du fortsette?"
},
"hostKey": {
"verifyNewHost": "Bekreft SSH-vertsnøkkel",
@@ -2323,6 +2371,13 @@
"serviceLinks": "Tjenestelenker",
"homepagePreview": "Forhåndsvisning av hjemmeside"
},
"donation": {
"title": "Liker du Termix?",
"body": "Termix er gratis og åpen kildekode, bygget og vedlikeholdt av et team på to personer på fritiden. Hvis det erstatter et kommersielt verktøy du ellers ville betalt for, bidrar en donasjon til å dekke hostingkostnader og holde utviklingen i gang. Per nå er donasjoner kun kryptovaluta.",
"milestones": "Donasjoner bidrar også til å finansiere tiden som er nødvendig for å undersøke og lære hva som trengs for å bygge funksjoner som SAML og Kubernetes-støtte. Se fremdriften på donasjonssiden.",
"cta": "Donere",
"dismiss": "Kanskje senere"
},
"dashboardTab": {
"stable": "STABIL",
"hostsOnline": "Verter på nett",
@@ -2790,6 +2845,71 @@
"updateAdminStatusFailed": "Kunne ikke oppdatere administratorstatusen",
"allSessionsRevoked": "Alle økter tilbakekalt",
"revokeSessionsFailed": "Kunne ikke tilbakekalle økter",
"manageUserData": "Administrer brukerdata",
"backToUsers": "Tilbake til brukere",
"manageTabAccount": "Konto",
"manageTabHosts": "Verter",
"manageTabCredentials": "Legitimasjon",
"manageTabSnippets": "Utdrag",
"manageTabSessions": "Økter",
"manageTabDanger": "Fare",
"manageEditorBack": "Tilbake til dataene til {{username}}",
"dataLockedBadge": "LÅST",
"dataLockedNotice": "{{username}}sine data forblir låst til de logger seg inn igjen. Vertene, påloggingsinformasjonen og kodeutdragene deres kan ikke vises eller redigeres før da.",
"resetPasswordTitle": "Tilbakestill passord",
"resetPasswordOidcOnly": "Denne brukeren logger seg på via en ekstern leverandør og har ikke noe passord.",
"resetPasswordPlaceholder": "Nytt passord",
"resetPasswordBtn": "Tilbakestill",
"resetPasswordWorking": "Tilbakestiller...",
"resetPasswordSuccess": "Tilbakestilling av passord",
"resetPasswordSuccessWiped": "Tilbakestilling av passord. Brukerens krypterte data ble slettet.",
"resetPasswordFailed": "Kunne ikke tilbakestille passordet",
"resetPasswordConfirmWipe": "{{username}} har ikke logget inn siden krypteringsoppgraderingen, så dataene deres kan ikke gjenopprettes. Tilbakestilling nå vil slette vertene, påloggingsinformasjonen og kodebitene deres. Vil du fortsette?",
"totpSectionTitle": "Tofaktorautentisering",
"totpStatusEnabled": "TOTP er aktivert for denne brukeren",
"totpStatusDisabled": "TOTP er ikke aktivert for denne brukeren",
"disableTotp": "Deaktiver",
"disableTotpConfirm": "Deaktiver tofaktorautentisering for {{username}}? De vil kunne logge inn med bare passordet sitt.",
"totpDisabledSuccess": "TOTP deaktivert",
"totpDisableFailed": "Kunne ikke deaktivere TOTP",
"manageApiKeys": "API-nøkler",
"apiKeyNamePlaceholder": "Nøkkelnavn",
"apiKeyCopyNotice": "Kopier denne nøkkelen nå, den vil ikke vises igjen.",
"noApiKeysForUser": "Ingen API-nøkler",
"apiKeyDeleteFailed": "Kunne ikke slette API-nøkkelen",
"exportUserData": "Dataeksport",
"exportUserDataDesc": "Last ned denne brukerens verter, påloggingsinformasjon og filbehandlingsdata som JSON. Hemmelighetene dekrypteres.",
"exportUserDataSuccess": "Brukerdata eksportert",
"exportUserDataFailed": "Kunne ikke eksportere brukerdata",
"hostsCount": "{{count}} verter",
"addHostForUser": "Legg til vert",
"noHostsForUser": "Denne brukeren har ingen verter",
"connectToHost": "Koble til",
"deleteHostConfirm": "Slett verten \"{{name}}\" som tilhører {{username}}?",
"hostDeletedSuccess": "Verten er slettet",
"hostDeleteFailed": "Kunne ikke slette verten",
"credentialsCount": "{{count}} legitimasjon",
"addCredentialForUser": "Legg til legitimasjon",
"noCredentialsForUser": "Denne brukeren har ingen påloggingsinformasjon",
"deleteCredentialConfirm": "Slett legitimasjonen «{{name}}» som tilhører {{username}}?",
"credentialDeletedSuccess": "Legitimasjon slettet",
"credentialDeleteFailed": "Kunne ikke slette legitimasjonen",
"snippetsCount": "{{count}} utdrag",
"addSnippetForUser": "Legg til utdrag",
"noSnippetsForUser": "Denne brukeren har ingen utdrag",
"snippetRequiredFields": "Navn og innhold på utdrag er obligatorisk",
"snippetNamePlaceholder": "Navn på utdrag",
"snippetContentPlaceholder": "Kommandoinnhold",
"snippetFolderPlaceholder": "Mappe (valgfritt)",
"snippetSaved": "Kodestykket er lagret",
"snippetSaveFailed": "Kunne ikke lagre utdraget",
"deleteSnippetConfirm": "Slett utdraget «{{name}}» som tilhører {{username}}?",
"snippetDeletedSuccess": "Utdrag slettet",
"snippetDeleteFailed": "Kunne ikke slette utdraget",
"noSessionsForUser": "Ingen aktive økter",
"deleteUserDangerDesc": "Slett {{username}} og alle dataene deres (verter, påloggingsinformasjon, kodesnutter, historikk) permanent. Dette kan ikke angres.",
"deleteUserConfirm": "Vil du slette {{username}} og alle dataene deres permanent?",
"deleteUserAdminBlocked": "Fjern administratorstatusen før du sletter denne brukeren.",
"createRoleRequired": "Navn og visningsnavn er obligatorisk",
"createRoleSuccess": "Rollen «{{name}}» ble opprettet",
"createRoleFailed": "Kunne ikke opprette rollen",
@@ -2822,7 +2942,16 @@
"hostDefaultsCommandHistory": "Kommandohistorikk",
"hostDefaultsCommandHistoryDesc": "Spor kommandohistorikk på nye verter som standard",
"hostDefaultsSaved": "Vertsstandarder lagret",
"hostDefaultsSaveFailed": "Kunne ikke lagre standardverdiene for verten"
"hostDefaultsSaveFailed": "Kunne ikke lagre standardverdiene for verten",
"rolePermissions": {
"count": "{{count}} tillatelser",
"editAction": "Rediger tillatelser",
"loadError": "Kunne ikke laste inn tillatelseskatalogen",
"saved": "Rolletillatelser lagret",
"saveError": "Kunne ikke lagre rolletillatelser",
"save": "Spare",
"saving": "Lagrer..."
}
},
"newUi": {
"sidebar": {
@@ -2991,6 +3120,10 @@
"executionFailed": "Mislyktes"
},
"userProfile": {
"donateTitle": "Hold Termix i live",
"donateDescription": "Termix er gratis, selvhostet og bygget av bare noen få personer på fritiden. Hvis det har spart deg tid eller penger, bidrar en kryptodonasjon til å holde det gående.",
"donateMilestones": "Donasjoner bidrar til å finansiere tiden som er nødvendig for å undersøke og lære hva som trengs for å bygge SAML-, Kubernetes- og agentstøtte. Se fremdriften og doner.",
"donateButton": "Doner krypto",
"storageModeLocal": "Nettleser",
"storageModeCloud": "Database",
"storageModeDescription": "Nettleseren lagrer innstillinger bare i denne nettleseren. Databasen synkroniseres med serveren og lastes inn på en hvilken som helst enhet.",
@@ -3011,6 +3144,10 @@
"twoFaOn": "På",
"twoFaOff": "Av",
"versionLabel": "Versjon",
"betaProgramTitle": "Betaprogram",
"betaProgramDescription": "Prøv kommende funksjoner tidlig med den ukentlige rullerende :beta Docker-taggen. Ustabil, ikke for produksjon.",
"betaProgramFeedback": "Fant du en feil? Rapporter den her.",
"betaProgramLearnMore": "Lær mer",
"deleteAccount": "Slett konto",
"deleteAccountDescription": "Slett kontoen din permanent",
"changeServerDescription": "Bytt til en annen Termix-backend-server",
+158 -21
View File
@@ -864,6 +864,21 @@
"proxmoxDockerDetectionDesc": "Wzorce nazw rozdzielone przecinkami, które umożliwiają Dockerowi dopasowywanie gości",
"proxmoxPreferredRanges": "Preferowane zakresy adresów IP",
"proxmoxPreferredRangesDesc": "Prefiksy rozdzielone przecinkami w kolejności priorytetów wyboru adresu IP, gdy gość ma wiele interfejsów",
"proxmoxAutoSync": "Automatyczna synchronizacja gości",
"proxmoxAutoSyncDesc": "Okresowo wykrywaj ten węzeł Proxmox i twórz lub aktualizuj importowane hosty gościnne, gdy Twoja sesja jest odblokowana.",
"proxmoxSyncInterval": "Interwał synchronizacji (minuty)",
"proxmoxSyncIntervalDesc": "Minimum 5 minut. Harmonogram pomija zablokowane sesje użytkowników.",
"proxmoxMarkMissing": "Oznacz brakujących gości",
"proxmoxMarkMissingDesc": "Dodaj znacznik proxmox-missing, gdy wcześniej zaimportowany gość zniknie, zamiast go usuwać.",
"proxmoxLastSync": "Ostatnia synchronizacja",
"proxmoxLastSyncNever": "Jeszcze nie zsynchronizowano",
"proxmoxLastSyncNoResult": "Brak szczegółów wyników",
"proxmoxLastSyncSummary": "{{created}} utworzono, {{updated}} zaktualizowano, {{markedMissing}} brakuje, {{skipped}} pominięto",
"proxmoxLastSyncStatus": {
"success": "Sukces",
"error": "Przegrany",
"pending": "Aż do"
},
"proxmoxDiscoverAction": "Odkryj i importuj gości Proxmox",
"proxmoxImportTitle": "Import z Proxmox",
"proxmoxSelectHost": "Wybierz hosta Proxmox…",
@@ -1167,7 +1182,57 @@
"shareHost": "Udostępnij hosta",
"shareHostTitle": "Udostępnij: {{name}}",
"sharing": {
"requiresCredential": "Ten host musi użyć danych uwierzytelniających, aby umożliwić udostępnianie. Najpierw edytuj hosta i przypisz dane uwierzytelniające."
"loadError": "Nie udało się załadować udostępnianych danych. Spróbuj ponownie.",
"shareWithSection": "Podziel się z",
"usersTab": "Użytkownicy",
"rolesTab": "Role",
"searchPlaceholder": "Wyszukaj użytkowników lub role...",
"noMatches": "Nie znaleziono wyników",
"permissionLevelLabel": "Poziom uprawnień",
"levels": {
"connect": {
"label": "Łączyć",
"description": "Tylko sesje otwarte: terminal, pulpit zdalny, menedżer plików, tunele i Docker. Brak dostępu do konfiguracji hosta."
},
"view": {
"label": "Pogląd",
"description": "Połącz się i zobacz konfigurację hosta. Sekrety nigdy nie są widoczne."
},
"edit": {
"label": "Redagować",
"description": "Wyświetlaj i modyfikuj hosta. Sekrety można zastąpić, ale nigdy odczytać; przypisanie poświadczeń pozostaje przypisane tylko właścicielowi."
},
"manage": {
"label": "Zarządzać",
"description": "Edytuj, udostępniaj hosta innym osobom, zmieniaj poziomy uprawnień i cofaj dostęp."
}
},
"expiryLabel": "Wygaśnięcie dostępu",
"expiry": {
"never": "Nigdy",
"oneHour": "1 godzina",
"oneDay": "24 godziny",
"sevenDays": "7 dni",
"thirtyDays": "30 dni",
"custom": "Zwyczaj"
},
"customHoursPlaceholder": "Godziny do wygaśnięcia dostępu",
"shareButton": "Udział",
"shareWithCount": "Udostępnij ({{count}})",
"currentAccess": "Aktualny dostęp",
"noAccessEntries": "Ten host nie został jeszcze udostępniony",
"grantedBy": "Przyznane przez",
"expires": "Wygasa",
"expired": "Wygasły",
"never": "Nigdy",
"revoke": "Unieważnić",
"accessUpdated": "Dostęp zaktualizowany",
"accessUpdateFailed": "Nie udało się zaktualizować dostępu",
"sharedBadge": "Wspólny",
"sharedBadgeTooltip": "Udostępnione przez {{owner}} (dostęp{{level}})",
"viewOnlyBanner": "Ten host jest udostępniany Tobie przez {{owner}} z dostępem do przeglądania. Konfiguracja jest tylko do odczytu.",
"sharedEditBanner": "Ten host jest udostępniany Tobie przez {{owner}} z dostępem do edycji. Zmiany dotyczą rzeczywistego hosta; referencje uwierzytelniające może zmieniać tylko właściciel.",
"ownerOnlyControl": "Tylko właściciel hosta może to zmienić"
},
"guac": {
"connection": "Połączenie",
@@ -1302,28 +1367,10 @@
"backspaceKey": "Klawisz Backspace",
"saveHostFirst": "Najpierw zapisz hosta.",
"sharingOptionsAfterSave": "Opcje udostępniania będą dostępne po zapisaniu hosta.",
"sharingLoadError": "Nie udało się załadować udostępnianych danych. Sprawdź połączenie i spróbuj ponownie.",
"shareHostSection": "Udostępnij hosta",
"shareWithUser": "Udostępnij użytkownikowi",
"shareWithRole": "Udostępnij z rolą",
"selectUser": "Wybierz użytkownika",
"selectRole": "Wybierz rolę",
"selectUserOption": "Wybierz użytkownika...",
"selectRoleOption": "Wybierz rolę...",
"permissionLevel": "Poziom uprawnień",
"expiresInHours": "Wygasa za (godziny)",
"noExpiryPlaceholder": "Pozostaw puste, aby nie utracić ważności",
"shareBtn": "Udział",
"currentAccess": "Aktualny dostęp",
"typeHeader": "Typ",
"targetHeader": "Cel",
"permissionHeader": "Pozwolenie",
"grantedByHeader": "Przyznane przez",
"expiresHeader": "Wygasa",
"noAccessEntries": "Brak wpisów dostępu.",
"expiredLabel": "Wygasły",
"neverLabel": "Nigdy",
"revokeBtn": "Unieważnić",
"cancelBtn": "Anulować",
"savingBtn": "Oszczędność...",
"updateHostBtn": "Aktualizacja hosta",
@@ -2190,7 +2237,8 @@
"passwordLoginDisabledDesc": "Logowanie hasłem jest wyłączone. Użyj klucza dostępu lub zewnętrznego dostawcy uwierzytelniania.",
"signInWithPasskey": "Zaloguj się za pomocą klucza dostępu",
"passkeyLoginFailed": "Nie udało się zalogować za pomocą klucza dostępu",
"attemptsRemaining": "Pozostało {{count}} prób"
"attemptsRemaining": "Pozostało {{count}} prób",
"confirmResetDataWipe": "To konto nie było logowane od czasu aktualizacji szyfrowania, więc nie można odzyskać zapisanych na nim danych bez starego hasła. Zresetowanie spowoduje trwałe usunięcie hostów, danych logowania i fragmentów kodu. Kontynuować?"
},
"hostKey": {
"verifyNewHost": "Zweryfikuj klucz hosta SSH",
@@ -2323,6 +2371,13 @@
"serviceLinks": "Linki serwisowe",
"homepagePreview": "Podgląd strony głównej"
},
"donation": {
"title": "Podoba Ci się Termix?",
"body": "Termix jest darmowy i ma otwarte oprogramowanie, tworzone i utrzymywane przez dwuosobowy zespół w wolnym czasie. Jeśli zastąpi on komercyjne narzędzie, za które w przeciwnym razie musiałbyś płacić, darowizna pomoże pokryć koszty hostingu i umożliwi dalszy rozwój. Na chwilę obecną darowizny są możliwe wyłącznie w kryptowalutach.",
"milestones": "Darowizny pomagają również sfinansować czas poświęcony na badania i zdobywanie wiedzy niezbędnej do tworzenia funkcji takich jak obsługa SAML i Kubernetes. Postępy można śledzić na stronie darowizn.",
"cta": "Podarować",
"dismiss": "Może później"
},
"dashboardTab": {
"stable": "STABILNY",
"hostsOnline": "Gospodarze online",
@@ -2790,6 +2845,71 @@
"updateAdminStatusFailed": "Nie udało się zaktualizować statusu administratora",
"allSessionsRevoked": "Wszystkie sesje odwołane",
"revokeSessionsFailed": "Nie udało się odwołać sesji",
"manageUserData": "Zarządzaj danymi użytkownika",
"backToUsers": "Powrót do użytkowników",
"manageTabAccount": "Konto",
"manageTabHosts": "Zastępy niebieskie",
"manageTabCredentials": "Referencje",
"manageTabSnippets": "Fragmenty",
"manageTabSessions": "Sesje",
"manageTabDanger": "Niebezpieczeństwo",
"manageEditorBack": "Powrót do danych {{username}}",
"dataLockedBadge": "ZAMKNIĘTY",
"dataLockedNotice": "Dane użytkownika {{username}}pozostaną zablokowane do momentu jego następnego zalogowania. Do tego czasu nie będzie można przeglądać ani edytować jego hostów, danych uwierzytelniających i fragmentów kodu.",
"resetPasswordTitle": "Zresetuj hasło",
"resetPasswordOidcOnly": "Ten użytkownik loguje się za pośrednictwem zewnętrznego dostawcy i nie ma hasła.",
"resetPasswordPlaceholder": "Nowe hasło",
"resetPasswordBtn": "Nastawić",
"resetPasswordWorking": "Resetowanie...",
"resetPasswordSuccess": "Resetowanie hasła",
"resetPasswordSuccessWiped": "Resetowanie hasła. Zaszyfrowane dane użytkownika zostały usunięte.",
"resetPasswordFailed": "Nie udało się zresetować hasła",
"resetPasswordConfirmWipe": "{{username}} nie zalogował się od czasu aktualizacji szyfrowania, więc jego danych nie da się odzyskać. Zresetowanie teraz spowoduje usunięcie jego hostów, danych logowania i fragmentów kodu. Kontynuować?",
"totpSectionTitle": "Uwierzytelnianie dwuskładnikowe",
"totpStatusEnabled": "TOTP jest włączony dla tego użytkownika",
"totpStatusDisabled": "TOTP nie jest włączony dla tego użytkownika",
"disableTotp": "Wyłączyć",
"disableTotpConfirm": "Wyłączyć uwierzytelnianie dwuskładnikowe dla {{username}}? Będą mogli się logować, podając tylko swoje hasło.",
"totpDisabledSuccess": "TOTP wyłączony",
"totpDisableFailed": "Nie udało się wyłączyć TOTP",
"manageApiKeys": "Klucze API",
"apiKeyNamePlaceholder": "Nazwa klucza",
"apiKeyCopyNotice": "Skopiuj ten klucz teraz, nie będzie on już więcej wyświetlany.",
"noApiKeysForUser": "Brak kluczy API",
"apiKeyDeleteFailed": "Nie udało się usunąć klucza API",
"exportUserData": "Eksport danych",
"exportUserDataDesc": "Pobierz hosty, dane uwierzytelniające i dane menedżera plików tego użytkownika w formacie JSON. Sekrety są odszyfrowane.",
"exportUserDataSuccess": "Wyeksportowano dane użytkownika",
"exportUserDataFailed": "Nie udało się wyeksportować danych użytkownika",
"hostsCount": "{{count}} gospodarzy",
"addHostForUser": "Dodaj hosta",
"noHostsForUser": "Ten użytkownik nie ma żadnych hostów",
"connectToHost": "Łączyć",
"deleteHostConfirm": "Usunąć hosta „{{name}}” należącego do {{username}}?",
"hostDeletedSuccess": "Usunięto hosta",
"hostDeleteFailed": "Nie udało się usunąć hosta",
"credentialsCount": "{{count}} dane uwierzytelniające",
"addCredentialForUser": "Dodaj poświadczenia",
"noCredentialsForUser": "Ten użytkownik nie ma żadnych danych uwierzytelniających",
"deleteCredentialConfirm": "Usunąć poświadczenie „{{name}}” należące do {{username}}?",
"credentialDeletedSuccess": "Usunięto poświadczenie",
"credentialDeleteFailed": "Nie udało się usunąć poświadczeń",
"snippetsCount": "{{count}} fragmenty",
"addSnippetForUser": "Dodaj fragment kodu",
"noSnippetsForUser": "Ten użytkownik nie ma żadnych fragmentów",
"snippetRequiredFields": "Nazwa i treść fragmentu kodu są wymagane",
"snippetNamePlaceholder": "Nazwa fragmentu kodu",
"snippetContentPlaceholder": "Treść polecenia",
"snippetFolderPlaceholder": "Folder (opcjonalnie)",
"snippetSaved": "Fragment zapisany",
"snippetSaveFailed": "Nie udało się zapisać fragmentu kodu",
"deleteSnippetConfirm": "Usunąć fragment kodu „{{name}}” należący do {{username}}?",
"snippetDeletedSuccess": "Usunięto fragment",
"snippetDeleteFailed": "Nie udało się usunąć fragmentu kodu",
"noSessionsForUser": "Brak aktywnych sesji",
"deleteUserDangerDesc": "Trwale usuń {{username}} i wszystkie jego dane (hosty, dane uwierzytelniające, fragmenty kodu, historię). Tej operacji nie można cofnąć.",
"deleteUserConfirm": "Czy trwale usunąć {{username}} i wszystkie jego dane?",
"deleteUserAdminBlocked": "Przed usunięciem tego użytkownika usuń status administratora.",
"createRoleRequired": "Wymagane jest podanie imienia i nazwiska oraz nazwy wyświetlanej",
"createRoleSuccess": "Utworzono rolę „{{name}}”",
"createRoleFailed": "Nie udało się utworzyć roli",
@@ -2822,7 +2942,16 @@
"hostDefaultsCommandHistory": "Historia dowodzenia",
"hostDefaultsCommandHistoryDesc": "Domyślnie śledź historię poleceń na nowych hostach",
"hostDefaultsSaved": "Zapisano ustawienia domyślne hosta",
"hostDefaultsSaveFailed": "Nie udało się zapisać domyślnych ustawień hosta"
"hostDefaultsSaveFailed": "Nie udało się zapisać domyślnych ustawień hosta",
"rolePermissions": {
"count": "{{count}} uprawnienia",
"editAction": "Edytuj uprawnienia",
"loadError": "Nie udało się załadować katalogu uprawnień",
"saved": "Zapisano uprawnienia roli",
"saveError": "Nie udało się zapisać uprawnień roli",
"save": "Ratować",
"saving": "Oszczędność..."
}
},
"newUi": {
"sidebar": {
@@ -2991,6 +3120,10 @@
"executionFailed": "Przegrany"
},
"userProfile": {
"donateTitle": "Utrzymajmy Termix przy życiu",
"donateDescription": "Termix jest darmowy, hostowany samodzielnie i tworzony przez zaledwie kilka osób w wolnym czasie. Jeśli zaoszczędziłeś czas lub pieniądze, darowizna w kryptowalutach pomoże Ci go utrzymać.",
"donateMilestones": "Darowizny pomagają nam sfinansować czas poświęcony na badania i naukę potrzebną do wdrożenia obsługi SAML, Kubernetes i agentów. Zobacz postępy i przekaż darowiznę.",
"donateButton": "Przekaż kryptowalutę",
"storageModeLocal": "Przeglądarka",
"storageModeCloud": "Baza danych",
"storageModeDescription": "Przeglądarka zapisuje ustawienia tylko w tej przeglądarce. Baza danych synchronizuje się z serwerem i ładuje się na dowolnym urządzeniu.",
@@ -3011,6 +3144,10 @@
"twoFaOn": "NA",
"twoFaOff": "Wyłączony",
"versionLabel": "Wersja",
"betaProgramTitle": "Program beta",
"betaProgramDescription": "Wypróbuj nadchodzące funkcje już teraz dzięki cotygodniowemu tagowi :beta Docker. Wersja niestabilna, nie do użytku produkcyjnego.",
"betaProgramFeedback": "Znalazłeś błąd? Zgłoś go tutaj.",
"betaProgramLearnMore": "Dowiedz się więcej",
"deleteAccount": "Usuń konto",
"deleteAccountDescription": "Trwale usuń swoje konto",
"changeServerDescription": "Przełącz się na inny serwer zaplecza Termix",
+158 -21
View File
@@ -864,6 +864,21 @@
"proxmoxDockerDetectionDesc": "Padrões de nomes separados por vírgulas que permitem ao Docker identificar máquinas virtuais.",
"proxmoxPreferredRanges": "Faixas de IP preferenciais",
"proxmoxPreferredRangesDesc": "Prefixos separados por vírgula em ordem de prioridade para seleção de IP quando um convidado possui múltiplas interfaces.",
"proxmoxAutoSync": "Sincronização automática de convidados",
"proxmoxAutoSyncDesc": "Descubra periodicamente este nó Proxmox e crie ou atualize hosts convidados importados enquanto sua sessão estiver desbloqueada.",
"proxmoxSyncInterval": "Intervalo de sincronização (minutos)",
"proxmoxSyncIntervalDesc": "Tempo mínimo de 5 minutos. O agendador ignora sessões de usuário bloqueadas.",
"proxmoxMarkMissing": "Marque os hóspedes ausentes",
"proxmoxMarkMissingDesc": "Adicione a tag proxmox-missing quando uma máquina virtual previamente importada desaparecer, em vez de excluí-la.",
"proxmoxLastSync": "Última sincronização",
"proxmoxLastSyncNever": "Ainda não sincronizado",
"proxmoxLastSyncNoResult": "Nenhum detalhe do resultado",
"proxmoxLastSyncSummary": "{{created}} criado, {{updated}} atualizado, {{markedMissing}} ausente, {{skipped}} ignorado",
"proxmoxLastSyncStatus": {
"success": "Sucesso",
"error": "Fracassado",
"pending": "Pendente"
},
"proxmoxDiscoverAction": "Descubra e importe hóspedes do Proxmox",
"proxmoxImportTitle": "Importar do Proxmox",
"proxmoxSelectHost": "Selecione um host Proxmox…",
@@ -1167,7 +1182,57 @@
"shareHost": "Compartilhar Host",
"shareHostTitle": "Compartilhar: {{name}}",
"sharing": {
"requiresCredential": "Este host precisa usar uma credencial para permitir o compartilhamento. Edite o host e atribua uma credencial primeiro."
"loadError": "Não foi possível carregar os dados de compartilhamento. Tente novamente.",
"shareWithSection": "Compartilhe com",
"usersTab": "Usuários",
"rolesTab": "Funções",
"searchPlaceholder": "Pesquisar usuários ou funções...",
"noMatches": "Nenhuma correspondência encontrada",
"permissionLevelLabel": "Nível de permissão",
"levels": {
"connect": {
"label": "Conectar",
"description": "Somente sessões abertas: terminal, área de trabalho remota, gerenciador de arquivos, túneis e Docker. Sem acesso à configuração do host."
},
"view": {
"label": "Visualizar",
"description": "Conecte-se e veja a configuração do host. Os segredos nunca são exibidos."
},
"edit": {
"label": "Editar",
"description": "Visualize e modifique o host. Os segredos podem ser substituídos, mas nunca lidos; as atribuições de credenciais permanecem exclusivas do proprietário."
},
"manage": {
"label": "Gerenciar",
"description": "Edite, compartilhe o host com outras pessoas, altere os níveis de permissão e revogue o acesso."
}
},
"expiryLabel": "Expiração do acesso",
"expiry": {
"never": "Nunca",
"oneHour": "1 hora",
"oneDay": "24 horas",
"sevenDays": "7 dias",
"thirtyDays": "30 dias",
"custom": "Personalizado"
},
"customHoursPlaceholder": "Horas restantes até o acesso expirar",
"shareButton": "Compartilhar",
"shareWithCount": "Compartilhar ({{count}})",
"currentAccess": "Acesso atual",
"noAccessEntries": "Este host ainda não foi compartilhado.",
"grantedBy": "Concedido por",
"expires": "Expira",
"expired": "Expirado",
"never": "Nunca",
"revoke": "Revogar",
"accessUpdated": "Acesso atualizado",
"accessUpdateFailed": "Falha ao atualizar o acesso",
"sharedBadge": "Compartilhado",
"sharedBadgeTooltip": "Compartilhado por {{owner}} ({{level}} acesso)",
"viewOnlyBanner": "Este host foi compartilhado com você por {{owner}} com acesso de visualização. A configuração é somente leitura.",
"sharedEditBanner": "Este host foi compartilhado com você por {{owner}} com acesso de edição. As alterações se aplicam ao host real; as referências de autenticação só podem ser alteradas pelo proprietário.",
"ownerOnlyControl": "Somente o proprietário do host pode alterar isso."
},
"guac": {
"connection": "Conexão",
@@ -1302,28 +1367,10 @@
"backspaceKey": "Tecla Backspace",
"saveHostFirst": "Primeiro, salve o host.",
"sharingOptionsAfterSave": "As opções de compartilhamento ficam disponíveis depois que o anfitrião for salvo.",
"sharingLoadError": "Não foi possível carregar os dados de compartilhamento. Verifique sua conexão e tente novamente.",
"shareHostSection": "Compartilhar Host",
"shareWithUser": "Compartilhar com o usuário",
"shareWithRole": "Compartilhar com a função",
"selectUser": "Selecionar usuário",
"selectRole": "Selecione a função",
"selectUserOption": "Selecione um usuário...",
"selectRoleOption": "Selecione uma função...",
"permissionLevel": "Nível de permissão",
"expiresInHours": "Expira em (horas)",
"noExpiryPlaceholder": "Deixe em branco para não expirar.",
"shareBtn": "Compartilhar",
"currentAccess": "Acesso atual",
"typeHeader": "Tipo",
"targetHeader": "Alvo",
"permissionHeader": "Permissão",
"grantedByHeader": "Concedido por",
"expiresHeader": "Expira",
"noAccessEntries": "Ainda não há entradas de acesso.",
"expiredLabel": "Expirado",
"neverLabel": "Nunca",
"revokeBtn": "Revogar",
"cancelBtn": "Cancelar",
"savingBtn": "Salvando...",
"updateHostBtn": "Atualizar Host",
@@ -2190,7 +2237,8 @@
"passwordLoginDisabledDesc": "O login com senha está desativado. Use uma chave de acesso ou um provedor de autenticação externo.",
"signInWithPasskey": "Faça login com a chave de acesso.",
"passkeyLoginFailed": "Falha no login com a chave de acesso",
"attemptsRemaining": "{{count}} tentativas restantes"
"attemptsRemaining": "{{count}} tentativas restantes",
"confirmResetDataWipe": "Esta conta não foi acessada desde a atualização da criptografia, portanto, seus dados armazenados não podem ser recuperados sem a senha antiga. A redefinição excluirá permanentemente seus hosts, credenciais e trechos de código. Continuar?"
},
"hostKey": {
"verifyNewHost": "Verificar chave de host SSH",
@@ -2323,6 +2371,13 @@
"serviceLinks": "Links de serviço",
"homepagePreview": "Pré-visualização da página inicial"
},
"donation": {
"title": "Gostando do Termix?",
"body": "O Termix é gratuito e de código aberto, desenvolvido e mantido por uma equipe de duas pessoas em nosso tempo livre. Se ele substituir uma ferramenta comercial pela qual você pagaria, uma doação ajuda a cobrir os custos de hospedagem e a manter o desenvolvimento em andamento. No momento, as doações são aceitas somente em criptomoedas.",
"milestones": "As doações também ajudam a financiar o tempo necessário para pesquisar e aprender o que é preciso para desenvolver recursos como suporte a SAML e Kubernetes. Veja o progresso na página de doações.",
"cta": "Doar",
"dismiss": "Talvez mais tarde"
},
"dashboardTab": {
"stable": "ESTÁVEL",
"hostsOnline": "Anfitriões Online",
@@ -2790,6 +2845,71 @@
"updateAdminStatusFailed": "Falha ao atualizar o status de administrador",
"allSessionsRevoked": "Todas as sessões foram revogadas.",
"revokeSessionsFailed": "Falha ao revogar sessões",
"manageUserData": "Gerenciar dados do usuário",
"backToUsers": "Voltar aos usuários",
"manageTabAccount": "Conta",
"manageTabHosts": "Anfitriões",
"manageTabCredentials": "Credenciais",
"manageTabSnippets": "Trechos",
"manageTabSessions": "Sessões",
"manageTabDanger": "Perigo",
"manageEditorBack": "Voltar aos dados de {{username}}",
"dataLockedBadge": "TRANCADO",
"dataLockedNotice": "Os dados de {{username}}permanecem bloqueados até o próximo login. Seus hosts, credenciais e trechos de código não podem ser visualizados ou editados até então.",
"resetPasswordTitle": "Redefinir senha",
"resetPasswordOidcOnly": "Este usuário faz login por meio de um provedor externo e não possui senha.",
"resetPasswordPlaceholder": "Nova Senha",
"resetPasswordBtn": "Reiniciar",
"resetPasswordWorking": "Reiniciando...",
"resetPasswordSuccess": "Redefinição de senha",
"resetPasswordSuccessWiped": "Redefinição de senha. Os dados criptografados do usuário foram apagados.",
"resetPasswordFailed": "Falha ao redefinir a senha",
"resetPasswordConfirmWipe": "{{username}} não fez login desde a atualização de criptografia, portanto, seus dados não podem ser recuperados. Redefinir agora excluirá seus hosts, credenciais e trechos de código. Continuar?",
"totpSectionTitle": "Autenticação de dois fatores",
"totpStatusEnabled": "O TOTP está ativado para este usuário.",
"totpStatusDisabled": "O TOTP não está habilitado para este usuário.",
"disableTotp": "Desativar",
"disableTotpConfirm": "Desativar autenticação de dois fatores para {{username}}? Eles poderão fazer login apenas com a senha.",
"totpDisabledSuccess": "TOTP desativado",
"totpDisableFailed": "Falha ao desativar o TOTP",
"manageApiKeys": "Chaves de API",
"apiKeyNamePlaceholder": "Nome da chave",
"apiKeyCopyNotice": "Copie esta chave agora, ela não será exibida novamente.",
"noApiKeysForUser": "Sem chaves de API",
"apiKeyDeleteFailed": "Falha ao excluir a chave da API",
"exportUserData": "Exportação de dados",
"exportUserDataDesc": "Baixe os hosts, credenciais e dados do gerenciador de arquivos deste usuário em formato JSON. Os segredos estão descriptografados.",
"exportUserDataSuccess": "Dados do usuário exportados",
"exportUserDataFailed": "Falha ao exportar dados do usuário",
"hostsCount": "{{count}} anfitriões",
"addHostForUser": "Adicionar host",
"noHostsForUser": "Este usuário não possui hosts.",
"connectToHost": "Conectar",
"deleteHostConfirm": "Excluir host \"{{name}}\" pertencente a {{username}}?",
"hostDeletedSuccess": "Host excluído",
"hostDeleteFailed": "Falha ao excluir o host",
"credentialsCount": "{{count}} credenciais",
"addCredentialForUser": "Adicionar credencial",
"noCredentialsForUser": "Este usuário não possui credenciais.",
"deleteCredentialConfirm": "Excluir credencial \"{{name}}\" pertencente a {{username}}?",
"credentialDeletedSuccess": "Credencial excluída",
"credentialDeleteFailed": "Falha ao excluir credencial",
"snippetsCount": "{{count}} trechos",
"addSnippetForUser": "Adicionar trecho de código",
"noSnippetsForUser": "Este usuário não possui trechos de código.",
"snippetRequiredFields": "O nome e o conteúdo do snippet são obrigatórios.",
"snippetNamePlaceholder": "Nome do trecho",
"snippetContentPlaceholder": "Conteúdo do comando",
"snippetFolderPlaceholder": "Pasta (opcional)",
"snippetSaved": "Trecho salvo",
"snippetSaveFailed": "Falha ao salvar o trecho de código",
"deleteSnippetConfirm": "Excluir trecho \"{{name}}\" pertencente a {{username}}?",
"snippetDeletedSuccess": "Trecho excluído",
"snippetDeleteFailed": "Falha ao excluir o trecho",
"noSessionsForUser": "Nenhuma sessão ativa",
"deleteUserDangerDesc": "Exclua permanentemente {{username}} e todos os seus dados (hosts, credenciais, trechos de código, histórico). Esta ação é irreversível.",
"deleteUserConfirm": "Apagar permanentemente {{username}} e todos os seus dados?",
"deleteUserAdminBlocked": "Remova o status de administrador antes de excluir este usuário.",
"createRoleRequired": "Nome e nome de exibição são obrigatórios.",
"createRoleSuccess": "Função \"{{name}}\" criada",
"createRoleFailed": "Falha ao criar a função",
@@ -2822,7 +2942,16 @@
"hostDefaultsCommandHistory": "Histórico de comandos",
"hostDefaultsCommandHistoryDesc": "Por padrão, rastreie o histórico de comandos em novos hosts.",
"hostDefaultsSaved": "Configurações padrão do host salvas",
"hostDefaultsSaveFailed": "Falha ao salvar as configurações padrão do host"
"hostDefaultsSaveFailed": "Falha ao salvar as configurações padrão do host",
"rolePermissions": {
"count": "{{count}} permissões",
"editAction": "Editar permissões",
"loadError": "Falha ao carregar o catálogo de permissões",
"saved": "Permissões de função salvas",
"saveError": "Falha ao salvar as permissões de função.",
"save": "Salvar",
"saving": "Salvando..."
}
},
"newUi": {
"sidebar": {
@@ -2991,6 +3120,10 @@
"executionFailed": "Fracassado"
},
"userProfile": {
"donateTitle": "Mantenha o Termix vivo",
"donateDescription": "O Termix é gratuito, auto-hospedado e desenvolvido por apenas algumas pessoas em seu tempo livre. Se ele lhe economizou tempo ou dinheiro, uma doação em criptomoedas ajuda a mantê-lo funcionando.",
"donateMilestones": "As doações ajudam a financiar o tempo necessário para pesquisar e aprender o que é preciso para desenvolver suporte para SAML, Kubernetes e Agentes. Veja o progresso e doe.",
"donateButton": "Doe criptomoedas",
"storageModeLocal": "Navegador",
"storageModeCloud": "Banco de dados",
"storageModeDescription": "O navegador armazena as configurações apenas neste navegador. O banco de dados é sincronizado com o servidor e carregado em qualquer dispositivo.",
@@ -3011,6 +3144,10 @@
"twoFaOn": "Sobre",
"twoFaOff": "Desligado",
"versionLabel": "Versão",
"betaProgramTitle": "Programa Beta",
"betaProgramDescription": "Experimente antecipadamente os próximos recursos com a tag Docker :beta, distribuída semanalmente. Instável, não recomendada para produção.",
"betaProgramFeedback": "Encontrou um erro? Reporte-o aqui.",
"betaProgramLearnMore": "Saber mais",
"deleteAccount": "Excluir conta",
"deleteAccountDescription": "Excluir sua conta permanentemente",
"changeServerDescription": "Mude para um servidor backend Termix diferente.",
+158 -21
View File
@@ -864,6 +864,21 @@
"proxmoxDockerDetectionDesc": "Padrões de nomes separados por vírgulas que permitem ao Docker identificar máquinas virtuais.",
"proxmoxPreferredRanges": "Faixas de IP preferenciais",
"proxmoxPreferredRangesDesc": "Prefixos separados por vírgula por ordem de prioridade para a seleção de IP quando um convidado tem várias interfaces.",
"proxmoxAutoSync": "Sincronização automática de convidados",
"proxmoxAutoSyncDesc": "Descubra periodicamente este nó Proxmox e crie ou atualize hosts convidados importados enquanto a sua sessão estiver desbloqueada.",
"proxmoxSyncInterval": "Intervalo de sincronização (minutos)",
"proxmoxSyncIntervalDesc": "Tempo mínimo de 5 minutos. O agendador ignora as sessões de utilizador bloqueadas.",
"proxmoxMarkMissing": "Marque os hóspedes ausentes",
"proxmoxMarkMissingDesc": "Adicione a etiqueta proxmox-missing quando uma máquina virtual previamente importada desaparecer, em vez de a eliminar.",
"proxmoxLastSync": "Última sincronização",
"proxmoxLastSyncNever": "Ainda não sincronizado",
"proxmoxLastSyncNoResult": "Sem detalhe do resultado",
"proxmoxLastSyncSummary": "{{created}} criado, {{updated}} atualizado, {{markedMissing}} ausente, {{skipped}} ignorado",
"proxmoxLastSyncStatus": {
"success": "Sucesso",
"error": "Falhou",
"pending": "Pendente"
},
"proxmoxDiscoverAction": "Descubra e importe hóspedes do Proxmox",
"proxmoxImportTitle": "Importar do Proxmox",
"proxmoxSelectHost": "Selecione um host Proxmox…",
@@ -1167,7 +1182,57 @@
"shareHost": "Partilhar Host",
"shareHostTitle": "Partilhar: {{name}}",
"sharing": {
"requiresCredential": "Este host necessita de usar uma credencial para permitir a partilha. Edite o host e atribua primeiro uma credencial."
"loadError": "Não foi possível carregar os dados de partilha. Tente novamente.",
"shareWithSection": "Partilhe com",
"usersTab": "Usuários",
"rolesTab": "Funções",
"searchPlaceholder": "Pesquisar utilizadores ou funções...",
"noMatches": "Nenhuma correspondência encontrada",
"permissionLevelLabel": "Nível de permissão",
"levels": {
"connect": {
"label": "Ligar",
"description": "Apenas sessões abertas: terminal, ambiente de trabalho remoto, gestor de ficheiros, túneis e Docker. Sem acesso à configuração do host."
},
"view": {
"label": "Ver",
"description": "Ligue-se e veja a configuração do host. Os segredos nunca são exibidos."
},
"edit": {
"label": "Editar",
"description": "Visualize e modifique o host. Os segredos podem ser substituídos, mas nunca lidos; as atribuições de credenciais permanecem exclusivas do proprietário."
},
"manage": {
"label": "Gerir",
"description": "Edite, partilhe o host com outras pessoas, altere os níveis de permissão e revogue o acesso."
}
},
"expiryLabel": "Expiração do acesso",
"expiry": {
"never": "Nunca",
"oneHour": "1 hora",
"oneDay": "24 horas",
"sevenDays": "7 dias",
"thirtyDays": "30 dias",
"custom": "Personalizado"
},
"customHoursPlaceholder": "Horas restantes até o acesso expirar",
"shareButton": "Partilhar",
"shareWithCount": "Partilhar ({{count}})",
"currentAccess": "Acesso atual",
"noAccessEntries": "Este host ainda não foi partilhado.",
"grantedBy": "Concedido por",
"expires": "Expira",
"expired": "Expirado",
"never": "Nunca",
"revoke": "Revogar",
"accessUpdated": "Acesso atualizado",
"accessUpdateFailed": "Falha ao atualizar o acesso",
"sharedBadge": "Compartilhado",
"sharedBadgeTooltip": "Partilhado por {{owner}} ({{level}} acesso)",
"viewOnlyBanner": "Este host foi partilhado consigo por {{owner}} com acesso de visualização. A configuração é de leitura apenas.",
"sharedEditBanner": "Este host foi partilhado consigo por {{owner}} com acesso de edição. As alterações aplicam-se ao host real; as referências de autenticação só podem ser alteradas pelo proprietário.",
"ownerOnlyControl": "Apenas o proprietário do host pode alterar isso."
},
"guac": {
"connection": "Conexão",
@@ -1302,28 +1367,10 @@
"backspaceKey": "Tecla Backspace",
"saveHostFirst": "Primeiro, guarde o host.",
"sharingOptionsAfterSave": "As opções de partilha ficam disponíveis depois de o anfitrião ser guardado.",
"sharingLoadError": "Não foi possível carregar os dados de partilha. Verifique a sua ligação e tente novamente.",
"shareHostSection": "Partilhar Host",
"shareWithUser": "Partilhar com o usuário",
"shareWithRole": "Partilhar com a função",
"selectUser": "Selecionar utilizador",
"selectRole": "Selecione a função",
"selectUserOption": "Selecione um utilizador...",
"selectRoleOption": "Selecione uma função...",
"permissionLevel": "Nível de permissão",
"expiresInHours": "Expira em (horas)",
"noExpiryPlaceholder": "Deixe em branco para não expirar.",
"shareBtn": "Partilhar",
"currentAccess": "Acesso atual",
"typeHeader": "Tipo",
"targetHeader": "Objetivo",
"permissionHeader": "Permissão",
"grantedByHeader": "Concedido por",
"expiresHeader": "Expira",
"noAccessEntries": "Ainda não existem entradas de acesso.",
"expiredLabel": "Expirado",
"neverLabel": "Nunca",
"revokeBtn": "Revogar",
"cancelBtn": "Cancelar",
"savingBtn": "Salvando...",
"updateHostBtn": "Atualizar Host",
@@ -2190,7 +2237,8 @@
"passwordLoginDisabledDesc": "O login com palavra-passe está desativado. Utilize uma chave de acesso ou um fornecedor de autenticação externo.",
"signInWithPasskey": "Faça login com a chave de acesso.",
"passkeyLoginFailed": "Falha no login com a chave de acesso",
"attemptsRemaining": "{{count}} restantes tentativas"
"attemptsRemaining": "{{count}} restantes tentativas",
"confirmResetDataWipe": "Esta conta não foi acedida desde a atualização da encriptação, pelo que os seus dados armazenados não podem ser recuperados sem a palavra-passe antiga. A reposição eliminará permanentemente os seus hosts, credenciais e trechos de código. Continuar?"
},
"hostKey": {
"verifyNewHost": "Verificar chave de host SSH",
@@ -2323,6 +2371,13 @@
"serviceLinks": "Links de serviço",
"homepagePreview": "Pré-visualização da página inicial"
},
"donation": {
"title": "Gosta do Termix?",
"body": "O Termix é gratuito e de código aberto, desenvolvido e mantido por uma equipa de duas pessoas no nosso tempo livre. Se substituir uma ferramenta comercial pela qual pagaria, um donativo ajuda a cobrir os custos de alojamento e a manter o desenvolvimento em curso. Neste momento, as doações são aceites apenas em criptomoedas.",
"milestones": "Os donativos também ajudam a financiar o tempo necessário para pesquisar e aprender o que é necessário para desenvolver recursos como o apoio ao SAML e ao Kubernetes. Veja o progresso na página de donativos.",
"cta": "Doar",
"dismiss": "Talvez mais tarde"
},
"dashboardTab": {
"stable": "ESTÁVEL",
"hostsOnline": "Anfitriões Online",
@@ -2790,6 +2845,71 @@
"updateAdminStatusFailed": "Falha ao atualizar o estado de administrador",
"allSessionsRevoked": "Todas as sessões foram revogadas.",
"revokeSessionsFailed": "Falha ao revogar sessões",
"manageUserData": "Gerir dados do utilizador",
"backToUsers": "Voltar aos utilizadores",
"manageTabAccount": "Conta",
"manageTabHosts": "Anfitriões",
"manageTabCredentials": "Credenciais",
"manageTabSnippets": "Trechos",
"manageTabSessions": "Sessões",
"manageTabDanger": "Perigo",
"manageEditorBack": "Voltar aos dados de {{username}}",
"dataLockedBadge": "TRANCADO",
"dataLockedNotice": "Os dados de {{username}}permanecem bloqueados até ao próximo login. Os seus hosts, credenciais e pedaços de código não podem ser visualizados ou editados até então.",
"resetPasswordTitle": "Redefinir palavra-passe",
"resetPasswordOidcOnly": "Este utilizador faz login através de um fornecedor externo e não possui palavra-passe.",
"resetPasswordPlaceholder": "Nova Senha",
"resetPasswordBtn": "Reiniciar",
"resetPasswordWorking": "Reiniciando...",
"resetPasswordSuccess": "Redefinição de palavra-passe",
"resetPasswordSuccessWiped": "Redefinição de palavra-passe. Os dados encriptados do utilizador foram apagados.",
"resetPasswordFailed": "Falha ao redefinir a palavra-passe",
"resetPasswordConfirmWipe": "{{username}} não fez login desde a atualização de encriptação, pelo que os seus dados não podem ser recuperados. O reset irá agora excluir os seus hosts, credenciais e trechos de código. Continuar?",
"totpSectionTitle": "Autenticação de dois fatores",
"totpStatusEnabled": "O TOTP está ativado para este utilizador.",
"totpStatusDisabled": "O TOTP não está ativado para este utilizador.",
"disableTotp": "Desativar",
"disableTotpConfirm": "Desativar a autenticação de dois fatores para {{username}}? Poderão fazer login apenas com a palavra-passe.",
"totpDisabledSuccess": "TOTP desativado",
"totpDisableFailed": "Falha ao desativar o TOTP",
"manageApiKeys": "Chaves API",
"apiKeyNamePlaceholder": "Nome da chave",
"apiKeyCopyNotice": "Copie esta chave agora, não voltará a ser apresentada.",
"noApiKeysForUser": "Sem chaves API",
"apiKeyDeleteFailed": "Falha ao eliminar a chave API",
"exportUserData": "Exportação de dados",
"exportUserDataDesc": "Transfira os hosts, as credenciais e os dados do gestor de ficheiros deste utilizador em formato JSON. Os segredos estão desencriptados.",
"exportUserDataSuccess": "Dados do utilizador exportados",
"exportUserDataFailed": "Falha ao exportar dados do utilizador",
"hostsCount": "{{count}} anfitriões",
"addHostForUser": "Adicionar host",
"noHostsForUser": "Este utilizador não possui hosts.",
"connectToHost": "Ligar",
"deleteHostConfirm": "Apagar host \"{{name}}\" pertencente a {{username}}?",
"hostDeletedSuccess": "Host excluído",
"hostDeleteFailed": "Falha ao eliminar o host",
"credentialsCount": "{{count}} credenciais",
"addCredentialForUser": "Adicionar credencial",
"noCredentialsForUser": "Este utilizador não possui credenciais.",
"deleteCredentialConfirm": "Apagar credencial \"{{name}}\" pertencente a {{username}}?",
"credentialDeletedSuccess": "Credencial excluída",
"credentialDeleteFailed": "Falha ao eliminar credencial",
"snippetsCount": "{{count}} excertos",
"addSnippetForUser": "Adicionar trecho de código",
"noSnippetsForUser": "Este utilizador não possui trechos de código.",
"snippetRequiredFields": "O nome e o conteúdo do excerto são obrigatórios.",
"snippetNamePlaceholder": "Nome do excerto",
"snippetContentPlaceholder": "Conteúdo do comando",
"snippetFolderPlaceholder": "Pasta (opcional)",
"snippetSaved": "Trecho salvo",
"snippetSaveFailed": "Falha ao guardar o trecho de código",
"deleteSnippetConfirm": "Apagar excerto \"{{name}}\" pertencente a {{username}}?",
"snippetDeletedSuccess": "Trecho excluído",
"snippetDeleteFailed": "Falha ao eliminar o trecho",
"noSessionsForUser": "Nenhuma sessão ativa",
"deleteUserDangerDesc": "Apague permanentemente {{username}} e todos os seus dados (hosts, credenciais, excertos de código, histórico). Esta ação é irreversível.",
"deleteUserConfirm": "Apagar permanentemente {{username}} e todos os seus dados?",
"deleteUserAdminBlocked": "Remova o estado de administrador antes de eliminar este utilizador.",
"createRoleRequired": "O nome e o nome de exibição são obrigatórios.",
"createRoleSuccess": "Função \"{{name}}\" criada",
"createRoleFailed": "Falha ao criar a função",
@@ -2822,7 +2942,16 @@
"hostDefaultsCommandHistory": "Histórico de comandos",
"hostDefaultsCommandHistoryDesc": "Por predefinição, acompanhe o histórico de comandos em novos hosts.",
"hostDefaultsSaved": "Configurações padrão do host guardadas",
"hostDefaultsSaveFailed": "Falha ao guardar as definições padrão do host"
"hostDefaultsSaveFailed": "Falha ao guardar as definições padrão do host",
"rolePermissions": {
"count": "{{count}} permissões",
"editAction": "Editar permissões",
"loadError": "Falha ao carregar o catálogo de permissões",
"saved": "Permissões de função guardadas",
"saveError": "Falha ao guardar as permissões de função.",
"save": "Guardar",
"saving": "Salvando..."
}
},
"newUi": {
"sidebar": {
@@ -2991,6 +3120,10 @@
"executionFailed": "Falhou"
},
"userProfile": {
"donateTitle": "Mantenha o Termix vivo",
"donateDescription": "O Termix é gratuito, auto-hospedado e desenvolvido por apenas algumas pessoas no seu tempo livre. Se ele lhe poupou tempo ou dinheiro, uma doação em criptomoedas ajuda a mantê-lo em funcionamento.",
"donateMilestones": "Os donativos ajudam a financiar o tempo necessário para pesquisar e aprender o que é necessário para desenvolver o apoio para SAML, Kubernetes e Agentes. Veja o progresso e doe.",
"donateButton": "Doe criptomoedas",
"storageModeLocal": "Navegador",
"storageModeCloud": "Banco de dados",
"storageModeDescription": "O navegador armazena as definições apenas neste navegador. A base de dados é sincronizada com o servidor e carregada em qualquer dispositivo.",
@@ -3011,6 +3144,10 @@
"twoFaOn": "Em",
"twoFaOff": "Desligado",
"versionLabel": "Versão",
"betaProgramTitle": "Programa Beta",
"betaProgramDescription": "Experimente antecipadamente os próximos recursos com a etiqueta Docker :beta, distribuída semanalmente. Instável, não recomendada para produção.",
"betaProgramFeedback": "Encontrou um erro? Reporte-o aqui.",
"betaProgramLearnMore": "Saber mais",
"deleteAccount": "Apagar conta",
"deleteAccountDescription": "Apagar a sua conta permanentemente",
"changeServerDescription": "Mude para um servidor de backend Termix diferente.",
+158 -21
View File
@@ -864,6 +864,21 @@
"proxmoxDockerDetectionDesc": "Modele de nume separate prin virgulă care activează Docker pentru invitații potriviți",
"proxmoxPreferredRanges": "Intervalele IP preferate",
"proxmoxPreferredRangesDesc": "Prefixe separate prin virgulă în ordinea priorității pentru selectarea adresei IP atunci când un oaspete are mai multe interfețe",
"proxmoxAutoSync": "Sincronizare automată a invitaților",
"proxmoxAutoSyncDesc": "Descoperiți periodic acest nod Proxmox și creați sau actualizați gazdele invitate importate în timp ce sesiunea este deblocată.",
"proxmoxSyncInterval": "Interval de sincronizare (minute)",
"proxmoxSyncIntervalDesc": "Minim 5 minute. Planificatorul omite sesiunile utilizatorilor blocate.",
"proxmoxMarkMissing": "Marchează oaspeții lipsă",
"proxmoxMarkMissingDesc": "Adăugați o etichetă proxmox-missing atunci când un invitat importat anterior dispare, în loc să îl ștergeți.",
"proxmoxLastSync": "Ultima sincronizare",
"proxmoxLastSyncNever": "Încă nesincronizat",
"proxmoxLastSyncNoResult": "Niciun detaliu al rezultatului",
"proxmoxLastSyncSummary": "{{created}} creat, {{updated}} actualizat, {{markedMissing}} lipsă, {{skipped}} omis",
"proxmoxLastSyncStatus": {
"success": "Succes",
"error": "Eșuat",
"pending": "În așteptare"
},
"proxmoxDiscoverAction": "Descoperiți și importați invitați Proxmox",
"proxmoxImportTitle": "Import din Proxmox",
"proxmoxSelectHost": "Selectați o gazdă Proxmox…",
@@ -1167,7 +1182,57 @@
"shareHost": "Gazdă partajată",
"shareHostTitle": "Distribuie: {{name}}",
"sharing": {
"requiresCredential": "Această gazdă trebuie să utilizeze o acreditare pentru a activa partajarea. Editați gazda și atribuiți mai întâi o acreditare."
"loadError": "Nu s-au putut încărca datele de partajare. Vă rugăm să încercați din nou.",
"shareWithSection": "Distribuiți cu",
"usersTab": "Utilizatori",
"rolesTab": "Roluri",
"searchPlaceholder": "Căutați utilizatori sau roluri...",
"noMatches": "Nu s-au găsit potriviri",
"permissionLevelLabel": "Nivel de permisiune",
"levels": {
"connect": {
"label": "Conectează-te",
"description": "Deschideți doar sesiunile: terminal, desktop la distanță, manager de fișiere, tuneluri și Docker. Fără acces la configurația gazdei."
},
"view": {
"label": "Vedere",
"description": "Conectează-te și vezi configurația gazdei. Secretele nu sunt afișate niciodată."
},
"edit": {
"label": "Edita",
"description": "Vizualizați și modificați gazda. Secretele pot fi înlocuite, dar niciodată citite; atribuirea acreditărilor rămâne doar proprietarului."
},
"manage": {
"label": "Gestionează",
"description": "Editați, plus partajați gazda cu alții, modificați nivelurile de permisiune și revocați accesul."
}
},
"expiryLabel": "Expirarea accesului",
"expiry": {
"never": "Nu",
"oneHour": "1 oră",
"oneDay": "24 de ore",
"sevenDays": "7 zile",
"thirtyDays": "30 de zile",
"custom": "Personalizat"
},
"customHoursPlaceholder": "Ore până la expirarea accesului",
"shareButton": "Distribuie",
"shareWithCount": "Distribuie ({{count}})",
"currentAccess": "Acces curent",
"noAccessEntries": "Această gazdă nu a fost încă partajată",
"grantedBy": "Acordat de",
"expires": "Expiră",
"expired": "Expirat",
"never": "Nu",
"revoke": "Revoca",
"accessUpdated": "Acces actualizat",
"accessUpdateFailed": "Nu s-a putut actualiza accesul",
"sharedBadge": "Partajat",
"sharedBadgeTooltip": "Distribuit de {{owner}} ( acces{{level}})",
"viewOnlyBanner": "Această gazdă este partajată cu dvs. de către {{owner}} cu acces de vizualizare. Configurația este doar pentru citire.",
"sharedEditBanner": "Această gazdă este partajată cu dvs. de către {{owner}} cu acces de editare. Modificările se aplică gazdei reale; referințele de autentificare pot fi modificate numai de către proprietar.",
"ownerOnlyControl": "Doar proprietarul gazdei poate modifica acest lucru"
},
"guac": {
"connection": "Conexiune",
@@ -1302,28 +1367,10 @@
"backspaceKey": "Tasta Backspace",
"saveHostFirst": "Salvează mai întâi gazda.",
"sharingOptionsAfterSave": "Opțiunile de partajare sunt disponibile după ce gazda a fost salvată.",
"sharingLoadError": "Nu s-au putut încărca datele de partajare. Verificați conexiunea și încercați din nou.",
"shareHostSection": "Gazdă partajată",
"shareWithUser": "Partajați cu utilizatorul",
"shareWithRole": "Partajare cu Rol",
"selectUser": "Selectați utilizatorul",
"selectRole": "Selectați rolul",
"selectUserOption": "Selectați un utilizator...",
"selectRoleOption": "Selectați un rol...",
"permissionLevel": "Nivel de permisiune",
"expiresInHours": "Expiră în (ore)",
"noExpiryPlaceholder": "Se lasă gol dacă nu există expirare",
"shareBtn": "Distribuie",
"currentAccess": "Acces curent",
"typeHeader": "Tip",
"targetHeader": "Ţintă",
"permissionHeader": "Permisiune",
"grantedByHeader": "Acordat de",
"expiresHeader": "Expiră",
"noAccessEntries": "Încă nu există intrări de acces.",
"expiredLabel": "Expirat",
"neverLabel": "Nu",
"revokeBtn": "Revoca",
"cancelBtn": "Anula",
"savingBtn": "Economisire...",
"updateHostBtn": "Actualizați gazda",
@@ -2190,7 +2237,8 @@
"passwordLoginDisabledDesc": "Conectarea cu parolă este dezactivată. Folosiți o cheie de acces sau un furnizor de autentificare extern.",
"signInWithPasskey": "Conectați-vă cu cheia de acces",
"passkeyLoginFailed": "Conectarea cu parola a eșuat",
"attemptsRemaining": "{{count}} încercări rămase"
"attemptsRemaining": "{{count}} încercări rămase",
"confirmResetDataWipe": "Acest cont nu s-a mai conectat de la actualizarea criptării, așadar datele sale stocate nu pot fi recuperate fără vechea parolă. Resetarea va șterge definitiv gazdele, acreditările și fragmentele de date. Continuați?"
},
"hostKey": {
"verifyNewHost": "Verificați cheia gazdă SSH",
@@ -2323,6 +2371,13 @@
"serviceLinks": "Linkuri de servicii",
"homepagePreview": "Previzualizare pagină principală"
},
"donation": {
"title": "Îți place Termix?",
"body": "Termix este gratuit și open source, construit și întreținut de o echipă de două persoane în timpul liber. Dacă înlocuiește un instrument comercial pentru care altfel ați plăti, o donație ajută la acoperirea costurilor de găzduire și menține dezvoltarea în desfășurare. În prezent, donațiile sunt doar criptomonede.",
"milestones": "Donațiile ajută, de asemenea, la finanțarea timpului necesar pentru cercetare și învățare a ceea ce este necesar pentru a construi funcții precum suportul SAML și Kubernetes. Vedeți progresul pe pagina de donații.",
"cta": "Dona",
"dismiss": "Poate mai târziu"
},
"dashboardTab": {
"stable": "STABIL",
"hostsOnline": "Gazde online",
@@ -2790,6 +2845,71 @@
"updateAdminStatusFailed": "Nu s-a putut actualiza starea de administrator",
"allSessionsRevoked": "Toate sesiunile revocate",
"revokeSessionsFailed": "Nu s-au putut revoca sesiunile",
"manageUserData": "Gestionați datele utilizatorilor",
"backToUsers": "Înapoi la utilizatori",
"manageTabAccount": "Cont",
"manageTabHosts": "Gazde",
"manageTabCredentials": "Acreditări",
"manageTabSnippets": "Fragmente",
"manageTabSessions": "Sesiuni",
"manageTabDanger": "Pericol",
"manageEditorBack": "Înapoi la datele lui {{username}}",
"dataLockedBadge": "BLOCAT",
"dataLockedNotice": "Datele utilizatorului {{username}}rămân blocate până la următoarea conectare. Gazdele, acreditările și fragmentele de date nu pot fi vizualizate sau editate până atunci.",
"resetPasswordTitle": "Resetare parolă",
"resetPasswordOidcOnly": "Acest utilizator se conectează printr-un furnizor extern și nu are parolă.",
"resetPasswordPlaceholder": "Parolă Nouă",
"resetPasswordBtn": "Resetare",
"resetPasswordWorking": "Resetare...",
"resetPasswordSuccess": "Resetare parolă",
"resetPasswordSuccessWiped": "Resetare parolă. Datele criptate ale utilizatorului au fost șterse.",
"resetPasswordFailed": "Resetarea parolei a eșuat",
"resetPasswordConfirmWipe": "{{username}} nu s-a conectat de la actualizarea criptării, prin urmare datele sale nu pot fi recuperate. Resetarea acum va șterge gazdele, acreditările și fragmentele de date. Continuați?",
"totpSectionTitle": "Autentificare cu doi factori",
"totpStatusEnabled": "TOTP este activat pentru acest utilizator",
"totpStatusDisabled": "TOTP nu este activat pentru acest utilizator",
"disableTotp": "Dezactivare",
"disableTotpConfirm": "Dezactivați autentificarea cu doi factori pentru {{username}}? Aceștia se vor putea conecta doar cu parola.",
"totpDisabledSuccess": "TOTP dezactivat",
"totpDisableFailed": "Nu s-a putut dezactiva TOTP",
"manageApiKeys": "Chei API",
"apiKeyNamePlaceholder": "Numele cheii",
"apiKeyCopyNotice": "Copiază această cheie acum, nu va mai fi afișată.",
"noApiKeysForUser": "Fără chei API",
"apiKeyDeleteFailed": "Cheia API nu a putut fi ștersă.",
"exportUserData": "Exportul datelor",
"exportUserDataDesc": "Descarcă gazdele, acreditările și datele managerului de fișiere ale acestui utilizator ca JSON. Secretele sunt decriptate.",
"exportUserDataSuccess": "Datele utilizatorului exportate",
"exportUserDataFailed": "Exportul datelor utilizatorului a eșuat",
"hostsCount": "gazde {{count}}",
"addHostForUser": "Adăugați gazdă",
"noHostsForUser": "Acest utilizator nu are gazde",
"connectToHost": "Conectează-te",
"deleteHostConfirm": "Ștergeți gazda „{{name}}” aparținând lui {{username}}?",
"hostDeletedSuccess": "Gazdă ștearsă",
"hostDeleteFailed": "Nu s-a putut șterge gazda",
"credentialsCount": "acreditări {{count}}",
"addCredentialForUser": "Adăugați acreditări",
"noCredentialsForUser": "Acest utilizator nu are acreditări",
"deleteCredentialConfirm": "Ștergeți acreditările „{{name}}” aparținând lui {{username}}?",
"credentialDeletedSuccess": "Acreditare ștearsă",
"credentialDeleteFailed": "Ștergerea acreditării nu a reușit",
"snippetsCount": "fragmente {{count}}",
"addSnippetForUser": "Adăugați fragment",
"noSnippetsForUser": "Acest utilizator nu are fragmente",
"snippetRequiredFields": "Numele și conținutul fragmentului sunt obligatorii",
"snippetNamePlaceholder": "Nume fragment",
"snippetContentPlaceholder": "Conținutul comenzii",
"snippetFolderPlaceholder": "Dosar (opțional)",
"snippetSaved": "Fragment salvat",
"snippetSaveFailed": "Nu s-a putut salva fragmentul",
"deleteSnippetConfirm": "Ștergeți fragmentul „{{name}}” aparținând lui {{username}}?",
"snippetDeletedSuccess": "Fragment șters",
"snippetDeleteFailed": "Nu s-a putut șterge fragmentul",
"noSessionsForUser": "Nicio sesiune activă",
"deleteUserDangerDesc": "Ștergeți definitiv {{username}} și toate datele aferente (gazde, acreditări, fragmente de date, istoric). Această acțiune nu poate fi anulată.",
"deleteUserConfirm": "Ștergeți definitiv {{username}} și toate datele aferente?",
"deleteUserAdminBlocked": "Eliminați statutul de administrator înainte de a șterge acest utilizator.",
"createRoleRequired": "Numele și numele afișat sunt obligatorii",
"createRoleSuccess": "Rolul „{{name}}” a fost creat",
"createRoleFailed": "Nu s-a putut crea rolul",
@@ -2822,7 +2942,16 @@
"hostDefaultsCommandHistory": "Istoricul comenzilor",
"hostDefaultsCommandHistoryDesc": "Urmăriți istoricul comenzilor pe gazdele noi în mod implicit",
"hostDefaultsSaved": "Setările implicite ale gazdei au fost salvate",
"hostDefaultsSaveFailed": "Nu s-au putut salva setările implicite ale gazdei"
"hostDefaultsSaveFailed": "Nu s-au putut salva setările implicite ale gazdei",
"rolePermissions": {
"count": "permisiuni {{count}}",
"editAction": "Editați permisiunile",
"loadError": "Nu s-a putut încărca catalogul de permisiuni",
"saved": "Permisiunile de rol au fost salvate",
"saveError": "Nu s-au putut salva permisiunile rolului",
"save": "Salva",
"saving": "Economisire..."
}
},
"newUi": {
"sidebar": {
@@ -2991,6 +3120,10 @@
"executionFailed": "Eșuat"
},
"userProfile": {
"donateTitle": "Menține Termix în viață",
"donateDescription": "Termix este gratuit, găzduit în regim self-hosting și construit de doar câțiva oameni în timpul lor liber. Dacă ți-a economisit timp sau bani, o donație în criptomonede ajută la menținerea activității.",
"donateMilestones": "Donațiile ajută la finanțarea timpului necesar pentru cercetare și învățare a ceea ce este necesar pentru a dezvolta suport SAML, Kubernetes și pentru agenți. Vedeți progresul și donați.",
"donateButton": "Donează criptomonede",
"storageModeLocal": "Browser",
"storageModeCloud": "Bază de date",
"storageModeDescription": "Browserul stochează setările doar în acest browser. Baza de date se sincronizează cu serverul și se încarcă pe orice dispozitiv.",
@@ -3011,6 +3144,10 @@
"twoFaOn": "Pe",
"twoFaOff": "Dezactivat",
"versionLabel": "Versiune",
"betaProgramTitle": "Programul Beta",
"betaProgramDescription": "Încercați din timp funcțiile viitoare cu eticheta Docker săptămânală :beta. Instabil, nu este pentru producție.",
"betaProgramFeedback": "Ai găsit o eroare? Raportează-o aici.",
"betaProgramLearnMore": "Află mai multe",
"deleteAccount": "Șterge contul",
"deleteAccountDescription": "Ștergeți definitiv contul dvs.",
"changeServerDescription": "Treceți la un alt server backend Termix",
File diff suppressed because it is too large Load Diff
+158 -21
View File
@@ -864,6 +864,21 @@
"proxmoxDockerDetectionDesc": "Обрасци имена одвојени зарезима који омогућавају Докеру да упари госте",
"proxmoxPreferredRanges": "Жељени IP опсези",
"proxmoxPreferredRangesDesc": "Префикси одвојени зарезима по редоследу приоритета за избор IP адресе када гост има више интерфејса",
"proxmoxAutoSync": "Аутоматска синхронизација гостију",
"proxmoxAutoSyncDesc": "Периодично откривајте овај Proxmox чвор и креирајте или ажурирајте увезене гостујуће хостове док је ваша сесија откључана.",
"proxmoxSyncInterval": "Интервал синхронизације (минути)",
"proxmoxSyncIntervalDesc": "Минимум 5 минута. Планер прескаче закључане корисничке сесије.",
"proxmoxMarkMissing": "Означи госте који недостају",
"proxmoxMarkMissingDesc": "Додајте ознаку proxmox-missing када претходно увезени гост нестане уместо да га избришете.",
"proxmoxLastSync": "Последња синхронизација",
"proxmoxLastSyncNever": "Још није синхронизовано",
"proxmoxLastSyncNoResult": "Нема детаља о резултатима",
"proxmoxLastSyncSummary": "{{created}} креирано, {{updated}} ажурирано, {{markedMissing}} недостаје, {{skipped}} прескочено",
"proxmoxLastSyncStatus": {
"success": "Успех",
"error": "Неуспешно",
"pending": "На чекању"
},
"proxmoxDiscoverAction": "Откријте и увезите госте Proxmox-а",
"proxmoxImportTitle": "Увоз из Proxmox-а",
"proxmoxSelectHost": "Изаберите Proxmox хост…",
@@ -1167,7 +1182,57 @@
"shareHost": "Дели хост",
"shareHostTitle": "Подели: {{name}}",
"sharing": {
"requiresCredential": "Овај хост мора да користи акредитив да би омогућио дељење. Прво измените хост и доделите акредитив."
"loadError": "Учитавање података о дељењу није успело. Молимо покушајте поново.",
"shareWithSection": "Подели са",
"usersTab": "Корисници",
"rolesTab": "Улоге",
"searchPlaceholder": "Претражите кориснике или улоге...",
"noMatches": "Није пронађено ниједно подударање",
"permissionLevelLabel": "Ниво дозволе",
"levels": {
"connect": {
"label": "Повежи се",
"description": "Само отворене сесије: терминал, удаљена радна површина, менаџер датотека, тунели и Докер. Нема приступа конфигурацији хоста."
},
"view": {
"label": "Поглед",
"description": "Повежите се, плус погледајте конфигурацију хоста. Тајне се никада не приказују."
},
"edit": {
"label": "Измени",
"description": "Преглед и измена хоста. Тајне се могу заменити, али никада читати; доделе акредитива остају само за власника."
},
"manage": {
"label": "Управљај",
"description": "Уредите, плус делите хост са другима, мењајте нивое дозвола и опозовите приступ."
}
},
"expiryLabel": "Истек приступа",
"expiry": {
"never": "Никад",
"oneHour": "1 сат",
"oneDay": "24 сата",
"sevenDays": "7 дана",
"thirtyDays": "30 дана",
"custom": "Прилагођено"
},
"customHoursPlaceholder": "Сати до истека приступа",
"shareButton": "Дели",
"shareWithCount": "Подели ({{count}})",
"currentAccess": "Тренутни приступ",
"noAccessEntries": "Овај хост још није дељен",
"grantedBy": "Додељено од стране",
"expires": "Истиче",
"expired": "Истекло",
"never": "Никад",
"revoke": "Поништи",
"accessUpdated": "Приступ је ажуриран",
"accessUpdateFailed": "Ажурирање приступа није успело",
"sharedBadge": "Дељено",
"sharedBadgeTooltip": "Дели {{owner}} ({{level}} приступ)",
"viewOnlyBanner": "Овај хост дели са вама {{owner}} са приступом за преглед. Конфигурација је само за читање.",
"sharedEditBanner": "Овај хост дели са вама {{owner}} са приступом за уређивање. Промене се примењују на правог хоста; референце за аутентификацију може променити само власник.",
"ownerOnlyControl": "Само власник хоста може ово да промени"
},
"guac": {
"connection": "Веза",
@@ -1302,28 +1367,10 @@
"backspaceKey": "Тастер Backspace",
"saveHostFirst": "Прво сачувајте домаћина.",
"sharingOptionsAfterSave": "Опције дељења су доступне након што је хост сачуван.",
"sharingLoadError": "Учитавање података за дељење није успело. Проверите везу и покушајте поново.",
"shareHostSection": "Дели хост",
"shareWithUser": "Подели са корисником",
"shareWithRole": "Дели са улогом",
"selectUser": "Изаберите корисника",
"selectRole": "Изаберите улогу",
"selectUserOption": "Изаберите корисника...",
"selectRoleOption": "Изаберите улогу...",
"permissionLevel": "Ниво дозволе",
"expiresInHours": "Истиче за (сати)",
"noExpiryPlaceholder": "Оставите празно ако нема рока истека",
"shareBtn": "Дели",
"currentAccess": "Тренутни приступ",
"typeHeader": "Тип",
"targetHeader": "Циљ",
"permissionHeader": "Дозвола",
"grantedByHeader": "Доделио/ла",
"expiresHeader": "Истиче",
"noAccessEntries": "Још нема уноса за приступ.",
"expiredLabel": "Истекло",
"neverLabel": "Никад",
"revokeBtn": "Поништи",
"cancelBtn": "Откажи",
"savingBtn": "Чување...",
"updateHostBtn": "Ажурирај хост",
@@ -2190,7 +2237,8 @@
"passwordLoginDisabledDesc": "Пријава лозинком је онемогућена. Користите шифру или екстерног добављача аутентификације.",
"signInWithPasskey": "Пријавите се помоћу лозинке",
"passkeyLoginFailed": "Пријава лозинком није успела",
"attemptsRemaining": "{{count}} преосталих покушаја"
"attemptsRemaining": "{{count}} преосталих покушаја",
"confirmResetDataWipe": "Овај налог се није пријављивао од надоградње шифровања, тако да се његови сачувани подаци не могу вратити без старе лозинке. Ресетовање ће трајно избрисати његове хостове, акредитиве и фрагменте. Желите да наставите?"
},
"hostKey": {
"verifyNewHost": "Проверите SSH кључ хоста",
@@ -2323,6 +2371,13 @@
"serviceLinks": "Линкови услуга",
"homepagePreview": "Преглед почетне странице"
},
"donation": {
"title": "Уживате у Термиксу?",
"body": "Termix је бесплатан и отвореног кода, који развија и одржава двочлани тим у слободно време. Ако замени комерцијални алат за који бисте иначе плаћали, донација помаже у покривању трошкова хостинга и одржавању развоја. Тренутно, донације су само у криптовалутама.",
"milestones": "Донације такође помажу у финансирању времена за истраживање и учење шта је потребно за изградњу функција попут подршке за SAML и Kubernetes. Погледајте напредак на страници за донације.",
"cta": "Донирајте",
"dismiss": "Можда касније"
},
"dashboardTab": {
"stable": "СТАБИЛНО",
"hostsOnline": "Домаћини на мрежи",
@@ -2790,6 +2845,71 @@
"updateAdminStatusFailed": "Ажурирање статуса администратора није успело",
"allSessionsRevoked": "Све сесије су опозване",
"revokeSessionsFailed": "Опозивање сесија није успело",
"manageUserData": "Управљајте корисничким подацима",
"backToUsers": "Назад на кориснике",
"manageTabAccount": "Налог",
"manageTabHosts": "Домаћини",
"manageTabCredentials": "Акредитиви",
"manageTabSnippets": "Исечци",
"manageTabSessions": "Сесије",
"manageTabDanger": "Опасност",
"manageEditorBack": "Назад на податке корисника {{username}}",
"dataLockedBadge": "ЗАКЉУЧАНО",
"dataLockedNotice": "Подаци корисника {{username}}остају закључани до њиховог следећег пријављивања. Њихови хостови, акредитиви и исечци не могу бити прегледани или уређивани до тада.",
"resetPasswordTitle": "Ресетуј лозинку",
"resetPasswordOidcOnly": "Овај корисник се пријављује преко екстерног провајдера и нема лозинку.",
"resetPasswordPlaceholder": "Нова лозинка",
"resetPasswordBtn": "Ресетуј",
"resetPasswordWorking": "Ресетовање...",
"resetPasswordSuccess": "Ресетовање лозинке",
"resetPasswordSuccessWiped": "Ресетовање лозинке. Шифровани подаци корисника су обрисани.",
"resetPasswordFailed": "Ресетовање лозинке није успело",
"resetPasswordConfirmWipe": "{{username}} се није пријавио од надоградње шифровања, тако да се његови подаци не могу повратити. Ресетовањем сада ће се избрисати његови хостови, акредитиви и фрагменти. Желите ли да наставите?",
"totpSectionTitle": "Двофакторска аутентификација",
"totpStatusEnabled": "TOTP је омогућен за овог корисника",
"totpStatusDisabled": "TOTP није омогућен за овог корисника",
"disableTotp": "Онемогући",
"disableTotpConfirm": "Онемогућите двофакторску аутентификацију за {{username}}? Моћи ће да се пријаве само са својом лозинком.",
"totpDisabledSuccess": "ТОТП је онемогућен",
"totpDisableFailed": "Онемогућавање TOTP-а није успело",
"manageApiKeys": "API кључеви",
"apiKeyNamePlaceholder": "Назив кључа",
"apiKeyCopyNotice": "Копирајте овај кључ сада, више се неће приказивати.",
"noApiKeysForUser": "Нема API кључева",
"apiKeyDeleteFailed": "Брисање API кључа није успело",
"exportUserData": "Извоз података",
"exportUserDataDesc": "Преузмите хостове, акредитиве и податке менаџера датотека овог корисника као JSON. Тајне су дешифроване.",
"exportUserDataSuccess": "Извезени подаци корисника",
"exportUserDataFailed": "Извоз корисничких података није успео",
"hostsCount": "{{count}} домаћини",
"addHostForUser": "Додај хоста",
"noHostsForUser": "Овај корисник нема хостове",
"connectToHost": "Повежи се",
"deleteHostConfirm": "Да ли желите да обришете хост „{{name}}“ који припада {{username}}?",
"hostDeletedSuccess": "Хост је обрисан",
"hostDeleteFailed": "Брисање хоста није успело",
"credentialsCount": "{{count}} акредитиви",
"addCredentialForUser": "Додај акредитив",
"noCredentialsForUser": "Овај корисник нема акредитиве",
"deleteCredentialConfirm": "Обришите акредитив „{{name}}“ који припада {{username}}?",
"credentialDeletedSuccess": "Акредитив је обрисан",
"credentialDeleteFailed": "Брисање акредитива није успело",
"snippetsCount": "{{count}} исечци",
"addSnippetForUser": "Додај исечак",
"noSnippetsForUser": "Овај корисник нема исечке кода",
"snippetRequiredFields": "Назив и садржај фрагмента су обавезни",
"snippetNamePlaceholder": "Назив исечка",
"snippetContentPlaceholder": "Садржај команде",
"snippetFolderPlaceholder": "Фасцикла (опционо)",
"snippetSaved": "Исечак је сачуван",
"snippetSaveFailed": "Чување фрагмента није успело",
"deleteSnippetConfirm": "Да ли желите да обришете исечак „{{name}}“ који припада {{username}}?",
"snippetDeletedSuccess": "Исечак је обрисан",
"snippetDeleteFailed": "Брисање исечка није успело",
"noSessionsForUser": "Нема активних сесија",
"deleteUserDangerDesc": "Трајно обриши {{username}} и све њихове податке (хостове, акредитиве, фрагменте, историју). Ова радња се не може поништити.",
"deleteUserConfirm": "Трајно избрисати {{username}} и све њихове податке?",
"deleteUserAdminBlocked": "Уклоните администраторски статус пре брисања овог корисника.",
"createRoleRequired": "Име и име за приказ су обавезни",
"createRoleSuccess": "Улога „{{name}}“ је креирана",
"createRoleFailed": "Није успело креирање улоге",
@@ -2822,7 +2942,16 @@
"hostDefaultsCommandHistory": "Историја команди",
"hostDefaultsCommandHistoryDesc": "Праћење историје команди на новим хостовима подразумевано",
"hostDefaultsSaved": "Подразумевана подешавања хоста су сачувана",
"hostDefaultsSaveFailed": "Чување подразумеваних подешавања хоста није успело"
"hostDefaultsSaveFailed": "Чување подразумеваних подешавања хоста није успело",
"rolePermissions": {
"count": "{{count}} дозволе",
"editAction": "Измени дозволе",
"loadError": "Учитавање каталога дозвола није успело",
"saved": "Дозволе за улогу су сачуване",
"saveError": "Чување дозвола за улогу није успело",
"save": "Сачувај",
"saving": "Чување..."
}
},
"newUi": {
"sidebar": {
@@ -2991,6 +3120,10 @@
"executionFailed": "Неуспешно"
},
"userProfile": {
"donateTitle": "Нека Термикс остане жив",
"donateDescription": "Termix је бесплатан, самостално хостован и изградило га је само неколико људи у своје слободно време. Ако вам је уштедео време или новац, донација криптовалута помаже да се одржи.",
"donateMilestones": "Донације помажу у финансирању времена за истраживање и учење шта је потребно за изградњу подршке за SAML, Kubernetes и агенте. Пратите напредак и донирајте.",
"donateButton": "Донирајте криптовалуту",
"storageModeLocal": "Прегледач",
"storageModeCloud": "База података",
"storageModeDescription": "Прегледач чува подешавања само у овом прегледачу. База података се синхронизује са сервером и учитава на било ком уређају.",
@@ -3011,6 +3144,10 @@
"twoFaOn": "Укључено",
"twoFaOff": "Искључено",
"versionLabel": "Верзија",
"betaProgramTitle": "Бета програм",
"betaProgramDescription": "Испробајте предстојеће функције рано уз недељну ознаку :beta Docker. Нестабилно, није за продукцију.",
"betaProgramFeedback": "Пронашли сте грешку? Пријавите је овде.",
"betaProgramLearnMore": "Сазнајте више",
"deleteAccount": "Обриши налог",
"deleteAccountDescription": "Трајно избришите свој налог",
"changeServerDescription": "Пребаците се на други Termix backend сервер",
+158 -21
View File
@@ -864,6 +864,21 @@
"proxmoxDockerDetectionDesc": "Kommaavgränsade namnmönster som gör det möjligt för Docker att matcha gäster",
"proxmoxPreferredRanges": "Föredragna IP-intervall",
"proxmoxPreferredRangesDesc": "Kommaavgränsade prefix i prioritetsordning för IP-val när en gäst har flera gränssnitt",
"proxmoxAutoSync": "Synkronisera gäster automatiskt",
"proxmoxAutoSyncDesc": "Upptäck regelbundet denna Proxmox-nod och skapa eller uppdatera importerade gästvärdar medan din session är upplåst.",
"proxmoxSyncInterval": "Synkroniseringsintervall (minuter)",
"proxmoxSyncIntervalDesc": "Minst 5 minuter. Schemaläggaren hoppar över låsta användarsessioner.",
"proxmoxMarkMissing": "Markera saknade gäster",
"proxmoxMarkMissingDesc": "Lägg till en proxmox-missing-tagg när en tidigare importerad gäst försvinner istället för att ta bort den.",
"proxmoxLastSync": "Senaste synkronisering",
"proxmoxLastSyncNever": "Inte synkroniserad än",
"proxmoxLastSyncNoResult": "Inga resultatdetaljer",
"proxmoxLastSyncSummary": "{{created}} skapad, {{updated}} uppdaterad, {{markedMissing}} saknas, {{skipped}} hoppad över",
"proxmoxLastSyncStatus": {
"success": "Framgång",
"error": "Misslyckades",
"pending": "I avvaktan på"
},
"proxmoxDiscoverAction": "Upptäck och importera Proxmox-gäster",
"proxmoxImportTitle": "Importera från Proxmox",
"proxmoxSelectHost": "Välj en Proxmox-värd…",
@@ -1167,7 +1182,57 @@
"shareHost": "Dela värd",
"shareHostTitle": "Dela: {{name}}",
"sharing": {
"requiresCredential": "Den här värden måste använda en autentiseringsuppgift för att aktivera delning. Redigera värden och tilldela en autentiseringsuppgift först."
"loadError": "Misslyckades med att ladda delningsdata. Försök igen.",
"shareWithSection": "Dela med",
"usersTab": "Användare",
"rolesTab": "Roller",
"searchPlaceholder": "Sök användare eller roller...",
"noMatches": "Inga träffar hittades",
"permissionLevelLabel": "Behörighetsnivå",
"levels": {
"connect": {
"label": "Ansluta",
"description": "Endast öppna sessioner: terminal, fjärrskrivbord, filhanterare, tunnlar och Docker. Ingen åtkomst till värdkonfigurationen."
},
"view": {
"label": "Se",
"description": "Anslut, plus se värdkonfigurationen. Hemligheter visas aldrig."
},
"edit": {
"label": "Redigera",
"description": "Visa och ändra värden. Hemligheter kan ersättas men aldrig läsas; tilldelningar av autentiseringsuppgifter förblir endast ägarens ansvar."
},
"manage": {
"label": "Hantera",
"description": "Redigera, plus dela värden med andra, ändra behörighetsnivåer och återkalla åtkomst."
}
},
"expiryLabel": "Åtkomstutgång",
"expiry": {
"never": "Aldrig",
"oneHour": "1 timme",
"oneDay": "24 timmar",
"sevenDays": "7 dagar",
"thirtyDays": "30 dagar",
"custom": "Beställnings"
},
"customHoursPlaceholder": "Timmar tills åtkomsten upphör",
"shareButton": "Dela",
"shareWithCount": "Dela ({{count}})",
"currentAccess": "Nuvarande åtkomst",
"noAccessEntries": "Denna värd har inte delats ännu",
"grantedBy": "Beviljad av",
"expires": "Utgår",
"expired": "Utgången",
"never": "Aldrig",
"revoke": "Återkalla",
"accessUpdated": "Åtkomst uppdaterad",
"accessUpdateFailed": "Misslyckades med att uppdatera åtkomsten",
"sharedBadge": "Delad",
"sharedBadgeTooltip": "Delad av {{owner}} ({{level}} åtkomst)",
"viewOnlyBanner": "Denna värd delas med dig av {{owner}} med läsbehörighet. Konfigurationen är skrivskyddad.",
"sharedEditBanner": "Denna värd delas med dig av {{owner}} med redigeringsåtkomst. Ändringarna gäller för den verkliga värden; autentiseringsreferenser kan endast ändras av ägaren.",
"ownerOnlyControl": "Endast värdägaren kan ändra detta"
},
"guac": {
"connection": "Förbindelse",
@@ -1302,28 +1367,10 @@
"backspaceKey": "Backstegstangenten",
"saveHostFirst": "Spara värden först.",
"sharingOptionsAfterSave": "Delningsalternativ är tillgängliga efter att värden har sparats.",
"sharingLoadError": "Misslyckades med att ladda delningsdata. Kontrollera din anslutning och försök igen.",
"shareHostSection": "Dela värd",
"shareWithUser": "Dela med användaren",
"shareWithRole": "Dela med roll",
"selectUser": "Välj användare",
"selectRole": "Välj roll",
"selectUserOption": "Välj en användare...",
"selectRoleOption": "Välj en roll...",
"permissionLevel": "Behörighetsnivå",
"expiresInHours": "Utgår om (timmar)",
"noExpiryPlaceholder": "Lämna tomt för inget utgångsdatum",
"shareBtn": "Dela",
"currentAccess": "Nuvarande åtkomst",
"typeHeader": "Typ",
"targetHeader": "Mål",
"permissionHeader": "Tillstånd",
"grantedByHeader": "Beviljad av",
"expiresHeader": "Utgår",
"noAccessEntries": "Inga åtkomstposter ännu.",
"expiredLabel": "Utgången",
"neverLabel": "Aldrig",
"revokeBtn": "Återkalla",
"cancelBtn": "Avboka",
"savingBtn": "Sparande...",
"updateHostBtn": "Uppdatera värd",
@@ -2190,7 +2237,8 @@
"passwordLoginDisabledDesc": "Lösenordsinloggning är inaktiverad. Använd en lösenordsnyckel eller en extern autentiseringsleverantör.",
"signInWithPasskey": "Logga in med lösenord",
"passkeyLoginFailed": "Inloggning med lösenordsnyckel misslyckades",
"attemptsRemaining": "{{count}} försök återstår"
"attemptsRemaining": "{{count}} försök återstår",
"confirmResetDataWipe": "Det här kontot har inte loggats in sedan krypteringsuppgraderingen, så dess lagrade data kan inte återställas utan det gamla lösenordet. Om du återställer kontot tas dess värdar, inloggningsuppgifter och kodavsnitt permanent bort. Vill du fortsätta?"
},
"hostKey": {
"verifyNewHost": "Verifiera SSH-värdnyckel",
@@ -2323,6 +2371,13 @@
"serviceLinks": "Servicelänkar",
"homepagePreview": "Förhandsvisning av startsidan"
},
"donation": {
"title": "Gillar du Termix?",
"body": "Termix är gratis och öppen källkod, byggt och underhållet av ett team på två personer på vår fritid. Om det ersätter ett kommersiellt verktyg som du annars skulle betala för, hjälper en donation till att täcka hostingkostnader och hålla utvecklingen igång. För närvarande är donationer endast kryptovalutor.",
"milestones": "Donationer hjälper också till att finansiera tiden som krävs för att undersöka och lära sig vad som behövs för att bygga funktioner som SAML och Kubernetes-stöd. Se framstegen på donationssidan.",
"cta": "Donera",
"dismiss": "Kanske senare"
},
"dashboardTab": {
"stable": "STABIL",
"hostsOnline": "Värdar online",
@@ -2790,6 +2845,71 @@
"updateAdminStatusFailed": "Misslyckades med att uppdatera administratörsstatusen",
"allSessionsRevoked": "Alla sessioner återkallade",
"revokeSessionsFailed": "Misslyckades med att återkalla sessioner",
"manageUserData": "Hantera användardata",
"backToUsers": "Tillbaka till användarna",
"manageTabAccount": "Konto",
"manageTabHosts": "Värdar",
"manageTabCredentials": "Referenser",
"manageTabSnippets": "Snuttar",
"manageTabSessions": "Sessioner",
"manageTabDanger": "Fara",
"manageEditorBack": "Tillbaka till {{username}}s data",
"dataLockedBadge": "LÅST",
"dataLockedNotice": "{{username}}s data förblir låsta tills de loggar in nästa gång. Deras värdar, inloggningsuppgifter och utdrag kan inte visas eller redigeras förrän då.",
"resetPasswordTitle": "Återställ lösenord",
"resetPasswordOidcOnly": "Den här användaren loggar in via en extern leverantör och har inget lösenord.",
"resetPasswordPlaceholder": "Nytt lösenord",
"resetPasswordBtn": "Återställa",
"resetPasswordWorking": "Återställer...",
"resetPasswordSuccess": "Återställning av lösenord",
"resetPasswordSuccessWiped": "Lösenordsåterställning. Användarens krypterade data har raderats.",
"resetPasswordFailed": "Misslyckades med att återställa lösenordet",
"resetPasswordConfirmWipe": "{{username}} har inte loggat in sedan krypteringsuppgraderingen, så deras data kan inte återställas. Om du återställer nu raderas deras värdar, inloggningsuppgifter och kodavsnitt. Vill du fortsätta?",
"totpSectionTitle": "Tvåfaktorsautentisering",
"totpStatusEnabled": "TOTP är aktiverat för den här användaren",
"totpStatusDisabled": "TOTP är inte aktiverat för den här användaren",
"disableTotp": "Inaktivera",
"disableTotpConfirm": "Inaktivera tvåfaktorsautentisering för {{username}}? De kommer bara att kunna logga in med sitt lösenord.",
"totpDisabledSuccess": "TOTP inaktiverad",
"totpDisableFailed": "Misslyckades med att inaktivera TOTP",
"manageApiKeys": "API-nycklar",
"apiKeyNamePlaceholder": "Nyckelnamn",
"apiKeyCopyNotice": "Kopiera den här nyckeln nu, den kommer inte att visas igen.",
"noApiKeysForUser": "Inga API-nycklar",
"apiKeyDeleteFailed": "Misslyckades med att ta bort API-nyckeln",
"exportUserData": "Dataexport",
"exportUserDataDesc": "Ladda ner den här användarens värdar, inloggningsuppgifter och filhanteringsdata som JSON. Hemligheterna dekrypteras.",
"exportUserDataSuccess": "Användardata exporterad",
"exportUserDataFailed": "Misslyckades med att exportera användardata",
"hostsCount": "{{count}} värdar",
"addHostForUser": "Lägg till värd",
"noHostsForUser": "Den här användaren har inga värdar",
"connectToHost": "Ansluta",
"deleteHostConfirm": "Radera värden \"{{name}}\" som tillhör {{username}}?",
"hostDeletedSuccess": "Värd borttagen",
"hostDeleteFailed": "Misslyckades med att ta bort värden",
"credentialsCount": "{{count}} inloggningsuppgifter",
"addCredentialForUser": "Lägg till autentiseringsuppgifter",
"noCredentialsForUser": "Den här användaren har inga inloggningsuppgifter",
"deleteCredentialConfirm": "Ta bort inloggningsuppgifterna \"{{name}}\" som tillhör {{username}}?",
"credentialDeletedSuccess": "Inloggningsuppgifter raderade",
"credentialDeleteFailed": "Misslyckades med att ta bort inloggningsuppgifter",
"snippetsCount": "{{count}} utdrag",
"addSnippetForUser": "Lägg till utdrag",
"noSnippetsForUser": "Den här användaren har inga utdrag",
"snippetRequiredFields": "Namn och innehåll på utdrag krävs",
"snippetNamePlaceholder": "Namn på utdrag",
"snippetContentPlaceholder": "Kommandoinnehåll",
"snippetFolderPlaceholder": "Mapp (valfritt)",
"snippetSaved": "Utdrag sparat",
"snippetSaveFailed": "Kunde inte spara utdraget",
"deleteSnippetConfirm": "Ta bort utdraget \"{{name}}\" som tillhör {{username}}?",
"snippetDeletedSuccess": "Utdraget raderat",
"snippetDeleteFailed": "Misslyckades med att ta bort utdraget",
"noSessionsForUser": "Inga aktiva sessioner",
"deleteUserDangerDesc": "Ta bort {{username}} och all dess data (värdar, inloggningsuppgifter, kodavsnitt, historik) permanent. Detta kan inte ångras.",
"deleteUserConfirm": "Vill du permanent radera {{username}} och all deras data?",
"deleteUserAdminBlocked": "Ta bort administratörsstatus innan du tar bort den här användaren.",
"createRoleRequired": "Namn och visningsnamn krävs",
"createRoleSuccess": "Rollen \"{{name}}\" skapades",
"createRoleFailed": "Misslyckades med att skapa rollen",
@@ -2822,7 +2942,16 @@
"hostDefaultsCommandHistory": "Kommandohistorik",
"hostDefaultsCommandHistoryDesc": "Spåra kommandohistorik på nya värdar som standard",
"hostDefaultsSaved": "Värdstandardvärden sparade",
"hostDefaultsSaveFailed": "Misslyckades med att spara värdens standardinställningar"
"hostDefaultsSaveFailed": "Misslyckades med att spara värdens standardinställningar",
"rolePermissions": {
"count": "{{count}} behörigheter",
"editAction": "Redigera behörigheter",
"loadError": "Det gick inte att läsa in behörighetskatalogen",
"saved": "Rollbehörigheter sparade",
"saveError": "Kunde inte spara rollbehörigheter",
"save": "Spara",
"saving": "Sparande..."
}
},
"newUi": {
"sidebar": {
@@ -2991,6 +3120,10 @@
"executionFailed": "Misslyckades"
},
"userProfile": {
"donateTitle": "Håll Termix vid liv",
"donateDescription": "Termix är gratis, självhostat och byggt av endast ett fåtal personer på sin fritid. Om det har sparat dig tid eller pengar, hjälper en kryptodonation till att hålla det igång.",
"donateMilestones": "Donationer hjälper till att finansiera tiden för att undersöka och lära sig vad som behövs för att bygga stöd för SAML, Kubernetes och agenter. Se framstegen och donera.",
"donateButton": "Donera krypto",
"storageModeLocal": "Webbläsare",
"storageModeCloud": "Databas",
"storageModeDescription": "Webbläsaren lagrar inställningar endast i den här webbläsaren. Databasen synkroniseras med servern och laddas på valfri enhet.",
@@ -3011,6 +3144,10 @@
"twoFaOn": "På",
"twoFaOff": "Av",
"versionLabel": "Version",
"betaProgramTitle": "Betaprogram",
"betaProgramDescription": "Testa kommande funktioner tidigt med den veckovisa rullande :beta Docker-taggen. Instabil, inte för produktion.",
"betaProgramFeedback": "Hittat ett fel? Rapportera det här.",
"betaProgramLearnMore": "Läs mer",
"deleteAccount": "Ta bort konto",
"deleteAccountDescription": "Ta bort ditt konto permanent",
"changeServerDescription": "Byt till en annan Termix-backendserver",
+158 -21
View File
@@ -864,6 +864,21 @@
"proxmoxDockerDetectionDesc": "รูปแบบชื่อที่คั่นด้วยเครื่องหมายจุลภาค ซึ่งช่วยให้ Docker สามารถจับคู่กับเครื่องเสมือนได้",
"proxmoxPreferredRanges": "ช่วง IP ที่ต้องการ",
"proxmoxPreferredRangesDesc": "คำนำหน้า IP ที่คั่นด้วยเครื่องหมายจุลภาคเรียงตามลำดับความสำคัญสำหรับการเลือก IP เมื่อผู้ใช้งานมีอินเทอร์เฟซหลายตัว",
"proxmoxAutoSync": "ซิงค์ข้อมูลแขกโดยอัตโนมัติ",
"proxmoxAutoSyncDesc": "ตรวจสอบโหนด Proxmox นี้เป็นระยะ และสร้างหรืออัปเดตโฮสต์แขกที่นำเข้าในขณะที่เซสชันของคุณไม่ได้ล็อกอยู่",
"proxmoxSyncInterval": "ช่วงเวลาการซิงค์ (นาที)",
"proxmoxSyncIntervalDesc": "อย่างน้อย 5 นาที ระบบกำหนดเวลาจะข้ามเซสชันของผู้ใช้ที่ถูกล็อกไว้",
"proxmoxMarkMissing": "ทำเครื่องหมายแขกที่ขาดหายไป",
"proxmoxMarkMissingDesc": "เพิ่มแท็ก proxmox-missing เมื่อเครื่องเสมือนที่นำเข้าก่อนหน้านี้หายไป แทนที่จะลบออก",
"proxmoxLastSync": "การซิงค์ครั้งล่าสุด",
"proxmoxLastSyncNever": "ยังไม่ได้ซิงค์ข้อมูล",
"proxmoxLastSyncNoResult": "ไม่มีรายละเอียดผลลัพธ์",
"proxmoxLastSyncSummary": "{{created}} ถูกสร้างขึ้น, {{updated}} อัปเดตแล้ว, {{markedMissing}} หายไป, {{skipped}} ข้ามไป",
"proxmoxLastSyncStatus": {
"success": "ความสำเร็จ",
"error": "ล้มเหลว",
"pending": "รอดำเนินการ"
},
"proxmoxDiscoverAction": "ค้นหาและนำเข้าแขกของ Proxmox",
"proxmoxImportTitle": "นำเข้าจาก Proxmox",
"proxmoxSelectHost": "เลือกโฮสต์ Proxmox…",
@@ -1167,7 +1182,57 @@
"shareHost": "แชร์โฮสต์",
"shareHostTitle": "แชร์: {{name}}",
"sharing": {
"requiresCredential": "โฮสต์นี้ต้องใช้ข้อมูลรับรองเพื่อเปิดใช้งานการแชร์ แก้ไขข้อมูลโฮสต์และกำหนดข้อมูลรับรองก่อน"
"loadError": "ไม่สามารถโหลดข้อมูลการแชร์ได้ โปรดลองอีกครั้ง",
"shareWithSection": "แบ่งปันกับ",
"usersTab": "ผู้ใช้",
"rolesTab": "บทบาท",
"searchPlaceholder": "ค้นหาผู้ใช้หรือบทบาท...",
"noMatches": "ไม่พบผลลัพธ์ที่ตรงกัน",
"permissionLevelLabel": "ระดับการอนุญาต",
"levels": {
"connect": {
"label": "เชื่อมต่อ",
"description": "อนุญาตเฉพาะเซสชันที่เปิดใช้งานเท่านั้น: เทอร์มินัล, เดสก์ท็อประยะไกล, ตัวจัดการไฟล์, อุโมงค์ และ Docker ไม่สามารถเข้าถึงการกำหนดค่าโฮสต์ได้"
},
"view": {
"label": "ดู",
"description": "เชื่อมต่อและดูการตั้งค่าโฮสต์ ข้อมูลลับจะไม่ถูกเปิดเผย"
},
"edit": {
"label": "แก้ไข",
"description": "ดูและแก้ไขข้อมูลโฮสต์ได้ สามารถเปลี่ยนรหัสลับได้ แต่ไม่สามารถอ่านได้ การกำหนดสิทธิ์การเข้าถึงจะสงวนไว้เฉพาะเจ้าของเท่านั้น"
},
"manage": {
"label": "จัดการ",
"description": "แก้ไข รวมถึงแชร์โฮสต์กับผู้อื่น เปลี่ยนระดับสิทธิ์ และเพิกถอนการเข้าถึงได้"
}
},
"expiryLabel": "วันหมดอายุการเข้าถึง",
"expiry": {
"never": "ไม่เคย",
"oneHour": "1 ชั่วโมง",
"oneDay": "24 ชั่วโมง",
"sevenDays": "7 วัน",
"thirtyDays": "30 วัน",
"custom": "กำหนดเอง"
},
"customHoursPlaceholder": "เหลือเวลาอีกกี่ชั่วโมงก่อนที่สิทธิ์การเข้าถึงจะหมดอายุ",
"shareButton": "แบ่งปัน",
"shareWithCount": "แชร์ ({{count}})",
"currentAccess": "การเข้าถึงปัจจุบัน",
"noAccessEntries": "โฮสต์นี้ยังไม่ได้ถูกแชร์",
"grantedBy": "ได้รับอนุญาตจาก",
"expires": "หมดอายุ",
"expired": "หมดอายุ",
"never": "ไม่เคย",
"revoke": "ถอน",
"accessUpdated": "การเข้าถึงได้รับการอัปเดตแล้ว",
"accessUpdateFailed": "ไม่สามารถอัปเดตการเข้าถึงได้",
"sharedBadge": "แชร์",
"sharedBadgeTooltip": "แชร์โดย {{owner}} ({{level}} เข้าถึงได้)",
"viewOnlyBanner": "โฮสต์นี้ถูกแชร์กับคุณโดย {{owner}} โดยมีสิทธิ์ในการดู การตั้งค่าเป็นแบบอ่านอย่างเดียว",
"sharedEditBanner": "โฮสต์นี้ถูกแชร์กับคุณโดย {{owner}} โดยมีสิทธิ์ในการแก้ไข การเปลี่ยนแปลงจะมีผลกับโฮสต์จริงเท่านั้น ข้อมูลอ้างอิงการตรวจสอบสิทธิ์สามารถเปลี่ยนแปลงได้โดยเจ้าของเท่านั้น",
"ownerOnlyControl": "เฉพาะเจ้าของโฮสต์เท่านั้นที่สามารถเปลี่ยนแปลงสิ่งนี้ได้"
},
"guac": {
"connection": "การเชื่อมต่อ",
@@ -1302,28 +1367,10 @@
"backspaceKey": "ปุ่มลบ",
"saveHostFirst": "บันทึกข้อมูลโฮสต์ก่อน",
"sharingOptionsAfterSave": "ตัวเลือกการแชร์จะพร้อมใช้งานหลังจากบันทึกโฮสต์แล้ว",
"sharingLoadError": "ไม่สามารถโหลดข้อมูลการแชร์ได้ โปรดตรวจสอบการเชื่อมต่อของคุณแล้วลองใหม่อีกครั้ง",
"shareHostSection": "แชร์โฮสต์",
"shareWithUser": "แชร์กับผู้ใช้",
"shareWithRole": "แชร์กับบทบาท",
"selectUser": "เลือกผู้ใช้",
"selectRole": "เลือกบทบาท",
"selectUserOption": "เลือกผู้ใช้...",
"selectRoleOption": "เลือกบทบาท...",
"permissionLevel": "ระดับการอนุญาต",
"expiresInHours": "หมดอายุใน (ชั่วโมง)",
"noExpiryPlaceholder": "เว้นว่างไว้เพื่อป้องกันการหมดอายุ",
"shareBtn": "แบ่งปัน",
"currentAccess": "การเข้าถึงปัจจุบัน",
"typeHeader": "พิมพ์",
"targetHeader": "เป้า",
"permissionHeader": "การอนุญาต",
"grantedByHeader": "ได้รับอนุญาตโดย",
"expiresHeader": "หมดอายุ",
"noAccessEntries": "ยังไม่มีข้อมูลการเข้าถึงใดๆ",
"expiredLabel": "หมดอายุ",
"neverLabel": "ไม่เคย",
"revokeBtn": "ถอน",
"cancelBtn": "ยกเลิก",
"savingBtn": "ประหยัด...",
"updateHostBtn": "อัปเดตโฮสต์",
@@ -2190,7 +2237,8 @@
"passwordLoginDisabledDesc": "การเข้าสู่ระบบด้วยรหัสผ่านถูกปิดใช้งาน โปรดใช้รหัสผ่านเฉพาะหรือผู้ให้บริการตรวจสอบสิทธิ์ภายนอก",
"signInWithPasskey": "ลงชื่อเข้าใช้ด้วยรหัสผ่าน",
"passkeyLoginFailed": "การเข้าสู่ระบบด้วยรหัสผ่านล้มเหลว",
"attemptsRemaining": "เหลือการลองอีก {{count}} ครั้ง"
"attemptsRemaining": "เหลือการลองอีก {{count}} ครั้ง",
"confirmResetDataWipe": "บัญชีนี้ไม่ได้เข้าสู่ระบบตั้งแต่การอัปเกรดการเข้ารหัส ดังนั้นข้อมูลที่จัดเก็บไว้จึงไม่สามารถกู้คืนได้หากไม่มีรหัสผ่านเดิม การรีเซ็ตจะลบโฮสต์ ข้อมูลประจำตัว และส่วนย่อยของบัญชีนี้อย่างถาวร ต้องการดำเนินการต่อหรือไม่?"
},
"hostKey": {
"verifyNewHost": "ตรวจสอบคีย์โฮสต์ SSH",
@@ -2323,6 +2371,13 @@
"serviceLinks": "ลิงก์บริการ",
"homepagePreview": "ตัวอย่างหน้าแรก"
},
"donation": {
"title": "ชอบ Termix ไหม?",
"body": "Termix เป็นซอฟต์แวร์โอเพนซอร์สฟรี สร้างและดูแลโดยทีมงานสองคนในเวลาว่าง หาก Termix เข้ามาแทนที่เครื่องมือเชิงพาณิชย์ที่คุณต้องจ่ายเงินอยู่แล้ว การบริจาคจะช่วยครอบคลุมค่าใช้จ่ายในการโฮสติ้งและพัฒนาต่อไป ปัจจุบันรับบริจาคเฉพาะคริปโตเคอร์เรนซีเท่านั้น",
"milestones": "เงินบริจาคยังช่วยสนับสนุนเวลาในการวิจัยและเรียนรู้สิ่งที่จำเป็นในการสร้างฟีเจอร์ต่างๆ เช่น SAML และการรองรับ Kubernetes ดูความคืบหน้าได้ที่หน้าบริจาค",
"cta": "บริจาค",
"dismiss": "อาจจะทีหลัง"
},
"dashboardTab": {
"stable": "มั่นคง",
"hostsOnline": "โฮสต์ออนไลน์",
@@ -2790,6 +2845,71 @@
"updateAdminStatusFailed": "ไม่สามารถอัปเดตสถานะผู้ดูแลระบบได้",
"allSessionsRevoked": "เซสชันทั้งหมดถูกยกเลิก",
"revokeSessionsFailed": "ไม่สามารถยกเลิกเซสชันได้",
"manageUserData": "จัดการข้อมูลผู้ใช้",
"backToUsers": "กลับสู่หน้าผู้ใช้",
"manageTabAccount": "บัญชี",
"manageTabHosts": "โฮสต์",
"manageTabCredentials": "คุณสมบัติ",
"manageTabSnippets": "เศษเสี้ยว",
"manageTabSessions": "การประชุม",
"manageTabDanger": "อันตราย",
"manageEditorBack": "กลับไปที่ข้อมูลของ {{username}}",
"dataLockedBadge": "ล็อกอยู่",
"dataLockedNotice": "ข้อมูลของ {{username}}จะถูกล็อกไว้จนกว่าพวกเขาจะเข้าสู่ระบบครั้งถัดไป จะไม่สามารถดูหรือแก้ไขโฮสต์ ข้อมูลประจำตัว และข้อความย่อของพวกเขาได้จนกว่าจะถึงเวลานั้น",
"resetPasswordTitle": "รีเซ็ตรหัสผ่าน",
"resetPasswordOidcOnly": "ผู้ใช้รายนี้เข้าสู่ระบบผ่านผู้ให้บริการภายนอกและไม่มีรหัสผ่าน",
"resetPasswordPlaceholder": "รหัสผ่านใหม่",
"resetPasswordBtn": "รีเซ็ต",
"resetPasswordWorking": "กำลังรีเซ็ต...",
"resetPasswordSuccess": "รีเซ็ตรหัสผ่าน",
"resetPasswordSuccessWiped": "รีเซ็ตรหัสผ่าน ข้อมูลที่เข้ารหัสของผู้ใช้ถูกลบออกแล้ว",
"resetPasswordFailed": "ไม่สามารถรีเซ็ตรหัสผ่านได้",
"resetPasswordConfirmWipe": "ผู้ใช้ {{username}} ไม่ได้ล็อกอินเข้าสู่ระบบตั้งแต่การอัปเกรดการเข้ารหัส ดังนั้นจึงไม่สามารถกู้คืนข้อมูลได้ การรีเซ็ตในขณะนี้จะลบโฮสต์ ข้อมูลประจำตัว และข้อมูลย่อของผู้ใช้ดังกล่าว ต้องการดำเนินการต่อหรือไม่?",
"totpSectionTitle": "การตรวจสอบสิทธิ์แบบสองปัจจัย",
"totpStatusEnabled": "TOTP เปิดใช้งานสำหรับผู้ใช้รายนี้แล้ว",
"totpStatusDisabled": "TOTP ไม่ได้เปิดใช้งานสำหรับผู้ใช้รายนี้",
"disableTotp": "ปิดใช้งาน",
"disableTotpConfirm": "ปิดใช้งานการตรวจสอบสิทธิ์แบบสองขั้นตอนสำหรับ {{username}}หรือไม่? พวกเขาจะสามารถเข้าสู่ระบบได้โดยใช้เพียงรหัสผ่านเท่านั้น",
"totpDisabledSuccess": "TOTP ถูกปิดใช้งาน",
"totpDisableFailed": "ไม่สามารถปิดใช้งาน TOTP ได้",
"manageApiKeys": "คีย์ API",
"apiKeyNamePlaceholder": "ชื่อหลัก",
"apiKeyCopyNotice": "คัดลอกรหัสนี้ไว้ ตอนนี้จะไม่ปรากฏอีกแล้ว",
"noApiKeysForUser": "ไม่มีคีย์ API",
"apiKeyDeleteFailed": "ไม่สามารถลบคีย์ API ได้",
"exportUserData": "การส่งออกข้อมูล",
"exportUserDataDesc": "ดาวน์โหลดข้อมูลโฮสต์ ข้อมูลประจำตัว และข้อมูลตัวจัดการไฟล์ของผู้ใช้รายนี้ในรูปแบบ JSON ข้อมูลลับจะถูกถอดรหัส",
"exportUserDataSuccess": "ข้อมูลผู้ใช้ที่ส่งออก",
"exportUserDataFailed": "ไม่สามารถส่งออกข้อมูลผู้ใช้ได้",
"hostsCount": "{{count}} โฮสต์",
"addHostForUser": "เพิ่มโฮสต์",
"noHostsForUser": "ผู้ใช้รายนี้ไม่มีโฮสต์",
"connectToHost": "เชื่อมต่อ",
"deleteHostConfirm": "ลบโฮสต์ \"{{name}}\" ที่เป็นของ {{username}} หรือไม่?",
"hostDeletedSuccess": "โฮสต์ถูกลบ",
"hostDeleteFailed": "ไม่สามารถลบโฮสต์ได้",
"credentialsCount": "{{count}} ข้อมูลประจำตัว",
"addCredentialForUser": "เพิ่มข้อมูลรับรอง",
"noCredentialsForUser": "ผู้ใช้รายนี้ไม่มีสิทธิ์เข้าสู่ระบบ",
"deleteCredentialConfirm": "ลบข้อมูลประจำตัว \"{{name}}\" ที่เป็นของ {{username}} หรือไม่?",
"credentialDeletedSuccess": "ข้อมูลประจำตัวถูกลบแล้ว",
"credentialDeleteFailed": "ไม่สามารถลบข้อมูลประจำตัวได้",
"snippetsCount": "{{count}} ตัวอย่างข้อความ",
"addSnippetForUser": "เพิ่มโค้ดตัวอย่าง",
"noSnippetsForUser": "ผู้ใช้รายนี้ไม่มีตัวอย่างโค้ด",
"snippetRequiredFields": "จำเป็นต้องระบุชื่อและเนื้อหาของโค้ดตัวอย่าง",
"snippetNamePlaceholder": "ชื่อส่วนย่อย",
"snippetContentPlaceholder": "เนื้อหาคำสั่ง",
"snippetFolderPlaceholder": "โฟลเดอร์ (ไม่จำเป็น)",
"snippetSaved": "บันทึกส่วนย่อยแล้ว",
"snippetSaveFailed": "ไม่สามารถบันทึกส่วนย่อยได้",
"deleteSnippetConfirm": "ลบส่วนย่อย \"{{name}}\" ที่เป็นของ {{username}} หรือไม่?",
"snippetDeletedSuccess": "ข้อความบางส่วนถูกลบแล้ว",
"snippetDeleteFailed": "ไม่สามารถลบส่วนย่อยได้",
"noSessionsForUser": "ไม่มีเซสชันที่ใช้งานอยู่",
"deleteUserDangerDesc": "ลบ {{username}} และข้อมูลทั้งหมด (โฮสต์ ข้อมูลรับรอง ตัวอย่างโค้ด ประวัติ) อย่างถาวร การดำเนินการนี้ไม่สามารถย้อนกลับได้",
"deleteUserConfirm": "ลบ {{username}} และข้อมูลทั้งหมดของพวกเขาอย่างถาวร?",
"deleteUserAdminBlocked": "กรุณาลบสิทธิ์ผู้ดูแลระบบก่อนลบผู้ใช้รายนี้",
"createRoleRequired": "ต้องระบุชื่อและชื่อที่แสดง",
"createRoleSuccess": "บทบาท \"{{name}}\" ถูกสร้างขึ้น",
"createRoleFailed": "ไม่สามารถสร้างบทบาทได้",
@@ -2822,7 +2942,16 @@
"hostDefaultsCommandHistory": "ประวัติคำสั่ง",
"hostDefaultsCommandHistoryDesc": "โดยค่าเริ่มต้น จะมีการบันทึกประวัติคำสั่งบนโฮสต์ใหม่",
"hostDefaultsSaved": "บันทึกค่าเริ่มต้นของโฮสต์แล้ว",
"hostDefaultsSaveFailed": "ไม่สามารถบันทึกค่าเริ่มต้นของโฮสต์ได้"
"hostDefaultsSaveFailed": "ไม่สามารถบันทึกค่าเริ่มต้นของโฮสต์ได้",
"rolePermissions": {
"count": "สิทธิ์ {{count}}",
"editAction": "แก้ไขสิทธิ์การเข้าถึง",
"loadError": "ไม่สามารถโหลดแคตตาล็อกสิทธิ์ได้",
"saved": "บันทึกสิทธิ์การใช้งานบทบาทแล้ว",
"saveError": "ไม่สามารถบันทึกสิทธิ์การเข้าถึงบทบาทได้",
"save": "บันทึก",
"saving": "ประหยัด..."
}
},
"newUi": {
"sidebar": {
@@ -2991,6 +3120,10 @@
"executionFailed": "ล้มเหลว"
},
"userProfile": {
"donateTitle": "รักษา Termix ให้คงอยู่ต่อไป",
"donateDescription": "Termix เป็นโปรแกรมฟรี ติดตั้งบนเซิร์ฟเวอร์ของคุณเอง และสร้างขึ้นโดยคนเพียงไม่กี่คนในเวลาว่าง หากโปรแกรมนี้ช่วยประหยัดเวลาหรือเงินของคุณ การบริจาคด้วยคริปโตเคอร์เรนซีจะช่วยให้โปรแกรมนี้ดำเนินต่อไปได้",
"donateMilestones": "เงินบริจาคช่วยสนับสนุนเวลาในการวิจัยและเรียนรู้สิ่งที่จำเป็นในการสร้าง SAML, Kubernetes และ Agent ดูความคืบหน้าและบริจาคได้ที่นี่",
"donateButton": "บริจาคคริปโต",
"storageModeLocal": "เบราว์เซอร์",
"storageModeCloud": "ฐานข้อมูล",
"storageModeDescription": "เบราว์เซอร์จะบันทึกการตั้งค่าไว้ในเบราว์เซอร์นี้เท่านั้น ฐานข้อมูลจะซิงค์กับเซิร์ฟเวอร์และโหลดได้บนทุกอุปกรณ์",
@@ -3011,6 +3144,10 @@
"twoFaOn": "บน",
"twoFaOff": "ปิด",
"versionLabel": "เวอร์ชั่น",
"betaProgramTitle": "โปรแกรมเบต้า",
"betaProgramDescription": "ทดลองใช้ฟีเจอร์ใหม่ ๆ ก่อนใครด้วยแท็ก Docker :beta ที่มีการอัปเดตทุกสัปดาห์ ยังไม่เสถียร ไม่เหมาะสำหรับใช้งานจริง",
"betaProgramFeedback": "พบข้อผิดพลาดใช่ไหม? รายงานได้ที่นี่",
"betaProgramLearnMore": "เรียนรู้เพิ่มเติม",
"deleteAccount": "ลบบัญชีผู้ใช้",
"deleteAccountDescription": "ลบบัญชีของคุณอย่างถาวร",
"changeServerDescription": "เปลี่ยนไปใช้เซิร์ฟเวอร์แบ็กเอนด์ Termix อื่น",
+158 -21
View File
@@ -864,6 +864,21 @@
"proxmoxDockerDetectionDesc": "Docker'ın konukları eşleştirmesini sağlayan virgülle ayrılmış ad kalıpları.",
"proxmoxPreferredRanges": "Tercih edilen IP aralıkları",
"proxmoxPreferredRangesDesc": "Misafir işletim sisteminin birden fazla arayüzü olduğunda IP seçimi için öncelik sırasına göre virgülle ayrılmış ön ekler.",
"proxmoxAutoSync": "Konukları otomatik olarak senkronize et",
"proxmoxAutoSyncDesc": "Oturumunuzun kilidi açıkken, periyodik olarak bu Proxmox düğümünü keşfedin ve içe aktarılan konuk sunucuları oluşturun veya güncelleyin.",
"proxmoxSyncInterval": "Senkronizasyon aralığı (dakika)",
"proxmoxSyncIntervalDesc": "Minimum 5 dakika. Zamanlayıcı, kilitli kullanıcı oturumlarını atlar.",
"proxmoxMarkMissing": "Mark'ın kayıp misafirleri",
"proxmoxMarkMissingDesc": "Daha önce içe aktarılan bir konuk işletim sistemi kaybolduğunda, onu silmek yerine proxmox-missing etiketi ekleyin.",
"proxmoxLastSync": "Son senkronizasyon",
"proxmoxLastSyncNever": "Henüz senkronize edilmedi",
"proxmoxLastSyncNoResult": "Sonuç bilgisi yok.",
"proxmoxLastSyncSummary": "{{created}} oluşturuldu, {{updated}} güncellendi, {{markedMissing}} eksik, {{skipped}} atlandı",
"proxmoxLastSyncStatus": {
"success": "Başarı",
"error": "Arızalı",
"pending": "Askıda olması"
},
"proxmoxDiscoverAction": "Proxmox misafirlerini keşfedin ve içe aktarın",
"proxmoxImportTitle": "Proxmox'tan içe aktar",
"proxmoxSelectHost": "Bir Proxmox sunucusu seçin…",
@@ -1167,7 +1182,57 @@
"shareHost": "Paylaşımlı Sunucu",
"shareHostTitle": "Paylaş: {{name}}",
"sharing": {
"requiresCredential": "Bu sunucunun paylaşımı etkinleştirmek için bir kimlik bilgisi kullanması gerekir. Öncelikle sunucuyu düzenleyin ve bir kimlik bilgisi atayın."
"loadError": "Paylaşım verileri yüklenemedi. Lütfen tekrar deneyin.",
"shareWithSection": "Şununla paylaş",
"usersTab": "Kullanıcılar",
"rolesTab": "Roller",
"searchPlaceholder": "Kullanıcıları veya rolleri ara...",
"noMatches": "Eşleşme bulunamadı.",
"permissionLevelLabel": "İzin seviyesi",
"levels": {
"connect": {
"label": "Bağlamak",
"description": "Yalnızca açık oturumlar: terminal, uzak masaüstü, dosya yöneticisi, tüneller ve Docker. Sunucu yapılandırmasına erişim yok."
},
"view": {
"label": "Görüş",
"description": "Bağlanın ve sunucu yapılandırmasını görün. Gizli bilgiler asla gösterilmez."
},
"edit": {
"label": "Düzenlemek",
"description": "Sunucuyu görüntüleyin ve değiştirin. Gizli bilgiler değiştirilebilir ancak asla okunamaz; kimlik bilgisi atamaları yalnızca sahibine özel kalır."
},
"manage": {
"label": "Üstesinden gelmek",
"description": "Sunucuyu düzenleyin, başkalarıyla paylaşın, izin seviyelerini değiştirin ve erişimi iptal edin."
}
},
"expiryLabel": "Erişim süresinin dolması",
"expiry": {
"never": "Asla",
"oneHour": "1 saat",
"oneDay": "24 saat",
"sevenDays": "7 gün",
"thirtyDays": "30 gün",
"custom": "Gelenek"
},
"customHoursPlaceholder": "Erişim süresinin dolmasına kalan saatler",
"shareButton": "Paylaşmak",
"shareWithCount": "Paylaş ({{count}})",
"currentAccess": "Mevcut erişim",
"noAccessEntries": "Bu sunucu henüz paylaşılmadı.",
"grantedBy": "tarafından verilmiştir.",
"expires": "Süresi doluyor",
"expired": "Günü geçmiş",
"never": "Asla",
"revoke": "Geri çekmek",
"accessUpdated": "Güncellenmiş erişim",
"accessUpdateFailed": "Erişim bilgilerini güncelleme başarısız oldu.",
"sharedBadge": "Paylaşıldı",
"sharedBadgeTooltip": "{{owner}} tarafından paylaşıldı ({{level}} erişim)",
"viewOnlyBanner": "Bu sunucu, {{owner}} tarafından sizinle görüntüleme erişimiyle paylaşılıyor. Yapılandırma yalnızca okunabilir durumdadır.",
"sharedEditBanner": "Bu sunucu, düzenleme erişimiyle {{owner}} tarafından sizinle paylaşılmaktadır. Değişiklikler gerçek sunucuya uygulanır; kimlik doğrulama referansları yalnızca sahibi tarafından değiştirilebilir.",
"ownerOnlyControl": "Bunu yalnızca ev sahibi değiştirebilir."
},
"guac": {
"connection": "Bağlantı",
@@ -1302,28 +1367,10 @@
"backspaceKey": "Geri tuşu",
"saveHostFirst": "Önce sunucuyu kurtarın.",
"sharingOptionsAfterSave": "Sunucu kaydedildikten sonra paylaşım seçenekleri kullanılabilir hale gelir.",
"sharingLoadError": "Paylaşım verileri yüklenemedi. Bağlantınızı kontrol edin ve tekrar deneyin.",
"shareHostSection": "Paylaşımlı Sunucu",
"shareWithUser": "Kullanıcıyla paylaş",
"shareWithRole": "Rol ile paylaş",
"selectUser": "Kullanıcı Seç",
"selectRole": "Rol Seçin",
"selectUserOption": "Bir kullanıcı seçin...",
"selectRoleOption": "Bir rol seçin...",
"permissionLevel": "İzin Seviyesi",
"expiresInHours": "(Saat) içinde sona eriyor",
"noExpiryPlaceholder": "Son kullanma tarihi olmaması için boş bırakın.",
"shareBtn": "Paylaşmak",
"currentAccess": "Mevcut Erişim",
"typeHeader": "Tip",
"targetHeader": "Hedef",
"permissionHeader": "İzin",
"grantedByHeader": "Veren",
"expiresHeader": "Süresi doluyor",
"noAccessEntries": "Henüz erişim kaydı yok.",
"expiredLabel": "Günü geçmiş",
"neverLabel": "Asla",
"revokeBtn": "Geri çekmek",
"cancelBtn": "İptal etmek",
"savingBtn": "Tasarruf...",
"updateHostBtn": "Sunucuyu Güncelle",
@@ -2190,7 +2237,8 @@
"passwordLoginDisabledDesc": "Parola ile giriş devre dışı bırakılmıştır. Lütfen bir parola anahtarı veya harici bir kimlik doğrulama sağlayıcısı kullanın.",
"signInWithPasskey": "Parola ile giriş yapın",
"passkeyLoginFailed": "Parola girişi başarısız oldu.",
"attemptsRemaining": "{{count}} deneme kaldı"
"attemptsRemaining": "{{count}} deneme kaldı",
"confirmResetDataWipe": "Şifreleme güncellemesinden bu yana bu hesaba giriş yapılmadı, bu nedenle eski parola olmadan saklanan veriler kurtarılamaz. Sıfırlama işlemi, ana bilgisayarlarını, kimlik bilgilerini ve kod parçacıklarını kalıcı olarak silecektir. Devam edilsin mi?"
},
"hostKey": {
"verifyNewHost": "SSH Ana Bilgisayar Anahtarını Doğrulayın",
@@ -2323,6 +2371,13 @@
"serviceLinks": "Hizmet Bağlantıları",
"homepagePreview": "Ana Sayfa Önizlemesi"
},
"donation": {
"title": "Termix'ten keyif alıyor musunuz?",
"body": "Termix, boş zamanlarımızda iki kişilik bir ekip tarafından geliştirilen ve sürdürülen ücretsiz ve açık kaynaklı bir platformdur. Eğer daha önce ücretli olarak kullandığınız ticari bir aracın yerini aldıysa, bağışlarınız barındırma maliyetlerini karşılamaya ve geliştirmenin devam etmesine yardımcı olur. Şu an itibariyle bağışlar yalnızca kripto para birimleriyle kabul edilmektedir.",
"milestones": "Bağışlar ayrıca SAML ve Kubernetes desteği gibi özelliklerin geliştirilmesi için gereken araştırma ve öğrenme süreçlerine harcanan zamana da katkı sağlıyor. Bağış sayfasından gelişmeler hakkında bilgi edinebilirsiniz.",
"cta": "Bağış yapmak",
"dismiss": "Belki daha sonra"
},
"dashboardTab": {
"stable": "STABİL",
"hostsOnline": "Çevrimiçi Sunucular",
@@ -2790,6 +2845,71 @@
"updateAdminStatusFailed": "Yönetici durumunu güncelleme başarısız oldu.",
"allSessionsRevoked": "Tüm oturumlar iptal edildi.",
"revokeSessionsFailed": "Oturumları iptal etme başarısız oldu.",
"manageUserData": "Kullanıcı verilerini yönetin",
"backToUsers": "Kullanıcılara geri dön",
"manageTabAccount": "Hesap",
"manageTabHosts": "Ev sahipleri",
"manageTabCredentials": "Kimlik Bilgileri",
"manageTabSnippets": "Kısa bölümler",
"manageTabSessions": "Oturumlar",
"manageTabDanger": "Tehlike",
"manageEditorBack": "{{username}}'ın verilerine geri dön",
"dataLockedBadge": "KİLİTLİ",
"dataLockedNotice": "{{username}}'ın verileri, bir sonraki girişlerine kadar kilitli kalır. O zamana kadar sunucuları, kimlik bilgileri ve kod parçacıkları görüntülenemez veya düzenlenemez.",
"resetPasswordTitle": "Şifreyi Sıfırla",
"resetPasswordOidcOnly": "Bu kullanıcı harici bir sağlayıcı aracılığıyla giriş yapıyor ve şifresi yok.",
"resetPasswordPlaceholder": "Yeni Şifre",
"resetPasswordBtn": "Sıfırla",
"resetPasswordWorking": "Sıfırlanıyor...",
"resetPasswordSuccess": "Şifre sıfırlama",
"resetPasswordSuccessWiped": "Parola sıfırlama. Kullanıcının şifrelenmiş verileri silindi.",
"resetPasswordFailed": "Parola sıfırlama başarısız oldu.",
"resetPasswordConfirmWipe": "{{username}} şifreleme yükseltmesinden beri giriş yapmadı, bu nedenle verileri kurtarılamaz. Şimdi sıfırlama işlemi, ana bilgisayarlarını, kimlik bilgilerini ve kod parçacıklarını silecektir. Devam edilsin mi?",
"totpSectionTitle": "İki Faktörlü Kimlik Doğrulama",
"totpStatusEnabled": "Bu kullanıcı için TOTP etkinleştirilmiştir.",
"totpStatusDisabled": "Bu kullanıcı için TOTP etkinleştirilmemiş.",
"disableTotp": "Devre dışı bırakmak",
"disableTotpConfirm": "{{username}}için iki faktörlü kimlik doğrulamayı devre dışı bırak? Sadece şifreleriyle giriş yapabilecekler.",
"totpDisabledSuccess": "TOTP devre dışı bırakıldı",
"totpDisableFailed": "TOTP devre dışı bırakılamadı.",
"manageApiKeys": "API Anahtarları",
"apiKeyNamePlaceholder": "Anahtar adı",
"apiKeyCopyNotice": "Bu anahtarı şimdi kopyalayın, bir daha gösterilmeyecek.",
"noApiKeysForUser": "API anahtarı yok",
"apiKeyDeleteFailed": "API anahtarını silme işlemi başarısız oldu.",
"exportUserData": "Veri Dışa Aktarımı",
"exportUserDataDesc": "Bu kullanıcının sunucularını, kimlik bilgilerini ve dosya yöneticisi verilerini JSON formatında indirin. Gizli bilgiler şifresi çözülmüştür.",
"exportUserDataSuccess": "Kullanıcı verileri dışa aktarıldı",
"exportUserDataFailed": "Kullanıcı verilerini dışa aktarma başarısız oldu.",
"hostsCount": "{{count}} sunucular",
"addHostForUser": "Sunucu Ekle",
"noHostsForUser": "Bu kullanıcının hiç sunucusu yok.",
"connectToHost": "Bağlamak",
"deleteHostConfirm": "{{username}}'e ait \"{{name}}\" sunucusunu silmek mi istiyorsunuz?",
"hostDeletedSuccess": "Sunucu silindi",
"hostDeleteFailed": "Sunucuyu silme işlemi başarısız oldu.",
"credentialsCount": "{{count}} kimlik bilgileri",
"addCredentialForUser": "Kimlik Bilgisi Ekle",
"noCredentialsForUser": "Bu kullanıcının kimlik bilgileri yok.",
"deleteCredentialConfirm": "{{username}}'e ait \"{{name}}\" kimlik bilgilerini silmek mi istiyorsunuz?",
"credentialDeletedSuccess": "Kimlik bilgisi silindi",
"credentialDeleteFailed": "Kimlik bilgilerini silme işlemi başarısız oldu.",
"snippetsCount": "{{count}} parçalar",
"addSnippetForUser": "Kod Parçası Ekle",
"noSnippetsForUser": "Bu kullanıcının hiç özeti yok.",
"snippetRequiredFields": "Kod parçasının adı ve içeriği zorunludur.",
"snippetNamePlaceholder": "Parça adı",
"snippetContentPlaceholder": "Komut içeriği",
"snippetFolderPlaceholder": "Klasör (isteğe bağlı)",
"snippetSaved": "Kısa bölüm kaydedildi",
"snippetSaveFailed": "Kod parçacığı kaydedilemedi.",
"deleteSnippetConfirm": "{{username}}'e ait \"{{name}}\" kod parçasını silmek mi istiyorsunuz?",
"snippetDeletedSuccess": "Kod parçası silindi",
"snippetDeleteFailed": "Kod parçasını silme işlemi başarısız oldu.",
"noSessionsForUser": "Aktif oturum yok",
"deleteUserDangerDesc": "{{username}} ve tüm verileri (ana bilgisayarlar, kimlik bilgileri, kod parçacıkları, geçmiş) kalıcı olarak silin. Bu işlem geri alınamaz.",
"deleteUserConfirm": "{{username}} ve tüm verilerini kalıcı olarak silmek mi?",
"deleteUserAdminBlocked": "Bu kullanıcıyı silmeden önce yönetici statüsünü kaldırın.",
"createRoleRequired": "Ad ve görünen ad zorunludur.",
"createRoleSuccess": "\"{{name}}\" rolü oluşturuldu",
"createRoleFailed": "Rol oluşturulamadı.",
@@ -2822,7 +2942,16 @@
"hostDefaultsCommandHistory": "Komut Geçmişi",
"hostDefaultsCommandHistoryDesc": "Yeni sunucularda komut geçmişini varsayılan olarak izle.",
"hostDefaultsSaved": "Sunucu varsayılan ayarları kaydedildi.",
"hostDefaultsSaveFailed": "Sunucu varsayılan ayarları kaydedilemedi."
"hostDefaultsSaveFailed": "Sunucu varsayılan ayarları kaydedilemedi.",
"rolePermissions": {
"count": "{{count}} izinler",
"editAction": "Düzenleme izinleri",
"loadError": "İzin kataloğu yüklenemedi.",
"saved": "Rol izinleri kaydedildi.",
"saveError": "Rol izinleri kaydedilemedi.",
"save": "Kaydetmek",
"saving": "Tasarruf..."
}
},
"newUi": {
"sidebar": {
@@ -2991,6 +3120,10 @@
"executionFailed": "Arızalı"
},
"userProfile": {
"donateTitle": "Termix'i hayatta tutun",
"donateDescription": "Termix ücretsizdir, kendi sunucunuzda barındırır ve sadece birkaç kişi tarafından boş zamanlarında geliştirilmiştir. Eğer size zaman veya para kazandırdıysa, kripto para bağışı projenin devam etmesine yardımcı olur.",
"donateMilestones": "Bağışlar, SAML, Kubernetes ve Agent desteği oluşturmak için gerekenleri araştırmak ve öğrenmek için harcanan zamana katkı sağlıyor. Gelişmeleri takip edin ve bağış yapın.",
"donateButton": "Kripto para bağışı yapın",
"storageModeLocal": "Tarayıcı",
"storageModeCloud": "Veritabanı",
"storageModeDescription": "Tarayıcı ayarları yalnızca bu tarayıcıda saklar. Veritabanı sunucuyla senkronize olur ve herhangi bir cihazda yüklenir.",
@@ -3011,6 +3144,10 @@
"twoFaOn": "Açık",
"twoFaOff": "Kapalı",
"versionLabel": "Sürüm",
"betaProgramTitle": "Beta Programı",
"betaProgramDescription": "Haftalık olarak güncellenen :beta Docker etiketiyle yeni özellikleri erkenden deneyin. Kararsızdır, üretim ortamı için uygun değildir.",
"betaProgramFeedback": "Bir hata mı buldunuz? Buradan bildirin.",
"betaProgramLearnMore": "Daha fazla bilgi edin",
"deleteAccount": "Hesabı Sil",
"deleteAccountDescription": "Hesabınızı kalıcı olarak silin.",
"changeServerDescription": "Farklı bir Termix arka uç sunucusuna geçin.",
+158 -21
View File
@@ -864,6 +864,21 @@
"proxmoxDockerDetectionDesc": "Шаблони імен, розділені комами, що дозволяють Docker знаходити гостьові файли",
"proxmoxPreferredRanges": "Бажані діапазони IP-адрес",
"proxmoxPreferredRangesDesc": "Префікси, розділені комами, у порядку пріоритетності для вибору IP-адреси, коли гость має кілька інтерфейсів",
"proxmoxAutoSync": "Автоматична синхронізація гостей",
"proxmoxAutoSyncDesc": "Періодично виявляйте цей вузол Proxmox та створюйте або оновлюйте імпортовані гостьові хости, поки ваш сеанс розблоковано.",
"proxmoxSyncInterval": "Інтервал синхронізації (хвилини)",
"proxmoxSyncIntervalDesc": "Мінімум 5 хвилин. Планувальник пропускає заблоковані сеанси користувачів.",
"proxmoxMarkMissing": "Позначте відсутніх гостей",
"proxmoxMarkMissingDesc": "Додати тег proxmox-missing, коли раніше імпортований гостьовий сервер зникає, замість його видалення.",
"proxmoxLastSync": "Остання синхронізація",
"proxmoxLastSyncNever": "Ще не синхронізовано",
"proxmoxLastSyncNoResult": "Немає деталей результату",
"proxmoxLastSyncSummary": "{{created}} створено, {{updated}} оновлено, {{markedMissing}} відсутнє, {{skipped}} пропущено",
"proxmoxLastSyncStatus": {
"success": "Успіх",
"error": "Не вдалося",
"pending": "Очікує на розгляд"
},
"proxmoxDiscoverAction": "Пошук та імпорт гостьових систем Proxmox",
"proxmoxImportTitle": "Імпорт з Proxmox",
"proxmoxSelectHost": "Виберіть хост Proxmox…",
@@ -1167,7 +1182,57 @@
"shareHost": "Спільний хост",
"shareHostTitle": "Поділитися: {{name}}",
"sharing": {
"requiresCredential": "Цей хост повинен використовувати облікові дані, щоб увімкнути спільний доступ. Спочатку відредагуйте хост і призначте облікові дані."
"loadError": "Не вдалося завантажити дані для спільного доступу. Спробуйте ще раз.",
"shareWithSection": "Поділитися з",
"usersTab": "Користувачі",
"rolesTab": "Ролі",
"searchPlaceholder": "Пошук користувачів або ролей...",
"noMatches": "Збігів не знайдено",
"permissionLevelLabel": "Рівень дозволів",
"levels": {
"connect": {
"label": "Підключитися",
"description": "Тільки відкриті сеанси: термінал, віддалений робочий стіл, файловий менеджер, тунелі та Docker. Без доступу до конфігурації хоста."
},
"view": {
"label": "Переглянути",
"description": "Підключитися, а також переглянути конфігурацію хоста. Секрети ніколи не відображаються."
},
"edit": {
"label": "Редагувати",
"description": "Перегляд і зміна хоста. Секрети можна замінити, але ніколи не прочитати; призначення облікових даних залишається лише для власника."
},
"manage": {
"label": "Керувати",
"description": "Редагувати, а також ділитися хостом з іншими, змінювати рівні дозволів та скасовувати доступ."
}
},
"expiryLabel": "Термін дії доступу",
"expiry": {
"never": "Ніколи",
"oneHour": "1 година",
"oneDay": "24 години",
"sevenDays": "7 днів",
"thirtyDays": "30 днів",
"custom": "Користувацька"
},
"customHoursPlaceholder": "Години до закінчення терміну дії доступу",
"shareButton": "Поділитися",
"shareWithCount": "Поділитися ({{count}})",
"currentAccess": "Поточний доступ",
"noAccessEntries": "Цей хост ще не був спільним",
"grantedBy": "Надано",
"expires": "Термін дії закінчується",
"expired": "Термін дії минув",
"never": "Ніколи",
"revoke": "Скасувати",
"accessUpdated": "Доступ оновлено",
"accessUpdateFailed": "Не вдалося оновити доступ",
"sharedBadge": "Спільний доступ",
"sharedBadgeTooltip": "Поділився {{owner}} (доступ{{level}})",
"viewOnlyBanner": "Цей хост надано вам спільний доступ до перегляду користувачем {{owner}} . Конфігурація доступна лише для читання.",
"sharedEditBanner": "Цей хост надано вам спільний доступ до редагування користувачем {{owner}} . Зміни застосовуються до справжнього хоста; посилання на автентифікацію може змінити лише власник.",
"ownerOnlyControl": "Тільки власник хоста може це змінити"
},
"guac": {
"connection": "З'єднання",
@@ -1302,28 +1367,10 @@
"backspaceKey": "Клавіша Backspace",
"saveHostFirst": "Спочатку збережіть хост.",
"sharingOptionsAfterSave": "Опції спільного доступу доступні після збереження хоста.",
"sharingLoadError": "Не вдалося завантажити дані для спільного доступу. Перевірте з’єднання та повторіть спробу.",
"shareHostSection": "Спільний хост",
"shareWithUser": "Поділитися з користувачем",
"shareWithRole": "Поділитися з роллю",
"selectUser": "Виберіть користувача",
"selectRole": "Виберіть роль",
"selectUserOption": "Виберіть користувача...",
"selectRoleOption": "Виберіть роль...",
"permissionLevel": "Рівень дозволів",
"expiresInHours": "Термін дії закінчується через (годин)",
"noExpiryPlaceholder": "Залиште порожнім, якщо термін дії не визначено.",
"shareBtn": "Поділитися",
"currentAccess": "Поточний доступ",
"typeHeader": "Тип",
"targetHeader": "Ціль",
"permissionHeader": "Дозвіл",
"grantedByHeader": "Надано",
"expiresHeader": "Термін дії закінчується",
"noAccessEntries": "Записів доступу поки що немає.",
"expiredLabel": "Термін дії минув",
"neverLabel": "Ніколи",
"revokeBtn": "Скасувати",
"cancelBtn": "Скасувати",
"savingBtn": "Збереження...",
"updateHostBtn": "Оновити хост",
@@ -2190,7 +2237,8 @@
"passwordLoginDisabledDesc": "Вхід за паролем вимкнено. Використайте ключ доступу або зовнішній постачальник автентифікації.",
"signInWithPasskey": "Увійти за допомогою ключа доступу",
"passkeyLoginFailed": "Не вдалося ввійти з ключем доступу",
"attemptsRemaining": "Залишилося {{count}} спроб"
"attemptsRemaining": "Залишилося {{count}} спроб",
"confirmResetDataWipe": "Цей обліковий запис не входив у систему з моменту оновлення шифрування, тому його збережені дані неможливо відновити без старого пароля. Скидання налаштувань призведе до остаточного видалення його хостів, облікових даних та фрагментів. Продовжити?"
},
"hostKey": {
"verifyNewHost": "Перевірте ключ хоста SSH",
@@ -2323,6 +2371,13 @@
"serviceLinks": "Посилання на сервіси",
"homepagePreview": "Попередній перегляд головної сторінки"
},
"donation": {
"title": "Подобається Термікс?",
"body": "Termix — це безкоштовний продукт з відкритим кодом, який створює та підтримує команда з двох осіб у вільний час. Якщо він замінює комерційний інструмент, за який ви б інакше платили, пожертва допомагає покрити витрати на хостинг і продовжувати розробку. Наразі пожертви надаються лише у криптовалюті.",
"milestones": "Пожертви також допомагають фінансувати час для дослідження та вивчення того, що потрібно для створення таких функцій, як підтримка SAML та Kubernetes. Слідкуйте за прогресом на сторінці пожертв.",
"cta": "Пожертвувати",
"dismiss": "Можливо, пізніше"
},
"dashboardTab": {
"stable": "СТАБІЛЬНИЙ",
"hostsOnline": "Хости онлайн",
@@ -2790,6 +2845,71 @@
"updateAdminStatusFailed": "Не вдалося оновити статус адміністратора",
"allSessionsRevoked": "Усі сесії скасовано",
"revokeSessionsFailed": "Не вдалося скасувати сеанси",
"manageUserData": "Керування даними користувачів",
"backToUsers": "Назад до користувачів",
"manageTabAccount": "Обліковий запис",
"manageTabHosts": "Хости",
"manageTabCredentials": "Облікові дані",
"manageTabSnippets": "Фрагменти",
"manageTabSessions": "Сесії",
"manageTabDanger": "Небезпека",
"manageEditorBack": "Назад до даних {{username}}",
"dataLockedBadge": "ЗАБЛОКОВАНО",
"dataLockedNotice": "Дані {{username}}залишаються заблокованими до наступного входу в систему. До того часу їхні хости, облікові дані та фрагменти не можна переглядати або редагувати.",
"resetPasswordTitle": "Скинути пароль",
"resetPasswordOidcOnly": "Цей користувач входить через зовнішнього постачальника та не має пароля.",
"resetPasswordPlaceholder": "Новий пароль",
"resetPasswordBtn": "Скинути",
"resetPasswordWorking": "Скидання...",
"resetPasswordSuccess": "Скидання пароля",
"resetPasswordSuccessWiped": "Скидання пароля. Зашифровані дані користувача було видалено.",
"resetPasswordFailed": "Не вдалося скинути пароль",
"resetPasswordConfirmWipe": "{{username}} не входив у систему після оновлення шифрування, тому його дані неможливо відновити. Скидання налаштувань зараз призведе до видалення його хостів, облікових даних та фрагментів. Продовжити?",
"totpSectionTitle": "Двофакторна автентифікація",
"totpStatusEnabled": "TOTP увімкнено для цього користувача",
"totpStatusDisabled": "TOTP не ввімкнено для цього користувача",
"disableTotp": "Вимкнути",
"disableTotpConfirm": "Вимкнути двофакторну автентифікацію для {{username}}? Вони зможуть увійти лише за допомогою пароля.",
"totpDisabledSuccess": "TOTP вимкнено",
"totpDisableFailed": "Не вдалося вимкнути TOTP",
"manageApiKeys": "Ключі API",
"apiKeyNamePlaceholder": "Назва ключа",
"apiKeyCopyNotice": "Скопіюйте цей ключ зараз, він більше не відображатиметься.",
"noApiKeysForUser": "Без ключів API",
"apiKeyDeleteFailed": "Не вдалося видалити ключ API",
"exportUserData": "Експорт даних",
"exportUserDataDesc": "Завантажити хости, облікові дані та дані файлового менеджера цього користувача у форматі JSON. Секрети розшифровано.",
"exportUserDataSuccess": "Експортовано дані користувача",
"exportUserDataFailed": "Не вдалося експортувати дані користувача",
"hostsCount": "{{count}} хости",
"addHostForUser": "Додати хоста",
"noHostsForUser": "Цей користувач не має хостів",
"connectToHost": "Підключитися",
"deleteHostConfirm": "Видалити хост \"{{name}}\", що належить {{username}}?",
"hostDeletedSuccess": "Хост видалено",
"hostDeleteFailed": "Не вдалося видалити хост",
"credentialsCount": "{{count}} облікові дані",
"addCredentialForUser": "Додати облікові дані",
"noCredentialsForUser": "Цей користувач не має облікових даних",
"deleteCredentialConfirm": "Видалити облікові дані \"{{name}}\", що належать {{username}}?",
"credentialDeletedSuccess": "Облікові дані видалено",
"credentialDeleteFailed": "Не вдалося видалити облікові дані",
"snippetsCount": "{{count}} фрагменти",
"addSnippetForUser": "Додати фрагмент",
"noSnippetsForUser": "Цей користувач не має фрагментів",
"snippetRequiredFields": "Назва та вміст фрагмента обов'язкові",
"snippetNamePlaceholder": "Назва фрагмента",
"snippetContentPlaceholder": "Зміст команди",
"snippetFolderPlaceholder": "Папка (необов'язково)",
"snippetSaved": "Фрагмент збережено",
"snippetSaveFailed": "Не вдалося зберегти фрагмент",
"deleteSnippetConfirm": "Видалити фрагмент \"{{name}}\", що належить {{username}}?",
"snippetDeletedSuccess": "Фрагмент видалено",
"snippetDeleteFailed": "Не вдалося видалити фрагмент",
"noSessionsForUser": "Немає активних сеансів",
"deleteUserDangerDesc": "Остаточно видалити {{username}} та всі його дані (хости, облікові дані, фрагменти коду, історію). Цю дію неможливо скасувати.",
"deleteUserConfirm": "Остаточно видалити {{username}} та всі його дані?",
"deleteUserAdminBlocked": "Видаліть статус адміністратора, перш ніж видаляти цього користувача.",
"createRoleRequired": "Ім'я та ім'я для відображення обов'язкові",
"createRoleSuccess": "Роль «{{name}}» створено",
"createRoleFailed": "Не вдалося створити роль",
@@ -2822,7 +2942,16 @@
"hostDefaultsCommandHistory": "Історія команд",
"hostDefaultsCommandHistoryDesc": "Відстеження історії команд на нових хостах за замовчуванням",
"hostDefaultsSaved": "Налаштування хоста за замовчуванням збережено",
"hostDefaultsSaveFailed": "Не вдалося зберегти налаштування хоста за замовчуванням"
"hostDefaultsSaveFailed": "Не вдалося зберегти налаштування хоста за замовчуванням",
"rolePermissions": {
"count": "{{count}} дозволи",
"editAction": "Редагувати дозволи",
"loadError": "Не вдалося завантажити каталог дозволів",
"saved": "Дозволи ролі збережено",
"saveError": "Не вдалося зберегти дозволи ролі",
"save": "Зберегти",
"saving": "Збереження..."
}
},
"newUi": {
"sidebar": {
@@ -2991,6 +3120,10 @@
"executionFailed": "Не вдалося"
},
"userProfile": {
"donateTitle": "Збережи Термікс живим",
"donateDescription": "Termix безкоштовний, розміщується самостійно та створений лише кількома людьми у вільний час. Якщо це заощадило вам час або гроші, пожертва у криптовалюті допоможе йому продовжувати роботу.",
"donateMilestones": "Пожертви допомагають фінансувати час для дослідження та вивчення того, що потрібно для створення підтримки SAML, Kubernetes та Agent. Слідкуйте за прогресом та робіть пожертви.",
"donateButton": "Пожертвувати криптовалюту",
"storageModeLocal": "Браузер",
"storageModeCloud": "База даних",
"storageModeDescription": "Браузер зберігає налаштування лише в цьому браузері. База даних синхронізується із сервером і завантажується на будь-якому пристрої.",
@@ -3011,6 +3144,10 @@
"twoFaOn": "Увімкнено",
"twoFaOff": "Вимкнено",
"versionLabel": "Версія",
"betaProgramTitle": "Бета-програма",
"betaProgramDescription": "Спробуйте майбутні функції заздалегідь за допомогою щотижневого тегу :beta Docker. Нестабільна версія, не для продакшену.",
"betaProgramFeedback": "Знайшли помилку? Повідомте про неї тут.",
"betaProgramLearnMore": "Дізнатися більше",
"deleteAccount": "Видалити обліковий запис",
"deleteAccountDescription": "Видалити обліковий запис назавжди",
"changeServerDescription": "Переключитися на інший сервер Termix",
+158 -21
View File
@@ -864,6 +864,21 @@
"proxmoxDockerDetectionDesc": "Các mẫu tên được phân tách bằng dấu phẩy cho phép Docker khớp với các máy khách.",
"proxmoxPreferredRanges": "Dải IP ưu tiên",
"proxmoxPreferredRangesDesc": "Các tiền tố được phân tách bằng dấu phẩy theo thứ tự ưu tiên để lựa chọn địa chỉ IP khi khách có nhiều giao diện.",
"proxmoxAutoSync": "Tự động đồng bộ khách",
"proxmoxAutoSyncDesc": "Định kỳ tìm kiếm nút Proxmox này và tạo hoặc cập nhật các máy chủ khách đã nhập trong khi phiên làm việc của bạn đang được mở khóa.",
"proxmoxSyncInterval": "Khoảng thời gian đồng bộ (phút)",
"proxmoxSyncIntervalDesc": "Tối thiểu 5 phút. Trình lập lịch sẽ bỏ qua các phiên người dùng bị khóa.",
"proxmoxMarkMissing": "Đánh dấu những vị khách vắng mặt",
"proxmoxMarkMissingDesc": "Thêm thẻ proxmox-missing khi khách ảo được nhập trước đó biến mất thay vì xóa nó.",
"proxmoxLastSync": "Lần đồng bộ cuối cùng",
"proxmoxLastSyncNever": "Chưa được đồng bộ hóa",
"proxmoxLastSyncNoResult": "Không có thông tin chi tiết về kết quả.",
"proxmoxLastSyncSummary": "{{created}} đã tạo, {{updated}} đã cập nhật, {{markedMissing}} bị thiếu, {{skipped}} đã bỏ qua",
"proxmoxLastSyncStatus": {
"success": "Thành công",
"error": "Thất bại",
"pending": "Chưa giải quyết"
},
"proxmoxDiscoverAction": "Khám phá và nhập khách Proxmox",
"proxmoxImportTitle": "Nhập từ Proxmox",
"proxmoxSelectHost": "Chọn máy chủ Proxmox…",
@@ -1167,7 +1182,57 @@
"shareHost": "Chia sẻ máy chủ",
"shareHostTitle": "Chia sẻ: {{name}}",
"sharing": {
"requiresCredential": "Máy chủ này phải sử dụng thông tin xác thực để cho phép chia sẻ. Hãy chỉnh sửa máy chủ và gán thông tin xác thực trước."
"loadError": "Không thể tải dữ liệu chia sẻ. Vui lòng thử lại.",
"shareWithSection": "Chia sẻ với",
"usersTab": "Người dùng",
"rolesTab": "Vai trò",
"searchPlaceholder": "Tìm kiếm người dùng hoặc vai trò...",
"noMatches": "Không tìm thấy kết quả phù hợp",
"permissionLevelLabel": "Cấp độ quyền hạn",
"levels": {
"connect": {
"label": "Kết nối",
"description": "Chỉ các phiên mở: terminal, máy tính từ xa, trình quản lý tập tin, đường hầm và Docker. Không có quyền truy cập vào cấu hình máy chủ."
},
"view": {
"label": "Xem",
"description": "Kết nối và xem cấu hình máy chủ. Thông tin bí mật sẽ không bao giờ được hiển thị."
},
"edit": {
"label": "Biên tập",
"description": "Xem và chỉnh sửa máy chủ. Có thể thay thế các thông tin bí mật nhưng không bao giờ đọc được; việc gán thông tin xác thực chỉ dành riêng cho chủ sở hữu."
},
"manage": {
"label": "Quản lý",
"description": "Chỉnh sửa, chia sẻ máy chủ với người khác, thay đổi cấp độ quyền và thu hồi quyền truy cập."
}
},
"expiryLabel": "Hết hạn truy cập",
"expiry": {
"never": "Không bao giờ",
"oneHour": "1 giờ",
"oneDay": "24 giờ",
"sevenDays": "7 ngày",
"thirtyDays": "30 ngày",
"custom": "Phong tục"
},
"customHoursPlaceholder": "Còn vài giờ nữa là quyền truy cập hết hạn.",
"shareButton": "Chia sẻ",
"shareWithCount": "Chia sẻ ({{count}})",
"currentAccess": "Quyền truy cập hiện tại",
"noAccessEntries": "Máy chủ này chưa được chia sẻ.",
"grantedBy": "Được cấp bởi",
"expires": "Hết hạn",
"expired": "Hết hạn",
"never": "Không bao giờ",
"revoke": "Thu hồi",
"accessUpdated": "Truy cập thông tin cập nhật",
"accessUpdateFailed": "Không thể cập nhật quyền truy cập",
"sharedBadge": "Chia sẻ",
"sharedBadgeTooltip": "Được chia sẻ bởi {{owner}} (quyền truy cập{{level}})",
"viewOnlyBanner": "Máy chủ này được chia sẻ với bạn bởi {{owner}} với quyền truy cập xem. Cấu hình chỉ có thể đọc.",
"sharedEditBanner": "Máy chủ này được chia sẻ với bạn bởi {{owner}} với quyền chỉnh sửa. Các thay đổi sẽ áp dụng cho máy chủ thực; chỉ chủ sở hữu mới có thể thay đổi các tham chiếu xác thực.",
"ownerOnlyControl": "Chỉ chủ sở hữu máy chủ mới có thể thay đổi điều này."
},
"guac": {
"connection": "Sự liên quan",
@@ -1302,28 +1367,10 @@
"backspaceKey": "Phím xóa lùi",
"saveHostFirst": "Hãy lưu máy chủ trước.",
"sharingOptionsAfterSave": "Các tùy chọn chia sẻ sẽ khả dụng sau khi máy chủ đã được lưu.",
"sharingLoadError": "Không thể tải dữ liệu chia sẻ. Vui lòng kiểm tra kết nối và thử lại.",
"shareHostSection": "Chia sẻ máy chủ",
"shareWithUser": "Chia sẻ với người dùng",
"shareWithRole": "Chia sẻ với vai trò",
"selectUser": "Chọn người dùng",
"selectRole": "Chọn vai trò",
"selectUserOption": "Chọn người dùng...",
"selectRoleOption": "Chọn một vai trò...",
"permissionLevel": "Cấp độ quyền hạn",
"expiresInHours": "Hết hạn sau (giờ)",
"noExpiryPlaceholder": "Để trống cho đến khi hết hạn sử dụng.",
"shareBtn": "Chia sẻ",
"currentAccess": "Truy cập hiện tại",
"typeHeader": "Kiểu",
"targetHeader": "Mục tiêu",
"permissionHeader": "Sự cho phép",
"grantedByHeader": "Được cấp bởi",
"expiresHeader": "Hết hạn",
"noAccessEntries": "Chưa có thông tin đăng nhập nào.",
"expiredLabel": "Hết hạn",
"neverLabel": "Không bao giờ",
"revokeBtn": "Thu hồi",
"cancelBtn": "Hủy bỏ",
"savingBtn": "Đang lưu...",
"updateHostBtn": "Cập nhật máy chủ",
@@ -2190,7 +2237,8 @@
"passwordLoginDisabledDesc": "Chức năng đăng nhập bằng mật khẩu đã bị vô hiệu hóa. Vui lòng sử dụng mã xác thực hoặc nhà cung cấp xác thực bên ngoài.",
"signInWithPasskey": "Đăng nhập bằng mật khẩu",
"passkeyLoginFailed": "Đăng nhập bằng mật khẩu thất bại",
"attemptsRemaining": "{{count}} lần thử còn lại"
"attemptsRemaining": "{{count}} lần thử còn lại",
"confirmResetDataWipe": "Tài khoản này chưa đăng nhập kể từ khi nâng cấp mã hóa, vì vậy dữ liệu đã lưu trữ không thể khôi phục nếu không có mật khẩu cũ. Việc đặt lại sẽ xóa vĩnh viễn các máy chủ, thông tin đăng nhập và đoạn mã của tài khoản này. Bạn có muốn tiếp tục không?"
},
"hostKey": {
"verifyNewHost": "Xác minh khóa máy chủ SSH",
@@ -2323,6 +2371,13 @@
"serviceLinks": "Liên kết dịch vụ",
"homepagePreview": "Xem trước trang chủ"
},
"donation": {
"title": "Bạn thấy Termix thế nào?",
"body": "Termix là phần mềm mã nguồn mở miễn phí, được xây dựng và duy trì bởi một nhóm hai người trong thời gian rảnh rỗi. Nếu nó thay thế công cụ thương mại mà bạn lẽ ra phải trả tiền, khoản quyên góp của bạn sẽ giúp trang trải chi phí lưu trữ và duy trì quá trình phát triển. Hiện tại, chúng tôi chỉ chấp nhận quyên góp bằng tiền điện tử.",
"milestones": "Các khoản quyên góp cũng giúp tài trợ cho thời gian nghiên cứu và học hỏi những gì cần thiết để xây dựng các tính năng như hỗ trợ SAML và Kubernetes. Xem tiến độ trên trang quyên góp.",
"cta": "Quyên tặng",
"dismiss": "Có lẽ sau này"
},
"dashboardTab": {
"stable": "ỔN ĐỊNH",
"hostsOnline": "Hosts Online",
@@ -2790,6 +2845,71 @@
"updateAdminStatusFailed": "Không thể cập nhật trạng thái quản trị viên.",
"allSessionsRevoked": "Tất cả các phiên đã bị hủy bỏ",
"revokeSessionsFailed": "Không thể hủy bỏ các phiên",
"manageUserData": "Quản lý dữ liệu người dùng",
"backToUsers": "Trở lại trang người dùng",
"manageTabAccount": "Tài khoản",
"manageTabHosts": "Người dẫn chương trình",
"manageTabCredentials": "Thông tin xác thực",
"manageTabSnippets": "Những đoạn trích",
"manageTabSessions": "Phiên họp",
"manageTabDanger": "Sự nguy hiểm",
"manageEditorBack": "Quay lại dữ liệu của {{username}}",
"dataLockedBadge": "ĐÃ KHÓA",
"dataLockedNotice": "Dữ liệu của {{username}}sẽ bị khóa cho đến khi họ đăng nhập lần tiếp theo. Máy chủ, thông tin đăng nhập và đoạn mã của họ không thể được xem hoặc chỉnh sửa cho đến lúc đó.",
"resetPasswordTitle": "Đặt lại mật khẩu",
"resetPasswordOidcOnly": "Người dùng này đăng nhập thông qua nhà cung cấp bên ngoài và không có mật khẩu.",
"resetPasswordPlaceholder": "Mật khẩu mới",
"resetPasswordBtn": "Cài lại",
"resetPasswordWorking": "Đang thiết lập lại...",
"resetPasswordSuccess": "Đặt lại mật khẩu",
"resetPasswordSuccessWiped": "Đã đặt lại mật khẩu. Dữ liệu được mã hóa của người dùng đã bị xóa.",
"resetPasswordFailed": "Không thể đặt lại mật khẩu.",
"resetPasswordConfirmWipe": "{{username}} chưa đăng nhập kể từ khi nâng cấp mã hóa, vì vậy dữ liệu của họ không thể khôi phục. Việc đặt lại ngay bây giờ sẽ xóa máy chủ, thông tin đăng nhập và đoạn mã của họ. Tiếp tục?",
"totpSectionTitle": "Xác thực hai yếu tố",
"totpStatusEnabled": "TOTP đã được bật cho người dùng này.",
"totpStatusDisabled": "TOTP chưa được bật cho người dùng này.",
"disableTotp": "Vô hiệu hóa",
"disableTotpConfirm": "Vô hiệu hóa xác thực hai yếu tố cho {{username}}? Họ sẽ có thể đăng nhập chỉ bằng mật khẩu của mình.",
"totpDisabledSuccess": "TOTP bị vô hiệu hóa",
"totpDisableFailed": "Không thể vô hiệu hóa TOTP.",
"manageApiKeys": "Khóa API",
"apiKeyNamePlaceholder": "Tên khóa",
"apiKeyCopyNotice": "Hãy sao chép khóa này ngay bây giờ, nó sẽ không hiển thị lại nữa.",
"noApiKeysForUser": "Không có khóa API",
"apiKeyDeleteFailed": "Không thể xóa khóa API.",
"exportUserData": "Xuất dữ liệu",
"exportUserDataDesc": "Tải xuống dữ liệu về máy chủ, thông tin đăng nhập và trình quản lý tập tin của người dùng này dưới dạng JSON. Thông tin bí mật đã được giải mã.",
"exportUserDataSuccess": "Dữ liệu người dùng đã xuất",
"exportUserDataFailed": "Không thể xuất dữ liệu người dùng.",
"hostsCount": "{{count}} máy chủ",
"addHostForUser": "Thêm máy chủ",
"noHostsForUser": "Người dùng này không có máy chủ",
"connectToHost": "Kết nối",
"deleteHostConfirm": "Xóa máy chủ \"{{name}}\" thuộc về {{username}}?",
"hostDeletedSuccess": "Máy chủ đã bị xóa",
"hostDeleteFailed": "Không thể xóa máy chủ",
"credentialsCount": "{{count}} thông tin xác thực",
"addCredentialForUser": "Thêm thông tin đăng nhập",
"noCredentialsForUser": "Người dùng này không có thông tin đăng nhập.",
"deleteCredentialConfirm": "Xóa thông tin xác thực \"{{name}}\" thuộc về {{username}}?",
"credentialDeletedSuccess": "Thông tin đăng nhập đã bị xóa",
"credentialDeleteFailed": "Không thể xóa thông tin đăng nhập",
"snippetsCount": "{{count}} đoạn trích",
"addSnippetForUser": "Thêm đoạn mã",
"noSnippetsForUser": "Người dùng này không có đoạn trích nào.",
"snippetRequiredFields": "Tên và nội dung đoạn mã là bắt buộc",
"snippetNamePlaceholder": "Tên đoạn trích",
"snippetContentPlaceholder": "Nội dung lệnh",
"snippetFolderPlaceholder": "Thư mục (tùy chọn)",
"snippetSaved": "Đoạn mã đã được lưu",
"snippetSaveFailed": "Không thể lưu đoạn mã",
"deleteSnippetConfirm": "Xóa đoạn mã \"{{name}}\" thuộc về {{username}}?",
"snippetDeletedSuccess": "Đoạn văn đã bị xóa",
"snippetDeleteFailed": "Không thể xóa đoạn mã",
"noSessionsForUser": "Không có phiên hoạt động nào",
"deleteUserDangerDesc": "Xóa vĩnh viễn {{username}} và tất cả dữ liệu của chúng (máy chủ, thông tin đăng nhập, đoạn trích, lịch sử). Thao tác này không thể hoàn tác.",
"deleteUserConfirm": "Xóa vĩnh viễn {{username}} và tất cả dữ liệu của chúng?",
"deleteUserAdminBlocked": "Hãy gỡ bỏ quyền quản trị trước khi xóa người dùng này.",
"createRoleRequired": "Tên và tên hiển thị là bắt buộc",
"createRoleSuccess": "Vai trò \"{{name}}\" đã được tạo",
"createRoleFailed": "Không thể tạo vai trò",
@@ -2822,7 +2942,16 @@
"hostDefaultsCommandHistory": "Lịch sử lệnh",
"hostDefaultsCommandHistoryDesc": "Theo dõi lịch sử lệnh trên các máy chủ mới theo mặc định",
"hostDefaultsSaved": "Cài đặt mặc định của máy chủ đã được lưu",
"hostDefaultsSaveFailed": "Không thể lưu cài đặt mặc định của máy chủ."
"hostDefaultsSaveFailed": "Không thể lưu cài đặt mặc định của máy chủ.",
"rolePermissions": {
"count": "{{count}} quyền hạn",
"editAction": "Chỉnh sửa quyền",
"loadError": "Không thể tải danh mục quyền.",
"saved": "Quyền hạn vai trò đã được lưu",
"saveError": "Không thể lưu quyền quản trị",
"save": "Cứu",
"saving": "Đang lưu..."
}
},
"newUi": {
"sidebar": {
@@ -2991,6 +3120,10 @@
"executionFailed": "Thất bại"
},
"userProfile": {
"donateTitle": "Hãy giữ cho Termix luôn sống sót",
"donateDescription": "Termix là phần mềm miễn phí, tự lưu trữ và được xây dựng bởi một vài người trong thời gian rảnh rỗi. Nếu nó giúp bạn tiết kiệm thời gian hoặc tiền bạc, một khoản quyên góp bằng tiền điện tử sẽ giúp duy trì hoạt động của nó.",
"donateMilestones": "Các khoản quyên góp giúp tài trợ cho thời gian nghiên cứu và học hỏi những gì cần thiết để xây dựng hỗ trợ SAML, Kubernetes và Agent. Hãy theo dõi tiến độ và quyên góp.",
"donateButton": "Quyên góp tiền điện tử",
"storageModeLocal": "Trình duyệt",
"storageModeCloud": "Cơ sở dữ liệu",
"storageModeDescription": "Trình duyệt chỉ lưu trữ cài đặt trong trình duyệt này. Cơ sở dữ liệu được đồng bộ hóa với máy chủ và tải trên mọi thiết bị.",
@@ -3011,6 +3144,10 @@
"twoFaOn": "TRÊN",
"twoFaOff": "Tắt",
"versionLabel": "Phiên bản",
"betaProgramTitle": "Chương trình Beta",
"betaProgramDescription": "Hãy thử nghiệm các tính năng sắp ra mắt sớm với thẻ Docker :beta được cập nhật hàng tuần. Phiên bản này chưa ổn định, không dành cho môi trường sản xuất.",
"betaProgramFeedback": "Phát hiện lỗi? Báo cáo tại đây.",
"betaProgramLearnMore": "Tìm hiểu thêm",
"deleteAccount": "Xóa tài khoản",
"deleteAccountDescription": "Xóa vĩnh viễn tài khoản của bạn",
"changeServerDescription": "Chuyển sang máy chủ phụ trợ Termix khác",
+193 -56
View File
@@ -46,7 +46,7 @@
"fromVault": "来自凭证库",
"linkedToTermixId": "通过 Termix ID 发布",
"selectCredential": "选择凭证...",
"import": "进口",
"import": "导入",
"caTitle": "证书颁发机构",
"caIntro": "在服务器上信任此 CA,它将接受您签署的任何证书。轮换证书可以一次性撤销所有证书;证书也会自动过期。",
"caEnable": "启用 CA",
@@ -72,7 +72,7 @@
"folders": "文件夹",
"folder": "文件夹",
"password": "密码",
"key": "钥",
"key": "钥",
"sshPrivateKey": "SSH 私钥",
"upload": "上传",
"keyPassword": "密钥密码",
@@ -126,7 +126,7 @@
"openFullView": "打开完整视图",
"previewTitle": "首页预览",
"cancel": "取消",
"save": "节省",
"save": "保存",
"title_label": "标题",
"widgetTitlePlaceholder": "小部件标题(可选)",
"url": "URL",
@@ -143,13 +143,13 @@
"format24h": "24小时",
"content": "内容",
"backgroundColor": "背景颜色",
"host": "主持人",
"host": "主",
"showMetrics": "显示指标",
"links": "链接",
"addLink": "添加链接",
"linkLabel": "标签",
"linkUrl": "URL",
"removeLink": "除",
"removeLink": "除",
"categoryLinks": "链接",
"categoryInfo": "信息",
"categorySystem": "系统",
@@ -215,14 +215,14 @@
"overviewDatabase": "数据库",
"overviewUptime": "正常运行时间",
"noHosts": "未配置任何主机",
"hostGridHosts": "主持人",
"hostGridHosts": "主",
"hostGridAllHint": "留空则显示所有主机。",
"columns": "列",
"showIp": "显示 IP 地址",
"connectionType": "连接类型",
"layout": "布局",
"showStatus": "显示状态",
"showHostName": "节目主持人姓名",
"showHostName": "显示主机名称",
"noDockerActivity": "无 Docker 活动",
"noActivity": "无活动",
"showAcknowledged": "显示已确认",
@@ -269,25 +269,25 @@
"widgetSearchLinksDesc": "带有内联输入的快速搜索快捷按钮",
"widgetLinkTreeName": "链接树",
"widgetLinkTreeDesc": "带有标题的链接分组",
"calMon": "",
"calTue": "",
"calWed": "我们",
"calThu": "",
"calFri": "弗朗西斯",
"calSat": "",
"calSun": "",
"calMon": "星期一",
"calTue": "星期二",
"calWed": "星期三",
"calThu": "星期四",
"calFri": "星期五",
"calSat": "星期六",
"calSun": "星期日",
"startOnMonday": "周一开始新的一周",
"countdownNoDate": "未设定目标日期",
"countdownPast": "事件已结束",
"countdownDays": "天",
"countdownHours": "小时",
"countdownMinutes": "最小",
"countdownMinutes": "",
"countdownSeconds": "秒",
"countdownLabel": "标签",
"countdownLabelPlaceholder": "例如:发布日",
"countdownShowDays": "演出日",
"countdownShowHours": "演出时间",
"targetDate": "目标日期",
"countdownShowDays": "显示天",
"countdownShowHours": "显示小时",
"targetDate": "显示日期",
"searchEngine": "搜索引擎",
"customSearchUrl": "自定义搜索网址",
"searchPlaceholder": "搜索...",
@@ -312,7 +312,7 @@
"displayField": "显示字段",
"jsonPath": "JSON 路径",
"customApiLabel": "标签",
"customApiLabelPlaceholder": "例如温度",
"customApiLabelPlaceholder": "例如温度",
"customApiUnit": "单元",
"customApiNoUrl": "未配置 API URL",
"customApiError": "获取失败",
@@ -327,7 +327,7 @@
"compactMode": "紧凑模式",
"showDetailed": "显示详细信息",
"selectHosts": "选择主机",
"allHosts": "所有主持人",
"allHosts": "所有主",
"listLayout": "列表",
"gridLayout": "网格",
"terminal": "终端",
@@ -337,8 +337,8 @@
"range1h": "1小时",
"range6h": "6小时",
"range24h": "24小时",
"metricNetRx": "下载",
"metricNetTx": "上传",
"metricNetRx": "网络下载",
"metricNetTx": "网络上传",
"severityFilter": "严重性过滤器",
"filterAll": "全部",
"accentColor": "强调色",
@@ -362,7 +362,7 @@
"widgetFileManagerDesc": "为已配置主机提供的嵌入式 SFTP 文件管理器",
"widgetDockerName": "Docker 管理器",
"widgetDockerDesc": "为已配置主机提供的嵌入式 Docker 容器管理器",
"widgetTunnelName": "隧道经理",
"widgetTunnelName": "隧道管理器",
"widgetTunnelDesc": "为已配置主机提供的嵌入式 SSH 隧道管理器",
"widgetNoHostSelected": "未配置主机"
},
@@ -387,7 +387,7 @@
"localServer": "本地服务器",
"savedServers": "已保存的服务器",
"noSavedServers": "没有已保存的服务器",
"removeServer": "除"
"removeServer": "除"
},
"versionCheck": {
"error": "版本检查错误",
@@ -408,7 +408,7 @@
"common": {
"close": "关闭",
"minimize": "最小化",
"online": "在线",
"online": "在线",
"offline": "离线",
"unknown": "未知",
"continue": "继续",
@@ -424,7 +424,7 @@
"copied": "已复制",
"connecting": "正在连接...",
"updateAvailable": "更新可用",
"appName": "特米克斯",
"appName": "Termix",
"openInNewTab": "在新标签页中打开",
"noReleases": "无发布",
"updatesAndReleases": "更新与发布",
@@ -436,21 +436,21 @@
"username": "用户名",
"login": "登录",
"logout": "注销",
"register": "登记",
"register": "注册",
"password": "密码",
"confirmPassword": "确认密码",
"back": "后退",
"save": "节省",
"back": "返回",
"save": "保存",
"saving": "保存...",
"delete": "删除",
"rename": "重命名",
"edit": "编辑",
"add": "添加",
"confirm": "确认",
"no": "",
"or": "或",
"next": "下一",
"previous": "以前的",
"no": "",
"or": "或",
"next": "下一",
"previous": "上一步",
"refresh": "刷新",
"language": "语言",
"checking": "检查...",
@@ -474,7 +474,7 @@
"required": "必需的"
},
"nav": {
"home": "",
"home": "主页",
"terminal": "终端",
"docker": "Docker",
"tunnels": "隧道",
@@ -864,6 +864,21 @@
"proxmoxDockerDetectionDesc": "逗号分隔的名称模式使 Docker 能够匹配虚拟机",
"proxmoxPreferredRanges": "首选 IP 地址范围",
"proxmoxPreferredRangesDesc": "当访客拥有多个接口时,用于选择 IP 地址的优先级前缀以逗号分隔",
"proxmoxAutoSync": "自动同步访客",
"proxmoxAutoSyncDesc": "定期发现此 Proxmox 节点,并在会话解锁期间创建或更新导入的客户主机。",
"proxmoxSyncInterval": "同步间隔(分钟)",
"proxmoxSyncIntervalDesc": "最短 5 分钟。调度程序会跳过已锁定的用户会话。",
"proxmoxMarkMissing": "马克缺席的客人",
"proxmoxMarkMissingDesc": "如果之前导入的虚拟机消失而不是被删除,则添加 proxmox-missing 标签。",
"proxmoxLastSync": "上次同步",
"proxmoxLastSyncNever": "尚未同步",
"proxmoxLastSyncNoResult": "无结果详情",
"proxmoxLastSyncSummary": "{{created}} 已创建, {{updated}} 已更新, {{markedMissing}} 缺失, {{skipped}} 已跳过",
"proxmoxLastSyncStatus": {
"success": "成功",
"error": "失败的",
"pending": "待办的"
},
"proxmoxDiscoverAction": "发现并导入 Proxmox 访客",
"proxmoxImportTitle": "从 Proxmox 导入",
"proxmoxSelectHost": "选择 Proxmox 主机…",
@@ -1167,7 +1182,57 @@
"shareHost": "共享主机",
"shareHostTitle": "分享: {{name}}",
"sharing": {
"requiresCredential": "此主机必须使用凭据才能启用共享。请先编辑主机并分配凭据。"
"loadError": "分享数据加载失败,请重试。",
"shareWithSection": "分享",
"usersTab": "用户",
"rolesTab": "角色",
"searchPlaceholder": "搜索用户或角色...",
"noMatches": "未找到匹配项",
"permissionLevelLabel": "权限级别",
"levels": {
"connect": {
"label": "连接",
"description": "仅允许打开会话:终端、远程桌面、文件管理器、隧道和 Docker。无权访问主机配置。"
},
"view": {
"label": "看法",
"description": "连接后,即可查看主机配置。密钥永远不会显示。"
},
"edit": {
"label": "编辑",
"description": "查看并修改主机。密钥可以替换但永远无法读取;凭证分配仅限所有者操作。"
},
"manage": {
"label": "管理",
"description": "编辑、与他人共享主机、更改权限级别和撤销访问权限。"
}
},
"expiryLabel": "访问权限到期",
"expiry": {
"never": "绝不",
"oneHour": "1小时",
"oneDay": "24小时",
"sevenDays": "7天",
"thirtyDays": "30天",
"custom": "风俗"
},
"customHoursPlaceholder": "访问权限到期时间(剩余时间)",
"shareButton": "分享",
"shareWithCount": "分享({{count}}",
"currentAccess": "当前访问权限",
"noAccessEntries": "此主机尚未共享",
"grantedBy": "由……授予",
"expires": "过期",
"expired": "已到期",
"never": "绝不",
"revoke": "撤销",
"accessUpdated": "访问已更新",
"accessUpdateFailed": "更新访问权限失败",
"sharedBadge": "共享",
"sharedBadgeTooltip": "由 {{owner}} 共享({{level}} 访问权限)",
"viewOnlyBanner": "此主机由 {{owner}} 与您共享,您拥有查看权限。配置信息为只读。",
"sharedEditBanner": "此主机由 {{owner}} 与您共享,并授予您编辑权限。更改将应用于实际主机;身份验证引用只能由所有者更改。",
"ownerOnlyControl": "只有房东才能更改此设置"
},
"guac": {
"connection": "联系",
@@ -1302,28 +1367,10 @@
"backspaceKey": "退格键",
"saveHostFirst": "先保存主机。",
"sharingOptionsAfterSave": "保存主机后即可使用分享选项。",
"sharingLoadError": "共享数据加载失败。请检查您的网络连接并重试。",
"shareHostSection": "共享主机",
"shareWithUser": "与用户分享",
"shareWithRole": "分享角色",
"selectUser": "选择用户",
"selectRole": "选择角色",
"selectUserOption": "选择用户...",
"selectRoleOption": "选择一个角色……",
"permissionLevel": "权限级别",
"expiresInHours": "(小时)后过期",
"noExpiryPlaceholder": "无需清洗,因为没有有效期。",
"shareBtn": "分享",
"currentAccess": "当前访问权限",
"typeHeader": "类型",
"targetHeader": "目标",
"permissionHeader": "允许",
"grantedByHeader": "授予",
"expiresHeader": "过期",
"noAccessEntries": "暂无访问记录。",
"expiredLabel": "已到期",
"neverLabel": "绝不",
"revokeBtn": "撤销",
"cancelBtn": "取消",
"savingBtn": "保存...",
"updateHostBtn": "更新主机",
@@ -1803,7 +1850,7 @@
"parallelSegmentsLabel": "平行换乘车道",
"parallelSegmentsOption": "{{count}} 车道",
"parallelSegmentsHint": "大文件会被分割成 256 MB 的数据块。多通道使用独立的连接(类似于同时启动多个传输)以提高总吞吐量。",
"progressTotalSpeed": "{{speed}} total ({{lanes}} lanes)",
"progressTotalSpeed": "{{speed}} 总计 ({{lanes}} 条车道)",
"progressTransferringItems": "正在传输文件({{current}} 中的 {{total}})…",
"progressBytes": "{{transferred}} / {{total}}",
"progressItems": "{{current}} / {{total}} 文件",
@@ -2190,7 +2237,8 @@
"passwordLoginDisabledDesc": "密码登录已禁用。请使用密码密钥或外部身份验证提供商。",
"signInWithPasskey": "使用密码登录",
"passkeyLoginFailed": "密码登录失败",
"attemptsRemaining": "{{count}} 剩余尝试次数"
"attemptsRemaining": "{{count}} 剩余尝试次数",
"confirmResetDataWipe": "此帐户自加密升级后未登录,因此没有旧密码将无法恢复其存储的数据。重置将永久删除其主机名、凭据和代码片段。是否继续?"
},
"hostKey": {
"verifyNewHost": "验证 SSH 主机密钥",
@@ -2323,6 +2371,13 @@
"serviceLinks": "服务链接",
"homepagePreview": "首页预览"
},
"donation": {
"title": "喜欢Termix吗?",
"body": "Termix 是一个免费开源的软件,由我们两人团队利用业余时间开发和维护。如果您用它替代了原本需要付费的商业工具,您的捐赠将有助于支付托管费用并维持软件的持续开发。目前,我们仅接受加密货币捐赠。",
"milestones": "捐款还能帮助我们投入时间进行研究和学习,以开发诸如 SAML 和 Kubernetes 支持等功能。请在捐款页面查看项目进展。",
"cta": "捐",
"dismiss": "也许以后"
},
"dashboardTab": {
"stable": "稳定的",
"hostsOnline": "在线主持人",
@@ -2790,6 +2845,71 @@
"updateAdminStatusFailed": "更新管理员状态失败",
"allSessionsRevoked": "所有会话均被撤销",
"revokeSessionsFailed": "撤销会话失败",
"manageUserData": "管理用户数据",
"backToUsers": "返回用户",
"manageTabAccount": "帐户",
"manageTabHosts": "主持人",
"manageTabCredentials": "证书",
"manageTabSnippets": "片段",
"manageTabSessions": "会议",
"manageTabDanger": "危险",
"manageEditorBack": "返回 {{username}}的数据",
"dataLockedBadge": "已锁定",
"dataLockedNotice": "用户 {{username}}的数据将被锁定,直到他们下次登录。在此之前,他们的主机名、凭据和代码片段将无法查看或编辑。",
"resetPasswordTitle": "重置密码",
"resetPasswordOidcOnly": "该用户通过外部服务提供商登录,没有密码。",
"resetPasswordPlaceholder": "新密码",
"resetPasswordBtn": "重置",
"resetPasswordWorking": "正在重置……",
"resetPasswordSuccess": "密码重置",
"resetPasswordSuccessWiped": "密码已重置。用户的加密数据已被清除。",
"resetPasswordFailed": "重置密码失败",
"resetPasswordConfirmWipe": "自加密升级以来,{{username}} 一直未登录,因此其数据无法恢复。立即重置将删除其主机、凭据和代码片段。是否继续?",
"totpSectionTitle": "双因素身份验证",
"totpStatusEnabled": "此用户已启用 TOTP",
"totpStatusDisabled": "此用户未启用 TOTP。",
"disableTotp": "禁用",
"disableTotpConfirm": "是否禁用 {{username}}的双因素身份验证?他们将能够仅使用密码登录。",
"totpDisabledSuccess": "TOTP 已禁用",
"totpDisableFailed": "禁用 TOTP 失败",
"manageApiKeys": "API密钥",
"apiKeyNamePlaceholder": "键名",
"apiKeyCopyNotice": "立即复制此密钥,它不会再次显示。",
"noApiKeysForUser": "没有 API 密钥",
"apiKeyDeleteFailed": "删除 API 密钥失败",
"exportUserData": "数据导出",
"exportUserDataDesc": "以 JSON 格式下载此用户的 hosts 文件、凭据和文件管理器数据。密钥已解密。",
"exportUserDataSuccess": "用户数据导出",
"exportUserDataFailed": "用户数据导出失败",
"hostsCount": "{{count}} 主机",
"addHostForUser": "添加主机",
"noHostsForUser": "该用户没有主机。",
"connectToHost": "连接",
"deleteHostConfirm": "删除属于 {{username}} 的主机“{{name}}”?",
"hostDeletedSuccess": "主机已删除",
"hostDeleteFailed": "删除主机失败",
"credentialsCount": "{{count}} 凭证",
"addCredentialForUser": "添加凭据",
"noCredentialsForUser": "此用户没有凭据",
"deleteCredentialConfirm": "删除属于 {{username}} 的凭据“{{name}}”?",
"credentialDeletedSuccess": "凭证已删除",
"credentialDeleteFailed": "删除凭据失败",
"snippetsCount": "{{count}} 代码片段",
"addSnippetForUser": "添加代码片段",
"noSnippetsForUser": "该用户没有任何代码片段。",
"snippetRequiredFields": "代码片段名称和内容均为必填项。",
"snippetNamePlaceholder": "代码片段名称",
"snippetContentPlaceholder": "命令内容",
"snippetFolderPlaceholder": "文件夹(可选)",
"snippetSaved": "代码片段已保存",
"snippetSaveFailed": "保存代码片段失败",
"deleteSnippetConfirm": "删除属于 {{username}} 的片段“{{name}}”?",
"snippetDeletedSuccess": "片段已删除",
"snippetDeleteFailed": "删除代码片段失败",
"noSessionsForUser": "没有活动会话",
"deleteUserDangerDesc": "永久删除 {{username}} 及其所有数据(主机、凭据、代码片段、历史记录)。此操作无法撤销。",
"deleteUserConfirm": "永久删除 {{username}} 及其所有数据?",
"deleteUserAdminBlocked": "删除此用户前,请先移除其管理员权限。",
"createRoleRequired": "姓名和显示名称是必填项。",
"createRoleSuccess": "角色“{{name}}”已创建",
"createRoleFailed": "创建角色失败",
@@ -2822,7 +2942,16 @@
"hostDefaultsCommandHistory": "命令历史记录",
"hostDefaultsCommandHistoryDesc": "默认情况下跟踪新主机上的命令历史记录",
"hostDefaultsSaved": "主机默认设置已保存",
"hostDefaultsSaveFailed": "保存主机默认设置失败"
"hostDefaultsSaveFailed": "保存主机默认设置失败",
"rolePermissions": {
"count": "{{count}} 权限",
"editAction": "编辑权限",
"loadError": "加载权限目录失败",
"saved": "角色权限已保存",
"saveError": "保存角色权限失败",
"save": "节省",
"saving": "保存..."
}
},
"newUi": {
"sidebar": {
@@ -2991,6 +3120,10 @@
"executionFailed": "失败的"
},
"userProfile": {
"donateTitle": "让Termix活下去",
"donateDescription": "Termix 是免费的,由几位业余人士自行托管和开发。如果它为您节省了时间或金钱,欢迎您捐赠加密货币以支持其持续运营。",
"donateMilestones": "捐款将用于资助我们研究和学习构建 SAML、Kubernetes 和 Agent 支持所需的知识。查看进展并捐款。",
"donateButton": "捐赠加密货币",
"storageModeLocal": "浏览器",
"storageModeCloud": "数据库",
"storageModeDescription": "浏览器仅将设置存储在此浏览器中。数据库会同步到服务器,并可在任何设备上加载。",
@@ -3011,6 +3144,10 @@
"twoFaOn": "在",
"twoFaOff": "离开",
"versionLabel": "版本",
"betaProgramTitle": "测试计划",
"betaProgramDescription": "使用每周滚动更新的 :beta Docker 标签,即可提前体验即将推出的功能。此版本不稳定,不适用于生产环境。",
"betaProgramFeedback": "发现漏洞?请在此处报告。",
"betaProgramLearnMore": "了解更多",
"deleteAccount": "删除帐户",
"deleteAccountDescription": "永久删除您的帐户",
"changeServerDescription": "切换到其他 Termix 后端服务器",
+158 -21
View File
@@ -864,6 +864,21 @@
"proxmoxDockerDetectionDesc": "逗號分隔的名稱模式使 Docker 能夠匹配虛擬機",
"proxmoxPreferredRanges": "首選 IP 位址範圍",
"proxmoxPreferredRangesDesc": "當訪客擁有多個介面時,用於選擇 IP 位址的優先順序以逗號分隔",
"proxmoxAutoSync": "自動同步訪客",
"proxmoxAutoSyncDesc": "定期發現此 Proxmox 節點,並在會話解鎖期間建立或更新匯入的客戶主機。",
"proxmoxSyncInterval": "同步間隔(分鐘)",
"proxmoxSyncIntervalDesc": "最短 5 分鐘。調度程式會跳過已鎖定的使用者會話。",
"proxmoxMarkMissing": "馬克缺席的客人",
"proxmoxMarkMissingDesc": "如果先前匯入的虛擬機器消失而不是被刪除,則新增 proxmox-missing 標籤。",
"proxmoxLastSync": "上次同步",
"proxmoxLastSyncNever": "尚未同步",
"proxmoxLastSyncNoResult": "無結果詳情",
"proxmoxLastSyncSummary": "{{created}} 已創建, {{updated}} 已更新, {{markedMissing}} 缺失, {{skipped}} 已跳過",
"proxmoxLastSyncStatus": {
"success": "成功",
"error": "失敗的",
"pending": "待辦的"
},
"proxmoxDiscoverAction": "發現並導入 Proxmox 訪客",
"proxmoxImportTitle": "從 Proxmox 導入",
"proxmoxSelectHost": "選擇 Proxmox 主機…",
@@ -1167,7 +1182,57 @@
"shareHost": "共享主機",
"shareHostTitle": "分享: {{name}}",
"sharing": {
"requiresCredential": "此主機必須使用憑證才能啟用共用。請先編輯主機並指派憑證。"
"loadError": "分享資料載入失敗,請重試。",
"shareWithSection": "分享",
"usersTab": "使用者",
"rolesTab": "角色",
"searchPlaceholder": "搜尋使用者或角色...",
"noMatches": "未找到匹配項",
"permissionLevelLabel": "權限等級",
"levels": {
"connect": {
"label": "連接",
"description": "僅允許開啟會話:終端機、遠端桌面、檔案管理器、隧道和 Docker。無權存取主機配置。"
},
"view": {
"label": "看法",
"description": "連線後,即可查看主機配置。密鑰永遠不會顯示。"
},
"edit": {
"label": "編輯",
"description": "查看並修改主機。金鑰可以替換但永遠無法讀取;憑證分配僅限擁有者操作。"
},
"manage": {
"label": "管理",
"description": "編輯、與他人共享主機、更改權限等級和撤銷存取權限。"
}
},
"expiryLabel": "存取權限到期",
"expiry": {
"never": "絕不",
"oneHour": "1小時",
"oneDay": "24小時",
"sevenDays": "7天",
"thirtyDays": "30天",
"custom": "風俗"
},
"customHoursPlaceholder": "存取權限到期時間(剩餘時間)",
"shareButton": "分享",
"shareWithCount": "分享({{count}}",
"currentAccess": "目前存取權限",
"noAccessEntries": "此主機尚未共用",
"grantedBy": "由……授予",
"expires": "過期",
"expired": "已到期",
"never": "絕不",
"revoke": "撤銷",
"accessUpdated": "訪問已更新",
"accessUpdateFailed": "更新存取權限失敗",
"sharedBadge": "共享",
"sharedBadgeTooltip": "由 {{owner}} 共用({{level}} 存取權限)",
"viewOnlyBanner": "此主機由 {{owner}} 與您分享,您擁有檢視權限。配置資訊為唯讀。",
"sharedEditBanner": "此主機由 {{owner}} 與您分享,並授予您編輯權限。更改將應用於實際主機;身份驗證引用只能由所有者更改。",
"ownerOnlyControl": "只有房東才能更改此設置"
},
"guac": {
"connection": "聯繫",
@@ -1302,28 +1367,10 @@
"backspaceKey": "退格鍵",
"saveHostFirst": "先保存主機。",
"sharingOptionsAfterSave": "儲存主機後即可使用分享選項。",
"sharingLoadError": "共享資料加載失敗。請檢查您的網路連線並重試。",
"shareHostSection": "共享主機",
"shareWithUser": "與用戶分享",
"shareWithRole": "分享角色",
"selectUser": "選擇用戶",
"selectRole": "選擇角色",
"selectUserOption": "選擇用戶...",
"selectRoleOption": "選擇一個角色…",
"permissionLevel": "權限等級",
"expiresInHours": "剩餘時間(小時)",
"noExpiryPlaceholder": "無需清洗,因為沒有有效期限。",
"shareBtn": "分享",
"currentAccess": "目前存取權限",
"typeHeader": "類型",
"targetHeader": "目標",
"permissionHeader": "允許",
"grantedByHeader": "授予",
"expiresHeader": "過期",
"noAccessEntries": "暫無訪問記錄。",
"expiredLabel": "已到期",
"neverLabel": "絕不",
"revokeBtn": "撤銷",
"cancelBtn": "取消",
"savingBtn": "儲存...",
"updateHostBtn": "更新主機",
@@ -2190,7 +2237,8 @@
"passwordLoginDisabledDesc": "密碼登入已停用。請使用密碼金鑰或外部身份驗證提供者。",
"signInWithPasskey": "使用密碼登入",
"passkeyLoginFailed": "密碼登入失敗",
"attemptsRemaining": "{{count}} 剩餘嘗試次數"
"attemptsRemaining": "{{count}} 剩餘嘗試次數",
"confirmResetDataWipe": "此帳戶自加密升級後未登錄,因此沒有舊密碼將無法恢復其儲存的資料。重置將永久刪除其主機名稱、憑證和程式碼片段。是否繼續?"
},
"hostKey": {
"verifyNewHost": "驗證 SSH 主機金鑰",
@@ -2323,6 +2371,13 @@
"serviceLinks": "服務連結",
"homepagePreview": "首頁預覽"
},
"donation": {
"title": "喜歡Termix嗎?",
"body": "Termix 是一個免費開源的軟體,由我們兩人團隊利用業餘時間開發和維護。如果您用它取代了原本需要付費的商業工具,您的捐贈將有助於支付託管費用並維持軟體的持續開發。目前,我們僅接受加密貨幣捐贈。",
"milestones": "捐款也能幫助我們投入時間進行研究和學習,以發展諸如 SAML 和 Kubernetes 支援等功能。請在捐款頁面查看專案進度。",
"cta": "捐",
"dismiss": "也許以後"
},
"dashboardTab": {
"stable": "穩定的",
"hostsOnline": "線上主持人",
@@ -2790,6 +2845,71 @@
"updateAdminStatusFailed": "更新管理員狀態失敗",
"allSessionsRevoked": "所有會話均被撤銷",
"revokeSessionsFailed": "撤銷會話失敗",
"manageUserData": "管理用戶數據",
"backToUsers": "返回用戶",
"manageTabAccount": "帳戶",
"manageTabHosts": "主持人",
"manageTabCredentials": "證書",
"manageTabSnippets": "片段",
"manageTabSessions": "會議",
"manageTabDanger": "危險",
"manageEditorBack": "傳回 {{username}}的數據",
"dataLockedBadge": "已鎖定",
"dataLockedNotice": "使用者 {{username}}的資料將被鎖定,直到他們下次登入。在此之前,他們的主機名稱、憑證和程式碼片段將無法查看或編輯。",
"resetPasswordTitle": "重設密碼",
"resetPasswordOidcOnly": "該用戶透過外部服務提供者登錄,沒有密碼。",
"resetPasswordPlaceholder": "新密碼",
"resetPasswordBtn": "重置",
"resetPasswordWorking": "正在重置…",
"resetPasswordSuccess": "密碼重設",
"resetPasswordSuccessWiped": "密碼已重設。用戶的加密資料已清除。",
"resetPasswordFailed": "重置密碼失敗",
"resetPasswordConfirmWipe": "自加密升級以來,{{username}} 一直未登錄,因此其資料無法恢復。立即重置將刪除其主機、憑證和程式碼片段。是否繼續?",
"totpSectionTitle": "雙重身份驗證",
"totpStatusEnabled": "此使用者已啟用 TOTP",
"totpStatusDisabled": "此使用者未啟用 TOTP。",
"disableTotp": "停用",
"disableTotpConfirm": "是否停用 {{username}}的雙重認證?他們將能夠僅使用密碼登入。",
"totpDisabledSuccess": "TOTP 已停用",
"totpDisableFailed": "停用 TOTP 失敗",
"manageApiKeys": "API金鑰",
"apiKeyNamePlaceholder": "鍵名",
"apiKeyCopyNotice": "立即複製此密鑰,它不會再次顯示。",
"noApiKeysForUser": "沒有 API 金鑰",
"apiKeyDeleteFailed": "刪除 API 金鑰失敗",
"exportUserData": "數據導出",
"exportUserDataDesc": "以 JSON 格式下載此使用者的 hosts 檔案、憑證和檔案管理器資料。密鑰已解密。",
"exportUserDataSuccess": "用戶資料匯出",
"exportUserDataFailed": "用戶資料匯出失敗",
"hostsCount": "{{count}} 主機",
"addHostForUser": "新增主機",
"noHostsForUser": "該用戶沒有主機。",
"connectToHost": "連接",
"deleteHostConfirm": "刪除屬於 {{username}} 的主機「{{name}}」?",
"hostDeletedSuccess": "主機已刪除",
"hostDeleteFailed": "刪除主機失敗",
"credentialsCount": "{{count}} 憑證",
"addCredentialForUser": "新增憑證",
"noCredentialsForUser": "此用戶沒有憑證",
"deleteCredentialConfirm": "刪除屬於 {{username}} 的憑證「{{name}}」?",
"credentialDeletedSuccess": "憑證已刪除",
"credentialDeleteFailed": "刪除憑證失敗",
"snippetsCount": "{{count}} 程式碼片段",
"addSnippetForUser": "新增程式碼片段",
"noSnippetsForUser": "該用戶沒有任何程式碼片段。",
"snippetRequiredFields": "程式碼片段名稱和內容均為必填項。",
"snippetNamePlaceholder": "程式碼片段名稱",
"snippetContentPlaceholder": "命令內容",
"snippetFolderPlaceholder": "資料夾(可選)",
"snippetSaved": "程式碼片段已儲存",
"snippetSaveFailed": "儲存程式碼片段失敗",
"deleteSnippetConfirm": "刪除屬於 {{username}} 的片段「{{name}}」?",
"snippetDeletedSuccess": "片段已刪除",
"snippetDeleteFailed": "刪除程式碼片段失敗",
"noSessionsForUser": "沒有活動會話",
"deleteUserDangerDesc": "永久刪除 {{username}} 及其所有資料(主機、憑證、程式碼片段、歷史記錄)。此操作無法撤銷。",
"deleteUserConfirm": "永久刪除 {{username}} 及其所有資料?",
"deleteUserAdminBlocked": "刪除此使用者前,請先移除其管理員權限。",
"createRoleRequired": "姓名和顯示名稱是必填項。",
"createRoleSuccess": "角色“{{name}}”已創建",
"createRoleFailed": "創建角色失敗",
@@ -2822,7 +2942,16 @@
"hostDefaultsCommandHistory": "命令歷史記錄",
"hostDefaultsCommandHistoryDesc": "預設會追蹤新主機上的命令歷史記錄",
"hostDefaultsSaved": "主機預設設定已儲存",
"hostDefaultsSaveFailed": "儲存主機預設設定失敗"
"hostDefaultsSaveFailed": "儲存主機預設設定失敗",
"rolePermissions": {
"count": "{{count}} 權限",
"editAction": "編輯權限",
"loadError": "載入權限目錄失敗",
"saved": "角色權限已儲存",
"saveError": "儲存角色權限失敗",
"save": "節省",
"saving": "儲存..."
}
},
"newUi": {
"sidebar": {
@@ -2991,6 +3120,10 @@
"executionFailed": "失敗的"
},
"userProfile": {
"donateTitle": "讓Termix活下去",
"donateDescription": "Termix 是免費的,由幾位業餘人士自行託管和開發。如果它為您節省了時間或金錢,歡迎您捐贈加密貨幣以支持其持續運作。",
"donateMilestones": "捐款將用於資助我們研究和學習建立 SAML、Kubernetes 和 Agent 支援所需的知識。查看進度並捐款。",
"donateButton": "捐贈加密貨幣",
"storageModeLocal": "瀏覽器",
"storageModeCloud": "資料庫",
"storageModeDescription": "瀏覽器僅將設定儲存在此瀏覽器中。資料庫會同步到伺服器,並可在任何裝置上載入。",
@@ -3011,6 +3144,10 @@
"twoFaOn": "在",
"twoFaOff": "離開",
"versionLabel": "版本",
"betaProgramTitle": "測試計劃",
"betaProgramDescription": "使用每週滾動更新的 :beta Docker 標籤,即可提前體驗即將推出的功能。此版本不穩定,不適用於生產環境。",
"betaProgramFeedback": "發現漏洞?請在此處報告。",
"betaProgramLearnMore": "了解更多",
"deleteAccount": "刪除帳戶",
"deleteAccountDescription": "永久刪除您的帳戶",
"changeServerDescription": "切換到其他 Termix 後端伺服器",

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