Compare commits

..

1 Commits

Author SHA1 Message Date
Crowdin Bot acddd9183e New Crowdin translations by GitHub Action
Lint / Linting Checks (push) Has been cancelled
Tests / vitest (1) (push) Has been cancelled
Tests / vitest (2) (push) Has been cancelled
Tests / vitest (3) (push) Has been cancelled
Tests / vitest (4) (push) Has been cancelled
2026-08-30 00:57:15 +00:00
341 changed files with 2852 additions and 5739 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: crowdin action
uses: crowdin/github-action@9af557de76d70c480f88065d336f445a362f402b # v3.1.0
uses: crowdin/github-action@8f01d54f70f1713ee3f09d82c2bbb2daeac28689 # v2
with:
upload_translations: false
download_translations: true
+4 -4
View File
@@ -62,7 +62,7 @@ jobs:
nextjs-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install pnpm
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
with:
version: 10
run_install: false
@@ -99,14 +99,14 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Setup QEMU
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
- name: Setup Docker buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
+3 -3
View File
@@ -23,7 +23,7 @@ jobs:
with:
python-version-file: ".python-version"
- name: Install uv
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
- run: sudo apt-get install pngquant
- name: Test Docs Build
run: uv run --frozen zensical build --clean
@@ -45,12 +45,12 @@ jobs:
with:
python-version-file: ".python-version"
- name: Install uv
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
- run: sudo apt-get install pngquant
- name: Build Docs
run: uv run --frozen zensical build --clean
- uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
with:
path: site
- uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1
- uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
id: deployment
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
- name: Install pnpm
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
with:
version: 10
run_install: false
+2 -2
View File
@@ -18,7 +18,7 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
with:
version: 9
@@ -31,7 +31,7 @@ jobs:
# Run Vitest directly so `--shard` is parsed as an option
- run: pnpm -s exec vitest run --coverage --shard ${{ matrix.shard }}/4 --pool forks
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/lcov.info
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 794 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 337 KiB

-58
View File
@@ -430,64 +430,6 @@ headerStyle: clean
headerStyle: boxedWidgets
```
## Card Style
Service and bookmark cards use a drop shadow by default. The `hairline` style replaces it with a thin border that brightens on hover.
<img width="896" alt="default cards" src="../../assets/style-default.webp">
```yaml
# default, shadowed cards
```
---
<img width="896" alt="hairline cards" src="../../assets/card-style-hairline.webp">
```yaml
cardStyle: hairline
```
## Block Style
Widget blocks (the value / label pairs inside a service card) can use the `refined` style to make values heavier and easier to read.
<img width="896" alt="default blocks" src="../../assets/style-default.webp">
```yaml
# default, standard blocks
```
---
<img width="896" alt="refined blocks" src="../../assets/block-style-refined.webp">
```yaml
blockStyle: refined
```
## Font
Homepage uses Manrope by default. You can switch to IBM Plex Mono instead, either everywhere or for data only.
<img width="896" alt="IBM Plex Mono everywhere" src="../../assets/font-plex.webp">
```yaml
font: plex # the whole dashboard
```
---
<img width="896" alt="IBM Plex Mono for data only" src="../../assets/font-plex-data.webp">
```yaml
font: plexData # only values, stats, the clock and status tags
```
With `plexData`, labels and names stay in Manrope while numbers line up in columns.
Both fonts are bundled with Homepage, so no external requests are made.
## Base URL
In some proxy configurations, it may be necessary to set the documents base URL. You can do this by providing a `base` value, like so:
-5
View File
@@ -69,11 +69,6 @@ For OIDC login (overrides password login):
- `HOMEPAGE_OIDC_CLIENT_ID`
- `HOMEPAGE_OIDC_CLIENT_SECRET`
- Optional: `HOMEPAGE_OIDC_NAME` (display name), `HOMEPAGE_OIDC_SCOPE` (defaults to `openid email profile`)
- Optional: `HOMEPAGE_OIDC_AUTO_LOGIN=true` to skip the login page and send unauthenticated visitors straight to the provider
!!! tip
With auto-login enabled, visit `/auth/signin?autologin=0` to reach the login page without being redirected, e.g. in case something goes wrong!
!!! warning
-36
View File
@@ -1,36 +0,0 @@
---
title: Custom API
description: Custom API Information Widget Configuration
---
_(Find the Custom API service widget [here](../services/customapi.md))_
The Custom API information widget shows values from a custom self-hosted or third party API in your Homepage header, alongside an icon.
```yaml
- customapi:
url: http://custom.api.host.or.ip:port/path/to/exact/api/endpoint
icon: mdi-api # optional, see below
label: My API # optional, shown below the values
href: https://custom.api.host # optional, makes the widget a link
refreshInterval: 10000 # optional - in milliseconds, defaults to 10s
username: username # auth - optional
password: password # auth - optional
method: GET # optional, e.g. POST
headers: # optional, must be object
X-API-Token: token
requestBody: # optional, can be string or object
mappings:
- field: key
label: Field 1
- field: path.to.key2
label: Field 2
format: number # optional - defaults to text
- field: path.to.key3
label: Field 3
format: percent
```
The `icon` accepts the same values as [service icons](../../configs/services.md#icons), e.g. `mdi-api`, `si-github`, `sh-homepage`, a dashboard icon name like `homepage.png`, or a full URL. If no icon is set, a generic API icon is shown.
Mappings support the same `field`, `format`, `remap`, `scale`, `prefix` and `suffix` options as the [Custom API service widget](../services/customapi.md). The `additionalField` option and the `display` modes are not supported.
-1
View File
@@ -7,7 +7,6 @@ search:
You can also find a list of all available info widgets in the sidebar navigation.
- [Custom API](customapi.md)
- [Date & Time](datetime.md)
- [Glances](glances.md)
- [Greeting](greeting.md)
+1 -2
View File
@@ -6,7 +6,6 @@ description: Arcane Widget Configuration
Learn more about [Arcane](https://github.com/getarcaneapp/arcane).
**Allowed fields** (max 4): `running`, `stopped`, `total`, `images`, `images_used`, `images_unused`, `image_updates`.
**Default fields**: `running`, `stopped`, `total`, `image_updates`.
```yaml
@@ -18,4 +17,4 @@ widget:
fields: ["running", "stopped", "total", "image_updates"] # optional
```
Using an API key for the widget requires permissions for: `containers:list`, `images:list`, and `image-updates:read`.
Using an api key for the widget requires permissions for: `containers:list` `images:list and `image-updates:read`
+2 -3
View File
@@ -9,9 +9,7 @@ See the [crowdsec docs](https://docs.crowdsec.net/docs/local_api/intro/#machines
in most instances you can use the default credentials (`/etc/crowdsec/local_api_credentials.yaml`).
!!! note
`alerts` counts alerts raised by your own engine in the last 24 hours. Alerts originating from the
CrowdSec community blocklist are excluded.
Without the `limit24h` option, the widget will fetch all alerts which is limited to 100 by the API to avoid performance issues.
Allowed fields: `["alerts", "bans"]`.
@@ -21,4 +19,5 @@ widget:
url: http://crowdsechostorip:port
username: localhost # machine_id in crowdsec
password: password
limit24h: true # optional, limits alerts to last 24h. Default: false
```
-2
View File
@@ -3,8 +3,6 @@ title: Custom API
description: Custom Widget Configuration from the API
---
_(Find the Custom API information widget [here](../info/customapi.md))_
This widget can show information from custom self-hosted or third party API.
Fields need to be defined in the `mappings` section YAML object to correlate with the value in the APIs JSON object. Final field definition needs to be the key with the desired value information.
-46
View File
@@ -1,46 +0,0 @@
---
title: Feed
description: RSS / Atom feed widget
---
This widget shows the latest items from an RSS 2.0 or Atom feed.
```yaml
widget:
type: feed
url: https://feeds.bbci.co.uk/news/rss.xml
maxItems: 5 # optional - defaults to 5
layout: list # optional - possible values list, grid - defaults to list
images: true # optional - set to false to hide images - defaults to true
```
## Layouts
`list` shows one row per item. Items with an image get a small thumbnail.
![Feed widget list layout](../../assets/widget_feed_list.webp)
![Feed widget list layout with thumbnails](../../assets/widget_feed_thumbnails.webp)
`grid` shows image tiles that re-flow to fit the width of the widget, e.g.:
```yaml
- NASA Image of the Day:
widget:
type: feed
url: https://www.nasa.gov/feeds/iotd-feed/
layout: grid
maxItems: 8
```
![Feed widget grid layout](../../assets/widget_feed_grid.webp)
## Images
Images are taken from the feed's `media:thumbnail` / `media:content` tags or image enclosures, falling back to the first image in the item's content. Some feeds do not include images at all.
Images are loaded by your browser directly from the feed's host, at whatever size the feed provides. Feeds that link full-size originals can be slow to load; set `images: false` for those.
## Notes
Feeds are fetched by homepage, not your browser, and cached for 10 minutes.
-1
View File
@@ -39,7 +39,6 @@ You can also find a list of all available service widgets in the sidebar navigat
- [Emby](emby.md)
- [ESPHome](esphome.md)
- [EVCC](evcc.md)
- [Feed](feed.md)
- [Filebrowser](filebrowser.md)
- [Fileflows](fileflows.md)
- [Firefly III](firefly.md)
+2 -2
View File
@@ -13,8 +13,8 @@ Allowed fields: `["movies", "series", "episodes", "songs", "albums"]`.
| Jellyfin Version | Homepage Widget Version |
| ---------------- | ----------------------- |
| < 12 (10.12) | 1 (default) |
| >= 12 (10.12) | 2 |
| < 10.12 | 1 (default) |
| >= 10.12 | 2 |
```yaml
widget:
+3 -3
View File
@@ -5,7 +5,7 @@ description: Nginx Proxy Manager Widget Configuration
Learn more about [Nginx Proxy Manager](https://nginxproxymanager.com/).
Use a "view-only" user for the username and password.
Login with the same admin username and password used to access the web UI.
Allowed fields: `["enabled", "disabled", "total"]`.
@@ -13,6 +13,6 @@ Allowed fields: `["enabled", "disabled", "total"]`.
widget:
type: npm
url: http://npm.host.or.ip
username: view_only_username
password: view_only_password
username: admin_username
password: admin_password
```
-1
View File
@@ -13,5 +13,4 @@ widget:
url: http://whatsupdocker:port
username: username # optional
password: password # optional
key: bearer-token # optional, takes precedence over username/password
```
+21 -4
View File
@@ -1,13 +1,30 @@
import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
import prettierConfig from "eslint-config-prettier/flat";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { fixupConfigRules } from "@eslint/compat";
import { FlatCompat } from "@eslint/eslintrc";
import js from "@eslint/js";
import prettier from "eslint-plugin-prettier";
import { defineConfig, globalIgnores } from "eslint/config";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
});
export default defineConfig([
...nextCoreWebVitals,
prettierConfig,
{
files: ["**/*.{js,mjs,cjs,jsx}"],
extends: fixupConfigRules(compat.extends("next/core-web-vitals", "prettier", "plugin:react-hooks/recommended")),
plugins: {
prettier,
},
languageOptions: {
ecmaVersion: 6,
sourceType: "module",
-2
View File
@@ -65,7 +65,6 @@ nav:
- widgets/services/emby.md
- widgets/services/esphome.md
- widgets/services/evcc.md
- widgets/services/feed.md
- widgets/services/filebrowser.md
- widgets/services/fileflows.md
- widgets/services/firefly.md
@@ -195,7 +194,6 @@ nav:
- widgets/services/zabbix.md
- "Information Widgets":
- widgets/info/index.md
- widgets/info/customapi.md
- widgets/info/datetime.md
- widgets/info/glances.md
- widgets/info/greeting.md
-6
View File
@@ -106,12 +106,6 @@ function relativeDate(date, formatter) {
const unitIndex = cutoffs.findIndex((cutoff) => cutoff > Math.abs(delta));
const divisor = unitIndex ? cutoffs[unitIndex - 1] : 1;
if (units[unitIndex] === "day") {
// compare calendar days, not elapsed 24h blocks
const startOfDay = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate());
return formatter.format(Math.round((startOfDay(date) - startOfDay(new Date())) / 86400000), "day");
}
return formatter.format(Math.floor(delta / divisor), units[unitIndex]);
}
+14 -7
View File
@@ -1,6 +1,6 @@
{
"name": "homepage",
"version": "2.4.0",
"version": "2.1.0",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",
@@ -23,12 +23,12 @@
"gamedig": "^5.3.3",
"i18next": "^26.3.6",
"ical.js": "^2.2.1",
"js-yaml": "^5.4.2",
"js-yaml": "^5.3.0",
"json-rpc-2.0": "^1.7.1",
"luxon": "^3.7.2",
"memory-cache": "^0.2.0",
"minecraftstatuspinger": "^1.2.2",
"next": "^16.3.3",
"next": "^16.2.12",
"next-auth": "^4.24.15",
"next-i18next": "^16.0.8",
"ping": "^1.0.0",
@@ -36,10 +36,9 @@
"raw-body": "^4.0.0",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-i18next": "^17.0.12",
"react-i18next": "^17.0.11",
"react-icons": "^5.6.0",
"recharts": "^3.1.2",
"sax": "^1.6.1",
"swr": "^2.5.1",
"systeminformation": "^5.33.1",
"tough-cookie": "^6.0.2",
@@ -49,21 +48,29 @@
"xml-js": "^1.6.11"
},
"devDependencies": {
"@eslint/compat": "^2.1.0",
"@eslint/eslintrc": "^3.3.3",
"@eslint/js": "^9.39.4",
"@tailwindcss/forms": "^0.5.11",
"@tailwindcss/postcss": "^4.3.3",
"@testing-library/jest-dom": "^7.0.1",
"@testing-library/react": "^16.3.0",
"@vitest/coverage-v8": "^4.1.10",
"eslint": "^9.25.1",
"eslint-config-next": "^16.3.3",
"eslint-config-next": "^15.5.11",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.5.6",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^7.1.1",
"jsdom": "^30.0.1",
"postcss": "^8.5.26",
"prettier": "^3.8.4",
"prettier-plugin-organize-imports": "^4.3.0",
"tailwind-scrollbar": "^4.0.2",
"tailwindcss": "^4.3.3",
"typescript": "^6.0.3",
"typescript": "^5.7.3",
"vitest": "^4.1.0"
},
"optionalDependencies": {
+501 -424
View File
File diff suppressed because it is too large Load Diff
+5 -10
View File
@@ -21,7 +21,7 @@
"seconds": "s"
},
"auth": {
"signout": "Teken uit"
"signout": "Sign out"
},
"widget": {
"missing_type": "Ontbrekende legstuk-tipe: {{type}}",
@@ -40,8 +40,8 @@
},
"search": {
"placeholder": "Soek…",
"provider": "Soekenjin: {{name}}",
"select_provider": "Kies soekenjin"
"provider": "Search engine: {{name}}",
"select_provider": "Select search engine"
},
"resources": {
"cpu": "SVE",
@@ -99,12 +99,7 @@
"unhealthy": "Ongesond",
"not_found": "Nie Gevind Nie",
"exited": "Verlaat",
"partial": "Gedeeltelik",
"created": "Geskep",
"paused": "Onderbreek",
"restarting": "Herlaai",
"removing": "Verwydering",
"dead": "Dood"
"partial": "Gedeeltelik"
},
"ping": {
"error": "Fout",
@@ -479,7 +474,7 @@
"users": "Gebruikers",
"loginsLast24H": "Aantekenings (24h)",
"failedLoginsLast24H": "Mislukte Aantekenings (24h)",
"authorizationsLast24H": "Magtigings (24h)"
"authorizationsLast24H": "Auths (24h)"
},
"proxmox": {
"mem": "GEH",
+1 -6
View File
@@ -99,12 +99,7 @@
"unhealthy": "غير سليم",
"not_found": "غير موجود",
"exited": "خرجت",
"partial": "جزئي",
"created": "Created",
"paused": "Paused",
"restarting": "Restarting",
"removing": "Removing",
"dead": "Dead"
"partial": "جزئي"
},
"ping": {
"error": "خطأ",
+1 -6
View File
@@ -99,12 +99,7 @@
"unhealthy": "Нездравословен",
"not_found": "Не е открит",
"exited": "Напуснал",
"partial": "Частично",
"created": "Created",
"paused": "Paused",
"restarting": "Restarting",
"removing": "Removing",
"dead": "Dead"
"partial": "Частично"
},
"ping": {
"error": "Грешка",
+260 -265
View File
@@ -21,7 +21,7 @@
"seconds": "s"
},
"auth": {
"signout": "Tanca la sessió"
"signout": "Sign out"
},
"widget": {
"missing_type": "Falta el tipus de widget: {{type}}",
@@ -40,8 +40,8 @@
},
"search": {
"placeholder": "Cerca…",
"provider": "Motor de cerca: {{name}}",
"select_provider": "Escull motor de cerca"
"provider": "Search engine: {{name}}",
"select_provider": "Select search engine"
},
"resources": {
"cpu": "CPU",
@@ -72,17 +72,17 @@
"empty_data": "Estat del subsistema desconegut"
},
"unifi_drive": {
"healthy": "Saludable",
"degraded": "Degradat",
"no_data": "Sense dades d'emmagatzematge disponible"
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"duplicati": {
"jobs": "Tasques",
"stored": "Desat",
"lastBackup": "Darrera execució",
"nextRun": "Propera execució",
"running": "En execució",
"warnings": "Avisos",
"jobs": "Jobs",
"stored": "Stored",
"lastBackup": "Last Run",
"nextRun": "Next Run",
"running": "Running",
"warnings": "Warnings",
"errors": "Errors"
},
"docker": {
@@ -99,12 +99,7 @@
"unhealthy": "No saludable",
"not_found": "No trobat",
"exited": "Tancat",
"partial": "Parcial",
"created": "Creat",
"paused": "Pausat",
"restarting": "Reiniciant",
"removing": "Eliminant",
"dead": "Mort"
"partial": "Parcial"
},
"ping": {
"error": "Error",
@@ -140,7 +135,7 @@
"series": "Sèries",
"episodes": "Episodis",
"songs": "Cançons",
"albums": "Àlbums"
"albums": "Albums"
},
"esphome": {
"offline": "Desconnectat",
@@ -251,10 +246,10 @@
"seed": "Sembrat"
},
"qbittorrent": {
"download": "Baixada",
"upload": "Pujada",
"leech": "Sangonera",
"seed": "Sembrat"
"download": "Download",
"upload": "Upload",
"leech": "Leech",
"seed": "Seed"
},
"qnap": {
"cpuUsage": "Ús de CPU",
@@ -265,44 +260,44 @@
"invalid": "No vàlid"
},
"deluge": {
"download": "Baixada",
"upload": "Pujada",
"leech": "Sangonera",
"seed": "Sembrat"
"download": "Download",
"upload": "Upload",
"leech": "Leech",
"seed": "Seed"
},
"develancacheui": {
"cachehitbytes": "Bytes trobats a la memòria cau",
"cachemissbytes": "Bytes no trobats a la memòria cau"
},
"downloadstation": {
"download": "Baixada",
"upload": "Pujada",
"leech": "Sangonera",
"seed": "Sembrat"
"download": "Download",
"upload": "Upload",
"leech": "Leech",
"seed": "Seed"
},
"sonarr": {
"wanted": "Pendents",
"wanted": "Volgut",
"queued": "En cua",
"series": "Sèries",
"queue": "Cua",
"unknown": "Desconegut"
"series": "Series",
"queue": "Queue",
"unknown": "Unknown"
},
"radarr": {
"wanted": "Pendents",
"wanted": "Wanted",
"missing": "Falten",
"queued": "En cua",
"movies": "Pel·lícules",
"queue": "Cua",
"unknown": "Desconegut"
"queued": "Queued",
"movies": "Movies",
"queue": "Queue",
"unknown": "Unknown"
},
"lidarr": {
"wanted": "Pendents",
"queued": "En cua",
"wanted": "Wanted",
"queued": "Queued",
"artists": "Artistes"
},
"readarr": {
"wanted": "Pendents",
"queued": "En cua",
"wanted": "Wanted",
"queued": "Queued",
"books": "Llibres"
},
"bazarr": {
@@ -315,16 +310,16 @@
"available": "Disponible"
},
"seerr": {
"pending": "Pendent",
"approved": "Aprovat",
"available": "Disponible",
"completed": "Completat",
"processing": "Processant",
"issues": "Incidències"
"pending": "Pending",
"approved": "Approved",
"available": "Available",
"completed": "Completed",
"processing": "Processing",
"issues": "Open Issues"
},
"netalertx": {
"total": "Total",
"connected": "Connectat",
"connected": "Connected",
"new_devices": "Nous dispositius",
"down_alerts": "Alertes de caigudes"
},
@@ -335,18 +330,18 @@
"gravity": "Gravetat"
},
"adguard": {
"queries": "Consultes",
"blocked": "Bloquejat",
"queries": "Queries",
"blocked": "Blocked",
"filtered": "Filtrat",
"latency": "Latència"
},
"speedtest": {
"upload": "Pujada",
"download": "Baixada",
"ping": "Latència"
"upload": "Upload",
"download": "Download",
"ping": "Ping"
},
"portainer": {
"running": "En execució",
"running": "Running",
"stopped": "Aturat",
"total": "Total"
},
@@ -364,16 +359,16 @@
"address": "Adreça",
"expires": "Caduca",
"never": "Mai",
"user": "Usuari",
"hostname": "Nom d'hoste",
"name": "Nom",
"client_version": "Versió de client",
"user": "User",
"hostname": "Hostname",
"name": "Name",
"client_version": "Client Version",
"os": "OS",
"created": "Creat",
"authorized": "Autoritzat",
"is_external": "És extern",
"update_available": "Actualització disponible",
"tags": "Etiquetes",
"created": "Created",
"authorized": "Authorized",
"is_external": "Is External",
"update_available": "Update Available",
"tags": "Tags",
"last_seen": "Vist per darrer cop",
"now": "Ara",
"years": "{{number}}a",
@@ -383,7 +378,7 @@
"minutes": "{{number}}m",
"seconds": "{{number}}s",
"ago": "Fa {{value}}",
"true": "Sí",
"true": "Yes",
"false": "No"
},
"technitium": {
@@ -460,8 +455,8 @@
"domain_count": "Dominis"
},
"medusa": {
"wanted": "Pendents",
"queued": "En cua",
"wanted": "Volguts",
"queued": "Encuat",
"series": "Sèries"
},
"minecraft": {
@@ -479,7 +474,7 @@
"users": "Usuaris",
"loginsLast24H": "Inicis de sessió (24h)",
"failedLoginsLast24H": "Errors d'inici de sessió (24h)",
"authorizationsLast24H": "Autentificacions (últ. 24h)"
"authorizationsLast24H": "Auths (24h)"
},
"proxmox": {
"mem": "MEM",
@@ -547,37 +542,37 @@
"61-day": "Pluja lleugera",
"61-night": "Pluja lleugera",
"63-day": "Pluja",
"63-night": "Pluja",
"63-night": "Rain",
"65-day": "Pluja intensa",
"65-night": "Pluja forta",
"65-night": "Heavy Rain",
"66-day": "Pluja gelada",
"66-night": "Pluja gelada",
"67-day": "Pluja gelada",
"67-night": "Pluja gelada",
"66-night": "Freezing Rain",
"67-day": "Freezing Rain",
"67-night": "Freezing Rain",
"71-day": "Neu lleugera",
"71-night": "Nevada lleugera",
"71-night": "Light Snow",
"73-day": "Neu",
"73-night": "Nevada",
"73-night": "Snow",
"75-day": "Neu intensa",
"75-night": "Nevada forta",
"75-night": "Heavy Snow",
"77-day": "Neu lleugera",
"77-night": "Flocs de neu",
"77-night": "Snow Grains",
"80-day": "Plovisqueig",
"80-night": "Plugims suaus",
"80-night": "Light Showers",
"81-day": "Xàfecs",
"81-night": "Plugims",
"81-night": "Showers",
"82-day": "Xàfecs intensos",
"82-night": "Plugims forts",
"82-night": "Heavy Showers",
"85-day": "Xàfecs de neu",
"85-night": "Plugims amb neu",
"86-day": "Plugims amb neu",
"86-night": "Plugims amb neu",
"85-night": "Snow Showers",
"86-day": "Snow Showers",
"86-night": "Snow Showers",
"95-day": "Tempesta",
"95-night": "Tormenta",
"95-night": "Thunderstorm",
"96-day": "Tempesta amb calamarsa",
"96-night": "Tormenta amb calamarsa",
"99-day": "Tormenta amb calamarsa",
"99-night": "Tormenta amb calamarsa"
"96-night": "Thunderstorm With Hail",
"99-day": "Thunderstorm With Hail",
"99-night": "Thunderstorm With Hail"
},
"homebridge": {
"available_update": "Sistema",
@@ -586,18 +581,18 @@
"up_to_date": "Actualitzat",
"child_bridges": "Ponts fills",
"child_bridges_status": "{{ok}}/{{total}}",
"up": "En línia",
"pending": "Pendent",
"down": "Inactiu",
"up": "Up",
"pending": "Pending",
"down": "Down",
"ok": "Ok"
},
"healthchecks": {
"new": "Nou",
"up": "Actiu",
"up": "Up",
"grace": "En Període de gràcia",
"down": "Inactiu",
"down": "Down",
"paused": "En pausa",
"status": "Estat",
"status": "Status",
"last_ping": "Últim ping",
"never": "Sense pings"
},
@@ -607,74 +602,74 @@
"containers_failed": "Error"
},
"autobrr": {
"approvedPushes": "Aprovat",
"approvedPushes": "Approved",
"rejectedPushes": "Rebutjat",
"filters": "Filtres",
"indexers": "Indexadors"
"indexers": "Indexers"
},
"tubearchivist": {
"downloads": "Cua",
"downloads": "Queue",
"videos": "Vídeos",
"channels": "Canals",
"playlists": "Llistes de reproducció"
},
"truenas": {
"load": "Càrrega del sistema",
"uptime": "Temps actiu",
"alerts": "Alertes"
"uptime": "Uptime",
"alerts": "Alerts"
},
"pyload": {
"speed": "Velocitat",
"active": "Actiu",
"queue": "Cua",
"active": "Active",
"queue": "Queue",
"total": "Total"
},
"gluetun": {
"public_ip": "IP Pública",
"region": "Regió",
"country": "País",
"port_forwarded": "Ports redirigits"
"port_forwarded": "Port Forwarded"
},
"hdhomerun": {
"channels": "Canals",
"channels": "Channels",
"hd": "HD",
"tunerCount": "Sintonitzadors",
"channelNumber": "Canal",
"channelNetwork": "Xarxa",
"signalStrength": "Intensitat",
"signalQuality": "Qualitat",
"symbolQuality": "Qualitat",
"networkRate": "Taxa de bits",
"symbolQuality": "Quality",
"networkRate": "Bitrate",
"clientIP": "Client"
},
"scrutiny": {
"passed": "Aprovat",
"failed": "Error",
"unknown": "Desconegut"
"failed": "Failed",
"unknown": "Unknown"
},
"paperlessngx": {
"inbox": "Safata d'entrada",
"total": "Total"
},
"pangolin": {
"orgs": "Organitzacions",
"sites": "Llocs",
"resources": "Recursos",
"targets": "Objectius",
"traffic": "Tràfic",
"in": "Dins",
"out": "Fora"
"orgs": "Orgs",
"sites": "Sites",
"resources": "Resources",
"targets": "Targets",
"traffic": "Traffic",
"in": "In",
"out": "Out"
},
"peanut": {
"battery_charge": "Càrrega de la bateria",
"ups_load": "Càrrega del SAI",
"ups_status": "Estat del SAI",
"online": "En línia",
"online": "Online",
"on_battery": "En Bateria",
"low_battery": "Bateria Baixa"
},
"nextdns": {
"wait": "Espereu si us plau",
"wait": "Please Wait",
"no_devices": "No s'han rebut dades del Dispositiu"
},
"mikrotik": {
@@ -767,8 +762,8 @@
},
"mylar": {
"series": "Sèries",
"issues": "Números",
"wanted": "Pendents"
"issues": "Problemes",
"wanted": "Volguts"
},
"photoprism": {
"albums": "Àlbums",
@@ -862,37 +857,37 @@
"jdownloader": {
"downloadCount": "Cua",
"downloadBytesRemaining": "Restant",
"downloadTotalBytes": "Mida",
"downloadSpeed": "Velocitat"
"downloadTotalBytes": "Size",
"downloadSpeed": "Speed"
},
"kavita": {
"seriesCount": "Sèries",
"totalFiles": "Fitxers"
"seriesCount": "Series",
"totalFiles": "Files"
},
"azuredevops": {
"result": "Resultat",
"status": "Estat",
"status": "Status",
"buildId": "Id de compilació",
"succeeded": "Amb èxit",
"notStarted": "No Iniciat",
"failed": "Error",
"failed": "Failed",
"canceled": "Cancel·lat",
"inProgress": "En curs",
"totalPrs": "PRs Totals",
"myPrs": "Les meves PRs",
"approved": "Aprovat"
"approved": "Approved"
},
"gamedig": {
"status": "Estat",
"online": "En línia",
"offline": "Desconnectat",
"status": "Status",
"online": "Online",
"offline": "Offline",
"name": "Nom",
"map": "Mapa",
"currentPlayers": "Jugadors actuals",
"players": "Jugadors",
"players": "Players",
"maxPlayers": "Màxim de jugadors",
"bots": "Bots",
"ping": "Latència"
"ping": "Ping"
},
"urbackup": {
"ok": "Ok",
@@ -902,39 +897,39 @@
},
"mealie": {
"recipes": "Receptes",
"users": "Usuaris",
"users": "Users",
"categories": "Categories",
"tags": "Etiquetes"
},
"openmediavault": {
"downloading": "Descarregant",
"total": "Total",
"running": "En execució",
"stopped": "Aturat",
"passed": "Aprovat",
"failed": "Error"
"running": "Running",
"stopped": "Stopped",
"passed": "Passed",
"failed": "Failed"
},
"openwrt": {
"uptime": "Temps actiu",
"uptime": "Uptime",
"cpuLoad": "Càrrega mitjana de CPU (5min)",
"up": "En línia",
"down": "Desconnectat",
"up": "Up",
"down": "Down",
"bytesTx": "Enviat",
"bytesRx": "Rebut"
"bytesRx": "Received"
},
"uptimerobot": {
"status": "Estat",
"uptime": "Temps actiu",
"status": "Status",
"uptime": "Uptime",
"lastDown": "Darrera Inactivitat",
"downDuration": "Duració d'Inactivitat",
"sitesUp": "Pàgines actives",
"sitesDown": "Pàgines inactives",
"paused": "Pausat",
"sitesUp": "Sites Up",
"sitesDown": "Sites Down",
"paused": "Paused",
"notyetchecked": "Sense verificar",
"up": "En línia",
"up": "Up",
"seemsdown": "Sembla caigut",
"down": "Desconnectat",
"unknown": "Desconegut"
"down": "Down",
"unknown": "Unknown"
},
"calendar": {
"inCinemas": "En cines",
@@ -942,7 +937,7 @@
"digitalRelease": "Estrena digital",
"noEventsToday": "Cap esdeveniment per avui!",
"noEventsFound": "No s'han trobat esdeveniments",
"errorWhenLoadingData": "Error carregant dades del calendari"
"errorWhenLoadingData": "Error when loading calendar data"
},
"romm": {
"platforms": "Plataformes",
@@ -953,50 +948,50 @@
"totalfilesize": "Tamany total"
},
"mailcow": {
"domains": "Dominis",
"domains": "Domains",
"mailboxes": "Bústies",
"mails": "Correus",
"storage": "Emmagatzematge"
"storage": "Storage"
},
"maintainerr": {
"itemsHandled": "Items gestionats",
"episodesHandled": "Episodis gestionats",
"moviesHandled": "Pel·lícules gestionades",
"showsHandled": "Sèries gestionades",
"seasonsHandled": "Temporades gestionades",
"reclaimable": "Reclamable",
"movieReclaimable": "Pel·lícula reclamable",
"showReclaimable": "Sèrie reclamable",
"totalCapacity": "Capacitat total"
"itemsHandled": "Items Handled",
"episodesHandled": "Episodes Handled",
"moviesHandled": "Movies Handled",
"showsHandled": "Shows Handled",
"seasonsHandled": "Seasons Handled",
"reclaimable": "Reclaimable",
"movieReclaimable": "Movie Reclaimable",
"showReclaimable": "Show Reclaimable",
"totalCapacity": "Total Capacity"
},
"netdata": {
"warnings": "Avisos",
"criticals": "Crítics"
},
"ntfy": {
"title": "Títol",
"priority": "Prioritat",
"lastReceived": "Rebut per últim cop",
"message": "Missatge",
"tags": "Etiquetes",
"none": "Cap",
"title": "Title",
"priority": "Priority",
"lastReceived": "Last Received",
"message": "Message",
"tags": "Tags",
"none": "None",
"min": "Min",
"low": "Baix",
"default": "Per defecte",
"high": "Alt",
"low": "Low",
"default": "Default",
"high": "High",
"urgent": "Urgent"
},
"plantit": {
"events": "Esdeveniments",
"plants": "Plantes",
"photos": "Fotos",
"photos": "Photos",
"species": "Espècies"
},
"gitea": {
"notifications": "Notificacions",
"issues": "Incidències",
"issues": "Issues",
"pulls": "Sol·licitud de Canvis",
"repositories": "Repositoris"
"repositories": "Repositories"
},
"stash": {
"scenes": "Escenes",
@@ -1010,13 +1005,13 @@
"galleries": "Biblioteques",
"performers": "Intèrprets",
"studios": "Estudis",
"movies": "Pel·lícules",
"tags": "Etiquetes",
"movies": "Movies",
"tags": "Tags",
"oCount": "O Count"
},
"tandoor": {
"users": "Usuaris",
"recipes": "Receptes",
"users": "Users",
"recipes": "Recipes",
"keywords": "Paraules claus"
},
"homebox": {
@@ -1024,17 +1019,17 @@
"totalWithWarranty": "Amb Garantia",
"locations": "Ubicacions",
"labels": "Etiquetes",
"users": "Usuaris",
"users": "Users",
"totalValue": "Valor total"
},
"crowdsec": {
"alerts": "Alertes",
"alerts": "Alerts",
"bans": "Prohibicions"
},
"wgeasy": {
"connected": "Connectat",
"enabled": "Habilitat",
"disabled": "Deshabilitat",
"connected": "Connected",
"enabled": "Enabled",
"disabled": "Disabled",
"total": "Total"
},
"swagdashboard": {
@@ -1044,9 +1039,9 @@
"banned": "Bloquejat"
},
"myspeed": {
"ping": "Latència",
"download": "Baixada",
"upload": "Pujada"
"ping": "Ping",
"download": "Download",
"upload": "Upload"
},
"stocks": {
"stocks": "Accions",
@@ -1124,7 +1119,7 @@
},
"gitlab": {
"groups": "Grups",
"issues": "Incidències",
"issues": "Problemes",
"merges": "Merge Requests",
"projects": "Projectes"
},
@@ -1143,120 +1138,120 @@
"tags": "Etiquetes"
},
"slskd": {
"slskStatus": "Xarxa",
"connected": "Connectat",
"disconnected": "Desconnectat",
"updateStatus": "Actualitza",
"update_yes": "Disponible",
"update_no": "Actualitzat",
"downloads": "Descàrregues",
"uploads": "Pujades",
"sharedFiles": "Fitxers"
"slskStatus": "Network",
"connected": "Connected",
"disconnected": "Disconnected",
"updateStatus": "Update",
"update_yes": "Available",
"update_no": "Up to Date",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Files"
},
"jellystat": {
"songs": "Cançons",
"movies": "Pel·lícules",
"episodes": "Episodis",
"other": "Altres"
"songs": "Songs",
"movies": "Movies",
"episodes": "Episodes",
"other": "Other"
},
"checkmk": {
"serviceErrors": "Errors de servei",
"hostErrors": "Errors de l'hoste"
"serviceErrors": "Service issues",
"hostErrors": "Host issues"
},
"komodo": {
"total": "Total",
"running": "En execució",
"stopped": "Aturat",
"down": "Fora de servei",
"unhealthy": "No saludable",
"unknown": "Desconegut",
"servers": "Servidors",
"running": "Running",
"stopped": "Stopped",
"down": "Down",
"unhealthy": "Unhealthy",
"unknown": "Unknown",
"servers": "Servers",
"stacks": "Stacks",
"containers": "Contenidors"
"containers": "Containers"
},
"filebrowser": {
"available": "Disponible",
"used": "Utilitzat",
"available": "Available",
"used": "Used",
"total": "Total"
},
"wallos": {
"activeSubscriptions": "Subscripcions",
"thisMonthlyCost": "Aquest mes",
"nextMonthlyCost": "Mes que ve",
"previousMonthlyCost": "Mes passat",
"nextRenewingSubscription": "Proper pagament"
"activeSubscriptions": "Subscriptions",
"thisMonthlyCost": "This Month",
"nextMonthlyCost": "Next Month",
"previousMonthlyCost": "Prev. Month",
"nextRenewingSubscription": "Next Payment"
},
"unraid": {
"STARTED": "Iniciat",
"STOPPED": "Aturat",
"NEW_ARRAY": "Nou array",
"RECON_DISK": "Reconstruint disc",
"DISABLE_DISK": "Disc desactivat",
"SWAP_DSBL": "Swap desactivat",
"INVALID_EXPANSION": "Expansió invàlida",
"PARITY_NOT_BIGGEST": "Paritat no del més gran",
"TOO_MANY_MISSING_DISKS": "Falten masses discs",
"NEW_DISK_TOO_SMALL": "Nou disc massa petit",
"NO_DATA_DISKS": "Sense discs de dades",
"notifications": "Notificacions",
"status": "Estat",
"STARTED": "Started",
"STOPPED": "Stopped",
"NEW_ARRAY": "New Array",
"RECON_DISK": "Reconstructing Disk",
"DISABLE_DISK": "Disk Disabled",
"SWAP_DSBL": "Swap Disable",
"INVALID_EXPANSION": "Invalid Expansion",
"PARITY_NOT_BIGGEST": "Parity Not Biggest",
"TOO_MANY_MISSING_DISKS": "Too Many Missing Disks",
"NEW_DISK_TOO_SMALL": "New Disk Too Small",
"NO_DATA_DISKS": "No Data Disks",
"notifications": "Notifications",
"status": "Status",
"cpu": "CPU",
"memoryUsed": "Memòria emprada",
"memoryAvailable": "Memòria disponible",
"arrayUsed": "Array en ús",
"arrayFree": "Array lliure",
"poolUsed": "{{pool}} en ús",
"poolFree": "{{pool}} lliure"
"memoryUsed": "Memory Used",
"memoryAvailable": "Memory Available",
"arrayUsed": "Array Used",
"arrayFree": "Array Free",
"poolUsed": "{{pool}} Used",
"poolFree": "{{pool}} Free"
},
"backrest": {
"num_plans": "Plans",
"num_success_30": "Èxits",
"num_failure_30": "Fallades",
"num_success_latest": "Triomfant",
"num_failure_latest": "Fallant",
"bytes_added_30": "Bytes afegits"
"num_success_30": "Successes",
"num_failure_30": "Failures",
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "Cançons",
"time": "Temps",
"artists": "Artistes"
"songs": "Songs",
"time": "Time",
"artists": "Artists"
},
"arcane": {
"containers": "Contenidors",
"images": "Imatges",
"image_updates": "Actualitzacions d'imatges",
"images_unused": "Sense utilitzar",
"environment_required": "ID d'entorn requerit"
"containers": "Containers",
"images": "Images",
"image_updates": "Image Updates",
"images_unused": "Unused",
"environment_required": "Environment ID Required"
},
"dockhand": {
"running": "En execució",
"stopped": "Aturats",
"running": "Running",
"stopped": "Stopped",
"cpu": "CPU",
"memory": "Memòria",
"images": "Imatges",
"volumes": "Volums",
"events_today": "Esdeveniments avui",
"pending_updates": "Actualitzacions pendents",
"memory": "Memory",
"images": "Images",
"volumes": "Volumes",
"events_today": "Events Today",
"pending_updates": "Pending Updates",
"stacks": "Stacks",
"paused": "Pausats",
"paused": "Paused",
"total": "Total",
"environment_not_found": "Entorn no trobat"
"environment_not_found": "Environment Not Found"
},
"sparkyfitness": {
"eaten": "Menjat",
"burned": "Cremat",
"remaining": "Restant",
"steps": "Passes"
"eaten": "Eaten",
"burned": "Burned",
"remaining": "Remaining",
"steps": "Steps"
},
"syncthing": {
"connected": "Connectat",
"synced": "Sincronitzat",
"connected": "Connected",
"synced": "Synced",
"errors": "Errors",
"storage": "Emmagatzematge"
"storage": "Storage"
},
"sportarr": {
"wanted": "Pendents",
"queued": "En cua",
"leagues": "Lligues"
"wanted": "Wanted",
"queued": "Queued",
"leagues": "Leagues"
}
}
+1 -6
View File
@@ -99,12 +99,7 @@
"unhealthy": "Nezdravý",
"not_found": "Nenalezen",
"exited": "Ukončen",
"partial": "Částečný",
"created": "Created",
"paused": "Paused",
"restarting": "Restarting",
"removing": "Removing",
"dead": "Dead"
"partial": "Částečný"
},
"ping": {
"error": "Chyba",
+1 -6
View File
@@ -99,12 +99,7 @@
"unhealthy": "Usund",
"not_found": "Ikke Fundet",
"exited": "Forladt",
"partial": "Delvis",
"created": "Created",
"paused": "Paused",
"restarting": "Restarting",
"removing": "Removing",
"dead": "Dead"
"partial": "Delvis"
},
"ping": {
"error": "Error",
+41 -46
View File
@@ -21,7 +21,7 @@
"seconds": "s"
},
"auth": {
"signout": "Abmelden"
"signout": "Sign out"
},
"widget": {
"missing_type": "Fehlender Widget-Typ: {{type}}",
@@ -40,8 +40,8 @@
},
"search": {
"placeholder": "Suche…",
"provider": "Suchmaschine: {{name}}",
"select_provider": "Suchmaschine auswählen"
"provider": "Search engine: {{name}}",
"select_provider": "Select search engine"
},
"resources": {
"cpu": "CPU",
@@ -77,13 +77,13 @@
"no_data": "Keine Speicherdaten verfügbar"
},
"duplicati": {
"jobs": "Aufgaben",
"stored": "Gespeichert",
"lastBackup": "Zuletzt ausgeführt",
"nextRun": "Nächste Ausführung",
"running": "Wird ausgeführt",
"warnings": "Warnungen",
"errors": "Fehler"
"jobs": "Jobs",
"stored": "Stored",
"lastBackup": "Last Run",
"nextRun": "Next Run",
"running": "Running",
"warnings": "Warnings",
"errors": "Errors"
},
"docker": {
"rx": "RX",
@@ -99,12 +99,7 @@
"unhealthy": "Fehlerhaft",
"not_found": "Nicht gefunden",
"exited": "Beendet",
"partial": "Teilweise",
"created": "Erstellt am",
"paused": "Pausiert",
"restarting": "Wird neu gestartet",
"removing": "Wird entfernt",
"dead": "Tot"
"partial": "Teilweise"
},
"ping": {
"error": "Fehler",
@@ -140,7 +135,7 @@
"series": "Serien",
"episodes": "Episoden",
"songs": "Titel",
"albums": "Alben"
"albums": "Albums"
},
"esphome": {
"offline": "Offline",
@@ -364,15 +359,15 @@
"address": "Adresse",
"expires": "Läuft ab",
"never": "Nie",
"user": "Benutzer",
"user": "User",
"hostname": "Hostname",
"name": "Name",
"client_version": "Client-Version",
"os": "Betriebssystem",
"created": "Erstellt am",
"authorized": "Autorisiert",
"is_external": "Ist extern",
"update_available": "Aktualisierung verfügbar",
"client_version": "Client Version",
"os": "OS",
"created": "Created",
"authorized": "Authorized",
"is_external": "Is External",
"update_available": "Update Available",
"tags": "Tags",
"last_seen": "Zuletzt gesehen",
"now": "Jetzt",
@@ -383,8 +378,8 @@
"minutes": "{{number}}min",
"seconds": "{{number}}s",
"ago": "Vor {{value}}",
"true": "Ja",
"false": "Nein"
"true": "Yes",
"false": "No"
},
"technitium": {
"totalQueries": "Anfragen",
@@ -479,7 +474,7 @@
"users": "Benutzer",
"loginsLast24H": "Anmeldungen (24 h)",
"failedLoginsLast24H": "Fehlversuche (24 h)",
"authorizationsLast24H": "Autorisierungen (24h)"
"authorizationsLast24H": "Auths (24h)"
},
"proxmox": {
"mem": "RAM",
@@ -488,7 +483,7 @@
"vms": "VMs"
},
"pulse": {
"nodes": "Knoten",
"nodes": "Nodes",
"vms": "VMs",
"lxcs": "LXCs"
},
@@ -657,13 +652,13 @@
"total": "Total"
},
"pangolin": {
"orgs": "Organisationen",
"orgs": "Orgs",
"sites": "Seiten",
"resources": "Ressourcen",
"targets": "Ziele",
"traffic": "Traffic",
"in": "Eingehend",
"out": "Ausgehend"
"in": "In",
"out": "Out"
},
"peanut": {
"battery_charge": "Akkuladung",
@@ -959,27 +954,27 @@
"storage": "Speicher"
},
"maintainerr": {
"itemsHandled": "Items bearbeitet",
"episodesHandled": "Episoden behandelt",
"moviesHandled": "Filme behandelt",
"showsHandled": "Serien behandelt",
"seasonsHandled": "Staffeln bearbeitet",
"reclaimable": "Zurückforderbar",
"movieReclaimable": "Film zurückforderbar",
"showReclaimable": "Serie zurückforderbar",
"totalCapacity": "Gesamte Kapazität"
"itemsHandled": "Items Handled",
"episodesHandled": "Episodes Handled",
"moviesHandled": "Movies Handled",
"showsHandled": "Shows Handled",
"seasonsHandled": "Seasons Handled",
"reclaimable": "Reclaimable",
"movieReclaimable": "Movie Reclaimable",
"showReclaimable": "Show Reclaimable",
"totalCapacity": "Total Capacity"
},
"netdata": {
"warnings": "Warnungen",
"criticals": "Kritisch"
},
"ntfy": {
"title": "Titel",
"priority": "Priorität",
"lastReceived": "Zuletzt erhalten",
"message": "Nachricht",
"tags": "Schlagwörter",
"none": "Keine",
"title": "Title",
"priority": "Priority",
"lastReceived": "Last Received",
"message": "Message",
"tags": "Tags",
"none": "None",
"min": "Min",
"low": "Low",
"default": "Default",
+281 -286
View File
@@ -14,14 +14,14 @@
"date": "{{value, date}}",
"relativeDate": "{{value, relativeDate}}",
"duration": "{{value, duration}}",
"months": "Μ",
"days": "μ",
"hours": "ω",
"minutes": "λ",
"seconds": "δ"
"months": "mo",
"days": "d",
"hours": "h",
"minutes": "m",
"seconds": "s"
},
"auth": {
"signout": "Αποσύνδεση"
"signout": "Sign out"
},
"widget": {
"missing_type": "Λείπει ο τύπος widget: {{type}}",
@@ -40,8 +40,8 @@
},
"search": {
"placeholder": "Αναζήτηση…",
"provider": "Μηχανή αναζήτησης: {{name}}",
"select_provider": "Επιλέξτε τις μηχανές αναζήτησης"
"provider": "Search engine: {{name}}",
"select_provider": "Select search engine"
},
"resources": {
"cpu": "Επεξεργαστής",
@@ -66,24 +66,24 @@
"wlan_devices": "WLAN Συσκευές",
"lan_users": "LAN Χρήστες",
"wlan_users": "WLAN Χρήστες",
"up": "ΕΠΑΝΩ",
"up": "UP",
"down": "ΚΑΤΩ",
"wait": "Παρακαλώ περιμένετε",
"wait": "Please wait",
"empty_data": "Άγνωστη κατάσταση υποσυστήματος"
},
"unifi_drive": {
"healthy": "Υγειές",
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "Δεν υπάρχουν διαθέσιμα δεδομένα χώρου αποθήκευσης"
"no_data": "No storage data available"
},
"duplicati": {
"jobs": "Εργασίες",
"stored": "Αποθηκευμένα",
"lastBackup": "Τελευταία εκτέλεση",
"nextRun": "Επόμενη εκτέλεση",
"running": "Εκτελείται",
"warnings": "Προειδοποιήσεις",
"errors": "Σφάλματα"
"jobs": "Jobs",
"stored": "Stored",
"lastBackup": "Last Run",
"nextRun": "Next Run",
"running": "Running",
"warnings": "Warnings",
"errors": "Errors"
},
"docker": {
"rx": "RX",
@@ -99,15 +99,10 @@
"unhealthy": "Άρρωστο",
"not_found": "Δεν βρέθηκε",
"exited": "Έκλεισε",
"partial": "Μερικό",
"created": "Δημιουργήθηκε",
"paused": "Σε παύση",
"restarting": "Γίνεται επανεκκίνηση",
"removing": "Αφαιρείται",
"dead": "Νεκρό"
"partial": "Μερικό"
},
"ping": {
"error": "Σφάλμα",
"error": "Error",
"ping": "Ping",
"down": "Ping down",
"up": "Ping up",
@@ -115,11 +110,11 @@
},
"siteMonitor": {
"http_status": "Κατάσταση HTTP",
"error": "Σφάλμα",
"error": "Error",
"response": "Απόκριση",
"down": "Κάτω",
"up": "Πάνω",
"not_available": "Μη Διαθέσιμο"
"down": "Down",
"up": "Up",
"not_available": "Not Available"
},
"emby": {
"playing": "Παίζει",
@@ -132,22 +127,22 @@
"songs": "Τραγούδια"
},
"jellyfin": {
"playing": "Γίνεται αναπαραγωγή",
"transcoding": "Επανακωδικοποίηση",
"bitrate": "Ρυθμός μετάδοσης",
"no_active": "Δεν Υπάρχουν Ενεργές Ροές",
"movies": "Ταινίες",
"series": "Σειρές",
"episodes": "Επεισόδια",
"songs": "Τραγούδια",
"albums": "Άλμπουμ"
"playing": "Playing",
"transcoding": "Transcoding",
"bitrate": "Bitrate",
"no_active": "No Active Streams",
"movies": "Movies",
"series": "Series",
"episodes": "Episodes",
"songs": "Songs",
"albums": "Albums"
},
"esphome": {
"offline": "Εκτός σύνδεσης",
"offline_alt": "Χωρίς Σύνδεση",
"offline": "Offline",
"offline_alt": "Offline",
"online": "Συνδεδεμένοι",
"total": "Σύνολο",
"unknown": "Άγνωστο"
"total": "Total",
"unknown": "Unknown"
},
"evcc": {
"pv_power": "Παραγωγή",
@@ -168,7 +163,7 @@
"unread": "Μη Διαβασμένο"
},
"fritzbox": {
"connectionStatus": "Κατάσταση",
"connectionStatus": "Status",
"connectionStatusUnconfigured": "Μη Ρυθμισμένο",
"connectionStatusConnecting": "Κατάσταση Σύνδεσης",
"connectionStatusAuthenticating": "Ταυτοποίηση",
@@ -176,16 +171,16 @@
"connectionStatusDisconnecting": "Αποσύνδεση",
"connectionStatusDisconnected": "Αποσυνδέθηκε",
"connectionStatusConnected": "Συνδέθηκε",
"uptime": "Συνεχής λειτουργία",
"uptime": "Uptime",
"maxDown": "Μέγιστο Download",
"maxUp": "Μέγιστο Upload",
"down": "Κάτω",
"up": "Πάνω",
"down": "Down",
"up": "Up",
"received": "Ληφθέντα",
"sent": "Απεσταλμένα",
"externalIPAddress": "Εξωτερική IP",
"externalIPv6Address": "Εξωτ. IPv6",
"externalIPv6Prefix": "Εξωτ. IPv6-Prefix"
"externalIPv6Address": "Ext. IPv6",
"externalIPv6Prefix": "Ext. IPv6-Prefix"
},
"caddy": {
"upstreams": "Upstreams",
@@ -203,10 +198,10 @@
"passes": "Περάσματα"
},
"tautulli": {
"playing": "Γίνεται αναπαραγωγή",
"transcoding": "Γίνεται επανακωδικοποίηση",
"bitrate": "Ρυθμός μετάδοσης",
"no_active": "Δεν Υπάρχουν Ενεργές Ροές",
"playing": "Playing",
"transcoding": "Transcoding",
"bitrate": "Bitrate",
"no_active": "No Active Streams",
"plex_connection_error": "Έλεγχος Σύνδεσης με Plex"
},
"tracearr": {
@@ -293,16 +288,16 @@
"queued": "Queued",
"movies": "Movies",
"queue": "Queue",
"unknown": "Άγνωστο"
"unknown": "Unknown"
},
"lidarr": {
"wanted": "Επιθυμητό",
"queued": "Σε αναμονή",
"wanted": "Wanted",
"queued": "Queued",
"artists": "Καλλιτέχνες"
},
"readarr": {
"wanted": "Επιθυμητό",
"queued": "Σε αναμονή",
"wanted": "Wanted",
"queued": "Queued",
"books": "Βιβλία"
},
"bazarr": {
@@ -315,16 +310,16 @@
"available": "Διαθέσιμο"
},
"seerr": {
"pending": "Σε εκκρεμότητα",
"approved": "Εγκρίθηκε",
"available": "Διαθέσιμα",
"completed": "Ολοκληρώθηκε",
"processing": "Σε επεξεργασία",
"issues": "Ανοιχτά θέματα"
"pending": "Pending",
"approved": "Approved",
"available": "Available",
"completed": "Completed",
"processing": "Processing",
"issues": "Open Issues"
},
"netalertx": {
"total": "Σύνολο",
"connected": "Σε σύνδεση",
"total": "Total",
"connected": "Connected",
"new_devices": "Νέες συσκευές",
"down_alerts": "Ειδοποιήσεις offline"
},
@@ -335,45 +330,45 @@
"gravity": "Gravity"
},
"adguard": {
"queries": "Ερωτήματα",
"blocked": "Αποκλεισμένα",
"queries": "Queries",
"blocked": "Blocked",
"filtered": "Φιλτραρισμένα",
"latency": "Καθυστέρηση"
},
"speedtest": {
"upload": "Αποστολή",
"download": "Λήψη",
"upload": "Upload",
"download": "Download",
"ping": "Ping"
},
"portainer": {
"running": "Εκτελείται",
"running": "Running",
"stopped": "Σταματημένο",
"total": "Σύνολο"
"total": "Total"
},
"suwayomi": {
"download": "Ελήφθησαν",
"nondownload": "Δεν Ελήφθησαν",
"read": "Διαβάστηκε",
"unread": "Μη αναγνωσμένα",
"downloadedread": "Ληφθέν & Αναγνωσμένο",
"downloadedunread": "Ληφθέν & Μη Αναγνωσμένο",
"nondownloadedread": "Μη Ληφθέν & Αναγνωσμένο",
"nondownloadedunread": "Μη Ληφθέν & Μη Αναγνωσμένο"
"download": "Downloaded",
"nondownload": "Non-Downloaded",
"read": "Read",
"unread": "Unread",
"downloadedread": "Downloaded & Read",
"downloadedunread": "Downloaded & Unread",
"nondownloadedread": "Non-Downloaded & Read",
"nondownloadedunread": "Non-Downloaded & Unread"
},
"tailscale": {
"address": "Διεύθυνση",
"expires": "Λήγει",
"never": "Ποτέ",
"user": "Χρήστης",
"hostname": "Όνομα υπολογιστή",
"name": "Όνομα",
"client_version": "Έκδοση Πελάτη",
"os": "Λειτουργικό",
"created": "Δημιουργήθηκε",
"authorized": "Εξουσιοδοτημένο",
"is_external": "Είναι Εξωτερική",
"update_available": "Διαθέσιμη ενημέρωση",
"tags": "Ετικέτες",
"user": "User",
"hostname": "Hostname",
"name": "Name",
"client_version": "Client Version",
"os": "OS",
"created": "Created",
"authorized": "Authorized",
"is_external": "Is External",
"update_available": "Update Available",
"tags": "Tags",
"last_seen": "Τελευταία Σύνδεση",
"now": "Τώρα",
"years": "{{number}}χρόνια",
@@ -383,20 +378,20 @@
"minutes": "{{number}}λεπτά",
"seconds": "{{number}}δευτερόλεπτα",
"ago": "{{value}} πρίν",
"true": "Ναι",
"false": "Όχι"
"true": "Yes",
"false": "No"
},
"technitium": {
"totalQueries": "Ερωτήματα",
"totalNoError": "Επιτυχία",
"totalServerFailure": "Σφάλματα",
"totalNxDomain": "Τομείς NX",
"totalRefused": "Απορρίφθηκαν",
"totalAuthoritative": "Εξουσιοδοτημένο",
"totalQueries": "Queries",
"totalNoError": "Success",
"totalServerFailure": "Failures",
"totalNxDomain": "NX Domains",
"totalRefused": "Refused",
"totalAuthoritative": "Authoritative",
"totalRecursive": "Recursive",
"totalCached": "Cached",
"totalBlocked": "Αποκλεισμένα",
"totalDropped": "Απορρίφθηκαν",
"totalBlocked": "Blocked",
"totalDropped": "Dropped",
"totalClients": "Πελάτες"
},
"tdarr": {
@@ -529,75 +524,75 @@
"2-day": "Αραιές Νεφώσεις",
"2-night": "Partly Cloudy",
"3-day": "Συννεφιές",
"3-night": "Συννεφιά",
"3-night": "Cloudy",
"45-day": "Ομίχλη",
"45-night": "Ομίχλη",
"48-day": "Ομίχλη",
"48-night": "Ομίχλη",
"45-night": "Foggy",
"48-day": "Foggy",
"48-night": "Foggy",
"51-day": "Ψιλόβροχο",
"51-night": "Ελαφρύ Ψιλόβροχο",
"51-night": "Light Drizzle",
"53-day": "Ψιλόβροχο",
"53-night": "Ψιλόβροχο",
"55-day": "Βαρύ Ψιλόβροχο",
"55-night": "Βαρύ Ψιλόβροχο",
"56-day": "Ελαφρύ Παγωμένο Ψιλόβροχο",
"56-night": "Ελαφρύ Παγωμένο Ψιλόβροχο",
"53-night": "Drizzle",
"55-day": "Heavy Drizzle",
"55-night": "Heavy Drizzle",
"56-day": "Light Freezing Drizzle",
"56-night": "Light Freezing Drizzle",
"57-day": "Παγωμένο ψιχάλισμα",
"57-night": "Παγωμένο ψιλόβροχο",
"57-night": "Freezing Drizzle",
"61-day": "Ψιλόβροχο",
"61-night": "Ελαφριά Βροχή",
"61-night": "Light Rain",
"63-day": "Βροχή",
"63-night": "Βροχή",
"63-night": "Rain",
"65-day": "Δυνατή βροχή",
"65-night": "Δυνατή βροχή",
"65-night": "Heavy Rain",
"66-day": "Παγωμένη βροχή",
"66-night": "Παγωμένη βροχή",
"67-day": "Παγωμένη βροχή",
"67-night": "Παγωμένη βροχή",
"66-night": "Freezing Rain",
"67-day": "Freezing Rain",
"67-night": "Freezing Rain",
"71-day": "Ελαφριά Χιονόπτωση",
"71-night": "Ελαφρά Χιονόπτωση",
"71-night": "Light Snow",
"73-day": "Χιόνι",
"73-night": "Χιόνι",
"73-night": "Snow",
"75-day": "Ισχυρή χιονόπτωση",
"75-night": "Ισχυρή χιονόπτωση",
"75-night": "Heavy Snow",
"77-day": "Κόκκοι Χιονιού",
"77-night": "Χιονάκι",
"77-night": "Snow Grains",
"80-day": "Ασθενείς βροχές",
"80-night": "Ασθενείς βροχές",
"80-night": "Light Showers",
"81-day": "Βροχοπτώσεις",
"81-night": "Βροχοπτώσεις",
"81-night": "Showers",
"82-day": "Ισχυρές βροχοπτώσεις",
"82-night": "Ισχυρές βροχοπτώσεις",
"82-night": "Heavy Showers",
"85-day": "Χιονοπτώσεις",
"85-night": "Χιονοπτώσεις",
"86-day": "Χιονοπτώσεις",
"86-night": "Χιονοπτώσεις",
"85-night": "Snow Showers",
"86-day": "Snow Showers",
"86-night": "Snow Showers",
"95-day": "Καταιγίδα",
"95-night": "Καταιγίδα",
"95-night": "Thunderstorm",
"96-day": "Καταιγίδα Με Χαλάζι",
"96-night": "Καταιγίδα Με Χαλάζι",
"99-day": "Καταιγίδα Με Χαλάζι",
"99-night": "Καταιγίδα Με Χαλάζι"
"96-night": "Thunderstorm With Hail",
"99-day": "Thunderstorm With Hail",
"99-night": "Thunderstorm With Hail"
},
"homebridge": {
"available_update": "Σύστημα",
"updates": "Ενημερώσεις",
"update_available": "Διαθέσιμη ενημέρωση",
"up_to_date": "Ενημερωμένο",
"child_bridges": "Θυγατρικές Γέφυρες",
"child_bridges": "Child Bridges",
"child_bridges_status": "{{ok}}/{{total}}",
"up": "Πάνω",
"pending": "Σε εκκρεμότητα",
"down": "Κάτω",
"ok": "Οκ"
"up": "Up",
"pending": "Pending",
"down": "Down",
"ok": "Ok"
},
"healthchecks": {
"new": "Νέο",
"up": "Πάνω",
"grace": "Σε Περίοδο Χάριτος",
"down": "Κάτω",
"paused": "Σε παύση",
"status": "Κατάσταση",
"new": "New",
"up": "Up",
"grace": "In Grace Period",
"down": "Down",
"paused": "Paused",
"status": "Status",
"last_ping": "Τελευταίο Ping",
"never": "Δεν υπάρχουν ping ακόμα"
},
@@ -607,26 +602,26 @@
"containers_failed": "Απέτυχε"
},
"autobrr": {
"approvedPushes": "Εγκρίθηκε",
"approvedPushes": "Approved",
"rejectedPushes": "Απορρίφθηκε",
"filters": "Φίλτρα",
"indexers": "Ευρετήρια"
"indexers": "Indexers"
},
"tubearchivist": {
"downloads": "Αναμονή",
"downloads": "Queue",
"videos": "Βίντεο",
"channels": "Κανάλια",
"playlists": "Λίστες αναπαραγωγής"
},
"truenas": {
"load": "Φόρτος Συστήματος",
"uptime": "Συνεχής λειτουργία",
"alerts": "Ειδοποιήσεις"
"uptime": "Uptime",
"alerts": "Alerts"
},
"pyload": {
"speed": "Ταχύτητα",
"active": "Ενεργό",
"queue": "Αναμονή",
"active": "Active",
"queue": "Queue",
"total": "Total"
},
"gluetun": {
@@ -735,56 +730,56 @@
"immich": {
"users": "Users",
"photos": "Φωτογραφίες",
"videos": "Βίντεο",
"videos": "Videos",
"storage": "Αποθηκευτικός χώρος"
},
"uptimekuma": {
"up": "Online τοποθεσίες",
"down": "Offline τοποθεσίες",
"uptime": "Συνεχής λειτουργία",
"uptime": "Uptime",
"incident": "Περιστατικό",
"m": "λ"
"m": "m"
},
"atsumeru": {
"series": "Σειρές",
"archives": "Αρχεία",
"chapters": "Κεφάλαια",
"series": "Series",
"archives": "Archives",
"chapters": "Chapters",
"categories": "Κατηγορίες"
},
"komga": {
"libraries": "Βιβλιοθήκες",
"series": "Σειρές",
"books": "Βιβλία"
"libraries": "Libraries",
"series": "Series",
"books": "Books"
},
"diskstation": {
"days": "Ημέρες",
"uptime": "Συνεχής λειτουργία",
"volumeAvailable": "Διαθέσιμα"
"days": "Days",
"uptime": "Uptime",
"volumeAvailable": "Available"
},
"dispatcharr": {
"channels": "Κανάλια",
"streams": "Ροές"
"channels": "Channels",
"streams": "Streams"
},
"mylar": {
"series": "Σειρές",
"issues": "Προβλήματα",
"wanted": "Επιθυμητά"
"series": "Series",
"issues": "Issues",
"wanted": "Wanted"
},
"photoprism": {
"albums": "Άλμπουμ",
"photos": "Φωτογραφίες",
"videos": "Βίντεο",
"albums": "Albums",
"photos": "Photos",
"videos": "Videos",
"people": "Άνθρωποι"
},
"fileflows": {
"queue": "Αναμονή",
"processing": "Σε επεξεργασία",
"processed": "Επεξεργασμένα",
"queue": "Queue",
"processing": "Processing",
"processed": "Processed",
"time": "Ώρα"
},
"firefly": {
"networth": "Καθαρή αξία",
"budget": "Προϋπολογισμός"
"networth": "Net Worth",
"budget": "Budget"
},
"grafana": {
"dashboards": "Πίνακας Ελέγχου",
@@ -801,15 +796,15 @@
"numshares": "Κοινόχρηστα στοιχεία"
},
"kopia": {
"status": "Κατάσταση",
"status": "Status",
"size": "Μέγεθος",
"lastrun": "Τελευταία εκτέλεση",
"nextrun": "Επόμενη εκτέλεση",
"failed": "Απέτυχε"
"failed": "Failed"
},
"unmanic": {
"active_workers": "Ενεργοί χρήστες",
"total_workers": "Σύνολο Εργατών",
"total_workers": "Total Workers",
"records_total": "Μήκος Ουράς"
},
"pterodactyl": {
@@ -822,21 +817,21 @@
"targets_total": "Συνολικοί Στόχοι"
},
"gatus": {
"up": "Ιστοσελίδες Πάνω",
"down": "Ιστοσελίδες Κάτω",
"uptime": "Συνεχής λειτουργία"
"up": "Sites Up",
"down": "Sites Down",
"uptime": "Uptime"
},
"ghostfolio": {
"gross_percent_today": "Σήμερα",
"gross_percent_today": "Today",
"gross_percent_1y": "Ένας χρόνος",
"gross_percent_max": "Διαχρονικά",
"net_worth": "Καθαρή αξία"
"net_worth": "Net Worth"
},
"audiobookshelf": {
"podcasts": "Podcasts",
"books": "Βιβλία",
"books": "Books",
"podcastsDuration": "Διάρκεια",
"booksDuration": "Διάρκεια"
"booksDuration": "Duration"
},
"homeassistant": {
"people_home": "Σύνολο ανθρώπων στο σπίτι",
@@ -845,33 +840,33 @@
},
"whatsupdocker": {
"monitoring": "Παρακολούθηση",
"updates": "Ενημερώσεις"
"updates": "Updates"
},
"calibreweb": {
"books": "Βιβλία",
"books": "Books",
"authors": "Συντάκτες",
"categories": "Κατηγορίες",
"series": "Σειρές"
"categories": "Categories",
"series": "Series"
},
"booklore": {
"libraries": "Βιβλιοθήκες",
"books": "Βιβλία",
"reading": "Ανάγνωση",
"finished": "Ολοκληρώθηκε"
"libraries": "Libraries",
"books": "Books",
"reading": "Reading",
"finished": "Finished"
},
"jdownloader": {
"downloadCount": "Αναμονή",
"downloadBytesRemaining": "Απομένουν",
"downloadTotalBytes": "Μέγεθος",
"downloadSpeed": "Ταχύτητα"
"downloadCount": "Queue",
"downloadBytesRemaining": "Remaining",
"downloadTotalBytes": "Size",
"downloadSpeed": "Speed"
},
"kavita": {
"seriesCount": "Σειρές",
"totalFiles": "Αρχεία"
"seriesCount": "Series",
"totalFiles": "Files"
},
"azuredevops": {
"result": "Αποτέλεσμα",
"status": "Κατάσταση",
"status": "Status",
"buildId": "Build ID",
"succeeded": "Πέτυχε",
"notStarted": "Δεν ξεκίνησε",
@@ -956,7 +951,7 @@
"domains": "Domains",
"mailboxes": "Mailboxes",
"mails": "Mails",
"storage": "Αποθηκευτικός χώρος"
"storage": "Storage"
},
"maintainerr": {
"itemsHandled": "Items Handled",
@@ -967,61 +962,61 @@
"reclaimable": "Reclaimable",
"movieReclaimable": "Movie Reclaimable",
"showReclaimable": "Show Reclaimable",
"totalCapacity": "Συνολική χωρητικότητα"
"totalCapacity": "Total Capacity"
},
"netdata": {
"warnings": "Προειδοποιήσεις",
"criticals": "Κρίσιμα"
"warnings": "Warnings",
"criticals": "Criticals"
},
"ntfy": {
"title": "Τίτλος",
"priority": "Προτεραιότητα",
"lastReceived": "Τελευταία Ληφθέντα",
"message": "Μήνυμα",
"tags": "Ετικέτες",
"none": "Κανένα",
"min": "Ελάχιστο",
"low": "Χαμηλό",
"default": "Προεπιλογή",
"high": "Υψηλό",
"urgent": "Επείγον"
"title": "Title",
"priority": "Priority",
"lastReceived": "Last Received",
"message": "Message",
"tags": "Tags",
"none": "None",
"min": "Min",
"low": "Low",
"default": "Default",
"high": "High",
"urgent": "Urgent"
},
"plantit": {
"events": "Συμβάντα",
"plants": "Φυτά",
"photos": "Φωτογραφίες",
"species": "Είδη"
"events": "Events",
"plants": "Plants",
"photos": "Photos",
"species": "Species"
},
"gitea": {
"notifications": "Ειδοποιήσεις",
"issues": "Προβλήματα",
"notifications": "Notifications",
"issues": "Issues",
"pulls": "Pull Requests",
"repositories": "Αποθετήρια"
"repositories": "Repositories"
},
"stash": {
"scenes": "Σκηνές",
"scenesPlayed": "Σκηνές Που Παίχτηκαν",
"playCount": "Συνολικές Αναπαραγωγές",
"playDuration": "Ώρα Παρακολούθησης",
"sceneSize": "Μέγεθος Σκηνών",
"sceneDuration": "Διάρκεια Σκηνών",
"images": "Εικόνες",
"imageSize": "Μέγεθος Εικόνων",
"galleries": "Συλλογές",
"performers": "Καλλιτέχνες",
"studios": "Στούντιο",
"movies": "Ταινίες",
"tags": "Ετικέτες",
"oCount": "Μετρητής O"
"scenes": "Scenes",
"scenesPlayed": "Scenes Played",
"playCount": "Total Plays",
"playDuration": "Time Watched",
"sceneSize": "Scenes Size",
"sceneDuration": "Scenes Duration",
"images": "Images",
"imageSize": "Images Size",
"galleries": "Galleries",
"performers": "Performers",
"studios": "Studios",
"movies": "Movies",
"tags": "Tags",
"oCount": "O Count"
},
"tandoor": {
"users": "Χρήστες",
"recipes": "Συνταγές",
"keywords": "Λέξεις-κλειδιά"
"users": "Users",
"recipes": "Recipes",
"keywords": "Keywords"
},
"homebox": {
"items": "Στοιχεία",
"totalWithWarranty": "Με Εγγύηση",
"items": "Items",
"totalWithWarranty": "With Warranty",
"locations": "Locations",
"labels": "Labels",
"users": "Users",
@@ -1084,72 +1079,72 @@
"vikunja": {
"projects": "Active Projects",
"tasks7d": "Tasks Due This Week",
"tasksOverdue": "Εκπρόθεσμες Εργασίες",
"tasksInProgress": "Εργασίες Σε Εξέλιξη"
"tasksOverdue": "Overdue Tasks",
"tasksInProgress": "Tasks In Progress"
},
"headscale": {
"name": "Όνομα",
"address": "Διεύθυνση",
"last_seen": "Τελευταία Σύνδεση",
"status": "Κατάσταση",
"online": "Σε Σύνδεση",
"offline": "Χωρίς Σύνδεση"
"name": "Name",
"address": "Address",
"last_seen": "Last Seen",
"status": "Status",
"online": "Online",
"offline": "Offline"
},
"beszel": {
"name": "Όνομα",
"systems": "Συστήματα",
"up": "Πάνω",
"down": "Κάτω",
"paused": "Σε παύση",
"pending": "Σε εκκρεμότητα",
"status": "Κατάσταση",
"updated": "Ενημερώθηκε",
"name": "Name",
"systems": "Systems",
"up": "Up",
"down": "Down",
"paused": "Paused",
"pending": "Pending",
"status": "Status",
"updated": "Updated",
"cpu": "CPU",
"memory": "MEM",
"disk": "Δίσκος",
"network": "Δίκτυο"
"disk": "Disk",
"network": "NET"
},
"argocd": {
"apps": "Εφαρμογές",
"synced": "Συγχρονισμένα",
"outOfSync": "Εκτός Συγχρονισμού",
"healthy": "Υγιή",
"apps": "Apps",
"synced": "Synced",
"outOfSync": "Out Of Sync",
"healthy": "Healthy",
"degraded": "Degraded",
"progressing": "Σε εξέλιξη",
"missing": "Λείπουν",
"suspended": "Σε αναστολή"
"progressing": "Progressing",
"missing": "Missing",
"suspended": "Suspended"
},
"spoolman": {
"loading": "Γίνεται φόρτωση"
"loading": "Loading"
},
"gitlab": {
"groups": "Ομάδες",
"issues": "Προβλήματα",
"merges": "Αιτήματα Συγχώνευσης",
"groups": "Groups",
"issues": "Issues",
"merges": "Merge Requests",
"projects": "Projects"
},
"apcups": {
"status": "Κατάσταση",
"load": "Φόρτωση",
"bcharge": "Φόρτιση Μπαταρίας",
"timeleft": "Χρόνος που απομένει"
"status": "Status",
"load": "Load",
"bcharge": "Battery Charge",
"timeleft": "Time Left"
},
"karakeep": {
"bookmarks": "Σελιδοδείκτες",
"favorites": "Αγαπημένα",
"archived": "Αρχειοθετημένα",
"highlights": "Επισημάνσεις",
"lists": "Λίστες",
"tags": "Ετικέτες"
"bookmarks": "Bookmarks",
"favorites": "Favorites",
"archived": "Archived",
"highlights": "Highlights",
"lists": "Lists",
"tags": "Tags"
},
"slskd": {
"slskStatus": "Δίκτυο",
"connected": "Σε σύνδεση",
"disconnected": "Αποσυνδέθηκε",
"updateStatus": "Ενημέρωση",
"update_yes": "Διαθέσιμα",
"update_no": "Ενημερωμένο",
"downloads": "Λήψεις",
"slskStatus": "Network",
"connected": "Connected",
"disconnected": "Disconnected",
"updateStatus": "Update",
"update_yes": "Available",
"update_no": "Up to Date",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Files"
},
+1 -9
View File
@@ -99,12 +99,7 @@
"unhealthy": "Unhealthy",
"not_found": "Not Found",
"exited": "Exited",
"partial": "Partial",
"created": "Created",
"paused": "Paused",
"restarting": "Restarting",
"removing": "Removing",
"dead": "Dead"
"partial": "Partial"
},
"ping": {
"error": "Error",
@@ -1258,8 +1253,5 @@
"wanted": "Wanted",
"queued": "Queued",
"leagues": "Leagues"
},
"feed": {
"noItems": "No items"
}
}
+1 -6
View File
@@ -99,12 +99,7 @@
"unhealthy": "Malsana",
"not_found": "Ne trovita",
"exited": "Eliris",
"partial": "Parta",
"created": "Created",
"paused": "Paused",
"restarting": "Restarting",
"removing": "Removing",
"dead": "Dead"
"partial": "Parta"
},
"ping": {
"error": "Error",
+3 -8
View File
@@ -40,8 +40,8 @@
},
"search": {
"placeholder": "Buscar…",
"provider": "Motor de búsqueda: {{name}}",
"select_provider": "Seleccionar motores de búsqueda"
"provider": "Search engine: {{name}}",
"select_provider": "Select search engine"
},
"resources": {
"cpu": "CPU",
@@ -99,12 +99,7 @@
"unhealthy": "No saludable",
"not_found": "No encontrado",
"exited": "Terminado",
"partial": "Parcial",
"created": "Creado",
"paused": "Pausado",
"restarting": "Reiniciando",
"removing": "Eliminando",
"dead": "Muerto"
"partial": "Parcial"
},
"ping": {
"error": "Error",
+1 -6
View File
@@ -99,12 +99,7 @@
"unhealthy": "Unhealthy",
"not_found": "Not Found",
"exited": "Exited",
"partial": "Partial",
"created": "Created",
"paused": "Paused",
"restarting": "Restarting",
"removing": "Removing",
"dead": "Dead"
"partial": "Partial"
},
"ping": {
"error": "Error",
+1 -6
View File
@@ -99,12 +99,7 @@
"unhealthy": "Unhealthy",
"not_found": "Not Found",
"exited": "Exited",
"partial": "Partial",
"created": "Created",
"paused": "Paused",
"restarting": "Restarting",
"removing": "Removing",
"dead": "Dead"
"partial": "Partial"
},
"ping": {
"error": "Error",
+1 -6
View File
@@ -99,12 +99,7 @@
"unhealthy": "Mauvaise santé",
"not_found": "Inconnu",
"exited": "Arrêté",
"partial": "Partiel",
"created": "Créé",
"paused": "En pause",
"restarting": "Redémarrage",
"removing": "Suppression",
"dead": "Mort"
"partial": "Partiel"
},
"ping": {
"error": "Erreur",
+59 -64
View File
@@ -21,7 +21,7 @@
"seconds": "שניות"
},
"auth": {
"signout": "התנתק"
"signout": "Sign out"
},
"widget": {
"missing_type": "סוג ווידג'ט חסר: {{type}}",
@@ -40,8 +40,8 @@
},
"search": {
"placeholder": "חיפוש…",
"provider": "מנוע חיפוש: {{name}}",
"select_provider": "בחר מנוע חיפוש"
"provider": "Search engine: {{name}}",
"select_provider": "Select search engine"
},
"resources": {
"cpu": "מעבד",
@@ -72,18 +72,18 @@
"empty_data": "מצב תת-מערכת לא ידוע"
},
"unifi_drive": {
"healthy": "בריא",
"degraded": "ירוד",
"no_data": "אין אחסון זמין"
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"duplicati": {
"jobs": "משימות",
"stored": "שמור",
"jobs": "Jobs",
"stored": "Stored",
"lastBackup": "Last Run",
"nextRun": "Next Run",
"running": "Running",
"warnings": "הזהרות",
"errors": "שגיאות"
"warnings": "Warnings",
"errors": "Errors"
},
"docker": {
"rx": "RX",
@@ -99,12 +99,7 @@
"unhealthy": "לא בריא",
"not_found": "לא נמצא",
"exited": "יצא",
"partial": "חלקי",
"created": "Created",
"paused": "Paused",
"restarting": "Restarting",
"removing": "Removing",
"dead": "Dead"
"partial": "חלקי"
},
"ping": {
"error": "שגיאה",
@@ -132,15 +127,15 @@
"songs": "שירים"
},
"jellyfin": {
"playing": "מנגן",
"playing": "Playing",
"transcoding": "Transcoding",
"bitrate": "Bitrate",
"no_active": "אין מדיה פעילה",
"movies": "סרטים",
"series": "סדרות",
"episodes": "פרקים",
"songs": "שירים",
"albums": "אלבומים"
"no_active": "No Active Streams",
"movies": "Movies",
"series": "Series",
"episodes": "Episodes",
"songs": "Songs",
"albums": "Albums"
},
"esphome": {
"offline": "מכובה",
@@ -315,11 +310,11 @@
"available": "זמין"
},
"seerr": {
"pending": "ממתין",
"approved": "מאושר",
"available": "זמין",
"completed": "הסתיים",
"processing": "מעבד",
"pending": "Pending",
"approved": "Approved",
"available": "Available",
"completed": "Completed",
"processing": "Processing",
"issues": "Open Issues"
},
"netalertx": {
@@ -364,16 +359,16 @@
"address": "כתובת",
"expires": "תאריך תפוגה",
"never": "אף פעם",
"user": "משתמש",
"user": "User",
"hostname": "Hostname",
"name": "שם",
"name": "Name",
"client_version": "Client Version",
"os": "מערכת הפעלה",
"created": "יוצר",
"authorized": "מורשה",
"is_external": "האם חיצוני",
"update_available": "עדכון זמין",
"tags": "תגיות",
"os": "OS",
"created": "Created",
"authorized": "Authorized",
"is_external": "Is External",
"update_available": "Update Available",
"tags": "Tags",
"last_seen": "נראה לאחרונה",
"now": "כעת",
"years": "{{number}}y",
@@ -383,8 +378,8 @@
"minutes": "{{number}}m",
"seconds": "{{number}}s",
"ago": "{{value}} Ago",
"true": "כן",
"false": "לא"
"true": "Yes",
"false": "No"
},
"technitium": {
"totalQueries": "שאילתות",
@@ -489,7 +484,7 @@
},
"pulse": {
"nodes": "Nodes",
"vms": "מכונות וירטואליות",
"vms": "VMs",
"lxcs": "LXCs"
},
"glances": {
@@ -589,7 +584,7 @@
"up": "למעלה",
"pending": "ממתין",
"down": "למטה",
"ok": "אוקיי"
"ok": "Ok"
},
"healthchecks": {
"new": "חדש",
@@ -658,9 +653,9 @@
},
"pangolin": {
"orgs": "Orgs",
"sites": "אתרים",
"resources": "משאבים",
"targets": "מטרות",
"sites": "Sites",
"resources": "Resources",
"targets": "Targets",
"traffic": "Traffic",
"in": "In",
"out": "Out"
@@ -762,7 +757,7 @@
"volumeAvailable": "זמין"
},
"dispatcharr": {
"channels": "ערוצים",
"channels": "Channels",
"streams": "Streams"
},
"mylar": {
@@ -1187,9 +1182,9 @@
"nextRenewingSubscription": "תשלום הבא"
},
"unraid": {
"STARTED": "התחיל",
"STOPPED": "נעצר",
"NEW_ARRAY": "מערך חדש",
"STARTED": "Started",
"STOPPED": "Stopped",
"NEW_ARRAY": "New Array",
"RECON_DISK": "Reconstructing Disk",
"DISABLE_DISK": "Disk Disabled",
"SWAP_DSBL": "Swap Disable",
@@ -1201,31 +1196,31 @@
"notifications": "Notifications",
"status": "Status",
"cpu": "CPU",
"memoryUsed": "זיכרון בשימוש",
"memoryAvailable": "זיכרון זמין",
"memoryUsed": "Memory Used",
"memoryAvailable": "Memory Available",
"arrayUsed": "Array Used",
"arrayFree": "Array Free",
"poolUsed": "{{pool}} Used",
"poolFree": "{{pool}} Free"
},
"backrest": {
"num_plans": "תוכניות",
"num_success_30": "הצלחות",
"num_failure_30": "כשלונות",
"num_plans": "Plans",
"num_success_30": "Successes",
"num_failure_30": "Failures",
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "שירים",
"time": "זמן",
"artists": "אומנים"
"songs": "Songs",
"time": "Time",
"artists": "Artists"
},
"arcane": {
"containers": "Containers",
"images": "Images",
"image_updates": "Image Updates",
"images_unused": "לא בשימוש",
"images_unused": "Unused",
"environment_required": "Environment ID Required"
},
"dockhand": {
@@ -1235,7 +1230,7 @@
"memory": "Memory",
"images": "Images",
"volumes": "Volumes",
"events_today": "אירועים היום",
"events_today": "Events Today",
"pending_updates": "Pending Updates",
"stacks": "Stacks",
"paused": "Paused",
@@ -1243,16 +1238,16 @@
"environment_not_found": "Environment Not Found"
},
"sparkyfitness": {
"eaten": "נאכלו",
"burned": "נשרפו",
"remaining": "נותרו",
"steps": "צעדים"
"eaten": "Eaten",
"burned": "Burned",
"remaining": "Remaining",
"steps": "Steps"
},
"syncthing": {
"connected": "Connected",
"synced": "מסונכרן",
"errors": "שגיאות",
"storage": "אחסון"
"synced": "Synced",
"errors": "Errors",
"storage": "Storage"
},
"sportarr": {
"wanted": "Wanted",
+1 -6
View File
@@ -99,12 +99,7 @@
"unhealthy": "Unhealthy",
"not_found": "Not Found",
"exited": "Exited",
"partial": "Partial",
"created": "Created",
"paused": "Paused",
"restarting": "Restarting",
"removing": "Removing",
"dead": "Dead"
"partial": "Partial"
},
"ping": {
"error": "Error",
+15 -20
View File
@@ -21,7 +21,7 @@
"seconds": "s"
},
"auth": {
"signout": "Odjavi se"
"signout": "Sign out"
},
"widget": {
"missing_type": "Nedostajuća vrsta widgeta: {{type}}",
@@ -40,8 +40,8 @@
},
"search": {
"placeholder": "Traži …",
"provider": "Tražilica: {{name}}",
"select_provider": "Odaberi tražilicu"
"provider": "Search engine: {{name}}",
"select_provider": "Select search engine"
},
"resources": {
"cpu": "CPU",
@@ -99,12 +99,7 @@
"unhealthy": "Nefunkcionalno",
"not_found": "Nepronađeno",
"exited": "Zatoreno",
"partial": "Djelomično",
"created": "Created",
"paused": "Paused",
"restarting": "Restarting",
"removing": "Removing",
"dead": "Dead"
"partial": "Djelomično"
},
"ping": {
"error": "Greška",
@@ -140,7 +135,7 @@
"series": "Serije",
"episodes": "Epizode",
"songs": "Pjesme",
"albums": "Albumi"
"albums": "Albums"
},
"esphome": {
"offline": "Offline",
@@ -479,7 +474,7 @@
"users": "Korisnici",
"loginsLast24H": "Prijave (24 h)",
"failedLoginsLast24H": "Neuspjele prijave (24 h)",
"authorizationsLast24H": "Autorizacije (24h)"
"authorizationsLast24H": "Auths (24h)"
},
"proxmox": {
"mem": "MEM",
@@ -959,15 +954,15 @@
"storage": "Spremište"
},
"maintainerr": {
"itemsHandled": "Obrađene stavke",
"episodesHandled": "Obrađene epizode",
"moviesHandled": "Obrađeni filmovi",
"showsHandled": "Obrađene emisije",
"seasonsHandled": "Obrađene sezone",
"itemsHandled": "Items Handled",
"episodesHandled": "Episodes Handled",
"moviesHandled": "Movies Handled",
"showsHandled": "Shows Handled",
"seasonsHandled": "Seasons Handled",
"reclaimable": "Reclaimable",
"movieReclaimable": "Movie Reclaimable",
"showReclaimable": "Show Reclaimable",
"totalCapacity": "Ukupni kapacitet"
"totalCapacity": "Total Capacity"
},
"netdata": {
"warnings": "Upozorenja",
@@ -1255,8 +1250,8 @@
"storage": "Spremište"
},
"sportarr": {
"wanted": "Željeno",
"queued": "U redu čekanja",
"leagues": "Lige"
"wanted": "Wanted",
"queued": "Queued",
"leagues": "Leagues"
}
}
+1 -6
View File
@@ -99,12 +99,7 @@
"unhealthy": "Egészségtelen",
"not_found": "Nem található",
"exited": "Kilépett",
"partial": "Részleges",
"created": "Created",
"paused": "Paused",
"restarting": "Restarting",
"removing": "Removing",
"dead": "Dead"
"partial": "Részleges"
},
"ping": {
"error": "Hiba",
+1 -6
View File
@@ -99,12 +99,7 @@
"unhealthy": "Tidak Lancar",
"not_found": "Tidak Ditemukan",
"exited": "Terkeluar",
"partial": "Sebagian",
"created": "Created",
"paused": "Paused",
"restarting": "Restarting",
"removing": "Removing",
"dead": "Dead"
"partial": "Sebagian"
},
"ping": {
"error": "Error",
+1 -6
View File
@@ -99,12 +99,7 @@
"unhealthy": "Non sano",
"not_found": "Non trovato",
"exited": "Uscito",
"partial": "Parziale",
"created": "Created",
"paused": "Paused",
"restarting": "Restarting",
"removing": "Removing",
"dead": "Dead"
"partial": "Parziale"
},
"ping": {
"error": "Errore",
+147 -152
View File
@@ -39,9 +39,9 @@
"wait": "お待ちください"
},
"search": {
"placeholder": "検索 ...",
"placeholder": "検索…",
"provider": "Search engine: {{name}}",
"select_provider": "検索エンジンを選択"
"select_provider": "Select search engine"
},
"resources": {
"cpu": "CPU",
@@ -99,12 +99,7 @@
"unhealthy": "非健全",
"not_found": "不明",
"exited": "停止しました",
"partial": "部分的",
"created": "Created",
"paused": "Paused",
"restarting": "Restarting",
"removing": "Removing",
"dead": "Dead"
"partial": "部分的"
},
"ping": {
"error": "エラー",
@@ -132,18 +127,18 @@
"songs": "曲"
},
"jellyfin": {
"playing": "再生中",
"transcoding": "変換中",
"bitrate": "ビットレート",
"no_active": "アクティブ・ストリーム無し",
"movies": "映画",
"series": "シリーズ",
"episodes": "エピソード",
"songs": "曲",
"albums": "アルバム"
"playing": "Playing",
"transcoding": "Transcoding",
"bitrate": "Bitrate",
"no_active": "No Active Streams",
"movies": "Movies",
"series": "Series",
"episodes": "Episodes",
"songs": "Songs",
"albums": "Albums"
},
"esphome": {
"offline": "オフライン ",
"offline": "",
"offline_alt": "オフライン",
"online": "オンライン",
"total": "Total",
@@ -241,18 +236,18 @@
},
"rutorrent": {
"active": "アクティブ",
"upload": "アップロード",
"download": "ダウンロード"
"upload": "Upload",
"download": "Download"
},
"transmission": {
"download": "ダウンロード",
"upload": "アップロード",
"download": "Download",
"upload": "Upload",
"leech": "Leech",
"seed": "Seed"
},
"qbittorrent": {
"download": "ダウンロード",
"upload": "アップロード",
"download": "Download",
"upload": "Upload",
"leech": "Leech",
"seed": "Seed"
},
@@ -285,7 +280,7 @@
"queued": "待機中",
"series": "Series",
"queue": "Queue",
"unknown": "不明"
"unknown": "Unknown"
},
"radarr": {
"wanted": "Wanted",
@@ -365,15 +360,15 @@
"expires": "失効",
"never": "なし",
"user": "User",
"hostname": "ホスト名",
"name": "名前",
"client_version": "クライアントバージョン",
"hostname": "Hostname",
"name": "Name",
"client_version": "Client Version",
"os": "OS",
"created": "作成済み",
"authorized": "承認済み",
"created": "Created",
"authorized": "Authorized",
"is_external": "Is External",
"update_available": "アップデートが利用できます",
"tags": "タグ",
"update_available": "Update Available",
"tags": "Tags",
"last_seen": "最終日時",
"now": "現在",
"years": "{{number}}年",
@@ -383,11 +378,11 @@
"minutes": "{{number}}分",
"seconds": "{{number}}秒",
"ago": "{{value}} 前",
"true": "はい",
"false": "いいえ"
"true": "Yes",
"false": "No"
},
"technitium": {
"totalQueries": "クエリ",
"totalQueries": "Queries",
"totalNoError": "成功",
"totalServerFailure": "失敗",
"totalNxDomain": "NXドメイン",
@@ -395,7 +390,7 @@
"totalAuthoritative": "正式",
"totalRecursive": "再帰的",
"totalCached": "キャッシュ済み",
"totalBlocked": "ブロック中",
"totalBlocked": "Blocked",
"totalDropped": "ドロップ済み",
"totalClients": "クライアント"
},
@@ -411,13 +406,13 @@
"middleware": "ミドルウェア"
},
"trilium": {
"version": "バージョン",
"notesCount": "メモ",
"dbSize": "データベースサイズ",
"unknown": "不明"
"version": "Version",
"notesCount": "Notes",
"dbSize": "Database Size",
"unknown": "Unknown"
},
"navidrome": {
"nothing_streaming": "アクティブストリーム無し",
"nothing_streaming": "No Active Streams",
"please_wait": "お待ちください"
},
"npm": {
@@ -482,7 +477,7 @@
"authorizationsLast24H": "Auths (24h)"
},
"proxmox": {
"mem": "メモリ",
"mem": "MEM",
"cpu": "CPU",
"lxc": "LXC",
"vms": "VM"
@@ -494,17 +489,17 @@
},
"glances": {
"cpu": "CPU",
"load": "負荷",
"wait": "お待ちください",
"temp": "温度",
"load": "Load",
"wait": "Please wait",
"temp": "TEMP",
"_temp": "温度",
"warn": "警告",
"uptime": "稼働時間",
"total": "合計",
"free": "空き容量",
"used": "使用済み",
"days": "日",
"hours": "時",
"uptime": "UP",
"total": "Total",
"free": "Free",
"used": "Used",
"days": "d",
"hours": "h",
"crit": "クリティカル",
"read": "Read",
"write": "書き込み",
@@ -545,39 +540,39 @@
"57-day": "着氷性の小雨",
"57-night": "Freezing Drizzle",
"61-day": "小雨",
"61-night": "小雨",
"61-night": "Light Rain",
"63-day": "雨",
"63-night": "雨",
"63-night": "Rain",
"65-day": "大雨",
"65-night": "大雨",
"65-night": "Heavy Rain",
"66-day": "着氷性の雨",
"66-night": "着氷性の雨",
"67-day": "着氷性の雨",
"67-night": "着氷性の雨",
"66-night": "Freezing Rain",
"67-day": "Freezing Rain",
"67-night": "Freezing Rain",
"71-day": "小雪",
"71-night": "小雪",
"71-night": "Light Snow",
"73-day": "雪",
"73-night": "雪",
"73-night": "Snow",
"75-day": "大雪",
"75-night": "Heavy Snow",
"77-day": "霧雪",
"77-night": "Snow Grains",
"80-day": "弱いにわか雨",
"80-night": "弱いにわか雨",
"80-night": "Light Showers",
"81-day": "にわか雨",
"81-night": "にわか雨",
"81-night": "Showers",
"82-day": "強いにわか雨",
"82-night": "強いにわか雨",
"82-night": "Heavy Showers",
"85-day": "にわか雪",
"85-night": "にわか雪",
"86-day": "にわか雪",
"86-night": "にわか雪",
"85-night": "Snow Showers",
"86-day": "Snow Showers",
"86-night": "Snow Showers",
"95-day": "雷雨",
"95-night": "雷雨",
"95-night": "Thunderstorm",
"96-day": "雷雨・ひょう",
"96-night": "雹を伴う雷雨",
"99-day": "雹を伴う雷雨",
"99-night": "雹を伴う雷雨"
"96-night": "Thunderstorm With Hail",
"99-day": "Thunderstorm With Hail",
"99-night": "Thunderstorm With Hail"
},
"homebridge": {
"available_update": "システム",
@@ -645,7 +640,7 @@
"signalQuality": "クオリティ",
"symbolQuality": "Quality",
"networkRate": "Bitrate",
"clientIP": "クライアントIP"
"clientIP": "クライアント IP"
},
"scrutiny": {
"passed": "合格",
@@ -661,7 +656,7 @@
"sites": "Sites",
"resources": "Resources",
"targets": "Targets",
"traffic": "トラフィック",
"traffic": "Traffic",
"in": "In",
"out": "Out"
},
@@ -674,7 +669,7 @@
"low_battery": "バッテリー残量低下"
},
"nextdns": {
"wait": "お待ちください",
"wait": "Please Wait",
"no_devices": "デバイス データを受信していません"
},
"mikrotik": {
@@ -695,7 +690,7 @@
"limit": "リミット"
},
"opnsense": {
"cpu": "CPU負荷",
"cpu": "CPU Load",
"memory": "アクティブ・メモリ",
"wanUpload": "WANアップロード",
"wanDownload": "WANダウンロード"
@@ -707,14 +702,14 @@
"layers": "レイヤー"
},
"octoprint": {
"printer_state": "ステータス",
"printer_state": "Status",
"temp_tool": "ツール温度",
"temp_bed": "ベッド温度",
"job_completion": "完了"
},
"cloudflared": {
"origin_ip": "オリジンIP",
"status": "ステータス"
"status": "Status"
},
"pfsense": {
"load": "読み込み平均",
@@ -733,17 +728,17 @@
"memory_usage": "メモリ"
},
"immich": {
"users": "ユーザ",
"users": "Users",
"photos": "写真",
"videos": "ビデオ",
"videos": "Videos",
"storage": "ストレージ"
},
"uptimekuma": {
"up": "サイトUp",
"down": "サイトDown",
"uptime": "稼働時間",
"uptime": "Uptime",
"incident": "事件",
"m": "分"
"m": "m"
},
"atsumeru": {
"series": "Series",
@@ -772,8 +767,8 @@
},
"photoprism": {
"albums": "Albums",
"photos": "写真",
"videos": "動画",
"photos": "Photos",
"videos": "Videos",
"people": "人"
},
"fileflows": {
@@ -884,12 +879,12 @@
},
"gamedig": {
"status": "Status",
"online": "オンライン",
"offline": "オフライン ",
"online": "Online",
"offline": "Offline",
"name": "名前",
"map": "マップ",
"currentPlayers": "現在のプレーヤー",
"players": "プレイヤー",
"players": "Players",
"maxPlayers": "最大プレイヤー数",
"bots": "ボット",
"ping": "Ping"
@@ -902,21 +897,21 @@
},
"mealie": {
"recipes": "レシピ",
"users": "ユーザ",
"categories": "カテゴリ",
"users": "Users",
"categories": "Categories",
"tags": "タグ"
},
"openmediavault": {
"downloading": "ダウンロード中",
"total": "合計",
"running": "実行中",
"stopped": "停止中",
"total": "Total",
"running": "Running",
"stopped": "Stopped",
"passed": "Passed",
"failed": "Failed"
},
"openwrt": {
"uptime": "Uptime",
"cpuLoad": "CPU 平均負荷(5分)",
"cpuLoad": "CPU 平均負荷(5 分)",
"up": "Up",
"down": "Down",
"bytesTx": "送信済み",
@@ -924,7 +919,7 @@
},
"uptimerobot": {
"status": "Status",
"uptime": "稼働時間",
"uptime": "Uptime",
"lastDown": "最後のダウンタイム",
"downDuration": "ダウンタイム時間",
"sitesUp": "Sites Up",
@@ -954,9 +949,9 @@
},
"mailcow": {
"domains": "Domains",
"mailboxes": "受信箱",
"mails": "メール",
"storage": "ストレージ"
"mailboxes": "Mailboxes",
"mails": "Mails",
"storage": "Storage"
},
"maintainerr": {
"itemsHandled": "Items Handled",
@@ -974,7 +969,7 @@
"criticals": "重大"
},
"ntfy": {
"title": "タイトル",
"title": "Title",
"priority": "Priority",
"lastReceived": "Last Received",
"message": "Message",
@@ -989,7 +984,7 @@
"plantit": {
"events": "イベント",
"plants": "植物",
"photos": "写真",
"photos": "Photos",
"species": "種"
},
"gitea": {
@@ -1137,27 +1132,27 @@
"karakeep": {
"bookmarks": "Bookmarks",
"favorites": "Favorites",
"archived": "アーカイブ済み",
"highlights": "ハイライト",
"lists": "リスト",
"tags": "タグ"
"archived": "Archived",
"highlights": "Highlights",
"lists": "Lists",
"tags": "Tags"
},
"slskd": {
"slskStatus": "ネットワーク",
"connected": "接続されています",
"disconnected": "切断されています",
"updateStatus": "更新",
"update_yes": "利用可能",
"update_no": "最新",
"downloads": "ダウンロード",
"uploads": "アップロード",
"sharedFiles": "ファイル"
"slskStatus": "Network",
"connected": "Connected",
"disconnected": "Disconnected",
"updateStatus": "Update",
"update_yes": "Available",
"update_no": "Up to Date",
"downloads": "Downloads",
"uploads": "Uploads",
"sharedFiles": "Files"
},
"jellystat": {
"songs": "曲",
"movies": "映画",
"episodes": "エピソード",
"other": "その他"
"songs": "Songs",
"movies": "Movies",
"episodes": "Episodes",
"other": "Other"
},
"checkmk": {
"serviceErrors": "Service issues",
@@ -1198,11 +1193,11 @@
"TOO_MANY_MISSING_DISKS": "Too Many Missing Disks",
"NEW_DISK_TOO_SMALL": "New Disk Too Small",
"NO_DATA_DISKS": "No Data Disks",
"notifications": "通知",
"status": "ステータス",
"notifications": "Notifications",
"status": "Status",
"cpu": "CPU",
"memoryUsed": "メモリ使用量",
"memoryAvailable": "使用可能なメモリ",
"memoryUsed": "Memory Used",
"memoryAvailable": "Memory Available",
"arrayUsed": "Array Used",
"arrayFree": "Array Free",
"poolUsed": "{{pool}} Used",
@@ -1210,53 +1205,53 @@
},
"backrest": {
"num_plans": "Plans",
"num_success_30": "成功",
"num_failure_30": "失敗",
"num_success_latest": "成功",
"num_failure_latest": "失敗",
"bytes_added_30": "バイトが追加されました"
"num_success_30": "Successes",
"num_failure_30": "Failures",
"num_success_latest": "Succeeding",
"num_failure_latest": "Failing",
"bytes_added_30": "Bytes Added"
},
"yourspotify": {
"songs": "曲",
"time": "時間",
"artists": "アーティスト"
"songs": "Songs",
"time": "Time",
"artists": "Artists"
},
"arcane": {
"containers": "コンテナ",
"images": "イメージ",
"image_updates": "イメージの更新",
"images_unused": "未使用",
"environment_required": "Enviroment ID が必要です"
"containers": "Containers",
"images": "Images",
"image_updates": "Image Updates",
"images_unused": "Unused",
"environment_required": "Environment ID Required"
},
"dockhand": {
"running": "実行中",
"stopped": "停止中",
"running": "Running",
"stopped": "Stopped",
"cpu": "CPU",
"memory": "メモリ",
"images": "イメージ",
"volumes": "ボリューム",
"events_today": "今日のイベント",
"pending_updates": "保留中の更新",
"stacks": "スタック数",
"paused": "一時停止中",
"total": "合計",
"environment_not_found": "環境が見つかりません"
"memory": "Memory",
"images": "Images",
"volumes": "Volumes",
"events_today": "Events Today",
"pending_updates": "Pending Updates",
"stacks": "Stacks",
"paused": "Paused",
"total": "Total",
"environment_not_found": "Environment Not Found"
},
"sparkyfitness": {
"eaten": "摂取カロリー",
"burned": "消費カロリー",
"remaining": "残り",
"steps": "ステップ"
"eaten": "Eaten",
"burned": "Burned",
"remaining": "Remaining",
"steps": "Steps"
},
"syncthing": {
"connected": "接続されています",
"synced": "同期しました",
"errors": "エラー",
"storage": "ストレージ"
"connected": "Connected",
"synced": "Synced",
"errors": "Errors",
"storage": "Storage"
},
"sportarr": {
"wanted": "募集中",
"queued": "処理待ち",
"leagues": "リーグ"
"wanted": "Wanted",
"queued": "Queued",
"leagues": "Leagues"
}
}
+3 -8
View File
@@ -40,8 +40,8 @@
},
"search": {
"placeholder": "검색…",
"provider": "검색 엔진: {{name}}",
"select_provider": "검색 엔진 선택"
"provider": "Search engine: {{name}}",
"select_provider": "Select search engine"
},
"resources": {
"cpu": "CPU",
@@ -99,12 +99,7 @@
"unhealthy": "비정상",
"not_found": "찾을 수 없음",
"exited": "종료됨",
"partial": "부분적",
"created": "Created",
"paused": "Paused",
"restarting": "Restarting",
"removing": "Removing",
"dead": "Dead"
"partial": "부분적"
},
"ping": {
"error": "오류",
+1 -6
View File
@@ -99,12 +99,7 @@
"unhealthy": "Unhealthy",
"not_found": "Not Found",
"exited": "Exited",
"partial": "Partial",
"created": "Created",
"paused": "Paused",
"restarting": "Restarting",
"removing": "Removing",
"dead": "Dead"
"partial": "Partial"
},
"ping": {
"error": "Error",
+1 -6
View File
@@ -99,12 +99,7 @@
"unhealthy": "Kurang sihat",
"not_found": "Tidak dijumpai",
"exited": "Dimatikan",
"partial": "Sebahagian",
"created": "Created",
"paused": "Paused",
"restarting": "Restarting",
"removing": "Removing",
"dead": "Dead"
"partial": "Sebahagian"
},
"ping": {
"error": "Error",
+1 -6
View File
@@ -99,12 +99,7 @@
"unhealthy": "Ongezond",
"not_found": "Niet Gevonden",
"exited": "Gestopt",
"partial": "Gedeeltelijk",
"created": "Created",
"paused": "Paused",
"restarting": "Restarting",
"removing": "Removing",
"dead": "Dead"
"partial": "Gedeeltelijk"
},
"ping": {
"error": "Fout",
+1 -6
View File
@@ -99,12 +99,7 @@
"unhealthy": "Usunn",
"not_found": "Not Found",
"exited": "Exited",
"partial": "Delvis",
"created": "Created",
"paused": "Paused",
"restarting": "Restarting",
"removing": "Removing",
"dead": "Dead"
"partial": "Delvis"
},
"ping": {
"error": "Error",
+1 -6
View File
@@ -99,12 +99,7 @@
"unhealthy": "Niezdrowy",
"not_found": "Nie znaleziono",
"exited": "Zakończony",
"partial": "Częściowy",
"created": "Created",
"paused": "Paused",
"restarting": "Restarting",
"removing": "Removing",
"dead": "Dead"
"partial": "Częściowy"
},
"ping": {
"error": "Błąd",
+1 -6
View File
@@ -99,12 +99,7 @@
"unhealthy": "Não-saudável",
"not_found": "Não Encontrado",
"exited": "Saiu",
"partial": "Parcial",
"created": "Created",
"paused": "Paused",
"restarting": "Restarting",
"removing": "Removing",
"dead": "Dead"
"partial": "Parcial"
},
"ping": {
"error": "Erro",
+17 -22
View File
@@ -21,7 +21,7 @@
"seconds": "s"
},
"auth": {
"signout": "Encerrar Sessão"
"signout": "Sign out"
},
"widget": {
"missing_type": "Tipo de Widget ausente: {{type}}",
@@ -40,8 +40,8 @@
},
"search": {
"placeholder": "Pesquisar…",
"provider": "Motor de pesquisa: {{name}}",
"select_provider": "Selecionar motor de busca"
"provider": "Search engine: {{name}}",
"select_provider": "Select search engine"
},
"resources": {
"cpu": "CPU",
@@ -99,12 +99,7 @@
"unhealthy": "Não-saudável",
"not_found": "Não Encontrado",
"exited": "Encerrado",
"partial": "Parcial",
"created": "Criado",
"paused": "Pausado",
"restarting": "Reiniciando",
"removing": "Removendo",
"dead": "Inativo"
"partial": "Parcial"
},
"ping": {
"error": "Erro",
@@ -140,7 +135,7 @@
"series": "Séries",
"episodes": "Episódios",
"songs": "Músicas",
"albums": "Álbuns"
"albums": "Albums"
},
"esphome": {
"offline": "Offline",
@@ -959,15 +954,15 @@
"storage": "Armazenamento"
},
"maintainerr": {
"itemsHandled": "Itens manuseados",
"episodesHandled": "Episódios tratados",
"moviesHandled": "Filmes tratados",
"showsHandled": "Séries tratados",
"seasonsHandled": "Temporadas tratadas",
"reclaimable": "Reclamável",
"movieReclaimable": "Filme Reclamável",
"showReclaimable": "Show Reclamável",
"totalCapacity": "Capacidade total"
"itemsHandled": "Items Handled",
"episodesHandled": "Episodes Handled",
"moviesHandled": "Movies Handled",
"showsHandled": "Shows Handled",
"seasonsHandled": "Seasons Handled",
"reclaimable": "Reclaimable",
"movieReclaimable": "Movie Reclaimable",
"showReclaimable": "Show Reclaimable",
"totalCapacity": "Total Capacity"
},
"netdata": {
"warnings": "Alertas",
@@ -1255,8 +1250,8 @@
"storage": "Armazenagem"
},
"sportarr": {
"wanted": "Desejado",
"queued": "Na fila",
"leagues": "Ligas"
"wanted": "Wanted",
"queued": "Queued",
"leagues": "Leagues"
}
}
+1 -6
View File
@@ -99,12 +99,7 @@
"unhealthy": "Nesănătos",
"not_found": "Negăsit",
"exited": "Ieşit",
"partial": "Parțial",
"created": "Created",
"paused": "Paused",
"restarting": "Restarting",
"removing": "Removing",
"dead": "Dead"
"partial": "Parțial"
},
"ping": {
"error": "Error",
+1 -6
View File
@@ -99,12 +99,7 @@
"unhealthy": "Нездоровый",
"not_found": "Не найдено",
"exited": "Вышел",
"partial": "Частичный",
"created": "Создан",
"paused": "Приостановлен",
"restarting": "Перезапуск",
"removing": "Удаление",
"dead": "Мёртв"
"partial": "Частичный"
},
"ping": {
"error": "Ошибка",
+3 -8
View File
@@ -40,8 +40,8 @@
},
"search": {
"placeholder": "Hľadať…",
"provider": "Vyhľadávač: {{name}}",
"select_provider": "Vyberte vyhľadávač"
"provider": "Search engine: {{name}}",
"select_provider": "Select search engine"
},
"resources": {
"cpu": "CPU",
@@ -99,12 +99,7 @@
"unhealthy": "Nezdravý",
"not_found": "Nenájdené",
"exited": "Ukončené",
"partial": "Čiastočný",
"created": "Vytvorené",
"paused": "Pozastavené",
"restarting": "Reštartuje sa",
"removing": "Odstraňuje sa",
"dead": "Mŕtve"
"partial": "Čiastočný"
},
"ping": {
"error": "Chyba",
+1 -6
View File
@@ -99,12 +99,7 @@
"unhealthy": "Ni zdrav",
"not_found": "Ni najden",
"exited": "V izhodu",
"partial": "Delni",
"created": "Created",
"paused": "Paused",
"restarting": "Restarting",
"removing": "Removing",
"dead": "Dead"
"partial": "Delni"
},
"ping": {
"error": "Error",
+1 -6
View File
@@ -99,12 +99,7 @@
"unhealthy": "Нездравих",
"not_found": "Није пронађено",
"exited": "Напуштених",
"partial": "Делимично",
"created": "Created",
"paused": "Paused",
"restarting": "Restarting",
"removing": "Removing",
"dead": "Dead"
"partial": "Делимично"
},
"ping": {
"error": "Грешка",
+1 -6
View File
@@ -99,12 +99,7 @@
"unhealthy": "Unhealthy",
"not_found": "Not Found",
"exited": "Exited",
"partial": "Partial",
"created": "Created",
"paused": "Paused",
"restarting": "Restarting",
"removing": "Removing",
"dead": "Dead"
"partial": "Partial"
},
"ping": {
"error": "Error",
+1 -6
View File
@@ -99,12 +99,7 @@
"unhealthy": "Unhealthy",
"not_found": "Not Found",
"exited": "Exited",
"partial": "Partial",
"created": "Created",
"paused": "Paused",
"restarting": "Restarting",
"removing": "Removing",
"dead": "Dead"
"partial": "Partial"
},
"ping": {
"error": "Error",
+1 -6
View File
@@ -99,12 +99,7 @@
"unhealthy": "Unhealthy",
"not_found": "Not Found",
"exited": "Exited",
"partial": "Partial",
"created": "Created",
"paused": "Paused",
"restarting": "Restarting",
"removing": "Removing",
"dead": "Dead"
"partial": "Partial"
},
"ping": {
"error": "Error",
+1 -6
View File
@@ -99,12 +99,7 @@
"unhealthy": "Sağlıksız",
"not_found": "Bulunamadı",
"exited": "Kapandı",
"partial": "Kısmi",
"created": "Created",
"paused": "Paused",
"restarting": "Restarting",
"removing": "Removing",
"dead": "Dead"
"partial": "Kısmi"
},
"ping": {
"error": "Hata",
+1 -6
View File
@@ -99,12 +99,7 @@
"unhealthy": "Нездоровий",
"not_found": "Не знайдено",
"exited": "Вийшов",
"partial": "Частковий",
"created": "Created",
"paused": "Paused",
"restarting": "Restarting",
"removing": "Removing",
"dead": "Dead"
"partial": "Частковий"
},
"ping": {
"error": "Помилка",
+201 -206
View File
@@ -21,7 +21,7 @@
"seconds": "giây"
},
"auth": {
"signout": "Đăng xuất"
"signout": "Sign out"
},
"widget": {
"missing_type": "Thiếu loại Widget: {{type}}",
@@ -40,8 +40,8 @@
},
"search": {
"placeholder": "Tìm kiếm…",
"provider": "Công cụ tìm kiếm: {{name}}",
"select_provider": "Chọn công cụ tìm kiếm"
"provider": "Search engine: {{name}}",
"select_provider": "Select search engine"
},
"resources": {
"cpu": "CPU",
@@ -50,7 +50,7 @@
"free": "Dư",
"used": "Đã dùng",
"load": "\n",
"temp": "Tạm thời",
"temp": "TEMP",
"max": "Tối đa",
"uptime": "UP"
},
@@ -66,24 +66,24 @@
"wlan_devices": "Thiết bị trong mạng WLAN",
"lan_users": "Người dùng mạng LAN",
"wlan_users": "Người dùng mạng WLAN",
"up": "Hoạt động",
"down": "Không hoạt động",
"up": "UP",
"down": "DOWN",
"wait": "Vui lòng chờ",
"empty_data": "Trạng thái hệ thống phụ không xác định"
},
"unifi_drive": {
"healthy": "Bình thường",
"degraded": "Suy giảm",
"no_data": "Không có dữ liệu"
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"duplicati": {
"jobs": "Tác vụ",
"stored": "Đã lưu",
"lastBackup": "Lần chạy cuối",
"nextRun": "Lần chạy tiếp theo",
"running": "Đang chạy",
"warnings": "Cảnh báo",
"errors": "Lỗi"
"jobs": "Jobs",
"stored": "Stored",
"lastBackup": "Last Run",
"nextRun": "Next Run",
"running": "Running",
"warnings": "Warnings",
"errors": "Errors"
},
"docker": {
"rx": "RX",
@@ -98,13 +98,8 @@
"starting": "Đang bắt đầu",
"unhealthy": "Bất thường",
"not_found": "Không tìm thấy",
"exited": "Đã thoát",
"partial": "Một phần",
"created": "Created",
"paused": "Paused",
"restarting": "Restarting",
"removing": "Removing",
"dead": "Dead"
"exited": "Exited",
"partial": "Partial"
},
"ping": {
"error": "Lỗi",
@@ -125,35 +120,35 @@
"playing": "Đang chơi",
"transcoding": "Chuyển định dạng",
"bitrate": "Bitrate",
"no_active": "Không có luồng đang hoạt động",
"no_active": "No Active Streams",
"movies": "Phim ảnh",
"series": "Series",
"episodes": "Tập",
"episodes": "Episodes",
"songs": "Bài hát"
},
"jellyfin": {
"playing": "Đang phát",
"transcoding": "Đang chuyển mã",
"bitrate": "Tốc độ bit",
"no_active": "Không có luồng đang hoạt động",
"movies": "Phim",
"playing": "Playing",
"transcoding": "Transcoding",
"bitrate": "Bitrate",
"no_active": "No Active Streams",
"movies": "Movies",
"series": "Series",
"episodes": "Tập",
"songs": "Bài hát",
"albums": "Album"
"episodes": "Episodes",
"songs": "Songs",
"albums": "Albums"
},
"esphome": {
"offline": "Offline",
"offline_alt": "Offline",
"online": "Online",
"total": "Tổng cộng",
"total": "Total",
"unknown": "Không xác định"
},
"evcc": {
"pv_power": "Công suất phát",
"pv_power": "Production",
"battery_soc": "Pin",
"grid_power": "Lưới",
"home_power": "Công suất tiêu thụ",
"home_power": "Consumption",
"charge_power": "Bộ sạc",
"kilowatt": "kW"
},
@@ -161,7 +156,7 @@
"download": "Tải xuống",
"upload": "Tải lên",
"leech": "",
"seed": "Seeder"
"seed": "Seed"
},
"freshrss": {
"subscriptions": "Đăng ký",
@@ -180,115 +175,115 @@
"maxDown": "Tải xuống tối đa",
"maxUp": "Tải lên tối đa",
"down": "Down",
"up": "Tải lên",
"received": "Đã nhận",
"sent": "Đã gửi",
"externalIPAddress": "Địa chỉ IP bên ngoài",
"externalIPv6Address": "Địa chỉ IPv6 bên ngoài",
"externalIPv6Prefix": "Tiền tố IPv6 bên ngoài"
"up": "Up",
"received": "Received",
"sent": "Sent",
"externalIPAddress": "Ext. IP",
"externalIPv6Address": "Ext. IPv6",
"externalIPv6Prefix": "Ext. IPv6-Prefix"
},
"caddy": {
"upstreams": "Máy chủ backend",
"requests": "Yêu cầu hiện tại",
"requests_failed": "Yêu cầu thất bại"
"upstreams": "Upstreams",
"requests": "Current requests",
"requests_failed": "Failed requests"
},
"changedetectionio": {
"totalObserved": "Tổng số đã theo dõi",
"diffsDetected": "Số thay đổi đã phát hiện"
"totalObserved": "Total Observed",
"diffsDetected": "Diffs Detected"
},
"channelsdvrserver": {
"shows": "Chương trình",
"recordings": "Bản ghi",
"scheduled": "Đã lên lịch",
"passes": "Quy tắc ghi"
"shows": "Shows",
"recordings": "Recordings",
"scheduled": "Scheduled",
"passes": "Passes"
},
"tautulli": {
"playing": "Đang phát",
"transcoding": "Đang chuyển mã",
"bitrate": "Tốc độ bit",
"no_active": "Không có luồng đang hoạt động",
"plex_connection_error": "Kiểm tra kết nối Plex"
"playing": "Playing",
"transcoding": "Transcoding",
"bitrate": "Bitrate",
"no_active": "No Active Streams",
"plex_connection_error": "Check Plex Connection"
},
"tracearr": {
"no_active": "Không có luồng đang hoạt động",
"streams": "Luồng",
"transcodes": "Luồng chuyển mã",
"directplay": "Phát trực tiếp",
"bitrate": "Tốc độ bit"
"no_active": "No Active Streams",
"streams": "Streams",
"transcodes": "Transcodes",
"directplay": "Direct Play",
"bitrate": "Bitrate"
},
"omada": {
"connectedAp": "AP đã kết nối",
"activeUser": "Thiết bị đang hoạt động",
"alerts": "Cảnh báo",
"connectedGateways": "Gateway đã kết nối",
"connectedSwitches": "Switch đã kết nối"
"connectedAp": "Connected APs",
"activeUser": "Active devices",
"alerts": "Alerts",
"connectedGateways": "Connected gateways",
"connectedSwitches": "Connected switches"
},
"nzbget": {
"rate": "Tốc độ",
"remaining": "Còn lại",
"rate": "Rate",
"remaining": "Remaining",
"downloaded": "Đã tải"
},
"plex": {
"streams": "Luồng đang hoạt động",
"albums": "Album",
"movies": "Phim",
"tv": "Chương trình TV"
"streams": "Active Streams",
"albums": "Albums",
"movies": "Movies",
"tv": "TV Shows"
},
"sabnzbd": {
"rate": "Tốc độ",
"rate": "Rate",
"queue": "Hàng chờ",
"timeleft": "Thời gian còn lại"
},
"rutorrent": {
"active": "Hoạt động",
"upload": "Tải lên",
"download": "Tải xuống"
"upload": "Upload",
"download": "Download"
},
"transmission": {
"download": "Tải xuống",
"upload": "Tải lên",
"leech": "Leecher",
"seed": "Seeder"
"download": "Download",
"upload": "Upload",
"leech": "Leech",
"seed": "Seed"
},
"qbittorrent": {
"download": "Tải xuống",
"upload": "Tải lên",
"leech": "Leecher",
"seed": "Seeder"
"download": "Download",
"upload": "Upload",
"leech": "Leech",
"seed": "Seed"
},
"qnap": {
"cpuUsage": "Mức sử dụng CPU",
"memUsage": "Mức sử dụng bộ nhớ",
"systemTempC": "Nhiệt độ hệ thống",
"poolUsage": "Mức sử dụng pool",
"volumeUsage": "Mức sử dụng volume",
"invalid": "Không hợp lệ"
"cpuUsage": "CPU Usage",
"memUsage": "MEM Usage",
"systemTempC": "System Temp",
"poolUsage": "Pool Usage",
"volumeUsage": "Volume Usage",
"invalid": "Invalid"
},
"deluge": {
"download": "Tải xuống",
"upload": "Tải lên",
"leech": "Leecher",
"seed": "Seeder"
"download": "Download",
"upload": "Upload",
"leech": "Leech",
"seed": "Seed"
},
"develancacheui": {
"cachehitbytes": "Dung lượng cache hit",
"cachemissbytes": "Dung lượng cache miss"
"cachehitbytes": "Cache Hit Bytes",
"cachemissbytes": "Cache Miss Bytes"
},
"downloadstation": {
"download": "Tải xuống",
"upload": "Tải lên",
"leech": "Leecher",
"seed": "Seeder"
"download": "Download",
"upload": "Upload",
"leech": "Leech",
"seed": "Seed"
},
"sonarr": {
"wanted": "Cần tải",
"wanted": "Wanted",
"queued": "Queued",
"series": "Phim bộ",
"series": "Series",
"queue": "Queue",
"unknown": "Unknown"
},
"radarr": {
"wanted": "Cần tải",
"wanted": "Wanted",
"missing": "Missing",
"queued": "Queued",
"movies": "Movies",
@@ -296,12 +291,12 @@
"unknown": "Unknown"
},
"lidarr": {
"wanted": "Cần tải",
"wanted": "Wanted",
"queued": "Queued",
"artists": "Artists"
},
"readarr": {
"wanted": "Cần tải",
"wanted": "Wanted",
"queued": "Queued",
"books": "Sách"
},
@@ -341,66 +336,66 @@
"latency": "Latency"
},
"speedtest": {
"upload": "Tải lên",
"download": "Tải xuống",
"upload": "Upload",
"download": "Download",
"ping": "Ping"
},
"portainer": {
"running": "Đang chạy",
"stopped": "Đã dừng",
"total": "Tổng cộng"
"running": "Running",
"stopped": "Stopped",
"total": "Total"
},
"suwayomi": {
"download": "Đã tải xuống",
"nondownload": "Chưa tải xuống",
"read": "Đã đọc",
"unread": "Chưa đọc",
"downloadedread": "Đã tải xuống & Đã đọc",
"downloadedunread": "Đã tải xuống & Chưa đọc",
"nondownloadedread": "Chưa tải xuống & Đã đọc",
"nondownloadedunread": "Chưa tải xuống & Chưa đọc"
"download": "Downloaded",
"nondownload": "Non-Downloaded",
"read": "Read",
"unread": "Unread",
"downloadedread": "Downloaded & Read",
"downloadedunread": "Downloaded & Unread",
"nondownloadedread": "Non-Downloaded & Read",
"nondownloadedunread": "Non-Downloaded & Unread"
},
"tailscale": {
"address": "Địa chỉ",
"expires": "Hết hạn",
"never": "Không bao giờ",
"user": "Người dùng",
"hostname": "Tên máy chủ",
"name": "Tên",
"client_version": "Phiên bản máy khách",
"os": "Hệ điều hành",
"created": "Đã tạo",
"authorized": "Đã cấp quyền",
"is_external": "Là bên ngoài",
"update_available": "Có bản cập nhật",
"tags": "Thẻ",
"last_seen": "Lần cuối nhìn thấy",
"now": "Bây giờ",
"years": "{{number}} năm",
"weeks": "{{number}} tuần",
"days": "{{number}} ngày",
"hours": "{{number}} giờ",
"minutes": "{{number}} phút",
"seconds": "{{number}} giây",
"ago": "{{value}} trước",
"true": "Có",
"false": "Không"
"address": "Address",
"expires": "Expires",
"never": "Never",
"user": "User",
"hostname": "Hostname",
"name": "Name",
"client_version": "Client Version",
"os": "OS",
"created": "Created",
"authorized": "Authorized",
"is_external": "Is External",
"update_available": "Update Available",
"tags": "Tags",
"last_seen": "Last Seen",
"now": "Now",
"years": "{{number}}y",
"weeks": "{{number}}w",
"days": "{{number}}d",
"hours": "{{number}}h",
"minutes": "{{number}}m",
"seconds": "{{number}}s",
"ago": "{{value}} Ago",
"true": "Yes",
"false": "No"
},
"technitium": {
"totalQueries": "Tổng số truy vấn",
"totalNoError": "Thành công",
"totalServerFailure": "Lỗi máy chủ",
"totalNxDomain": "NX Domain",
"totalRefused": "Bị từ chối",
"totalQueries": "Queries",
"totalNoError": "Success",
"totalServerFailure": "Failures",
"totalNxDomain": "NX Domains",
"totalRefused": "Refused",
"totalAuthoritative": "Authoritative",
"totalRecursive": "Recursive",
"totalCached": "Được lưu trong bộ nhớ đệm",
"totalBlocked": "Bị chặn",
"totalDropped": "Bị loại bỏ",
"totalClients": "Số máy khách"
"totalCached": "Cached",
"totalBlocked": "Blocked",
"totalDropped": "Dropped",
"totalClients": "Clients"
},
"tdarr": {
"queue": "Hàng đợi",
"queue": "Queue",
"processed": "Processed",
"errored": "Errored",
"saved": "Saved"
@@ -417,7 +412,7 @@
"unknown": "Unknown"
},
"navidrome": {
"nothing_streaming": "Không có luồng đang hoạt động",
"nothing_streaming": "No Active Streams",
"please_wait": "Please Wait"
},
"npm": {
@@ -460,9 +455,9 @@
"domain_count": "Domains"
},
"medusa": {
"wanted": "Cần tải",
"wanted": "Wanted",
"queued": "Queued",
"series": "Phim bộ"
"series": "Series"
},
"minecraft": {
"players": "Players",
@@ -479,66 +474,66 @@
"users": "Users",
"loginsLast24H": "Logins (24h)",
"failedLoginsLast24H": "Failed Logins (24h)",
"authorizationsLast24H": "Lượt ủy quyền (24 giờ)"
"authorizationsLast24H": "Auths (24h)"
},
"proxmox": {
"mem": "Bộ nhớ",
"mem": "MEM",
"cpu": "CPU",
"lxc": "LXC",
"vms": "VM"
"vms": "VMs"
},
"pulse": {
"nodes": "Node",
"vms": "VM",
"lxcs": "LXC"
"nodes": "Nodes",
"vms": "VMs",
"lxcs": "LXCs"
},
"glances": {
"cpu": "CPU",
"load": "Tải hệ thống",
"wait": "Vui lòng đợi",
"temp": "Nhiệt độ",
"_temp": "Nhiệt độ",
"warn": "Cảnh báo",
"uptime": "Thời gian hoạt động",
"total": "Tổng cộng",
"free": "Còn trống",
"used": "Đã sử dụng",
"days": "ngày",
"hours": "giờ",
"crit": "Nghiêm trọng",
"read": "Đọc",
"write": "Ghi",
"load": "Load",
"wait": "Please wait",
"temp": "TEMP",
"_temp": "Temp",
"warn": "Warn",
"uptime": "UP",
"total": "Total",
"free": "Free",
"used": "Used",
"days": "d",
"hours": "h",
"crit": "Crit",
"read": "Read",
"write": "Write",
"gpu": "GPU",
"mem": "Bộ nhớ",
"mem": "Mem",
"swap": "Swap"
},
"quicklaunch": {
"bookmark": "Dấu trang",
"service": "Dịch vụ",
"search": "Tìm kiếm",
"custom": "Tùy chỉnh",
"visit": "Truy cập",
"bookmark": "Bookmark",
"service": "Service",
"search": "Search",
"custom": "Custom",
"visit": "Visit",
"url": "URL",
"searchsuggestion": "Gợi ý"
"searchsuggestion": "Suggestion"
},
"wmo": {
"0-day": "Trời nắng",
"0-night": "Trời quang",
"1-day": "Chủ yếu có nắng",
"1-night": "Chủ yếu trời quang",
"2-day": "Có mây từng phần",
"2-night": "Có mây từng phần",
"3-day": "Nhiều mây",
"3-night": "Nhiều mây",
"45-day": "Có sương mù",
"45-night": "Có sương mù",
"48-day": "Có sương mù",
"48-night": "Có sương mù",
"51-day": "Mưa phùn nhẹ",
"51-night": "Mưa phùn nhẹ",
"53-day": "Mưa phùn",
"53-night": "Mưa phùn",
"55-day": "Mưa phùn lớn",
"0-day": "Sunny",
"0-night": "Clear",
"1-day": "Mainly Sunny",
"1-night": "Mainly Clear",
"2-day": "Partly Cloudy",
"2-night": "Partly Cloudy",
"3-day": "Cloudy",
"3-night": "Cloudy",
"45-day": "Foggy",
"45-night": "Foggy",
"48-day": "Foggy",
"48-night": "Foggy",
"51-day": "Light Drizzle",
"51-night": "Light Drizzle",
"53-day": "Drizzle",
"53-night": "Drizzle",
"55-day": "Heavy Drizzle",
"55-night": "Heavy Drizzle",
"56-day": "Light Freezing Drizzle",
"56-night": "Light Freezing Drizzle",
@@ -746,14 +741,14 @@
"m": "m"
},
"atsumeru": {
"series": "Bộ truyện",
"series": "Series",
"archives": "Archives",
"chapters": "Chapters",
"categories": "Categories"
},
"komga": {
"libraries": "Libraries",
"series": "Bộ truyện",
"series": "Series",
"books": "Books"
},
"diskstation": {
@@ -766,9 +761,9 @@
"streams": "Streams"
},
"mylar": {
"series": "Phim bộ",
"series": "Series",
"issues": "Issues",
"wanted": "Cần tải"
"wanted": "Wanted"
},
"photoprism": {
"albums": "Albums",
@@ -851,7 +846,7 @@
"books": "Books",
"authors": "Authors",
"categories": "Categories",
"series": "Bộ sách"
"series": "Series"
},
"booklore": {
"libraries": "Libraries",
@@ -866,7 +861,7 @@
"downloadSpeed": "Speed"
},
"kavita": {
"seriesCount": "Số bộ truyện",
"seriesCount": "Series",
"totalFiles": "Files"
},
"azuredevops": {
@@ -1255,7 +1250,7 @@
"storage": "Storage"
},
"sportarr": {
"wanted": "Cần tải",
"wanted": "Wanted",
"queued": "Queued",
"leagues": "Leagues"
}
+1 -6
View File
@@ -99,12 +99,7 @@
"unhealthy": "不健康的",
"not_found": "未找到",
"exited": "已退出",
"partial": "部分",
"created": "Created",
"paused": "Paused",
"restarting": "Restarting",
"removing": "Removing",
"dead": "Dead"
"partial": "部分"
},
"ping": {
"error": "Error",
+1 -6
View File
@@ -99,12 +99,7 @@
"unhealthy": "不健康",
"not_found": "未找到",
"exited": "已退出",
"partial": "部分",
"created": "Created",
"paused": "Paused",
"restarting": "Restarting",
"removing": "Removing",
"dead": "Dead"
"partial": "部分"
},
"ping": {
"error": "错误",
+1 -6
View File
@@ -99,12 +99,7 @@
"unhealthy": "不健康的",
"not_found": "找不到",
"exited": "已退出",
"partial": "部分",
"created": "Created",
"paused": "Paused",
"restarting": "Restarting",
"removing": "Removing",
"dead": "Dead"
"partial": "部分"
},
"ping": {
"error": "Error",
@@ -1,60 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { getDockerStats, logger } = vi.hoisted(() => ({
getDockerStats: vi.fn(),
logger: { error: vi.fn() },
}));
vi.mock("utils/docker/stats", () => ({ getDockerStats }));
vi.mock("utils/logger", () => ({
default: () => logger,
}));
import handler from "pages/api/docker/stats";
describe("pages/api/docker/stats", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns the stats map for the requested server", async () => {
getDockerStats.mockResolvedValue({ stats: { app: { cpu: 20, mem: 900, rx: 4, tx: 6 } } });
const req = { query: { server: "local" } };
const res = createMockRes();
await handler(req, res);
expect(getDockerStats).toHaveBeenCalledWith("local");
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({ stats: { app: { cpu: 20, mem: 900, rx: 4, tx: 6 } } });
});
it("returns 500 when the stats lookup reports an error", async () => {
getDockerStats.mockResolvedValue({ error: "query failed" });
const req = { query: { server: "local" } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(500);
expect(res.body).toEqual({ error: "query failed" });
});
it("logs and returns 500 when the stats lookup throws", async () => {
getDockerStats.mockRejectedValue(new Error("boom"));
const req = { query: { server: "local" } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(500);
expect(res.body).toEqual({ error: { message: "boom" } });
expect(logger.error).toHaveBeenCalled();
});
});
@@ -0,0 +1,153 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { state, DockerCtor, getDockerArguments, logger } = vi.hoisted(() => {
const state = {
docker: null,
dockerArgs: { conn: { socketPath: "/var/run/docker.sock" }, swarm: false },
};
function DockerCtor() {
return state.docker;
}
return {
state,
DockerCtor,
getDockerArguments: vi.fn(() => state.dockerArgs),
logger: { error: vi.fn() },
};
});
vi.mock("dockerode", () => ({
default: DockerCtor,
}));
vi.mock("utils/config/docker", () => ({
default: getDockerArguments,
}));
vi.mock("utils/logger", () => ({
default: () => logger,
}));
import handler from "pages/api/docker/stats/[...service]";
describe("pages/api/docker/stats/[...service]", () => {
beforeEach(() => {
vi.clearAllMocks();
state.dockerArgs = { conn: { socketPath: "/var/run/docker.sock" }, swarm: false };
state.docker = {
listContainers: vi.fn(),
getContainer: vi.fn(),
listTasks: vi.fn(),
};
});
it("returns 400 when container name/server params are missing", async () => {
const req = { query: { service: [] } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(400);
expect(res.body).toEqual({ error: "docker query parameters are required" });
});
it("returns 500 when docker returns a non-array containers payload", async () => {
state.docker.listContainers.mockResolvedValue(Buffer.from("bad"));
const req = { query: { service: ["c", "local"] } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(500);
expect(res.body).toEqual({ error: "query failed" });
});
it("returns stats for an existing container", async () => {
state.docker.listContainers.mockResolvedValue([{ Names: ["/myapp"], Id: "cid1" }]);
const containerStats = { cpu_stats: { cpu_usage: { total_usage: 1 } } };
state.docker.getContainer.mockReturnValue({
stats: vi.fn().mockResolvedValue(containerStats),
});
const req = { query: { service: ["myapp", "local"] } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({ stats: containerStats });
});
it("uses swarm tasks to locate a container and reports a friendly error when stats cannot be retrieved", async () => {
state.dockerArgs.swarm = true;
state.docker.listContainers.mockResolvedValue([{ Names: ["/other"], Id: "local1" }]);
state.docker.listTasks.mockResolvedValue([
{ Status: { ContainerStatus: { ContainerID: "local1" } } },
{ Status: { ContainerStatus: { ContainerID: "remote1" } } },
]);
state.docker.getContainer.mockReturnValue({
stats: vi.fn().mockRejectedValue(new Error("nope")),
});
const req = { query: { service: ["svc", "local"] } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({ error: "Unable to retrieve stats" });
});
it("returns stats for a swarm task container when present locally", async () => {
state.dockerArgs.swarm = true;
state.docker.listContainers.mockResolvedValue([{ Names: ["/other"], Id: "local1" }]);
state.docker.listTasks.mockResolvedValue([{ Status: { ContainerStatus: { ContainerID: "local1" } } }]);
const containerStats = { cpu_stats: { cpu_usage: { total_usage: 2 } } };
state.docker.getContainer.mockReturnValue({
stats: vi.fn().mockResolvedValue(containerStats),
});
const req = { query: { service: ["svc", "local"] } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({ stats: containerStats });
});
it("returns 404 when no container or swarm task is found", async () => {
state.dockerArgs.swarm = true;
state.docker.listContainers.mockResolvedValue([{ Names: ["/other"], Id: "local1" }]);
state.docker.listTasks.mockResolvedValue([]);
const req = { query: { service: ["missing", "local"] } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(404);
expect(res.body).toEqual({ error: "not found" });
});
it("logs and returns 500 when the docker query throws", async () => {
getDockerArguments.mockImplementationOnce(() => {
throw new Error("boom");
});
const req = { query: { service: ["myapp", "local"] } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(500);
expect(res.body).toEqual({ error: { message: "boom" } });
expect(logger.error).toHaveBeenCalled();
});
});
@@ -0,0 +1,211 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { state, DockerCtor, getDockerArguments, logger } = vi.hoisted(() => {
const state = {
docker: null,
dockerCtorArgs: [],
dockerArgs: { conn: { socketPath: "/var/run/docker.sock" }, swarm: false },
};
function DockerCtor(conn) {
state.dockerCtorArgs.push(conn);
return state.docker;
}
return {
state,
DockerCtor,
getDockerArguments: vi.fn(() => state.dockerArgs),
logger: { error: vi.fn() },
};
});
vi.mock("dockerode", () => ({
default: DockerCtor,
}));
vi.mock("utils/config/docker", () => ({
default: getDockerArguments,
}));
vi.mock("utils/logger", () => ({
default: () => logger,
}));
import handler from "pages/api/docker/status/[...service]";
describe("pages/api/docker/status/[...service]", () => {
beforeEach(() => {
vi.clearAllMocks();
state.dockerCtorArgs.length = 0;
state.dockerArgs = { conn: { socketPath: "/var/run/docker.sock" }, swarm: false };
state.docker = {
listContainers: vi.fn(),
getContainer: vi.fn(),
getService: vi.fn(),
listTasks: vi.fn(),
};
});
it("returns 400 when container name/server params are missing", async () => {
const req = { query: { service: [] } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(400);
expect(res.body).toEqual({ error: "docker query parameters are required" });
});
it("returns 500 when docker returns a non-array containers payload", async () => {
state.docker.listContainers.mockResolvedValue(Buffer.from("bad"));
const req = { query: { service: ["c", "local"] } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(500);
expect(res.body).toEqual({ error: "query failed" });
});
it("inspects an existing container and returns status + health", async () => {
state.docker.listContainers.mockResolvedValue([{ Names: ["/myapp"], Id: "cid1" }]);
state.docker.getContainer.mockReturnValue({
inspect: vi.fn().mockResolvedValue({ State: { Status: "running", Health: { Status: "healthy" } } }),
});
const req = { query: { service: ["myapp", "local"] } };
const res = createMockRes();
await handler(req, res);
expect(getDockerArguments).toHaveBeenCalledWith("local");
expect(state.dockerCtorArgs).toHaveLength(1);
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({ status: "running", health: "healthy" });
});
it("returns 404 when container does not exist and swarm is disabled", async () => {
state.docker.listContainers.mockResolvedValue([{ Names: ["/other"], Id: "cid1" }]);
const req = { query: { service: ["missing", "local"] } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(404);
expect(res.body).toEqual({ status: "not found" });
});
it("reports replicated swarm service status based on desired replicas", async () => {
state.dockerArgs.swarm = true;
state.docker.listContainers.mockResolvedValue([{ Names: ["/other"], Id: "cid1" }]);
state.docker.getService.mockReturnValue({
inspect: vi.fn().mockResolvedValue({ Spec: { Mode: { Replicated: { Replicas: "2" } } } }),
});
state.docker.listTasks.mockResolvedValue([{ Status: {} }, { Status: {} }]);
const req = { query: { service: ["svc", "local"] } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({ status: "running 2/2" });
});
it("reports partial status for replicated services with fewer running tasks", async () => {
state.dockerArgs.swarm = true;
state.docker.listContainers.mockResolvedValue([{ Names: ["/other"], Id: "cid1" }]);
state.docker.getService.mockReturnValue({
inspect: vi.fn().mockResolvedValue({ Spec: { Mode: { Replicated: { Replicas: "3" } } } }),
});
state.docker.listTasks.mockResolvedValue([{ Status: {} }]);
const req = { query: { service: ["svc", "local"] } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({ status: "partial 1/3" });
});
it("handles global services by inspecting a local task container when possible", async () => {
state.dockerArgs.swarm = true;
state.docker.listContainers.mockResolvedValue([{ Names: ["/other"], Id: "local1" }]);
state.docker.getService.mockReturnValue({
inspect: vi.fn().mockResolvedValue({ Spec: { Mode: { Global: {} } } }),
});
state.docker.listTasks.mockResolvedValue([
{ Status: { ContainerStatus: { ContainerID: "local1" }, State: "running" } },
]);
state.docker.getContainer.mockReturnValue({
inspect: vi.fn().mockResolvedValue({ State: { Status: "running", Health: { Status: "unhealthy" } } }),
});
const req = { query: { service: ["svc", "local"] } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({ status: "running", health: "unhealthy" });
});
it("falls back to task status when global service container inspect fails", async () => {
state.dockerArgs.swarm = true;
state.docker.listContainers.mockResolvedValue([{ Names: ["/other"], Id: "local1" }]);
state.docker.getService.mockReturnValue({
inspect: vi.fn().mockResolvedValue({ Spec: { Mode: { Global: {} } } }),
});
state.docker.listTasks.mockResolvedValue([
{ Status: { ContainerStatus: { ContainerID: "local1" }, State: "pending" } },
]);
state.docker.getContainer.mockReturnValue({
inspect: vi.fn().mockRejectedValue(new Error("nope")),
});
const req = { query: { service: ["svc", "local"] } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({ status: "pending" });
});
it("returns 404 when swarm is enabled but the service does not exist", async () => {
state.dockerArgs.swarm = true;
state.docker.listContainers.mockResolvedValue([{ Names: ["/other"], Id: "cid1" }]);
state.docker.getService.mockReturnValue({
inspect: vi.fn().mockRejectedValue(new Error("not found")),
});
const req = { query: { service: ["svc", "local"] } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(404);
expect(res.body).toEqual({ status: "not found" });
});
it("logs and returns 500 when the docker query throws", async () => {
getDockerArguments.mockImplementationOnce(() => {
throw new Error("boom");
});
const req = { query: { service: ["svc", "local"] } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(500);
expect(res.body).toEqual({ error: { message: "boom" } });
expect(logger.error).toHaveBeenCalled();
});
});
@@ -1,265 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { state, DockerCtor, getDockerArguments, containersFromConfig, hasHomepageLabels, getSettings, logger } =
vi.hoisted(() => {
const state = {
docker: null,
containers: [],
health: {},
containers: [],
health: {},
dockerArgs: { conn: { socketPath: "/var/run/docker.sock" }, swarm: false },
};
function DockerCtor() {
return state.docker;
}
return {
state,
DockerCtor,
getDockerArguments: vi.fn(() => state.dockerArgs),
containersFromConfig: vi.fn(async () => new Set()),
hasHomepageLabels: vi.fn(() => false),
getSettings: vi.fn(() => ({ instanceName: undefined })),
logger: { error: vi.fn() },
};
});
vi.mock("dockerode", () => ({
default: DockerCtor,
}));
vi.mock("utils/config/docker", () => ({
default: getDockerArguments,
}));
vi.mock("utils/config/service-helpers", () => ({
containersFromConfig,
hasHomepageLabels,
}));
vi.mock("utils/config/config", () => ({
getSettings,
}));
vi.mock("utils/logger", () => ({
default: () => logger,
}));
import handler from "pages/api/docker/statuses";
describe("pages/api/docker/statuses", () => {
beforeEach(() => {
vi.clearAllMocks();
state.dockerArgs = { conn: { socketPath: "/var/run/docker.sock" }, swarm: false };
state.containers = [];
state.health = {};
state.docker = {
listContainers: vi.fn(async (options) => {
const health = options?.filters?.health?.[0];
return health ? (state.health[health] ?? []) : state.containers;
}),
listServices: vi.fn(),
listTasks: vi.fn(),
};
containersFromConfig.mockResolvedValue(new Set());
hasHomepageLabels.mockReturnValue(false);
getSettings.mockReturnValue({ instanceName: undefined });
});
it("returns configured container statuses with health from the docker health filters", async () => {
containersFromConfig.mockResolvedValue(new Set(["glance", "share"]));
state.containers = [
{ Names: ["/glance"], Id: "cid-glance", State: "running" },
{ Names: ["/share"], Id: "cid-share", State: "exited" },
];
state.health = { healthy: [{ Id: "cid-glance" }] };
const req = { query: { server: "local" } };
const res = createMockRes();
await handler(req, res);
expect(getDockerArguments).toHaveBeenCalledWith("local");
expect(containersFromConfig).toHaveBeenCalledWith("local");
// one list plus one per health state, constant regardless of container count
expect(state.docker.listContainers).toHaveBeenCalledTimes(4);
expect(state.docker.listServices).not.toHaveBeenCalled();
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({
statuses: {
glance: { status: "running", health: "healthy" },
share: { status: "exited" },
},
});
});
it("takes health from the filter results rather than the status string", async () => {
containersFromConfig.mockResolvedValue(new Set(["app"]));
state.containers = [{ Names: ["/app"], Id: "cid1", State: "running", Status: "Up 2 hours (healthy)" }];
state.health = { unhealthy: [{ Id: "cid1" }] };
const req = { query: { server: "local" } };
const res = createMockRes();
await handler(req, res);
expect(res.body).toEqual({ statuses: { app: { status: "running", health: "unhealthy" } } });
});
it("omits health when the docker daemon rejects the health filter", async () => {
containersFromConfig.mockResolvedValue(new Set(["app"]));
state.containers = [{ Names: ["/app"], Id: "cid1", State: "running", Status: "Up 2 hours (healthy)" }];
state.docker.listContainers.mockImplementation(async (options) => {
if (options?.filters?.health) throw new Error("filter unsupported");
return state.containers;
});
const req = { query: { server: "local" } };
const res = createMockRes();
await handler(req, res);
expect(res.body).toEqual({ statuses: { app: { status: "running" } } });
});
it("omits containers that are neither configured nor labelled for homepage", async () => {
containersFromConfig.mockResolvedValue(new Set(["glance"]));
state.containers = [
{ Names: ["/glance"], State: "running", Status: "Up" },
{ Names: ["/secret-db"], State: "running", Status: "Up" },
];
const req = { query: { server: "local" } };
const res = createMockRes();
await handler(req, res);
expect(res.body).toEqual({ statuses: { glance: { status: "running" } } });
expect(res.body.statuses["secret-db"]).toBeUndefined();
});
it("includes containers discovered through homepage labels", async () => {
hasHomepageLabels.mockImplementation((labels) => labels?.["homepage.name"] !== undefined);
state.containers = [
{ Names: ["/labelled"], State: "running", Status: "Up", Labels: { "homepage.name": "App" } },
{ Names: ["/unlabelled"], State: "running", Status: "Up", Labels: {} },
];
const req = { query: { server: "local" } };
const res = createMockRes();
await handler(req, res);
expect(res.body).toEqual({ statuses: { labelled: { status: "running" } } });
});
it("includes swarm services when the server is in swarm mode", async () => {
state.dockerArgs.swarm = true;
containersFromConfig.mockResolvedValue(new Set(["web", "api"]));
state.containers = [{ Names: ["/web"], Id: "cid1", State: "running", Status: "Up" }];
state.docker.listServices.mockResolvedValue([
{ ID: "sid", Spec: { Name: "api", Mode: { Replicated: { Replicas: "2" } } } },
]);
state.docker.listTasks.mockResolvedValue([
{ ServiceID: "sid", Status: {} },
{ ServiceID: "sid", Status: {} },
]);
const req = { query: { server: "swarm" } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({
statuses: {
web: { status: "running" },
api: { status: "running 2/2" },
},
});
});
it("omits swarm services that are neither configured nor labelled", async () => {
state.dockerArgs.swarm = true;
containersFromConfig.mockResolvedValue(new Set());
state.containers = [];
state.docker.listServices.mockResolvedValue([
{ ID: "sid", Spec: { Name: "internal", Mode: { Replicated: { Replicas: "1" } } } },
]);
state.docker.listTasks.mockResolvedValue([{ ServiceID: "sid", Status: {} }]);
const req = { query: { server: "swarm" } };
const res = createMockRes();
await handler(req, res);
expect(res.body).toEqual({ statuses: {} });
});
it("includes swarm services discovered through homepage labels", async () => {
state.dockerArgs.swarm = true;
hasHomepageLabels.mockImplementation((labels) => labels?.["homepage.name"] !== undefined);
state.containers = [];
state.docker.listServices.mockResolvedValue([
{
ID: "sid",
Spec: { Name: "api", Labels: { "homepage.name": "Api" }, Mode: { Replicated: { Replicas: "1" } } },
},
]);
state.docker.listTasks.mockResolvedValue([{ ServiceID: "sid", Status: {} }]);
const req = { query: { server: "swarm" } };
const res = createMockRes();
await handler(req, res);
expect(res.body).toEqual({ statuses: { api: { status: "running 1/1" } } });
});
it("returns only listed containers when swarm queries fail", async () => {
state.dockerArgs.swarm = true;
containersFromConfig.mockResolvedValue(new Set(["web"]));
state.containers = [{ Names: ["/web"], Id: "cid1", State: "running", Status: "Up" }];
state.docker.listServices.mockRejectedValue(new Error("no services"));
state.docker.listTasks.mockRejectedValue(new Error("no tasks"));
const req = { query: { server: "swarm" } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({ statuses: { web: { status: "running" } } });
});
it("returns 500 when docker returns a non-array containers payload", async () => {
state.containers = Buffer.from("bad");
const req = { query: { server: "local" } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(500);
expect(res.body).toEqual({ error: "query failed" });
});
it("logs and returns 500 when the docker query throws", async () => {
getDockerArguments.mockImplementationOnce(() => {
throw new Error("boom");
});
const req = { query: { server: "local" } };
const res = createMockRes();
await handler(req, res);
expect(res.statusCode).toBe(500);
expect(res.body).toEqual({ error: { message: "boom" } });
expect(logger.error).toHaveBeenCalled();
});
});
@@ -1,118 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { getPrivateWidgetOptions, httpProxy, logger } = vi.hoisted(() => ({
getPrivateWidgetOptions: vi.fn(),
httpProxy: vi.fn(),
logger: { debug: vi.fn() },
}));
vi.mock("utils/config/widget-helpers", () => ({
getPrivateWidgetOptions,
}));
vi.mock("utils/proxy/http", () => ({
httpProxy,
}));
vi.mock("utils/logger", () => ({
default: () => logger,
}));
import handler from "pages/api/widgets/customapi";
const json = (body) => Buffer.from(JSON.stringify(body));
async function callRoute(options, index = "0") {
getPrivateWidgetOptions.mockResolvedValueOnce(options);
const res = createMockRes();
await handler({ query: { index } }, res);
return res;
}
describe("pages/api/widgets/customapi", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it.each([
["no widget at that index", undefined],
["a widget without a url", { method: "GET" }],
["an invalid url", { url: "api.local/data" }],
])("returns 400 without calling upstream for %s", async (_, options) => {
const res = await callRoute(options, "3");
expect(getPrivateWidgetOptions).toHaveBeenCalledWith("customapi", "3");
expect(res.statusCode).toBe(400);
expect(httpProxy).not.toHaveBeenCalled();
});
it("proxies an authenticated POST with custom headers and a JSON body", async () => {
httpProxy.mockResolvedValueOnce([200, "application/json", json({ json: { hello: "world" } })]);
const res = await callRoute({
url: "https://httpbin.org/post",
username: "admin",
password: "hunter2",
method: "POST",
headers: { "X-Secret-Token": "super-secret", "Content-Type": "application/json" },
requestBody: { hello: "world" },
});
expect(httpProxy).toHaveBeenCalledWith(new URL("https://httpbin.org/post"), {
method: "POST",
headers: {
"User-Agent": "homepage",
Accept: "application/json",
"X-Secret-Token": "super-secret",
"Content-Type": "application/json",
Authorization: `Basic ${Buffer.from("admin:hunter2").toString("base64")}`,
},
body: '{"hello":"world"}',
});
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({ json: { hello: "world" } });
});
it("sends a string body as-is and lets config override default headers", async () => {
httpProxy.mockResolvedValueOnce([200, "application/json", json({ data: { viewer: { login: "shamoon" } } })]);
const query = '{"query":"{ viewer { login } }"}';
const res = await callRoute({
url: "https://api.github.com/graphql",
method: "POST",
headers: { Authorization: "Bearer ghp_token", "User-Agent": "my-dashboard" },
requestBody: query,
});
expect(httpProxy).toHaveBeenCalledWith(expect.any(URL), {
method: "POST",
headers: { "User-Agent": "my-dashboard", Accept: "application/json", Authorization: "Bearer ghp_token" },
body: query,
});
expect(res.body).toEqual({ data: { viewer: { login: "shamoon" } } });
});
it.each([
["an HTTP error", [403, "application/json", json({ message: "API rate limit exceeded for 1.2.3.4" })]],
[
"a network failure",
[500, "application/json", { error: { message: "connect ECONNREFUSED", rawError: { address: "10.0.0.5" } } }],
],
["a non-JSON response", [200, "text/html", Buffer.from("<html><body>Login</body></html>")]],
])("returns only a sanitized error for %s", async (_, upstream) => {
httpProxy.mockResolvedValueOnce(upstream);
const res = await callRoute({ url: "https://api.github.com/repos/gethomepage/homepage?token=abc" });
expect(httpProxy).toHaveBeenCalledWith(expect.any(URL), {
method: "GET",
headers: { "User-Agent": "homepage", Accept: "application/json" },
});
expect(res.statusCode).toBe(upstream[0] === 200 ? 500 : upstream[0]);
expect(res.body.error.url).toBe("api.github.com (see logs for details)");
expect(JSON.stringify(res.body)).not.toMatch(/rate limit|ECONNREFUSED|10\.0\.0\.5|<html>|token=abc/);
expect(res.setHeader).not.toHaveBeenCalled();
});
});
+3 -92
View File
@@ -1,15 +1,11 @@
// @vitest-environment jsdom
import { render, screen, waitFor } from "@testing-library/react";
import { StrictMode } from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { describe, expect, it, vi } from "vitest";
const { getSettingsMock, authOptionsMock, signInMock, replaceMock, routerQuery } = vi.hoisted(() => ({
const { getSettingsMock, authOptionsMock } = vi.hoisted(() => ({
getSettingsMock: vi.fn(),
authOptionsMock: vi.fn(),
signInMock: vi.fn(),
replaceMock: vi.fn(),
routerQuery: {},
}));
vi.mock("utils/config/config", () => ({
@@ -24,25 +20,13 @@ vi.mock("pages/api/auth/[...nextauth]", () => ({
vi.mock("next/router", () => ({
useRouter: () => ({
query: routerQuery,
replace: replaceMock,
query: {},
}),
}));
vi.mock("next-auth/react", () => ({ signIn: signInMock }));
import SignInPage, { getServerSideProps } from "pages/auth/signin";
const OIDC_PROVIDERS = { "homepage-oidc": { id: "homepage-oidc", name: "Homepage OIDC", type: "oauth" } };
const SETTINGS = { theme: "dark", color: "slate", title: "Homepage" };
describe("pages/auth/signin", () => {
beforeEach(() => {
vi.clearAllMocks();
Object.keys(routerQuery).forEach((key) => delete routerQuery[key]);
window.sessionStorage.clear();
});
it("renders an error state when no providers are configured", async () => {
render(
<SignInPage
@@ -82,67 +66,6 @@ describe("pages/auth/signin", () => {
expect(screen.getByRole("button", { name: /login via oidc/i })).toBeInTheDocument();
});
it("redirects to the provider when auto-login is enabled", () => {
routerQuery.callbackUrl = "/some/page";
render(<SignInPage providers={OIDC_PROVIDERS} settings={SETTINGS} autoLogin />);
expect(signInMock).toHaveBeenCalledWith("homepage-oidc", { callbackUrl: "/some/page" });
expect(screen.getByText(/redirecting to homepage oidc/i)).toBeInTheDocument();
expect(screen.queryByRole("button", { name: /login via/i })).not.toBeInTheDocument();
});
it("does not auto-login when the provider returned an error", () => {
routerQuery.error = "OAuthCallback";
render(<SignInPage providers={OIDC_PROVIDERS} settings={SETTINGS} autoLogin />);
expect(signInMock).not.toHaveBeenCalled();
expect(screen.getByRole("button", { name: /login via homepage oidc/i })).toBeInTheDocument();
});
it("does not auto-login when it is explicitly disabled in the url", () => {
routerQuery.autologin = "0";
render(<SignInPage providers={OIDC_PROVIDERS} settings={SETTINGS} autoLogin />);
expect(signInMock).not.toHaveBeenCalled();
expect(screen.getByRole("button", { name: /login via homepage oidc/i })).toBeInTheDocument();
});
it("redirects once under strict mode, rather than tripping its own loop guard", () => {
render(
<StrictMode>
<SignInPage providers={OIDC_PROVIDERS} settings={SETTINGS} autoLogin />
</StrictMode>,
);
expect(signInMock).toHaveBeenCalledTimes(1);
expect(replaceMock).not.toHaveBeenCalled();
});
it("stops auto-login and hands back the page when the session never sticks", () => {
routerQuery.callbackUrl = "/some/page";
render(<SignInPage providers={OIDC_PROVIDERS} settings={SETTINGS} autoLogin />);
render(<SignInPage providers={OIDC_PROVIDERS} settings={SETTINGS} autoLogin />);
expect(signInMock).toHaveBeenCalledTimes(1);
expect(replaceMock).toHaveBeenCalledWith("/auth/signin?autologin=0&callbackUrl=%2Fsome%2Fpage");
});
it("does not auto-login the password provider", () => {
render(
<SignInPage
providers={{ credentials: { id: "credentials", name: "Password", type: "credentials" } }}
settings={SETTINGS}
autoLogin
/>,
);
expect(signInMock).not.toHaveBeenCalled();
});
it("getServerSideProps returns providers and only public sign-in settings", async () => {
authOptionsMock.mockReturnValueOnce({ providers: [{ id: "foo", name: "Foo", type: "oauth" }] });
getSettingsMock.mockReturnValueOnce({
@@ -165,7 +88,6 @@ describe("pages/auth/signin", () => {
expect(getSettingsMock).toHaveBeenCalled();
expect(res).toEqual({
props: {
autoLogin: false,
providers: { foo: { id: "foo", name: "Foo", type: "oauth" } },
settings: {
theme: "dark",
@@ -180,17 +102,6 @@ describe("pages/auth/signin", () => {
expect(res.props.settings).not.toHaveProperty("layout");
});
it("getServerSideProps enables auto-login from the environment", async () => {
authOptionsMock.mockReturnValueOnce({ providers: [] });
getSettingsMock.mockReturnValueOnce({ theme: "dark" });
vi.stubEnv("HOMEPAGE_OIDC_AUTO_LOGIN", "true");
const res = await getServerSideProps({});
expect(res.props.autoLogin).toBe(true);
vi.unstubAllEnvs();
});
it("getServerSideProps falls back to no providers when auth options fail to load", async () => {
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
authOptionsMock.mockImplementationOnce(() => {
+2 -3
View File
@@ -1,11 +1,10 @@
import { Disclosure, Transition } from "@headlessui/react";
import classNames from "classnames";
import { useEffect, useRef } from "react";
import { MdKeyboardArrowDown } from "react-icons/md";
import List from "components/bookmarks/list";
import ErrorBoundary from "components/errorboundry";
import ResolvedIcon from "components/resolvedicon";
import { useEffect, useRef } from "react";
import { MdKeyboardArrowDown } from "react-icons/md";
export default function BookmarksGroup({
bookmarks,
+2 -8
View File
@@ -1,16 +1,11 @@
import classNames from "classnames";
import { useContext } from "react";
import ResolvedIcon from "components/resolvedicon";
import { useContext } from "react";
import { SettingsContext } from "utils/contexts/settings";
export default function Item({ bookmark, iconOnly = false }) {
const description = bookmark.description ?? new URL(bookmark.href).hostname;
const { settings } = useContext(SettingsContext);
const cardStyle =
settings.cardStyle === "hairline"
? "shadow-xs inset-ring inset-ring-black/5 dark:inset-ring-white/5 hover:inset-ring-black/10 dark:hover:inset-ring-white/10"
: "shadow-md shadow-theme-900/10 dark:shadow-theme-900/20";
return (
<li
@@ -26,8 +21,7 @@ export default function Item({ bookmark, iconOnly = false }) {
target={bookmark.target ?? settings.target ?? "_blank"}
className={classNames(
settings.cardBlur !== undefined && `backdrop-blur${settings.cardBlur.length ? "-" : ""}${settings.cardBlur}`,
cardStyle,
"text-left cursor-pointer transition-all rounded-md font-medium text-theme-700 dark:text-theme-200 dark:hover:text-theme-300 bg-theme-100/20 hover:bg-theme-300/20 dark:bg-white/5 dark:hover:bg-white/10",
"text-left cursor-pointer transition-all rounded-md font-medium text-theme-700 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 hover:bg-theme-300/20 dark:bg-white/5 dark:hover:bg-white/10",
iconOnly ? "h-[60px] w-[60px] grid" : "block w-full mb-3",
)}
>
-10
View File
@@ -21,16 +21,6 @@ describe("components/bookmarks/item", () => {
expect(screen.getByRole("link").getAttribute("target")).toBe("_self");
});
it("applies hairline card styles when cardStyle is set", () => {
renderWithProviders(<Item bookmark={{ name: "A", href: "http://example.com/x", abbr: "A" }} iconOnly={false} />, {
settings: { cardStyle: "hairline" },
});
const className = screen.getByRole("link").className;
expect(className).toContain("inset-ring");
expect(className).not.toContain("shadow-md");
});
it("renders icon-only layout with icon when provided", () => {
renderWithProviders(
<Item bookmark={{ name: "A", href: "http://example.com/x", abbr: "A", icon: "mdi-home" }} iconOnly />,
+1 -2
View File
@@ -1,9 +1,8 @@
import classNames from "classnames";
import Item from "components/bookmarks/item";
import { columnMap } from "../../utils/layout/columns";
import Item from "components/bookmarks/item";
export default function List({ bookmarks, layout, bookmarksStyle }) {
let classes = layout?.style === "row" ? `grid ${columnMap[layout?.columns]} gap-x-2` : "flex flex-col bookmark-list";
const style = {};
+1 -1
View File
@@ -1,8 +1,8 @@
/* eslint-disable @next/next/no-img-element */
/* eslint-disable jsx-a11y/alt-text */
import { useContext, useEffect, useRef } from "react";
import { ColorContext } from "utils/contexts/color";
import themes from "utils/styles/themes";
export function Svg({ svgRef = null }) {
+4 -6
View File
@@ -3,12 +3,11 @@ import { useTranslation } from "next-i18next/pages";
import { useCallback, useContext, useEffect, useRef, useState } from "react";
import { FiSearch } from "react-icons/fi";
import useSWR from "swr";
import { SettingsContext } from "utils/contexts/settings";
import ResolvedIcon from "./resolvedicon";
import { getStoredProvider, searchProviders } from "./widgets/search/search";
import { SettingsContext } from "utils/contexts/settings";
const MOBILE_BUTTON_POSITIONS = {
"top-left": "top-4 left-4",
"top-right": "top-4 right-4",
@@ -124,8 +123,8 @@ export default function QuickLaunch({ servicesAndBookmarks, searchString, setSea
? MOBILE_BUTTON_POSITIONS[settings.quicklaunch.mobileButtonPosition]
: null;
function openCurrentItem(newWindow, index = activeItemIndex) {
const result = results[index];
function openCurrentItem(newWindow) {
const result = results[activeItemIndex];
window.open(
result.href,
newWindow ? "_blank" : (result.target ?? searchProvider?.target ?? settings.target ?? "_blank"),
@@ -180,8 +179,7 @@ export default function QuickLaunch({ servicesAndBookmarks, searchString, setSea
function handleItemClick(event) {
closeAndReset();
// in case hover doesnt fire, use the clicked item, not the highlighted one
openCurrentItem(event.metaKey, parseInt(event.currentTarget.dataset.index, 10));
openCurrentItem(event.metaKey);
}
function handleItemKeyDown(event) {
-31
View File
@@ -263,37 +263,6 @@ describe("components/quicklaunch", () => {
openSpy.mockRestore();
});
it("opens the clicked result even without a preceding hover event", async () => {
const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);
renderWithProviders(
<Wrapper
servicesAndBookmarks={[
{ name: "Alpha", href: "https://alpha.example" },
{ name: "Alpine", href: "https://alpine.example" },
{ name: "Almond", href: "https://almond.example" },
]}
/>,
{ settings: { target: "_self", quicklaunch: { showSearchSuggestions: false } } },
);
const input = screen.getByPlaceholderText("Search");
await waitFor(() => expect(input).toHaveFocus());
fireEvent.change(input, { target: { value: "al" } });
await waitFor(() => expect(document.querySelector('button[data-index="2"]')).toBeTruthy());
// touch devices don't fire mouseEnter, so the highlighted item is still the first one
fireEvent.click(document.querySelector('button[data-index="2"]'));
await act(async () => {
await new Promise((r) => setTimeout(r, 350));
});
expect(openSpy).toHaveBeenCalledWith("https://almond.example", "_self", "noreferrer");
openSpy.mockRestore();
});
it("handles Escape on a result button (not just the input)", async () => {
renderWithProviders(<Wrapper servicesAndBookmarks={[{ name: "Alpha", href: "https://alpha.example" }]} />, {
settings: { quicklaunch: { showSearchSuggestions: false } },
-1
View File
@@ -1,6 +1,5 @@
import Image from "next/image";
import { useContext } from "react";
import { SettingsContext } from "utils/contexts/settings";
import { ThemeContext } from "utils/contexts/theme";
+2 -3
View File
@@ -1,13 +1,12 @@
import { Disclosure, Transition } from "@headlessui/react";
import classNames from "classnames";
import ResolvedIcon from "components/resolvedicon";
import List from "components/services/list";
import { useEffect, useRef } from "react";
import { MdKeyboardArrowDown } from "react-icons/md";
import { columnMap } from "../../utils/layout/columns";
import ResolvedIcon from "components/resolvedicon";
import List from "components/services/list";
export default function ServicesGroup({
group,
layout,
+6 -12
View File
@@ -1,5 +1,10 @@
import classNames from "classnames";
import ResolvedIcon from "components/resolvedicon";
import { useContext, useState } from "react";
import { SettingsContext } from "utils/contexts/settings";
import Docker from "widgets/docker/component";
import Kubernetes from "widgets/kubernetes/component";
import ProxmoxVM from "widgets/proxmoxvm/component";
import KubernetesStatus from "./kubernetes-status";
import Ping from "./ping";
@@ -8,21 +13,11 @@ import SiteMonitor from "./site-monitor";
import Status from "./status";
import Widget from "./widget";
import ResolvedIcon from "components/resolvedicon";
import { SettingsContext } from "utils/contexts/settings";
import Docker from "widgets/docker/component";
import Kubernetes from "widgets/kubernetes/component";
import ProxmoxVM from "widgets/proxmoxvm/component";
export default function Item({ service, groupName, useEqualHeights }) {
const hasLink = service.href && service.href !== "#";
const { settings } = useContext(SettingsContext);
const showStats = service.showStats === false ? false : settings.showStats;
const statusStyle = service.statusStyle !== undefined ? service.statusStyle : settings.statusStyle;
const cardStyle =
settings.cardStyle === "hairline"
? "shadow-xs inset-ring inset-ring-black/5 dark:inset-ring-white/5 hover:inset-ring-black/10 dark:hover:inset-ring-white/10"
: "shadow-md shadow-theme-900/10 dark:shadow-theme-900/20";
const [statsOpen, setStatsOpen] = useState(service.showStats);
const [statsClosing, setStatsClosing] = useState(false);
@@ -43,8 +38,7 @@ export default function Item({ service, groupName, useEqualHeights }) {
className={classNames(
settings.cardBlur !== undefined && `backdrop-blur${settings.cardBlur.length ? "-" : ""}${settings.cardBlur}`,
useEqualHeights && "h-[calc(100%-0.5rem)]",
cardStyle,
"transition-all mb-2 p-1 rounded-md font-medium text-theme-700 dark:text-theme-200 dark:hover:text-theme-300 bg-theme-100/20 hover:bg-theme-300/20 dark:bg-white/5 dark:hover:bg-white/10 relative overflow-clip service-card",
"transition-all mb-2 p-1 rounded-md font-medium text-theme-700 dark:text-theme-200 dark:hover:text-theme-300 shadow-md shadow-theme-900/10 dark:shadow-theme-900/20 bg-theme-100/20 hover:bg-theme-300/20 dark:bg-white/5 dark:hover:bg-white/10 relative overflow-clip service-card",
)}
>
<div className="flex select-none z-0 service-title">
+1 -2
View File
@@ -1,9 +1,8 @@
import classNames from "classnames";
import Item from "components/services/item";
import { columnMap } from "../../utils/layout/columns";
import Item from "components/services/item";
export default function List({ groupName, services, layout, useEqualHeights, header }) {
return (
<ul
+24 -40
View File
@@ -1,61 +1,45 @@
import { useTranslation } from "next-i18next/pages";
import useSWR from "swr";
const WARNING_CLASS = "text-orange-400/50 dark:text-orange-400/80";
// docker container states other than running
const STATE_LABELS = {
created: "docker.created",
dead: "docker.dead",
exited: "docker.exited",
"not found": "docker.not_found",
paused: "docker.paused",
removing: "docker.removing",
restarting: "docker.restarting",
};
// states that mean something is wrong rather than merely not started
const WARNING_STATES = new Set(["dead", "exited", "not found", "restarting"]);
export default function Status({ service, style }) {
const { t } = useTranslation();
const { data: response, error } = useSWR(`/api/docker/statuses?server=${encodeURIComponent(service.server || "")}`);
const statusError = error ?? response?.error;
const { statuses } = response ?? {};
const data = statuses ? (statuses[service.container] ?? { status: "not found" }) : undefined;
const { data, error } = useSWR(`/api/docker/status/${service.container}/${service.server || ""}`);
let statusLabel = t("docker.unknown");
let backgroundClass = "px-1.5 py-0.5 bg-theme-500/10 dark:bg-theme-900/50";
let colorClass = "text-black/20 dark:text-white/40 ";
if (statusError) {
if (error) {
statusLabel = t("docker.error");
colorClass = "text-rose-500/80";
} else if (data?.status?.includes("running")) {
colorClass = "text-emerald-500/80";
} else if (data) {
if (data.status?.includes("running")) {
colorClass = "text-emerald-500/80";
if (!data.health) {
statusLabel = data.status.replace("running", t("docker.running"));
} else {
statusLabel = data.health === "healthy" ? t("docker.healthy") : data.health;
if (!data.health) {
statusLabel = data.status.replace("running", t("docker.running"));
} else {
statusLabel = data.health === "healthy" ? t("docker.healthy") : data.health;
if (data.health === "starting") {
statusLabel = t("docker.starting");
colorClass = "text-blue-500/80";
}
if (data.health === "starting") {
statusLabel = t("docker.starting");
colorClass = "text-blue-500/80";
}
if (data.health === "unhealthy") {
statusLabel = t("docker.unhealthy");
colorClass = WARNING_CLASS;
if (data.health === "unhealthy") {
statusLabel = t("docker.unhealthy");
colorClass = "text-orange-400/50 dark:text-orange-400/80";
}
}
}
} else if (data?.status?.startsWith("partial")) {
statusLabel = data.status.replace("partial", t("docker.partial"));
colorClass = WARNING_CLASS;
} else if (data && STATE_LABELS[data.status]) {
statusLabel = t(STATE_LABELS[data.status]);
if (WARNING_STATES.has(data.status)) colorClass = WARNING_CLASS;
if (data.status === "not found" || data.status === "exited" || data.status?.startsWith("partial")) {
if (data.status === "not found") statusLabel = t("docker.not_found");
else if (data.status === "exited") statusLabel = t("docker.exited");
else statusLabel = data.status.replace("partial", t("docker.partial"));
colorClass = "text-orange-400/50 dark:text-orange-400/80";
}
}
if (style === "dot") {
+8 -60
View File
@@ -21,11 +21,8 @@ describe("components/services/status", () => {
render(<Status service={{ container: "c", server: "s" }} />);
expect(useSWR).toHaveBeenCalledWith("/api/docker/statuses?server=s");
expect(useSWR).toHaveBeenCalledWith("/api/docker/status/c/s");
expect(screen.getByText("docker.unknown")).toBeInTheDocument();
render(<Status service={{ container: "c" }} />);
expect(useSWR).toHaveBeenCalledWith("/api/docker/statuses?server=");
});
it("renders error when SWR fails", () => {
@@ -36,87 +33,38 @@ describe("components/services/status", () => {
expect(screen.getByText("docker.error")).toBeInTheDocument();
});
it("renders error when the api returns an error payload with a 200-parsed body", () => {
useSWR.mockReturnValue({ data: { error: "query failed" }, error: undefined });
render(<Status service={{ container: "c", server: "s" }} />);
expect(screen.getByText("docker.error")).toBeInTheDocument();
expect(screen.queryByText("docker.not_found")).not.toBeInTheDocument();
});
it("renders healthy/unhealthy and partial/exited/not found statuses", () => {
useSWR.mockReturnValue({ data: { statuses: { c: { status: "running", health: "healthy" } } }, error: undefined });
useSWR.mockReturnValue({ data: { status: "running", health: "healthy" }, error: undefined });
render(<Status service={{ container: "c", server: "s" }} />);
expect(screen.getByText("docker.healthy")).toBeInTheDocument();
useSWR.mockReturnValue({ data: { statuses: { c: { status: "running", health: "unhealthy" } } }, error: undefined });
useSWR.mockReturnValue({ data: { status: "running", health: "unhealthy" }, error: undefined });
render(<Status service={{ container: "c", server: "s" }} />);
expect(screen.getByText("docker.unhealthy")).toBeInTheDocument();
useSWR.mockReturnValue({ data: { statuses: { c: { status: "partial 1/2" } } }, error: undefined });
useSWR.mockReturnValue({ data: { status: "partial 1/2" }, error: undefined });
render(<Status service={{ container: "c", server: "s" }} />);
expect(screen.getByText("docker.partial 1/2")).toBeInTheDocument();
useSWR.mockReturnValue({ data: { statuses: { c: { status: "exited" } } }, error: undefined });
useSWR.mockReturnValue({ data: { status: "exited" }, error: undefined });
render(<Status service={{ container: "c", server: "s" }} />);
expect(screen.getByText("docker.exited")).toBeInTheDocument();
useSWR.mockReturnValue({ data: { statuses: {} }, error: undefined });
useSWR.mockReturnValue({ data: { status: "not found" }, error: undefined });
render(<Status service={{ container: "c", server: "s" }} />);
expect(screen.getByText("docker.not_found")).toBeInTheDocument();
});
it("renders starting health when container is running and starting", () => {
useSWR.mockReturnValue({ data: { statuses: { c: { status: "running", health: "starting" } } }, error: undefined });
useSWR.mockReturnValue({ data: { status: "running", health: "starting" }, error: undefined });
render(<Status service={{ container: "c", server: "s" }} />);
expect(screen.getByText("docker.starting")).toBeInTheDocument();
});
it("renders the remaining container states instead of falling through to unknown", () => {
const states = {
created: "docker.created",
paused: "docker.paused",
restarting: "docker.restarting",
removing: "docker.removing",
dead: "docker.dead",
};
Object.entries(states).forEach(([status, label]) => {
useSWR.mockReturnValue({ data: { statuses: { c: { status } } }, error: undefined });
render(<Status service={{ container: "c", server: "s" }} />);
expect(screen.getByText(label)).toBeInTheDocument();
});
expect(screen.queryByText("docker.unknown")).not.toBeInTheDocument();
});
it("colors problem states as warnings and merely stopped states neutrally", () => {
useSWR.mockReturnValue({ data: { statuses: { c: { status: "restarting" } } }, error: undefined });
const { container: restarting } = render(<Status service={{ container: "c", server: "s" }} style="dot" />);
expect(restarting.querySelector(".bg-orange-400")).toBeInTheDocument();
useSWR.mockReturnValue({ data: { statuses: { c: { status: "paused" } } }, error: undefined });
const { container: paused } = render(<Status service={{ container: "c", server: "s" }} style="dot" />);
expect(paused.querySelector(".bg-orange-400")).not.toBeInTheDocument();
});
it("does not surface health for containers that are not running", () => {
useSWR.mockReturnValue({
data: { statuses: { c: { status: "paused", health: "unhealthy" } } },
error: undefined,
});
render(<Status service={{ container: "c", server: "s" }} />);
expect(screen.getByText("docker.paused")).toBeInTheDocument();
expect(screen.queryByText("docker.unhealthy")).not.toBeInTheDocument();
});
it("renders a dot when style is dot", () => {
useSWR.mockReturnValue({ data: { statuses: { c: { status: "running" } } }, error: undefined });
useSWR.mockReturnValue({ data: { status: "running" }, error: undefined });
const { container } = render(<Status service={{ container: "c", server: "s" }} style="dot" />);
+1 -1
View File
@@ -1,6 +1,6 @@
import ErrorBoundary from "components/errorboundry";
import { useTranslation } from "next-i18next/pages";
import ErrorBoundary from "components/errorboundry";
import components from "widgets/components";
export default function Widget({ widget, service }) {
+2 -16
View File
@@ -4,14 +4,11 @@ import { useContext, useMemo } from "react";
import { BlockHighlightContext } from "./highlight-context";
import { SettingsContext } from "utils/contexts/settings";
import { evaluateHighlight, getHighlightClass } from "utils/highlights";
export default function Block({ value, highlightValue, label, field }) {
const { t } = useTranslation();
const highlightConfig = useContext(BlockHighlightContext);
const { settings } = useContext(SettingsContext);
const refined = settings?.blockStyle === "refined";
const highlight = useMemo(() => {
if (!highlightConfig) return null;
@@ -48,20 +45,9 @@ export default function Block({ value, highlightValue, label, field }) {
data-highlight-level={highlight?.level}
data-highlight-source={highlight?.source}
>
<div className="font-thin text-sm">{value === undefined || value === null ? "-" : value}</div>
<div
className={classNames(
refined ? "text-[13px] font-medium tabular-nums" : "font-thin text-sm",
"service-block-value",
)}
>
{value === undefined || value === null ? "-" : value}
</div>
<div
className={classNames(
refined ? "text-[10px] font-semibold uppercase tracking-wider opacity-60" : "font-bold text-xs uppercase",
applyToValueOnly && "text-theme-700 dark:text-theme-200",
"service-block-label",
)}
className={classNames("font-bold text-xs uppercase", applyToValueOnly && "text-theme-700 dark:text-theme-200")}
>
{t(label)}
</div>
@@ -39,24 +39,6 @@ describe("components/services/widget/block", () => {
expect(el.className).toContain("danger-class");
});
it("uses the standard block styles by default", () => {
const { container } = renderWithProviders(<Block label="some.label" value="1" />, { settings: {} });
expect(container.querySelector(".service-block-value").className).toContain("font-thin");
expect(container.querySelector(".service-block-label").className).toContain("font-bold");
});
it("applies refined block styles when blockStyle is set", () => {
const { container } = renderWithProviders(<Block label="some.label" value="1" />, {
settings: { blockStyle: "refined" },
});
const value = container.querySelector(".service-block-value");
expect(value.className).toContain("tabular-nums");
expect(value.className).not.toContain("font-thin");
expect(container.querySelector(".service-block-label").className).toContain("tracking-wider");
});
it("prefers highlightValue over the rendered value for numeric highlighting", () => {
const highlightConfig = {
levels: { warn: "warn-class" },
+1 -1
View File
@@ -1,9 +1,9 @@
import { useContext, useMemo } from "react";
import { SettingsContext } from "utils/contexts/settings";
import Error from "./error";
import { BlockHighlightContext } from "./highlight-context";
import { SettingsContext } from "utils/contexts/settings";
import { buildHighlightConfig } from "utils/highlights";
const ALIASED_WIDGETS = {
-1
View File
@@ -1,6 +1,5 @@
import classNames from "classnames";
import { useContext } from "react";
import { TabContext } from "utils/contexts/tab";
function slugify(tabName) {
-1
View File
@@ -2,7 +2,6 @@ import { Popover, Transition } from "@headlessui/react";
import classNames from "classnames";
import { Fragment, useContext } from "react";
import { IoColorPalette } from "react-icons/io5";
import { ColorContext } from "utils/contexts/color";
const colors = [
+1 -5
View File
@@ -12,11 +12,7 @@ export default function SignOut() {
return (
<div id="signout" className="rounded-full flex align-middle self-center mr-3">
<button
type="button"
onClick={() => signOut({ callbackUrl: "/auth/signin?autologin=0" })}
className="outline-hidden"
>
<button type="button" onClick={() => signOut({ callbackUrl: "/" })} className="outline-hidden">
<MdLogout className="text-theme-800 dark:text-theme-200 w-6 h-6 cursor-pointer" aria-hidden="true" />
<span className="sr-only">{t("auth.signout")}</span>
</button>
+1 -2
View File
@@ -32,7 +32,6 @@ describe("components/toggles/signout", () => {
const { getByRole } = render(<SignOut />);
fireEvent.click(getByRole("button"));
// Not "/", which would bounce straight back into the provider when auto-login is on
expect(signOut).toHaveBeenCalledWith({ callbackUrl: "/auth/signin?autologin=0" });
expect(signOut).toHaveBeenCalledWith({ callbackUrl: "/" });
});
});

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