`, ``, ``, ``, and ` `.
+- **Trailing spaces**: Do NOT leave trailing whitespace on any line. Do
+ NOT use two-space line breaks — use a blank line instead.
+- **Bare URLs**: Bare URLs are permitted and do not need to be wrapped
+ in angle brackets.
+- **Table formatting**: Align table columns with padding when the table fits
+ within 80 characters. If the table exceeds 80 characters or triggers an MD060
+ linter warning, switch to a compact format using single spaces only. This
+ applies to the separator row as well—it should be written as `| --- |`,
+ not `|--|`.
+
+ Example of correct layout:
+
+ ```markdown
+ | Col1 | Col2 |
+ | ------ | ------ |
+ | Value1 | Value2 |
+ ```
+
+ Do NOT use extra padding or alignment characters beyond single spaces.
+
+**Rationale**: Uniform Markdown formatting improves readability for both
+humans and AI agents that consume project documentation.
+
+## Other
+
+### Accessibility
+
+All interactive components must be keyboard-operable and screen-reader
+friendly:
+
+- Use semantic HTML controls for every interaction (`button`, `a`, `input`,
+ `select`). Do **not** attach `onClick` to `div`, `span`, or icon elements —
+ wrap them in a ``.
+- Every interactive element must have an accessible name. Icon-only buttons
+ must carry `aria-label={intl.getMessage('key')}` — the label string must be
+ a dedicated translation key (see existing usage in `Pagination.tsx`,
+ `CopiedText.tsx`, `Input.tsx`).
+- Visible labels pair a `` with a matching input `id`.
+- Focusable rows acting as buttons must be keyboard-activatable: `tabindex` of
+ `0` plus an `onKeyDown` handler for Enter/Space.
+- Any `id` used for labeling must be unique per rendered instance.
+
+### Translations
+
+- All user-facing strings must be localized. Add new keys to the base locale
+ `src/__locales/en.json`; other locales are managed externally via Twosky.
+- Access translations via the `intl` object from `panel/common/intl`:
+ - `intl.getMessage('key', values?)` — returns a plain localized string.
+ Pass interpolation values as the second argument, referenced inside the
+ translation value as `%name%` placeholders (e.g.
+ `intl.getMessage('client_blocked', { name })` → `"%name% blocked"`).
+ - `intl.getPlural('key', number, values?)` — returns a pluralized string
+ selected by `number`. Plural values are pipe-delimited into three forms:
+ `zero | singular | plural`, each separated by `|`. The leading `|`
+ denotes the zero form (used when `number === 0`); it is usually left
+ empty, which is why values start with `|`. Use `%count%` (and other
+ `%name%`) placeholders for the interpolated count, e.g.
+ `"| %count% query total | %count% queries total"`. Always pass the count
+ as the second argument and reuse the existing plural key rather than
+ branching into separate `getMessage` calls (see
+ `Dashboard/blocks/Header/Header.tsx`, `Settings/helpers.ts`).
+- **Never use dynamic keys.** Always pass a string literal as the `key`
+ argument to `getMessage`/`getPlural`. Do not build keys from variables,
+ template literals, or concatenation (e.g. `getMessage(\`btn\_\${action}\`)`).
+ Dynamic keys cannot be statically detected, so the translation tooling will
+ not pick them up and the strings will go untranslated.
+- Do not add duplicate keys to any locale file.
+- Run `npm run translations:check` after adding or editing translation keys.
+- Every `aria-*` string must have its own dedicated translation key.
diff --git a/client_v2/DEVELOPMENT.md b/client_v2/DEVELOPMENT.md
new file mode 100644
index 00000000000..91106c8eec9
--- /dev/null
+++ b/client_v2/DEVELOPMENT.md
@@ -0,0 +1,321 @@
+# Development Guide — client_v2
+
+This document explains how to set up a development environment for the
+`client_v2` frontend, run it locally, and contribute code. It is intended for
+developers working on the next-generation AdGuard Home web UI.
+
+For code guidelines, architecture, and project structure, see
+[AGENTS.md](./AGENTS.md). For user-facing documentation, see the root
+[README.md](../README.md).
+
+## Table of Contents
+
+- [Development Guide — client\_v2](#development-guide--client_v2)
+ - [Table of Contents](#table-of-contents)
+ - [Prerequisites](#prerequisites)
+ - [Getting Started](#getting-started)
+ - [Clone and Install](#clone-and-install)
+ - [Running the Dev Server](#running-the-dev-server)
+ - [Production Build](#production-build)
+ - [Development Workflow](#development-workflow)
+ - [Branching and Pull Requests](#branching-and-pull-requests)
+ - [Code Style and Formatting](#code-style-and-formatting)
+ - [Type Checking](#type-checking)
+ - [Running Tests](#running-tests)
+ - [Translations](#translations)
+ - [Common Tasks](#common-tasks)
+ - [Adding a Component](#adding-a-component)
+ - [Adding a Store](#adding-a-store)
+ - [Adding an API Method](#adding-an-api-method)
+ - [Adding a Translation Key](#adding-a-translation-key)
+ - [Debugging in VS Code](#debugging-in-vs-code)
+ - [Troubleshooting](#troubleshooting)
+ - [Dev server cannot find `../AdguardHome.yaml`](#dev-server-cannot-find-adguardhomeyaml)
+ - [API requests return 404 or connection refused](#api-requests-return-404-or-connection-refused)
+ - [E2e tests fail to start](#e2e-tests-fail-to-start)
+ - ["Multiple instances of Solid" errors in tests](#multiple-instances-of-solid-errors-in-tests)
+ - [Port already in use](#port-already-in-use)
+ - [Type errors after adding imports](#type-errors-after-adding-imports)
+ - [Additional Resources](#additional-resources)
+
+## Prerequisites
+
+Before you begin, install the following tools:
+
+- **Node.js** 20 LTS or newer. The project uses `@types/node` 22; a current
+ LTS release is recommended.
+- **npm** 10.x or newer (bundled with Node.js).
+- **An AdGuard Home backend** running locally. The dev server proxies
+ `/control` API requests to it, so a built `AdGuardHome` binary in the
+ repository root (or any reachable instance) is required for the UI to
+ function. See the root [README.md](../README.md) and
+ [HACKING.md](../HACKING.md) for building the Go backend.
+
+No global packages are required — all tooling (Webpack, ESLint, Vitest,
+Playwright, TypeScript) is installed locally via `npm install`.
+
+## Getting Started
+
+### Clone and Install
+
+All commands in this guide are run from the `client_v2/` directory.
+
+```sh
+git clone https://github.com/AdguardTeam/AdGuardHome.git
+cd AdGuardHome/client_v2
+npm install
+```
+
+`npm install` also runs the Playwright browser install postinstall step. If it
+does not, run `npx playwright install` manually before running e2e tests.
+
+### Running the Dev Server
+
+The dev server reads the backend host and port from the root
+`AdguardHome.yaml` file (the `bind_host` and `bind_port` fields) and proxies
+`/control` requests to that backend. Make sure a backend config exists at
+`../AdguardHome.yaml` and that the AdGuard Home backend is running.
+
+Start the dev server:
+
+```sh
+npm run dev
+```
+
+By default the dev server listens on `bind_port + 8000` (for example, if the
+backend runs on port `3000`, the dev server runs on `11000`). Override the
+port with the `DEV_SERVER_PORT` environment variable:
+
+```sh
+DEV_SERVER_PORT=8080 npm run dev
+```
+
+The server opens the main dashboard automatically. The four HTML entry points
+are:
+
+- `main` — the dashboard / control panel (HashRouter SPA)
+- `install.html` — the first-run setup wizard
+- `login.html` — the sign-in page
+- `forgot_password.html` — the password-reset flow
+
+If the backend is at `http://127.0.0.1:3000`, the dev server is at
+`http://127.0.0.1:11000`, and you can open `install.html`, `login.html`, or
+`forgot_password.html` directly.
+
+### Production Build
+
+The production build emits static assets into `../build/static/`, which the Go
+binary embeds at compile time.
+
+```sh
+npm run build-prod
+```
+
+To produce a development build without starting a server:
+
+```sh
+npm run build-dev
+```
+
+To watch for changes and rebuild continuously:
+
+```sh
+npm run watch
+```
+
+## Development Workflow
+
+### Branching and Pull Requests
+
+- Create a feature branch from `master` (for example,
+ `feature/my-new-feature`).
+- Keep changes focused; one concern per pull request.
+- Ensure the full check passes before opening a PR (see
+ [Running Tests](#running-tests)).
+- Follow the contribution workflow described in the root
+ [CONTRIBUTING.md](../CONTRIBUTING.md).
+
+### Code Style and Formatting
+
+Lint the source:
+
+```sh
+npm run lint
+```
+
+Auto-fix lint issues and format with Prettier:
+
+```sh
+npm run lint:fix
+```
+
+Linting and formatting rules are defined in `.eslintrc` files and `.prettierrc`.
+Do not modify these configs without justification. For the full set of code
+conventions (path aliases, component structure, CSS modules, reactivity, etc.),
+see [AGENTS.md](./AGENTS.md).
+
+### Type Checking
+
+Run the TypeScript compiler in check-only mode:
+
+```sh
+npm run typecheck
+```
+
+Watch mode for continuous type checking:
+
+```sh
+npm run typecheck:watch
+```
+
+### Running Tests
+
+Run the full local gate (lint + typecheck + unit tests) with a single command:
+
+```sh
+npm run check
+```
+
+Unit tests (Vitest, jsdom environment):
+
+```sh
+npm run test # single run
+npm run test:watch # watch mode
+```
+
+Unit tests live under `src/__tests__/` and match `*.{test,spec}.{ts,tsx}`.
+Mirror the source path under `__tests__/` when adding new tests.
+
+End-to-end tests (Playwright):
+
+```sh
+npm run test:e2e # run all e2e specs
+npm run test:e2e:interactive # open the Playwright UI
+npm run test:e2e:debug # debug mode
+npm run test:e2e:codegen # generate tests by recording actions
+```
+
+E2E specs live in `tests/e2e/`. They run against a real AdGuard Home backend
+that Playwright starts automatically (see the `webServer` config in
+`playwright.config.ts`). The `./AdGuardHome` binary must be present in the
+repository root for this to work. E2E tests are **not** part of
+`npm run check`; run them explicitly.
+
+### Translations
+
+The base locale is `src/__locales/en.json`. After adding or editing
+translation keys, verify consistency:
+
+```sh
+npm run translations:check
+```
+
+This audits source files for `intl.getMessage` / `intl.getPlural` usage and
+reports missing, unused, and dynamic keys. Other locales are managed
+externally via Twosky (configured in the root `.twosky.json`).
+
+## Common Tasks
+
+### Adding a Component
+
+1. Create a PascalCase directory under `src/components/` (or
+ `src/common/controls/` for primitives, `src/common/ui/` for higher-level
+ UI).
+2. Add an `index.tsx` and a co-located `*.module.pcss`.
+3. Place sub-components in a `blocks/` subfolder.
+4. Import styles as `s` and compose classes with `clsx` (imported as `cn`).
+5. Use the `panel/*` path alias for all `src/` imports.
+6. Add a co-located unit test under `src/__tests__/`.
+
+See the [Code Quality](./AGENTS.md#code-quality) section of `AGENTS.md` for
+the full set of conventions.
+
+### Adding a Store
+
+1. Create a `camelCase.ts` module under `src/stores/` with a module-scoped
+ `createStore` singleton.
+2. Async actions set a `processing*` flag, call `apiClient`, then `setState`.
+3. Surface errors via `addErrorToast` from the toasts store.
+4. Add a test under `src/__tests__/stores/`.
+
+### Adding an API Method
+
+Add a typed method to the `Api` class in `src/api/Api.ts`. All HTTP calls must
+go through this singleton — do not call `fetch` directly from components or
+stores.
+
+### Adding a Translation Key
+
+1. Add the key to `src/__locales/en.json`.
+2. Use it via `intl.getMessage('key', values?)` or
+ `intl.getPlural('key', number, values?)`.
+3. Never build keys dynamically — always pass a string literal.
+4. Run `npm run translations:check` to verify.
+
+### Debugging in VS Code
+
+- The dev server runs Webpack Dev Server with hot module replacement and
+ source maps (`eval-source-map`).
+- Set breakpoints directly in `src/` files; VS Code maps them to the bundled
+ output automatically.
+- For unit tests, use the Vitest VS Code extension or run
+ `npm run test:watch` and attach a debugger to the Vitest process.
+
+## Troubleshooting
+
+### Dev server cannot find `../AdguardHome.yaml`
+
+The dev server reads the backend host and port from the root
+`AdguardHome.yaml`. Make sure the file exists at the repository root and
+contains `bind_host` and `bind_port`. If it is missing, the server falls back
+to `0.0.0.0:80` and the proxy will not reach the backend. Start the backend or
+create the config file.
+
+### API requests return 404 or connection refused
+
+The dev server proxies `/control` to the backend at `bind_host:bind_port`.
+Verify the AdGuard Home backend is running and that the port in
+`AdguardHome.yaml` matches the running instance.
+
+### E2e tests fail to start
+
+E2e tests require the `./AdGuardHome` binary in the repository root. Build the
+backend first (see [HACKING.md](../HACKING.md)). Playwright starts the backend
+automatically; if a server is already running on port 3000, stop it first:
+
+```sh
+kill $(lsof -ti :3000)
+```
+
+### "Multiple instances of Solid" errors in tests
+
+The Vitest config (see `vitest.config.ts`) forces a single `solid-js` instance
+via `dedupe` and `ssr.noExternal`. If you add a dependency that pulls in its
+own copy of `solid-js`, add it to the `dedupe` and `noExternal` lists.
+
+### Port already in use
+
+Stop any process occupying the dev server port or override it with
+`DEV_SERVER_PORT`:
+
+```sh
+kill $(lsof -ti :11000)
+DEV_SERVER_PORT=8080 npm run dev
+```
+
+### Type errors after adding imports
+
+Use the `panel/*` alias (`panel/api/Api`, `panel/initialState`, etc.) instead
+of deep relative paths. The alias is defined in `tsconfig.json` and consumed
+by Webpack, Vitest, and ESLint.
+
+## Additional Resources
+
+- [AGENTS.md](./AGENTS.md) — code guidelines, architecture, and project
+ structure
+- [README.md](../README.md) — AdGuard Home project overview and user manual
+- [HACKING.md](../HACKING.md) — AdGuard Home developer guidelines (Go backend)
+- [CONTRIBUTING.md](../CONTRIBUTING.md) — contribution workflow
+- [CHANGELOG.md](../CHANGELOG.md) — changelog
+- [AdGuard Code Guidelines](https://github.com/AdguardTeam/CodeGuidelines) —
+ general code style guidelines
diff --git a/client_v2/babel.config.cjs b/client_v2/babel.config.cjs
new file mode 100644
index 00000000000..69cca5bd3be
--- /dev/null
+++ b/client_v2/babel.config.cjs
@@ -0,0 +1,10 @@
+module.exports = (api) => {
+ api.cache(false);
+ return {
+ presets: [
+ '@babel/preset-env',
+ '@babel/preset-typescript',
+ 'babel-preset-solid',
+ ],
+ };
+};
diff --git a/client_v2/constants.js b/client_v2/constants.js
new file mode 100644
index 00000000000..1f2c6fe5414
--- /dev/null
+++ b/client_v2/constants.js
@@ -0,0 +1,6 @@
+export const BUILD_ENVS = {
+ dev: 'development',
+ prod: 'production',
+};
+
+export const BASE_URL = 'control';
diff --git a/client_v2/dev.eslintrc b/client_v2/dev.eslintrc
new file mode 100644
index 00000000000..9d7e549340b
--- /dev/null
+++ b/client_v2/dev.eslintrc
@@ -0,0 +1,6 @@
+{
+ "extends": ".eslintrc",
+ "rules": {
+ "no-debugger":"warn"
+ }
+}
diff --git a/client_v2/global.d.ts b/client_v2/global.d.ts
new file mode 100644
index 00000000000..910d022a69a
--- /dev/null
+++ b/client_v2/global.d.ts
@@ -0,0 +1,14 @@
+declare module '*.svg' {
+ const src: string;
+ export default src;
+}
+
+declare module '*.module.pcss' {
+ const classes: { [key: string]: string };
+ export default classes;
+}
+
+declare module '*.pcss' {
+ const classes: { [key: string]: string };
+ export default classes;
+}
diff --git a/client_v2/package-lock.json b/client_v2/package-lock.json
new file mode 100644
index 00000000000..9d1eee694f3
--- /dev/null
+++ b/client_v2/package-lock.json
@@ -0,0 +1,16147 @@
+{
+ "name": "dashboard",
+ "version": "3.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "dashboard",
+ "version": "3.0.0",
+ "dependencies": {
+ "@adguard/translate": "^1.0.2",
+ "@ark-ui/solid": "^5.37.1",
+ "@modular-forms/solid": "^0.25.0",
+ "@solidjs/router": "^0.15.0",
+ "chart.js": "^4.4.0",
+ "clsx": "^2.1.1",
+ "copy-to-clipboard": "^3.3.3",
+ "countries-and-timezones": "^3.6.0",
+ "date-fns": "^4.1.0",
+ "ipaddr.js": "^2.2.0",
+ "js-yaml": "^4.1.0",
+ "lodash": "^4.17.19",
+ "nanoid": "^5.1.0",
+ "qs": "^6.14.0",
+ "solid-js": "^1.9.0",
+ "solid-transition-group": "^0.3.0"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.24.5",
+ "@babel/preset-env": "^7.24.5",
+ "@babel/preset-typescript": "^7.24.1",
+ "@playwright/test": "1.56.0",
+ "@solidjs/testing-library": "^0.8.0",
+ "@testing-library/jest-dom": "^6.9.1",
+ "@testing-library/user-event": "^14.6.1",
+ "@types/lodash": "^4.17.4",
+ "@types/node": "^22.13.10",
+ "@types/qs": "^6.15.1",
+ "@typescript-eslint/eslint-plugin": "^8.60.1",
+ "@typescript-eslint/parser": "^8.60.1",
+ "autoprefixer": "^10.4.21",
+ "babel-loader": "^9.1.3",
+ "babel-preset-solid": "^1.9.0",
+ "clean-webpack-plugin": "^4.0.0",
+ "copy-webpack-plugin": "^14.0.0",
+ "cross-env": "^7.0.3",
+ "css-loader": "^7.1.2",
+ "eslint": "^8.57.0",
+ "eslint-config-prettier": "^9.1.0",
+ "eslint-import-resolver-typescript": "^3.5.5",
+ "eslint-plugin-import": "^2.29.0",
+ "eslint-plugin-prettier": "^5.1.3",
+ "eslint-plugin-solid": "^0.14.0",
+ "html-webpack-plugin": "^5.6.0",
+ "jsdom": "^26.1.0",
+ "mini-css-extract-plugin": "^2.9.0",
+ "playwright": "^1.61.0",
+ "postcss": "^8.5.6",
+ "postcss-import": "^16.1.1",
+ "postcss-loader": "^8.1.1",
+ "postcss-nested": "^7.0.2",
+ "prettier": "^3.2.5",
+ "stylelint": "^16.5.0",
+ "typescript": "^5.4.0",
+ "vite": "^6.0.0",
+ "vite-plugin-solid": "^2.11.12",
+ "vitest": "^4.1.0",
+ "webpack": "^5.91.0",
+ "webpack-cli": "^5.1.4",
+ "webpack-dev-server": "^5.0.4",
+ "webpack-merge": "^5.10.0"
+ }
+ },
+ "node_modules/@adguard/translate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@adguard/translate/-/translate-1.0.2.tgz",
+ "integrity": "sha512-TeQV4ngse/fcpFDCrtU5VqjZfQsdytSuGT8wF4biIM6c6v6W/tkUdrTDhNItq126hAZe65V8QgipTAp8qHdeZw==",
+ "license": "MIT"
+ },
+ "node_modules/@adobe/css-tools": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz",
+ "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@ark-ui/solid": {
+ "version": "5.37.1",
+ "resolved": "https://registry.npmjs.org/@ark-ui/solid/-/solid-5.37.1.tgz",
+ "integrity": "sha512-0/X+mcQyFbbfICmd4LMUR/bR45f1WOmlXFzYCBNU8AQLidCu8Sd6UFdjSOuXwjgenkYRYHOqxXW2vvzNQxg7XQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@internationalized/date": "3.12.2",
+ "@zag-js/accordion": "1.41.2",
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/angle-slider": "1.41.2",
+ "@zag-js/async-list": "1.41.2",
+ "@zag-js/auto-resize": "1.41.2",
+ "@zag-js/avatar": "1.41.2",
+ "@zag-js/carousel": "1.41.2",
+ "@zag-js/cascade-select": "1.41.2",
+ "@zag-js/checkbox": "1.41.2",
+ "@zag-js/clipboard": "1.41.2",
+ "@zag-js/collapsible": "1.41.2",
+ "@zag-js/collection": "1.41.2",
+ "@zag-js/color-picker": "1.41.2",
+ "@zag-js/color-utils": "1.41.2",
+ "@zag-js/combobox": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/date-input": "1.41.2",
+ "@zag-js/date-picker": "1.41.2",
+ "@zag-js/date-utils": "1.41.2",
+ "@zag-js/dialog": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/drawer": "1.41.2",
+ "@zag-js/editable": "1.41.2",
+ "@zag-js/file-upload": "1.41.2",
+ "@zag-js/file-utils": "1.41.2",
+ "@zag-js/floating-panel": "1.41.2",
+ "@zag-js/focus-trap": "1.41.2",
+ "@zag-js/focus-visible": "1.41.2",
+ "@zag-js/highlight-word": "1.41.2",
+ "@zag-js/hover-card": "1.41.2",
+ "@zag-js/i18n-utils": "1.41.2",
+ "@zag-js/image-cropper": "1.41.2",
+ "@zag-js/json-tree-utils": "1.41.2",
+ "@zag-js/listbox": "1.41.2",
+ "@zag-js/marquee": "1.41.2",
+ "@zag-js/menu": "1.41.2",
+ "@zag-js/navigation-menu": "1.41.2",
+ "@zag-js/number-input": "1.41.2",
+ "@zag-js/pagination": "1.41.2",
+ "@zag-js/password-input": "1.41.2",
+ "@zag-js/pin-input": "1.41.2",
+ "@zag-js/popover": "1.41.2",
+ "@zag-js/presence": "1.41.2",
+ "@zag-js/progress": "1.41.2",
+ "@zag-js/qr-code": "1.41.2",
+ "@zag-js/radio-group": "1.41.2",
+ "@zag-js/rating-group": "1.41.2",
+ "@zag-js/scroll-area": "1.41.2",
+ "@zag-js/select": "1.41.2",
+ "@zag-js/signature-pad": "1.41.2",
+ "@zag-js/slider": "1.41.2",
+ "@zag-js/solid": "1.41.2",
+ "@zag-js/splitter": "1.41.2",
+ "@zag-js/steps": "1.41.2",
+ "@zag-js/switch": "1.41.2",
+ "@zag-js/tabs": "1.41.2",
+ "@zag-js/tags-input": "1.41.2",
+ "@zag-js/timer": "1.41.2",
+ "@zag-js/toast": "1.41.2",
+ "@zag-js/toggle": "1.41.2",
+ "@zag-js/toggle-group": "1.41.2",
+ "@zag-js/tooltip": "1.41.2",
+ "@zag-js/tour": "1.41.2",
+ "@zag-js/tree-view": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ },
+ "peerDependencies": {
+ "solid-js": ">=1.6.0"
+ }
+ },
+ "node_modules/@asamuzakjp/css-color": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
+ "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/css-calc": "^2.1.3",
+ "@csstools/css-color-parser": "^3.0.9",
+ "@csstools/css-parser-algorithms": "^3.0.4",
+ "@csstools/css-tokenizer": "^3.0.3",
+ "lru-cache": "^10.4.3"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
+ "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
+ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helpers": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/core/node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@babel/core/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
+ "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-annotate-as-pure": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz",
+ "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
+ "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.29.7",
+ "@babel/helper-validator-option": "^7.29.7",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-create-class-features-plugin": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz",
+ "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.29.7",
+ "@babel/helper-member-expression-to-functions": "^7.29.7",
+ "@babel/helper-optimise-call-expression": "^7.29.7",
+ "@babel/helper-replace-supers": "^7.29.7",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7",
+ "@babel/traverse": "^7.29.7",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-create-regexp-features-plugin": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.29.7.tgz",
+ "integrity": "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.29.7",
+ "regexpu-core": "^6.3.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-define-polyfill-provider": {
+ "version": "0.6.8",
+ "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz",
+ "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-plugin-utils": "^7.28.6",
+ "debug": "^4.4.3",
+ "lodash.debounce": "^4.0.8",
+ "resolve": "^1.22.11"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/@babel/helper-define-polyfill-provider/node_modules/resolve": {
+ "version": "1.22.12",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
+ "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-member-expression-to-functions": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz",
+ "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-optimise-call-expression": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz",
+ "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
+ "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-remap-async-to-generator": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.29.7.tgz",
+ "integrity": "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.29.7",
+ "@babel/helper-wrap-function": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-replace-supers": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz",
+ "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-member-expression-to-functions": "^7.29.7",
+ "@babel/helper-optimise-call-expression": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-skip-transparent-expression-wrappers": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz",
+ "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-wrap-function": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.29.7.tgz",
+ "integrity": "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.29.7",
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
+ "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
+ "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.7"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.29.7.tgz",
+ "integrity": "sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.29.7.tgz",
+ "integrity": "sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.29.7.tgz",
+ "integrity": "sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.7.tgz",
+ "integrity": "sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.29.7.tgz",
+ "integrity": "sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7",
+ "@babel/plugin-transform-optional-chaining": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.13.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.29.7.tgz",
+ "integrity": "sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-proposal-private-property-in-object": {
+ "version": "7.21.0-placeholder-for-preset-env.2",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
+ "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-import-assertions": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.29.7.tgz",
+ "integrity": "sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-import-attributes": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz",
+ "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-jsx": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz",
+ "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-typescript": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz",
+ "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-unicode-sets-regex": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz",
+ "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.18.6",
+ "@babel/helper-plugin-utils": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-arrow-functions": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz",
+ "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-async-generator-functions": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz",
+ "integrity": "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-remap-async-to-generator": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-async-to-generator": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.29.7.tgz",
+ "integrity": "sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-remap-async-to-generator": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-block-scoped-functions": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.29.7.tgz",
+ "integrity": "sha512-cUSmjh72N+rN4PrkFlN1dJwNCwjVp5d38/CQrEsFggkD10UiFlBFgdH3tv5dNsLuHY+3S8db2xCHjhZcv5WgvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-block-scoping": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz",
+ "integrity": "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-class-properties": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz",
+ "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-class-static-block": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.29.7.tgz",
+ "integrity": "sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.12.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-classes": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz",
+ "integrity": "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.29.7",
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-globals": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-replace-supers": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-computed-properties": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.29.7.tgz",
+ "integrity": "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/template": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-destructuring": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz",
+ "integrity": "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-dotall-regex": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.29.7.tgz",
+ "integrity": "sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-duplicate-keys": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.29.7.tgz",
+ "integrity": "sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.7.tgz",
+ "integrity": "sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-dynamic-import": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.29.7.tgz",
+ "integrity": "sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-explicit-resource-management": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.29.7.tgz",
+ "integrity": "sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/plugin-transform-destructuring": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-exponentiation-operator": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.29.7.tgz",
+ "integrity": "sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-export-namespace-from": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.29.7.tgz",
+ "integrity": "sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-for-of": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.7.tgz",
+ "integrity": "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-function-name": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.29.7.tgz",
+ "integrity": "sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-json-strings": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.29.7.tgz",
+ "integrity": "sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-literals": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.29.7.tgz",
+ "integrity": "sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-logical-assignment-operators": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.29.7.tgz",
+ "integrity": "sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-member-expression-literals": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.29.7.tgz",
+ "integrity": "sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-amd": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.29.7.tgz",
+ "integrity": "sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-commonjs": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz",
+ "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-systemjs": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.7.tgz",
+ "integrity": "sha512-TM2ZcQLoG2/y4HODiStCo10DibYhWhGWAwVv+EQKmG/7GFl0N+AAmUiXOMKM+aiJ9XBJ9AHVZBvTzMnJ2sM3cQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-umd": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.29.7.tgz",
+ "integrity": "sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-named-capturing-groups-regex": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.7.tgz",
+ "integrity": "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-new-target": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.29.7.tgz",
+ "integrity": "sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-nullish-coalescing-operator": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz",
+ "integrity": "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-numeric-separator": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.29.7.tgz",
+ "integrity": "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-object-rest-spread": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.29.7.tgz",
+ "integrity": "sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/plugin-transform-destructuring": "^7.29.7",
+ "@babel/plugin-transform-parameters": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-object-super": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.29.7.tgz",
+ "integrity": "sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-replace-supers": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-optional-catch-binding": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.29.7.tgz",
+ "integrity": "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-optional-chaining": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz",
+ "integrity": "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-parameters": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.29.7.tgz",
+ "integrity": "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-private-methods": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz",
+ "integrity": "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-private-property-in-object": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.29.7.tgz",
+ "integrity": "sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.29.7",
+ "@babel/helper-create-class-features-plugin": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-property-literals": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.29.7.tgz",
+ "integrity": "sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-regenerator": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.7.tgz",
+ "integrity": "sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-regexp-modifiers": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.29.7.tgz",
+ "integrity": "sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-reserved-words": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.29.7.tgz",
+ "integrity": "sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-shorthand-properties": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz",
+ "integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-spread": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.7.tgz",
+ "integrity": "sha512-/u5K1QWada7tbYNqTjMh96718g9NTwh9tfPJMsSmVsQwGT447FskV+KcfeXkXq2GWki4EM/MuTdmBec+hOuVTQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-sticky-regex": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.29.7.tgz",
+ "integrity": "sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-template-literals": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz",
+ "integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-typeof-symbol": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.29.7.tgz",
+ "integrity": "sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-typescript": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz",
+ "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.29.7",
+ "@babel/helper-create-class-features-plugin": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7",
+ "@babel/plugin-syntax-typescript": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-unicode-escapes": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.29.7.tgz",
+ "integrity": "sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-unicode-property-regex": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.29.7.tgz",
+ "integrity": "sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-unicode-regex": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.29.7.tgz",
+ "integrity": "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-unicode-sets-regex": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.29.7.tgz",
+ "integrity": "sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/preset-env": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.7.tgz",
+ "integrity": "sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.29.7",
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-validator-option": "^7.29.7",
+ "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.29.7",
+ "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.29.7",
+ "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.29.7",
+ "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.7",
+ "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.29.7",
+ "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.29.7",
+ "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2",
+ "@babel/plugin-syntax-import-assertions": "^7.29.7",
+ "@babel/plugin-syntax-import-attributes": "^7.29.7",
+ "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6",
+ "@babel/plugin-transform-arrow-functions": "^7.29.7",
+ "@babel/plugin-transform-async-generator-functions": "^7.29.7",
+ "@babel/plugin-transform-async-to-generator": "^7.29.7",
+ "@babel/plugin-transform-block-scoped-functions": "^7.29.7",
+ "@babel/plugin-transform-block-scoping": "^7.29.7",
+ "@babel/plugin-transform-class-properties": "^7.29.7",
+ "@babel/plugin-transform-class-static-block": "^7.29.7",
+ "@babel/plugin-transform-classes": "^7.29.7",
+ "@babel/plugin-transform-computed-properties": "^7.29.7",
+ "@babel/plugin-transform-destructuring": "^7.29.7",
+ "@babel/plugin-transform-dotall-regex": "^7.29.7",
+ "@babel/plugin-transform-duplicate-keys": "^7.29.7",
+ "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.7",
+ "@babel/plugin-transform-dynamic-import": "^7.29.7",
+ "@babel/plugin-transform-explicit-resource-management": "^7.29.7",
+ "@babel/plugin-transform-exponentiation-operator": "^7.29.7",
+ "@babel/plugin-transform-export-namespace-from": "^7.29.7",
+ "@babel/plugin-transform-for-of": "^7.29.7",
+ "@babel/plugin-transform-function-name": "^7.29.7",
+ "@babel/plugin-transform-json-strings": "^7.29.7",
+ "@babel/plugin-transform-literals": "^7.29.7",
+ "@babel/plugin-transform-logical-assignment-operators": "^7.29.7",
+ "@babel/plugin-transform-member-expression-literals": "^7.29.7",
+ "@babel/plugin-transform-modules-amd": "^7.29.7",
+ "@babel/plugin-transform-modules-commonjs": "^7.29.7",
+ "@babel/plugin-transform-modules-systemjs": "^7.29.7",
+ "@babel/plugin-transform-modules-umd": "^7.29.7",
+ "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.7",
+ "@babel/plugin-transform-new-target": "^7.29.7",
+ "@babel/plugin-transform-nullish-coalescing-operator": "^7.29.7",
+ "@babel/plugin-transform-numeric-separator": "^7.29.7",
+ "@babel/plugin-transform-object-rest-spread": "^7.29.7",
+ "@babel/plugin-transform-object-super": "^7.29.7",
+ "@babel/plugin-transform-optional-catch-binding": "^7.29.7",
+ "@babel/plugin-transform-optional-chaining": "^7.29.7",
+ "@babel/plugin-transform-parameters": "^7.29.7",
+ "@babel/plugin-transform-private-methods": "^7.29.7",
+ "@babel/plugin-transform-private-property-in-object": "^7.29.7",
+ "@babel/plugin-transform-property-literals": "^7.29.7",
+ "@babel/plugin-transform-regenerator": "^7.29.7",
+ "@babel/plugin-transform-regexp-modifiers": "^7.29.7",
+ "@babel/plugin-transform-reserved-words": "^7.29.7",
+ "@babel/plugin-transform-shorthand-properties": "^7.29.7",
+ "@babel/plugin-transform-spread": "^7.29.7",
+ "@babel/plugin-transform-sticky-regex": "^7.29.7",
+ "@babel/plugin-transform-template-literals": "^7.29.7",
+ "@babel/plugin-transform-typeof-symbol": "^7.29.7",
+ "@babel/plugin-transform-unicode-escapes": "^7.29.7",
+ "@babel/plugin-transform-unicode-property-regex": "^7.29.7",
+ "@babel/plugin-transform-unicode-regex": "^7.29.7",
+ "@babel/plugin-transform-unicode-sets-regex": "^7.29.7",
+ "@babel/preset-modules": "0.1.6-no-external-plugins",
+ "babel-plugin-polyfill-corejs2": "^0.4.15",
+ "babel-plugin-polyfill-corejs3": "^0.14.0",
+ "babel-plugin-polyfill-regenerator": "^0.6.6",
+ "core-js-compat": "^3.48.0",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/preset-env/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/preset-modules": {
+ "version": "0.1.6-no-external-plugins",
+ "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz",
+ "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.0.0",
+ "@babel/types": "^7.4.4",
+ "esutils": "^2.0.2"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/@babel/preset-typescript": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.29.7.tgz",
+ "integrity": "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7",
+ "@babel/helper-validator-option": "^7.29.7",
+ "@babel/plugin-syntax-jsx": "^7.29.7",
+ "@babel/plugin-transform-modules-commonjs": "^7.29.7",
+ "@babel/plugin-transform-typescript": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
+ "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
+ "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-globals": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
+ "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@cacheable/memory": {
+ "version": "2.0.9",
+ "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.9.tgz",
+ "integrity": "sha512-HdMx6DoGywB30vacDbBsITbIX4pgFqj1zsrV58jZBUw3klzkNoXhj7qOqAgledhxG7YZI5rBSJg7Zp8/VG0DuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@cacheable/utils": "^2.4.1",
+ "@keyv/bigmap": "^1.3.1",
+ "hookified": "^1.15.1",
+ "keyv": "^5.6.0"
+ }
+ },
+ "node_modules/@cacheable/memory/node_modules/@keyv/bigmap": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz",
+ "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hashery": "^1.4.0",
+ "hookified": "^1.15.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "keyv": "^5.6.0"
+ }
+ },
+ "node_modules/@cacheable/memory/node_modules/keyv": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz",
+ "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@keyv/serialize": "^1.1.1"
+ }
+ },
+ "node_modules/@cacheable/utils": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.4.1.tgz",
+ "integrity": "sha512-eiFgzCbIneyMlLOmNG4g9xzF7Hv3Mga4LjxjcSC/ues6VYq2+gUbQI8JqNuw/ZM8tJIeIaBGpswAsqV2V7ApgA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hashery": "^1.5.1",
+ "keyv": "^5.6.0"
+ }
+ },
+ "node_modules/@cacheable/utils/node_modules/keyv": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz",
+ "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@keyv/serialize": "^1.1.1"
+ }
+ },
+ "node_modules/@csstools/color-helpers": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
+ "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@csstools/css-calc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
+ "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-color-parser": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
+ "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/color-helpers": "^5.1.0",
+ "@csstools/css-calc": "^2.1.4"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
+ "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-syntax-patches-for-csstree": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz",
+ "integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "peerDependencies": {
+ "css-tree": "^3.2.1"
+ },
+ "peerDependenciesMeta": {
+ "css-tree": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
+ "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@csstools/media-query-list-parser": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.3.tgz",
+ "integrity": "sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/selector-specificity": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz",
+ "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "postcss-selector-parser": "^7.0.0"
+ }
+ },
+ "node_modules/@discoveryjs/json-ext": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz",
+ "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/@dual-bundle/import-meta-resolve": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.2.1.tgz",
+ "integrity": "sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/JounQin"
+ }
+ },
+ "node_modules/@emnapi/core": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
+ "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.1",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
+ "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
+ "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
+ "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.6.0",
+ "globals": "^13.19.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
+ "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "8.57.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz",
+ "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.5",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
+ "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.6",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
+ "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.5",
+ "@floating-ui/utils": "^0.2.11"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.11",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
+ "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
+ "license": "MIT"
+ },
+ "node_modules/@humanwhocodes/config-array": {
+ "version": "0.13.0",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
+ "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==",
+ "deprecated": "Use @eslint/config-array instead",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanwhocodes/object-schema": "^2.0.3",
+ "debug": "^4.3.1",
+ "minimatch": "^3.0.5"
+ },
+ "engines": {
+ "node": ">=10.10.0"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
+ "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/object-schema": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
+ "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
+ "deprecated": "Use @eslint/object-schema instead",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@internationalized/date": {
+ "version": "3.12.2",
+ "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.2.tgz",
+ "integrity": "sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/helpers": "^0.5.0"
+ }
+ },
+ "node_modules/@internationalized/number": {
+ "version": "3.6.6",
+ "resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.6.tgz",
+ "integrity": "sha512-iFgmQaXHE0vytNfpLZWOC2mEJCBRzcUxt53Xf/yCXG93lRvqas237i3r7X4RKMwO3txiyZD4mQjKAByFv6UGSQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/helpers": "^0.5.0"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/source-map": {
+ "version": "0.3.11",
+ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
+ "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.25"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@jsonjoy.com/base64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz",
+ "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/buffers": {
+ "version": "17.67.0",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz",
+ "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/codegen": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz",
+ "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/fs-core": {
+ "version": "4.57.7",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.57.7.tgz",
+ "integrity": "sha512-GDKuYHjP7vAI1kjBo73V+STKr9XIMZknW/xirpRW/EcShX0IKSev/ALafeRfC8Q331nodrXUFu04PugPB0MAhw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jsonjoy.com/fs-node-builtins": "4.57.7",
+ "@jsonjoy.com/fs-node-utils": "4.57.7",
+ "thingies": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/fs-fsa": {
+ "version": "4.57.7",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.57.7.tgz",
+ "integrity": "sha512-1rWsah2nZtRbNeP+c61QcfGfVrJXBmBD0Hm7Akvv4C9MKEasXnbiOS//iH3T3HwUSSBATGrfSp0Xi8nlNhATeQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jsonjoy.com/fs-core": "4.57.7",
+ "@jsonjoy.com/fs-node-builtins": "4.57.7",
+ "@jsonjoy.com/fs-node-utils": "4.57.7",
+ "thingies": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/fs-node": {
+ "version": "4.57.7",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.57.7.tgz",
+ "integrity": "sha512-xhnyeyEVTiIOibFvda/5n89nChMLCPKHHM2WQ+GGDf6+U/IrQBW3Qx6x+Uq1bkDbxBkybLOdIGoBtVBrE8Nngg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jsonjoy.com/fs-core": "4.57.7",
+ "@jsonjoy.com/fs-node-builtins": "4.57.7",
+ "@jsonjoy.com/fs-node-utils": "4.57.7",
+ "@jsonjoy.com/fs-print": "4.57.7",
+ "@jsonjoy.com/fs-snapshot": "4.57.7",
+ "glob-to-regex.js": "^1.0.0",
+ "thingies": "^2.5.0"
+ },
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/fs-node-builtins": {
+ "version": "4.57.7",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.57.7.tgz",
+ "integrity": "sha512-LWqfY1m+uAosjwM1RrKhMkUnP9jcq1RUczHsNO779ovm1E9v8I/pmj04eBAcoBjhC7ltcPbNFGyRJ5JqSJ7Jdg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/fs-node-to-fsa": {
+ "version": "4.57.7",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.57.7.tgz",
+ "integrity": "sha512-9T0zC9LKcAWXDoTLRdLMoJ0seOvJ5bgDKq1tSBoQAFQpPDstQUeV1Oe7PLypdu7F2D3ddRstmwgeNUEN/VaZ4Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jsonjoy.com/fs-fsa": "4.57.7",
+ "@jsonjoy.com/fs-node-builtins": "4.57.7",
+ "@jsonjoy.com/fs-node-utils": "4.57.7"
+ },
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/fs-node-utils": {
+ "version": "4.57.7",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.57.7.tgz",
+ "integrity": "sha512-jjWSDOsfcog2cZnUCwX5AHmlIq6b6wx5Pz/2LAcNjJ62Rajwg89Fy7ubN+lDHew0/1reLDa9Z5urybYadhh37g==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jsonjoy.com/fs-node-builtins": "4.57.7"
+ },
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/fs-print": {
+ "version": "4.57.7",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.57.7.tgz",
+ "integrity": "sha512-mFM4P4Gjq0QQHkLnXzPYPEMFrAoe6a5Myedgb6+CmL+nGd3MKvTxYPuD7N1dLIH9RBy1fLdzxd80qvuK8xrx3Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jsonjoy.com/fs-node-utils": "4.57.7",
+ "tree-dump": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/fs-snapshot": {
+ "version": "4.57.7",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.57.7.tgz",
+ "integrity": "sha512-1GS3+plfm2giB3PqokiqyydyqYTPLcCQIKSkp0TdMNRh3KVk7rqRM6U785FLlVRG7XLmkc0KWr215OY+22K3QA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jsonjoy.com/buffers": "^17.65.0",
+ "@jsonjoy.com/fs-node-utils": "4.57.7",
+ "@jsonjoy.com/json-pack": "^17.65.0",
+ "@jsonjoy.com/util": "^17.65.0"
+ },
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/base64": {
+ "version": "17.67.0",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.67.0.tgz",
+ "integrity": "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/codegen": {
+ "version": "17.67.0",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz",
+ "integrity": "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pack": {
+ "version": "17.67.0",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz",
+ "integrity": "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jsonjoy.com/base64": "17.67.0",
+ "@jsonjoy.com/buffers": "17.67.0",
+ "@jsonjoy.com/codegen": "17.67.0",
+ "@jsonjoy.com/json-pointer": "17.67.0",
+ "@jsonjoy.com/util": "17.67.0",
+ "hyperdyperid": "^1.2.0",
+ "thingies": "^2.5.0",
+ "tree-dump": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pointer": {
+ "version": "17.67.0",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz",
+ "integrity": "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jsonjoy.com/util": "17.67.0"
+ },
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/util": {
+ "version": "17.67.0",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.67.0.tgz",
+ "integrity": "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jsonjoy.com/buffers": "17.67.0",
+ "@jsonjoy.com/codegen": "17.67.0"
+ },
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/json-pack": {
+ "version": "1.21.0",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz",
+ "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jsonjoy.com/base64": "^1.1.2",
+ "@jsonjoy.com/buffers": "^1.2.0",
+ "@jsonjoy.com/codegen": "^1.0.0",
+ "@jsonjoy.com/json-pointer": "^1.0.2",
+ "@jsonjoy.com/util": "^1.9.0",
+ "hyperdyperid": "^1.2.0",
+ "thingies": "^2.5.0",
+ "tree-dump": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/json-pack/node_modules/@jsonjoy.com/buffers": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz",
+ "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/json-pointer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz",
+ "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jsonjoy.com/codegen": "^1.0.0",
+ "@jsonjoy.com/util": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/util": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz",
+ "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jsonjoy.com/buffers": "^1.0.0",
+ "@jsonjoy.com/codegen": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@jsonjoy.com/util/node_modules/@jsonjoy.com/buffers": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz",
+ "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/@keyv/serialize": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz",
+ "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@kurkle/color": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
+ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
+ "license": "MIT"
+ },
+ "node_modules/@leichtgewicht/ip-codec": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
+ "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@modular-forms/solid": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/@modular-forms/solid/-/solid-0.25.1.tgz",
+ "integrity": "sha512-issNZ3xl4tj+1K7KT4dNTQaRq5SmVUXgUPeGTMjtrAzCeTnwM/u6vUxSuTY2bcMw4GzTxreFXLx1xMMhrFkt0A==",
+ "license": "MIT",
+ "dependencies": {
+ "valibot": "^1.0.0-beta.6"
+ },
+ "peerDependencies": {
+ "solid-js": "^1.3.1"
+ }
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
+ "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
+ }
+ },
+ "node_modules/@noble/hashes": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz",
+ "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nolyfill/is-core-module": {
+ "version": "1.0.39",
+ "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz",
+ "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.4.0"
+ }
+ },
+ "node_modules/@peculiar/asn1-cms": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.8.0.tgz",
+ "integrity": "sha512-NgekZOrSJFSBFLFoLfwePguAWAx7z1+f2TEsWFUMyiqqfntZ4+S/S5hzqME3q4pCA0iOsFKdwiQ35dwY24eVqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/asn1-schema": "^2.8.0",
+ "@peculiar/asn1-x509": "^2.8.0",
+ "@peculiar/asn1-x509-attr": "^2.8.0",
+ "asn1js": "^3.0.10",
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/@peculiar/asn1-csr": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.8.0.tgz",
+ "integrity": "sha512-akbF8+uvleHs8sejNPQxwmVFuInAg6FMNHOwMILXfP518YfFJwdR3jr6oNUPOaEJfuEhn/vkNOCIT6ASUd4mbg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/asn1-schema": "^2.8.0",
+ "@peculiar/asn1-x509": "^2.8.0",
+ "asn1js": "^3.0.10",
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/@peculiar/asn1-ecc": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.8.0.tgz",
+ "integrity": "sha512-ohwlk+u9Rv2NOAY1c6MfHj45ATVF8R1DUN/WCgABiRtLi2ZftlZWZX7KvpAbU8v9xPcmoILfELeEABj/rn18AQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/asn1-schema": "^2.8.0",
+ "@peculiar/asn1-x509": "^2.8.0",
+ "asn1js": "^3.0.10",
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/@peculiar/asn1-pfx": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.8.0.tgz",
+ "integrity": "sha512-5yof1ytoB++RQtaFbqSUJ8pxDJtZT6vbVqZ8XoJ61ph7UjNVvfFwAilnCodqkNsAodpy13gDhoxZXw00pghnyg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/asn1-cms": "^2.8.0",
+ "@peculiar/asn1-pkcs8": "^2.8.0",
+ "@peculiar/asn1-rsa": "^2.8.0",
+ "@peculiar/asn1-schema": "^2.8.0",
+ "asn1js": "^3.0.10",
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/@peculiar/asn1-pkcs8": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.8.0.tgz",
+ "integrity": "sha512-qAKXtLpBEw9LqhKpjw3ajZSXlBur+ipW+y2ivVBQAG6F6qRx94yO+1ZR4mvw+YaCfKSaOzLeYEzsPaBp4SJELA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/asn1-schema": "^2.8.0",
+ "@peculiar/asn1-x509": "^2.8.0",
+ "asn1js": "^3.0.10",
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/@peculiar/asn1-pkcs9": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.8.0.tgz",
+ "integrity": "sha512-b5nDWCnkV60+cQ141D6sVVwK9nz64R5n3zSVnklGd+ECdkW2Ol3U1a6yYFlalpSOaD557yuJB64A+q42jG7lUQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/asn1-cms": "^2.8.0",
+ "@peculiar/asn1-pfx": "^2.8.0",
+ "@peculiar/asn1-pkcs8": "^2.8.0",
+ "@peculiar/asn1-schema": "^2.8.0",
+ "@peculiar/asn1-x509": "^2.8.0",
+ "@peculiar/asn1-x509-attr": "^2.8.0",
+ "asn1js": "^3.0.10",
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/@peculiar/asn1-rsa": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.8.0.tgz",
+ "integrity": "sha512-zHEUlCqB2mk7x2lxDwHHJy7hWZOPdGHVlsmITWKB5/PbQo61atbu9PJ/0r9dQNMwFzbKPXZ8uK8/91eUhRznSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/asn1-schema": "^2.8.0",
+ "@peculiar/asn1-x509": "^2.8.0",
+ "asn1js": "^3.0.10",
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/@peculiar/asn1-schema": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.8.0.tgz",
+ "integrity": "sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/utils": "^2.0.2",
+ "asn1js": "^3.0.10",
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/@peculiar/asn1-x509": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.8.0.tgz",
+ "integrity": "sha512-N0CMuhWUzsWEVq6F1q9X6+VKUnWzSW+cSVg+aPaGGwDdbFoFWTYgin5MHwXgpWd6y9COMBxnfy/Qc+Xc7F0Zwg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/asn1-schema": "^2.8.0",
+ "@peculiar/utils": "^2.0.2",
+ "asn1js": "^3.0.10",
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/@peculiar/asn1-x509-attr": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.8.0.tgz",
+ "integrity": "sha512-tHjkfS/qhMnmrlB2J9NhflQlQ7In3khO3CfmVrriOlpTeErY9ZIKOso1hQ5JQiyrJ7ShvqVPk7E5fQmbclkSKA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/asn1-schema": "^2.8.0",
+ "@peculiar/asn1-x509": "^2.8.0",
+ "asn1js": "^3.0.10",
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/@peculiar/utils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz",
+ "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/@peculiar/x509": {
+ "version": "1.14.3",
+ "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz",
+ "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/asn1-cms": "^2.6.0",
+ "@peculiar/asn1-csr": "^2.6.0",
+ "@peculiar/asn1-ecc": "^2.6.0",
+ "@peculiar/asn1-pkcs9": "^2.6.0",
+ "@peculiar/asn1-rsa": "^2.6.0",
+ "@peculiar/asn1-schema": "^2.6.0",
+ "@peculiar/asn1-x509": "^2.6.0",
+ "pvtsutils": "^1.3.6",
+ "reflect-metadata": "^0.2.2",
+ "tslib": "^2.8.1",
+ "tsyringe": "^4.10.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@pkgr/core": {
+ "version": "0.3.6",
+ "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz",
+ "integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/pkgr"
+ }
+ },
+ "node_modules/@playwright/test": {
+ "version": "1.56.0",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0.tgz",
+ "integrity": "sha512-Tzh95Twig7hUwwNe381/K3PggZBZblKUe2wv25oIpzWLr6Z0m4KgV1ZVIjnR6GM9ANEqjZD7XsZEa6JL/7YEgg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright": "1.56.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@playwright/test/node_modules/playwright": {
+ "version": "1.56.0",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0.tgz",
+ "integrity": "sha512-X5Q1b8lOdWIE4KAoHpW3SE8HvUB+ZZsUoN64ZhjnN8dOb1UpujxBtENGiZFE+9F/yhzJwYa+ca3u43FeLbboHA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.56.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/@playwright/test/node_modules/playwright-core": {
+ "version": "1.56.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0.tgz",
+ "integrity": "sha512-1SXl7pMfemAMSDn5rkPeZljxOCYAmQnYLBTExuh6E8USHXGSX3dx6lYZN/xPpTz1vimXmPA9CDnILvmJaB8aSQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz",
+ "integrity": "sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.0.tgz",
+ "integrity": "sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.0.tgz",
+ "integrity": "sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.0.tgz",
+ "integrity": "sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.0.tgz",
+ "integrity": "sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.0.tgz",
+ "integrity": "sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.0.tgz",
+ "integrity": "sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.0.tgz",
+ "integrity": "sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.0.tgz",
+ "integrity": "sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.0.tgz",
+ "integrity": "sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.0.tgz",
+ "integrity": "sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.0.tgz",
+ "integrity": "sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.0.tgz",
+ "integrity": "sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.0.tgz",
+ "integrity": "sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.0.tgz",
+ "integrity": "sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.0.tgz",
+ "integrity": "sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.0.tgz",
+ "integrity": "sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.0.tgz",
+ "integrity": "sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.0.tgz",
+ "integrity": "sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.0.tgz",
+ "integrity": "sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.0.tgz",
+ "integrity": "sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.0.tgz",
+ "integrity": "sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.0.tgz",
+ "integrity": "sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.0.tgz",
+ "integrity": "sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.0.tgz",
+ "integrity": "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rtsao/scc": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
+ "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@solid-primitives/keyed": {
+ "version": "1.5.3",
+ "resolved": "https://registry.npmjs.org/@solid-primitives/keyed/-/keyed-1.5.3.tgz",
+ "integrity": "sha512-zNadtyYBhJSOjXtogkGHmRxjGdz9KHc8sGGVAGlUABkE8BED2tbIZoxkwSqzOwde8OcUEH0bb5DLZUWIMvyBSA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "solid-js": "^1.6.12"
+ }
+ },
+ "node_modules/@solid-primitives/refs": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@solid-primitives/refs/-/refs-1.1.3.tgz",
+ "integrity": "sha512-aam02fjNKpBteewF/UliPSQCVJsIIGOLEWQOh+ll6R/QePzBOOBMcC4G+5jTaO75JuUS1d/14Q1YXT3X0Ow6iA==",
+ "license": "MIT",
+ "dependencies": {
+ "@solid-primitives/utils": "^6.4.0"
+ },
+ "peerDependencies": {
+ "solid-js": "^1.6.12"
+ }
+ },
+ "node_modules/@solid-primitives/transition-group": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@solid-primitives/transition-group/-/transition-group-1.1.2.tgz",
+ "integrity": "sha512-gnHS0OmcdjeoHN9n7Khu8KNrOlRc8a2weETDt2YT6o1zeW/XtUC6Db3Q9pkMU/9cCKdEmN4b0a/41MKAHRhzWA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "solid-js": "^1.6.12"
+ }
+ },
+ "node_modules/@solid-primitives/utils": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@solid-primitives/utils/-/utils-6.4.0.tgz",
+ "integrity": "sha512-AeGTBg8Wtkh/0s+evyLtP8piQoS4wyqqQaAFs2HJcFMMjYAtUgo+ZPduRXLjPlqKVc2ejeR544oeqpbn8Egn8A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "solid-js": "^1.6.12"
+ }
+ },
+ "node_modules/@solidjs/router": {
+ "version": "0.15.4",
+ "resolved": "https://registry.npmjs.org/@solidjs/router/-/router-0.15.4.tgz",
+ "integrity": "sha512-WOpgg9a9T638cR+5FGbFi/IV4l2FpmBs1GpIMSPa0Ce9vyJN7Wts+X2PqMf9IYn0zUj2MlSJtm1gp7/HI/n5TQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "solid-js": "^1.8.6"
+ }
+ },
+ "node_modules/@solidjs/testing-library": {
+ "version": "0.8.10",
+ "resolved": "https://registry.npmjs.org/@solidjs/testing-library/-/testing-library-0.8.10.tgz",
+ "integrity": "sha512-qdeuIerwyq7oQTIrrKvV0aL9aFeuwTd86VYD3afdq5HYEwoox1OBTJy4y8A3TFZr8oAR0nujYgCzY/8wgHGfeQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@testing-library/dom": "^10.4.0"
+ },
+ "engines": {
+ "node": ">= 14"
+ },
+ "peerDependencies": {
+ "@solidjs/router": ">=0.9.0",
+ "solid-js": ">=1.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@solidjs/router": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@swc/helpers": {
+ "version": "0.5.23",
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz",
+ "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.8.0"
+ }
+ },
+ "node_modules/@testing-library/dom": {
+ "version": "10.4.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
+ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/runtime": "^7.12.5",
+ "@types/aria-query": "^5.0.1",
+ "aria-query": "5.3.0",
+ "dom-accessibility-api": "^0.5.9",
+ "lz-string": "^1.5.0",
+ "picocolors": "1.1.1",
+ "pretty-format": "^27.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@testing-library/jest-dom": {
+ "version": "6.9.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz",
+ "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@adobe/css-tools": "^4.4.0",
+ "aria-query": "^5.0.0",
+ "css.escape": "^1.5.1",
+ "dom-accessibility-api": "^0.6.3",
+ "picocolors": "^1.1.1",
+ "redent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=14",
+ "npm": ">=6",
+ "yarn": ">=1"
+ }
+ },
+ "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
+ "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@testing-library/user-event": {
+ "version": "14.6.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz",
+ "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": ">=7.21.4"
+ }
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.2",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
+ "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/aria-query": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
+ "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/body-parser": {
+ "version": "1.19.6",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
+ "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/bonjour": {
+ "version": "3.5.13",
+ "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz",
+ "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/connect": {
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
+ "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/connect-history-api-fallback": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz",
+ "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/express-serve-static-core": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/express": {
+ "version": "4.17.25",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz",
+ "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "^4.17.33",
+ "@types/qs": "*",
+ "@types/serve-static": "^1"
+ }
+ },
+ "node_modules/@types/express-serve-static-core": {
+ "version": "4.19.8",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz",
+ "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*",
+ "@types/send": "*"
+ }
+ },
+ "node_modules/@types/glob": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz",
+ "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/minimatch": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/html-minifier-terser": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
+ "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/http-errors": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
+ "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/http-proxy": {
+ "version": "1.17.17",
+ "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz",
+ "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json5": {
+ "version": "0.0.29",
+ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
+ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/lodash": {
+ "version": "4.17.24",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz",
+ "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/mime": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
+ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/minimatch": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
+ "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "22.19.21",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.21.tgz",
+ "integrity": "sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/qs": {
+ "version": "6.15.1",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz",
+ "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/range-parser": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
+ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/retry": {
+ "version": "0.12.2",
+ "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz",
+ "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/serve-index": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz",
+ "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/express": "*"
+ }
+ },
+ "node_modules/@types/serve-static": {
+ "version": "1.15.10",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz",
+ "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-errors": "*",
+ "@types/node": "*",
+ "@types/send": "<1"
+ }
+ },
+ "node_modules/@types/serve-static/node_modules/@types/send": {
+ "version": "0.17.6",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz",
+ "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/mime": "^1",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/sockjs": {
+ "version": "0.3.36",
+ "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz",
+ "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.61.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz",
+ "integrity": "sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.61.0",
+ "@typescript-eslint/type-utils": "8.61.0",
+ "@typescript-eslint/utils": "8.61.0",
+ "@typescript-eslint/visitor-keys": "8.61.0",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.61.0",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.61.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.0.tgz",
+ "integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.61.0",
+ "@typescript-eslint/types": "8.61.0",
+ "@typescript-eslint/typescript-estree": "8.61.0",
+ "@typescript-eslint/visitor-keys": "8.61.0",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.61.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.0.tgz",
+ "integrity": "sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.61.0",
+ "@typescript-eslint/types": "^8.61.0",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.61.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz",
+ "integrity": "sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.61.0",
+ "@typescript-eslint/visitor-keys": "8.61.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.61.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz",
+ "integrity": "sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.61.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz",
+ "integrity": "sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.61.0",
+ "@typescript-eslint/typescript-estree": "8.61.0",
+ "@typescript-eslint/utils": "8.61.0",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.61.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.0.tgz",
+ "integrity": "sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.61.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz",
+ "integrity": "sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.61.0",
+ "@typescript-eslint/tsconfig-utils": "8.61.0",
+ "@typescript-eslint/types": "8.61.0",
+ "@typescript-eslint/visitor-keys": "8.61.0",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.61.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.0.tgz",
+ "integrity": "sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.61.0",
+ "@typescript-eslint/types": "8.61.0",
+ "@typescript-eslint/typescript-estree": "8.61.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.61.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz",
+ "integrity": "sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.61.0",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@ungap/structured-clone": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz",
+ "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/@unrs/resolver-binding-android-arm-eabi": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz",
+ "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-android-arm64": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz",
+ "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-darwin-arm64": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz",
+ "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-darwin-x64": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz",
+ "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-freebsd-x64": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz",
+ "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz",
+ "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz",
+ "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-arm64-gnu": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz",
+ "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-arm64-musl": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz",
+ "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-loong64-gnu": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz",
+ "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-loong64-musl": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz",
+ "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz",
+ "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz",
+ "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-riscv64-musl": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz",
+ "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-s390x-gnu": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz",
+ "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-x64-gnu": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz",
+ "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-linux-x64-musl": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz",
+ "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-openharmony-arm64": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz",
+ "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-wasm32-wasi": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz",
+ "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "1.10.0",
+ "@emnapi/runtime": "1.10.0",
+ "@napi-rs/wasm-runtime": "^1.1.4"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@unrs/resolver-binding-win32-arm64-msvc": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz",
+ "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-win32-ia32-msvc": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz",
+ "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@unrs/resolver-binding-win32-x64-msvc": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz",
+ "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@vitest/expect": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz",
+ "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.1.0",
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "4.1.9",
+ "@vitest/utils": "4.1.9",
+ "chai": "^6.2.2",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz",
+ "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "4.1.9",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.21"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz",
+ "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz",
+ "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "4.1.9",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz",
+ "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.9",
+ "@vitest/utils": "4.1.9",
+ "magic-string": "^0.30.21",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz",
+ "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz",
+ "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.9",
+ "convert-source-map": "^2.0.0",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@webassemblyjs/ast": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
+ "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/helper-numbers": "1.13.2",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2"
+ }
+ },
+ "node_modules/@webassemblyjs/floating-point-hex-parser": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz",
+ "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/helper-api-error": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz",
+ "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/helper-buffer": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz",
+ "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/helper-numbers": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz",
+ "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/floating-point-hex-parser": "1.13.2",
+ "@webassemblyjs/helper-api-error": "1.13.2",
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@webassemblyjs/helper-wasm-bytecode": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz",
+ "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/helper-wasm-section": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz",
+ "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-buffer": "1.14.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/wasm-gen": "1.14.1"
+ }
+ },
+ "node_modules/@webassemblyjs/ieee754": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz",
+ "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@xtuc/ieee754": "^1.2.0"
+ }
+ },
+ "node_modules/@webassemblyjs/leb128": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz",
+ "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@webassemblyjs/utf8": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz",
+ "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@webassemblyjs/wasm-edit": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz",
+ "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-buffer": "1.14.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/helper-wasm-section": "1.14.1",
+ "@webassemblyjs/wasm-gen": "1.14.1",
+ "@webassemblyjs/wasm-opt": "1.14.1",
+ "@webassemblyjs/wasm-parser": "1.14.1",
+ "@webassemblyjs/wast-printer": "1.14.1"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-gen": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz",
+ "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/ieee754": "1.13.2",
+ "@webassemblyjs/leb128": "1.13.2",
+ "@webassemblyjs/utf8": "1.13.2"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-opt": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz",
+ "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-buffer": "1.14.1",
+ "@webassemblyjs/wasm-gen": "1.14.1",
+ "@webassemblyjs/wasm-parser": "1.14.1"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-parser": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz",
+ "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@webassemblyjs/helper-api-error": "1.13.2",
+ "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+ "@webassemblyjs/ieee754": "1.13.2",
+ "@webassemblyjs/leb128": "1.13.2",
+ "@webassemblyjs/utf8": "1.13.2"
+ }
+ },
+ "node_modules/@webassemblyjs/wast-printer": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz",
+ "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.14.1",
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@webpack-cli/configtest": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz",
+ "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.15.0"
+ },
+ "peerDependencies": {
+ "webpack": "5.x.x",
+ "webpack-cli": "5.x.x"
+ }
+ },
+ "node_modules/@webpack-cli/info": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz",
+ "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.15.0"
+ },
+ "peerDependencies": {
+ "webpack": "5.x.x",
+ "webpack-cli": "5.x.x"
+ }
+ },
+ "node_modules/@webpack-cli/serve": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz",
+ "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.15.0"
+ },
+ "peerDependencies": {
+ "webpack": "5.x.x",
+ "webpack-cli": "5.x.x"
+ },
+ "peerDependenciesMeta": {
+ "webpack-dev-server": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@xtuc/ieee754": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
+ "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@xtuc/long": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
+ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@zag-js/accordion": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/accordion/-/accordion-1.41.2.tgz",
+ "integrity": "sha512-7G//V7svGGT8k5avw7bbQvbRC0Q/9QtX51b4iyAB1alR9E5mFd6Ch8q4njwcXClMQ7xePS3jUfVnzVGiRInEiQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/anatomy": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/anatomy/-/anatomy-1.41.2.tgz",
+ "integrity": "sha512-Fm9hqdrvaCzCsdcf19G8WZxYtHElKltkGHdhqMEt4XU+ULTr1DK7KbOtDDv9J27CuzqSLALUz5QfRjPftoKHwg==",
+ "license": "MIT"
+ },
+ "node_modules/@zag-js/angle-slider": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/angle-slider/-/angle-slider-1.41.2.tgz",
+ "integrity": "sha512-+7bZHAZx0MEbjTMr2tD+meFJ0EJwFfUEcTqmdLzFGr/ySAMCWlcadDBz+ZmrSn03aKLps8FxliVLzsFJNgUqIQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/rect-utils": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/aria-hidden": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/aria-hidden/-/aria-hidden-1.41.2.tgz",
+ "integrity": "sha512-qEcYmwlQr3qjA0T/IZ5a/o7fRUxfQ14tXjAFhR3GXCtxBKaqS+wnq/LN09Xw4bin3QWTINU+Z0oXFs9spWtNwA==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/dom-query": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/async-list": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/async-list/-/async-list-1.41.2.tgz",
+ "integrity": "sha512-NZZEIGFdeDp2uHjsLVegLAGJOYGwI9HPJI1V2c/P1TQmfmrfWyWELAvnnW4kWYVUKYD9TxKQkm6LvqpHrQzgfQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/core": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/auto-resize": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/auto-resize/-/auto-resize-1.41.2.tgz",
+ "integrity": "sha512-CYq+JQ1TTkEiK7OcNcMTS0f4wFvtmManvUltije5o50gDQ7vFYA81oQh4A9pAB1keUi9Zv06CNefFARaTcne5w==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/dom-query": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/avatar": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/avatar/-/avatar-1.41.2.tgz",
+ "integrity": "sha512-+4K0tRtIQysMGuERh5JRmn46uq7gJ6IjJd5DKj74VBtVVY8T6HGk0D0DZxmOIgADHP1qSvowLDoOX8kUyBHarQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/carousel": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/carousel/-/carousel-1.41.2.tgz",
+ "integrity": "sha512-H6sDxyWIzkvtHN4M/BYvFy7pi8j+kLEtfPZvgNl2+gRnfAHVK9/XqpoUsjw1DAo5Fh25pPuaTnh52Kml9OK0iA==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/scroll-snap": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/cascade-select": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/cascade-select/-/cascade-select-1.41.2.tgz",
+ "integrity": "sha512-dBByZmIAJU/B+YIAzczng05lCJXEwEm4df6GmUZtioKHZdeN+WEKUP4qzFDFZdytXympGfJCIBvtgf87gACYVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/collection": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dismissable": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/focus-visible": "1.41.2",
+ "@zag-js/popper": "1.41.2",
+ "@zag-js/rect-utils": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/checkbox": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/checkbox/-/checkbox-1.41.2.tgz",
+ "integrity": "sha512-aQ5dZWHUHRIw4cbLtzrR+dc8M0N6wSiiCml/zbU3ciHOTBXSrs2rKnp/L99xhi97Lj2uCEhpIZ704Ou/MjGuCg==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/focus-visible": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/clipboard": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/clipboard/-/clipboard-1.41.2.tgz",
+ "integrity": "sha512-FM+PNGEeY+YpF1dVr9d8kg3DQM66LRnkR4Mva1oprqnrCXTYWj+k7uig893ubSG2xw1GO5b/WJd27kSmNoKnmw==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/collapsible": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/collapsible/-/collapsible-1.41.2.tgz",
+ "integrity": "sha512-uMIp4rqd3iI6VFPMAOcbu9dh9WV4l85nNPYQiBtMKRHgbkfaZdfzF+E+NX3KquIeHW6JiBFUiIyCU0Sf2Cscdw==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/collection": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/collection/-/collection-1.41.2.tgz",
+ "integrity": "sha512-ZZWuvfPZI8ccWd4aLpuU47k1jSc9eO+F3FM3iBJuvgegCH6g3+HDEwGN6wdePHnYfv7zyIKCGKr816zXcIWQbw==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/color-picker": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/color-picker/-/color-picker-1.41.2.tgz",
+ "integrity": "sha512-UynyJ/bTBeSlq6ziHr9GVrFycDjVxfLFIb8Aivu/XvihrAAvkhXUxwy+1ax+hj7peGlTY590xoQAvQixV+McBA==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/color-utils": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dismissable": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/popper": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/color-utils": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/color-utils/-/color-utils-1.41.2.tgz",
+ "integrity": "sha512-Lsi5c2ztGZqud0oeDtAn3xFhgrYMaeaz1zi4p+mr0zXOuQNuZzfsrJ0stQ45s8L/xT8UJtGhYyEVy1+xjE4ARA==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/combobox": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/combobox/-/combobox-1.41.2.tgz",
+ "integrity": "sha512-V9jQteyQHs8ZNQx/FcA98P1NVZas/yjiW1V7s4L+h8MnRS3AK97+FAHAT83KCiZ34/vkpLF6ZEHI2zkcQpNXcg==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/collection": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dismissable": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/focus-visible": "1.41.2",
+ "@zag-js/live-region": "1.41.2",
+ "@zag-js/popper": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/core": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/core/-/core-1.41.2.tgz",
+ "integrity": "sha512-xXTN3zKwOtMI4+5dG2cG+T1B4WR3X9alXiYaPJKaGd4N2eYRj9JEPte3Hv9gtFm+RM0b9VIwksHEg25rqE4apw==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/date-input": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/date-input/-/date-input-1.41.2.tgz",
+ "integrity": "sha512-J8PdpEpM9TBxZBEE8/Nxi39LNJOZY7lilTbgcDABR5uiviZUk5++5GSdUTspcjFUIXx5SvqmdCk2RF7hsUEHVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/date-utils": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/live-region": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ },
+ "peerDependencies": {
+ "@internationalized/date": ">=3.0.0"
+ }
+ },
+ "node_modules/@zag-js/date-picker": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/date-picker/-/date-picker-1.41.2.tgz",
+ "integrity": "sha512-j9LaznCV4QCfrq/y/mNAN9XgIRFFg+414TxGT4TK8mfWouIaFvxEoKdGP0l/LL4DFv1NRDaRdSBBcw3eXtMVnA==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/date-utils": "1.41.2",
+ "@zag-js/dismissable": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/live-region": "1.41.2",
+ "@zag-js/popper": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ },
+ "peerDependencies": {
+ "@internationalized/date": ">=3.0.0"
+ }
+ },
+ "node_modules/@zag-js/date-utils": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/date-utils/-/date-utils-1.41.2.tgz",
+ "integrity": "sha512-jdcWLa5fYLTsvGWJkx4v/9Hzqf6UHdvJDeP6NxHpwoEmzYy7+AghYKBNSnRrJIBCQJgl1vM9OkpMvYrLNHr9jw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@internationalized/date": ">=3.0.0"
+ }
+ },
+ "node_modules/@zag-js/dialog": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/dialog/-/dialog-1.41.2.tgz",
+ "integrity": "sha512-t1N72snpFGiKj7cg2PivPdsy+X1YdgXPv7bzovpqjMLy0kN+gYTPoV1Iy/hQA+g021dz9XGgXzkDuU45sJqsFA==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/aria-hidden": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dismissable": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/focus-trap": "1.41.2",
+ "@zag-js/remove-scroll": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/dismissable": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/dismissable/-/dismissable-1.41.2.tgz",
+ "integrity": "sha512-hO/tFhRZ7S+LOOljGOQJIubbc3MXg41+iWR1yUXKl76cAenbxaCit1LZmUCwQPvRN0GndK6bDQo5ETjHZz/k7A==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/interact-outside": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/dom-query": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/dom-query/-/dom-query-1.41.2.tgz",
+ "integrity": "sha512-+eBk1nlJA312mNmY/GSThLRwcCRqMIL+A1pLsWvTlQLQjmH1/UxoAuv6l2yvRCT33XmC8FBlBIKnXhOCpDvIZA==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/types": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/drawer": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/drawer/-/drawer-1.41.2.tgz",
+ "integrity": "sha512-aJql6L0cfHd1wXbemfLcNnjqTA/CbwVgQdWEVn5Qci6zhy4UJXPQeBBfxfgPgEOAbW60gL/gaaXG3d9Vx6+6oA==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/aria-hidden": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dismissable": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/focus-trap": "1.41.2",
+ "@zag-js/remove-scroll": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/editable": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/editable/-/editable-1.41.2.tgz",
+ "integrity": "sha512-loTM1lrHBBqYfR8SJZrYayVdWHMpXCLVir+aDTQ8/d4bb/Gfl/L8CJNs3BRj3yt9zvLoWTLO+4LOY3hLyQSR2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/interact-outside": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/file-upload": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/file-upload/-/file-upload-1.41.2.tgz",
+ "integrity": "sha512-WFwIaKvHpCUjyYMvp42VoydT1WIP5DhDlpmG/nrF4i0ro7pLGb1A4dvyBrAfGcozCB88yR1y1iZKPDY1I9/uUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/file-utils": "1.41.2",
+ "@zag-js/i18n-utils": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/file-utils": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/file-utils/-/file-utils-1.41.2.tgz",
+ "integrity": "sha512-Ih+8ULbId0M+CFR4IsqG5y/0VLCk2l+1rgPH+21L40dlSB6z6qKSP2tG7W69Cj2/3vryZsn67ibn26iCPG/vOw==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/i18n-utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/floating-panel": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/floating-panel/-/floating-panel-1.41.2.tgz",
+ "integrity": "sha512-nJP3oZ4YrJh+7H5YdwNccSzPGXFqjQN1ujZ/xGDhegjz4XtL48QMFnAasxlRI8VGjse9Tj8VXlQZxXPrASGzlA==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/popper": "1.41.2",
+ "@zag-js/rect-utils": "1.41.2",
+ "@zag-js/store": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/focus-trap": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/focus-trap/-/focus-trap-1.41.2.tgz",
+ "integrity": "sha512-3QTtGUjFU2OLbyrDlyoYWvKZecCmtn/+bsfsHW159jJHiEGHVYK6CY8AI1ePsMk9gVay48bXh008j+lVli7gAw==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/dom-query": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/focus-visible": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/focus-visible/-/focus-visible-1.41.2.tgz",
+ "integrity": "sha512-oRjwtgafUdGVwLJUN6mKsnBQbez/CHYAPkPg1FxOnr5GFpEpr8oMTOZJ3wdPM2U1ynS9QnUUu/sXc3KQv/jX0Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/dom-query": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/highlight-word": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/highlight-word/-/highlight-word-1.41.2.tgz",
+ "integrity": "sha512-95zcZKqNrL7JlqAckfzHa+LRbnfoz1lj6skUhq/uHSnni1vH6+8fNWT8ruo9G7vpGopbyRmmcie5p41SbAwQjQ==",
+ "license": "MIT"
+ },
+ "node_modules/@zag-js/hover-card": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/hover-card/-/hover-card-1.41.2.tgz",
+ "integrity": "sha512-Xn9RVzgTkKaVzyJTDdBJiXmCleNi1/hmW8z73tC0vOiQvSSvPejg/JkzqTOLFODvQHK3NOw54QHZvmjYp9Mubw==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dismissable": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/popper": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/i18n-utils": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/i18n-utils/-/i18n-utils-1.41.2.tgz",
+ "integrity": "sha512-f1xqaEY79awBxgUyjFso0UEpIoEHZq+zRvB0nUVFHJRW7Ds/QhaFHKSRnf2QBTlP/ObvCT225R+piNAAc17Aaw==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/dom-query": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/image-cropper": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/image-cropper/-/image-cropper-1.41.2.tgz",
+ "integrity": "sha512-750aT4U+J/TJw/Z1QVoLU9JG0luCtf9CyvShtJFIxeS+i25aUoBI9pOKgSFABsOutIqNJTPq4gYpqtuxFjSxRw==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/interact-outside": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/interact-outside/-/interact-outside-1.41.2.tgz",
+ "integrity": "sha512-dM4Fn9iyqQeqkCMRYZP+bAgWEPKRVQRqMmcPsN0OXBhhFKC31Em15LTIZXaOtVKAjH+iwx+UvSYFRiWwEjkOEA==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/json-tree-utils": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/json-tree-utils/-/json-tree-utils-1.41.2.tgz",
+ "integrity": "sha512-gNaOzsbCwmTd2HM3/u0xQdWX5UDBfl8tCXFavzbamkFH0iYQOXJb7cqUXBVuI4KScIbHPCKwrzZjqA5Sg9qzAQ==",
+ "license": "MIT"
+ },
+ "node_modules/@zag-js/listbox": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/listbox/-/listbox-1.41.2.tgz",
+ "integrity": "sha512-iDGrZleP3ui2Q6Jgmr9RYlbd7njdJHs8Qb3IJrSIZBIeYyYmFvVUfAFjk0g/z/amjTx6uYxRASWSPy/RETd4ug==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/collection": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/focus-visible": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/live-region": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/live-region/-/live-region-1.41.2.tgz",
+ "integrity": "sha512-7ubIW5AQt1wx9S/gFN+rU4TyvuFWJrL/DhnDWPNlH5g3luDVHSNeYGeeqf4d4tkcibtpYZa0pg9CbXxRxrfwVA==",
+ "license": "MIT"
+ },
+ "node_modules/@zag-js/marquee": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/marquee/-/marquee-1.41.2.tgz",
+ "integrity": "sha512-cT77aMhrtAK3oe2O6+X3TLtQs8wupdPUtZgHMWVxCcQOwagrk7RUBEQwU3Cx7cTswpBdTKSuDYgUggfgCs96wg==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/menu": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/menu/-/menu-1.41.2.tgz",
+ "integrity": "sha512-wwix8hcAUSi0scpWXCiDppfdZV01Za8nN0gqLt9GdhCiVSlr0rs9pK1ROgPKJTyc43UZfyFPqtTWVHvEHMM70w==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dismissable": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/focus-visible": "1.41.2",
+ "@zag-js/popper": "1.41.2",
+ "@zag-js/rect-utils": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/navigation-menu": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/navigation-menu/-/navigation-menu-1.41.2.tgz",
+ "integrity": "sha512-aROB9CHzskZpnoFuGFkp7dbkZdsXvp2nxQgsgld02I1sDqiwcQd+YMdB0/6Ik0oz6X8I70RKdUuQM9WQFQfDnA==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dismissable": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/number-input": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/number-input/-/number-input-1.41.2.tgz",
+ "integrity": "sha512-QoQWCiVHO+ciUbq8uL8Kxhtk4o3UpwzYpJkfsOfitzsZoYpPc7V/A6+n5yABV2SOwpqBODwNASZdxiEa60kfow==",
+ "license": "MIT",
+ "dependencies": {
+ "@internationalized/number": "3.6.6",
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/pagination": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/pagination/-/pagination-1.41.2.tgz",
+ "integrity": "sha512-vZTxz4DrIDfedMcTjDeEkOc1iXD9wkP8eKuEcDieHOodnjSnNNwtmoFw5DCWv+yEa/TByIamXBZ8ZxHY144JgA==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/password-input": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/password-input/-/password-input-1.41.2.tgz",
+ "integrity": "sha512-OWKFl0S12Qnlf4R4WbMCJ/YU0kGfezm5tP0UiyubMO/Fixv6H0twDFaJSPg2F6POv5uomCcGubc1H7gO+fIhsQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/pin-input": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/pin-input/-/pin-input-1.41.2.tgz",
+ "integrity": "sha512-Wrn3YDbmWL3qvUIzN4QyLO7PzEhunX7z8DwBpmrK04p7HxR9pniTLVIPk27xk2MzyAPYcl4mwd9/Mc88tBxHsg==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/popover": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/popover/-/popover-1.41.2.tgz",
+ "integrity": "sha512-h/LlVMIERM+NWzYV0ZHURlJuaqkT8XxwyEV96XfVzjknDRFNoPSl5IttVYtoaPoUqC0p/Y4oTSiee4mUZKOLHw==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/aria-hidden": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dismissable": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/focus-trap": "1.41.2",
+ "@zag-js/popper": "1.41.2",
+ "@zag-js/remove-scroll": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/popper": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/popper/-/popper-1.41.2.tgz",
+ "integrity": "sha512-Iz4D5YAIiIPn4IHGjhX3QatR/RyGaDt43lBSZv0RYcPQYtFg9sUuek3wizjW9qXgdvItevvNMqRdpl7f3es09g==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.7.6",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/presence": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/presence/-/presence-1.41.2.tgz",
+ "integrity": "sha512-OhOLPAf/DYPmgoEntrlrf3LOrkwA+Y0J3K03NXHXPnkRB7h8jyIbHqzHS0jRTb1pvsO2P/yowRyoYtptY2Zmog==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/types": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/progress": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/progress/-/progress-1.41.2.tgz",
+ "integrity": "sha512-SnzrqN+Z568NoO4rrgjrIc/S4EXMEne5CDgWwt2kQ8yq9VysdH8TtHAjyciFRIio7cdgEZNHKw9jSccpMWgRwA==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/qr-code": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/qr-code/-/qr-code-1.41.2.tgz",
+ "integrity": "sha512-+JLswCNnzf58aQTaX0SMUA9wRC8Hb/a/ZveJIXTz6853Siemay2HqOqB2WQIeF33HNldUFD/a4+4Q8ugg93ubA==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2",
+ "proxy-memoize": "3.0.1",
+ "uqr": "0.1.3"
+ }
+ },
+ "node_modules/@zag-js/radio-group": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/radio-group/-/radio-group-1.41.2.tgz",
+ "integrity": "sha512-EZjos61jKHZlNw8ez80vG2T9gUwpooxcVpYOKS2hyhuEXn3wEoefS5v1WFbmpoA/8TUpUQnYxisAeNuQfEQCuQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/focus-visible": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/rating-group": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/rating-group/-/rating-group-1.41.2.tgz",
+ "integrity": "sha512-SbJP4HiK5XRy/oC3xRowjZOCThq1n/QA2Z/XAkvKLJArVQCYjrH3MoWwWvMVNfNC5+ZJluborR2AGF7tlVLzxA==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/rect-utils": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/rect-utils/-/rect-utils-1.41.2.tgz",
+ "integrity": "sha512-GWBTamaMLMG1p7Fe6V0dsXeTEmk+tqG3ciovzmjxURCJ3Yq2EkAMRhS0v5DG0oo+PyrPEIzEWukGBQkh/XRgYQ==",
+ "license": "MIT"
+ },
+ "node_modules/@zag-js/remove-scroll": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/remove-scroll/-/remove-scroll-1.41.2.tgz",
+ "integrity": "sha512-ieIrOgPKlCikAGEBIboQJoU7oxrL5BEY670tDOu7Eg7rNOdAwGXLEKPX2A/q+lREhOiVYjx0D3//vHTvdte80Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/dom-query": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/scroll-area": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/scroll-area/-/scroll-area-1.41.2.tgz",
+ "integrity": "sha512-hJFAwfIFuS7XmNsS3nB5rPm9OWXfB4d98sID7fjurcaZtDH5LqFriqfXhceNvs7rz7K4f/u8rufXrb6tcvATIA==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/scroll-snap": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/scroll-snap/-/scroll-snap-1.41.2.tgz",
+ "integrity": "sha512-+70Al6LSASyEZtFyyffUJlDbE88KgYJDud05z8oZTqyEOLlTqnlSNkXq/P3siO7r3sNykg9H+TmAKn+/dVSKuw==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/dom-query": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/select": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/select/-/select-1.41.2.tgz",
+ "integrity": "sha512-3wGaKABILexoNBJ1bJiHqLLTctR/VMZaNA4cyKiqZeBEWtAkdMhgyY2xKobrP6KtUTqAeUFNVSTu4yCDrAQnUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/collection": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dismissable": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/focus-visible": "1.41.2",
+ "@zag-js/popper": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/signature-pad": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/signature-pad/-/signature-pad-1.41.2.tgz",
+ "integrity": "sha512-iNrOxY4gtqhsZdXYvlhF7s+LiOvwV7/kBpNnq8tJ1oYhgTs8wvKtJHrP86/CR05irEXQTaLfmeAJZuBEys9iRA==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2",
+ "perfect-freehand": "^1.2.3"
+ }
+ },
+ "node_modules/@zag-js/slider": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/slider/-/slider-1.41.2.tgz",
+ "integrity": "sha512-mKK2BwoDbIGxAdkdKkPZJA1SHtEQt3lS9hJ6WghefYU2vyd0BXoIKvcDV3xJOzly5LXYhH5cJITn6JtGK8353A==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/solid": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/solid/-/solid-1.41.2.tgz",
+ "integrity": "sha512-EyHtNpnDcVveJIRiK9WXjGWBYBRozJUXMYmdm24mnuCgPaKgvkOEY42lEYqbgF+YgUJ8BjE67Mhby0EjcM1HEg==",
+ "license": "MIT",
+ "dependencies": {
+ "@solid-primitives/keyed": "^1.5.3",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/store": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ },
+ "peerDependencies": {
+ "solid-js": ">=1.1.3"
+ }
+ },
+ "node_modules/@zag-js/splitter": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/splitter/-/splitter-1.41.2.tgz",
+ "integrity": "sha512-Ubp4hkmzvVysU31jCINYbBXVqruu7rEPGqugWMzeXC5Hwda648aHpbg4Jix/wRtaGLjsyh6KOVEwAmoJU9NwhQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/steps": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/steps/-/steps-1.41.2.tgz",
+ "integrity": "sha512-m0t2r8+FWwa2b2aU5JiNrHVdYHyNZYHK0G3Tq9lCOSQoDeoJIkyta+sIVehLVSY+0Ba9kOlkRUmYLbsnfXaW+Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/store": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/store/-/store-1.41.2.tgz",
+ "integrity": "sha512-dVZF7E1ezXzynrKhMH3rfSr2rBbCfvTjvXbXz7//1PNULuq58UU5dG93V+9l834npCZxI2+PrpY45wZLJPTsIA==",
+ "license": "MIT",
+ "dependencies": {
+ "proxy-compare": "3.0.1"
+ }
+ },
+ "node_modules/@zag-js/switch": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/switch/-/switch-1.41.2.tgz",
+ "integrity": "sha512-qHbQK95UUHN0tj+bf9LLphLcMo/uTg2Pvs0c1Gs03Zh4g3NtHf0uYIhMZKYlCH0hNVlKrmdzLKWgeoDxv9gySw==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/focus-visible": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/tabs": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/tabs/-/tabs-1.41.2.tgz",
+ "integrity": "sha512-7YVj2mCcxRbn1wMXP9anaTOVf+J0fa5uaPScr8e4+e2xc+/1WKzqN6V8IDeKS5wV/xzi1r3Ny307sX7Xz4ZJVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/tags-input": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/tags-input/-/tags-input-1.41.2.tgz",
+ "integrity": "sha512-aIPEndSO+9LHxyoXLUr0Uttxw2cYMyDuii5w50wn/N7lFJD49U3Sj+6XaB/oJSbDAwn10WrsRDtb7Gs2kmU+Qw==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/auto-resize": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/interact-outside": "1.41.2",
+ "@zag-js/live-region": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/timer": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/timer/-/timer-1.41.2.tgz",
+ "integrity": "sha512-PRYLWaip0+1FeVGEMNk5wMGAAIYgBIWwulZ4U5I+2Ayjohzp2NUAfwJ3sqoYvRrfjNYUNAPdU4eGu1zetC+oVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/toast": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/toast/-/toast-1.41.2.tgz",
+ "integrity": "sha512-+F3PsAo6EIz4rh73IOMCb/+FOUp7e3VjUY2q5sdU4IbfOzJBIbVSJxn0PQmHkuxkzWdCom0Lv0qSPFg/UTplnQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dismissable": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/toggle": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/toggle/-/toggle-1.41.2.tgz",
+ "integrity": "sha512-EFB9pb3pEtwXt7RSivVLWXV64dKUF8gsn75tt8TbYBgfy+zW85MsRLu8U48TXZN5teQWAKKNujr9bxQsW/CWvQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/toggle-group": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/toggle-group/-/toggle-group-1.41.2.tgz",
+ "integrity": "sha512-C6wn3A89h24hTs0BN9ryEuKatfR493u7QqxS06TeK9oI/KZBvm5Rwm8FPHSUJvsUTkAkow4PsXC0Ra19duzEdQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/tooltip": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/tooltip/-/tooltip-1.41.2.tgz",
+ "integrity": "sha512-68okWJCFXfW8r0h97kEcU2yKPkq6e0S6QkiYh09ifMXoYjrQw/shPol8PgrS1poqKJigUWtsKm+bw73abhMn3w==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/focus-visible": "1.41.2",
+ "@zag-js/popper": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/tour": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/tour/-/tour-1.41.2.tgz",
+ "integrity": "sha512-Q7UsvuHYYBo1Cs4b4OS0e/D8lxd2GpSRII93s5BQPi5HTcBjaWhVysAykmCFbat6Z56z0NBmFHNdhZ8oVPls1A==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dismissable": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/focus-trap": "1.41.2",
+ "@zag-js/interact-outside": "1.41.2",
+ "@zag-js/popper": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/tree-view": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/tree-view/-/tree-view-1.41.2.tgz",
+ "integrity": "sha512-QNi0VpV+RyzF4NP72+kSleUpauF9SMAzOAe59nxvs8jAUHqV5diDCInnbQos4+cyFDXFzGq3lElot26A1yI+7Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@zag-js/anatomy": "1.41.2",
+ "@zag-js/collection": "1.41.2",
+ "@zag-js/core": "1.41.2",
+ "@zag-js/dom-query": "1.41.2",
+ "@zag-js/types": "1.41.2",
+ "@zag-js/utils": "1.41.2"
+ }
+ },
+ "node_modules/@zag-js/types": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/types/-/types-1.41.2.tgz",
+ "integrity": "sha512-L6CNvK06lIVpy0X8eG3kbDIx8Uuv+3KHElxXYSzRXSJ7/OLCv1sTRgEvnxNtdIWOrksGgxF4JtT7PXtoClGqNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "3.2.3"
+ }
+ },
+ "node_modules/@zag-js/utils": {
+ "version": "1.41.2",
+ "resolved": "https://registry.npmjs.org/@zag-js/utils/-/utils-1.41.2.tgz",
+ "integrity": "sha512-Yj8FSrR7vGA6ahUhjrThfHAF+PM2Y1Yv2lkXkqZZd60mPBhixcot1+SHOfEMV63JimQcWrmQ8QbeYYMmF+ZpLQ==",
+ "license": "MIT"
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/accepts/node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.17.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
+ "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-import-phases": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz",
+ "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "peerDependencies": {
+ "acorn": "^8.14.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-formats": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
+ "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ajv-formats/node_modules/ajv": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-formats/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ansi-html-community": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz",
+ "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==",
+ "dev": true,
+ "engines": [
+ "node >= 0.8.0"
+ ],
+ "license": "Apache-2.0",
+ "bin": {
+ "ansi-html": "bin/ansi-html"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "license": "Python-2.0"
+ },
+ "node_modules/aria-query": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
+ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "dequal": "^2.0.3"
+ }
+ },
+ "node_modules/array-buffer-byte-length": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
+ "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "is-array-buffer": "^3.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/array-includes": {
+ "version": "3.1.9",
+ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz",
+ "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.24.0",
+ "es-object-atoms": "^1.1.1",
+ "get-intrinsic": "^1.3.0",
+ "is-string": "^1.1.1",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/array-uniq": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz",
+ "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/array.prototype.findlastindex": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz",
+ "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.9",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "es-shim-unscopables": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flat": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz",
+ "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flatmap": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz",
+ "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/arraybuffer.prototype.slice": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz",
+ "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.1",
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "is-array-buffer": "^3.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/asn1js": {
+ "version": "3.0.10",
+ "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz",
+ "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "pvtsutils": "^1.3.6",
+ "pvutils": "^1.1.5",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/astral-regex": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
+ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/async-function": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
+ "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz",
+ "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.28.2",
+ "caniuse-lite": "^1.0.30001787",
+ "fraction.js": "^5.3.4",
+ "picocolors": "^1.1.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+ "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "possible-typed-array-names": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/babel-loader": {
+ "version": "9.2.1",
+ "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz",
+ "integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "find-cache-dir": "^4.0.0",
+ "schema-utils": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 14.15.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.12.0",
+ "webpack": ">=5"
+ }
+ },
+ "node_modules/babel-plugin-jsx-dom-expressions": {
+ "version": "0.40.7",
+ "resolved": "https://registry.npmjs.org/babel-plugin-jsx-dom-expressions/-/babel-plugin-jsx-dom-expressions-0.40.7.tgz",
+ "integrity": "sha512-/O6JWUmjv03OI9lL2ry9bUjpD5S3PclM55RRJEyCdcFZ5W2SEA/59d+l2hNsk3gI6kiWRdRPdOtqZmsQzFN1pQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "7.18.6",
+ "@babel/plugin-syntax-jsx": "^7.18.6",
+ "@babel/types": "^7.20.7",
+ "html-entities": "2.3.3",
+ "parse5": "^7.1.2"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.20.12"
+ }
+ },
+ "node_modules/babel-plugin-jsx-dom-expressions/node_modules/@babel/helper-module-imports": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz",
+ "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-corejs2": {
+ "version": "0.4.17",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz",
+ "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-define-polyfill-provider": "^0.6.8",
+ "semver": "^6.3.1"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-corejs3": {
+ "version": "0.14.2",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz",
+ "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-define-polyfill-provider": "^0.6.8",
+ "core-js-compat": "^3.48.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-regenerator": {
+ "version": "0.6.8",
+ "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz",
+ "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-define-polyfill-provider": "^0.6.8"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/babel-preset-solid": {
+ "version": "1.9.12",
+ "resolved": "https://registry.npmjs.org/babel-preset-solid/-/babel-preset-solid-1.9.12.tgz",
+ "integrity": "sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "babel-plugin-jsx-dom-expressions": "^0.40.6"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0",
+ "solid-js": "^1.9.12"
+ },
+ "peerDependenciesMeta": {
+ "solid-js": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.37",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz",
+ "integrity": "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/batch": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
+ "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.5",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
+ "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "~1.2.0",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "on-finished": "~2.4.1",
+ "qs": "~6.15.1",
+ "raw-body": "~2.5.3",
+ "type-is": "~1.6.18",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/body-parser/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/body-parser/node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/body-parser/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/bonjour-service": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.4.1.tgz",
+ "integrity": "sha512-9KM4QMPKnaJqaja1v7gYO/+TXZGLtzPA05NmUTqDAJjcsWeVoOXKMvU9g0gfuuoYTQqJZ924hivICd5R/bCJbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "multicast-dns": "^7.2.5"
+ }
+ },
+ "node_modules/boolbase": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/brace-expansion": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
+ "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/bundle-name": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
+ "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "run-applescript": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/bytestreamjs": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz",
+ "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/cacheable": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.5.tgz",
+ "integrity": "sha512-EQfaKe09tl615iNvq/TBRWTFf1AKJNXYQSsMx0Z3EI0nA+pVsVPS8wJhnRlkbdacKPh1d0qVIhwTc2zsQNFEEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@cacheable/memory": "^2.0.8",
+ "@cacheable/utils": "^2.4.1",
+ "hookified": "^1.15.0",
+ "keyv": "^5.6.0",
+ "qified": "^0.10.1"
+ }
+ },
+ "node_modules/cacheable/node_modules/keyv": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz",
+ "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@keyv/serialize": "^1.1.1"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
+ "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "get-intrinsic": "^1.3.0",
+ "set-function-length": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camel-case": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz",
+ "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pascal-case": "^3.1.2",
+ "tslib": "^2.0.3"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001799",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz",
+ "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chai": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
+ "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chart.js": {
+ "version": "4.5.1",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
+ "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
+ "license": "MIT",
+ "dependencies": {
+ "@kurkle/color": "^0.3.0"
+ },
+ "engines": {
+ "pnpm": ">=8"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/chrome-trace-event": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
+ "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0"
+ }
+ },
+ "node_modules/clean-css": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz",
+ "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "source-map": "~0.6.0"
+ },
+ "engines": {
+ "node": ">= 10.0"
+ }
+ },
+ "node_modules/clean-webpack-plugin": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/clean-webpack-plugin/-/clean-webpack-plugin-4.0.0.tgz",
+ "integrity": "sha512-WuWE1nyTNAyW5T7oNyys2EN0cfP2fdRxhxnIQWiAp0bMabPdHhoGxM8A6YL2GhqwgrPnnaemVE7nv5XJ2Fhh2w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "del": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "webpack": ">=4.0.0 <6.0.0"
+ }
+ },
+ "node_modules/clone-deep": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
+ "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-plain-object": "^2.0.4",
+ "kind-of": "^6.0.2",
+ "shallow-clone": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/clone-deep/node_modules/is-plain-object": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/colord": {
+ "version": "2.9.3",
+ "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
+ "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/colorette": {
+ "version": "2.0.20",
+ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
+ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/commander": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
+ "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/common-path-prefix": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz",
+ "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/compressible": {
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
+ "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": ">= 1.43.0 < 2"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/compression": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
+ "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "compressible": "~2.0.18",
+ "debug": "2.6.9",
+ "negotiator": "~0.6.4",
+ "on-headers": "~1.1.0",
+ "safe-buffer": "5.2.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/compression/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/compression/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/connect-history-api-fallback": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz",
+ "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/copy-to-clipboard": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz",
+ "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==",
+ "license": "MIT",
+ "dependencies": {
+ "toggle-selection": "^1.0.6"
+ }
+ },
+ "node_modules/copy-webpack-plugin": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-14.0.0.tgz",
+ "integrity": "sha512-3JLW90aBGeaTLpM7mYQKpnVdgsUZRExY55giiZgLuX/xTQRUs1dOCwbBnWnvY6Q6rfZoXMNwzOQJCSZPppfqXA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "glob-parent": "^6.0.1",
+ "normalize-path": "^3.0.0",
+ "schema-utils": "^4.2.0",
+ "serialize-javascript": "^7.0.3",
+ "tinyglobby": "^0.2.12"
+ },
+ "engines": {
+ "node": ">= 20.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.1.0"
+ }
+ },
+ "node_modules/core-js-compat": {
+ "version": "3.49.0",
+ "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz",
+ "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.28.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
+ "node_modules/core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cosmiconfig": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz",
+ "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "env-paths": "^2.2.1",
+ "import-fresh": "^3.3.0",
+ "js-yaml": "^4.1.0",
+ "parse-json": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/d-fischer"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.9.5"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/countries-and-timezones": {
+ "version": "3.9.0",
+ "resolved": "https://registry.npmjs.org/countries-and-timezones/-/countries-and-timezones-3.9.0.tgz",
+ "integrity": "sha512-3upkVtUfZgdp5xOQI/Deg1ye73ae6bygJMO4UvtUi6wLjwcj7cBqx2PzSZ1VL8i5iveBbjhsQp2Judx7kPKivA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.x",
+ "npm": ">=5.x"
+ }
+ },
+ "node_modules/cross-env": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
+ "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.1"
+ },
+ "bin": {
+ "cross-env": "src/bin/cross-env.js",
+ "cross-env-shell": "src/bin/cross-env-shell.js"
+ },
+ "engines": {
+ "node": ">=10.14",
+ "npm": ">=6",
+ "yarn": ">=1"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/css-functions-list": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.3.3.tgz",
+ "integrity": "sha512-8HFEBPKhOpJPEPu70wJJetjKta86Gw9+CCyCnB3sui2qQfOvRyqBy4IKLKKAwdMpWb2lHXWk9Wb4Z6AmaUT1Pg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/css-loader": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.4.tgz",
+ "integrity": "sha512-vv3J9tlOl04WjiMvHQI/9tmIrCxVrj6PFbHemBB1iihpeRbi/I4h033eoFIhwxBBqLhI0KYFS7yvynBFhIZfTw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "icss-utils": "^5.1.0",
+ "postcss": "^8.4.40",
+ "postcss-modules-extract-imports": "^3.1.0",
+ "postcss-modules-local-by-default": "^4.0.5",
+ "postcss-modules-scope": "^3.2.0",
+ "postcss-modules-values": "^4.0.0",
+ "postcss-value-parser": "^4.2.0",
+ "semver": "^7.6.3"
+ },
+ "engines": {
+ "node": ">= 18.12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0",
+ "webpack": "^5.27.0"
+ },
+ "peerDependenciesMeta": {
+ "@rspack/core": {
+ "optional": true
+ },
+ "webpack": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/css-select": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz",
+ "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0",
+ "css-what": "^6.0.1",
+ "domhandler": "^4.3.1",
+ "domutils": "^2.8.0",
+ "nth-check": "^2.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/css-tree": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
+ "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.27.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+ }
+ },
+ "node_modules/css-what": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
+ "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">= 6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/css.escape": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
+ "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/cssstyle": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
+ "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/css-color": "^3.2.0",
+ "rrweb-cssom": "^0.8.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/data-urls": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
+ "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/data-view-buffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
+ "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/data-view-byte-length": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz",
+ "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/inspect-js"
+ }
+ },
+ "node_modules/data-view-byte-offset": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz",
+ "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/date-fns": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.4.0.tgz",
+ "integrity": "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/kossnocorp"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/default-browser": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz",
+ "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bundle-name": "^4.1.0",
+ "default-browser-id": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/default-browser-id": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz",
+ "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/define-lazy-prop": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
+ "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/del": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz",
+ "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/glob": "^7.1.1",
+ "globby": "^6.1.0",
+ "is-path-cwd": "^2.0.0",
+ "is-path-in-cwd": "^2.0.0",
+ "p-map": "^2.0.0",
+ "pify": "^4.0.1",
+ "rimraf": "^2.6.3"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/del/node_modules/array-union": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
+ "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-uniq": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/del/node_modules/globby": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
+ "integrity": "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-union": "^1.0.1",
+ "glob": "^7.0.3",
+ "object-assign": "^4.0.1",
+ "pify": "^2.0.0",
+ "pinkie-promise": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/del/node_modules/globby/node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/del/node_modules/pify": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
+ "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/del/node_modules/rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "deprecated": "Rimraf versions prior to v4 are no longer supported",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/detect-node": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
+ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/dir-glob": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+ "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-type": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dns-packet": {
+ "version": "5.6.1",
+ "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz",
+ "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@leichtgewicht/ip-codec": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/dom-accessibility-api": {
+ "version": "0.5.16",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
+ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/dom-converter": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",
+ "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "utila": "~0.4"
+ }
+ },
+ "node_modules/dom-serializer": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
+ "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.0.1",
+ "domhandler": "^4.2.0",
+ "entities": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/dom-serializer/node_modules/entities": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
+ "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/domelementtype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/domhandler": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz",
+ "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^2.2.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/domutils": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
+ "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "^1.0.1",
+ "domelementtype": "^2.2.0",
+ "domhandler": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
+ "node_modules/dot-case": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
+ "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "no-case": "^3.0.4",
+ "tslib": "^2.0.3"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.372",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.372.tgz",
+ "integrity": "sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.24.0",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.0.tgz",
+ "integrity": "sha512-SkE2t82KlkkxQRVMVLAGKxLfORGQfrkx5dkj+vlgXRVNEdPc4eZcR+J/Fvj8C+yKSFH5L0q3NFlyufOVQnCcYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.3.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/env-paths": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
+ "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/envinfo": {
+ "version": "7.21.0",
+ "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz",
+ "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "envinfo": "dist/cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
+ "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "node_modules/es-abstract": {
+ "version": "1.24.2",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz",
+ "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.2",
+ "arraybuffer.prototype.slice": "^1.0.4",
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "data-view-buffer": "^1.0.2",
+ "data-view-byte-length": "^1.0.2",
+ "data-view-byte-offset": "^1.0.1",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "es-set-tostringtag": "^2.1.0",
+ "es-to-primitive": "^1.3.0",
+ "function.prototype.name": "^1.1.8",
+ "get-intrinsic": "^1.3.0",
+ "get-proto": "^1.0.1",
+ "get-symbol-description": "^1.1.0",
+ "globalthis": "^1.0.4",
+ "gopd": "^1.2.0",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "internal-slot": "^1.1.0",
+ "is-array-buffer": "^3.0.5",
+ "is-callable": "^1.2.7",
+ "is-data-view": "^1.0.2",
+ "is-negative-zero": "^2.0.3",
+ "is-regex": "^1.2.1",
+ "is-set": "^2.0.3",
+ "is-shared-array-buffer": "^1.0.4",
+ "is-string": "^1.1.1",
+ "is-typed-array": "^1.1.15",
+ "is-weakref": "^1.1.1",
+ "math-intrinsics": "^1.1.0",
+ "object-inspect": "^1.13.4",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.7",
+ "own-keys": "^1.0.1",
+ "regexp.prototype.flags": "^1.5.4",
+ "safe-array-concat": "^1.1.3",
+ "safe-push-apply": "^1.0.0",
+ "safe-regex-test": "^1.1.0",
+ "set-proto": "^1.0.0",
+ "stop-iteration-iterator": "^1.1.0",
+ "string.prototype.trim": "^1.2.10",
+ "string.prototype.trimend": "^1.0.9",
+ "string.prototype.trimstart": "^1.0.8",
+ "typed-array-buffer": "^1.0.3",
+ "typed-array-byte-length": "^1.0.3",
+ "typed-array-byte-offset": "^1.0.4",
+ "typed-array-length": "^1.0.7",
+ "unbox-primitive": "^1.1.0",
+ "which-typed-array": "^1.1.19"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz",
+ "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-shim-unscopables": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz",
+ "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-to-primitive": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz",
+ "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.2.7",
+ "is-date-object": "^1.0.5",
+ "is-symbol": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "8.57.1",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz",
+ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
+ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.6.1",
+ "@eslint/eslintrc": "^2.1.4",
+ "@eslint/js": "8.57.1",
+ "@humanwhocodes/config-array": "^0.13.0",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@nodelib/fs.walk": "^1.2.8",
+ "@ungap/structured-clone": "^1.2.0",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
+ "doctrine": "^3.0.0",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^7.2.2",
+ "eslint-visitor-keys": "^3.4.3",
+ "espree": "^9.6.1",
+ "esquery": "^1.4.2",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "globals": "^13.19.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "is-path-inside": "^3.0.3",
+ "js-yaml": "^4.1.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3",
+ "strip-ansi": "^6.0.1",
+ "text-table": "^0.2.0"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-config-prettier": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz",
+ "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "eslint-config-prettier": "bin/cli.js"
+ },
+ "peerDependencies": {
+ "eslint": ">=7.0.0"
+ }
+ },
+ "node_modules/eslint-import-resolver-node": {
+ "version": "0.3.10",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz",
+ "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^3.2.7",
+ "is-core-module": "^2.16.1",
+ "resolve": "^2.0.0-next.6"
+ }
+ },
+ "node_modules/eslint-import-resolver-node/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-import-resolver-typescript": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz",
+ "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@nolyfill/is-core-module": "1.0.39",
+ "debug": "^4.4.0",
+ "get-tsconfig": "^4.10.0",
+ "is-bun-module": "^2.0.0",
+ "stable-hash": "^0.0.5",
+ "tinyglobby": "^0.2.13",
+ "unrs-resolver": "^1.6.2"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint-import-resolver-typescript"
+ },
+ "peerDependencies": {
+ "eslint": "*",
+ "eslint-plugin-import": "*",
+ "eslint-plugin-import-x": "*"
+ },
+ "peerDependenciesMeta": {
+ "eslint-plugin-import": {
+ "optional": true
+ },
+ "eslint-plugin-import-x": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-module-utils": {
+ "version": "2.13.0",
+ "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.13.0.tgz",
+ "integrity": "sha512-bLohSkT6469rRs8czj0tLTD8vaeIS/whvPRJVjDr7IuoTT1k5DYDERlNycjDj/HkOlvQdYurmfZ/g3fG5bgeLQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^3.2.7"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-module-utils/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-import": {
+ "version": "2.32.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
+ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rtsao/scc": "^1.1.0",
+ "array-includes": "^3.1.9",
+ "array.prototype.findlastindex": "^1.2.6",
+ "array.prototype.flat": "^1.3.3",
+ "array.prototype.flatmap": "^1.3.3",
+ "debug": "^3.2.7",
+ "doctrine": "^2.1.0",
+ "eslint-import-resolver-node": "^0.3.9",
+ "eslint-module-utils": "^2.12.1",
+ "hasown": "^2.0.2",
+ "is-core-module": "^2.16.1",
+ "is-glob": "^4.0.3",
+ "minimatch": "^3.1.2",
+ "object.fromentries": "^2.0.8",
+ "object.groupby": "^1.0.3",
+ "object.values": "^1.2.1",
+ "semver": "^6.3.1",
+ "string.prototype.trimend": "^1.0.9",
+ "tsconfig-paths": "^3.15.0"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/eslint-plugin-import/node_modules/brace-expansion": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
+ "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/eslint-plugin-prettier": {
+ "version": "5.5.6",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.6.tgz",
+ "integrity": "sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prettier-linter-helpers": "^1.0.1",
+ "synckit": "^0.11.13"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint-plugin-prettier"
+ },
+ "peerDependencies": {
+ "@types/eslint": ">=8.0.0",
+ "eslint": ">=8.0.0",
+ "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0",
+ "prettier": ">=3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/eslint": {
+ "optional": true
+ },
+ "eslint-config-prettier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-solid": {
+ "version": "0.14.5",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-solid/-/eslint-plugin-solid-0.14.5.tgz",
+ "integrity": "sha512-nfuYK09ah5aJG/oEN6P1qziy1zLgW4PDWe75VNPi4CEFYk1x2AEqwFeQfEPR7gNn0F2jOeqKhx2E+5oNCOBYWQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/utils": "^7.13.1 || ^8.0.0",
+ "estraverse": "^5.3.0",
+ "is-html": "^2.0.0",
+ "kebab-case": "^1.0.2",
+ "known-css-properties": "^0.30.0",
+ "style-to-object": "^1.0.6"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0",
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "7.2.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+ "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/eslint/node_modules/brace-expansion": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
+ "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint/node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/eslint/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/espree": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+ "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.9.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/eventemitter3": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
+ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/events": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.x"
+ }
+ },
+ "node_modules/expect-type": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
+ "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.22.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
+ "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "~1.20.5",
+ "content-disposition": "~0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "~0.7.1",
+ "cookie-signature": "~1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.3.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "~0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "~6.15.1",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "~0.19.0",
+ "serve-static": "~1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "~2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/express/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/express/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-diff": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
+ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
+ "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/fastest-levenshtein": {
+ "version": "1.0.16",
+ "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
+ "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4.9.1"
+ }
+ },
+ "node_modules/fastq": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
+ "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/faye-websocket": {
+ "version": "0.11.4",
+ "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz",
+ "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "websocket-driver": ">=0.5.1"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+ "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^3.0.4"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "~2.0.2",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/finalhandler/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/finalhandler/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/find-cache-dir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz",
+ "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "common-path-prefix": "^3.0.0",
+ "pkg-dir": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
+ "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "bin": {
+ "flat": "cli.js"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
+ "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.3",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
+ "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/for-each": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
+ "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.2.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/function.prototype.name": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz",
+ "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.9",
+ "call-bound": "^1.0.4",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "functions-have-names": "^1.2.3",
+ "has-property-descriptors": "^1.0.2",
+ "hasown": "^2.0.4",
+ "is-callable": "^1.2.7",
+ "is-document.all": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/functions-have-names": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/generator-function": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
+ "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/get-symbol-description": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
+ "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-tsconfig": {
+ "version": "4.14.0",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz",
+ "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/glob-to-regex.js": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz",
+ "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/glob-to-regexp": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
+ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/glob/node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/glob/node_modules/brace-expansion": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
+ "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/glob/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/global-modules": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz",
+ "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "global-prefix": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/global-prefix": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz",
+ "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ini": "^1.3.5",
+ "kind-of": "^6.0.2",
+ "which": "^1.3.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/global-prefix/node_modules/which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "which": "bin/which"
+ }
+ },
+ "node_modules/globals": {
+ "version": "13.24.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
+ "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globalthis": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
+ "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-properties": "^1.2.1",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/globby": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+ "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.2.9",
+ "ignore": "^5.2.0",
+ "merge2": "^1.4.1",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globby/node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/globjoin": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz",
+ "integrity": "sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/handle-thing": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",
+ "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/has-bigints": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
+ "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz",
+ "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hashery": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz",
+ "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hookified": "^1.15.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "he": "bin/he"
+ }
+ },
+ "node_modules/hookified": {
+ "version": "1.15.1",
+ "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz",
+ "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/hpack.js": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz",
+ "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.1",
+ "obuf": "^1.0.0",
+ "readable-stream": "^2.0.1",
+ "wbuf": "^1.1.0"
+ }
+ },
+ "node_modules/hpack.js/node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/hpack.js/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/hpack.js/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/hpack.js/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
+ "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-encoding": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/html-entities": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz",
+ "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/html-minifier-terser": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
+ "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "camel-case": "^4.1.2",
+ "clean-css": "^5.2.2",
+ "commander": "^8.3.0",
+ "he": "^1.2.0",
+ "param-case": "^3.0.4",
+ "relateurl": "^0.2.7",
+ "terser": "^5.10.0"
+ },
+ "bin": {
+ "html-minifier-terser": "cli.js"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/html-tags": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz",
+ "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/html-webpack-plugin": {
+ "version": "5.6.7",
+ "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.7.tgz",
+ "integrity": "sha512-md+vXtdCAe60s1k6AU3dUyMJnDxUyQAwfwPKoLisvgUF1IXjtlLsk2se54+qfL9Mdm26bbwvjJybpNx48NKRLw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/html-minifier-terser": "^6.0.0",
+ "html-minifier-terser": "^6.0.2",
+ "lodash": "^4.17.21",
+ "pretty-error": "^4.0.0",
+ "tapable": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/html-webpack-plugin"
+ },
+ "peerDependencies": {
+ "@rspack/core": "0.x || 1.x",
+ "webpack": "^5.20.0"
+ },
+ "peerDependenciesMeta": {
+ "@rspack/core": {
+ "optional": true
+ },
+ "webpack": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/htmlparser2": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
+ "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==",
+ "dev": true,
+ "funding": [
+ "https://github.com/fb55/htmlparser2?sponsor=1",
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.0.1",
+ "domhandler": "^4.0.0",
+ "domutils": "^2.5.2",
+ "entities": "^2.0.0"
+ }
+ },
+ "node_modules/htmlparser2/node_modules/entities": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
+ "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/http-deceiver": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz",
+ "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/http-parser-js": {
+ "version": "0.5.10",
+ "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz",
+ "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/http-proxy": {
+ "version": "1.18.1",
+ "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
+ "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eventemitter3": "^4.0.0",
+ "follow-redirects": "^1.0.0",
+ "requires-port": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/http-proxy-middleware": {
+ "version": "2.0.9",
+ "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz",
+ "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-proxy": "^1.17.8",
+ "http-proxy": "^1.18.1",
+ "is-glob": "^4.0.1",
+ "is-plain-obj": "^3.0.0",
+ "micromatch": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "@types/express": "^4.17.13"
+ },
+ "peerDependenciesMeta": {
+ "@types/express": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/hyperdyperid": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz",
+ "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.18"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/icss-utils": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
+ "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/import-local": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz",
+ "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pkg-dir": "^4.2.0",
+ "resolve-cwd": "^3.0.0"
+ },
+ "bin": {
+ "import-local-fixture": "fixtures/cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/import-local/node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/import-local/node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/import-local/node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/import-local/node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/import-local/node_modules/pkg-dir": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
+ "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "find-up": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/inline-style-parser": {
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
+ "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/internal-slot": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
+ "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "hasown": "^2.0.2",
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/interpret": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz",
+ "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/ipaddr.js": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz",
+ "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/is-array-buffer": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
+ "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-async-function": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz",
+ "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "async-function": "^1.0.0",
+ "call-bound": "^1.0.3",
+ "get-proto": "^1.0.1",
+ "has-tostringtag": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-bigint": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz",
+ "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-bigints": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-boolean-object": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
+ "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-bun-module": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz",
+ "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.7.1"
+ }
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.2",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
+ "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-data-view": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz",
+ "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "get-intrinsic": "^1.2.6",
+ "is-typed-array": "^1.1.13"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-date-object": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz",
+ "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-docker": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
+ "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-document.all": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz",
+ "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-finalizationregistry": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz",
+ "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-generator-function": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
+ "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.4",
+ "generator-function": "^2.0.0",
+ "get-proto": "^1.0.1",
+ "has-tostringtag": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-html": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-html/-/is-html-2.0.0.tgz",
+ "integrity": "sha512-S+OpgB5i7wzIue/YSE5hg0e5ZYfG3hhpNh9KGl6ayJ38p7ED6wxQLd1TV91xHpcTvw90KMJ9EwN3F/iNflHBVg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "html-tags": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-inside-container": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
+ "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-docker": "^3.0.0"
+ },
+ "bin": {
+ "is-inside-container": "cli.js"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-map": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
+ "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-negative-zero": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
+ "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-network-error": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.2.tgz",
+ "integrity": "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-number-object": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz",
+ "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-path-cwd": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz",
+ "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/is-path-in-cwd": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz",
+ "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-path-inside": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/is-path-in-cwd/node_modules/is-path-inside": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz",
+ "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-is-inside": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/is-path-inside": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-plain-obj": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz",
+ "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-plain-object": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
+ "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-regex": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
+ "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-set": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
+ "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-shared-array-buffer": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz",
+ "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-string": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
+ "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-symbol": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz",
+ "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-symbols": "^1.1.0",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-typed-array": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
+ "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "which-typed-array": "^1.1.16"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakmap": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
+ "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz",
+ "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakset": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz",
+ "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-what": {
+ "version": "4.1.16",
+ "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz",
+ "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mesqueeb"
+ }
+ },
+ "node_modules/is-wsl": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz",
+ "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-inside-container": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/isobject": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+ "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/jest-worker": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
+ "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "merge-stream": "^2.0.0",
+ "supports-color": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ }
+ },
+ "node_modules/jest-worker/node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
+ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
+ "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodeca"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsdom": {
+ "version": "26.1.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
+ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssstyle": "^4.2.1",
+ "data-urls": "^5.0.0",
+ "decimal.js": "^10.5.0",
+ "html-encoding-sniffer": "^4.0.0",
+ "http-proxy-agent": "^7.0.2",
+ "https-proxy-agent": "^7.0.6",
+ "is-potential-custom-element-name": "^1.0.1",
+ "nwsapi": "^2.2.16",
+ "parse5": "^7.2.1",
+ "rrweb-cssom": "^0.8.0",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^5.1.1",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-encoding": "^3.1.1",
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.1.1",
+ "ws": "^8.18.0",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "canvas": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+ "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/kebab-case": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/kebab-case/-/kebab-case-1.0.2.tgz",
+ "integrity": "sha512-7n6wXq4gNgBELfDCpzKc+mRrZFs7D+wgfF5WRFLNAr4DA/qtr9Js8uOAVAfHhuLMfAcQ0pRKqbpjx+TcJVdE1Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/kind-of": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/known-css-properties": {
+ "version": "0.30.0",
+ "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.30.0.tgz",
+ "integrity": "sha512-VSWXYUnsPu9+WYKkfmJyLKtIvaRJi1kXUqVmBACORXZQxT5oZDsoZ2vQP+bQFDnWtpI/4eq3MLoRMjI2fnLzTQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/launch-editor": {
+ "version": "2.14.1",
+ "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.1.tgz",
+ "integrity": "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picocolors": "^1.1.1",
+ "shell-quote": "^1.8.4"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/loader-runner": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz",
+ "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.11.5"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.debounce": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
+ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.truncate": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
+ "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lower-case": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
+ "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.3"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/lz-string": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
+ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "lz-string": "bin/bin.js"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mathml-tag-names": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz",
+ "integrity": "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/mdn-data": {
+ "version": "2.27.1",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
+ "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/memfs": {
+ "version": "4.57.7",
+ "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.57.7.tgz",
+ "integrity": "sha512-YZPphUQZSRGk6ddPlsNuMbztrLwsbUATFNZcqKscSbSJZ4g0+Y3vSZLJ/rfnGZaB1FFhC7SrywZXev6i8lnHgg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jsonjoy.com/fs-core": "4.57.7",
+ "@jsonjoy.com/fs-fsa": "4.57.7",
+ "@jsonjoy.com/fs-node": "4.57.7",
+ "@jsonjoy.com/fs-node-builtins": "4.57.7",
+ "@jsonjoy.com/fs-node-to-fsa": "4.57.7",
+ "@jsonjoy.com/fs-node-utils": "4.57.7",
+ "@jsonjoy.com/fs-print": "4.57.7",
+ "@jsonjoy.com/fs-snapshot": "4.57.7",
+ "@jsonjoy.com/json-pack": "^1.11.0",
+ "@jsonjoy.com/util": "^1.9.0",
+ "glob-to-regex.js": "^1.0.1",
+ "thingies": "^2.5.0",
+ "tree-dump": "^1.0.3",
+ "tslib": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/meow": {
+ "version": "13.2.0",
+ "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz",
+ "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/merge-anything": {
+ "version": "5.1.7",
+ "resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-5.1.7.tgz",
+ "integrity": "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-what": "^4.1.8"
+ },
+ "engines": {
+ "node": ">=12.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mesqueeb"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types/node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/min-indent": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
+ "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mini-css-extract-plugin": {
+ "version": "2.10.2",
+ "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.10.2.tgz",
+ "integrity": "sha512-AOSS0IdEB95ayVkxn5oGzNQwqAi2J0Jb/kKm43t7H73s8+f5873g0yuj0PNvK4dO75mu5DHg4nlgp4k6Kga8eg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "schema-utils": "^4.0.0",
+ "tapable": "^2.2.1"
+ },
+ "engines": {
+ "node": ">= 12.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.0.0"
+ }
+ },
+ "node_modules/minimalistic-assert": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+ "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/minimatch": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/multicast-dns": {
+ "version": "7.2.5",
+ "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz",
+ "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dns-packet": "^5.2.2",
+ "thunky": "^1.0.2"
+ },
+ "bin": {
+ "multicast-dns": "cli.js"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "5.1.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.11.tgz",
+ "integrity": "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.js"
+ },
+ "engines": {
+ "node": "^18 || >=20"
+ }
+ },
+ "node_modules/napi-postinstall": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz",
+ "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "napi-postinstall": "lib/cli.js"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/napi-postinstall"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
+ "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/neo-async": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/no-case": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
+ "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lower-case": "^2.0.2",
+ "tslib": "^2.0.3"
+ }
+ },
+ "node_modules/node-exports-info": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz",
+ "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array.prototype.flatmap": "^1.3.3",
+ "es-errors": "^1.3.0",
+ "object.entries": "^1.1.9",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/node-exports-info/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.47",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz",
+ "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/nth-check": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
+ "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/nth-check?sponsor=1"
+ }
+ },
+ "node_modules/nwsapi": {
+ "version": "2.2.24",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz",
+ "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
+ "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0",
+ "has-symbols": "^1.1.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.entries": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz",
+ "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.fromentries": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz",
+ "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.groupby": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz",
+ "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.values": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz",
+ "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/obuf": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
+ "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/obug": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz",
+ "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/on-headers": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
+ "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/open": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz",
+ "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "default-browser": "^5.2.1",
+ "define-lazy-prop": "^3.0.0",
+ "is-inside-container": "^1.0.0",
+ "wsl-utils": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/own-keys": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
+ "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-intrinsic": "^1.2.6",
+ "object-keys": "^1.1.1",
+ "safe-push-apply": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-map": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz",
+ "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/p-retry": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz",
+ "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/retry": "0.12.2",
+ "is-network-error": "^1.0.0",
+ "retry": "^0.13.1"
+ },
+ "engines": {
+ "node": ">=16.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/param-case": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
+ "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dot-case": "^3.0.4",
+ "tslib": "^2.0.3"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/pascal-case": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz",
+ "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "no-case": "^3.0.4",
+ "tslib": "^2.0.3"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-is-inside": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
+ "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==",
+ "dev": true,
+ "license": "(WTFPL OR MIT)"
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
+ "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/perfect-freehand": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/perfect-freehand/-/perfect-freehand-1.2.3.tgz",
+ "integrity": "sha512-bHZSfqDHGNlPpgH2yxXgPHlQSPpEbo+qg7li0M78J9vNAi2yjwLeA4x79BEQhX44lEWpCLSFCeRZwpw0niiXPA==",
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pinkie": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
+ "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pinkie-promise": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
+ "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pinkie": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pkg-dir": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz",
+ "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "find-up": "^6.3.0"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/find-up": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz",
+ "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^7.1.0",
+ "path-exists": "^5.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/locate-path": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz",
+ "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^6.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/p-limit": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz",
+ "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^1.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/p-locate": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz",
+ "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^4.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/path-exists": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz",
+ "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/yocto-queue": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz",
+ "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pkijs": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.0.tgz",
+ "integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@noble/hashes": "1.4.0",
+ "asn1js": "^3.0.6",
+ "bytestreamjs": "^2.0.1",
+ "pvtsutils": "^1.3.6",
+ "pvutils": "^1.1.3",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/playwright": {
+ "version": "1.61.0",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.0.tgz",
+ "integrity": "sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.61.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.61.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.0.tgz",
+ "integrity": "sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/possible-typed-array-names": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
+ "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.15",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
+ "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.12",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-import": {
+ "version": "16.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-16.1.1.tgz",
+ "integrity": "sha512-2xVS1NCZAfjtVdvXiyegxzJ447GyqCeEI5V7ApgQVOWnros1p5lGNovJNapwPpMombyFBfqDwt7AD3n2l0KOfQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.0.0",
+ "read-cache": "^1.0.0",
+ "resolve": "^1.1.7"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
+ "node_modules/postcss-import/node_modules/resolve": {
+ "version": "1.22.12",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
+ "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/postcss-loader": {
+ "version": "8.2.1",
+ "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.2.1.tgz",
+ "integrity": "sha512-k98jtRzthjj3f76MYTs9JTpRqV1RaaMhEU0Lpw9OTmQZQdppg4B30VZ74BojuBHt3F4KyubHJoXCMUeM8Bqeow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cosmiconfig": "^9.0.0",
+ "jiti": "^2.5.1",
+ "semver": "^7.6.2"
+ },
+ "engines": {
+ "node": ">= 18.12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0",
+ "postcss": "^7.0.0 || ^8.0.1",
+ "webpack": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@rspack/core": {
+ "optional": true
+ },
+ "webpack": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss-modules-extract-imports": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz",
+ "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/postcss-modules-local-by-default": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz",
+ "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "icss-utils": "^5.0.0",
+ "postcss-selector-parser": "^7.0.0",
+ "postcss-value-parser": "^4.1.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/postcss-modules-scope": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz",
+ "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "postcss-selector-parser": "^7.0.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/postcss-modules-values": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz",
+ "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "icss-utils": "^5.0.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/postcss-nested": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-7.0.2.tgz",
+ "integrity": "sha512-5osppouFc0VR9/VYzYxO03VaDa3e8F23Kfd6/9qcZTUI8P58GIYlArOET2Wq0ywSl2o2PjELhYOFI4W7l5QHKw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.14"
+ }
+ },
+ "node_modules/postcss-resolve-nested-selector": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.6.tgz",
+ "integrity": "sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/postcss-safe-parser": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz",
+ "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.31"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz",
+ "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/postcss/node_modules/nanoid": {
+ "version": "3.3.12",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.8.4",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz",
+ "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/prettier-linter-helpers": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz",
+ "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-diff": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/pretty-error": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz",
+ "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lodash": "^4.17.20",
+ "renderkid": "^3.0.0"
+ }
+ },
+ "node_modules/pretty-format": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
+ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^17.0.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/proxy-addr/node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/proxy-compare": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-3.0.1.tgz",
+ "integrity": "sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q==",
+ "license": "MIT"
+ },
+ "node_modules/proxy-memoize": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/proxy-memoize/-/proxy-memoize-3.0.1.tgz",
+ "integrity": "sha512-VDdG/VYtOgdGkWJx7y0o7p+zArSf2383Isci8C+BP3YXgMYDoPd3cCBjw0JdWb6YBb9sFiOPbAADDVTPJnh+9g==",
+ "license": "MIT",
+ "dependencies": {
+ "proxy-compare": "^3.0.0"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/pvtsutils": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz",
+ "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.8.1"
+ }
+ },
+ "node_modules/pvutils": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz",
+ "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/qified": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz",
+ "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hookified": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/qified/node_modules/hookified": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz",
+ "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/qs": {
+ "version": "6.15.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
+ "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/raw-body/node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "17.0.2",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/read-cache": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pify": "^2.3.0"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/rechoir": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz",
+ "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve": "^1.20.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ }
+ },
+ "node_modules/rechoir/node_modules/resolve": {
+ "version": "1.22.12",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
+ "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/redent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
+ "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "indent-string": "^4.0.0",
+ "strip-indent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/reflect-metadata": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
+ "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/reflect.getprototypeof": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
+ "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.9",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.7",
+ "get-proto": "^1.0.1",
+ "which-builtin-type": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/regenerate": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
+ "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/regenerate-unicode-properties": {
+ "version": "10.2.2",
+ "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz",
+ "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "regenerate": "^1.4.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/regexp.prototype.flags": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
+ "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-errors": "^1.3.0",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "set-function-name": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/regexpu-core": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz",
+ "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "regenerate": "^1.4.2",
+ "regenerate-unicode-properties": "^10.2.2",
+ "regjsgen": "^0.8.0",
+ "regjsparser": "^0.13.0",
+ "unicode-match-property-ecmascript": "^2.0.0",
+ "unicode-match-property-value-ecmascript": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/regjsgen": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz",
+ "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/regjsparser": {
+ "version": "0.13.2",
+ "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.2.tgz",
+ "integrity": "sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "jsesc": "~3.1.0"
+ },
+ "bin": {
+ "regjsparser": "bin/parser"
+ }
+ },
+ "node_modules/relateurl": {
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
+ "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/renderkid": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz",
+ "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "css-select": "^4.1.3",
+ "dom-converter": "^0.2.0",
+ "htmlparser2": "^6.1.0",
+ "lodash": "^4.17.21",
+ "strip-ansi": "^6.0.1"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/requires-port": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/resolve": {
+ "version": "2.0.0-next.7",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz",
+ "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "is-core-module": "^2.16.2",
+ "node-exports-info": "^1.6.0",
+ "object-keys": "^1.1.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-cwd": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz",
+ "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-from": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/resolve-cwd/node_modules/resolve-from": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/resolve-pkg-maps": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+ }
+ },
+ "node_modules/retry": {
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
+ "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "deprecated": "Rimraf versions prior to v4 are no longer supported",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz",
+ "integrity": "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.9"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.62.0",
+ "@rollup/rollup-android-arm64": "4.62.0",
+ "@rollup/rollup-darwin-arm64": "4.62.0",
+ "@rollup/rollup-darwin-x64": "4.62.0",
+ "@rollup/rollup-freebsd-arm64": "4.62.0",
+ "@rollup/rollup-freebsd-x64": "4.62.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.62.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.62.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.62.0",
+ "@rollup/rollup-linux-arm64-musl": "4.62.0",
+ "@rollup/rollup-linux-loong64-gnu": "4.62.0",
+ "@rollup/rollup-linux-loong64-musl": "4.62.0",
+ "@rollup/rollup-linux-ppc64-gnu": "4.62.0",
+ "@rollup/rollup-linux-ppc64-musl": "4.62.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.62.0",
+ "@rollup/rollup-linux-riscv64-musl": "4.62.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.62.0",
+ "@rollup/rollup-linux-x64-gnu": "4.62.0",
+ "@rollup/rollup-linux-x64-musl": "4.62.0",
+ "@rollup/rollup-openbsd-x64": "4.62.0",
+ "@rollup/rollup-openharmony-arm64": "4.62.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.62.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.62.0",
+ "@rollup/rollup-win32-x64-gnu": "4.62.0",
+ "@rollup/rollup-win32-x64-msvc": "4.62.0",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/rrweb-cssom": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
+ "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/run-applescript": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz",
+ "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/safe-array-concat": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz",
+ "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.9",
+ "call-bound": "^1.0.4",
+ "get-intrinsic": "^1.3.0",
+ "has-symbols": "^1.1.0",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">=0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safe-push-apply": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
+ "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-regex-test": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
+ "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-regex": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
+ "node_modules/schema-utils": {
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz",
+ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/json-schema": "^7.0.9",
+ "ajv": "^8.9.0",
+ "ajv-formats": "^2.1.1",
+ "ajv-keywords": "^5.1.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/schema-utils/node_modules/ajv": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/schema-utils/node_modules/ajv-keywords": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
+ "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3"
+ },
+ "peerDependencies": {
+ "ajv": "^8.8.2"
+ }
+ },
+ "node_modules/schema-utils/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/select-hose": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
+ "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/selfsigned": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-5.5.0.tgz",
+ "integrity": "sha512-ftnu3TW4+3eBfLRFnDEkzGxSF/10BJBkaLJuBHZX0kiPS7bRdlpZGu6YGt4KngMkdTwJE6MbjavFpqHvqVt+Ew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@peculiar/x509": "^1.14.2",
+ "pkijs": "^3.3.3"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.8.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
+ "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.1",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "~2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "~2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/send/node_modules/debug/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/serialize-javascript": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.5.tgz",
+ "integrity": "sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/seroval": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.4.tgz",
+ "integrity": "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/seroval-plugins": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.5.4.tgz",
+ "integrity": "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "seroval": "^1.0"
+ }
+ },
+ "node_modules/serve-index": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.2.tgz",
+ "integrity": "sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "batch": "0.6.1",
+ "debug": "2.6.9",
+ "escape-html": "~1.0.3",
+ "http-errors": "~1.8.0",
+ "mime-types": "~2.1.35",
+ "parseurl": "~1.3.3"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/serve-index/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/serve-index/node_modules/depd": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+ "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/serve-index/node_modules/http-errors": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
+ "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~1.1.2",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": ">= 1.5.0 < 2",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/serve-index/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/serve-index/node_modules/statuses": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+ "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.3",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "~0.19.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/set-function-length": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-function-name": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
+ "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "functions-have-names": "^1.2.3",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-proto": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz",
+ "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/shallow-clone": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
+ "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "kind-of": "^6.0.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shell-quote": {
+ "version": "1.8.4",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz",
+ "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
+ "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4",
+ "side-channel-list": "^1.0.1",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/slice-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
+ "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "astral-regex": "^2.0.0",
+ "is-fullwidth-code-point": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/sockjs": {
+ "version": "0.3.24",
+ "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz",
+ "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "faye-websocket": "^0.11.3",
+ "uuid": "^8.3.2",
+ "websocket-driver": "^0.7.4"
+ }
+ },
+ "node_modules/solid-js": {
+ "version": "1.9.13",
+ "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.13.tgz",
+ "integrity": "sha512-6hJeJMOcEX8ktqjpDoJZEmld3ijvcvWBDtiXBm7f4332SiFN66QeAQI1REQshvyUoISsSeJ4PHDauKYbwao9JQ==",
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.1.0",
+ "seroval": "~1.5.0",
+ "seroval-plugins": "~1.5.0"
+ }
+ },
+ "node_modules/solid-refresh": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/solid-refresh/-/solid-refresh-0.6.3.tgz",
+ "integrity": "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/generator": "^7.23.6",
+ "@babel/helper-module-imports": "^7.22.15",
+ "@babel/types": "^7.23.6"
+ },
+ "peerDependencies": {
+ "solid-js": "^1.3"
+ }
+ },
+ "node_modules/solid-transition-group": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/solid-transition-group/-/solid-transition-group-0.3.0.tgz",
+ "integrity": "sha512-gzFbtxkEnA8Hgi7UzmEjPPRl4rodoKLs+AGIXA7kJgjOr4j4qFRw39npu+WpdNSDV7aDKr+K0LFcoT9Kbm5PBA==",
+ "license": "MIT",
+ "dependencies": {
+ "@solid-primitives/refs": "^1.1.0",
+ "@solid-primitives/transition-group": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=20.0.0",
+ "pnpm": ">=9.0.0"
+ },
+ "peerDependencies": {
+ "solid-js": "^1.6.12"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/spdy": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz",
+ "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.0",
+ "handle-thing": "^2.0.0",
+ "http-deceiver": "^1.2.7",
+ "select-hose": "^2.0.0",
+ "spdy-transport": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/spdy-transport": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz",
+ "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.0",
+ "detect-node": "^2.0.4",
+ "hpack.js": "^2.1.6",
+ "obuf": "^1.1.2",
+ "readable-stream": "^3.0.6",
+ "wbuf": "^1.7.3"
+ }
+ },
+ "node_modules/stable-hash": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
+ "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/std-env": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz",
+ "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/stop-iteration-iterator": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
+ "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "internal-slot": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string.prototype.trim": {
+ "version": "1.2.11",
+ "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz",
+ "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.9",
+ "call-bound": "^1.0.4",
+ "define-data-property": "^1.1.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.24.2",
+ "es-object-atoms": "^1.1.2",
+ "has-property-descriptors": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimend": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz",
+ "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.9",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimstart": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz",
+ "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/strip-indent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
+ "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "min-indent": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/style-to-object": {
+ "version": "1.0.14",
+ "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz",
+ "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inline-style-parser": "0.2.7"
+ }
+ },
+ "node_modules/stylelint": {
+ "version": "16.26.1",
+ "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.26.1.tgz",
+ "integrity": "sha512-v20V59/crfc8sVTAtge0mdafI3AdnzQ2KsWe6v523L4OA1bJO02S7MO2oyXDCS6iWb9ckIPnqAFVItqSBQr7jw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/stylelint"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/stylelint"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-syntax-patches-for-csstree": "^1.0.19",
+ "@csstools/css-tokenizer": "^3.0.4",
+ "@csstools/media-query-list-parser": "^4.0.3",
+ "@csstools/selector-specificity": "^5.0.0",
+ "@dual-bundle/import-meta-resolve": "^4.2.1",
+ "balanced-match": "^2.0.0",
+ "colord": "^2.9.3",
+ "cosmiconfig": "^9.0.0",
+ "css-functions-list": "^3.2.3",
+ "css-tree": "^3.1.0",
+ "debug": "^4.4.3",
+ "fast-glob": "^3.3.3",
+ "fastest-levenshtein": "^1.0.16",
+ "file-entry-cache": "^11.1.1",
+ "global-modules": "^2.0.0",
+ "globby": "^11.1.0",
+ "globjoin": "^0.1.4",
+ "html-tags": "^3.3.1",
+ "ignore": "^7.0.5",
+ "imurmurhash": "^0.1.4",
+ "is-plain-object": "^5.0.0",
+ "known-css-properties": "^0.37.0",
+ "mathml-tag-names": "^2.1.3",
+ "meow": "^13.2.0",
+ "micromatch": "^4.0.8",
+ "normalize-path": "^3.0.0",
+ "picocolors": "^1.1.1",
+ "postcss": "^8.5.6",
+ "postcss-resolve-nested-selector": "^0.1.6",
+ "postcss-safe-parser": "^7.0.1",
+ "postcss-selector-parser": "^7.1.0",
+ "postcss-value-parser": "^4.2.0",
+ "resolve-from": "^5.0.0",
+ "string-width": "^4.2.3",
+ "supports-hyperlinks": "^3.2.0",
+ "svg-tags": "^1.0.0",
+ "table": "^6.9.0",
+ "write-file-atomic": "^5.0.1"
+ },
+ "bin": {
+ "stylelint": "bin/stylelint.mjs"
+ },
+ "engines": {
+ "node": ">=18.12.0"
+ }
+ },
+ "node_modules/stylelint/node_modules/balanced-match": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz",
+ "integrity": "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/stylelint/node_modules/file-entry-cache": {
+ "version": "11.1.3",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.3.tgz",
+ "integrity": "sha512-oMbq0PD6VIiIwMF6LIa7MEwd/l9huKwmqRKXqmrkqIZv8CvRbfowL+L0ryAl8h//HfAS0zS+4SbYoRyAoA6BJA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^6.1.22"
+ }
+ },
+ "node_modules/stylelint/node_modules/flat-cache": {
+ "version": "6.1.22",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.22.tgz",
+ "integrity": "sha512-N2dnzVJIphnNsjHcrxGW7DePckJ6haPrSFqpsBUhHYgwtKGVq4JrBGielEGD2fCVnsGm1zlBVZ8wGhkyuetgug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cacheable": "^2.3.4",
+ "flatted": "^3.4.2",
+ "hookified": "^1.15.0"
+ }
+ },
+ "node_modules/stylelint/node_modules/known-css-properties": {
+ "version": "0.37.0",
+ "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.37.0.tgz",
+ "integrity": "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/stylelint/node_modules/resolve-from": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-hyperlinks": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz",
+ "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0",
+ "supports-color": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/svg-tags": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz",
+ "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==",
+ "dev": true
+ },
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/synckit": {
+ "version": "0.11.13",
+ "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz",
+ "integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@pkgr/core": "^0.3.6"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/synckit"
+ }
+ },
+ "node_modules/table": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz",
+ "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "ajv": "^8.0.1",
+ "lodash.truncate": "^4.4.2",
+ "slice-ansi": "^4.0.0",
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/table/node_modules/ajv": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/table/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tapable": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
+ "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/terser": {
+ "version": "5.48.0",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz",
+ "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@jridgewell/source-map": "^0.3.3",
+ "acorn": "^8.15.0",
+ "commander": "^2.20.0",
+ "source-map-support": "~0.5.20"
+ },
+ "bin": {
+ "terser": "bin/terser"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/terser-webpack-plugin": {
+ "version": "5.6.1",
+ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz",
+ "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.25",
+ "jest-worker": "^27.4.5",
+ "schema-utils": "^4.3.0",
+ "terser": "^5.31.1"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.1.0"
+ },
+ "peerDependenciesMeta": {
+ "@minify-html/node": {
+ "optional": true
+ },
+ "@swc/core": {
+ "optional": true
+ },
+ "@swc/css": {
+ "optional": true
+ },
+ "@swc/html": {
+ "optional": true
+ },
+ "clean-css": {
+ "optional": true
+ },
+ "cssnano": {
+ "optional": true
+ },
+ "csso": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "html-minifier-terser": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "postcss": {
+ "optional": true
+ },
+ "uglify-js": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/terser/node_modules/commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/thingies": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.6.0.tgz",
+ "integrity": "sha512-rMHRjmlFLM1R96UYPvpmnc3LYtdFrT33JIB7L9hetGue1qAPfn1N2LJeEjxUSidu1Iku+haLZXDuEXUHNGO/lg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "^2"
+ }
+ },
+ "node_modules/thunky": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
+ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz",
+ "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
+ "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tldts": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
+ "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^6.1.86"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
+ "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toggle-selection": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
+ "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==",
+ "license": "MIT"
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/tough-cookie": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
+ "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^6.1.32"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tree-dump": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz",
+ "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/streamich"
+ },
+ "peerDependencies": {
+ "tslib": "2"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
+ "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/tsconfig-paths": {
+ "version": "3.15.0",
+ "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
+ "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/json5": "^0.0.29",
+ "json5": "^1.0.2",
+ "minimist": "^1.2.6",
+ "strip-bom": "^3.0.0"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/tsyringe": {
+ "version": "4.10.0",
+ "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz",
+ "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^1.9.3"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/tsyringe/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/typed-array-buffer": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
+ "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/typed-array-byte-length": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz",
+ "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "for-each": "^0.3.3",
+ "gopd": "^1.2.0",
+ "has-proto": "^1.2.0",
+ "is-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-byte-offset": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz",
+ "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "for-each": "^0.3.3",
+ "gopd": "^1.2.0",
+ "has-proto": "^1.2.0",
+ "is-typed-array": "^1.1.15",
+ "reflect.getprototypeof": "^1.0.9"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-length": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz",
+ "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.9",
+ "for-each": "^0.3.5",
+ "gopd": "^1.2.0",
+ "is-typed-array": "^1.1.15",
+ "possible-typed-array-names": "^1.1.0",
+ "reflect.getprototypeof": "^1.0.10"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/unbox-primitive": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
+ "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-bigints": "^1.0.2",
+ "has-symbols": "^1.1.0",
+ "which-boxed-primitive": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unicode-canonical-property-names-ecmascript": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz",
+ "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unicode-match-property-ecmascript": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz",
+ "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "unicode-canonical-property-names-ecmascript": "^2.0.0",
+ "unicode-property-aliases-ecmascript": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unicode-match-property-value-ecmascript": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz",
+ "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unicode-property-aliases-ecmascript": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz",
+ "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/unrs-resolver": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz",
+ "integrity": "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "napi-postinstall": "^0.3.4"
+ },
+ "funding": {
+ "url": "https://opencollective.com/unrs-resolver"
+ },
+ "optionalDependencies": {
+ "@unrs/resolver-binding-android-arm-eabi": "1.12.2",
+ "@unrs/resolver-binding-android-arm64": "1.12.2",
+ "@unrs/resolver-binding-darwin-arm64": "1.12.2",
+ "@unrs/resolver-binding-darwin-x64": "1.12.2",
+ "@unrs/resolver-binding-freebsd-x64": "1.12.2",
+ "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2",
+ "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2",
+ "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2",
+ "@unrs/resolver-binding-linux-arm64-musl": "1.12.2",
+ "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2",
+ "@unrs/resolver-binding-linux-loong64-musl": "1.12.2",
+ "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2",
+ "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2",
+ "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2",
+ "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2",
+ "@unrs/resolver-binding-linux-x64-gnu": "1.12.2",
+ "@unrs/resolver-binding-linux-x64-musl": "1.12.2",
+ "@unrs/resolver-binding-openharmony-arm64": "1.12.2",
+ "@unrs/resolver-binding-wasm32-wasi": "1.12.2",
+ "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2",
+ "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2",
+ "@unrs/resolver-binding-win32-x64-msvc": "1.12.2"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uqr": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/uqr/-/uqr-0.1.3.tgz",
+ "integrity": "sha512-0rjE8iEJe4YmT9TOhwsZtqCMRLc5DXZUI2UEYUUg63ikBkqqE5EYWaI0etFe/5KUcmcYwLih2RND1kq+hrUJXA==",
+ "license": "MIT"
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/utila": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz",
+ "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/valibot": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.4.1.tgz",
+ "integrity": "sha512-klCmFTz2jeDluy9RwX+F884TCiogtdBJ/YaxSx1EOBYXa3NXNWj8kR1jjN8rzluwojJVWWaHJ4r1U5LfICnM3g==",
+ "license": "MIT",
+ "peerDependencies": {
+ "typescript": ">=5"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/vite": {
+ "version": "6.4.3",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
+ "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-plugin-solid": {
+ "version": "2.11.12",
+ "resolved": "https://registry.npmjs.org/vite-plugin-solid/-/vite-plugin-solid-2.11.12.tgz",
+ "integrity": "sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.23.3",
+ "@types/babel__core": "^7.20.4",
+ "babel-preset-solid": "^1.8.4",
+ "merge-anything": "^5.1.7",
+ "solid-refresh": "^0.6.3",
+ "vitefu": "^1.0.4"
+ },
+ "peerDependencies": {
+ "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*",
+ "solid-js": "^1.7.2",
+ "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@testing-library/jest-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/vite/node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/vitefu": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz",
+ "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==",
+ "dev": true,
+ "license": "MIT",
+ "workspaces": [
+ "tests/deps/*",
+ "tests/projects/*",
+ "tests/projects/workspace/packages/*"
+ ],
+ "peerDependencies": {
+ "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz",
+ "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "4.1.9",
+ "@vitest/mocker": "4.1.9",
+ "@vitest/pretty-format": "4.1.9",
+ "@vitest/runner": "4.1.9",
+ "@vitest/snapshot": "4.1.9",
+ "@vitest/spy": "4.1.9",
+ "@vitest/utils": "4.1.9",
+ "es-module-lexer": "^2.0.0",
+ "expect-type": "^1.3.0",
+ "magic-string": "^0.30.21",
+ "obug": "^2.1.1",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.3",
+ "std-env": "^4.0.0-rc.1",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^1.0.2",
+ "tinyglobby": "^0.2.15",
+ "tinyrainbow": "^3.1.0",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "4.1.9",
+ "@vitest/browser-preview": "4.1.9",
+ "@vitest/browser-webdriverio": "4.1.9",
+ "@vitest/coverage-istanbul": "4.1.9",
+ "@vitest/coverage-v8": "4.1.9",
+ "@vitest/ui": "4.1.9",
+ "happy-dom": "*",
+ "jsdom": "*",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/coverage-istanbul": {
+ "optional": true
+ },
+ "@vitest/coverage-v8": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ },
+ "vite": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/vitest/node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/watchpack": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz",
+ "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.1.2"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/wbuf": {
+ "version": "1.7.3",
+ "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz",
+ "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minimalistic-assert": "^1.0.0"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/webpack": {
+ "version": "5.107.2",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.107.2.tgz",
+ "integrity": "sha512-v7RhXaJbpMlV0D7hC7lb2EbnxkoeUqf9qhKr6lozx3Q48pmFrqqNRmZFUEGmi7pSwm6fCQ2H1IjvCkHqdpVdjQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.8",
+ "@types/json-schema": "^7.0.15",
+ "@webassemblyjs/ast": "^1.14.1",
+ "@webassemblyjs/wasm-edit": "^1.14.1",
+ "@webassemblyjs/wasm-parser": "^1.14.1",
+ "acorn": "^8.16.0",
+ "acorn-import-phases": "^1.0.3",
+ "browserslist": "^4.28.1",
+ "chrome-trace-event": "^1.0.2",
+ "enhanced-resolve": "^5.22.0",
+ "es-module-lexer": "^2.1.0",
+ "eslint-scope": "5.1.1",
+ "events": "^3.2.0",
+ "glob-to-regexp": "^0.4.1",
+ "graceful-fs": "^4.2.11",
+ "loader-runner": "^4.3.2",
+ "mime-db": "^1.54.0",
+ "neo-async": "^2.6.2",
+ "schema-utils": "^4.3.3",
+ "tapable": "^2.3.0",
+ "terser-webpack-plugin": "^5.5.0",
+ "watchpack": "^2.5.1",
+ "webpack-sources": "^3.5.0"
+ },
+ "bin": {
+ "webpack": "bin/webpack.js"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependenciesMeta": {
+ "webpack-cli": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webpack-cli": {
+ "version": "5.1.4",
+ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz",
+ "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@discoveryjs/json-ext": "^0.5.0",
+ "@webpack-cli/configtest": "^2.1.1",
+ "@webpack-cli/info": "^2.0.2",
+ "@webpack-cli/serve": "^2.0.5",
+ "colorette": "^2.0.14",
+ "commander": "^10.0.1",
+ "cross-spawn": "^7.0.3",
+ "envinfo": "^7.7.3",
+ "fastest-levenshtein": "^1.0.12",
+ "import-local": "^3.0.2",
+ "interpret": "^3.1.1",
+ "rechoir": "^0.8.0",
+ "webpack-merge": "^5.7.3"
+ },
+ "bin": {
+ "webpack-cli": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=14.15.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "5.x.x"
+ },
+ "peerDependenciesMeta": {
+ "@webpack-cli/generators": {
+ "optional": true
+ },
+ "webpack-bundle-analyzer": {
+ "optional": true
+ },
+ "webpack-dev-server": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webpack-cli/node_modules/commander": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
+ "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/webpack-dev-middleware": {
+ "version": "7.4.5",
+ "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.5.tgz",
+ "integrity": "sha512-uxQ6YqGdE4hgDKNf7hUiPXOdtkXvBJXrfEGYSx7P7LC8hnUYGK70X6xQXUvXeNyBDDcsiQXpG2m3G9vxowaEuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "colorette": "^2.0.10",
+ "memfs": "^4.43.1",
+ "mime-types": "^3.0.1",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "schema-utils": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 18.12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "webpack": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webpack-dev-middleware/node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/webpack-dev-server": {
+ "version": "5.2.5",
+ "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.5.tgz",
+ "integrity": "sha512-4wZtCquSuv9CKX8oybo+mqxtxZqWz47uM1Ch94lxowBztOhWCbhqvRbfC/mODOwxgV2brY+JGZpHq58/SuVFYg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/bonjour": "^3.5.13",
+ "@types/connect-history-api-fallback": "^1.5.4",
+ "@types/express": "^4.17.25",
+ "@types/express-serve-static-core": "^4.17.21",
+ "@types/serve-index": "^1.9.4",
+ "@types/serve-static": "^1.15.5",
+ "@types/sockjs": "^0.3.36",
+ "@types/ws": "^8.5.10",
+ "ansi-html-community": "^0.0.8",
+ "bonjour-service": "^1.2.1",
+ "chokidar": "^3.6.0",
+ "colorette": "^2.0.10",
+ "compression": "^1.8.1",
+ "connect-history-api-fallback": "^2.0.0",
+ "express": "^4.22.1",
+ "graceful-fs": "^4.2.6",
+ "http-proxy-middleware": "^2.0.9",
+ "ipaddr.js": "^2.1.0",
+ "launch-editor": "^2.6.1",
+ "open": "^10.0.3",
+ "p-retry": "^6.2.0",
+ "schema-utils": "^4.2.0",
+ "selfsigned": "^5.5.0",
+ "serve-index": "^1.9.1",
+ "sockjs": "^0.3.24",
+ "spdy": "^4.0.2",
+ "webpack-dev-middleware": "^7.4.2",
+ "ws": "^8.18.0"
+ },
+ "bin": {
+ "webpack-dev-server": "bin/webpack-dev-server.js"
+ },
+ "engines": {
+ "node": ">= 18.12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "webpack": {
+ "optional": true
+ },
+ "webpack-cli": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webpack-merge": {
+ "version": "5.10.0",
+ "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz",
+ "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "clone-deep": "^4.0.1",
+ "flat": "^5.0.2",
+ "wildcard": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/webpack-sources": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.0.tgz",
+ "integrity": "sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/webpack/node_modules/eslint-scope": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/webpack/node_modules/estraverse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/websocket-driver": {
+ "version": "0.7.5",
+ "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.5.tgz",
+ "integrity": "sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "http-parser-js": ">=0.5.1",
+ "safe-buffer": ">=5.1.0",
+ "websocket-extensions": ">=0.1.1"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/websocket-extensions": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz",
+ "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/whatwg-encoding": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
+ "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
+ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "^5.1.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/which-boxed-primitive": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz",
+ "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-bigint": "^1.1.0",
+ "is-boolean-object": "^1.2.1",
+ "is-number-object": "^1.1.1",
+ "is-string": "^1.1.1",
+ "is-symbol": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-builtin-type": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz",
+ "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "function.prototype.name": "^1.1.6",
+ "has-tostringtag": "^1.0.2",
+ "is-async-function": "^2.0.0",
+ "is-date-object": "^1.1.0",
+ "is-finalizationregistry": "^1.1.0",
+ "is-generator-function": "^1.0.10",
+ "is-regex": "^1.2.1",
+ "is-weakref": "^1.0.2",
+ "isarray": "^2.0.5",
+ "which-boxed-primitive": "^1.1.0",
+ "which-collection": "^1.0.2",
+ "which-typed-array": "^1.1.16"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-collection": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
+ "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-map": "^2.0.3",
+ "is-set": "^2.0.3",
+ "is-weakmap": "^2.0.2",
+ "is-weakset": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-typed-array": {
+ "version": "1.1.22",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz",
+ "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.9",
+ "call-bound": "^1.0.4",
+ "for-each": "^0.3.5",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wildcard": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz",
+ "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/write-file-atomic": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz",
+ "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "imurmurhash": "^0.1.4",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.21.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
+ "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/wsl-utils": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz",
+ "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-wsl": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/client_v2/package.json b/client_v2/package.json
new file mode 100644
index 00000000000..e485b22df3e
--- /dev/null
+++ b/client_v2/package.json
@@ -0,0 +1,101 @@
+{
+ "name": "dashboard",
+ "version": "3.0.0",
+ "private": true,
+ "scripts": {
+ "dev": "cross-env NODE_ENV=development BUILD_ENV=dev webpack-dev-server --config webpack.dev.js",
+ "build-dev": "cross-env BUILD_ENV=dev webpack --config webpack.dev.js",
+ "build-prod": "cross-env BUILD_ENV=prod webpack --config webpack.prod.js",
+ "check": "npm run lint && npm run typecheck && npm run test",
+ "watch": "cross-env BUILD_ENV=dev webpack --config webpack.dev.js --watch",
+ "start": "cross-env NODE_ENV=development BUILD_ENV=dev webpack-dev-server --config webpack.dev.js",
+ "lint": "eslint --ext .ts,.tsx src",
+ "lint:fix": "prettier --write \"src/**/*.{ts,tsx,json}\" && eslint --ext .ts,.tsx src --fix",
+ "test": "vitest --run",
+ "test:watch": "vitest --watch",
+ "test:e2e": "npx playwright test tests/e2e",
+ "test:e2e:interactive": "npx playwright test --ui",
+ "test:e2e:debug": "npx playwright test --debug",
+ "test:e2e:codegen": "npx playwright codegen",
+ "typecheck": "tsc --noEmit",
+ "typecheck:watch": "tsc --noEmit --watch",
+ "translations:check": "node ./scripts/check-translations.js"
+ },
+ "type": "module",
+ "dependencies": {
+ "@adguard/translate": "^1.0.2",
+ "@ark-ui/solid": "^5.37.1",
+ "@modular-forms/solid": "^0.25.0",
+ "@solidjs/router": "^0.15.0",
+ "chart.js": "^4.4.0",
+ "clsx": "^2.1.1",
+ "copy-to-clipboard": "^3.3.3",
+ "countries-and-timezones": "^3.6.0",
+ "date-fns": "^4.1.0",
+ "ipaddr.js": "^2.2.0",
+ "js-yaml": "^4.1.0",
+ "lodash": "^4.17.19",
+ "nanoid": "^5.1.0",
+ "qs": "^6.14.0",
+ "solid-js": "^1.9.0",
+ "solid-transition-group": "^0.3.0"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.24.5",
+ "@babel/preset-env": "^7.24.5",
+ "@babel/preset-typescript": "^7.24.1",
+ "@playwright/test": "1.56.0",
+ "@solidjs/testing-library": "^0.8.0",
+ "@testing-library/jest-dom": "^6.9.1",
+ "@testing-library/user-event": "^14.6.1",
+ "@types/lodash": "^4.17.4",
+ "@types/node": "^22.13.10",
+ "@types/qs": "^6.15.1",
+ "@typescript-eslint/eslint-plugin": "^8.60.1",
+ "@typescript-eslint/parser": "^8.60.1",
+ "autoprefixer": "^10.4.21",
+ "babel-loader": "^9.1.3",
+ "babel-preset-solid": "^1.9.0",
+ "clean-webpack-plugin": "^4.0.0",
+ "copy-webpack-plugin": "^14.0.0",
+ "cross-env": "^7.0.3",
+ "css-loader": "^7.1.2",
+ "eslint": "^8.57.0",
+ "eslint-config-prettier": "^9.1.0",
+ "eslint-import-resolver-typescript": "^3.5.5",
+ "eslint-plugin-import": "^2.29.0",
+ "eslint-plugin-prettier": "^5.1.3",
+ "eslint-plugin-solid": "^0.14.0",
+ "html-webpack-plugin": "^5.6.0",
+ "jsdom": "^26.1.0",
+ "mini-css-extract-plugin": "^2.9.0",
+ "playwright": "^1.61.0",
+ "postcss": "^8.5.6",
+ "postcss-import": "^16.1.1",
+ "postcss-loader": "^8.1.1",
+ "postcss-nested": "^7.0.2",
+ "prettier": "^3.2.5",
+ "stylelint": "^16.5.0",
+ "typescript": "^5.4.0",
+ "vite": "^6.0.0",
+ "vite-plugin-solid": "^2.11.12",
+ "vitest": "^4.1.0",
+ "webpack": "^5.91.0",
+ "webpack-cli": "^5.1.4",
+ "webpack-dev-server": "^5.0.4",
+ "webpack-merge": "^5.10.0"
+ },
+ "browserslist": {
+ "development": [
+ "last 1 chrome version",
+ "last 1 firefox version",
+ "last 1 safari version"
+ ],
+ "production": [
+ "defaults",
+ "not dead",
+ "not ie 11",
+ "not op_mini all"
+ ]
+ }
+}
diff --git a/client_v2/playwright.config.ts b/client_v2/playwright.config.ts
new file mode 100644
index 00000000000..2db04f8fac9
--- /dev/null
+++ b/client_v2/playwright.config.ts
@@ -0,0 +1,71 @@
+import { defineConfig, devices } from '@playwright/test';
+
+import path from 'path';
+import { fileURLToPath } from 'url';
+import { ADMIN_USERNAME, CONFIG_FILE_PATH, PORT, WORK_DIR_PATH } from './tests/constants';
+
+const configDir = path.dirname(fileURLToPath(import.meta.url));
+const ADMIN_PASSWORD_HASH = '$2a$10$82RqoFQEf8GcFZwhCk4GFu.KHavhWaNajpZxCkdYsHhToNRe8ljO2';
+const DNS_PORT = 5353;
+const CONFIG_SCHEMA_VERSION = 34;
+
+const shellQuote = (value: string) => `'${value.replace(/'/g, `'\\''`)}'`;
+
+const prepareConfigScriptPath = path.resolve(configDir, './tests/e2e/prepareConfig.mjs');
+const prepareConfigEnv = [
+ `E2E_CONFIG_PATH=${shellQuote(CONFIG_FILE_PATH)}`,
+ `E2E_WORK_DIR=${shellQuote(WORK_DIR_PATH)}`,
+ `E2E_ADMIN_USERNAME=${shellQuote(ADMIN_USERNAME)}`,
+ `E2E_ADMIN_PASSWORD_HASH=${shellQuote(ADMIN_PASSWORD_HASH)}`,
+ `E2E_HTTP_PORT=${shellQuote(String(PORT))}`,
+ `E2E_DNS_PORT=${shellQuote(String(DNS_PORT))}`,
+ `E2E_SCHEMA_VERSION=${shellQuote(String(CONFIG_SCHEMA_VERSION))}`,
+].join(' ');
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ testDir: './tests/e2e',
+ globalSetup: path.resolve(configDir, './tests/e2e/globalSetup.ts'),
+ globalTeardown: path.resolve(configDir, './tests/e2e/globalTeardown.ts'),
+ timeout: 30000,
+ /* Run tests in files in parallel */
+ fullyParallel: true,
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
+ forbidOnly: !!process.env.CI,
+ /* Retry on CI only */
+ retries: process.env.CI ? 2 : 0,
+ /* Run tests in parallel on CI. */
+ workers: process.env.CI ? 4 : undefined,
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+ reporter: 'html',
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL: 'http://127.0.0.1:3000',
+
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+ trace: 'on-first-retry',
+ launchOptions: {
+ headless: true,
+ },
+ },
+
+ /* Configure projects for major browsers */
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ ],
+
+ webServer: {
+ stdout: process.env.CI ? 'pipe' : 'ignore',
+ command: `${prepareConfigEnv} node ${shellQuote(prepareConfigScriptPath)} && ./AdGuardHome --local-frontend -v -c ${CONFIG_FILE_PATH} --work-dir ${WORK_DIR_PATH}`,
+ url: 'http://127.0.0.1:3000',
+ cwd: path.resolve(configDir, '..'),
+ reuseExistingServer: !process.env.CI,
+ timeout: 50000,
+ },
+});
diff --git a/client_v2/postcss.config.js b/client_v2/postcss.config.js
new file mode 100644
index 00000000000..44a2b86af53
--- /dev/null
+++ b/client_v2/postcss.config.js
@@ -0,0 +1,7 @@
+import autoprefixer from 'autoprefixer';
+import postcssImport from 'postcss-import';
+import postcssNested from 'postcss-nested';
+
+export default {
+ plugins: [postcssImport(), postcssNested(), autoprefixer()],
+};
diff --git a/client_v2/prod.eslintrc b/client_v2/prod.eslintrc
new file mode 100644
index 00000000000..2bba5de8516
--- /dev/null
+++ b/client_v2/prod.eslintrc
@@ -0,0 +1,6 @@
+{
+ "rules": {
+ // disallow the use of debugger
+ "no-debugger": "error",
+ }
+}
diff --git a/client_v2/public/assets/apple-touch-icon.png b/client_v2/public/assets/apple-touch-icon.png
new file mode 100644
index 00000000000..b8d3869ee6a
Binary files /dev/null and b/client_v2/public/assets/apple-touch-icon.png differ
diff --git a/client_v2/public/assets/favicon.ico b/client_v2/public/assets/favicon.ico
new file mode 100644
index 00000000000..1be63b5ddb7
Binary files /dev/null and b/client_v2/public/assets/favicon.ico differ
diff --git a/client_v2/public/assets/favicon.svg b/client_v2/public/assets/favicon.svg
new file mode 100644
index 00000000000..b0d41618384
--- /dev/null
+++ b/client_v2/public/assets/favicon.svg
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client_v2/public/forgot_password.html b/client_v2/public/forgot_password.html
new file mode 100644
index 00000000000..ed7d4bca942
--- /dev/null
+++ b/client_v2/public/forgot_password.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+ Forgot Password
+
+
+
+
+
+ You need to enable JavaScript to run this app.
+
+
+
+
diff --git a/client_v2/public/index.html b/client_v2/public/index.html
new file mode 100644
index 00000000000..f5dbd912952
--- /dev/null
+++ b/client_v2/public/index.html
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ AdGuard Home
+
+
+
+
+ You need to enable JavaScript to run this app.
+
+
+
diff --git a/client_v2/public/install.html b/client_v2/public/install.html
new file mode 100644
index 00000000000..8e06bc91aa7
--- /dev/null
+++ b/client_v2/public/install.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Setup AdGuard Home
+
+
+
+
+ You need to enable JavaScript to run this app.
+
+
+
diff --git a/client_v2/public/login.html b/client_v2/public/login.html
new file mode 100644
index 00000000000..d523214fa16
--- /dev/null
+++ b/client_v2/public/login.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+ Login
+
+
+
+
+
+ You need to enable JavaScript to run this app.
+
+
+
+
diff --git a/client_v2/scripts/check-translations.js b/client_v2/scripts/check-translations.js
new file mode 100644
index 00000000000..f5e6c937116
--- /dev/null
+++ b/client_v2/scripts/check-translations.js
@@ -0,0 +1,50 @@
+// CLI entry point for auditing client_v2 translation usage against src/__locales/en.json.
+// It reports missing locale keys, unused locale keys, and dynamic intl usages that make the audit best-effort.
+import path from 'node:path';
+import process from 'node:process';
+import { fileURLToPath } from 'node:url';
+
+import {
+ auditTranslations,
+ collectTranslationUsageFromFiles,
+ formatAuditReport,
+ listSourceFiles,
+ loadLocaleMessages,
+} from './translation-audit.js';
+
+export const runTranslationAudit = async ({
+ rootDir = process.cwd(),
+ write = (chunk) => {
+ process.stdout.write(chunk);
+ },
+ writeError = (chunk) => {
+ process.stderr.write(chunk);
+ },
+} = {}) => {
+ try {
+ const srcDir = path.join(rootDir, 'src');
+ const localePath = path.join(srcDir, '__locales', 'en.json');
+ const filePaths = await listSourceFiles(srcDir);
+ const localeMessages = await loadLocaleMessages(localePath);
+ const usage = await collectTranslationUsageFromFiles(filePaths);
+ const report = auditTranslations({ localeMessages, usage });
+
+ write(`${formatAuditReport(report, { rootDir })}\n`);
+
+ return 0;
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+
+ writeError(`Translation audit failed: ${message}\n`);
+
+ return 1;
+ }
+};
+
+const currentFilePath = fileURLToPath(import.meta.url);
+
+if (process.argv[1] && path.resolve(process.argv[1]) === currentFilePath) {
+ const exitCode = await runTranslationAudit();
+
+ process.exitCode = exitCode;
+}
diff --git a/client_v2/scripts/translation-audit.js b/client_v2/scripts/translation-audit.js
new file mode 100644
index 00000000000..3c98f56f9fe
--- /dev/null
+++ b/client_v2/scripts/translation-audit.js
@@ -0,0 +1,218 @@
+import fs from 'node:fs/promises';
+import path from 'node:path';
+
+import ts from 'typescript';
+
+const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx']);
+const TRANSLATION_METHODS = new Set(['getMessage', 'getPlural']);
+
+const isIntlMethod = (node) => {
+ if (!ts.isPropertyAccessExpression(node)) {
+ return false;
+ }
+
+ return (
+ ts.isIdentifier(node.expression) &&
+ node.expression.text === 'intl' &&
+ ts.isIdentifier(node.name) &&
+ TRANSLATION_METHODS.has(node.name.text)
+ );
+};
+
+const getLiteralKey = (node) => {
+ if (!node) {
+ return null;
+ }
+
+ if (ts.isStringLiteral(node)) {
+ return node.text;
+ }
+
+ if (ts.isNoSubstitutionTemplateLiteral(node)) {
+ return node.text;
+ }
+
+ if (ts.isParenthesizedExpression(node)) {
+ return getLiteralKey(node.expression);
+ }
+
+ return null;
+};
+
+const sortByFileAndLine = (left, right) =>
+ left.filePath.localeCompare(right.filePath) ||
+ left.line - right.line ||
+ left.method.localeCompare(right.method);
+
+const toRelativePath = (filePath, rootDir) => {
+ if (!rootDir) {
+ return filePath;
+ }
+
+ const relativePath = path.relative(rootDir, filePath);
+
+ return relativePath === '' ? '.' : relativePath;
+};
+
+const formatNodeSource = (node, sourceFile) => sourceFile.text.slice(node.pos, node.end);
+
+export const collectTranslationUsageFromSource = (source, filePath) => {
+ const staticKeys = [];
+ const dynamicUsages = [];
+
+ const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true);
+
+ const visit = (node) => {
+ if (ts.isCallExpression(node) && isIntlMethod(node.expression)) {
+ const method = node.expression.name.text;
+ const pos = sourceFile.getLineAndCharacterOfPosition(node.pos);
+ const line = pos.line + 1;
+ const key = getLiteralKey(node.arguments[0]);
+
+ if (key !== null) {
+ staticKeys.push({ filePath, key, line, method });
+ } else {
+ dynamicUsages.push({
+ expression: formatNodeSource(node, sourceFile),
+ filePath,
+ line,
+ method,
+ });
+ }
+ }
+
+ // Always recurse into children to find nested intl.getMessage() calls
+ ts.forEachChild(node, visit);
+ };
+
+ ts.forEachChild(sourceFile, visit);
+
+ return {
+ dynamicUsages: dynamicUsages.sort(sortByFileAndLine),
+ staticKeys: staticKeys.sort(sortByFileAndLine),
+ };
+};
+
+export const auditTranslations = ({ localeMessages, usage }) => {
+ const localeKeys = Object.keys(localeMessages);
+ const usedStaticKeySet = new Set(usage.staticKeys.map((entry) => entry.key));
+ const missingKeys = [...usedStaticKeySet]
+ .filter((key) => !Object.hasOwn(localeMessages, key))
+ .sort();
+ const unusedKeys = localeKeys.filter((key) => !usedStaticKeySet.has(key)).sort();
+
+ return {
+ dynamicUsages: [...usage.dynamicUsages].sort(sortByFileAndLine),
+ localeKeyCount: localeKeys.length,
+ missingKeys,
+ resultsAreIncomplete: usage.dynamicUsages.length > 0,
+ staticKeyCount: usage.staticKeys.length,
+ unusedKeys,
+ };
+};
+
+export const formatAuditReport = (report, { rootDir } = {}) => {
+ const lines = [
+ 'client_v2 translation audit',
+ `Static translation calls: ${report.staticKeyCount}`,
+ `Locale keys in en.json: ${report.localeKeyCount}`,
+ `Dynamic translation calls: ${report.dynamicUsages.length}`,
+ '',
+ ];
+
+ if (report.resultsAreIncomplete) {
+ lines.push(
+ 'WARNING: Dynamic translation usages were found. Missing and unused results are best-effort until those calls are rewritten with string literals.',
+ '',
+ );
+ }
+
+ if (report.missingKeys.length === 0) {
+ lines.push('Missing translation keys: none', '');
+ } else {
+ lines.push('Missing translation keys:');
+ report.missingKeys.forEach((key) => {
+ lines.push(`- ${key}`);
+ });
+ lines.push('');
+ }
+
+ if (report.unusedKeys.length === 0) {
+ lines.push('Unused translation keys: none', '');
+ } else {
+ lines.push('Unused translation keys:');
+ report.unusedKeys.forEach((key) => {
+ lines.push(`- ${key}`);
+ });
+ lines.push('');
+ }
+
+ if (report.dynamicUsages.length === 0) {
+ lines.push('Dynamic translation usages: none');
+ } else {
+ lines.push('Dynamic translation usages:');
+ report.dynamicUsages.forEach((entry) => {
+ lines.push(
+ `- ${toRelativePath(entry.filePath, rootDir)}:${entry.line} ${entry.expression}`,
+ );
+ });
+ }
+
+ return lines.join('\n').trimEnd();
+};
+
+export const loadLocaleMessages = async (localePath) => {
+ const fileContents = await fs.readFile(localePath, 'utf8');
+
+ return JSON.parse(fileContents);
+};
+
+export const listSourceFiles = async (directory) => {
+ const entries = await fs.readdir(directory, { withFileTypes: true });
+ const files = await Promise.all(
+ entries.map(async (entry) => {
+ const entryPath = path.join(directory, entry.name);
+
+ if (entry.isDirectory()) {
+ if (entry.name === '__tests__') {
+ return [];
+ }
+
+ return listSourceFiles(entryPath);
+ }
+
+ if (!SOURCE_EXTENSIONS.has(path.extname(entry.name)) || entry.name.endsWith('.d.ts')) {
+ return [];
+ }
+
+ return [entryPath];
+ }),
+ );
+
+ return files.flat().sort();
+};
+
+export const collectTranslationUsageFromFiles = async (filePaths) => {
+ const usage = {
+ dynamicUsages: [],
+ staticKeys: [],
+ };
+
+ const fileUsages = await Promise.all(
+ filePaths.map(async (filePath) => {
+ const source = await fs.readFile(filePath, 'utf8');
+
+ return collectTranslationUsageFromSource(source, filePath);
+ }),
+ );
+
+ fileUsages.forEach((fileUsage) => {
+ usage.staticKeys.push(...fileUsage.staticKeys);
+ usage.dynamicUsages.push(...fileUsage.dynamicUsages);
+ });
+
+ usage.staticKeys.sort(sortByFileAndLine);
+ usage.dynamicUsages.sort(sortByFileAndLine);
+
+ return usage;
+};
diff --git a/client_v2/src/__locales/ar.json b/client_v2/src/__locales/ar.json
new file mode 100644
index 00000000000..57e6368293d
--- /dev/null
+++ b/client_v2/src/__locales/ar.json
@@ -0,0 +1,388 @@
+{
+ "access_settings_title": "إعدادات الوصول",
+ "actions_table_header": "الإجراءات",
+ "add": "إضافة",
+ "add_allowlist": "إضافة قائمة السماح",
+ "add_blocklist": "إضافة قائمة الحظر",
+ "add_persistent_client": "إضافة كعميل دائم",
+ "add_to_allowlist": "إضافة إلى القائمة البيضاء",
+ "ads_blocked": "الإعلانات المحظورة",
+ "ads_blocked_card": "الإعلانات المحظورة",
+ "adult_websites_blocked_tooltip": "عدد من المواقع (الإباحية) للبالغين تم حجبها",
+ "all_lists_up_to_date_toast": "جميع القوائم محدثة بالفعل",
+ "allowed": "المسموح به",
+ "allowlist_add": "إضافة قائمة السماح",
+ "allowlist_edit": "تحرير القائمة البيضاء",
+ "apply": "تطبيق",
+ "autofix_warning_result": "نتيجة لذلك ، ستتم معالجة جميع طلبات DNS من نظامك بواسطة AdGuard Home افتراضيًا.",
+ "autofix_warning_text": "إذا قمت بالنقر فوق \"إصلاح\" ، فسيقوم AdGuard Home بتهيئة نظامك لاستخدام خادم AdGuard Home DNS.",
+ "average_time_processing": "متوسط وقت المعالجة",
+ "average_upstream_response_time": "متوسط وقت استجابة المنبع",
+ "back": "رجوع",
+ "block": "حظر",
+ "block_for_this_client_only": "احجب هذا العميل فقط",
+ "blocked": "محظور",
+ "blocked_by_cname_or_ip": "حظر بواسطة CNAME or IP",
+ "blocked_service": "الخدمات المحجوبة",
+ "blocked_services": "الخوادم المحجوبة",
+ "blocked_threats": "التهديدات المحظورة",
+ "blocked_threats_chart": "التهديدات المحظورة",
+ "blocklist_add_manual": "أضف يدوياً",
+ "blocklist_edit": "تحرير قائمة الحظر",
+ "blocklist_homepage": "الصفحة الرئيسية",
+ "blocklist_url_file_path": "عنوان URL أو مسار الملف",
+ "bootstrap_dns": "خوادم Bootstrap DNS",
+ "browse": "تصفح",
+ "cache_cleared": "تم مسح ذاكرة التخزين المؤقت لنظام أسماء النطاق بنجاح",
+ "cache_config_clear": "مسح ذاكرة التخزين المؤقت",
+ "cache_config_optimistic": "متفائل التخزين المؤقت",
+ "cache_config_ttl_validation": "يجب أن يكون الحد الأدنى لتجاوز TTL لذاكرة التخزين المؤقت أقل من أو يساوي الحد الأقصى",
+ "cache_enabled": "تفعيل ذاكرة التخزين المؤقت",
+ "cancel": "إلغاء",
+ "cancel_btn": "إلغاء",
+ "category_tooltip": "الفئة",
+ "changes_saved_success": "تم حفظ التغييرات",
+ "check_dhcp_servers": "تحقق من خوادم DHCP",
+ "check_not_found": "غير موجود في قوائم التصفية الخاصة بك",
+ "check_updates_btn": "التحقق من وجود تحديثات",
+ "clear_btn": "مسح",
+ "client_add": "إضافة عميل",
+ "client_details": "تفاصيل العميل",
+ "client_id": "عنوان العميل الشخصي",
+ "client_id_placeholder": "ادخل عنوان العميل الشخصي",
+ "client_identifier": "المعّرف",
+ "client_settings": "إعدادات العميل",
+ "client_table_header": "عميل",
+ "clients_add_identifier": "أضافة معّرف",
+ "clients_add_placeholder": "ادخل اسم العميل",
+ "clients_dns_cache_size": "حجم ذاكرة التخزين المؤقت لنظام أسماء النطاقات، بالبايت",
+ "clients_not_found": "لم يتم العثور على عملاء",
+ "clients_protection": "الحماية",
+ "clients_tags": "وسوم",
+ "clients_use_global_settings": "استخدم إعدادات عالمية",
+ "close": "إغلاق",
+ "confirm": "تأكيد",
+ "copied": "تم النسخ إلى الحافظة",
+ "copy": "نسخ",
+ "copy_to_clipboard": "نسخ إلى الحافظة",
+ "custom_filtering_rules": "قواعد التصفية المخصصة",
+ "dark_theme": "المظهر الداكن",
+ "dashboard": "لوحة القيادة",
+ "delete_btn": "حذف",
+ "delete_table_action": "حذف",
+ "dhcp_add_static_lease": "إضافة عقد إيجار ثابت",
+ "dhcp_config_saved": "الإعدادات محفوظة لخادم DHCP",
+ "dhcp_edit_static_lease": "تحرير عقد الإيجار الثابت",
+ "dhcp_form_gateway_input": "IP البوابة",
+ "dhcp_form_subnet_input": "قناع الشبكة الفرعية",
+ "dhcp_leases_not_found": "لم يتم العثور على عقود إيجار DHCP",
+ "dhcp_leases_title": "عقود إيجار DHCP",
+ "dhcp_new_static_lease": "عقد إيجار ثابت جديد",
+ "dhcp_reset_leases": "إعادة تعيين كافة عقود الإيجار",
+ "dhcp_reset_leases_success": "إعادة تعيين تأجير DHCP بنجاح",
+ "dhcp_settings": "إعدادات DHCP",
+ "dhcp_static_leases_not_found": "لم يتم العثور على عقود إيجار ثابتة DHCP",
+ "dhcp_table_hostname": "اسم المضيف",
+ "dhcp_table_ip_address": "عنوان IP",
+ "disable": "تعطيل",
+ "disable_protection_btn": "تعطيل الحماية",
+ "disabled_dhcp": "خادم DHCP غير مفعل",
+ "disallow_this_client": "منع هذا العميل",
+ "dns_access_settings_title": "إعدادات الوصول",
+ "dns_allowed_clients": "العملاء المسموحين",
+ "dns_blocking_mode": "وضع الحجب",
+ "dns_blocking_mode_custom_ip": "عنوان IP مخصص",
+ "dns_blocking_mode_default": "افتراضي",
+ "dns_blocking_mode_ipv4_label": "حجب عنوان IPv4",
+ "dns_blocking_mode_ipv6_label": "حجب عنوان IPv6",
+ "dns_blocking_mode_nxdomain": "NXDOMAIN",
+ "dns_blocking_mode_refused": "مرفوض",
+ "dns_blocking_mode_title": "وضع الحجب",
+ "dns_bootstrap_dns_placeholder": "عناوين الـIP",
+ "dns_bootstrap_dns_title": "خوادم Bootstrap DNS",
+ "dns_bootstrap_servers": "خوادم Bootstrap DNS",
+ "dns_cache_config": "ضبط الملفات المؤقتة لـ DNS",
+ "dns_cache_size": "حجم ذاكرة التخزين المؤقت",
+ "dns_cache_size_label": "حجم ذاكرة التخزين المؤقت لنظام أسماء النطاقات، بالبايت",
+ "dns_cache_size_title": "حجم ذاكرة التخزين المؤقت",
+ "dns_clear_cache": "مسح ذاكرة التخزين المؤقت",
+ "dns_clear_cache_cancel": "إلغاء",
+ "dns_clear_cache_confirm": "نعم، مسح",
+ "dns_clear_cache_yes": "نعم، مسح",
+ "dns_config": "إعداد خادم DNS",
+ "dns_disallowed_clients": "العملاء غير المسموحين",
+ "dns_disallowed_clients_title": "العملاء غير المسموحين",
+ "dns_disallowed_domains": "النطاقات غير المسموح بها",
+ "dns_disallowed_domains_title": "النطاقات غير المسموح بها",
+ "dns_edns_custom_placeholder": "أدخل عنوان IP",
+ "dns_fallback_dns_title": "خوادم DNS الاحتياطية",
+ "dns_fallback_servers": "خوادم DNS الاحتياطية",
+ "dns_optimistic_caching": "متفائل التخزين المؤقت",
+ "dns_over_https": "DNS-over-HTTPS",
+ "dns_over_quic": "DNS-over-QUIC",
+ "dns_over_tls": "DNS-over-TLS",
+ "dns_override_max_ttl": "تجاوز الحد الاقصى من مدة البقاء TTL",
+ "dns_override_max_ttl_title": "تجاوز الحد الاقصى من مدة البقاء TTL",
+ "dns_override_min_ttl": "تجاوز الحد الأدنى من مدة البقاء TTL",
+ "dns_override_min_ttl_title": "تجاوز الحد الأدنى من مدة البقاء TTL",
+ "dns_privacy": "خصوصية DNS",
+ "dns_private_reverse_servers_title": "خوادم DNS العكسية الخاصة",
+ "dns_rate_limit": "حدود التقييم",
+ "dns_rate_limit_allowlist_placeholder": "عناوين الـIP",
+ "dns_rate_limit_placeholder": "ادخل حد التقييم",
+ "dns_rate_limit_title": "حدود التقييم",
+ "dns_rewrites": "إعادة كتابة DNS",
+ "dns_server_addresses": "عناوين الخوادم",
+ "dns_server_addresses_label": "عناوين الخادم واحد لكل سطر",
+ "dns_server_addresses_title": "عناوين الخوادم",
+ "dns_server_config_title": "إعداد خادم DNS",
+ "dns_settings": "DNS",
+ "dns_test_upstreams": "اختبار upstream",
+ "dns_upstream_servers_title": "خادم DNS لـ Upstream",
+ "dns_upstream_timeout": "مهلة الاتصال بالخادم العلوي",
+ "dns_upstream_timeout_title": "مهلة الاتصال بالخادم العلوي",
+ "dns_upstream_validation_invalid": "تنسيق غير صالح",
+ "domain": "النطاق",
+ "domain_or_client": "الدومين أو العميل",
+ "download_mobileconfig": "حمّل ملف الإعدادات",
+ "edit_table_action": "تحرير",
+ "enable": "تمكين",
+ "enabled_dhcp": "خادم DHCP مفعل",
+ "encryption_certificate_path": "مسار الشهادة",
+ "encryption_certificates": "الشهادات",
+ "encryption_certificates_input": "انسخ / الصق الشهادات المشفرة PEM هنا.",
+ "encryption_certificates_source_content": "الصق محتويات الشهادات",
+ "encryption_certificates_source_path": "قم بتعيين مسار ملف الشهادات",
+ "encryption_chain_invalid": "سلسلة الشهادات غير صالحة",
+ "encryption_chain_valid": "سلسلة الشهادات صالحة",
+ "encryption_config_saved": "تم حفظ اعدادات التشفير",
+ "encryption_doq": "DNS-over-QUIC port",
+ "encryption_dot": "منفذ DNS-over-TLS",
+ "encryption_https": "منفذ HTTPS",
+ "encryption_key": "مفتاح خاص",
+ "encryption_key_input": "انسخ / الصق مفتاحك الخاص المشفر بـ PEM لشهادتك هنا",
+ "encryption_key_source_content": "الصق محتويات المفتاح الخاص",
+ "encryption_plain_dns": "عنوان DNS العادي",
+ "encryption_plain_dns_error": "لتعطيل DNS العادي، قم بتمكين بروتوكول DNS المشفر على الأقل",
+ "encryption_private_key_path": "مسار المفتاح الخاص",
+ "encryption_server": "اسم الخادم",
+ "encryption_server_enter": "ادخل عنوان النطاق الخاص بك",
+ "encryption_title": "التعمية",
+ "enter_cache_size": "أدخل حجم ذاكرة التخزين المؤقت (بايت)",
+ "enter_ip_address_placeholder": "أدخل عنوان IP",
+ "error": "خطأ",
+ "fallback_dns": "خوادم DNS الاحتياطية",
+ "filter_category_general_desc": "القوائم التي تحظر التتبع والإعلانات على معظم الأجهزة",
+ "filter_category_other": "أخرى",
+ "filter_category_other_desc": "قوائم الحظر الأخرى",
+ "filter_category_regional": "إقليمي",
+ "filter_category_regional_desc": "القوائم التي تركز على الإعلانات الإقليمية وخوادم التتبع",
+ "filter_category_security": "الامان",
+ "filter_category_security_desc": "القوائم المصممة خصيصًا لحظر النطاقات الخبيثة والتصيد الاحتيالي والخداع",
+ "filters": "التصفية",
+ "forgot_password": "نسيت كلمة المرور؟",
+ "form_enter_blocked_response_ttl": "أدخل وقت الاستجابة المحظورة TTL (بالثواني)",
+ "form_error_client_id_format": "يجب أن يحتوي معرف العميل على الأرقام والأحرف الصغيرة والواصلات فقط",
+ "form_error_domain_format": "تنسيق النطاق غير صالح",
+ "form_error_equal": "يجب ألا تكون متساوية",
+ "form_error_format": "تنسيق غير صالح",
+ "form_error_gateway_ip": "لا يمكن الحصول على عنوان IP الخاص بالبوابة",
+ "form_error_ip4_format": "عنوان IPv4 غير صالح",
+ "form_error_ip6_format": "عنوان IPv6 غير صالح",
+ "form_error_ip_format": "عنوان IP غير صالح",
+ "form_error_mac_format": "عنوان MAC غير صالح",
+ "form_error_password": "كلمة السر غير مطابقة",
+ "form_error_port": "أدخل رقم منفذ صالح",
+ "form_error_port_range": "أدخل رقم المنفذ في النطاق 80-65535",
+ "form_error_port_unsafe": "منفذ غير آمن",
+ "form_error_server_name": "اسم الخادم غير صالح",
+ "form_error_url_format": "أدخل عنوان URL أو مسار ملف صالح",
+ "friday": "الجمعة",
+ "gateway_or_subnet_invalid": "قناع الشبكة الفرعية غير صالح",
+ "general_statistics": "الإحصاءات العامة",
+ "ignore_domains_title": "نطاقات تم تجاهلها",
+ "inactivity_schedule_add": "إضافة جدول زمني",
+ "inactivity_schedule_all_day": "طوال اليوم",
+ "inactivity_schedule_edit": "تحرير الجدول الزمني",
+ "install_auth_confirm": "تأكيد كلمة المرور",
+ "install_auth_password": "كلمة المرور",
+ "install_auth_password_enter": "أدخل كلمة المرور",
+ "install_auth_username": "اسم المستخدم",
+ "install_auth_username_enter": "أدخل اسم المستخدم",
+ "install_saved": "تم الحفظ بنجاح",
+ "install_settings_all_interfaces": "جميع الواجهات",
+ "install_settings_port": "المنفذ",
+ "install_static_error": "لا يمكن لـ AdGuard Home تكوينه تلقائيًا لواجهة الشبكة هذه. الرجاء البحث عن تعليمات حول كيفية القيام بذلك يدويًا.",
+ "install_static_ok": "أخبار جيدة! تم ضبط عنوان IP الثابت بالفعل",
+ "install_step": "خطوة",
+ "install_submit_title": "تهانينا!",
+ "ip_address": "عنوان IP",
+ "known_tracker": "متعقب معروف",
+ "language": "اللغة",
+ "light_theme": "المظهر فاتح",
+ "login": "تسجيل الدخول",
+ "logout": "تسجيل خروج",
+ "make_static": "اجعلها ثابتة",
+ "milliseconds_abbreviation": "ms",
+ "monday": "الإثنين",
+ "name": "الاسم",
+ "name_label": "الاسم",
+ "name_table_header": "الاسم",
+ "name_tooltip": "الاسم",
+ "next": "التالي",
+ "no": "لا",
+ "none_text": "لا شيء",
+ "nothing_found": "لم يتم العثور على شيء",
+ "notify_undo": "تراجع",
+ "notify_updated": "تم تحديثه في",
+ "parental_control": "الرِّقابة الأبوية",
+ "parental_group_cdn": "CDN",
+ "parental_group_development": "تطوير البرمجيات",
+ "parental_group_gaming": "الألعاب الرقمية",
+ "parental_group_hosting": "استضافة الويب",
+ "parental_group_privacy": "أدوات الخصوصية",
+ "parental_group_shopping": "التسوق",
+ "parental_group_social": "شبكات التواصل الإجتماعية",
+ "parental_group_social_network": "شبكات التواصل الإجتماعية",
+ "parental_group_software": "تطوير البرمجيات",
+ "parental_group_streaming": "البث",
+ "password_label": "كلمة المرور",
+ "password_placeholder": "أدخل كلمة المرور",
+ "plain_dns": "عنوان DNS العادي",
+ "privacy_policy": "سياسة الخصوصية",
+ "protection": "الحماية",
+ "protocol": "البروتوكول",
+ "query_log_allowed": "المسموح به",
+ "query_log_blocked": "محظور",
+ "query_log_blocked_services": "الخوادم المحجوبة",
+ "query_log_blocked_threats": "التهديدات المحظورة",
+ "query_log_custom_filtering_rules": "قواعد التصفية المخصصة",
+ "query_log_nothing_available": "لم يتم العثور على شيء",
+ "query_log_processed": "تم معالجته",
+ "query_log_rewritten": "أعيدت كتابته",
+ "query_log_safe_search": "البحث الأمن",
+ "query_log_strict_search": "استخدم علامات الاقتباس المزدوجة للبحث الدقيق",
+ "rate_limit_subnet_len_ipv4_error": "يجب أن يكون طول بادئة الشبكة IPv4 بين 0 و 32",
+ "rate_limit_subnet_len_ipv6_error": "يجب أن يكون طول بادئة الشبكة IPv6 بين 0 و 128",
+ "refresh_btn": "تحديث",
+ "refresh_statics": "تحيين الإحصائيات",
+ "release_notes": "ملاحظات الإصدار",
+ "remove": "إزالة",
+ "report_an_issue": "الإبلاغ عن مشكلة",
+ "request_table_header": "طلب",
+ "requests_table_header": "الطلبات",
+ "reset": "إعادة ضبط",
+ "reset_settings": "إعادة ضبط الإعدادات",
+ "reset_settings_confirm": "نعم، إعادة تعيين",
+ "response_details": "تفاصيل الاستجابة",
+ "response_time": "وقت الاستجابة",
+ "result": "نتيجة",
+ "rewrite_add": "إضافة إعادة كتابة DNS",
+ "rewrite_edit": "تحرير إعادة كتابة DNS",
+ "rewrite_hosts_applied": "أعيد كتابتها بواسطة قاعدة ملف المضيفين",
+ "rewrite_settings_updated": "تم تحديث إعدادات إعادة كتابة DNS بنجاح",
+ "rewrite_updated": "تم تحديث إعادة كتابة DNS بنجاح",
+ "rewritten": "أعيدت كتابته",
+ "rules_label": "القواعد",
+ "safe_browsing": "تصفح آمن",
+ "safe_search": "البحث الأمن",
+ "saturday": "السبت",
+ "save": "حفظ",
+ "save_btn": "حفظ",
+ "search_placeholder": "بحث",
+ "server_config_blocking_mode": "وضع الحجب",
+ "server_config_blocking_mode_ipv4": "حجب عنوان IPv4",
+ "server_config_blocking_mode_ipv6": "حجب عنوان IPv6",
+ "server_config_custom_ip": "عنوان IP مخصص",
+ "server_config_default": "افتراضي",
+ "server_config_disable_ipv6": "قم بتعطيل تحليل عناوين IPv6",
+ "server_config_dnssec_enable": "تفعيل DNSSEC",
+ "server_config_edns_enable": "فعل EDNS client subnet",
+ "server_config_nxdomain": "NXDOMAIN",
+ "server_config_rate_limit_placeholder": "ادخل حد التقييم",
+ "server_config_rate_limit_whitelist_faq": "عناوين IP المستبعدة من تحديد معدل الحد الأقصى",
+ "server_config_refused": "مرفوض",
+ "server_config_subnet_len_ipv4": "طول بادئة الشبكة لعناوين IPv4",
+ "server_config_subnet_len_ipv4_faq": "طول بادئة الشبكة لعناوين IPv4 المستخدمة لتحديد معدل الحد الأقصى. الافتراضي هو 24",
+ "server_config_subnet_len_ipv6": "طول بادئة الشبكة لعناوين IPv6",
+ "server_config_subnet_len_ipv6_faq": "طول بادئة الشبكة لعناوين IPv6 المستخدمة لتحديد معدل الحد الأقصى. الافتراضي هو 56",
+ "set_static_ip": "قم بتعيين عنوان IP ثابت",
+ "settings": "الإعدادات",
+ "settings_browsing_security": "أمان التصفح",
+ "settings_confirm_clear_statistics": "مسح الإحصائيات؟",
+ "settings_custom": "مخصص",
+ "settings_global": "عالمي",
+ "settings_notify_changes_saved": "تم حفظ التغييرات",
+ "settings_notify_statistics_cleared": "تم مسح الإحصائيات",
+ "settings_parental_control": "التحكم الأبوي",
+ "settings_rule_syntax": "صيغة القاعدة",
+ "settings_safe_search": "البحث الأمن",
+ "settings_statistics": "الإحصائيات",
+ "settings_statistics_clear": "مسح الإحصائيات",
+ "settings_statistics_retention": "الاحتفاظ بالإحصاءات",
+ "settings_yes_clear": "نعم، مسح",
+ "setup_devices_router_title": "راوتر",
+ "setup_dns_privacy_ioc_mac": "اعدادات iOS و macOS",
+ "setup_dns_privacy_other_title": "تطبيقات أخرى",
+ "show_blocked_responses": "محظور",
+ "show_processed_responses": "تم معالجته",
+ "source_label": "المصدر",
+ "source_tooltip": "المصدر",
+ "stats_adult": "حظر مواقع الويب الخاصة بالبالغين",
+ "stats_query_domain": "اعلى النطاقات التي تم الاستعلام عنها",
+ "status_table_header": "الحالة",
+ "sunday": "الأحد",
+ "system_host_files": "ملفات الهوست للنظام",
+ "table_client": "عميل",
+ "tags_title": "وسوم",
+ "test_upstreams": "اختبار upstream",
+ "threats_blocked": "التهديدات المحظورة",
+ "thursday": "الخميس",
+ "time_table_header": "الوقت",
+ "top_blocked_domains": "اعلى الدومينات المحظورة",
+ "top_clients": "كبار العملاء",
+ "top_upstreams": "أعلى الخوادم upstream",
+ "total": "المجموع",
+ "try_again": "حاول مرة أخرى",
+ "tuesday": "الثلاثاء",
+ "unavailable_dhcp": "DHCP غير متوفر",
+ "unavailable_dhcp_desc": "لا يمكن لـ AdGuard Home تشغيل خادم DHCP على نظام التشغيل الخاص بك",
+ "unblock": "إلغاء الحظر",
+ "update_button": "تحديث",
+ "update_filters_disable": "تعطيل",
+ "update_filters_interval_custom": "مخصص",
+ "update_filters_interval_daily": "كل يوم",
+ "update_filters_interval_hourly": "كل ساعة",
+ "update_filters_title": "تحديث الفلاتر تلقائيا",
+ "updates_checked": "يتوفر إصدار جديد من AdGuard Home",
+ "updates_version_equal": "AdGuard Home محدث",
+ "upstream": "Upstream",
+ "upstream_dns": "خادم DNS لـ Upstream",
+ "upstream_dns_addresses": "عناوين الخادم واحد لكل سطر",
+ "upstream_dns_fastest_addr": "أسرع عنوان IP",
+ "upstream_dns_parallel_requests": "الطلبات الموازية",
+ "upstream_dns_servers_title": "خادم DNS لـ Upstream",
+ "upstream_examples_title": "أمثلة",
+ "upstream_private_ptr_resolvers_title": "استخدم محللات DNS العكسية الخاصة",
+ "upstreams": "Upstreams",
+ "url_label": "الرابط",
+ "user_rules": "قواعد المستخدم",
+ "user_rules_add_to_allowlist": "إضافة إلى القائمة البيضاء",
+ "user_rules_check_button": "تحقق",
+ "user_rules_check_client_placeholder": "أدخل معرّف العميل",
+ "user_rules_check_hostname_label": "اسم المضيف أو اسم النطاق",
+ "user_rules_disable_filter": "تعطيل عامل التصفية",
+ "user_rules_disable_parental_control": "تعطيل الرقابة الأبوية",
+ "user_rules_dns_record_type_placeholder": "حدد نوع سجل DNS",
+ "user_rules_domain_blocked": "تم حظر النطاق",
+ "user_rules_edit_dns_rewrite": "تحرير إعادة كتابة DNS",
+ "user_rules_rewrite_rule_is_applied": "يتم تطبيق قاعدة إعادة الكتابة",
+ "username_label": "اسم المستخدم",
+ "username_placeholder": "أدخل اسم المستخدم",
+ "validated_with_dnssec": "تم التحقق من صحتها باستخدام DNSSEC",
+ "wednesday": "الأربعاء",
+ "whois": "WHOIS",
+ "yes": "نعم",
+ "yes_disable": "نعم، تعطيل",
+ "yes_reset": "نعم، إعادة تعيين"
+}
diff --git a/client_v2/src/__locales/be.json b/client_v2/src/__locales/be.json
new file mode 100644
index 00000000000..505f51d698e
--- /dev/null
+++ b/client_v2/src/__locales/be.json
@@ -0,0 +1,384 @@
+{
+ "access_settings_title": "Налады доступу",
+ "actions_table_header": "Дзеянні",
+ "add": "Дадаць",
+ "add_allowlist": "Дадаць у спіс дазволеных",
+ "add_blocklist": "Дадаць у спіс заблакіраваных",
+ "add_persistent_client": "Дадаць як пастаяннага кліента",
+ "add_to_allowlist": "Дадаць у спіс дазволеных",
+ "ads_blocked": "Рэклам заблакіравана",
+ "ads_blocked_card": "Рэклам заблакіравана",
+ "adult_websites_blocked_tooltip": "Колькасць заблакаваных «сайтаў для дарослых»",
+ "all_lists_up_to_date_toast": "Усе спісы ўжо абноўлены",
+ "allowed": "Дазволена",
+ "allowlist_add": "Дадаць у спіс дазволеных",
+ "allowlist_edit": "Рэдагаваць спіс дазволеных",
+ "apply": "Ужыць",
+ "autofix_warning_result": "У выніку ўсе запыты DNS ад вашай сістэмы будуць прадвызначана апрацоўвацца AdGuard Home.",
+ "autofix_warning_text": "Пры націску «Выправіць» AdGuard Home сканфігурыруе вашу сістэму для выкарыстання сервера DNS ад AdGuard Home.",
+ "average_time_processing": "Сярэдні час апрацоўкі",
+ "average_upstream_response_time": "Сярэдні час адказу сервера upstream",
+ "back": "Назад",
+ "block": "Заблакіраваць",
+ "block_for_this_client_only": "Заблакіраваць толькі для гэтага кліента",
+ "blocked": "Заблакіравана",
+ "blocked_by_cname_or_ip": "Заблакіравана па запісе CNAME або IP",
+ "blocked_service": "Заблакіраваны сэрвіс",
+ "blocked_services": "Заблакіраваныя сэрвісы",
+ "blocked_threats": "Заблакіравана пагроз",
+ "blocked_threats_chart": "Заблакіравана пагроз",
+ "blocklist_add_manual": "Дадаць уручную",
+ "blocklist_edit": "Рэдагаваць спіс заблакіраваных",
+ "blocklist_homepage": "Хатняя старонка",
+ "blocklist_url_file_path": "URL-адрас або шлях да файла",
+ "bootstrap_dns": "Bootstrap сервер DNS",
+ "browse": "Агляд",
+ "cache_cleared": "Кэш DNS паспяхова ачышчаны",
+ "cache_config_clear": "Ачысціць кэш",
+ "cache_config_optimistic": "Аптымістычнае кэшаванне",
+ "cache_config_ttl_validation": "Мінімальнае значэнне TTL кэша павінна быць менш ці роўна максімальнаму значэнню",
+ "cache_enabled": "Уключыць кэш",
+ "cancel": "Скасаваць",
+ "cancel_btn": "Скасаваць",
+ "category_tooltip": "Катэгорыя",
+ "changes_saved_success": "Змены захаваны",
+ "check_dhcp_servers": "Праверыць серверы DHCP",
+ "check_not_found": "Не знойдзена ў вашым спісе фільтраў",
+ "check_updates_btn": "Праверыць абнаўленні",
+ "clear_btn": "Ачысціць",
+ "client_add": "Дадаць кліента",
+ "client_details": "Падрабязнасці кліента",
+ "client_id": "ClientID",
+ "client_id_placeholder": "Увядзіце ClientID",
+ "client_identifier": "Ідэнтыфікатар",
+ "client_settings": "Налады кліентаў",
+ "client_table_header": "Кліент",
+ "clients_add_identifier": "Дадаць ідэнтыфікатар",
+ "clients_add_placeholder": "Увядзіце назву кліента",
+ "clients_dns_cache_size": "Памер кэшу DNS, у байтах",
+ "clients_not_found": "Кліенты не знойдзены",
+ "clients_protection": "Абарона",
+ "clients_tags": "Тэгі",
+ "clients_use_global_settings": "Выкарыстоўваць глабальныя налады",
+ "close": "Закрыць",
+ "confirm": "Пацвердзіць",
+ "copied": "Скапіявана ў буфер абмену",
+ "copy": "Скапіраваць",
+ "copy_to_clipboard": "Скапіяваць у буфер абмену",
+ "custom_filtering_rules": "Карыстальніцкія правілы фільтрацыі",
+ "dark_theme": "Цёмная тэма",
+ "dashboard": "Панэль кіравання",
+ "delete_btn": "Выдаліць",
+ "delete_table_action": "Выдаліць",
+ "dhcp_add_static_lease": "Дадаць статычную арэнду",
+ "dhcp_edit_static_lease": "Рэдагаваць статычную арэнду",
+ "dhcp_form_gateway_input": "IP-адрас шлюза",
+ "dhcp_form_subnet_input": "Маска падсеткі",
+ "dhcp_leases_not_found": "Арэнда DHCP не знойдзена",
+ "dhcp_leases_title": "Арэндныя DHCP",
+ "dhcp_new_static_lease": "Новая статычная арэнда",
+ "dhcp_reset_leases": "Скінуць усё арэнды",
+ "dhcp_reset_leases_success": "Арэнды DHCP паспяхова скінуты",
+ "dhcp_settings": "Налады DHCP",
+ "dhcp_static_leases_not_found": "Статычная арэнда DHCP не знойдзена",
+ "dhcp_table_hostname": "Назва вузла",
+ "dhcp_table_ip_address": "IP-адрас",
+ "disable": "Адключыць",
+ "disable_protection_btn": "Адключыць абарону",
+ "disabled_dhcp": "Сервер DHCP адключаны",
+ "disallow_this_client": "Забараніць доступ гэтаму кліенту",
+ "dns_access_settings_title": "Налады доступу",
+ "dns_allowed_clients": "Дазволеныя кліенты",
+ "dns_blocking_mode": "Рэжым блакіроўкі",
+ "dns_blocking_mode_custom_ip": "Карыстальніцкі IP-адрас",
+ "dns_blocking_mode_default": "Прадвызначана",
+ "dns_blocking_mode_ipv4_label": "Блакіроўка IPv4",
+ "dns_blocking_mode_ipv6_label": "Блакіроўка IPv6",
+ "dns_blocking_mode_nxdomain": "NXDOMAIN",
+ "dns_blocking_mode_refused": "REFUSED",
+ "dns_blocking_mode_title": "Рэжым блакіроўкі",
+ "dns_bootstrap_dns_placeholder": "IP-адрасы",
+ "dns_bootstrap_dns_title": "Bootstrap сервер DNS",
+ "dns_bootstrap_servers": "Bootstrap сервер DNS",
+ "dns_cache_config": "Канфігурацыя кэшу DNS",
+ "dns_cache_size": "Памер кэшу",
+ "dns_cache_size_label": "Памер кэшу DNS, у байтах",
+ "dns_cache_size_title": "Памер кэшу",
+ "dns_clear_cache": "Ачысціць кэш",
+ "dns_clear_cache_cancel": "Скасаваць",
+ "dns_clear_cache_confirm": "Так, скінуць",
+ "dns_clear_cache_yes": "Так, скінуць",
+ "dns_config": "Канфігурацыя сервера DNS",
+ "dns_disallowed_clients": "Забароненыя кліенты",
+ "dns_disallowed_clients_title": "Забароненыя кліенты",
+ "dns_disallowed_domains": "Забароненыя дамены",
+ "dns_disallowed_domains_title": "Забароненыя дамены",
+ "dns_edns_custom_placeholder": "Увядзіце IP-адрас",
+ "dns_fallback_dns_title": "Рэзервовыя сервер DNS",
+ "dns_fallback_servers": "Рэзервовыя сервер DNS",
+ "dns_optimistic_caching": "Аптымістычнае кэшаванне",
+ "dns_over_https": "DNS-over-HTTPS",
+ "dns_over_quic": "DNS-over-QUIC",
+ "dns_over_tls": "DNS-over-TLS",
+ "dns_override_max_ttl": "Перавызначыць максімальны TTL",
+ "dns_override_max_ttl_title": "Перавызначыць максімальны TTL",
+ "dns_override_min_ttl": "Перавызначыць мінімальны TTL",
+ "dns_override_min_ttl_title": "Перавызначыць мінімальны TTL",
+ "dns_privacy": "Прыватнасць DNS",
+ "dns_private_reverse_servers_title": "Прыватныя серверы для зваротнага DNS",
+ "dns_rate_limit": "Абмежаванні хуткасці",
+ "dns_rate_limit_allowlist_placeholder": "IP-адрасы",
+ "dns_rate_limit_placeholder": "Увядзіце абмежаванне хуткасці",
+ "dns_rate_limit_title": "Абмежаванні хуткасці",
+ "dns_rewrites": "Перазапісы DNS",
+ "dns_server_addresses": "Адрасы сервера",
+ "dns_server_addresses_label": "Адрасы сервераў, па адным у радку",
+ "dns_server_addresses_title": "Адрасы сервера",
+ "dns_server_config_title": "Канфігурацыя сервера DNS",
+ "dns_settings": "DNS",
+ "dns_test_upstreams": "Тэст сервераў upstream",
+ "dns_upstream_servers_title": "Серверы upstream DNS",
+ "dns_upstream_timeout": "Час чакання для upstream.",
+ "dns_upstream_timeout_title": "Час чакання для upstream.",
+ "dns_upstream_validation_invalid": "Памылковы фармат",
+ "domain": "Дамен",
+ "domain_or_client": "Дамен або кліент",
+ "download_mobileconfig": "Спампаваць файл канфігурацыі",
+ "edit_table_action": "Рэдагаваць",
+ "enable": "Уключыць",
+ "enabled_dhcp": "Сервер DHCP уключаны",
+ "encryption_certificate_path": "Шлях да сертыфіката",
+ "encryption_certificates": "Сертыфікаты",
+ "encryption_certificates_input": "Скапіюйце сюды сертыфікаты ў PEM-кадоўцы.",
+ "encryption_certificates_source_content": "Уставіць змесціва сертыфікатаў",
+ "encryption_certificates_source_path": "Задаць шлях да файла сертыфікатаў",
+ "encryption_chain_invalid": "Памылковы ланцужок сертыфікатаў",
+ "encryption_chain_valid": "Ланцужок сертыфікатаў з'яўляецца сапраўдным",
+ "encryption_config_saved": "Канфігурацыя шыфравання захавана",
+ "encryption_doq": "Порт DNS-over-QUIC",
+ "encryption_dot": "Порт DNS-over-TLS",
+ "encryption_https": "Порт HTTPS",
+ "encryption_key": "Прыватны ключ",
+ "encryption_key_input": "Скапіюйце сюды прыватны ключ у PEM-кадоўцы.",
+ "encryption_key_source_content": "Уставіць змесціва прыватнага ключа",
+ "encryption_plain_dns": "Звычайны DNS",
+ "encryption_plain_dns_error": "Уключыце прынамсі адзін зашыфраваны пратакол DNS для адключэння звычайнага DNS",
+ "encryption_private_key_path": "Шлях да прыватнага ключа",
+ "encryption_server": "Назва сервера",
+ "encryption_server_enter": "Увядзіце сваё даменнае імя",
+ "encryption_title": "Шыфраванне",
+ "enter_cache_size": "Увядзіце памер кэшу (у байтах)",
+ "enter_ip_address_placeholder": "Увядзіце IP-адрас",
+ "error": "Памылка",
+ "fallback_dns": "Рэзервовыя сервер DNS",
+ "filter_category_general_desc": "Спісы, якія блакіруюць праграмы сачэння і рэкламу на большасці прылад",
+ "filter_category_other": "Іншыя",
+ "filter_category_other_desc": "Іншыя спісы заблакіраваных",
+ "filter_category_regional": "Рэгіянальныя",
+ "filter_category_regional_desc": "Спісы, якія факусуюцца на рэгіянальнай рэкламе і серверах асочвання",
+ "filter_category_security": "Бяспека",
+ "filter_category_security_desc": "Спісы, якія спецыялізуюцца на блакаванні шкодных праграм, фішынгавых ці махлярскіх даменаў",
+ "filters": "Фільтры",
+ "forgot_password": "Забылі пароль?",
+ "form_enter_blocked_response_ttl": "Увядзіце TTL заблакіраванага адказу (у секундах)",
+ "form_error_client_id_format": "ClientID павінен утрымліваць толькі лічбы, малыя літары і злучкі",
+ "form_error_domain_format": "Памылковы фармат дамена",
+ "form_error_equal": "Нельга, каб былі роўнымі",
+ "form_error_format": "Памылковы фармат",
+ "form_error_gateway_ip": "Арэнда не можа мець IP-адрас шлюза",
+ "form_error_ip4_format": "Памылковы IPv4-адрас",
+ "form_error_ip6_format": "Памылковы IPv6-адрас",
+ "form_error_ip_format": "Памылковы IP-адрас",
+ "form_error_mac_format": "Памылковы MAC-адрас",
+ "form_error_password": "Паролі не супадаюць",
+ "form_error_port": "Увядзіце сапраўдны нумар парта",
+ "form_error_port_range": "Увядзіце нумар порта з інтэрвалу 80-65535",
+ "form_error_port_unsafe": "Небяспечны порт",
+ "form_error_server_name": "Памылковая назва сервера",
+ "form_error_url_format": "Увядзіце сапраўдны URL-адрас або шлях да файла",
+ "friday": "Пятніца",
+ "gateway_or_subnet_invalid": "Памылковая маска падсеткі",
+ "general_statistics": "Агульная статыстыка",
+ "ignore_domains_title": "Дамены, якія ігнаруюцца",
+ "inactivity_schedule_add": "Дадаць расклад",
+ "inactivity_schedule_all_day": "Увесь дзень",
+ "inactivity_schedule_edit": "Рэдагаваць расклад",
+ "install_auth_confirm": "Пацвердзіць пароль",
+ "install_auth_password": "Пароль",
+ "install_auth_password_enter": "Увядзіце пароль",
+ "install_auth_username": "Імя карыстальніка",
+ "install_auth_username_enter": "Увядзіце імя карыстальніка",
+ "install_saved": "Паспяхова захавана",
+ "install_settings_all_interfaces": "Усе інтэрфейсы",
+ "install_settings_port": "Порт",
+ "install_static_error": "AdGuard Home не можа аўтаматычна наладзіць яго для гэтага сеткавага інтэрфейсу. Азнаёмцеся з інструкцыяй па ўнясенні змен уручную.",
+ "install_static_ok": "Добрыя навіны! Ваш статычны IP-адрас ужо наладжаны",
+ "install_step": "Крок",
+ "install_submit_title": "Віншуем!",
+ "ip_address": "IP-адрас",
+ "known_tracker": "Вядомыя праграмы сачэння",
+ "language": "Мова",
+ "light_theme": "Светлая тэма",
+ "login": "Увайсці",
+ "logout": "Выйсці",
+ "make_static": "Зрабіць статычным",
+ "milliseconds_abbreviation": "мс",
+ "monday": "Панядзелак",
+ "name": "Назва",
+ "name_label": "Назва",
+ "name_table_header": "Назва",
+ "name_tooltip": "Назва",
+ "next": "Далей",
+ "no": "Не",
+ "none_text": "Адсутнічае",
+ "nothing_found": "Нічога не знойдзена",
+ "notify_undo": "Адрабіць",
+ "notify_updated": "Абноўлена",
+ "parental_control": "Бацькоўскі кантроль",
+ "parental_group_cdn": "CDN",
+ "parental_group_development": "Распрацоўка праграмнага забеспячэння",
+ "parental_group_gaming": "Гульні",
+ "parental_group_hosting": "Вэб-хостынг",
+ "parental_group_privacy": "Інструменты прыватнасці",
+ "parental_group_shopping": "Крамы",
+ "parental_group_social": "Сацыяльныя сеткі",
+ "parental_group_social_network": "Сацыяльныя сеткі",
+ "parental_group_software": "Распрацоўка праграмнага забеспячэння",
+ "password_label": "Пароль",
+ "password_placeholder": "Увядзіце пароль",
+ "plain_dns": "Звычайны DNS",
+ "privacy_policy": "Палітыка прыватнасці",
+ "protection": "Абарона",
+ "protocol": "Пратакол",
+ "query_log_allowed": "Дазволена",
+ "query_log_blocked": "Заблакіравана",
+ "query_log_blocked_services": "Заблакіраваныя сэрвісы",
+ "query_log_blocked_threats": "Заблакіравана пагроз",
+ "query_log_custom_filtering_rules": "Карыстальніцкія правілы фільтрацыі",
+ "query_log_nothing_available": "Нічога не знойдзена",
+ "query_log_processed": "Апрацавана",
+ "query_log_rewritten": "Перапісаныя",
+ "query_log_safe_search": "Бяспечны пошук",
+ "query_log_strict_search": "Скарыстайцеся двайнымі двукоссямі для строгага пошуку",
+ "rate_limit_subnet_len_ipv4_error": "Даўжыня прэфікса падсеткі IPv4 павінна быць ад 0 да 32",
+ "rate_limit_subnet_len_ipv6_error": "Даўжыня прэфікса падсеткі IPv6 павінна быць ад 0 да 128",
+ "refresh_btn": "Абнавіць",
+ "refresh_statics": "Абнавіць статыстыку",
+ "release_notes": "Нататкі пра выпуск",
+ "remove": "Выдаліць",
+ "report_an_issue": "Паведаміць аб праблеме",
+ "request_table_header": "Запыт",
+ "requests_table_header": "Запыты",
+ "reset": "Скінуць",
+ "reset_settings": "Скінуць налады",
+ "reset_settings_confirm": "Так, скінуць",
+ "response_details": "Падрабязнасці адказу",
+ "response_time": "Час адказу",
+ "result": "Вынік",
+ "rewrite_add": "Дадаць перазапіс DNS",
+ "rewrite_edit": "Рэдагаваць перазапіс DNS",
+ "rewrite_hosts_applied": "Перапісана правілам файла hosts",
+ "rewrite_settings_updated": "Налады перазапісу DNS паспяхова абноўлены",
+ "rewrite_updated": "Перазапіс DNS паспяхова абноўлены",
+ "rewritten": "Перапісаныя",
+ "rules_label": "Правілы",
+ "safe_browsing": "Бяспечны прагляд",
+ "safe_search": "Бяспечны пошук",
+ "saturday": "Субота",
+ "save": "Захаваць",
+ "save_btn": "Захаваць",
+ "search_placeholder": "Пошук",
+ "server_config_blocking_mode": "Рэжым блакіроўкі",
+ "server_config_blocking_mode_ipv4": "Блакіроўка IPv4",
+ "server_config_blocking_mode_ipv6": "Блакіроўка IPv6",
+ "server_config_custom_ip": "Карыстальніцкі IP-адрас",
+ "server_config_default": "Прадвызначана",
+ "server_config_dnssec_enable": "Уключыць DNSSEC",
+ "server_config_edns_enable": "Уключыць кліенцкую падсетку EDNS Client Subnet",
+ "server_config_nxdomain": "NXDOMAIN",
+ "server_config_rate_limit_placeholder": "Увядзіце абмежаванне хуткасці",
+ "server_config_rate_limit_whitelist_faq": "IP-адрасы выключаны з абмежавання хуткасці",
+ "server_config_refused": "REFUSED",
+ "server_config_subnet_len_ipv4": "Даўжыня прэфікса падсеткі для адрасоў IPv4",
+ "server_config_subnet_len_ipv4_faq": "Даўжыня прэфікса падсеткі для адрасоў IPv4, якія выкарыстоўваецца для абмежавання хуткасці. Прадвызначанае значэнне - 24",
+ "server_config_subnet_len_ipv6": "Даўжыня прэфікса падсеткі для адрасоў IPv6",
+ "server_config_subnet_len_ipv6_faq": "Даўжыня прэфікса падсеткі для адрасоў IPv6, якія выкарыстоўваецца для абмежавання хуткасці. Прадвызначанае значэнне - 56",
+ "set_static_ip": "Задаць статычны IP-адрас",
+ "settings": "Налады",
+ "settings_browsing_security": "Бяспека прагляду",
+ "settings_confirm_clear_statistics": "Ачысціць статыстыку?",
+ "settings_custom": "Карыстальніцкая",
+ "settings_global": "Глабальныя",
+ "settings_notify_changes_saved": "Змены захаваны",
+ "settings_notify_statistics_cleared": "Статыстыка ачышчана",
+ "settings_parental_control": "Бацькоўскі кантроль",
+ "settings_rule_syntax": "Сінтаксіс правіла",
+ "settings_safe_search": "Бяспечны пошук",
+ "settings_statistics": "Статыстыка",
+ "settings_statistics_clear": "Ачысціць статыстыку",
+ "settings_statistics_retention": "Захаванне статыстыкі",
+ "settings_yes_clear": "Так, скінуць",
+ "setup_devices_router_title": "Маршрутызатар",
+ "setup_dns_privacy_ioc_mac": "Канфігурацыя для iOS і macOS",
+ "setup_dns_privacy_other_title": "Іншыя рэалізацыі",
+ "show_blocked_responses": "Заблакіравана",
+ "show_processed_responses": "Апрацавана",
+ "source_label": "Крыніца",
+ "source_tooltip": "Крыніца",
+ "stats_adult": "Заблакаваныя вэб-сайты для дарослых",
+ "stats_query_domain": "Дамены, якія часта запытваюцца",
+ "status_table_header": "Статус",
+ "sunday": "Нядзеля",
+ "system_host_files": "Сістэмныя файлы hosts",
+ "table_client": "Кліент",
+ "tags_title": "Тэгі",
+ "test_upstreams": "Тэст сервераў upstream",
+ "threats_blocked": "Заблакіравана пагроз",
+ "thursday": "Чацвер",
+ "time_table_header": "Час",
+ "top_blocked_domains": "Дамены, якія часта блакіруюцца",
+ "top_clients": "Самыя актыўныя кліенты",
+ "top_upstreams": "Самыя частыя серверы upstream",
+ "total": "Усяго",
+ "try_again": "Паспрабаваць яшчэ раз",
+ "tuesday": "Аўторак",
+ "unavailable_dhcp": "DHCP недаступны",
+ "unavailable_dhcp_desc": "AdGuard Home не можа запусціць сервер DHCP на вашай АС",
+ "unblock": "Разблакіраваць",
+ "update_button": "Абнавіць",
+ "update_filters_disable": "Адключыць",
+ "update_filters_interval_custom": "Карыстальніцкая",
+ "update_filters_interval_daily": "Кожны дзень",
+ "update_filters_interval_hourly": "Кожную гадзіну",
+ "update_filters_title": "Абнаўляць фільтры аўтаматычна",
+ "updates_checked": "Даступна новая версія AdGuard Home",
+ "updates_version_equal": "У вас актуальная версія AdGuard Home",
+ "upstream": "Сервер Upstream",
+ "upstream_dns": "Серверы upstream DNS",
+ "upstream_dns_addresses": "Адрасы сервераў, па адным у радку",
+ "upstream_dns_fastest_addr": "Найхутчэйшы IP-адрас",
+ "upstream_dns_parallel_requests": "Паралельныя запыты",
+ "upstream_dns_servers_title": "Серверы upstream DNS",
+ "upstream_examples_title": "Прыклады",
+ "upstream_private_ptr_resolvers_title": "Выкарыстоўваць прыватныя адваротныя рэзолверы DNS",
+ "upstreams": "Upstreams",
+ "url_label": "URL-адрас",
+ "user_rules": "Правілы карыстальніка",
+ "user_rules_add_to_allowlist": "Дадаць у спіс дазволеных",
+ "user_rules_check_button": "Праверыць",
+ "user_rules_check_client_placeholder": "Увядзіце ідэнтыфікатар кліента",
+ "user_rules_check_hostname_label": "Назва вузла або даменнае імя",
+ "user_rules_disable_filter": "Адключыць фільтр",
+ "user_rules_disable_parental_control": "Адключыць бацькоўскі кантроль",
+ "user_rules_dns_record_type_placeholder": "Выберыце тып запісу DNS",
+ "user_rules_edit_dns_rewrite": "Рэдагаваць перазапіс DNS",
+ "user_rules_rewrite_rule_is_applied": "Ужыта правіла перазапісу",
+ "username_label": "Імя карыстальніка",
+ "username_placeholder": "Увядзіце імя карыстальніка",
+ "validated_with_dnssec": "Пацверджана з дапамогай DNSSEC",
+ "wednesday": "Серада",
+ "whois": "WHOIS",
+ "yes": "Так",
+ "yes_disable": "Так, адключыць",
+ "yes_reset": "Так, скінуць"
+}
diff --git a/client_v2/src/__locales/bg.json b/client_v2/src/__locales/bg.json
new file mode 100644
index 00000000000..af0341b3928
--- /dev/null
+++ b/client_v2/src/__locales/bg.json
@@ -0,0 +1,378 @@
+{
+ "access_settings_title": "Настройки за достъп",
+ "actions_table_header": "Действия",
+ "add": "ДОБАВИ",
+ "add_allowlist": "Добавяне на списък с разрешения",
+ "add_blocklist": "Добавяне на списък за блокиране",
+ "add_persistent_client": "Добавяне като постоянен клиент",
+ "add_to_allowlist": "Добавяне към списъка с разрешени",
+ "ads_blocked": "Блокирани реклами",
+ "ads_blocked_card": "Блокирани реклами",
+ "adult_websites_blocked_tooltip": "Сума на блокирани сайтове за възрастни",
+ "all_lists_up_to_date_toast": "Всички списъци вече са актуални",
+ "allowed": "В белия списък",
+ "allowlist_add": "Добавяне на списък с разрешения",
+ "allowlist_edit": "Редактиране на списък с разрешения",
+ "apply": "Приложи",
+ "autofix_warning_result": "В резултат на това всички DNS заявки от вашата система ще бъдат обработвани от AdGuard Home по подразбиране.",
+ "autofix_warning_text": "Ако щракнете на \"Поправи\", AdGuard Home ще конфигурира вашата система да използва DNS сървър на AdGuard Home.",
+ "average_time_processing": "Средно време за обработка",
+ "average_upstream_response_time": "Средно време за отговор на upstream",
+ "back": "Back",
+ "block": "Блокирай",
+ "block_for_this_client_only": "Блокирайте само за този клиент",
+ "blocked": "Блокирано",
+ "blocked_by_cname_or_ip": "Блокирано от CNAME или IP",
+ "blocked_service": "Блокирана услуга",
+ "blocked_services": "Блокирани услуги",
+ "blocked_threats": "Блокирани заплахи",
+ "blocked_threats_chart": "Блокирани заплахи",
+ "blocklist_add_manual": "Добавете ръчно",
+ "blocklist_edit": "Редактиране на списък за блокиране",
+ "blocklist_homepage": "Начална страница",
+ "blocklist_url_file_path": "URL или път до файл",
+ "bootstrap_dns": "Bootstrap DNS сървъри",
+ "browse": "Претърси",
+ "cache_cleared": "Кешът на DNS успешно изчистен",
+ "cache_config_clear": "Изчисти кеша",
+ "cache_config_optimistic": "Оптимистично кеширане",
+ "cache_config_ttl_validation": "Минималната стойност на TTL за кеша трябва да бъде по-малка или равна на максималната",
+ "cache_enabled": "Активиране на кеша",
+ "cancel": "Отказ",
+ "cancel_btn": "Отказ",
+ "category_tooltip": "Категория",
+ "changes_saved_success": "Промени запазени",
+ "check_dhcp_servers": "Проверка за активен DHCP сървър",
+ "check_not_found": "Не е намерено в списъците ви с филтри",
+ "check_updates_btn": "Провери за актуализация",
+ "clear_btn": "Изчисти",
+ "client_add": "Добавяне на клиент",
+ "client_details": "Детайли на клиента",
+ "client_id": "ClientID",
+ "client_id_placeholder": "Въведете ClientID",
+ "client_identifier": "Идентификатор",
+ "client_settings": "Kлиентски настройки",
+ "client_table_header": "Клиент",
+ "clients_add_identifier": "Добавете идентификатор",
+ "clients_add_placeholder": "Въведете името на клиента",
+ "clients_dns_cache_size": "Размер на кеша на DNS, в байтове",
+ "clients_not_found": "Нямa намерени адреси",
+ "clients_protection": "Защита",
+ "clients_tags": "Тагове",
+ "clients_use_global_settings": "Използвайте глобалните настройки",
+ "close": "Затвори",
+ "confirm": "Потвърждаване",
+ "copy": "Копирай",
+ "copy_to_clipboard": "Копиране в клипборда",
+ "custom_filtering_rules": "Местни правила за филтриране",
+ "dark_theme": "Тъмна тема",
+ "dashboard": "Табло",
+ "delete_btn": "Изтрий",
+ "delete_table_action": "Изтрий",
+ "dhcp_add_static_lease": "Добавяне на статична наемна",
+ "dhcp_config_saved": "Запиши конфигурацията на DHCP сървъра",
+ "dhcp_edit_static_lease": "Редактиране на статичен наем",
+ "dhcp_form_gateway_input": "IP шлюз",
+ "dhcp_form_subnet_input": "Мрежова маска",
+ "dhcp_leases_not_found": "Няма намерени активни DHCP адреси",
+ "dhcp_leases_title": "DHCP раздадени адреси",
+ "dhcp_new_static_lease": "Нова статична наемна",
+ "dhcp_reset_leases": "Нулирайте всички наеми",
+ "dhcp_reset_leases_success": "Наемите на DHCP бяха успешно нулирани",
+ "dhcp_settings": "Настройки на DHCP",
+ "dhcp_static_leases_not_found": "Няма намерени статични наеми DHCP",
+ "dhcp_table_hostname": "Име на устройство",
+ "dhcp_table_ip_address": "Потребителски IP адрес",
+ "disable": "Изключи",
+ "disable_protection_btn": "Изключи защитата",
+ "disabled_dhcp": "DHCP е забранен",
+ "disallow_this_client": "Забранете на този клиент",
+ "dns_access_settings_title": "Настройки за достъп",
+ "dns_allowed_clients": "Разрешени клиенти",
+ "dns_blocking_mode": "Режим на блокиране",
+ "dns_blocking_mode_custom_ip": "Персонализиран IP адрес",
+ "dns_blocking_mode_default": "По подразбиране",
+ "dns_blocking_mode_ipv4_label": "Блокиране на IPv4",
+ "dns_blocking_mode_ipv6_label": "Блокиране на IPv6",
+ "dns_blocking_mode_nxdomain": "NXDOMAIN",
+ "dns_blocking_mode_refused": "ОТКАЗАНО",
+ "dns_blocking_mode_title": "Режим на блокиране",
+ "dns_bootstrap_dns_placeholder": "IP адреси",
+ "dns_bootstrap_dns_title": "Bootstrap DNS сървъри",
+ "dns_bootstrap_servers": "Bootstrap DNS сървъри",
+ "dns_cache_config": "Конфигурация на DNS кеш",
+ "dns_cache_size": "Размер на кеша",
+ "dns_cache_size_label": "Размер на кеша на DNS, в байтове",
+ "dns_cache_size_title": "Размер на кеша",
+ "dns_clear_cache": "Изчисти кеша",
+ "dns_clear_cache_cancel": "Отказ",
+ "dns_clear_cache_confirm": "Да, изчисти",
+ "dns_clear_cache_yes": "Да, изчисти",
+ "dns_config": "Конфигурация на DNS сървъра",
+ "dns_disallowed_clients": "Забранени клиенти",
+ "dns_disallowed_clients_title": "Забранени клиенти",
+ "dns_disallowed_domains": "Недопустими домейни",
+ "dns_disallowed_domains_title": "Недопустими домейни",
+ "dns_fallback_dns_title": "Резервни DNS сървъри",
+ "dns_fallback_servers": "Резервни DNS сървъри",
+ "dns_optimistic_caching": "Оптимистично кеширане",
+ "dns_over_https": "DNS-пред-HTTPS",
+ "dns_over_quic": "DNS-over-QUIC",
+ "dns_override_max_ttl": "Презаписване на максимален TTL",
+ "dns_override_max_ttl_title": "Презаписване на максимален TTL",
+ "dns_override_min_ttl": "Преобръщане на минималния TTL",
+ "dns_override_min_ttl_title": "Преобръщане на минималния TTL",
+ "dns_privacy": "Доверителност на DNS",
+ "dns_private_reverse_servers_title": "Частни обратни DNS сървъри",
+ "dns_rate_limit": "Ограничение на скоростта",
+ "dns_rate_limit_allowlist_placeholder": "IP адреси",
+ "dns_rate_limit_placeholder": "Въведете ограничение на скоростта",
+ "dns_rate_limit_title": "Ограничение на скоростта",
+ "dns_rewrites": "DNS презаписи",
+ "dns_server_addresses": "Адреси на сървъри",
+ "dns_server_addresses_label": "Адреси на сървърите, по един ред",
+ "dns_server_addresses_title": "Адреси на сървъри",
+ "dns_server_config_title": "Конфигурация на DNS сървъра",
+ "dns_settings": "DNS",
+ "dns_test_upstreams": "Тествай главния DNS",
+ "dns_upstream_servers_title": "Главен DNS сървър",
+ "dns_upstream_timeout": "Време за изчакване на сървър нагоре по веригата",
+ "dns_upstream_timeout_title": "Време за изчакване на сървър нагоре по веригата",
+ "dns_upstream_validation_invalid": "Невалиден формат",
+ "domain": "Домейн",
+ "domain_or_client": "Домейн или клиент",
+ "download_mobileconfig": "Изтеглете конфигурационния файл",
+ "edit_table_action": "Редактирай",
+ "enable": "Включи",
+ "enabled_dhcp": "DHCP е разрешен",
+ "encryption_certificate_path": "Път на удостоверението",
+ "encryption_certificates": "Сертификати",
+ "encryption_certificates_input": "Копирай/постави вашия PEM-кодиран сертификат тук.",
+ "encryption_certificates_source_content": "Поставете съдържанието на сертификатите",
+ "encryption_certificates_source_path": "Задайте път за файл на сертификатите",
+ "encryption_chain_invalid": "Йерархията от сертификати е невалидна",
+ "encryption_chain_valid": "Йерархията от сертификати е валидна",
+ "encryption_config_saved": "Конфигурацията е успешно записана",
+ "encryption_doq": "Порт на DNS-over-QUIC",
+ "encryption_dot": "DNS-върху-TLS порт",
+ "encryption_https": "HTTPS порт",
+ "encryption_key": "Частен ключ",
+ "encryption_key_input": "Копирай/постави вашия PEM-кодиран чpастен ключ за вашия сертификат тук.",
+ "encryption_key_source_content": "Поставете съдържанието на ключа",
+ "encryption_plain_dns": "Обикновен DNS",
+ "encryption_plain_dns_error": "За да деактивирате обикновения DNS, активирайте поне един криптиран DNS протокол",
+ "encryption_private_key_path": "Път на частния ключ",
+ "encryption_server": "Име на сървъра",
+ "encryption_server_enter": "Въведете име на домейна",
+ "encryption_title": "Криптиране",
+ "enter_cache_size": "Въведете размер на кеша (байтове)",
+ "error": "Грешка",
+ "fallback_dns": "Резервни DNS сървъри",
+ "filter_category_general": "General",
+ "filter_category_general_desc": "Списъци, които блокират проследяването и рекламата на повечето устройства",
+ "filter_category_other": "Друго",
+ "filter_category_other_desc": "Други списъци с блокиране",
+ "filter_category_regional": "Регионални",
+ "filter_category_regional_desc": "Списъци, които се фокусират върху регионалните реклами и сървърите за проследяване",
+ "filter_category_security": "Сигурност",
+ "filter_category_security_desc": "Списъци, предназначени специално за блокиране на злонамерени, фишинг и измамни домейни",
+ "filters": "Филтри",
+ "forgot_password": "Забравили сте паролата?",
+ "form_enter_blocked_response_ttl": "Въведете TTL за блокиран отговор (секунди)",
+ "form_error_client_id_format": "ClientID трябва да съдържа само цифри, малки букви и тирета",
+ "form_error_domain_format": "Невалиден формат на домейн",
+ "form_error_equal": "Не трябва да съвпада",
+ "form_error_format": "Невалиден формат",
+ "form_error_gateway_ip": "Наемът не може да има IP адреса на шлюза",
+ "form_error_ip4_format": "Невалиден IPv4 адрес",
+ "form_error_ip6_format": "Невалиден IPv6 адрес",
+ "form_error_ip_format": "Невалиден IP адрес",
+ "form_error_mac_format": "Невалиден MAC адрес",
+ "form_error_password": "Паролата не съвпада",
+ "form_error_port": "Моля въведете валиден порт",
+ "form_error_port_range": "Въведете порт в диапазона 80-65535",
+ "form_error_port_unsafe": "Не е безопасно да използвате този порт",
+ "form_error_server_name": "Невалидно име на сървъра",
+ "form_error_url_format": "Въведете валиден URL адрес или път до файл",
+ "friday": "Петък",
+ "gateway_or_subnet_invalid": "Невалидна маска на подмрежа",
+ "general_statistics": "Обща статисика",
+ "ignore_domains_title": "Игнорирани домейни",
+ "inactivity_schedule_add": "Добавяне на график",
+ "inactivity_schedule_all_day": "Цял ден",
+ "inactivity_schedule_edit": "Редактиране на график",
+ "install_auth_confirm": "Потвърдете паролата",
+ "install_auth_password": "Парола",
+ "install_auth_password_enter": "Въведете парола",
+ "install_auth_username": "Потребител",
+ "install_auth_username_enter": "Въведете потребител",
+ "install_saved": "Успешно записано",
+ "install_settings_all_interfaces": "Всички интерфейси",
+ "install_settings_port": "Порт",
+ "install_static_error": "AdGuard Home не може автоматично да го конфигурира за този мрежов интерфейс. Моля, потърсете инструкция как да го направите ръчно.",
+ "install_static_ok": "Добри новини! Статичният IP адрес вече е конфигуриран",
+ "install_step": "Стъпка",
+ "install_submit_title": "Поздравления!",
+ "ip_address": "Потребителски IP адрес",
+ "known_tracker": "Известен проследяващ",
+ "language": "Език",
+ "light_theme": "Светла тема",
+ "login": "Вписване",
+ "logout": "Изход",
+ "make_static": "Направи статичен",
+ "milliseconds_abbreviation": "мс",
+ "monday": "Понеделник",
+ "name": "Име",
+ "name_label": "Име",
+ "name_table_header": "Име",
+ "name_tooltip": "Име",
+ "next": "Следващ",
+ "no": "Не",
+ "nothing_found": "Нищо не е намерено",
+ "notify_undo": "Отмени",
+ "notify_updated": "Обновено",
+ "parental_control": "Родителски контрол",
+ "parental_group_cdn": "CDN",
+ "parental_group_development": "Разработка на софтуер",
+ "parental_group_gaming": "Гейминг",
+ "parental_group_hosting": "Уеб хостинг",
+ "parental_group_privacy": "Инструменти за поверителност",
+ "parental_group_software": "Разработка на софтуер",
+ "password_label": "Парола",
+ "password_placeholder": "Въведете парола",
+ "plain_dns": "Обикновен DNS",
+ "privacy_policy": "Политика за поверителност",
+ "protection": "Защита",
+ "protocol": "Протокол",
+ "query_log_allowed": "В белия списък",
+ "query_log_blocked": "Блокирано",
+ "query_log_blocked_services": "Блокирани услуги",
+ "query_log_blocked_threats": "Блокирани заплахи",
+ "query_log_custom_filtering_rules": "Местни правила за филтриране",
+ "query_log_nothing_available": "Нищо не е намерено",
+ "query_log_processed": "Обработен",
+ "query_log_rewritten": "Преработено",
+ "query_log_safe_search": "Безопасно търсене",
+ "query_log_strict_search": "Използвайте двойни кавички за стриктно търсене",
+ "rate_limit_subnet_len_ipv4_error": "Дължината на префикса на подмрежата IPv4 трябва да бъде между 0 и 32",
+ "rate_limit_subnet_len_ipv6_error": "Дължината на префикса на подсетевите адреси за IPv6 трябва да бъде между 0 и 128",
+ "refresh_btn": "Обнови",
+ "refresh_statics": "Обнови статистиката",
+ "release_notes": "Бележки по изданието",
+ "remove": "Премахни",
+ "request_table_header": "Заявка",
+ "requests_table_header": "Заявки",
+ "reset": "Нулиране",
+ "reset_settings": "Изтрий всички настройки",
+ "reset_settings_confirm": "Да, нулирай",
+ "response_details": "Подробности за отговора",
+ "response_time": "Време за отговор",
+ "result": "Резултат",
+ "rewrite_add": "Добави DNS презапис",
+ "rewrite_edit": "Редактиране на ДНС перезаписване",
+ "rewrite_hosts_applied": "Преписано по правило на файла hosts",
+ "rewrite_settings_updated": "Успешно актуализиране на настройките за презапис на DNS",
+ "rewrite_updated": "ДНС перезаписването беше успешно актуализирано",
+ "rewritten": "Преработено",
+ "rules_label": "Правила",
+ "safe_browsing": "Безопасно сърфиране",
+ "safe_search": "Безопасно търсене",
+ "saturday": "Събота",
+ "save": "Запази",
+ "save_btn": "Запази",
+ "search_placeholder": "Търси",
+ "server_config_blocking_mode": "Режим на блокиране",
+ "server_config_blocking_mode_ipv4": "Блокиране на IPv4",
+ "server_config_blocking_mode_ipv6": "Блокиране на IPv6",
+ "server_config_custom_ip": "Персонализиран IP адрес",
+ "server_config_default": "По подразбиране",
+ "server_config_disable_ipv6": "Изключете IPv6 протокола",
+ "server_config_dnssec_enable": "Включете DNSSEC",
+ "server_config_edns_enable": "Активиране на подмрежата на клиенти EDNS",
+ "server_config_nxdomain": "NXDOMAIN",
+ "server_config_rate_limit_placeholder": "Въведете ограничение на скоростта",
+ "server_config_rate_limit_whitelist_faq": "IP адреси, изключени от ограничаването на скоростта",
+ "server_config_refused": "ОТКАЗАНО",
+ "server_config_subnet_len_ipv4": "Дължина на префикса на подмрежата за IPv4 адреси",
+ "server_config_subnet_len_ipv4_faq": "Дължина на префикса на подмрежата за IPv4 адреси, използвани за ограничаване на скоростта. Стандартно е 24",
+ "server_config_subnet_len_ipv6": "Дължина на префикса на подмрежата за IPv6 адреси",
+ "server_config_subnet_len_ipv6_faq": "Дължина на префикса на подсетеви адреси за IPv6, използвани за ограничаване на скоростта. Стандартната стойност е 56",
+ "set_static_ip": "Задайте статичен IP адрес",
+ "settings": "Настройки",
+ "settings_browsing_security": "Safebrowsing",
+ "settings_confirm_clear_statistics": "Изчистване на статистиката?",
+ "settings_custom": "Персонализиране",
+ "settings_general_short": "General",
+ "settings_global": "Глобални",
+ "settings_notify_changes_saved": "Промени запазени",
+ "settings_notify_statistics_cleared": "Статистиките са изчистени",
+ "settings_parental_control": "Родителски контрол",
+ "settings_rule_syntax": "Синтаксис на правилото",
+ "settings_safe_search": "Безопасно търсене",
+ "settings_statistics": "Статистика",
+ "settings_statistics_clear": "Нулирай статистиката",
+ "settings_statistics_retention": "Запазване на статистиката",
+ "settings_yes_clear": "Да, изчисти",
+ "setup_devices_router_title": "Рутер",
+ "setup_dns_privacy_ioc_mac": "Конфигурация за iOS и macOS",
+ "setup_dns_privacy_other_title": "Други реализации",
+ "show_blocked_responses": "Блокирано",
+ "show_processed_responses": "Обработен",
+ "source_label": "Източник",
+ "source_tooltip": "Източник",
+ "stats_adult": "сайтове за възрастни",
+ "stats_query_domain": "Най-отваряни страници",
+ "status_table_header": "Състояние",
+ "sunday": "Неделя",
+ "system_host_files": "Файлове на системен хост",
+ "table_client": "Клиент",
+ "tags_title": "Тагове",
+ "test_upstreams": "Тествай главния DNS",
+ "thursday": "Четвъртък",
+ "time_table_header": "Време",
+ "top_blocked_domains": "Най-блокирани страници",
+ "top_clients": "Най-активни IP адреси",
+ "top_upstreams": "Най-добри upstream сървъри",
+ "total": "Общо",
+ "try_again": "Опитай пак",
+ "tuesday": "Вторник",
+ "unavailable_dhcp": "DHCP не е наличен",
+ "unavailable_dhcp_desc": "AdGuard Home не може да стартира DHCP сървър във вашата операционна система",
+ "unblock": "Деблокирай",
+ "update_button": "Обнови",
+ "update_filters_disable": "Изключи",
+ "update_filters_interval_custom": "Персонализиране",
+ "update_filters_interval_daily": "Всеки ден",
+ "update_filters_interval_hourly": "На всеки час",
+ "update_filters_title": "Автоматично актуализиране на филтрите",
+ "updates_checked": "Достъпна е нова версия на AdGuard Home",
+ "updates_version_equal": "AdGuard Home е актуален.",
+ "upstream": "upstream",
+ "upstream_dns": "Главен DNS сървър",
+ "upstream_dns_addresses": "Адреси на сървърите, по един ред",
+ "upstream_dns_fastest_addr": "Най-бърз IP адрес",
+ "upstream_dns_parallel_requests": "Паралелни заявки",
+ "upstream_dns_servers_title": "Главен DNS сървър",
+ "upstream_examples_title": "Примери",
+ "upstream_private_ptr_resolvers_title": "Използвайте частни обратни DNS резолвери",
+ "upstreams": "Нагоре по веригата",
+ "url_label": "URL",
+ "user_rules": "Потребителски филтър",
+ "user_rules_add_to_allowlist": "Добавяне към списъка с разрешени",
+ "user_rules_check_button": "Провери",
+ "user_rules_check_client_placeholder": "Въведете идентификатор на клиента",
+ "user_rules_check_hostname_label": "Име на хост или домейн",
+ "user_rules_disable_filter": "Изключи филтъра",
+ "user_rules_disable_parental_control": "Деактивира родителския контрол",
+ "user_rules_dns_record_type_placeholder": "Изберете тип DNS запис",
+ "user_rules_edit_dns_rewrite": "Редактиране на ДНС перезаписване",
+ "user_rules_rewrite_rule_is_applied": "Правилото за презапис е приложено",
+ "username_label": "Потребител",
+ "username_placeholder": "Въведете потребител",
+ "validated_with_dnssec": "Проверено с DNSSEC",
+ "wednesday": "Сряда",
+ "whois": "WHOIS",
+ "yes": "Да",
+ "yes_disable": "Да, деактивирай",
+ "yes_reset": "Да, нулирай"
+}
diff --git a/client_v2/src/__locales/cs.json b/client_v2/src/__locales/cs.json
new file mode 100644
index 00000000000..acf398990e9
--- /dev/null
+++ b/client_v2/src/__locales/cs.json
@@ -0,0 +1,405 @@
+{
+ "access_settings_saved_toast": "Nastavení přístupu uloženo",
+ "access_settings_title": "Nastavení přístupu",
+ "actions_table_header": "Akce",
+ "add": "Přidat",
+ "add_allowlist": "Přidat seznam povolených",
+ "add_blocklist": "Přidat seznam blokovaných",
+ "add_persistent_client": "Přidat jako trvalého klienta",
+ "add_to_allowlist": "Přidat na seznam povolených",
+ "ads_blocked": "Zablokovaných reklam",
+ "ads_blocked_card": "Zablokovaných reklam",
+ "adult_websites_blocked_tooltip": "Počet zablokovaných stránek pro dospělé",
+ "all_lists_up_to_date_toast": "Všechny seznamy jsou již aktuální",
+ "allowed": "Na seznamu povolených",
+ "allowlist_add": "Přidat seznam povolených",
+ "allowlist_edit": "Upravit seznam povolených",
+ "apply": "Použít",
+ "autofix_warning_result": "Výsledkem je, že všechny požadavky DNS z vašeho systému jsou ve výchozím nastavení zpracovány službou AdGuard Home.",
+ "autofix_warning_text": "Pokud kliknete na „Opravit“, AdGuard Home nakonfiguruje váš systém tak, aby používal DNS server AdGuard Home.",
+ "average_time_processing": "Průměrný čas zpracování",
+ "average_upstream_response_time": "Průměrná doba odezvy odchozích připojení",
+ "back": "Zpět",
+ "block": "Blokovat",
+ "block_for_this_client_only": "Blokovat pouze pro tohoto klienta",
+ "blocked": "Zablokováno",
+ "blocked_by_cname_or_ip": "Zakázáno dle CNAME nebo IP",
+ "blocked_service": "Blokovaná služba",
+ "blocked_services": "Blokované služby",
+ "blocked_threats": "Zablokované hrozby",
+ "blocked_threats_chart": "Zablokované hrozby",
+ "blocklist_add_manual": "Přidat ručně",
+ "blocklist_edit": "Upravit seznam blokovaných",
+ "blocklist_homepage": "Domovská stránka",
+ "blocklist_url_file_path": "URL nebo cesta k souboru",
+ "bootstrap_dns": "Bootstrap DNS servery",
+ "browse": "Procházet",
+ "cache_cleared": "Mezipaměť DNS úspěšně vymazána",
+ "cache_config_clear": "Vymazat mezipaměť",
+ "cache_config_optimistic": "Optimistické ukládání do mezipaměti",
+ "cache_config_ttl_validation": "Minimální přepis TTL mezipaměti musí být menší nebo roven maximální hodnotě",
+ "cache_enabled": "Povolit mezipaměť",
+ "cancel": "Zrušit",
+ "cancel_btn": "Zrušit",
+ "category_tooltip": "Kategorie",
+ "changes_saved_success": "Změny uloženy",
+ "check_dhcp_servers": "Zkontrolovat DHCP servery",
+ "check_not_found": "Nenalezeno ve Vašich seznamech filtrů",
+ "check_updates_btn": "Zkontrolovat aktualizace",
+ "clear_btn": "Vymazat",
+ "client_add": "Přidat klienta",
+ "client_added": "Klient přidán",
+ "client_details": "Detaily klienta",
+ "client_id": "ID klienta",
+ "client_id_placeholder": "Zadejte ID klienta",
+ "client_identifier": "Identifikátor",
+ "client_removed": "Klient odebrán",
+ "client_settings": "Nastavení klienta",
+ "client_table_header": "Klient",
+ "clients_add_identifier": "Přidat identifikátor",
+ "clients_add_placeholder": "Zadejte název klienta",
+ "clients_desc": "Pravidla pro zařízení připojených k AdGuard Home",
+ "clients_dns_cache_size": "Velikost mezipaměti DNS v bajtech",
+ "clients_not_found": "Nenalezeny žádní klienti",
+ "clients_protection": "Ochrana",
+ "clients_tags": "Značky",
+ "clients_use_global_settings": "Použít globální nastavení",
+ "close": "Zavřít",
+ "confirm": "Potvrdit",
+ "copied": "Zkopírováno do schránky",
+ "copy": "Kopírovat",
+ "copy_to_clipboard": "Kopírovat do schránky",
+ "custom_filtering_rules": "Vlastní pravidla filtrování",
+ "dark_theme": "Tmavý motiv",
+ "dashboard": "Hlavní panel",
+ "delete_btn": "Vymazat",
+ "delete_table_action": "Vymazat",
+ "dhcp_add_static_lease": "Přidat statický pronájem",
+ "dhcp_config_saved": "Konfigurace DHCP byla úspěšně uložena",
+ "dhcp_edit_static_lease": "Upravit statický pronájem",
+ "dhcp_form_gateway_input": "IP brána",
+ "dhcp_form_subnet_input": "Maska podsítě",
+ "dhcp_leases_not_found": "Nebyly nalezeny žádné pronájmy DHCP",
+ "dhcp_leases_title": "Pronájem DHCP",
+ "dhcp_new_static_lease": "Nový statický pronájem",
+ "dhcp_reset_leases": "Resetovat všechny pronájmy",
+ "dhcp_reset_leases_success": "Pronájmy DHCP byli úspěšně resetovány",
+ "dhcp_settings": "Nastavení DHCP",
+ "dhcp_static_leases_not_found": "Nebyly nalezeny žádné statické pronájmy DHCP",
+ "dhcp_table_hostname": "Název hostitele",
+ "dhcp_table_ip_address": "IP adresa",
+ "disable": "Zakázat",
+ "disable_protection_btn": "Vypnout ochranu",
+ "disabled_dhcp": "DHCP server vypnutý",
+ "disallow_this_client": "Blokovat tohoto klienta",
+ "dns_access_settings_title": "Nastavení přístupu",
+ "dns_allowed_clients": "Povolení klienti",
+ "dns_blocking_mode": "Režim blokování",
+ "dns_blocking_mode_custom_ip": "Vlastní IP adresa",
+ "dns_blocking_mode_default": "Výchozí",
+ "dns_blocking_mode_ipv4_label": "Blokování IPv4",
+ "dns_blocking_mode_ipv6_label": "Blokování IPv6",
+ "dns_blocking_mode_nxdomain": "NXDOMAIN",
+ "dns_blocking_mode_refused": "REFUSED",
+ "dns_blocking_mode_title": "Režim blokování",
+ "dns_bootstrap_dns_placeholder": "IP adresa",
+ "dns_bootstrap_dns_title": "Bootstrap DNS servery",
+ "dns_bootstrap_servers": "Bootstrap DNS servery",
+ "dns_cache_config": "Konfigurace mezipaměti DNS",
+ "dns_cache_configuration_saved_toast": "Konfigurace mezipaměti DNS uložena",
+ "dns_cache_size": "Velikost mezipaměti",
+ "dns_cache_size_label": "Velikost mezipaměti DNS v bajtech",
+ "dns_cache_size_title": "Velikost mezipaměti",
+ "dns_clear_cache": "Vymazat mezipaměť",
+ "dns_clear_cache_cancel": "Zrušit",
+ "dns_clear_cache_confirm": "Ano, smazat",
+ "dns_clear_cache_yes": "Ano, smazat",
+ "dns_config": "Konfigurace DNS serveru",
+ "dns_disallowed_clients": "Blokovaní klienti",
+ "dns_disallowed_clients_title": "Blokovaní klienti",
+ "dns_disallowed_domains": "Blokované domény",
+ "dns_disallowed_domains_title": "Blokované domény",
+ "dns_edns_custom_placeholder": "Zadejte IP adresu",
+ "dns_fallback_dns_title": "Záložní DNS servery",
+ "dns_fallback_servers": "Záložní DNS servery",
+ "dns_optimistic_caching": "Optimistické ukládání do mezipaměti",
+ "dns_over_https": "DNS-over-HTTPS",
+ "dns_over_quic": "DNS-over-QUIC",
+ "dns_over_tls": "DNS-over-TLS",
+ "dns_override_max_ttl": "Přepsat maximální hodnotu TTL",
+ "dns_override_max_ttl_title": "Přepsat maximální hodnotu TTL",
+ "dns_override_min_ttl": "Přepsat minimální hodnotu TTL",
+ "dns_override_min_ttl_title": "Přepsat minimální hodnotu TTL",
+ "dns_privacy": "Soukromí DNS",
+ "dns_private_reverse_servers_title": "Soukromé reverzní DNS servery",
+ "dns_queries": "Dotazy DNS",
+ "dns_query": "Dotazy DNS",
+ "dns_rate_limit": "Rychlostní limit",
+ "dns_rate_limit_allowlist_placeholder": "IP adresa",
+ "dns_rate_limit_placeholder": "Zadejte rychlostní limit",
+ "dns_rate_limit_title": "Rychlostní limit",
+ "dns_rewrites": "Přesměrování DNS",
+ "dns_server_addresses": "Adresy serveru",
+ "dns_server_addresses_label": "Adresy serveru, jedna na řádek",
+ "dns_server_addresses_title": "Adresy serveru",
+ "dns_server_config_title": "Konfigurace DNS serveru",
+ "dns_server_configuration_saved_toast": "Konfigurace DNS serveru uložena",
+ "dns_settings": "DNS",
+ "dns_test_upstreams": "Test upstreamů",
+ "dns_upstream_servers_title": "Odchozí DNS servery",
+ "dns_upstream_timeout": "Časový limit odchozího serveru",
+ "dns_upstream_timeout_title": "Časový limit odchozího serveru",
+ "dns_upstream_validation_invalid": "Neplatný formát",
+ "domain": "Doména",
+ "domain_or_client": "Doména nebo klient",
+ "download_mobileconfig": "Stáhnout konfigurační soubor",
+ "edit_table_action": "Upravit",
+ "enable": "Povolit",
+ "enabled_dhcp": "DHCP server zapnutý",
+ "encryption_certificate_path": "Cesta k certifikátu",
+ "encryption_certificates": "Certifikáty",
+ "encryption_certificates_input": "Zde můžete nakopírovat/vložit certifikáty PEM.",
+ "encryption_certificates_source_content": "Vložte obsahy certifikátů",
+ "encryption_certificates_source_path": "Nastavte cestu k souboru certifikátů",
+ "encryption_chain_invalid": "Certifikační řetězec je neplatný",
+ "encryption_chain_valid": "Certifikační řetězec je platný",
+ "encryption_config_saved": "Konfigurace šifrování byla uložena",
+ "encryption_doq": "DNS-over-QUIC port",
+ "encryption_dot": "DNS-over-TLS port",
+ "encryption_https": "HTTPS port",
+ "encryption_key": "Osobní kód",
+ "encryption_key_input": "Zde můžete nakopírovat/vložit soukromý klíč k certifikátu PEM.",
+ "encryption_key_source_content": "Vložte obsahy soukromého klíče",
+ "encryption_key_source_path": "Nastavte soubor soukromého klíče",
+ "encryption_plain_dns": "Běžný DNS",
+ "encryption_plain_dns_error": "Chcete-li zakázat běžný DNS, povolte alespoň jeden šifrovaný protokol DNS",
+ "encryption_private_key_path": "Cesta k soukromému klíčí",
+ "encryption_server": "Název serveru",
+ "encryption_server_enter": "Zadejte název domény",
+ "encryption_title": "Šifrování",
+ "enter_cache_size": "Zadejte velikost mezipaměti (v bajtech)",
+ "enter_ip_address_placeholder": "Zadejte IP adresu",
+ "error": "Chyba",
+ "fallback_dns": "Záložní DNS servery",
+ "filter_category_general": "General",
+ "filter_category_general_desc": "Seznamy, které blokují slídiče a reklamu na většině zařízení",
+ "filter_category_other": "Ostatní",
+ "filter_category_other_desc": "Další seznamy zakázaných",
+ "filter_category_regional": "Regionální",
+ "filter_category_regional_desc": "Seznamy, které jsou zaměřené na regionální reklamy a sledovací servery",
+ "filter_category_security": "Zabezpečení",
+ "filter_category_security_desc": "Seznamy určené na blokování nebezpečných, zákeřných nebo podvodných domén",
+ "filters": "Filtry",
+ "forgot_password": "Zapomněli jste heslo?",
+ "form_enter_blocked_response_ttl": "Zadejte TTL blokované odezvy (v sekundách)",
+ "form_error_answer_format": "Neplatný formát výsledku",
+ "form_error_client_id_format": "ID klienta musí obsahovat pouze čísla, malá písmena a spojovníky",
+ "form_error_domain_format": "Neplatný formát domény",
+ "form_error_equal": "Nesmí se shodovat",
+ "form_error_format": "Neplatný formát",
+ "form_error_gateway_ip": "Pronájem nemůže mít IP adresu brány",
+ "form_error_ip4_format": "Neplatná adresa IPv4",
+ "form_error_ip6_format": "Neplatná adresa IPv6",
+ "form_error_ip_format": "Neplatná IP adresa",
+ "form_error_mac_format": "Neplatná adresa MAC",
+ "form_error_password": "Heslo se neshoduje",
+ "form_error_port": "Zadejte platné číslo portu",
+ "form_error_port_range": "Zadejte číslo portu v rozmezí 80-65535",
+ "form_error_port_unsafe": "Nezabezpečený port",
+ "form_error_server_name": "Neplatný název serveru",
+ "form_error_url_format": "Zadejte platnou URL nebo cestu k souboru",
+ "friday": "Pátek",
+ "gateway_or_subnet_invalid": "Neplatná maska podsítě",
+ "general_statistics": "Obecné statistiky",
+ "ignore_domains_title": "Ignorované domény",
+ "inactivity_schedule_add": "Přidat plán",
+ "inactivity_schedule_all_day": "Všechny dny",
+ "inactivity_schedule_edit": "Upravit plán",
+ "install_auth_confirm": "Potvrdit heslo",
+ "install_auth_password": "Heslo",
+ "install_auth_password_enter": "Zadejte heslo",
+ "install_auth_username": "Uživatelské jméno",
+ "install_auth_username_enter": "Zadejte uživatelské jméno",
+ "install_saved": "Úspěšně uloženo",
+ "install_settings_all_interfaces": "Všechna rozhraní",
+ "install_settings_port": "Port",
+ "install_static_error": "AdGuard Home nemůže automaticky nakonfigurovat toto síťové rozhraní. Prosím vyhledejte návod, jak to provést ručně.",
+ "install_static_ok": "Skvělá zpráva! Statická IP adresa je již nakonfigurována",
+ "install_step": "Krok",
+ "install_submit_title": "Gratulujeme!",
+ "ip_address": "IP adresa",
+ "known_tracker": "Známý slídič",
+ "language": "Jazyk",
+ "light_theme": "Světlý motiv",
+ "login": "Přihlásit se",
+ "logout": "Odhlásit se",
+ "make_static": "Nastavit jako statickou",
+ "milliseconds_abbreviation": "ms",
+ "monday": "Pondělí",
+ "name": "Název",
+ "name_label": "Název",
+ "name_table_header": "Název",
+ "name_tooltip": "Název",
+ "next": "Další",
+ "no": "Ne",
+ "none_text": "Žádná",
+ "nothing_found": "Nic nenalezeno",
+ "notify_undo": "Zpět",
+ "notify_updated": "Aktualizováno",
+ "open_dashboard": "Otevřít hlavní panel",
+ "parental_control": "Rodičovská ochrana",
+ "parental_group_cdn": "CDN",
+ "parental_group_development": "Vývoj softwaru",
+ "parental_group_gaming": "Hraní",
+ "parental_group_hosting": "Webhosting",
+ "parental_group_privacy": "Nástroje pro ochranu soukromí",
+ "parental_group_shopping": "Nakupování",
+ "parental_group_social": "Sociální sítě",
+ "parental_group_social_network": "Sociální sítě",
+ "parental_group_software": "Vývoj softwaru",
+ "parental_group_streaming": "Streamování",
+ "password_label": "Heslo",
+ "password_placeholder": "Zadejte heslo",
+ "plain_dns": "Běžný DNS",
+ "privacy_policy": "Zásady ochrany osobních údajů",
+ "protection": "Ochrana",
+ "protocol": "Protokol",
+ "query_log_allowed": "Na seznamu povolených",
+ "query_log_blocked": "Zablokováno",
+ "query_log_blocked_services": "Blokované služby",
+ "query_log_blocked_threats": "Zablokované hrozby",
+ "query_log_custom_filtering_rules": "Vlastní pravidla filtrování",
+ "query_log_nothing_available": "Nic nenalezeno",
+ "query_log_processed": "Zpracováno",
+ "query_log_retention": "Rotace logů",
+ "query_log_rewritten": "Přepsáno",
+ "query_log_safe_search": "Bezpečné vyhledávání",
+ "query_log_strict_search": "Pro striktní vyhledávání použijte dvojité uvozovky",
+ "rate_limit_subnet_len_ipv4_error": "Délka předpony podsítě IPv4 by měla být mezi 0 a 32",
+ "rate_limit_subnet_len_ipv6_error": "Délka předpony podsítě IPv6 by měla být mezi 0 a 128",
+ "refresh_btn": "Obnovit",
+ "refresh_statics": "Obnovit statistiky",
+ "release_notes": "Poznámky k vydání",
+ "remove": "Odstranit",
+ "report_an_issue": "Nahlásit problém",
+ "request_table_header": "Požadavek",
+ "requests_table_header": "Požadavky",
+ "reset": "Resetovat",
+ "reset_settings": "Resetovat nastavení",
+ "reset_settings_confirm": "Ano, resetovat",
+ "response_details": "Detail odpovědi",
+ "response_time": "Čas odezvy",
+ "result": "Výsledek",
+ "rewrite_add": "Přidat přesměrování DNS",
+ "rewrite_edit": "Upravit přesměrování DNS",
+ "rewrite_hosts_applied": "Přepsáno pravidlem souboru hosts",
+ "rewrite_settings_updated": "Nastavení přepisování DNS bylo úspěšně aktualizováno",
+ "rewrite_updated": "Přesměrování DNS bylo úspěšně aktualizováno",
+ "rewritten": "Přepsáno",
+ "rules_label": "Pravidla",
+ "safe_browsing": "Bezpečné prohlížení",
+ "safe_search": "Bezpečné vyhledávání",
+ "saturday": "Sobota",
+ "save": "Uložit",
+ "save_btn": "Uložit",
+ "search_placeholder": "Hledat",
+ "server_config_blocking_mode": "Režim blokování",
+ "server_config_blocking_mode_ipv4": "Blokování IPv4",
+ "server_config_blocking_mode_ipv6": "Blokování IPv6",
+ "server_config_custom_ip": "Vlastní IP adresa",
+ "server_config_default": "Výchozí",
+ "server_config_disable_ipv6": "Zakázat řešení IPv6 adres",
+ "server_config_dnssec_enable": "Zapnout DNSSEC",
+ "server_config_edns_enable": "Povolit klientskou podsíť EDNS",
+ "server_config_nxdomain": "NXDOMAIN",
+ "server_config_rate_limit_placeholder": "Zadejte rychlostní limit",
+ "server_config_rate_limit_whitelist_faq": "IP adresy vyloučené z omezení rychlosti",
+ "server_config_refused": "REFUSED",
+ "server_config_subnet_len_ipv4": "Délka předpony podsítě pro adresy IPv4",
+ "server_config_subnet_len_ipv4_faq": "Délka předpony podsítě pro adresy IPv4 používané pro omezení rychlosti. Výchozí hodnota je 24",
+ "server_config_subnet_len_ipv6": "Délka předpony podsítě pro adresy IPv6",
+ "server_config_subnet_len_ipv6_faq": "Délka předpony podsítě pro adresy IPv6 používané pro omezení rychlosti. Výchozí hodnota je 56",
+ "set_static_ip": "Nastavit statickou IP adresu",
+ "settings": "Nastavení",
+ "settings_browsing_security": "Bezpečné prohlížení",
+ "settings_confirm_clear_statistics": "Vyčistit statistiky?",
+ "settings_custom": "Vlastní",
+ "settings_general_short": "General",
+ "settings_global": "Globální",
+ "settings_notify_changes_saved": "Změny uloženy",
+ "settings_notify_statistics_cleared": "Statistiky vyčištěny",
+ "settings_parental_control": "Rodičovská ochrana",
+ "settings_rule_syntax": "Syntaxe pravidel",
+ "settings_safe_search": "Bezpečné vyhledávání",
+ "settings_statistics": "Statistiky",
+ "settings_statistics_clear": "Vyčistit statistiky",
+ "settings_statistics_retention": "Uchovávání statistik",
+ "settings_yes_clear": "Ano, smazat",
+ "setup_devices_router_title": "Router",
+ "setup_dns_privacy_ioc_mac": "Konfigurace pro iOS a macOS",
+ "setup_dns_privacy_other_title": "Další implementace",
+ "show_blocked_responses": "Zablokováno",
+ "show_processed_responses": "Zpracováno",
+ "source_label": "Zdroj",
+ "source_tooltip": "Zdroj",
+ "stats_adult": "Blokované stránky pro dospělé",
+ "stats_query_domain": "Nejčastěji dotazované domény",
+ "status_table_header": "Stav",
+ "sunday": "Neděle",
+ "system_host_files": "Systémové soubory hostitelů",
+ "table_client": "Klient",
+ "tags_title": "Značky",
+ "test_upstreams": "Test upstreamů",
+ "threats_blocked": "Zablokovaných hrozeb",
+ "thursday": "Čtvrtek",
+ "time_table_header": "Čas",
+ "top_blocked_domains": "Nejčastěji blokované domény",
+ "top_clients": "Nejčastější klienti",
+ "top_upstreams": "Top odchozí připojení",
+ "total": "Celkem",
+ "try_again": "Zkusit znovu",
+ "tuesday": "Úterý",
+ "unavailable_dhcp": "DHCP není k dispozici",
+ "unavailable_dhcp_desc": "AdGuard Home nemůže na vašem operačním systému provozovat DHCP server",
+ "unblock": "Odblokovat",
+ "unknown_filter": "Neznámý filtr %filterId%",
+ "update_button": "Aktualizovat",
+ "update_filters_disable": "Zakázat",
+ "update_filters_interval_custom": "Vlastní",
+ "update_filters_interval_daily": "Každý den",
+ "update_filters_interval_hourly": "Každou hodinu",
+ "update_filters_title": "Aktualizovat filtry automaticky",
+ "updated_upstream_dns_toast": "Upstream servery uloženy",
+ "updates_checked": "Nová verze AdGuard Home je k dispozici\n",
+ "updates_version_equal": "AdGuard Home je aktuální",
+ "upstream": "Odchozí připojení",
+ "upstream_dns": "Odchozí DNS servery",
+ "upstream_dns_addresses": "Adresy serveru, jedna na řádek",
+ "upstream_dns_configured_in_file": "Konfigurováno v %path%",
+ "upstream_dns_fastest_addr": "Nejrychlejší IP adresa",
+ "upstream_dns_parallel_requests": "Paralelní požadavky",
+ "upstream_dns_servers_title": "Odchozí DNS servery",
+ "upstream_examples_title": "Příklady",
+ "upstream_private_ptr_resolvers_title": "Použít soukromé reverzní rDNS řešitele",
+ "upstreams": "Odesláno",
+ "url_label": "URL",
+ "user_rules": "Uživatelská pravidla",
+ "user_rules_add_to_allowlist": "Přidat na seznam povolených",
+ "user_rules_check_button": "Zkontrolovat",
+ "user_rules_check_client_placeholder": "Zadejte identifikátor klienta",
+ "user_rules_check_hostname_label": "Název hostitele nebo domény",
+ "user_rules_disable_filter": "Zakázat filtr",
+ "user_rules_disable_parental_control": "Vypnout Rodičovskou ochranu",
+ "user_rules_dns_record_type_placeholder": "Vyberte typ DNS záznamu",
+ "user_rules_domain_blocked": "Doména je blokována",
+ "user_rules_edit_dns_rewrite": "Upravit přesměrování DNS",
+ "user_rules_rewrite_rule_is_applied": "Aplikované pravidlo přesměrování",
+ "username_label": "Uživatelské jméno",
+ "username_placeholder": "Zadejte uživatelské jméno",
+ "validated_with_dnssec": "Ověřeno pomocí DNSSEC",
+ "wednesday": "Středa",
+ "whois": "WHOIS",
+ "yes": "Ano",
+ "yes_disable": "Ano, zakázat",
+ "yes_reset": "Ano, resetovat"
+}
diff --git a/client_v2/src/__locales/da.json b/client_v2/src/__locales/da.json
new file mode 100644
index 00000000000..9d95bb8d7c7
--- /dev/null
+++ b/client_v2/src/__locales/da.json
@@ -0,0 +1,864 @@
+{
+ "access_settings_allowed_faq": "Liste over CIDR'er, IP-adresser eller ClientID'er . Indeholder feltet poster, accepterer AdGuard Home kun forespørgsler fra disse klienter",
+ "access_settings_allowed_placeholder": "Angiv tilladte klienter",
+ "access_settings_allowed_title": "Tilladte klienter, én pr. linje",
+ "access_settings_blocked_faq_1": "AdGuard Home dropper DNS-forespørgsler matchende disse domæner. Sådanne forespørgsler vises ikke i Forespørgselslog. Der kan angives præcise domænenavne, jokertegn eller URL-filterregler, såsom eksempel.org, *.eksempel.org eller ||eksempel.org^",
+ "access_settings_blocked_faq_2": "Ikke at forveksle med filtre",
+ "access_settings_blocked_placeholder": "Angiv ikke-tilladte domæner",
+ "access_settings_blocked_title": "Ikke tilladte domæner, ét pr. linje",
+ "access_settings_disallowed_faq": "Liste over CIDR'er, IP-adresser eller KlientID'er . Indeholder feltet poster, dropper AdGuard Home forespørgsler fra disse klienter. Feltet ignoreres, hvis der er poster i Tilladte klienter ",
+ "access_settings_disallowed_placeholder": "Angiv ikke-tilladte klienter",
+ "access_settings_disallowed_title": "Ikke-tilladte klienter, én pr. linje",
+ "access_settings_saved_toast": "Adgangsindstillinger gemt",
+ "access_settings_title": "Adgangsindstillinger",
+ "actions_table_header": "Handlinger",
+ "add": "Tilføj",
+ "add_allowlist": "Tilføj hvidliste",
+ "add_blocklist": "Tilføj sortliste",
+ "add_persistent_client": "Tilføj som vedvarende klient",
+ "add_to_allowlist": "Føj til hvidliste",
+ "ads_blocked": "Blokerede annoncer",
+ "ads_blocked_card": "Blokerede annoncer",
+ "ads_blocked_tooltip": "Antallet af DNS-forespørgsler blokeret af adblockfiltre og værtsblockliste",
+ "adult_websites_blocked": "Blokerede voksenwebsteder",
+ "adult_websites_blocked_tooltip": "Antallet af blokerede voksenwebsteder",
+ "all_domains": "Alle domæner",
+ "all_lists_up_to_date_toast": "Alle lister er allerede opdaterede",
+ "all_rewrites_disabled": "Alle tilpassede DNS-svar deaktiveres",
+ "all_rewrites_enabled": "Alle tilpassede DNS-svar aktiveres",
+ "allow_service": "Tillad tjeneste",
+ "allowed": "Hvidlistet",
+ "allowlist_add": "Tilføj hvidliste",
+ "allowlist_edit": "Hvidlisteredigering",
+ "allowlist_empty": "Ingen hvidlister endnu — tilføj den første ",
+ "allowlist_placeholder_example": "Min hvidliste",
+ "allowlist_remove": "Fjern hvidliste?",
+ "allowlist_remove_desc": "%value% fjernes",
+ "allowlists": "Hvidlister",
+ "allowlists_title": "Hvidlister",
+ "apply": "Anvend",
+ "aria_clear_input": "Ryd input",
+ "autofix_warning_list": "Den vil udføre disse opgaver:
Deaktivere systemets DNSStubListener
Sætte DNS-serveradressen til 127.0.0.1
Erstatte det symbolske linkmål for /etc/resolv.conf med /run/systemd/resolve/resolv.conf
Stoppe DNSStubListener (genindlæse tjenesten systemd-resolved)
",
+ "autofix_warning_result": "Det betyder, at alle DNS-forespørgsler fra dit system som standard behandles af AdGuard Home.",
+ "autofix_warning_text": "Klikker du på \"Reparér\", opsætter AdGuard Home dit system til brug med AdGuard Home DNS-server.",
+ "average_time_processing": "Gennemsnitlig behandlingstid",
+ "average_time_processing_tooltip": "Gennemsnitlig tid i millisekunder til behandling af en DNS-forespørgsel",
+ "average_upstream_response_time": "Gennemsnitlig upstream-responstid",
+ "back": "Tilbage",
+ "block": "Blokér",
+ "block_client": "Blokér klient",
+ "block_for_this_client_only": "Blokér kun for denne klient",
+ "blocked": "Blokeret",
+ "blocked_adult_websites": "Blokeret af Forældrekontrol",
+ "blocked_by_cname_or_ip": "Blokeret af CNAME eller IP",
+ "blocked_queries": "Blokerede forespørgsler",
+ "blocked_service": "Blokeret tjeneste",
+ "blocked_services": "Blokerede tjenester",
+ "blocked_services_desc": "Vælg populære websteder og tjenester, som skal blokeres",
+ "blocked_threats": "Blokerede trusler",
+ "blocked_threats_chart": "Blokerede trusler",
+ "blocked_total": "%value% blokeret i alt",
+ "blocklist_add_from_list": "Tilføj fra liste",
+ "blocklist_add_manual": "Tilføj manuelt",
+ "blocklist_contents": "Indhold (.%value%)",
+ "blocklist_edit": "Sortlisteredigering",
+ "blocklist_homepage": "Hjemmeside",
+ "blocklist_placeholder_example": "Min sortliste",
+ "blocklist_remove": "Fjern sortliste?",
+ "blocklist_remove_desc": "%value% fjernes",
+ "blocklist_url_file_path": "URL eller filsti",
+ "blocklists_add": "Tilføj sortliste",
+ "blocklists_add_desc": "Sortlister kan vælges fra listen eller tilføjes manuelt",
+ "blocklists_desc": "Domæner fra disse lister blokeres",
+ "blocklists_empty": "Ingen sortlister endnu — tilføj den første ",
+ "blocklists_title": "Sortlister",
+ "bootstrap_dns": "Bootstrap DNS-servere",
+ "browse": "Gennemse",
+ "cache_cleared": "DNS-cache hermed ryddet",
+ "cache_config_clear": "Ryd cache",
+ "cache_config_max_ttl": "Tilsidesæt maksimal TTL i sekunder",
+ "cache_config_max_ttl_faq": "Den maksimale TTL-værdi (time-to-live) for poster i DNS-cachen",
+ "cache_config_max_ttl_placeholder": "Angiv makimum TTL",
+ "cache_config_min_ttl": "Tilsidesæt minimum TTL i sekunder",
+ "cache_config_min_ttl_faq": "Den minimale TTL-værdi (time-to-live) for poster i DNS-cachen",
+ "cache_config_min_ttl_placeholder": "Angiv minimum TTL",
+ "cache_config_optimistic": "Optimistisk caching",
+ "cache_config_optimistic_desc": "AdGuard Home svarer fra cachen, selv hvis posterne er udløbet, og vil forsøge at opdatere dem",
+ "cache_config_size": "Cache-størrelse i bytes",
+ "cache_config_size_faq": "DNS-cache-størrelse. For at deaktivere caching, lad feltet stå tomt",
+ "cache_config_size_validation": "Når aktiveret, skal cache-størrelsen skal være større end nul",
+ "cache_config_ttl_validation": "Minimum cache TTL-værdi skal være mindre end eller lig med den maksimale værdi",
+ "cache_confirm_clear_desc": "Dette fjerner alle cache-lagrede DNS-svar. Efterfølgende DNS-forespørgsler kan være langsommere, indtil cachen er genopbygget",
+ "cache_confirm_clear_title": "Ryd DNS-cache",
+ "cache_enabled": "Aktivér cache",
+ "cache_enabled_desc": "Gem DNS-svar lokalt",
+ "cancel": "Afbryd",
+ "cancel_btn": "Afbryd",
+ "category_tooltip": "Kategori",
+ "changes_saved_success": "Ændringer gemt",
+ "check_dhcp_servers": "Søg efter DHCP-servere",
+ "check_not_found": "Ikke fundet i dine filterlister",
+ "check_updates_btn": "Søg efter opdateringer",
+ "clear_btn": "Ryd",
+ "clear_query_log": "Ryd Forespørgselslog",
+ "client_add": "Tilføj Klient",
+ "client_added": "Klient tilføjet",
+ "client_already_blocked": "Klienten %ip% er allerede blokeret",
+ "client_blocked_flash": "Klient blokeret",
+ "client_details": "Klientoplysninger",
+ "client_id": "KlientID",
+ "client_id_faq": "Klienter kan identificeres ved deres ClientID. For mere info, besøg AdGuard Home GitHub ",
+ "client_id_placeholder": "Angiv en KlientID",
+ "client_identifier": "Identifikator",
+ "client_ip": "Klient-IP",
+ "client_location": "Klientplacering",
+ "client_removed": "Klient fjernet",
+ "client_settings": "Klientindstillinger",
+ "client_table_header": "Klient",
+ "client_unblocked_flash": "Klient afblokeret",
+ "clients": "Klienter",
+ "clients_add": "Tilføj klient",
+ "clients_add_identifier": "Tilføj identifikator",
+ "clients_add_name": "Klientnavn",
+ "clients_add_placeholder": "Angiv klientnavn",
+ "clients_desc": "Regler for enheder tilsluttet AdGuard Home",
+ "clients_dns_cache_size": "DNS-cachestørrelse i bytes",
+ "clients_dns_cache_size_placeholder": "Benyt tal",
+ "clients_dont_collect_stats": "Indsaml ikke statistik for denne klient",
+ "clients_dont_log": "Log ikke denne klient",
+ "clients_edit": "Redigér Klient",
+ "clients_identifier_already_used": "Denne identifikator benyttes allerede",
+ "clients_identifier_format_error": "Brug tal, engelske minuskler og/eller bindestreger",
+ "clients_identifiers": "Identifikatorer",
+ "clients_identifiers_desc": "Klientens IP-adresse, CIDR, MAC-adresse eller ClientID (for DoT, DoH og DoQ) kan bruges. Læs mere ",
+ "clients_logs_and_statistics": "Logger og statistik",
+ "clients_not_found": "Ingen klienter fundet",
+ "clients_protection": "Beskyttelse",
+ "clients_protection_desc": "Aktivér eller deaktivér beskyttelsesfunktioner, logning og statistikker",
+ "clients_remove_desc": "%value% , inkl. indstillinger, fjernes fra Vedvarende klienter",
+ "clients_remove_title": "Fjern klient?",
+ "clients_tags": "Tags",
+ "clients_tags_desc": "Der kan føjes tags til denne klient og inkludere disse i filtreringsregler. Læs mere ",
+ "clients_upstreams_desc": "Upstream DNS-servere, én pr. linje",
+ "clients_use_dns_cache": "Brug DNS-cache",
+ "clients_use_global_settings": "Brug globale indstillinger",
+ "clients_use_global_settings_desc": "Arv beskyttelsesindstillinger fra AdGuard Home-kontoen",
+ "close": "Luk",
+ "close_result": "Luk resultat",
+ "confirm": "Bekræft",
+ "confirm_client_block_desc": "AdGuard Home dropper alle DNS-forespørgsler fra klienten %ip% ",
+ "confirm_client_block_title": "Blokér klient?",
+ "confirm_client_unblock_desc": "DNS-forespørgsler fra klienten %ip% tillades",
+ "confirm_client_unblock_title": "Afblokér klient?",
+ "confirm_static_ip": "AdGuard Home opsætter %ip% til at være den statiske IP-adresse. Fortsæt?",
+ "copied": "Kopieret til udklipsholder",
+ "copy": "Kopiér",
+ "copy_to_clipboard": "Kopiér til udklipsholder",
+ "custom_filtering_rules": "Tilpassede filtreringsregler",
+ "dark_theme": "Mørkt tema",
+ "dashboard": "Kontrolpanel",
+ "delete_btn": "Slet",
+ "delete_confirm": "Slet lejemål?",
+ "delete_confirm_desc": "Dette frigiver %ip% ",
+ "delete_table_action": "Slet",
+ "delete_table_action_confirm": "Ja, slet",
+ "device_type": "Enhedstype",
+ "dhcp_add_static_lease": "Tilføj statisk lease",
+ "dhcp_config_saved": "DHCP-opsætning gemt",
+ "dhcp_edit_static_lease": "Redigér statisk tildeling",
+ "dhcp_error": "Mislykkedes at tjekke DHCP-serveren. Vent lidt inden et nyt forsøg",
+ "dhcp_form_gateway_address": "Gateway IP-adresse",
+ "dhcp_form_gateway_input": "Gateway IP",
+ "dhcp_form_range_start": "Start-IP-adresse",
+ "dhcp_form_range_title": "IP-adresseområde",
+ "dhcp_form_subnet_input": "Undernetmaske",
+ "dhcp_found": "DHCP-serveren kan ikke aktiveres",
+ "dhcp_ipv4_settings": "DHCP IPv4-indstillinger",
+ "dhcp_ipv6_settings": "DHCP IPv6-indstillinger",
+ "dhcp_lease_added": "Statisk lejemål \"%key%\" er tilføjet",
+ "dhcp_lease_deleted": "Statisk lejemål \"%key%\" er slettet",
+ "dhcp_lease_updated": "Statisk lejemål \"%key%\" er opdateret",
+ "dhcp_leases_not_found": "Ingen DHCP-leases fundet",
+ "dhcp_leases_title": "DHCP-leases",
+ "dhcp_mac_address_already_added": "Denne MAC-adresse er allerede tilføjet",
+ "dhcp_new_static_lease": "Ny statisk lease",
+ "dhcp_not_found": "DHCP-serveren er aktiv",
+ "dhcp_reset": "Dette nulstiller DHCP-opsætningen. Alle ændringer går tabt",
+ "dhcp_reset_leases": "Nulstil alle gyldighedsperioder",
+ "dhcp_reset_leases_confirm": "Dette nulstiller alle lejemål",
+ "dhcp_reset_leases_success": "DHCP- gyldighedsperioder nulstillet",
+ "dhcp_settings": "DHCP-indstillinger",
+ "dhcp_static_ip_error": "DHCP-serveren kræver en statisk IP-adresse. Det kunne ikke bekræfte, at denne netværksgrænseflade har én",
+ "dhcp_static_leases_not_found": "Intet DHCP statisk leases fundet",
+ "dhcp_table_hostname": "Værtsnavn",
+ "dhcp_table_ip_address": "IP-adresse",
+ "dhcp_table_mac_address": "MAC-adresse",
+ "disable": "Deaktivér",
+ "disable_protection_btn": "Deaktivér beskyttelse",
+ "disabled_dhcp": "DHCP-server deaktiveret",
+ "disallow_client_confirm_allowlist_note": "Denne handling undtager også reglen %ip% fra listen over tilladte klienter.",
+ "disallow_client_confirm_title": "Afvis denne klient?",
+ "disallow_this_client": "Afvis denne klient",
+ "dns_access_settings_title": "Adgangsindstillinger",
+ "dns_allowed_clients": "Tilladte klienter",
+ "dns_allowed_clients_label": "Tilladte klienter, én pr. linje",
+ "dns_allowed_clients_placeholder": "Angiv tilladte klienter",
+ "dns_blocking_mode": "Blokeringstilstand",
+ "dns_blocking_mode_custom_ip": "Tilpasset IP-adresse",
+ "dns_blocking_mode_default": "Standard",
+ "dns_blocking_mode_ipv4_label": "IPv4-blokering",
+ "dns_blocking_mode_ipv4_placeholder": "Angiv IPv4-adresse",
+ "dns_blocking_mode_ipv6_label": "IPv6-blokering",
+ "dns_blocking_mode_ipv6_placeholder": "Angiv IPv6-adresse",
+ "dns_blocking_mode_null_ip": "Null IP-adresse",
+ "dns_blocking_mode_nxdomain": "NXDOMAIN",
+ "dns_blocking_mode_refused": "REFUSED",
+ "dns_blocking_mode_title": "Blokeringstilstand",
+ "dns_blocking_mode_ttl_label": "Blokeret svar-TTL i sekunder",
+ "dns_bootstrap_dns_label": "Bootstrap-DNS-servere, én pr. linje",
+ "dns_bootstrap_dns_placeholder": "IP-adresser",
+ "dns_bootstrap_dns_title": "Bootstrap DNS-servere",
+ "dns_bootstrap_servers": "Bootstrap DNS-servere",
+ "dns_cache_config": "DNS-cacheopsætning",
+ "dns_cache_configuration_saved_toast": "DNS-cacheopsætning gemt",
+ "dns_cache_size": "Cache-størrelse",
+ "dns_cache_size_label": "DNS-cachestørrelse i bytes",
+ "dns_cache_size_title": "Cache-størrelse",
+ "dns_clear_cache": "Ryd cache",
+ "dns_clear_cache_cancel": "Afbryd",
+ "dns_clear_cache_confirm": "Ja, ryd",
+ "dns_clear_cache_yes": "Ja, ryd",
+ "dns_config": "DNS-serveropsætning",
+ "dns_disallowed_clients": "Ikke tilladte klienter",
+ "dns_disallowed_clients_label": "Ikke-tilladte klienter, én pr. linje",
+ "dns_disallowed_clients_title": "Ikke tilladte klienter",
+ "dns_disallowed_domains": "Ikke tilladte domæner",
+ "dns_disallowed_domains_label": "Ikke tilladte domæner, ét pr. linje",
+ "dns_disallowed_domains_title": "Ikke tilladte domæner",
+ "dns_edns_custom_placeholder": "Indtast IP-adresse",
+ "dns_fallback_dns_label": "Reserve-DNS-servere, én pr. linje",
+ "dns_fallback_dns_placeholder": "IP-adresser, sdns://, quic://, https://, h3://, or tls://",
+ "dns_fallback_dns_title": "Reserve DNS-servere",
+ "dns_fallback_servers": "Reserve DNS-servere",
+ "dns_optimistic_caching": "Optimistisk caching",
+ "dns_over_https": "DNS-over-HTTPS",
+ "dns_over_quic": "DNS-over-QUIC",
+ "dns_over_tls": "DNS-over-TLS",
+ "dns_override_max_ttl": "Tilsidesæt maksimal TTL",
+ "dns_override_max_ttl_title": "Tilsidesæt maksimal TTL",
+ "dns_override_min_ttl": "Tilsidesæt minimum TTL",
+ "dns_override_min_ttl_title": "Tilsidesæt minimum TTL",
+ "dns_privacy": "DNS-fortrolighed",
+ "dns_private_reverse_servers_label": "Private reverse-DNS-servere, én pr. linje",
+ "dns_private_reverse_servers_placeholder": "IP-adresser, sdns://, quic://, https://, h3://, or tls://",
+ "dns_private_reverse_servers_title": "Private reverse DNS-servere",
+ "dns_queries": "DNS-forespørgsler",
+ "dns_queries_tooltip": "Antallet af DNS-forespørgsler behandlet i de seneste 24 timer",
+ "dns_query": "DNS-forespørgsler",
+ "dns_rate_limit": "Hyppighedsgrænse",
+ "dns_rate_limit_allowlist": "Frekvensbegrænsningshvidliste",
+ "dns_rate_limit_allowlist_placeholder": "IP-adresser",
+ "dns_rate_limit_allowlist_title": "Frekvensbegrænsningshvidliste",
+ "dns_rate_limit_placeholder": "Angiv antalsgrænse",
+ "dns_rate_limit_title": "Hyppighedsgrænse",
+ "dns_rewrites": "DNS-omskrivninger",
+ "dns_server_addresses": "Serveradresser",
+ "dns_server_addresses_label": "Serveradresser, én pr. linje",
+ "dns_server_addresses_placeholder": "IP-adresser, sdns://, quic://, https://, h3://, or tls://",
+ "dns_server_addresses_title": "Serveradresser",
+ "dns_server_config_title": "DNS-serveropsætning",
+ "dns_server_configuration_saved_toast": "DNS-serveropsætning gemt",
+ "dns_settings": "DNS",
+ "dns_subnet_placeholder": "Angiv præfikslængde",
+ "dns_test_upstreams": "Test upstreams",
+ "dns_upstream_servers_title": "Upstream DNS-servere",
+ "dns_upstream_timeout": "Upstream-timeout",
+ "dns_upstream_timeout_title": "Upstream-timeout",
+ "dns_upstream_validation_invalid": "Ugyldigt format",
+ "domain": "Domæne",
+ "domain_or_client": "Domæne eller klient",
+ "download_mobileconfig": "Download opsætningsfil",
+ "dynamic_dhcp_leases_not_found": "Dynamiske DHCP-lejemål ikke fundet",
+ "edit_table_action": "Redigér",
+ "enable": "Aktivér",
+ "enabled_dhcp": "DHCP-server aktiveret",
+ "encrypted_dns_addresses": "Krypterede DNS-serveradresser",
+ "encryption_certificate_has_issues": "Certifikatet har problemer",
+ "encryption_certificate_path": "Certifikatsti",
+ "encryption_certificates": "Certifikater",
+ "encryption_certificates_desc": "For at bruge kryptering skal der angives en gyldig SSL-certifikatkæde til domænet. Der kan fås et gratis SSL-certifikat fra letsencrypt.org eller købes et fra en betroet Certifikatudsteder (CA)",
+ "encryption_certificates_input": "Kopiér/indsæt dine PEM-kodede certifikater hér.",
+ "encryption_certificates_source_content": "Indsæt certifikatets indhold",
+ "encryption_certificates_source_path": "Opsæt en certifikatfilsti",
+ "encryption_chain_invalid": "Certifikatkæden er ugyldig",
+ "encryption_chain_valid": "Certifikatkæden er gyldig",
+ "encryption_config_saved": "Krypteringsopsætning gemt",
+ "encryption_confirm_clear": "Nulstil krypteringsindstillinger",
+ "encryption_confirm_clear_desc": "Alle krypteringsindstillinger nulstilles til deres standardværdier",
+ "encryption_disable_plain_dns": "Deaktivér almindelig DNS?",
+ "encryption_disable_plain_dns_desc": "Enheder, som ikke understøtter krypteret DNS, fungerer muligvis ikke korrekt",
+ "encryption_doq": "DNS-over-QUIC port",
+ "encryption_doq_tooltip": "Denne port bruges til at køre en DNS-over-QUIC-server",
+ "encryption_dot": "DNS-over-TLS port",
+ "encryption_dot_tooltip": "Denne port bruges til at køre en DNS-over-TLS-server",
+ "encryption_encrypted_dns": "Krypteret DNS",
+ "encryption_encrypted_dns_desc": "AdGuard Homes admin-grænseflade bruger HTTPS. DNS-serveren understøtter DNS-over-HTTPS, DNS-over-TLS og DNS-over-QUIC",
+ "encryption_expire": "Udløber: %value%",
+ "encryption_force_redirect": "Omdirigér automatisk fra HTTP til HTTPS",
+ "encryption_hostnames": "Værtsnavne: %value%",
+ "encryption_https": "HTTPS-port",
+ "encryption_https_tooltip": "Denne port giver HTTPS-adgang til AdGuard Homes admin-grænseflade. Den leverer også DNS-over-HTTPS på stien '/dns-query'",
+ "encryption_issuer": "Udsteder: %value%",
+ "encryption_key": "Privat nøgle",
+ "encryption_key_input": "Kopiér/indsæt dine PEM-kodede private nøgle til dit certifikat hér.",
+ "encryption_key_invalid": "Privat nøgle er ugyldig",
+ "encryption_key_source_content": "Indsæt indholdet af den private nøgle",
+ "encryption_key_source_path": "Opsæt en privat nøglefil",
+ "encryption_key_type": "Krypteringsalgoritme: %value%",
+ "encryption_key_valid": "Privat nøgle er gyldig",
+ "encryption_plain_dns": "Almindelig DNS",
+ "encryption_plain_dns_error": "Aktivér mindst én krypteret DNS-protokol for at deaktivere almindelig DNS",
+ "encryption_private_key_path": "Private nøgle-sti",
+ "encryption_server": "Servernavn",
+ "encryption_server_enter": "Angiv dit domænenavn",
+ "encryption_server_tooltip_1": "Hvis opsat, detekterer AdGuard Home KlientID'er, svarer på DDR-forespørgsler og udfører yderligere forbindelsesvalideringer. Hvis ikke opsat, er disse funktioner deaktiveret",
+ "encryption_server_tooltip_2": "Skal matche et DNS-navn i certifikatet",
+ "encryption_subject": "Emne: %value%",
+ "encryption_title": "Kryptering",
+ "enter_cache_size": "Angiv cache-størrelse (bytes)",
+ "enter_ip_address_placeholder": "Indtast IP-adresse",
+ "error": "Fejl",
+ "fallback_dns": "Reserve DNS-servere",
+ "filter_added_successfully": "Tilføjet sortliste: %value% ",
+ "filter_added_successfully_allowlist": "Tilføjet hvidliste: %value% ",
+ "filter_added_successfully_more": "Tilføjede sortlister: %value% og %more% flere",
+ "filter_category_general": "Generalt",
+ "filter_category_general_desc": "Lister, som blokerer sporing og reklamer på de fleste enheder",
+ "filter_category_other": "Andet",
+ "filter_category_other_desc": "Andre blokeringslister",
+ "filter_category_regional": "Regional",
+ "filter_category_regional_desc": "Lister målrettet regionale annoncer og sporingsservere",
+ "filter_category_security": "Sikkerhed",
+ "filter_category_security_desc": "Lister designet specifikt til at blokere malware-, phishing- eller svindel-domæner",
+ "filter_removed_successfully": "Fjernet sortliste: %value% ",
+ "filter_removed_successfully_allowlist": "Fjernet hvidliste: %value% ",
+ "filters": "Filtre",
+ "forgot_password": "Glemt adgangskode?",
+ "forgot_password_list_item_1": "Opret en ny adgangskode med htpasswd-værktøjet eller anden online BCrypt-generator.",
+ "forgot_password_list_item_2": "Stop AdGuard Home.",
+ "forgot_password_list_item_3": "Åbn AdGuardHome.yaml, find adgangskodefeltet, og erstat dets indhold med den nye værdi.",
+ "forgot_password_list_item_4": "Start AdGuard Home.",
+ "forgot_password_list_item_5": "Log ind på webgrænsefladen med den nye adgangskode.",
+ "forgot_password_list_title": "For at skifte adgangskoden:",
+ "forgot_password_page_desc": "AdGuard Home gemmer adgangskoder som BCrypt-krypterede hashværdier",
+ "form_enter_blocked_response_ttl": "Angiv blokeringssvar TTL (sekunder)",
+ "form_enter_hostname": "Angiv et værtsnavn",
+ "form_enter_ip": "Angiv en IP-adresse",
+ "form_enter_mac": "Angiv en MAC-adresse",
+ "form_error_answer_format": "Ugyldigt resultatformat",
+ "form_error_client_id_format": "KlientID må kun indeholde cifre, minuskler og bindestreger",
+ "form_error_domain_format": "Ugyldigt domæneformat",
+ "form_error_equal": "Må ikke svare til.",
+ "form_error_format": "Ugyldigt format",
+ "form_error_gateway_ip": "Lease kan ikke have gatewayens IP-adresse",
+ "form_error_hostname_format": "Brug tal, engelske minuskler og/eller bindestreger",
+ "form_error_ip4_format": "Ugyldig IPv4-adresse",
+ "form_error_ip6_format": "Ugyldig IPv6-adresse",
+ "form_error_ip_already_added": "Denne IP-adresse er allerede tilføjet",
+ "form_error_ip_format": "Ugyldig IP-adresse",
+ "form_error_mac_already_added": "Denne MAC-adresse er allerede tilføjet",
+ "form_error_mac_format": "Ugyldig MAC-adresse",
+ "form_error_password": "Adgangskoder matcher ikke.",
+ "form_error_port": "Angiv gyldigt portnummer",
+ "form_error_port_range": "Angiv portnummer i intervallet 80-65535",
+ "form_error_port_unsafe": "Ikke-sikker port",
+ "form_error_range": "Værdien skal være mellem %min% og %max%",
+ "form_error_server_name": "Ugyldigt servernavn",
+ "form_error_url_format": "Angiv en gyldig URL eller filsti",
+ "found_in_known_domains": "Fundet i de kendte domæner",
+ "friday": "Fredag",
+ "gateway_or_subnet_invalid": "Ugyldig undernetmaske",
+ "general_statistics": "Generelle statistikker",
+ "home_dns_addresses": "AdGuard Home DNS-adresser",
+ "home_dns_addresses_desc": "AdGuard Home DNS-serveren lytter på flg. grænseflader/porte:",
+ "ignore_domains_desc_log": "Forespørgsler matchende disse regler vises ikke i Forespørgselslog",
+ "ignore_domains_desc_stats": "Forespørgsler matchende disse regler medtages ikke i statistikken",
+ "ignore_domains_title": "Ignorerede domæner",
+ "inactivity_schedule": "Inaktivitetsplan",
+ "inactivity_schedule_add": "Tilføj tidsplan",
+ "inactivity_schedule_all_day": "Hele dagen",
+ "inactivity_schedule_delete": "Slet tidsplan",
+ "inactivity_schedule_delete_desc": "Tjenesteblokering er aktiv hele dagen d. %value% ",
+ "inactivity_schedule_desc": "Indstil inaktivitetsperioder til blokering af tjenester",
+ "inactivity_schedule_edit": "Redigér tidsplan",
+ "inactivity_schedule_no_schedules": "Ingen tidsplan",
+ "inactivity_schedule_replace_desc": "Denne tidsplan erstatter alle eksisterende for samme ugedag. Hver dag kan kun have én inaktivitetsperiode",
+ "inactivity_schedule_time_from": "Tid fra",
+ "inactivity_schedule_time_to": "Tid til",
+ "inactivity_schedule_timezone": "Tidszone",
+ "install_auth_confirm": "Bekræft adgangskode",
+ "install_auth_password": "Adgangskode",
+ "install_auth_password_enter": "Angiv adgangskode",
+ "install_auth_username": "Brugernavn",
+ "install_auth_username_enter": "Angiv brugernavn",
+ "install_saved": "Gemt",
+ "install_settings_all_interfaces": "Alle grænseflader",
+ "install_settings_port": "Port",
+ "install_static_configure": "AdGuard Home har registreret, at den dynamiske IP-adresse %ip% bruges. Indstil den som den statiske adresse?",
+ "install_static_error": "AdGuard Home kan ikke opsætte den automatisk for denne netværksgrænseflade. Søg information om, hvordan dette gøres manuelt.",
+ "install_static_ok": "Gode nyheder! Den statiske IP-adresse er allerede opsat",
+ "install_step": "Trin",
+ "install_submit_title": "Tillykke!",
+ "instructions": "Vejledning",
+ "invalid": "Ugyldig",
+ "ip_address": "IP-adresse",
+ "ip_addresses_placeholder": "Angiv IP-adresser, én pr. linje",
+ "known_tracker": "Kendt tracker",
+ "language": "Sprog",
+ "last_days": "| Seneste %count% dag | Seneste %count% dage",
+ "last_hour": "Sidste time",
+ "last_hours": "| Seneste %count% time | Seneste %count% timer",
+ "last_updated_label": "Senest opdateret",
+ "light_theme": "Lyst tema",
+ "list_updated": "| %count% liste opdateret | %count% lister opdateret",
+ "login": "Log ind",
+ "logout": "Log ud",
+ "make_static": "Gør statisk",
+ "make_static_desc": "Denne handling konverterer en dynamisk adresse til en statisk",
+ "milliseconds_abbreviation": "ms",
+ "monday": "Mandag",
+ "name": "Navn",
+ "name_label": "Navn",
+ "name_table_header": "Navn",
+ "name_tooltip": "Navn",
+ "network_interface": "Netværksgrænseflade",
+ "next": "Næste",
+ "no": "Nej",
+ "no_stats_yet": "Ingen statistikker endnu — fortsæt brugen af AdGuard Home for at se dem",
+ "none_text": "Ingen",
+ "nothing_found": "Intet blev fundet",
+ "notify_undo": "Fortryd",
+ "notify_updated": "Opdateret",
+ "open_dashboard": "Åbn kontrolpanel",
+ "parental_control": "Forældrekontrol",
+ "parental_group_ai": "AI",
+ "parental_group_cdn": "CDN",
+ "parental_group_dating": "Dating",
+ "parental_group_development": "Softwareudvikling",
+ "parental_group_gambling": "Hasardspil, væddemål",
+ "parental_group_gaming": "Gaming",
+ "parental_group_hosting": "Webhosting",
+ "parental_group_messaging": "Beskedtjenester",
+ "parental_group_messenger": "Beskedtjenester",
+ "parental_group_privacy": "Fortrolighedsværktøjer",
+ "parental_group_shopping": "Shopping",
+ "parental_group_social": "Sociale netværk",
+ "parental_group_social_network": "Sociale netværk",
+ "parental_group_software": "Softwareudvikling",
+ "parental_group_streaming": "Streaming",
+ "password_label": "Adgangskode",
+ "password_placeholder": "Angiv adgangskode",
+ "password_requirements": "Adgangskodekrav",
+ "password_requirements_characters": "Minimum 8 tegn",
+ "password_requirements_lowercase": "Minimum 1 minuskel",
+ "password_requirements_match": "Adgangskoder matcher",
+ "password_requirements_special": "Engelske bogstaver, tal og specialtegn",
+ "password_requirements_uppercase": "Minimum 1 majuskel",
+ "pause_for_hour": "Pauseret i %count% time",
+ "pause_for_minutes": "| Pauseret i %count% minut | Pauseret i %count% minutter",
+ "pause_for_seconds": "| Pauseret i %count% sekund | Pauseret i %count% sekunder",
+ "pause_until_tomorrow": "Pauseret indtil i morgen %time%",
+ "period_notify": "For at ændre de viste perioder, redigér Statistikopbevaring under Generelle indstillinger ",
+ "plain_dns": "Almindelig DNS",
+ "plain_dns_addresses": "Almindelige DNS-serveradresser",
+ "port_53_faq_link": "Port 53 er ofte optaget af \"DNSStubListener\"- eller \"systemd-resolved\"-tjenester. Læs denne vejledning om, hvordan dette løses.",
+ "port_in_use": "Denne port er allerede i brug",
+ "privacy_policy": "Fortrolighedspolitik",
+ "processing_time_ms": "%value% ms",
+ "protection": "Beskyttelse",
+ "protocol": "Protokol",
+ "queries": "Forespørgsler",
+ "queries_total": "| %count% forespørgsel i alt | %count% forespørgsler i alt",
+ "query_details": "Forespørgselsdetaljer",
+ "query_log": "Forespørgselslog",
+ "query_log_all_reasons": "Alle årsager",
+ "query_log_all_statuses": "Alle statusser",
+ "query_log_allowed": "Hvidlistet",
+ "query_log_blocked": "Blokeret",
+ "query_log_blocked_by_filter": "Blokeret af filter",
+ "query_log_blocked_by_parental_control": "Blokeret af forældrekontrol",
+ "query_log_blocked_services": "Blokerede tjenester",
+ "query_log_blocked_threats": "Blokerede trusler",
+ "query_log_custom_filtering_rules": "Tilpassede filtreringsregler",
+ "query_log_detail_address": "Adresse: %value% ",
+ "query_log_detail_category": "Kategori: %value% ",
+ "query_log_detail_country": "Land: %value% ",
+ "query_log_detail_date": "Dato: %value% ",
+ "query_log_detail_dns_server": "DNS-server: %value% ",
+ "query_log_detail_domain": "Domæne: %value% ",
+ "query_log_detail_ecs": "ECS: %value% ",
+ "query_log_detail_elapsed": "Forløbet: %value% ",
+ "query_log_detail_name": "Navn: %value% ",
+ "query_log_detail_network": "Netværk: %value% ",
+ "query_log_detail_original_response": "Oprindeligt svar:",
+ "query_log_detail_protocol": "Protokol: %value% ",
+ "query_log_detail_reason": "Årsag:",
+ "query_log_detail_response": "Svar:",
+ "query_log_detail_response_code": "Svarkode: %value% ",
+ "query_log_detail_rules": "Reg(el/ler):",
+ "query_log_detail_served_from_cache": "Leveret fra cache: %value% ",
+ "query_log_detail_service_name": "Tjenestenavn: %value% ",
+ "query_log_detail_source": "Kilde: %value% ",
+ "query_log_detail_status": "Status: %value% ",
+ "query_log_detail_time": "Tid: %value% ",
+ "query_log_detail_type": "Type: %value% ",
+ "query_log_nothing_available": "Intet blev fundet",
+ "query_log_processed": "Behandlet",
+ "query_log_retention": "Logrotation",
+ "query_log_rewritten": "Omskrevet",
+ "query_log_safe_search": "Sikker Søgning",
+ "query_log_strict_search": "Brug dobbelt anførselstegn til stringent søgning",
+ "rate_limit_subnet_len_ipv4_error": "Længden på IPv4-undernetpræfiks skal være mellem 0 og 32",
+ "rate_limit_subnet_len_ipv6_error": "Længden på IPv6-undernetpræfiks skal være mellem 0 og 128",
+ "reason_table_header": "Årsag",
+ "refresh_btn": "Opdatér",
+ "refresh_statics": "Opdatér statistikerne",
+ "release_notes": "Udgivelsesnoter",
+ "remove": "Fjern",
+ "remove_tag": "Fjern %value%",
+ "report_an_issue": "Anmeld et problem",
+ "request_table_header": "Forespørgsel",
+ "requests_table_header": "Forespørgsler",
+ "reset": "Nulstil",
+ "reset_settings": "Nulstil indstillinger",
+ "reset_settings_confirm": "Ja, nulstil",
+ "response_details": "Svardetaljer",
+ "response_time": "Responstid",
+ "result": "Resultat",
+ "resume_protection_timer": "Beskyttelsen genoptages om %time%",
+ "rewrite_add": "Tilføj DNS-omskrivning",
+ "rewrite_domain_input_placeholder": "Angiv domænenavnet eller et jokertegn",
+ "rewrite_edit": "Redigér DNS-omskrivning",
+ "rewrite_hosts_applied": "Omskrevet af værtsfilreglen",
+ "rewrite_settings_updated": "DNS-omskrivningsindstilinger opdateret",
+ "rewrite_updated": "DNS-omskrivning hermed opdateret",
+ "rewrites_answer_input_placeholder": "Angiv IP-adresse, CNAME-post, A eller AAAA",
+ "rewrites_empty": "Ingen omskrivninger endnu — tilføj den første ",
+ "rewrites_remove_desc": "DNS-omskrivningen for %value% fjernes",
+ "rewrites_remove_title": "Fjern DNS-omskrivning?",
+ "rewrites_tooltip_examples_item1": "Omskriv kun DNS-svar for dette domænenavn",
+ "rewrites_tooltip_examples_item2": "Omskriv DNS-svar for alle underdomæner på eksempel.org",
+ "rewrites_tooltip_instructions_item1": "For at opløse domænenavnet til en IP-adresse, angiv en IPv4-adresse for A-poster og en IPv6-adresse for AAAA-poster",
+ "rewrites_tooltip_instructions_item2": "For at omdirigere domænenavnet, angiv CNAME-posten for måldomænet",
+ "rewrites_tooltip_instructions_item3": "For kun at beholde A-poster, angiv",
+ "rewrites_tooltip_instructions_item4": "For kun at beholde AAAA-poster, angiv",
+ "rewritten": "Omskrevet",
+ "rows_per_page": "Rækker pr. side %value%",
+ "rules_label": "Regler",
+ "safe_browsing": "Sikker Browsing",
+ "safe_search": "Sikker Søgning",
+ "safe_search_used": "Safe search anvendt",
+ "safe_search_used_tooltip": "Antallet af DNS-forespørgsler til søgemaskiner, hvor Safe Search blev håndhævet",
+ "saturday": "Lørdag",
+ "save": "Gem",
+ "save_btn": "Gem",
+ "search_placeholder": "Søg",
+ "selected": "%value% valgt",
+ "server_config_blocking_mode": "Blokeringstilstand",
+ "server_config_blocking_mode_custom_ip_desc": "Tilpasset IP-adresse: Besvar med en manuelt angivet IP-adresse",
+ "server_config_blocking_mode_default_desc": "Standard: Besvar med 0.0.0.0 for A og :: for AAAA, hvis blokeret af en Adblock-lignende regel. Besvar med IP-adressen angivet i reglen, hvis blokeret af en /etc/hosts-lignende regel",
+ "server_config_blocking_mode_ipv4": "IPv4-blokering",
+ "server_config_blocking_mode_ipv4_faq": "IP-adresse returneret ved en blokeret A-forespørgsel",
+ "server_config_blocking_mode_ipv4_placeholder": "Angiv IPv4-adresse",
+ "server_config_blocking_mode_ipv6": "IPv6-blokering",
+ "server_config_blocking_mode_ipv6_faq": "IP-adresse returneret ved en blokeret AAAA-forespørgsel",
+ "server_config_blocking_mode_ipv6_placeholder": "Angiv IPv6-adresse",
+ "server_config_blocking_mode_null_ip_desc": "Null IP-adresse: Besvar med 0.0.0.0 for A og :: for AAAA",
+ "server_config_blocking_mode_nxdomain_desc": "NXDOMAIN: Besvar med NXDOMAIN-kode",
+ "server_config_blocking_mode_refused_desc": "REFUSED: Besvar med REFUSED-kode",
+ "server_config_blocking_mode_ttl": "Blokeret svar-TTL i sekunder",
+ "server_config_blocking_mode_ttl_faq": "Angiver, hvor længe klienter skal cache et filtreret svar",
+ "server_config_custom_ip": "Tilpasset IP-adresse",
+ "server_config_default": "Standard",
+ "server_config_disable_ipv6": "Deaktivér IPv6-adresseopløsning",
+ "server_config_disable_ipv6_desc": "Drop alle DNS-forespørgsler for IPv6-adresser (type AAAA) og fjern IPv6-hints fra HTTPS-svar",
+ "server_config_dnssec_enable": "Aktivér DNSSEC",
+ "server_config_dnssec_enable_desc": "Sæt DNSSEC-flaget i udgående DNS-forespørgsler, og kontroller resultatet. Kræver en DNSSEC-aktiveret opløser",
+ "server_config_edns_cs_desc": "Føj indstillingen EDNS Client Subnet (ECS) til upstream-forespørgsler og log værdierne sendt af klienterne i forespørgselsloggen",
+ "server_config_edns_enable": "Aktivér EDNS-klientundernet",
+ "server_config_edns_use_custom_ip": "Benyt tilpasset IP-adresse til EDNS",
+ "server_config_edns_use_custom_ip_desc": "Erstat klientens IP-adresse med den tilpassede IP-adresse nedenfor",
+ "server_config_null_ip": "Null IP-adresse",
+ "server_config_nxdomain": "NXDOMAIN",
+ "server_config_rate_limit": "Responsfrekvensgrænse, RPS",
+ "server_config_rate_limit_faq": "Antallet af forespørgsler pr. sekund, der er tilladt pr. klient. En værdi på 0 betyder ingen grænse",
+ "server_config_rate_limit_placeholder": "Angiv antalsgrænse",
+ "server_config_rate_limit_whitelist": "Frekvensbegrænsningshvidliste",
+ "server_config_rate_limit_whitelist_faq": "IP-adresser undtaget fra antalsbegrænsning",
+ "server_config_refused": "REFUSED",
+ "server_config_subnet_len_ipv4": "Længde på undernetpræfiks for IPv4-adresser",
+ "server_config_subnet_len_ipv4_faq": "Længde på undernetpræfiks for IPv4-adresser til hastighedsbegrænsning. Standard er 24",
+ "server_config_subnet_len_ipv6": "Længde på undernetpræfiks for IPv6-adresser",
+ "server_config_subnet_len_ipv6_faq": "Længde på undernetpræfiks for IPv6-adresser til hastighedsbegrænsning. Standard er 56",
+ "server_config_subnet_len_placeholder": "Angiv præfikslængde",
+ "set_static_ip": "Opsæt en statisk IP-adresse",
+ "settings": "Indstillinger",
+ "settings_anonymize_client_ip": "Anonymisér klienters IP-adresser",
+ "settings_anonymize_client_ip_desc": "Erstat en del af IP-adressen med nuller",
+ "settings_browsing_security": "Browsing sikkerhed",
+ "settings_browsing_security_desc": "Blokér adgang til domæner fundet i databasen over ondsindede og phishingdomæner",
+ "settings_confirm_clear_query_log": "Ryd Forespørgselslog?",
+ "settings_confirm_clear_query_log_desc": "Dette sletter alle forespørgsler fra Forespørgselslog . Statistikker vil stadig være tilgængelige",
+ "settings_confirm_clear_statistics": "Ryd statistikker?",
+ "settings_confirm_clear_statistics_desc": "Dette rydder alle statistikker. Forespørgselslog vil stadig være tilgængelig",
+ "settings_confirm_decrease_log_rotation_interval": "Reducér logrotationsinterval?",
+ "settings_confirm_decrease_log_rotation_interval_desc": "Dette sletter alle logfiler ældre end %value%",
+ "settings_confirm_decrease_stats_rotation_interval": "Reducér statistikopbevaringsinterval?",
+ "settings_confirm_decrease_stats_rotation_interval_desc": "Dette sletter alle statistikker ældre end %value%",
+ "settings_custom": "Tilpasset",
+ "settings_days": "| %count% dag | %count% dage",
+ "settings_domain_names": "Domænenavne, ét pr. linje",
+ "settings_filter_requests": "Filtrér forespørgseler",
+ "settings_filter_requests_desc": "Der kan bruges Sortlister , Hvidlister og Brugerregler til at opsætte filtreringsregler",
+ "settings_filtering_and_security": "Filtrering og sikkerhed",
+ "settings_general_short": "Generalt",
+ "settings_global": "Global",
+ "settings_hours": "| %count% time | %count% timer",
+ "settings_log_dns_requests": "Log DNS-forespørgsler",
+ "settings_log_rotation_hours": "Logrotation i timer",
+ "settings_notify_changes_saved": "Ændringer gemt",
+ "settings_notify_query_log_cleared": "Forespørgselslog ryddet",
+ "settings_notify_statistics_cleared": "Statistik ryddet",
+ "settings_parental_control": "Forældrekontrol",
+ "settings_parental_control_desc": "Blokér websteder med indhold upassende for børn",
+ "settings_rotation_placeholder": "Benyt tal",
+ "settings_rule_syntax": "Regelsyntaks",
+ "settings_safe_search": "Sikker Søgning",
+ "settings_safe_search_desc": "Blokér upassende indhold i søgeresultater",
+ "settings_statistics": "Statistikker",
+ "settings_statistics_clear": "Ryd statistikker",
+ "settings_statistics_desc": "Vis forespørgsels- og klientstatistik på kontrolpanelet",
+ "settings_statistics_retention": "Statistikbevarelse",
+ "settings_statistics_retention_hours": "Statistikopbevaring i timer",
+ "settings_tooltip_domain_names": "Domænenavne, jokertegn eller filtreringsregler",
+ "settings_tooltip_examples": "Eksempler:",
+ "settings_yes_clear": "Ja, ryd",
+ "settings_yes_decrease": "Ja, reducér",
+ "setup_complete": "Opsætningen er fuldført, og AdGuard Home er klar til brug",
+ "setup_devices_android_list_1": "Åbn enheds-appen Indstillinger , og vælg Wi-Fi .",
+ "setup_devices_android_list_2": "Tryk og hold på det aktuelle netværk og vælg dernæst Redigér netværk . Det er ikke muligt at opsætte tilpasset DNS for mobilinternetforbindelser.",
+ "setup_devices_android_list_3": "På visse enheder skal afkrydsningsfeltet Avanceret muligvis vælges for at se flere indstillinger. For at tilpasse Android-DNS-indstillingerne, skift IP-indstillinger fra DHCP til Statisk .",
+ "setup_devices_android_list_4": "Skift værdierne for DNS 1 og DNS 2 til adresserne på AdGuard Home-serverem.",
+ "setup_devices_android_title": "Android",
+ "setup_devices_dns_android_list_1": "Android 9 understøtter som standard DNS-over-TLS . For at opsætte det, gå til Indstillinger → Netværk og Internet → Avanceret → Privat DNS og angiv domænenavnet.",
+ "setup_devices_dns_android_list_2": "AdGuard til Android understøtter opsætning af DNS-over-HTTPS , DNS-over-TLS og DNS-over-QUIC .",
+ "setup_devices_dns_android_list_3": "Intra føjer DNS-over-HTTPS -understøttelse til Android.",
+ "setup_devices_dns_ios_list_1": "AdGuard til iOS understøtter opsætning af DNS-over-HTTPS , DNS-over-TLS og DNS-over-QUIC .",
+ "setup_devices_dns_ios_list_2": "DNSCloak understøtter DNS-over-HTTPS , men for at opsætte det til at brug af egen server, generér et DNS-stempel til det.",
+ "setup_devices_dns_list_1": "For DNS-over-TLS , benyt ",
+ "setup_devices_dns_list_2": "For DNS-over-HTTPS , benyt ",
+ "setup_devices_dns_list_3": "For DNS-over-QUIC , benyt ",
+ "setup_devices_dns_list_devices": "Her er en liste over software, som kan bruges.",
+ "setup_devices_dns_macos_desc": "På iOS 14- eller macOS Big Sur-enhedeer kan der downloades en særlig mobileconfig-fil, der føjer DNS-over-HTTPS - eller DNS-over-TLS -servere til DNS-indstillingerne.",
+ "setup_devices_dns_other_list_1": "AdGuard Home kan selv fungere som en sikker DNS-klient på enhver platform.",
+ "setup_devices_dns_other_list_2": "dnsproxy understøtter alle kendte sikre DNS-protokoller.",
+ "setup_devices_dns_other_list_3": "dnscrypt-proxy understøtter DNS-over-HTTPS .",
+ "setup_devices_dns_other_list_4": "Mozilla Firefox understøtter DNS-over-HTTPS .",
+ "setup_devices_dns_other_list_5": "For flere implementeringer, se DNSCrypt og DNS-fortrolighed .",
+ "setup_devices_ios_list_1": "Åbn enheds-appen Indstillinger og vælg Wi‑Fi . Det er ikke muligt at opsætte tilpasset DNS for mobilinternetforbindelser.",
+ "setup_devices_ios_list_2": "Tryk på navnet på det aktuelt aktive netværk.",
+ "setup_devices_ios_list_3": "Gå til DNS → Opsæt DNS → Manuel og tilføj adresserne på AdGuard Home-serveren.",
+ "setup_devices_ios_list_4": "Tryk på Gem .",
+ "setup_devices_ios_title": "iOS",
+ "setup_devices_macos_list_1": "Klik på Apple-menuen → Systempræferencer → Netværk .",
+ "setup_devices_macos_list_2": "Vælg den ønskede forbindelse, og klik på Avanceret .",
+ "setup_devices_macos_list_3": "Vælg fanen DNS , og angiv adresserne til AdGuard Home-serveren.",
+ "setup_devices_macos_title": "macOS",
+ "setup_devices_router_desc_1": "Opsæt AdGuard Home på hjemmenetværksrouteren og beskyt alle enheder, som er tilsluttet den. Ingen grund til manuel opsætning af hver enhed.",
+ "setup_devices_router_desc_2": "Visse routere tillader ikke opsætning af en tilpasset DNS-server. Prøv i så tilfælde at opsætte AdGuard Home som en DHCP-server . Fungerer dette ikke, tjek venligst routermanualen for en vejledning til, hvordan DNS-servere tilpasses den givne router.",
+ "setup_devices_router_list_1": "Disse kan normalt tilgås med en webbrowser via en URL, såsom http://192.168.0.1 eller http://192.168.1.1 . En adgangskode skal muligvis angives. Adgangskoden kan ofte nulstilles via en knap på selve routeren, men vær opmærksom på, at dette typisk også sletter hele routeropsætningen. Skal routeren opsættes via en app, installér denne på mobilen eller PC'en og brug den til at tilgå routerens indstillinger.",
+ "setup_devices_router_list_1_title": "Åbn routerens indstillinger.",
+ "setup_devices_router_list_2": "For at finde indstillingerne, kig efter DNS ved siden af et felt, der tillader to eller tre sæt tal, hver opdelt i fire grupper med ét til tre cifre.",
+ "setup_devices_router_list_2_title": "Angiv i DHCP/DNS-indstillingerne adresserne på AdGuard Home-serveren.",
+ "setup_devices_router_title": "Router",
+ "setup_devices_windows_list_1": "Åbn Kontrolpanel fra Startmenu eller Windows-søgning .",
+ "setup_devices_windows_list_2": "Klik på Netværk og internet → Netværks- og delingscenter → Skift indstillinger for netværkskort .",
+ "setup_devices_windows_list_3": "Højreklik på den aktive forbindelse og vælg Egenskaber .",
+ "setup_devices_windows_list_4": "På listen, vælg Internet Protocol Version 4 (TCP/IPv4) for IPv4 eller Internet Protocol Version 6 (TCP/IPv6) for IPv6.",
+ "setup_devices_windows_list_5": "Klik på Egenskaber → Brug flg. DNS-serveradresser .",
+ "setup_devices_windows_list_6": "Angiv adresserne på AdGuard Home-serveren.",
+ "setup_devices_windows_title": "Windows",
+ "setup_dns_desc": "Opsæt routeren eller andre enheder til brug af flg. DNS-serveradresser:",
+ "setup_dns_notice_new": "For brug af DNS-over-HTTPS eller DNS-over-TLS , opsæt kryptering i AdGuard Home-indstillingerne.",
+ "setup_dns_privacy_ioc_mac": "iOS- og macOS-opsætning",
+ "setup_dns_privacy_other_title": "Andre implementeringer",
+ "setup_dns_quote_desc": "For at fungere korrekt kræver AdGuard Home DNS-serveren en statisk IP-adresse på lokalnetværket. Routeren vil ellers med tiden tildele den en anden IP-adresse, hvilket afbryder DNS-beskyttelsen",
+ "setup_dns_quote_title": "Tildel AdGuard Home DNS-serveren en statisk IP-addresse på lokalnetværket",
+ "setup_dns_title": "",
+ "setup_dns_title_banner": "DNS-serverindstillinger",
+ "setup_guide": "Opsætningsvejledning",
+ "setup_guide_auth_privacy": "Jeg accepterer Fortrolighedspolitik og Vilkår og betingelser for AdGuard-websteder",
+ "setup_guide_auth_subtitle": "Dette er nødvendigt for at beskytte AdGuard Homes admin-panel mod uautoriseret adgang",
+ "setup_guide_auth_title": "Opsæt adgangskodebeskyttelse",
+ "setup_guide_desc": "Opsæt enhederne til brug af AdGuard Home",
+ "setup_guide_device_type": "Vælg enhedstypen",
+ "setup_guide_greeting_button": "Kør guiden",
+ "setup_guide_greeting_desc": "Denne guide assisterer med at opsætte AdGuard Home:",
+ "setup_guide_greeting_list_1": "Opret legitimationsoplysninger for at tilgå admin-panelet",
+ "setup_guide_greeting_list_2": "Indstil Web UI-adressen til AdGuard Home",
+ "setup_guide_greeting_list_3": "Indstil AdGuard Home DNS-serveradresse",
+ "setup_guide_greeting_list_4": "Få vejledninger til bestemte individuelle enheder",
+ "setup_guide_greeting_list_5": "Tilgå AdGuard Home-kontrolpanelet ",
+ "setup_guide_greeting_title": "Opsæt AdGuard Home",
+ "setup_guide_title": "Opsæt ønskede enheder til brug af AdGuard Home",
+ "setup_ui_desc": "Brug disse adresser til at tilgå AdGuard Homes webbrugerflade. Ændr om nødvendigt porten",
+ "setup_ui_title": "Indstil Web UI-adresserne til AdGuard Home",
+ "setup_ui_title_banner": "Web UI-indstillinger",
+ "show_blocked_responses": "Blokeret",
+ "show_more_count": "+ %count% flere",
+ "show_processed_responses": "Behandlet",
+ "sort_asc": "Sortér fra A-Z",
+ "sort_desc": "Sortér fra Z-A",
+ "source_label": "Kilde",
+ "source_tooltip": "Kilde",
+ "static_dhcp_leases_not_found": "Statiske DHCP-lejemål ikke fundet",
+ "stats_adult": "Blokerede voksne websteder",
+ "stats_query_domain": "Mest forespurgte domæner",
+ "status_table_header": "Status",
+ "sunday": "Søndag",
+ "system_host_files": "System hosts-filer",
+ "system_theme": "Systemtema",
+ "table_client": "Klient",
+ "tags_title": "Tags",
+ "test_upstreams": "Test upstreams",
+ "threats_blocked": "Blokerede trusler",
+ "threats_blocked_tooltip": "Antallet af DNS-forespørgsler blokeret af AdGuard-browsingsikkerhedsmodulet",
+ "thursday": "Torsdag",
+ "time_table_header": "Tid",
+ "top_blocked_domains": "Hyppigst blokerede domæner",
+ "top_clients": "Hyppigste klienter",
+ "top_upstreams": "Top-upstreams",
+ "total": "I alt",
+ "try_again": "Forsøg igen",
+ "tuesday": "Tirsdag",
+ "type_value": "Type: %value%",
+ "unavailable_dhcp": "DHCP utilgængelig",
+ "unavailable_dhcp_desc": "AdGuard Home kan ikke køre en DHCP-server i dit OS",
+ "unblock": "Afblokér",
+ "unblock_client": "Afblokér klient",
+ "unknown_filter": "Ukendt filter %filterId%",
+ "update_button": "Opdatér",
+ "update_filters_custom_hours": "Tilpasset interval i timer",
+ "update_filters_disable": "Deaktivér",
+ "update_filters_interval_custom": "Tilpasset",
+ "update_filters_interval_daily": "Hver dag",
+ "update_filters_interval_hourly": "Hver time",
+ "update_filters_interval_weekly": "Hver uge",
+ "update_filters_title": "Opdatér filtre automatisk",
+ "updated_upstream_dns_toast": "Upstream-servere gemt",
+ "updates_checked": "En ny version af AdGuard Home er tilgængelig\n",
+ "updates_version_equal": "AdGuard Home er opdateret",
+ "upstream": "Upstream",
+ "upstream_bootstrap_dns_faq": "IP-adresser på DNS-servere brugt til at slå IP-adresserne til DoH/DoT-upstream-opløsere op. Kommentarer ikke tilladt",
+ "upstream_bootstrap_dns_title": "Bootstrap-DNS-servere, én pr. linje",
+ "upstream_dns": "Upstream DNS-servere",
+ "upstream_dns_addresses": "Serveradresser, én pr. linje",
+ "upstream_dns_addresses_faq": "Læs mere om opsætning af upstream-DNS-servere. Her er en liste over kendte DNS-udbydere at vælge imellem.",
+ "upstream_dns_configured_in_file": "Opsat i %path%",
+ "upstream_dns_fastest_addr": "Hurtigste IP-adresse",
+ "upstream_dns_parallel_requests": "Parallelle forespørgsler",
+ "upstream_dns_placeholder": "IP-adresser, sdns://, quic://, https://, h3://, or tls://",
+ "upstream_dns_servers_title": "Upstream DNS-servere",
+ "upstream_enable_reverse_lookup_desc": "Omvendt opslag af klient-IP-adresser til deres værtsnavne ved at sende PTR-forespørgsler til passende opløsere: Private DNS-servere for lokale klienter eller upstream-servere for klienter med offentlige IP-adresser",
+ "upstream_enable_reverse_lookup_title": "Aktivér omvendt opslag af klient-IP-adresser",
+ "upstream_example_multiple_upstreams_reserved": "%value%: flere upstreams til bestemte domæner ",
+ "upstream_example_tcp": "%ipv4%,%ipv6%: almindelig DNS (over TCP)",
+ "upstream_example_tcp_hostname": "%value%: almindelig DNS (over TCP, værtsnavn)",
+ "upstream_example_tcp_port": "%ipv4%,%ipv6%: almindelig DNS (over TCP, med port)",
+ "upstream_example_udp": "%ipv4%,%ipv6%: almindelig DNS (over UDP)",
+ "upstream_example_udp_hostname": "%value%: almindelig DNS (over UDP, værtsnavn)",
+ "upstream_example_udp_port": "%ipv4%,%ipv6%: almindelig DNS (over UDP, med port)",
+ "upstream_example_upstream_comment": "%value%: En kommentar.",
+ "upstream_example_upstream_doh": "%value%: Krypteret DNS-over-HTTPS ",
+ "upstream_example_upstream_doh3": "%value%: Krypteret DNS-over-HTTPS med tvungen HTTP/3 og uden reserve HTTP/2-mulighed",
+ "upstream_example_upstream_doq": "%value%: Krypteret DNS-over-QUIC ",
+ "upstream_example_upstream_dot": "%value%: Krypteret DNS-over-TLS ",
+ "upstream_example_upstream_reserved": "%value%: En upstream til bestemte domæner ",
+ "upstream_example_upstream_sdns": "%value%: DNS-stempler til DNSCrypt - eller DNS-over-HTTPS -opløsere",
+ "upstream_examples_title": "Eksempler",
+ "upstream_fallback_title": "Reserve-DNS-servere, én pr. linje",
+ "upstream_fallback_title_faq": "Reserve-DNS-servere brugt ved utilgængelige upstream-DNS-servere. Brug samme syntaks som til upstream-DNS-serverne",
+ "upstream_private_ptr_resolvers_desc": "Opløs PTR-, SOA- og NS-forespørgsler til ARPA-domæner indeholdende private IP-adresser via private upstream-servere, DHCP, /etc/hosts mv. Hvis deaktiveret, besvarer AdGuard Home alle sådanne forespørgsler med NXDOMAIN",
+ "upstream_private_ptr_resolvers_title": "Brug private reverse-DNS-opløsere",
+ "upstream_ptr": "Private reverse-DNS-servere, én pr. linje",
+ "upstream_ptr_faq_1": "AdGuard Home bruger disse DNS-servere til private PTR-, SOA- og NS-forespørgsler. Private forespørgsler kommer fra klienter med en privat IP-adresse og spørger om et ARPA-domæne indeholdende et undernet fra et privat IP-adresseområde, såsom %value%",
+ "upstream_ptr_faq_2": "Er dette felt tomt, anvendes operativsystemets standard DNS-opløsere, undtagen til AdGuard Home IP-adresserne",
+ "upstream_ptr_faq_3": "AdGuard Home anvender som standard flg. reverse-DNS-opløsere: %value_1% og %value_2%",
+ "upstream_timeout": "Upstream-timeout i sekunder",
+ "upstream_timeout_faq": "Den tid, der skal ventes på et svar fra upstream-serveren",
+ "upstream_timeout_placeholder": "Angiv timeoutvarigheden for upstream-serveren",
+ "upstreams": "Upstreams",
+ "url_label": "URL",
+ "use_saved_key": "Brug en eksisterende privat nøgle",
+ "user_rules": "Brugerregler",
+ "user_rules_add_to_allowlist": "Føj til hvidliste",
+ "user_rules_allow_service": "Tillad tjeneste",
+ "user_rules_browsing_security_disabled": "Browsingsikkerhed deaktiveret",
+ "user_rules_check_button": "Tjek",
+ "user_rules_check_client_label": "Klientidentifikator (navn, ClientID eller IP-adresse)",
+ "user_rules_check_client_placeholder": "Angiv klientidentifikator",
+ "user_rules_check_dns_record_type_label": "DNS-posttype",
+ "user_rules_check_hostname_label": "Værts- eller domænenavn",
+ "user_rules_check_hostname_placeholder": "Angiv mål",
+ "user_rules_check_title": "Tjek domænefiltrering globalt eller for en klient",
+ "user_rules_cname": "CNAME: %cname%",
+ "user_rules_disable_browsing_security": "Deaktivér Browsingsikkerhed",
+ "user_rules_disable_filter": "Deaktivér filter",
+ "user_rules_disable_parental_control": "Deaktivér Forældrekontrol",
+ "user_rules_disable_safe_search": "Deaktivér Safe Search",
+ "user_rules_dns_record_type_placeholder": "Vælg DNS-posttype",
+ "user_rules_dns_rewrite_removed": "Regel fjernet fra DNS-omskrivning",
+ "user_rules_domain": "Domæne: %value%",
+ "user_rules_domain_blocked": "Domæne er blokeret",
+ "user_rules_domain_is_allowed": "Domæne er tilladt",
+ "user_rules_domain_is_processed": "Domæne behandles",
+ "user_rules_edit_dns_rewrite": "Redigér DNS-omskrivning",
+ "user_rules_example_block": "||%domain% Blokér adgang til %domain% inkl. underdomæner",
+ "user_rules_example_comment": "! Dette er en kommentar For at kommentere, start linjen med ! eller #",
+ "user_rules_example_comment_2": "# Dette er også en kommentar For at kommentere, start linjen med ! eller #",
+ "user_rules_example_regex": "/REGEX/ Blokér adgang til domæner matchende det angivne regulære udtryk",
+ "user_rules_example_response": "127.0.0.1 %domain% Besvar med 127.0.0.1 kun for %domain%, ikke for underdomænerne",
+ "user_rules_example_unblock": "@@||%domain% Afblokér adgang til %domain% inkl. underdomæner",
+ "user_rules_filter_was_disabled": "%value% er deaktiveret",
+ "user_rules_ip": "IP-adresser: %ip%",
+ "user_rules_learn_more": "Læs mere om oprettelse af egne værtslister.",
+ "user_rules_no_rules_matched": "Resultat: Ingen regler matchede",
+ "user_rules_parental_control_disabled": "Forældrekontrol deaktiveret",
+ "user_rules_placeholder": "Dette er en pladsholder",
+ "user_rules_reason": "Årsag: %reason%",
+ "user_rules_reason_allowed_by": "Tilladt af %source%",
+ "user_rules_reason_blocked_by": "Blokeret af %source%",
+ "user_rules_reason_filtered_by": "Filtreret af %source%",
+ "user_rules_redirected_to": "Omdirigeret til: %value%",
+ "user_rules_remove_dns_rewrite": "Fjern DNS-omskrivning",
+ "user_rules_rewrite_rule_is_applied": "Omskrivningsregel effektueret",
+ "user_rules_rewritten_to": "Omskrevet til: %value%",
+ "user_rules_rule": "Regel: %rule%",
+ "user_rules_rule_added_to_custom_filtering_rules": "Regel føjet til tilpassede filtreringsregler",
+ "user_rules_rule_removed": "Regel fjernet fra tilpassede filtreringsregler",
+ "user_rules_safe_search_disabled": "Safe search deaktiveret",
+ "user_rules_service": "Tjeneste: %service%",
+ "user_rules_service_allowed": "Tjenesten %value% er nu tilladt",
+ "user_rules_source": "Kilde: %value%",
+ "user_rules_source_allowlist": "Hvidliste: %value%",
+ "user_rules_source_blocklist": "Sortliste: %value%",
+ "user_rules_status": "Status: %reason%",
+ "username_label": "Brugernavn",
+ "username_placeholder": "Angiv brugernavn",
+ "validated_with_dnssec": "Valideret med DNSSEC",
+ "wednesday": "Onsdag",
+ "whois": "WHOIS",
+ "yes": "Ja",
+ "yes_disable": "Ja, deaktivér",
+ "yes_reset": "Ja, nulstil"
+}
diff --git a/client_v2/src/__locales/de.json b/client_v2/src/__locales/de.json
new file mode 100644
index 00000000000..7a2d8f61f49
--- /dev/null
+++ b/client_v2/src/__locales/de.json
@@ -0,0 +1,748 @@
+{
+ "access_settings_allowed_faq": "Eine Liste von CIDRs, IP-Adressen oder ClientIDs . Wenn dieses Feld Einträge enthält, akzeptiert AdGuard Home Anfragen nur von diesen Clients",
+ "access_settings_allowed_placeholder": "Zugelassene Clients eingeben",
+ "access_settings_allowed_title": "Zugelassene Clients (einer pro Zeile)",
+ "access_settings_blocked_faq_1": "AdGuard Home verwirft DNS-Anfragen, die auf diese Domains zutreffen. Solche Anfragen tauchen nicht im Anfragenprotokoll auf. Du kannst exakte Domainnamen, Platzhalter (Wildcards) oder URL-Filterregeln eingeben, wie zum Beispiel example.org, *.example.org oder ||example.org^",
+ "access_settings_blocked_faq_2": "Nicht zu verwechseln mit Filtern",
+ "access_settings_blocked_placeholder": "Nicht zugelassene Domains eingeben",
+ "access_settings_blocked_title": "Nicht zugelassene Domains (eine pro Zeile)",
+ "access_settings_disallowed_faq": "Eine Liste von CIDRs, IP-Adressen oder ClientIDs . Wenn dieses Feld Einträge enthält, verwirft AdGuard Home Anfragen von diesen Clients. Das Feld wird ignoriert, falls Zugelassene Clients Einträge enthält",
+ "access_settings_disallowed_placeholder": "Nicht zugelassene Clients eingeben",
+ "access_settings_disallowed_title": "Nicht zugelassene Clients (einer pro Zeile)",
+ "access_settings_saved_toast": "Zugriffseinstellungen gespeichert",
+ "access_settings_title": "Zugriffsrechte",
+ "actions_table_header": "Aktionen",
+ "add": "Hinzufügen",
+ "add_allowlist": "Zulassungsliste hinzufügen",
+ "add_blocklist": "Blockliste hinzufügen",
+ "add_persistent_client": "Als dauerhaften Client hinzufügen",
+ "add_to_allowlist": "Zur Positivliste hinzufügen",
+ "ads_blocked": "Anzeigen blockiert",
+ "ads_blocked_card": "Anzeigen blockiert",
+ "ads_blocked_tooltip": "Anzahl der DNS-Anfragen, die von deinen Adblock-Filtern und Hosts-Blocklisten blockiert wurden",
+ "adult_websites_blocked": "Erwachsenenseiten blockiert",
+ "adult_websites_blocked_tooltip": "Anzahl der gesperrten Websites mit jugendgefährdenden Inhalten",
+ "all_domains": "Alle Domains",
+ "all_lists_up_to_date_toast": "Alle Listen sind bereits auf dem neuesten Stand",
+ "all_rewrites_disabled": "Alle benutzerdefinierten DNS-Antworten werden deaktiviert",
+ "all_rewrites_enabled": "Alle benutzerdefinierten DNS-Antworten werden aktiviert",
+ "allow_service": "Service zulassen",
+ "allowed": "Auf Freigabeliste",
+ "allowlist_add": "Zulassungsliste hinzufügen",
+ "allowlist_edit": "Zulassungsliste bearbeiten",
+ "allowlist_placeholder_example": "Meine Zulassungsliste",
+ "allowlist_remove": "Zulassungsliste entfernen?",
+ "allowlist_remove_desc": "%value% wird entfernt",
+ "allowlists": "Zulassungslisten",
+ "allowlists_title": "Zulassungslisten",
+ "apply": "Anwenden",
+ "autofix_warning_result": "Als Folge daraus werden alle DNS-Anforderungen von Ihrem System standardmäßig von AdGuardHome verarbeitet.",
+ "autofix_warning_text": "Wenn Sie auf „Beheben“ klicken, konfiguriert AdGuardHome Ihr System für die Verwendung des AdGuardHome-DNS-Servers.",
+ "average_time_processing": "Durchschnittliche Bearbeitungsdauer",
+ "average_time_processing_tooltip": "Durchschnittliche Verarbeitungszeit einer DNS-Anfrage (in Millisekunden)",
+ "average_upstream_response_time": "Durchschnittliche Upstream-Antwortzeit",
+ "back": "Zurück",
+ "block": "Sperren",
+ "block_client": "Client blockieren",
+ "block_for_this_client_only": "Nur für diesen Client sperren",
+ "blocked": "Blockiert",
+ "blocked_adult_websites": "Durch Kindersicherung blockiert",
+ "blocked_by_cname_or_ip": "Gesperrt durch CNAME oder IP",
+ "blocked_queries": "Blockierte Anfragen",
+ "blocked_service": "Gesperrte Dienste",
+ "blocked_services": "Gesperrte Dienste",
+ "blocked_services_desc": "Beliebte Websites und Dienste auswählen, die blockiert werden sollen",
+ "blocked_threats": "Gesperrte Bedrohungen",
+ "blocked_threats_chart": "Gesperrte Bedrohungen",
+ "blocked_total": "%value% insgesamt blockiert",
+ "blocklist_add_from_list": "Aus Liste hinzufügen",
+ "blocklist_add_manual": "Manuell hinzufügen",
+ "blocklist_contents": "Inhalte (.%value%)",
+ "blocklist_edit": "Blockliste bearbeiten",
+ "blocklist_homepage": "Startseite",
+ "blocklist_placeholder_example": "Meine Blockliste",
+ "blocklist_remove": "Blockliste entfernen?",
+ "blocklist_remove_desc": "%value% wird entfernt",
+ "blocklist_url_file_path": "URL oder Dateipfad",
+ "blocklists_add": "Blocklisten hinzufügen",
+ "blocklists_add_desc": "Du kannst Blocklisten aus der Liste auswählen oder sie manuell hinzufügen",
+ "blocklists_desc": "Domains aus diesen Listen werden blockiert",
+ "blocklists_empty": "Noch keine Blocklisten. Blockliste hinzufügen ",
+ "blocklists_title": "Blocklisten",
+ "bootstrap_dns": "Bootstrap-DNS-Server",
+ "browse": "Durchsuchen",
+ "cache_cleared": "DNS-Cache erfolgreich geleert",
+ "cache_config_clear": "Cache leeren",
+ "cache_config_max_ttl": "Maximaler TTL-Override (in Sekunden)",
+ "cache_config_max_ttl_faq": "Der maximale TTL-Wert für Einträge im DNS-Cache",
+ "cache_config_max_ttl_placeholder": "Maximale TTL eingeben",
+ "cache_config_min_ttl": "Minimaler TTL-Override (in Sekunden)",
+ "cache_config_min_ttl_faq": "Der Mindest-TTL-Wert für Einträge im DNS-Cache",
+ "cache_config_min_ttl_placeholder": "Mindest-TTL eingeben",
+ "cache_config_optimistic": "Optimistisches Caching",
+ "cache_config_optimistic_desc": "AdGuard Home wird aus dem Cache antworten, selbst wenn die Einträge abgelaufen sind, und versuchen, sie zu aktualisieren",
+ "cache_config_size": "Cache-Größe in Bytes",
+ "cache_config_size_faq": "DNS-Cache-Größe. Um das Caching zu deaktivieren, leer lassen",
+ "cache_config_size_validation": "Die Cachegröße muss größer als Null sein, wenn diese Option aktiviert ist",
+ "cache_config_ttl_validation": "Der minimale Cache-TTL-Override muss kleiner oder gleich dem Maximum sein",
+ "cache_confirm_clear_desc": "Dadurch werden alle zwischengespeicherten DNS-Antworten gelöscht. Nachfolgende DNS-Anfragen können länger dauern, bis der Cache neu aufgebaut ist",
+ "cache_confirm_clear_title": "DNS-Cache leeren",
+ "cache_enabled": "Cache aktivieren",
+ "cache_enabled_desc": "DNS-Antworten lokal speichern",
+ "cancel": "Abbrechen",
+ "cancel_btn": "Abbrechen",
+ "category_tooltip": "Kategorie",
+ "changes_saved_success": "Änderungen gespeichert",
+ "check_dhcp_servers": "Auf DHCP-Server prüfen",
+ "check_not_found": "Nicht in Ihren Filterlisten enthalten",
+ "check_updates_btn": "Nach Updates suchen",
+ "clear_btn": "Leeren",
+ "clear_query_log": "Anfragenprotokoll löschen",
+ "client_add": "Client hinzufügen",
+ "client_added": "Client hinzugefügt",
+ "client_already_blocked": "Client %ip% ist bereits blockiert",
+ "client_blocked_flash": "Client blockiert",
+ "client_details": "Einzelheiten des Clients",
+ "client_id": "Client-ID",
+ "client_id_faq": "Clients können anhand ihrer ClientID identifiziert werden. Weitere Details findest du auf dem AdGuard Home GitHub ",
+ "client_id_placeholder": "Client-ID eingeben",
+ "client_identifier": "Bezeichner",
+ "client_ip": "Client-IP",
+ "client_location": "Standort des Clients",
+ "client_removed": "Client entfernt",
+ "client_settings": "Client-Einstellungen",
+ "client_table_header": "Client",
+ "client_unblocked_flash": "Client entsperrt",
+ "clients_add": "Client hinzufügen",
+ "clients_add_identifier": "Kennung hinzufügen",
+ "clients_add_placeholder": "Clientnamen eingeben",
+ "clients_desc": "Regeln für Geräte, die mit AdGuard Home verbunden sind",
+ "clients_dns_cache_size": "Größe des DNS-Cache, in Bytes",
+ "clients_dns_cache_size_placeholder": "Zahlen verwenden",
+ "clients_dont_collect_stats": "Keine Statistiken für diesen Client sammeln",
+ "clients_dont_log": "Diesen Client nicht protokollieren",
+ "clients_edit": "Client bearbeiten",
+ "clients_identifiers": "Kennungen",
+ "clients_logs_and_statistics": "Protokolle und Statistiken",
+ "clients_not_found": "Keine Clients gefunden",
+ "clients_protection": "Schutz",
+ "clients_tags": "Schlagwörter",
+ "clients_upstreams_desc": "Upstream-DNS-Server, einer pro Zeile",
+ "clients_use_dns_cache": "DNS-Cache verwenden",
+ "clients_use_global_settings": "Allgemeine Einstellungen nutzen",
+ "close": "Schließen",
+ "close_result": "Ergebnis schließen",
+ "confirm": "Bestätigen",
+ "confirm_client_block_desc": "AdGuard Home wird alle DNS-Anfragen vom Client %ip% verwerfen",
+ "confirm_client_block_title": "Client blockieren?",
+ "confirm_client_unblock_desc": "DNS-Anfragen vom Client %ip% werden zugelassen",
+ "confirm_client_unblock_title": "Client entsperren?",
+ "copied": "Kopiert",
+ "copy": "Kopieren",
+ "copy_to_clipboard": "Kopieren",
+ "custom_filtering_rules": "Benutzerdefinierte Filterregeln",
+ "dark_theme": "Dunkles Design",
+ "dashboard": "Übersicht",
+ "delete_btn": "Löschen",
+ "delete_table_action": "Löschen",
+ "device_type": "Gerätetyp",
+ "dhcp_add_static_lease": "Statische Zuweisung hinzufügen",
+ "dhcp_config_saved": "DHCP-Konfiguration erfolgreich gespeichert",
+ "dhcp_edit_static_lease": "Statische Zuweisung bearbeiten",
+ "dhcp_error": "Fehler bei der Überprüfung des DHCP-Servers. Bitte warte kurz und versuche es erneut",
+ "dhcp_form_gateway_input": "Gateway-IP",
+ "dhcp_form_subnet_input": "Subnetz-Maske",
+ "dhcp_found": "DHCP-Server kann nicht aktiviert werden",
+ "dhcp_ipv4_settings": "DHCP-IPv4-Einstellungen",
+ "dhcp_ipv6_settings": "DHCP-IPv6-Einstellungen",
+ "dhcp_leases_not_found": "Keine DHCP-Zuweisungen gefunden",
+ "dhcp_leases_title": "DHCP-Zuweisungen",
+ "dhcp_new_static_lease": "Neue statische Zuweisung",
+ "dhcp_not_found": "DHCP-Server ist aktiv",
+ "dhcp_reset_leases": "Alle Leases zurücksetzen",
+ "dhcp_reset_leases_confirm": "Dies setzt alle Leases zurück",
+ "dhcp_reset_leases_success": "DHCP-Zuweisungen erfolgreich zurückgesetzt",
+ "dhcp_settings": "DHCP-Einstellungen",
+ "dhcp_static_ip_error": "Der DHCP-Server benötigt eine statische IP-Adresse. Wir konnten nicht bestätigen, dass diese Netzwerkschnittstelle über eine solche verfügt",
+ "dhcp_static_leases_not_found": "Keine statischen DHCP-Zuweisungen gefunden",
+ "dhcp_table_hostname": "Hostname",
+ "dhcp_table_ip_address": "IP-Adresse",
+ "disable": "Deaktivieren",
+ "disable_protection_btn": "Schutz deaktivieren",
+ "disabled_dhcp": "DHCP-Server deaktiviert",
+ "disallow_this_client": "Diesen Client sperren",
+ "dns_access_settings_title": "Zugriffsrechte",
+ "dns_allowed_clients": "Zugelassene Clients",
+ "dns_allowed_clients_label": "Zugelassene Clients (einer pro Zeile)",
+ "dns_allowed_clients_placeholder": "Zugelassene Clients eingeben",
+ "dns_blocking_mode": "Sperrmodus",
+ "dns_blocking_mode_custom_ip": "Benutzerdefinierte IP-Adresse",
+ "dns_blocking_mode_default": "Standard",
+ "dns_blocking_mode_ipv4_label": "IPv4-Sperren",
+ "dns_blocking_mode_ipv4_placeholder": "IPv4-Adresse eingeben",
+ "dns_blocking_mode_ipv6_label": "IPv6-Sperren",
+ "dns_blocking_mode_ipv6_placeholder": "IPv6-Adresse eingeben",
+ "dns_blocking_mode_null_ip": "Null-IP-Adresse",
+ "dns_blocking_mode_nxdomain": "NXDomain",
+ "dns_blocking_mode_refused": "REFUSED",
+ "dns_blocking_mode_title": "Sperrmodus",
+ "dns_blocking_mode_ttl_label": "TTL für blockierte Antworten (in Sekunden)",
+ "dns_bootstrap_dns_label": "Bootstrap-DNS-Server, einer pro Zeile",
+ "dns_bootstrap_dns_placeholder": "IP-Adressen",
+ "dns_bootstrap_dns_title": "Bootstrap-DNS-Server",
+ "dns_bootstrap_servers": "Bootstrap-DNS-Server",
+ "dns_cache_config": "Konfiguration des DNS-Cache",
+ "dns_cache_configuration_saved_toast": "DNS-Cache-Konfiguration gespeichert",
+ "dns_cache_size": "Größe des Cache",
+ "dns_cache_size_label": "Größe des DNS-Cache, in Bytes",
+ "dns_cache_size_title": "Größe des Cache",
+ "dns_clear_cache": "Cache leeren",
+ "dns_clear_cache_cancel": "Abbrechen",
+ "dns_clear_cache_confirm": "Ja, bitte",
+ "dns_clear_cache_yes": "Ja, bitte",
+ "dns_config": "DNS-Serverkonfiguration",
+ "dns_disallowed_clients": "Nicht zugelassene Clients",
+ "dns_disallowed_clients_label": "Nicht zugelassene Clients (einer pro Zeile)",
+ "dns_disallowed_clients_title": "Nicht zugelassene Clients",
+ "dns_disallowed_domains": "Nicht zugelassene Domains",
+ "dns_disallowed_domains_label": "Nicht zugelassene Domains (eine pro Zeile)",
+ "dns_disallowed_domains_title": "Nicht zugelassene Domains",
+ "dns_edns_custom_placeholder": "IP-Adresse eingeben",
+ "dns_fallback_dns_label": "Fallback-DNS-Server, einer pro Zeile",
+ "dns_fallback_dns_placeholder": "IP-Adressen, sdns://, quic://, https://, h3:// oder tls://",
+ "dns_fallback_dns_title": "Fallback-DNS-Server",
+ "dns_fallback_servers": "Fallback-DNS-Server",
+ "dns_optimistic_caching": "Optimistisches Caching",
+ "dns_over_https": "DNS-over-HTTPS",
+ "dns_over_quic": "DNS-over-QUIC",
+ "dns_over_tls": "DNS-over-TLS",
+ "dns_override_max_ttl": "TTL-Höchstwert überschreiben",
+ "dns_override_max_ttl_title": "TTL-Höchstwert überschreiben",
+ "dns_override_min_ttl": "TTL-Minimalwert überschreiben",
+ "dns_override_min_ttl_title": "TTL-Minimalwert überschreiben",
+ "dns_privacy": "DNS-Datenschutz",
+ "dns_private_reverse_servers_label": "Private Reverse-DNS-Server, einer pro Zeile",
+ "dns_private_reverse_servers_placeholder": "IP-Adressen, sdns://, quic://, https://, h3:// oder tls://",
+ "dns_private_reverse_servers_title": "Private inverse DNS-Server",
+ "dns_queries": "DNS-Anfragen",
+ "dns_queries_tooltip": "Die Anzahl der in den letzten 24 Stunden verarbeiteten DNS-Anfragen",
+ "dns_query": "DNS-Anfragen",
+ "dns_rate_limit": "Begrenzungswert",
+ "dns_rate_limit_allowlist": "Ratenbegrenzung: Zulassungsliste",
+ "dns_rate_limit_allowlist_placeholder": "IP-Adressen",
+ "dns_rate_limit_allowlist_title": "Ratenbegrenzung: Zulassungsliste",
+ "dns_rate_limit_placeholder": "Begrenzungswert eingeben",
+ "dns_rate_limit_title": "Begrenzungswert",
+ "dns_rewrites": "DNS-Umschreibungen",
+ "dns_server_addresses": "Serveradressen",
+ "dns_server_addresses_label": "Serveradressen (eine pro Zeile)",
+ "dns_server_addresses_placeholder": "IP-Adressen, sdns://, quic://, https://, h3:// oder tls://",
+ "dns_server_addresses_title": "Serveradressen",
+ "dns_server_config_title": "DNS-Serverkonfiguration",
+ "dns_server_configuration_saved_toast": "DNS-Server-Konfiguration gespeichert",
+ "dns_settings": "DNS",
+ "dns_test_upstreams": "Upstreams testen",
+ "dns_upstream_servers_title": "Upstream-DNS-Server",
+ "dns_upstream_timeout": "Upstream-Timeout",
+ "dns_upstream_timeout_title": "Upstream-Timeout",
+ "dns_upstream_validation_invalid": "Ungültiges Format",
+ "domain": "Domain",
+ "domain_or_client": "Domain oder Client",
+ "download_mobileconfig": "Konfigurationsdatei herunterladen",
+ "dynamic_dhcp_leases_not_found": "Dynamische DHCP-Leases nicht gefunden",
+ "edit_table_action": "Bearbeiten",
+ "enable": "Aktivieren",
+ "enabled_dhcp": "DHCP-Server aktiviert",
+ "encrypted_dns_addresses": "Verschlüsselte DNS-Serveradressen",
+ "encryption_certificate_path": "Zertifikatspfad",
+ "encryption_certificates": "Zertifikate",
+ "encryption_certificates_desc": "Um Verschlüsselung zu nutzen, musst du eine gültige SSL-Zertifikatskette für deine Domain angeben. Du kannst ein kostenloses SSL-Zertifikat von letsencrypt.org erhalten oder eines von einer vertrauenswürdigen Zertifizierungsstelle kaufen",
+ "encryption_certificates_input": "Kopieren Sie Ihre PEM-codierten Zertifikate und fügen Sie sie hier ein.",
+ "encryption_certificates_source_content": "Inhalt des Zertifikats einfügen",
+ "encryption_certificates_source_path": "Pfad für die Zertifikatsdatei festlegen",
+ "encryption_chain_invalid": "Zertifikatskette ist ungültig",
+ "encryption_chain_valid": "Zertifikatskette ist gültig",
+ "encryption_config_saved": "Verschlüsselungskonfiguration gespeichert",
+ "encryption_disable_plain_dns": "Unverschlüsseltes DNS deaktivieren?",
+ "encryption_disable_plain_dns_desc": "Geräte, die kein verschlüsseltes DNS unterstützen, funktionieren möglicherweise nicht ordnungsgemäß",
+ "encryption_doq": "Port für DNS-over-QUIC",
+ "encryption_doq_tooltip": "Dieser Port wird verwendet, um einen DNS-over-QUIC-Server zu betreiben",
+ "encryption_dot": "DNS-over-TLS",
+ "encryption_dot_tooltip": "Dieser Port wird verwendet, um einen DNS-over-TLS-Server zu betreiben",
+ "encryption_encrypted_dns": "Verschlüsseltes DNS",
+ "encryption_encrypted_dns_desc": "Die Verwaltungsoberfläche von AdGuard Home verwendet HTTPS. Der DNS-Server unterstützt DNS-over-HTTPS, DNS-over-TLS und DNS-over-QUIC",
+ "encryption_force_redirect": "Automatisch von HTTP auf HTTPS umleiten",
+ "encryption_https": "HTTPS-Port",
+ "encryption_key": "Privater Schlüssel",
+ "encryption_key_input": "Kopieren Sie Ihren PEM-codierten privaten Schlüssel für Ihr Zertifikat und fügen Sie ihn hier ein.",
+ "encryption_key_source_content": "Inhalt des privaten Schlüssels einfügen",
+ "encryption_plain_dns": "Einfaches DNS",
+ "encryption_plain_dns_error": "Um einfaches DNS zu deaktivieren, aktivieren Sie mindestens ein verschlüsseltes DNS-Protokoll",
+ "encryption_private_key_path": "Pfad des privaten Schlüssels",
+ "encryption_server": "Servername",
+ "encryption_server_enter": "Domain-Namen eingeben",
+ "encryption_server_tooltip_1": "Wenn diese Option aktiviert ist, erkennt AdGuard Home Client-IDs, antwortet auf DDR-Anfragen und führt zusätzliche Verbindungsprüfungen durch. Andernfalls sind diese Funktionen deaktiviert",
+ "encryption_server_tooltip_2": "Muss einem DNS-Namen im Zertifikat entsprechen",
+ "encryption_title": "Verschlüsselung",
+ "enter_cache_size": "Größe des Cache (Bytes) eingeben",
+ "enter_ip_address_placeholder": "IP-Adresse eingeben",
+ "error": "Fehler",
+ "fallback_dns": "Fallback-DNS-Server",
+ "filter_added_successfully": "Blockliste hinzugefügt: %value% ",
+ "filter_added_successfully_allowlist": "Zulassungsliste hinzugefügt: %value% ",
+ "filter_category_general": "Allgemein",
+ "filter_category_general_desc": "Listen, die Tracking und Werbung auf den meisten Geräten sperren",
+ "filter_category_other": "Sonstiges",
+ "filter_category_other_desc": "Weitere Sperrlisten",
+ "filter_category_regional": "Regional",
+ "filter_category_regional_desc": "Listen mit Fokus auf regionale Werbung und Tracking-Server",
+ "filter_category_security": "Sicherheit",
+ "filter_category_security_desc": "Listen, die speziell zum Blockieren von bösartigen, Phishing- und Betrugsdomains entwickelt wurden",
+ "filter_removed_successfully": "Blockliste entfernt: %value% ",
+ "filter_removed_successfully_allowlist": "Zulassungsliste entfernt: %value% ",
+ "filters": "Filter",
+ "forgot_password": "Passwort vergessen?",
+ "forgot_password_list_item_1": "Erstelle ein neues Passwort mit dem htpasswd-Tool oder einem Online-BCrypt-Generator.",
+ "forgot_password_list_item_2": "Stoppe AdGuard Home.",
+ "forgot_password_list_item_4": "Starte AdGuard Home wieder.",
+ "forgot_password_list_item_5": "Melde dich mit deinem neuen Passwort auf der Weboberfläche an.",
+ "forgot_password_list_title": "Um dein Passwort zu ändern:",
+ "forgot_password_page_desc": "AdGuard Home speichert Passwörter als BCrypt-verschlüsselte Hashes",
+ "form_enter_blocked_response_ttl": "TTL für blockierte Antworten eingeben (Sekunden)",
+ "form_error_answer_format": "Ungültiges Ergebnisformat",
+ "form_error_client_id_format": "Client-ID muss nur Zahlen, Kleinbuchstaben und Bindestriche enthalten",
+ "form_error_domain_format": "Ungültiges Domainformat",
+ "form_error_equal": "Sollten nicht übereinstimmen",
+ "form_error_format": "Ungültiges Format",
+ "form_error_gateway_ip": "Lease kann nicht die IP-Adresse des Gateways haben",
+ "form_error_ip4_format": "Ungültige IPv4-Adresse",
+ "form_error_ip6_format": "Ungültige IPv6-Adresse",
+ "form_error_ip_format": "Ungültige IP-Adresse",
+ "form_error_mac_format": "Ungültige MAC-Adresse",
+ "form_error_password": "Passwörter stimmen nicht überein",
+ "form_error_port": "Gültige Portnummer eingeben",
+ "form_error_port_range": "Geben Sie die Portnummer zwischen 80 und 65535 ein",
+ "form_error_port_unsafe": "Unsicherer Port",
+ "form_error_range": "Der Wert muss zwischen %min% und %max% liegen",
+ "form_error_server_name": "Ungültiger Servername",
+ "form_error_url_format": "Geben Sie eine gültige URL oder einen Dateipfad ein",
+ "found_in_known_domains": "In den bekannten Domains gefunden",
+ "friday": "Freitag",
+ "gateway_or_subnet_invalid": "Ungültige Subnetzmaske",
+ "general_statistics": "Allgemeine Statistiken",
+ "home_dns_addresses": "AdGuard Home DNS-Adressen",
+ "home_dns_addresses_desc": "Der AdGuard Home-DNS-Server lauscht auf den folgenden Schnittstellen/Ports:",
+ "ignore_domains_desc_log": "Anfragen, die diesen Regeln entsprechen, werden im Anfragenprotokoll ausgeblendet",
+ "ignore_domains_desc_stats": "Anfragen, die diesen Regeln entsprechen, werden nicht in die Statistik aufgenommen",
+ "ignore_domains_title": "Ignorierte Domains",
+ "inactivity_schedule": "Zeitplan für Inaktivität",
+ "inactivity_schedule_add": "Zeitplan hinzufügen",
+ "inactivity_schedule_all_day": "Ganzen Tag",
+ "inactivity_schedule_delete": "Zeitplan löschen",
+ "inactivity_schedule_desc": "Zeiten festlegen, in denen die Dienstblockierung inaktiv sein soll",
+ "inactivity_schedule_edit": "Zeitplan bearbeiten",
+ "inactivity_schedule_no_schedules": "Kein Zeitplan",
+ "inactivity_schedule_replace_desc": "Dieser Zeitplan ersetzt alle bestehenden Zeitpläne für denselben Wochentag. Jeder Tag kann nur einen Inaktivitätszeitraum haben",
+ "inactivity_schedule_time_from": "Zeit von",
+ "inactivity_schedule_time_to": "Zeit bis",
+ "inactivity_schedule_timezone": "Zeitzone",
+ "install_auth_confirm": "Passwort bestätigen",
+ "install_auth_password": "Passwort",
+ "install_auth_password_enter": "Passwort eingeben",
+ "install_auth_username": "Benutzername",
+ "install_auth_username_enter": "Benutzernamen eingeben",
+ "install_saved": "Erfolgreich gespeichert",
+ "install_settings_all_interfaces": "Alle Schnittstellen",
+ "install_settings_port": "Port",
+ "install_static_error": "AdGuard Home kann nicht automatisch für diese Netzwerkschnittstelle konfiguriert werden. Bitte suchen Sie nach einer Anleitung, wie Sie dies manuell durchführen können.",
+ "install_static_ok": "Gute Nachrichten! Die feste IP-Adresse ist bereits konfiguriert",
+ "install_step": "Schritt",
+ "install_submit_title": "Gratulation!",
+ "instructions": "Anleitungen",
+ "ip_address": "IP-Adresse",
+ "ip_addresses_placeholder": "IP-Adressen eingeben, eine pro Zeile",
+ "known_tracker": "Bekannte Tracker",
+ "language": "Sprache",
+ "last_days": "Letzter %count% Tag | Letzte %count% Tage",
+ "last_hour": "Letzte Stunde",
+ "last_hours": "| Letzte %count% Stunde | Letzte %count% Stunden",
+ "last_updated_label": "Zuletzt aktualisiert",
+ "light_theme": "Helles Design",
+ "login": "Anmelden",
+ "logout": "Abmelden",
+ "make_static": "Statisch machen",
+ "milliseconds_abbreviation": "ms",
+ "monday": "Montag",
+ "name": "Name",
+ "name_label": "Name",
+ "name_table_header": "Name",
+ "name_tooltip": "Name",
+ "network_interface": "Netzwerkschnittstelle",
+ "next": "Weiter",
+ "no": "Nein",
+ "none_text": "Keine",
+ "nothing_found": "Nichts gefunden",
+ "notify_undo": "Rückgängig",
+ "notify_updated": "Aktualisiert",
+ "open_dashboard": "Dashboard öffnen",
+ "parental_control": "Kindersicherung",
+ "parental_group_ai": "KI",
+ "parental_group_cdn": "CDN",
+ "parental_group_dating": "Dating",
+ "parental_group_development": "Softwareentwicklung",
+ "parental_group_gambling": "Glücksspiele und Wetten",
+ "parental_group_gaming": "Spiele",
+ "parental_group_hosting": "Webhosting",
+ "parental_group_messaging": "Messaging",
+ "parental_group_messenger": "Messaging",
+ "parental_group_privacy": "Datenschutz-Tools",
+ "parental_group_shopping": "Einkaufen",
+ "parental_group_social": "Soziale Netzwerke",
+ "parental_group_social_network": "Soziale Netzwerke",
+ "parental_group_software": "Softwareentwicklung",
+ "parental_group_streaming": "Streaming",
+ "password_label": "Passwort",
+ "password_placeholder": "Passwort eingeben",
+ "password_requirements": "Passwortanforderungen",
+ "password_requirements_characters": "Mindestens 8 Zeichen",
+ "password_requirements_lowercase": "Mindestens 1 Kleinbuchstabe",
+ "password_requirements_match": "Passwörter stimmen überein",
+ "password_requirements_special": "Englische Buchstaben, Zahlen und Sonderzeichen",
+ "password_requirements_uppercase": "Mindestens 1 Großbuchstabe",
+ "pause_for_hour": "Pause für %count% Stunde",
+ "pause_until_tomorrow": "Pause bis morgen, %time%",
+ "period_notify": "Um die angezeigten Zeiträume zu ändern, bearbeite die Aufbewahrung der Statistiken in den Allgemeinen Einstellungen ",
+ "plain_dns": "Einfaches DNS",
+ "plain_dns_addresses": "Unverschlüsselte DNS-Serveradressen",
+ "port_in_use": "Dieser Port wird bereits verwendet",
+ "privacy_policy": "Datenschutzerklärung",
+ "processing_time_ms": "%value% ms",
+ "protection": "Schutz",
+ "protocol": "Protokoll",
+ "queries": "Anfragen",
+ "query_details": "Anfragedetails",
+ "query_log_all_reasons": "Alle Gründe",
+ "query_log_allowed": "Auf Freigabeliste",
+ "query_log_blocked": "Blockiert",
+ "query_log_blocked_by_filter": "Durch Filter blockiert",
+ "query_log_blocked_by_parental_control": "Durch Kindersicherung blockiert",
+ "query_log_blocked_services": "Gesperrte Dienste",
+ "query_log_blocked_threats": "Gesperrte Bedrohungen",
+ "query_log_custom_filtering_rules": "Benutzerdefinierte Filterregeln",
+ "query_log_detail_address": "Adresse: %value% ",
+ "query_log_detail_category": "Kategorie: %value% ",
+ "query_log_detail_country": "Land: %value% ",
+ "query_log_detail_date": "Datum: %value% ",
+ "query_log_detail_dns_server": "DNS-Server: %value% ",
+ "query_log_detail_domain": "Domain: %value% ",
+ "query_log_detail_ecs": "ECS: %value% ",
+ "query_log_detail_elapsed": "Dauer: %value% ",
+ "query_log_detail_name": "Name: %value% ",
+ "query_log_detail_network": "Netzwerk: %value% ",
+ "query_log_detail_original_response": "Ursprüngliche Antwort:",
+ "query_log_detail_protocol": "Protokoll: %value% ",
+ "query_log_detail_reason": "Grund:",
+ "query_log_detail_response": "Antwort:",
+ "query_log_detail_response_code": "Antwortcode: %value% ",
+ "query_log_detail_service_name": "Dienstname: %value% ",
+ "query_log_detail_status": "Status: %value% ",
+ "query_log_detail_time": "Zeit: %value% ",
+ "query_log_detail_type": "Typ: %value% ",
+ "query_log_nothing_available": "Nichts gefunden",
+ "query_log_processed": "Verarbeitet",
+ "query_log_retention": "Protokollrotation",
+ "query_log_rewritten": "Umgeschrieben",
+ "query_log_safe_search": "Sichere Suche",
+ "query_log_strict_search": "Doppelte Anführungszeichen für die strikte Suche verwenden",
+ "rate_limit_subnet_len_ipv4_error": "Die Subnetzpräfixlänge für IPv4-Adressen sollte zwischen 0 und 32 liegen",
+ "rate_limit_subnet_len_ipv6_error": "Die Subnetzpräfixlänge für IPv6-Adressen sollte zwischen 0 und 128 liegen",
+ "reason_table_header": "Grund",
+ "refresh_btn": "Aktualisieren",
+ "refresh_statics": "Statistiken aktualisieren",
+ "release_notes": "Versionshinweise",
+ "remove": "Entfernen",
+ "remove_tag": "%value% entfernen",
+ "report_an_issue": "Problem melden",
+ "request_table_header": "Anfrage",
+ "requests_table_header": "Anfragen",
+ "reset": "Zurücksetzen",
+ "reset_settings": "Einstellungen zurücksetzen",
+ "reset_settings_confirm": "Ja, zurücksetzen",
+ "response_details": "Einzelheiten der Antwort",
+ "response_time": "Antwortzeit",
+ "result": "Status",
+ "resume_protection_timer": "Schutz wird in %time% fortgesetzt",
+ "rewrite_add": "DNS-Umschreibung hinzufügen",
+ "rewrite_domain_input_placeholder": "Domainname oder Platzhalter eingeben",
+ "rewrite_edit": "DNS-Rewrite bearbeiten",
+ "rewrite_hosts_applied": "Von Hostdatei-Regel umgeschrieben",
+ "rewrite_settings_updated": "DNS-Rewrite-Einstellungen erfolgreich aktualisiert",
+ "rewrite_updated": "DNS-Rewrite erfolgreich aktualisiert",
+ "rewrites_answer_input_placeholder": "IP-Adresse, CNAME-Eintrag, A oder AAAA eingeben",
+ "rewrites_empty": "Noch keine Rewrites. Hinzufügen ",
+ "rewrites_remove_desc": "Der DNS-Rewrite für %value% wird entfernt",
+ "rewrites_remove_title": "DNS-Rewrite entfernen?",
+ "rewrites_tooltip_examples_item1": "DNS-Antworten nur für diesen Domainnamen umschreiben",
+ "rewrites_tooltip_examples_item2": "DNS-Antworten für alle Subdomains von example.org umschreiben",
+ "rewrites_tooltip_instructions_item1": "Um die Domain in eine IP-Adresse aufzulösen, gib eine IPv4-Adresse für A-Records und eine IPv6-Adresse für AAAA-Records ein",
+ "rewrites_tooltip_instructions_item2": "Um die Domain weiterzuleiten, gib den CNAME-Eintrag der Ziel-Domain ein",
+ "rewrites_tooltip_instructions_item3": "Um nur A-Einträge zu behalten, gib ein",
+ "rewrites_tooltip_instructions_item4": "Um nur AAAA-Einträge zu behalten, gib ein",
+ "rewritten": "Umgeschrieben",
+ "rows_per_page": "Zeilen pro Seite %value%",
+ "rules_label": "Regeln",
+ "safe_browsing": "Internetsicherheit",
+ "safe_search": "Sichere Suche",
+ "safe_search_used": "Sichere Suche verwendet",
+ "safe_search_used_tooltip": "Anzahl der DNS-Anfragen an Suchmaschinen, bei denen Sichere Suche erzwungen wurde",
+ "saturday": "Samstag",
+ "save": "Speichern",
+ "save_btn": "Speichern",
+ "search_placeholder": "Suchen",
+ "server_config_blocking_mode": "Sperrmodus",
+ "server_config_blocking_mode_custom_ip_desc": "Benutzerdefinierte IP: Mit einer manuell eingestellten IP-Adresse antworten",
+ "server_config_blocking_mode_ipv4": "IPv4-Sperren",
+ "server_config_blocking_mode_ipv4_faq": "IP-Adresse, die für eine blockierte A-Anfrage zurückgegeben wird",
+ "server_config_blocking_mode_ipv4_placeholder": "IPv4-Adresse eingeben",
+ "server_config_blocking_mode_ipv6": "IPv6-Sperren",
+ "server_config_blocking_mode_ipv6_faq": "IP-Adresse, die für eine blockierte AAAA-Anfrage zurückgegeben wird",
+ "server_config_blocking_mode_ipv6_placeholder": "IPv6-Adresse eingeben",
+ "server_config_blocking_mode_null_ip_desc": "Null-IP-Adresse: Mit 0.0.0.0 für A und :: für AAAA antworten",
+ "server_config_blocking_mode_nxdomain_desc": "NXDOMAIN: Mit NXDOMAIN-Code antworten",
+ "server_config_blocking_mode_refused_desc": "REFUSED: Mit REFUSED-Code antworten",
+ "server_config_blocking_mode_ttl": "TTL für blockierte Antworten (in Sekunden)",
+ "server_config_blocking_mode_ttl_faq": "Legt fest, wie lange Clients eine gefilterte Antwort zwischenspeichern sollten",
+ "server_config_custom_ip": "Benutzerdefinierte IP-Adresse",
+ "server_config_default": "Standard",
+ "server_config_disable_ipv6": "IPv6 deaktivieren",
+ "server_config_dnssec_enable": "DNSSEC aktivieren",
+ "server_config_edns_enable": "EDNS-Client-Subnetz aktivieren",
+ "server_config_null_ip": "Null-IP-Adresse",
+ "server_config_nxdomain": "NXDomain",
+ "server_config_rate_limit": "Ratenbegrenzung, RPS",
+ "server_config_rate_limit_placeholder": "Begrenzungswert eingeben",
+ "server_config_rate_limit_whitelist": "Ratenbegrenzung: Zulassungsliste",
+ "server_config_rate_limit_whitelist_faq": "IP-Adressen, die von der Ratebegrenzung ausgeschlossen sind",
+ "server_config_refused": "REFUSED",
+ "server_config_subnet_len_ipv4": "Länge des Subnetzpräfixes für IPv4-Adressen",
+ "server_config_subnet_len_ipv4_faq": "Subnetpräfixlänge für IPv4-Adressen, die für die Ratebegrenzung verwendet werden. Der Standardwert ist 24",
+ "server_config_subnet_len_ipv6": "Subnetzpräfixlänge für IPv6-Adressen",
+ "server_config_subnet_len_ipv6_faq": "Subnetpräfixlänge für IPv6-Adressen, die für die Ratebegrenzung verwendet werden. Der Standardwert ist 56",
+ "set_static_ip": "Feste IP-Adresse festlegen",
+ "settings": "Einstellungen",
+ "settings_anonymize_client_ip": "Client-IP-Adressen anonymisieren",
+ "settings_anonymize_client_ip_desc": "Einen Teil der IP-Adresse durch Nullen ersetzen",
+ "settings_browsing_security": "Internetsicherheit",
+ "settings_confirm_clear_query_log": "Anfragenprotokoll leeren?",
+ "settings_confirm_clear_query_log_desc": "Dadurch werden alle Anfragen aus dem Anfragenprotokoll gelöscht. Die Statistiken bleiben weiterhin verfügbar",
+ "settings_confirm_clear_statistics": "Statistiken leeren?",
+ "settings_confirm_decrease_log_rotation_interval": "Intervall für die Protokollrotation verringern?",
+ "settings_confirm_decrease_log_rotation_interval_desc": "Dadurch werden alle Protokolle gelöscht, die älter als %value% sind",
+ "settings_confirm_decrease_stats_rotation_interval": "Intervall für die Statistik-Vorhaltezeit verringern?",
+ "settings_confirm_decrease_stats_rotation_interval_desc": "Dadurch werden alle Statistiken gelöscht, die älter als %value% sind",
+ "settings_custom": "Benutzerdefiniert",
+ "settings_days": "| %count% Tag | %count% Tage",
+ "settings_domain_names": "Domainnamen, einer pro Zeile",
+ "settings_filter_requests": "Anfragen filtern",
+ "settings_filtering_and_security": "Filterung und Sicherheit",
+ "settings_general_short": "Allgemein",
+ "settings_global": "Allgemein",
+ "settings_hours": "| %count% Stunde | %count% Stunden",
+ "settings_log_dns_requests": "DNS-Anfragen protokollieren",
+ "settings_log_rotation_hours": "Protokollrotation (in Stunden)",
+ "settings_notify_changes_saved": "Änderungen gespeichert",
+ "settings_notify_query_log_cleared": "Anfragenprotokoll geleert",
+ "settings_notify_statistics_cleared": "Statistiken gelöscht",
+ "settings_parental_control": "Kindersicherung",
+ "settings_rotation_placeholder": "Zahlen verwenden",
+ "settings_rule_syntax": "Regelsyntax",
+ "settings_safe_search": "Sichere Suche",
+ "settings_safe_search_desc": "Unangemessene Inhalte in Suchergebnissen blockieren",
+ "settings_statistics": "Statistiken",
+ "settings_statistics_clear": "Statistiken zurücksetzen",
+ "settings_statistics_desc": "Anfragen- und Client-Statistiken im Dashboard anzeigen",
+ "settings_statistics_retention": "Aufbewahrung der Statistiken",
+ "settings_statistics_retention_hours": "Aufbewahrung der Statistiken (in Stunden)",
+ "settings_tooltip_domain_names": "Domainnamen, Platzhalter oder Filterregeln",
+ "settings_tooltip_examples": "Beispiele:",
+ "settings_yes_clear": "Ja, bitte",
+ "settings_yes_decrease": "Ja, verringern",
+ "setup_complete": "Einrichtung abgeschlossen. AdGuard Home ist startklar",
+ "setup_devices_android_list_1": "Öffne die Einstellungen deines Geräts und wähleWLAN .",
+ "setup_devices_android_list_2": "Halte dein aktuelles Netzwerk gedrückt und wähle dann Netzwerk bearbeiten . Für mobile Internetverbindungen kann kein benutzerdefinierter DNS festgelegt werden.",
+ "setup_devices_android_list_3": "Auf einigen Geräten musst du eventuell das Kontrollkästchen Erweitert aktivieren, um mehr Einstellungen zu sehen. Um deine Android-DNS-Einstellungen anzupassen, ändere die IP-Einstellungen von DHCP auf Statisch .",
+ "setup_devices_android_list_4": "Trage für DNS 1 und DNS 2 die Adressen deines AdGuard Home-Servers ein.",
+ "setup_devices_android_title": "Android",
+ "setup_devices_dns_android_list_1": "Android 9 unterstützt DNS-over-TLS nativ. Um es zu konfigurieren, gehe zuEinstellungen → Netzwerk & Internet → Erweitert → Privates DNS und gib deinen Domainnamen ein.",
+ "setup_devices_dns_android_list_2": "AdGuard für Android unterstützt die Einrichtung von DNS-over-HTTPS , DNS-over-TLS und DNS-over-QUIC .",
+ "setup_devices_dns_ios_list_1": "AdGuard für iOS unterstützt die Einrichtung von DNS-over-HTTPS , DNS-over-TLS und DNS-over-QUIC .",
+ "setup_devices_dns_ios_list_2": "DNSCloak unterstützt DNS-over-HTTPS . Um die App mit deinem eigenen Server zu konfigurieren, musst du vorab einen DNS Stamp dafür generieren.",
+ "setup_devices_dns_list_2": "Für DNS-over-HTTPS nutze ",
+ "setup_devices_dns_list_3": "Für DNS-over-QUIC nutze ",
+ "setup_devices_dns_list_devices": "Hier ist eine Liste von Software, die du verwenden kannst.",
+ "setup_devices_dns_macos_desc": "Unter iOS 14 oder macOS Big Sur kannst du eine spezielle mobileconfig-Datei herunterladen, um DNS-over-HTTPS - oder DNS-over-TLS -Server in den DNS-Einstellungen zu ergänzen.",
+ "setup_devices_dns_other_list_1": "AdGuard Home selbst kann auf jeder Plattform als sicherer DNS-Client fungieren.",
+ "setup_devices_dns_other_list_2": "dnsproxy unterstützt alle bekannten sicheren DNS-Protokolle.",
+ "setup_devices_dns_other_list_3": "dnscrypt-proxy unterstützt DNS-over-HTTPS .",
+ "setup_devices_dns_other_list_4": "Mozilla Firefox unterstützt DNS-over-HTTPS .",
+ "setup_devices_dns_other_list_5": "Weitere Implementierungen findest du unter DNSCrypt und DNS privacy .",
+ "setup_devices_ios_list_1": "Öffne die Einstellungen -App deines Geräts und wähle Wi-Fi . Für mobile Internetverbindungen kann kein benutzerdefinierter DNS festgelegt werden.",
+ "setup_devices_ios_list_2": "Tippe auf den Namen des aktuell aktiven Netzwerks.",
+ "setup_devices_ios_list_3": "Gehe zu DNS → DNS konfigurieren → Manuell und füge die Adressen deines AdGuard Home-Servers hinzu.",
+ "setup_devices_ios_list_4": "Tippe auf Speichern .",
+ "setup_devices_ios_title": "iOS",
+ "setup_devices_macos_list_1": "Klicke auf das Apple-Menü → Systemeinstellungen → Netzwerk .",
+ "setup_devices_macos_list_2": "Wähle die gewünschte Verbindung und klicke auf Weitere Optionen .",
+ "setup_devices_macos_list_3": "Wähle den Tab DNS und gib die Adressen deines AdGuard Home-Servers ein.",
+ "setup_devices_macos_title": "macOS",
+ "setup_devices_router_desc_1": "Richte AdGuard Home auf deinem Heimnetzwerk-Router ein und schütze alle damit verbundenen Geräte. Eine manuelle Konfiguration jedes einzelnen Geräts ist nicht nötig.",
+ "setup_devices_router_desc_2": "Manche Router erlauben das Einrichten eines eigenen DNS-Servers nicht. Du kannst dann versuchen, AdGuard Home als DHCP-Server einzurichten. Klappt das auch nicht, wirf bitte einen Blick in das Handbuch deines Routers, um zu sehen, wie du den DNS-Server für dein Modell änderst.",
+ "setup_devices_router_list_1": "Meistens kannst du über deinen Browser per URL darauf zugreifen, zum Beispiel unter http://192.168.0.1 oder http://192.168.1.1 . Eventuell wirst du nach einem Passwort gefragt. Oft lässt sich dieses über eine Taste direkt am Router zurücksetzen — beachte aber, dass du dabei deine gesamte Router-Konfiguration verlieren kannst. Falls dein Router über eine App eingerichtet wird, installiere diese auf deinem Smartphone oder PC und greife darüber auf die Einstellungen zu.",
+ "setup_devices_router_list_1_title": "Öffne die Einstellungen deines Routers.",
+ "setup_devices_router_list_2": "Um die Einstellungen zu finden, suche nach „DNS“ neben einem Feld, das zwei oder drei Zahlenreihen erlaubt — jeweils unterteilt in vier Gruppen aus ein bis drei Ziffern.",
+ "setup_devices_router_list_2_title": "Trage in den DHCP/DNS-Einstellungen die Adressen deines AdGuard Home-Servers ein.",
+ "setup_devices_router_title": "Router",
+ "setup_devices_windows_list_1": "Öffne Systemsteuerung über das Startmenü oder die Windows-Suche .",
+ "setup_devices_windows_list_2": "Klicke auf Netzwerk und Internet → Netzwerk- und Freigabecenter → Adaptereinstellungen ändern .",
+ "setup_devices_windows_list_3": "Mache einen Rechtsklick auf deine aktive Verbindung und wähle Eigenschaften .",
+ "setup_devices_windows_list_4": "Wähle in der Liste Internetprotokoll, Version 4 (TCP/IPv4) für IPv4 oder Internetprotokoll, Version 6 (TCP/IPv6) für IPv6.",
+ "setup_devices_windows_list_5": "Klicke auf Eigenschaften → Folgende DNS-Serveradressen verwenden .",
+ "setup_devices_windows_list_6": "Gib die Adressen deines AdGuard Home-Servers ein.",
+ "setup_devices_windows_title": "Windows",
+ "setup_dns_desc": "Konfiguriere deinen Router oder andere Geräte so, dass sie die folgenden DNS-Serveradressen verwenden:",
+ "setup_dns_privacy_ioc_mac": "Konfiguration für iOS und macOS",
+ "setup_dns_privacy_other_title": "Weitere Implementierungen",
+ "setup_dns_quote_desc": "Damit alles richtig funktioniert, braucht der AdGuard Home DNS-Server eine statische IP-Adresse in deinem lokalen Netzwerk. Andernfalls weist dein Router ihm später vielleicht eine andere IP-Adresse zu, was den DNS-Schutz stört",
+ "setup_dns_quote_title": "Eine statische IP-Adresse für den AdGuard Home DNS-Server in deinem lokalen Netzwerk zuweisen",
+ "setup_dns_title": "Die DNS-Serveradresse für AdGuard Home festlegen",
+ "setup_dns_title_banner": "DNS-Servereinstellungen",
+ "setup_guide": "Leitfaden zur Einrichtung",
+ "setup_guide_auth_privacy": "Ich akzeptiere die Datenschutzerklärung und die Allgemeinen Geschäftsbedingungen der AdGuard-Websites",
+ "setup_guide_auth_subtitle": "Dies ist erforderlich, um das AdGuard Home-Admin-Panel vor unbefugtem Zugriff zu schützen",
+ "setup_guide_auth_title": "Passwortschutz einrichten",
+ "setup_guide_desc": "Richte deine Geräte ein, um AdGuard Home zu nutzen",
+ "setup_guide_device_type": "Gerätetyp auswählen",
+ "setup_guide_greeting_button": "Starten",
+ "setup_guide_greeting_desc": "Dieser Assistent hilft dir, AdGuard Home einzurichten:",
+ "setup_guide_greeting_list_1": "Zugangsdaten für den Zugriff auf das Admin-Panel erstellen",
+ "setup_guide_greeting_list_2": "Die Web-UI-Adresse für AdGuard Home festlegen",
+ "setup_guide_greeting_list_3": "Die DNS-Serveradresse für AdGuard Home festlegen",
+ "setup_guide_greeting_list_4": "Anleitungen für deine einzelnen Geräte abrufen",
+ "setup_guide_greeting_list_5": "Zugriff auf das AdGuard Home-Dashboard erhalten",
+ "setup_guide_greeting_title": "AdGuard Home einrichten",
+ "setup_guide_title": "Geräte für AdGuard Home konfigurieren",
+ "setup_ui_title": "Die Web-UI-Adresse für AdGuard Home festlegen",
+ "setup_ui_title_banner": "Web-UI-Einstellungen",
+ "show_blocked_responses": "Blockiert",
+ "show_processed_responses": "Verarbeitet",
+ "sort_asc": "Sortieren: A–Z",
+ "sort_desc": "Sortieren: Z–A",
+ "source_label": "Quelle",
+ "source_tooltip": "Quelle",
+ "static_dhcp_leases_not_found": "Statische DHCP-Leases nicht gefunden",
+ "stats_adult": "Gesperrte jugendgefährdende Websites",
+ "stats_query_domain": "Am häufigsten angefragte Domains",
+ "status_table_header": "Status",
+ "sunday": "Sonntag",
+ "system_host_files": "Hosts-Datei des Systems",
+ "system_theme": "Systemdesign",
+ "table_client": "Client",
+ "tags_title": "Schlagwörter",
+ "test_upstreams": "Upstreams testen",
+ "threats_blocked": "Gesperrte Bedrohungen",
+ "threats_blocked_tooltip": "Anzahl der DNS-Anfragen, die vom AdGuard-Modul „Internetsicherheit“ blockiert wurden",
+ "thursday": "Donnerstag",
+ "time_table_header": "Zeit",
+ "top_blocked_domains": "Am häufigsten gesperrte Domains",
+ "top_clients": "Top Clients",
+ "top_upstreams": "Top Upstreams",
+ "total": "Insgesamt",
+ "try_again": "Erneut versuchen",
+ "tuesday": "Dienstag",
+ "unavailable_dhcp": "DHCP ist nicht verfügbar",
+ "unavailable_dhcp_desc": "AdGuard Home konnte keinen DHCP-Server auf Ihrem Betriebssystem ausführen",
+ "unblock": "Entsperren",
+ "unblock_client": "Client entsperren",
+ "unknown_filter": "Unbekannter Filter %filterId%",
+ "update_button": "Aktualisieren",
+ "update_filters_custom_hours": "Eigenes Intervall (in Stunden)",
+ "update_filters_disable": "Deaktivieren",
+ "update_filters_interval_custom": "Benutzerdefiniert",
+ "update_filters_interval_daily": "Täglich",
+ "update_filters_interval_hourly": "Jede Stunde",
+ "update_filters_interval_weekly": "Jede Woche",
+ "update_filters_title": "Filter automatisch aktualisieren",
+ "updated_upstream_dns_toast": "Upstream-Server gespeichert",
+ "updates_checked": "Neue Version von AdGuard Home ist jetzt verfügbar",
+ "updates_version_equal": "AdGuard Home ist aktuell",
+ "upstream": "Upstream",
+ "upstream_bootstrap_dns_faq": "IP-Adressen von DNS-Servern, die zur Auflösung der IP-Adressen von DoH/DoT-Upstream-Resolvern verwendet werden. Kommentare sind nicht erlaubt",
+ "upstream_bootstrap_dns_title": "Bootstrap-DNS-Server, einer pro Zeile",
+ "upstream_dns": "Upstream-DNS-Server",
+ "upstream_dns_addresses": "Serveradressen (eine pro Zeile)",
+ "upstream_dns_configured_in_file": "Konfiguriert in %path%",
+ "upstream_dns_fastest_addr": "Schnellste IP-Adresse",
+ "upstream_dns_parallel_requests": "Paralleles Abfragen",
+ "upstream_dns_placeholder": "IP-Adressen, sdns://, quic://, https://, h3:// oder tls://",
+ "upstream_dns_servers_title": "Upstream-DNS-Server",
+ "upstream_enable_reverse_lookup_desc": "Client-IP-Adressen durch das Senden von PTR-Anfragen an die entsprechenden Resolver rückwärts in ihre Hostnamen auflösen: private DNS-Server für lokale Clients oder Upstream-Server für Clients mit öffentlichen IP-Adressen",
+ "upstream_enable_reverse_lookup_title": "Reverse-Lookup von Client-IP-Adressen aktivieren",
+ "upstream_example_upstream_comment": "%value%: ein Kommentar.",
+ "upstream_examples_title": "Beispiele",
+ "upstream_fallback_title": "Fallback-DNS-Server, einer pro Zeile",
+ "upstream_fallback_title_faq": "Fallback-DNS-Server, die verwendet werden, wenn Upstream-DNS-Server nicht verfügbar sind. Verwende dieselbe Syntax wie für die Upstream-DNS-Server",
+ "upstream_private_ptr_resolvers_desc": "PTR-, SOA- und NS-Anfragen für ARPA-Domains mit privaten IP-Adressen über private Upstream-Server, DHCP, /etc/hosts usw. auflösen. Wenn diese Option deaktiviert ist, antwortet AdGuard Home auf alle diese Anfragen mit NXDOMAIN",
+ "upstream_private_ptr_resolvers_title": "Private Reverse-DNS-Resolver verwenden",
+ "upstream_ptr": "Private Reverse-DNS-Server, einer pro Zeile",
+ "upstream_ptr_faq_1": "AdGuard Home verwendet diese DNS-Server für private PTR-, SOA- und NS-Anfragen. Private Anfragen stammen von Clients mit einer privaten IP-Adresse und fordern eine ARPA-Domain an, die ein Subnetz aus einem privaten IP-Adressbereich enthält, wie zum Beispiel %value%",
+ "upstream_ptr_faq_2": "Wenn dieses Feld leer ist, werden die Standard-DNS-Resolver deines Betriebssystems verwendet, mit Ausnahme der AdGuard Home IP-Adressen",
+ "upstream_ptr_faq_3": "Standardmäßig verwendet AdGuard Home die folgenden Reverse-DNS-Resolver: %value_1% und %value_2%",
+ "upstream_timeout": "Upstream-Timeout (in Sekunden)",
+ "upstream_timeout_faq": "Wartezeit auf eine Antwort vom Upstream-Server",
+ "upstreams": "Upstreams",
+ "url_label": "URL",
+ "user_rules": "Benutzerregeln",
+ "user_rules_add_to_allowlist": "Zur Positivliste hinzufügen",
+ "user_rules_allow_service": "Service zulassen",
+ "user_rules_check_button": "Prüfen",
+ "user_rules_check_client_label": "Client-Kennung (Name, Client-ID oder IP-Adresse)",
+ "user_rules_check_client_placeholder": "Client-Kennung eingeben",
+ "user_rules_check_dns_record_type_label": "DNS-Eintragstyp",
+ "user_rules_check_hostname_label": "Hostname oder Domainname",
+ "user_rules_check_hostname_placeholder": "Ziel eingeben",
+ "user_rules_cname": "CNAME: %cname%",
+ "user_rules_disable_filter": "Filter deaktivieren",
+ "user_rules_disable_parental_control": "Kindersicherung deaktivieren",
+ "user_rules_dns_record_type_placeholder": "DNS-Datensatztyp auswählen",
+ "user_rules_dns_rewrite_removed": "Regel aus DNS-Rewrite entfernt",
+ "user_rules_domain": "Domain: %value%",
+ "user_rules_domain_blocked": "Domain ist gesperrt",
+ "user_rules_domain_is_allowed": "Domain zugelassen",
+ "user_rules_edit_dns_rewrite": "DNS-Rewrite bearbeiten",
+ "user_rules_example_comment": "! Das ist ein Kommentar Um zu kommentieren, beginne die Zeile mit ! oder #",
+ "user_rules_example_comment_2": "# Das ist auch ein Kommentar Um zu kommentieren, beginne die Zeile mit ! oder #",
+ "user_rules_ip": "IP-Adressen: %ip%",
+ "user_rules_parental_control_disabled": "Kindersicherung deaktiviert",
+ "user_rules_placeholder": "Dies ist ein Platzhalter",
+ "user_rules_reason": "Grund: %reason%",
+ "user_rules_reason_blocked_by": "Blockiert durch %source%",
+ "user_rules_redirected_to": "Weitergeleitet zu: %value%",
+ "user_rules_rewrite_rule_is_applied": "Umschreibungsregel ist angewendet",
+ "user_rules_rewritten_to": "Umgeschrieben zu: %value%",
+ "user_rules_rule": "Regel: %rule%",
+ "user_rules_rule_added_to_custom_filtering_rules": "Regel zu benutzerdefinierten Filterregeln hinzugefügt",
+ "user_rules_service": "Service: %service%",
+ "user_rules_service_allowed": "Der Dienst %value% zugelassen",
+ "user_rules_source": "Quelle: %value%",
+ "user_rules_source_allowlist": "Zulassungsliste: %value%",
+ "user_rules_source_blocklist": "Blockliste: %value%",
+ "user_rules_status": "Status: %reason%",
+ "username_label": "Benutzername",
+ "username_placeholder": "Benutzernamen eingeben",
+ "validated_with_dnssec": "Bestätigt mit DNSSEC",
+ "wednesday": "Mittwoch",
+ "whois": "Whois",
+ "yes": "Ja",
+ "yes_disable": "Stoppen",
+ "yes_reset": "Ja, zurücksetzen"
+}
diff --git a/client_v2/src/__locales/en.json b/client_v2/src/__locales/en.json
new file mode 100644
index 00000000000..68dd4cf92cb
--- /dev/null
+++ b/client_v2/src/__locales/en.json
@@ -0,0 +1,1019 @@
+{
+ "language": "Language",
+ "client_settings": "Client settings",
+ "bootstrap_dns": "Bootstrap DNS servers",
+ "fallback_dns": "Fallback DNS servers",
+ "form_error_ip4_format": "Invalid IPv4 address",
+ "form_error_ip6_format": "Invalid IPv6 address",
+ "form_error_ip_format": "Invalid IP address",
+ "form_error_mac_format": "Invalid MAC address",
+ "form_error_client_id_format": "ClientID must contain only numbers, lowercase letters, and hyphens",
+ "form_error_server_name": "Invalid server name",
+ "form_error_subnet": "The IP address should be in the subnet %cidr%",
+ "form_error_gateway_ip": "Lease can't have the IP address of the gateway",
+ "out_of_range_error": "The address must not be within the DHCP range %start%–%end%",
+ "greater_range_start_error": "The end of the range must be greater than the start",
+ "subnet_error": "Range addresses must be in the same subnet",
+ "gateway_or_subnet_invalid": "Invalid subnet mask",
+ "response_details": "Response details",
+ "client_details": "Client details",
+ "back": "Back",
+ "dashboard": "Dashboard",
+ "settings": "Settings",
+ "filters": "Filters",
+ "nothing_found": "Nothing found",
+ "protocol": "Protocol",
+ "report_an_issue": "Report a problem",
+ "refresh_statics": "Refresh statistics",
+ "dns_query": "DNS queries",
+ "stats_adult": "Blocked adult websites",
+ "stats_query_domain": "Top queried domains",
+ "top_blocked_domains": "Top blocked domains",
+ "top_clients": "Top clients",
+ "general_statistics": "General statistics",
+ "top_upstreams": "Top upstreams",
+ "average_upstream_response_time": "Average upstream response time",
+ "response_time": "Response time",
+ "custom_filtering_rules": "Custom filtering rules",
+ "upstream_dns_configured_in_file": "Configured in %path%",
+ "upstreams": "Upstreams",
+ "upstream": "Upstream",
+ "name_table_header": "Name",
+ "request_table_header": "Request",
+ "add_blocklist": "Add blocklist",
+ "add_allowlist": "Add allowlist",
+ "check_updates_btn": "Check for updates",
+ "form_error_url_format": "Enter a valid URL or file path",
+ "form_error_format": "Invalid format",
+ "form_error_format_line": "Invalid format on line %line%",
+ "form_error_format_lines": "Invalid format on lines %lines%",
+ "custom_rules": "Custom rules",
+ "system_host_files": "System hosts files",
+ "all_lists_up_to_date_toast": "All lists are already up-to-date",
+ "updated_upstream_dns_toast": "Upstream servers saved",
+ "dns_server_configuration_saved_toast": "DNS server configuration saved",
+ "dns_cache_configuration_saved_toast": "DNS cache configuration saved",
+ "access_settings_saved_toast": "Access settings saved",
+ "dns_test_ok_toast": "All upstreams are operational",
+ "dns_test_not_ok_toast": "Upstream %key% passed syntax check but failed the test. Verify the upstream settings",
+ "dns_test_parsing_error_toast": "Failed to extract upstream name from line %number% in the %section% section. Check the line format",
+ "dns_test_warning_toast": "Upstream %key% did not respond to the DNS query. Verify the upstream settings",
+ "unblock": "Unblock",
+ "block": "Block",
+ "disallow_this_client": "Disallow this client",
+ "block_for_this_client_only": "Block for this client only",
+ "add_persistent_client": "Add as persistent client",
+ "time_table_header": "Time",
+ "client_table_header": "Client",
+ "query_log_retention": "Log rotation",
+ "dns_config": "DNS server configuration",
+ "dns_cache_config": "DNS cache configuration",
+ "form_enter_blocked_response_ttl": "Enter blocked response TTL (seconds)",
+ "dns_over_https": "DNS-over-HTTPS",
+ "dns_over_tls": "DNS-over-TLS",
+ "dns_over_quic": "DNS-over-QUIC",
+ "client_id": "ClientID",
+ "client_id_placeholder": "Enter a ClientID",
+ "client_added": "Client added",
+ "client_removed": "Client removed",
+ "client_name_already_exists": "This client name already exists",
+ "download_mobileconfig": "Download configuration file",
+ "plain_dns": "Plain DNS",
+ "rate_limit_subnet_len_ipv4_error": "The IPv4 subnet prefix length should be between 0 and 32",
+ "rate_limit_subnet_len_ipv6_error": "The IPv6 subnet prefix length should be between 0 and 128",
+ "source_label": "Source",
+ "unknown_filter": "Unknown filter %filterId%",
+ "known_tracker": "Known tracker",
+ "install_settings_port": "Port",
+ "form_error_port": "Enter valid port number",
+ "install_settings_all_interfaces": "All interfaces",
+ "install_auth_username": "Username",
+ "install_auth_password": "Password",
+ "install_auth_confirm": "Confirm password",
+ "install_auth_username_enter": "Enter username",
+ "install_auth_password_enter": "Enter password",
+ "install_step": "Step",
+ "install_submit_title": "Congratulations!",
+ "next": "Next",
+ "open_dashboard": "Open dashboard",
+ "install_saved": "Saved successfully",
+ "encryption_config_saved": "Encryption configuration saved",
+ "encryption_server_enter": "Enter your domain name",
+ "encryption_certificates_input": "Copy/paste your PEM-encoded certificates here.",
+ "encryption_key_input": "Copy/paste your PEM-encoded private key for your certificate here.",
+ "encryption_plain_dns_error": "To disable plain DNS, enable at least one encrypted DNS protocol",
+ "encryption_invalid_data": "Invalid data",
+ "encryption_unable_read_cert": "Unable to read certificate file",
+ "encryption_unable_read_key": "Unable to read private key file",
+ "encryption_key_cert_mismatch": "Private key does not match certificate",
+ "form_error_port_range": "Enter port number in the range of %min%-%max%",
+ "form_error_port_unsafe": "Unsafe port",
+ "form_error_equal": "Must not be equal",
+ "form_error_password": "Password mismatch",
+ "clients_title": "Persistent",
+ "clients_desc": "Rules for devices connected to AdGuard Home",
+ "settings_global": "Global",
+ "table_client": "Client",
+ "client_add": "Add Client",
+ "client_identifier": "Identifier",
+ "ip_address": "IP address",
+ "name": "Name",
+ "copy_to_clipboard": "Copy to clipboard",
+ "clients_not_found": "No clients found",
+ "auto_clients_title": "Runtime",
+ "auto_clients_desc": "Information about the IP addresses of devices that can use AdGuard Home",
+ "updates_checked": "A new version of AdGuard Home is available",
+ "updates_version_equal": "AdGuard Home is up-to-date",
+ "dns_privacy": "DNS Privacy",
+ "setup_dns_privacy_other_title": "Other implementations",
+ "setup_dns_privacy_ioc_mac": "iOS and macOS configuration",
+ "rewrite_updated": "DNS rewrite successfully updated",
+ "rewrite_edit": "Edit DNS rewrite",
+ "rewrite_hosts_applied": "Rewritten by the hosts file rule",
+ "form_error_domain_format": "Invalid domain format",
+ "form_error_answer_format": "Invalid result format",
+ "encryption_certificate_path": "Certificate path",
+ "encryption_private_key_path": "Private key path",
+ "encryption_certificates_source_path": "Set a certificates file path",
+ "encryption_certificates_source_content": "Paste the certificates contents",
+ "encryption_key_source_path": "Set a private key file",
+ "encryption_key_source_content": "Paste the private key contents",
+ "filter_removed_successfully": "Removed blocklist: %value% ",
+ "filter_added_successfully": "Added blocklist: %value% ",
+ "filter_added_successfully_more": "Added blocklists: %value% and %more% more",
+ "filter_removed_successfully_allowlist": "Removed allowlist: %value% ",
+ "filter_added_successfully_allowlist": "Added allowlist: %value% ",
+ "ignore_domains_title": "Ignored domains",
+ "ignore_domains_desc_log": "Requests that match these rules are not displayed in Query log",
+ "ignore_domains_desc_stats": "Requests that match these rules are not included in the statistics",
+ "username_label": "Username",
+ "username_placeholder": "Enter username",
+ "password_label": "Password",
+ "password_placeholder": "Enter password",
+ "clear_btn": "Clear",
+ "whois": "WHOIS",
+ "blocked_by_cname_or_ip": "Blocked by CNAME or IP",
+ "autofix_warning_text": "If you click \"Fix\", AdGuard Home will configure your system to use AdGuard Home DNS server.",
+ "autofix_warning_list": "It will perform these tasks:
Deactivate system DNSStubListener
Set DNS server address to 127.0.0.1
Replace symbolic link target of /etc/resolv.conf with /run/systemd/resolve/resolv.conf
Stop DNSStubListener (reload systemd-resolved service)
",
+ "autofix_warning_result": "As a result all DNS requests from your system will be processed by AdGuard Home by default.",
+ "tags_title": "Tags",
+ "check_not_found": "Not found in your filter lists",
+ "set_static_ip": "Set a static IP address",
+ "install_static_ok": "Good news! The static IP address is already configured",
+ "install_static_error": "AdGuard Home cannot configure it automatically for this network interface. Please look for an instruction on how to do this manually.",
+ "install_static_configure": "AdGuard Home has detected that the dynamic IP address %ip% is used. Do you want it to be set as your static address?",
+ "confirm_static_ip": "AdGuard Home will configure %ip% to be your static IP address. Do you want to proceed?",
+ "list_updated": "| %count% list updated | %count% lists updated",
+ "validated_with_dnssec": "Validated with DNSSEC",
+ "show_blocked_responses": "Blocked",
+ "show_processed_responses": "Processed",
+ "blocked_adult_websites": "Blocked by Parental control",
+ "blocked_threats": "Blocked threats",
+ "allowed": "Allowed",
+ "rewritten": "Rewritten",
+ "safe_search": "Safe search",
+ "milliseconds_abbreviation": "ms",
+ "processing_time_ms": "%value% ms",
+ "enter_cache_size": "Enter cache size (bytes)",
+ "filter_category_general": "General",
+ "filter_category_security": "Security",
+ "filter_category_regional": "Regional",
+ "filter_category_other": "Other",
+ "filter_category_general_desc": "Lists that block tracking and advertising on most of the devices",
+ "filter_category_security_desc": "Lists designed specifically to block malicious, phishing, and scam domains",
+ "filter_category_regional_desc": "Lists that focus on regional ads and tracking servers",
+ "filter_category_other_desc": "Other blocklists",
+ "port_53_faq_link": "Port 53 is often occupied by \"DNSStubListener\" or \"systemd-resolved\" services. Please read this instruction on how to resolve this.",
+ "use_saved_key": "Use an existing private key",
+ "parental_control": "Parental Control",
+ "safe_browsing": "Safe Browsing",
+ "cache_cleared": "DNS cache successfully cleared",
+ "allowlists": "Allowlists",
+ "user_rules": "User rules",
+ "logout": "Log out",
+ "system_theme": "System theme",
+ "dark_theme": "Dark theme",
+ "light_theme": "Light theme",
+ "release_notes": "Release notes",
+ "privacy_policy": "Privacy policy",
+ "selected": "%value% selected",
+ "settings_general_short": "General",
+ "settings_filter_requests": "Filter requests",
+ "settings_filtering_and_security": "Filtering and security",
+ "settings_filter_requests_desc": "You can use Blocklists , Allowlists , and User rules to set up filtering rules",
+ "settings_browsing_security": "Browsing security",
+ "settings_browsing_security_desc": "Block access to domains found in the database of malicious and phishing domains",
+ "settings_parental_control": "Parental control",
+ "settings_parental_control_desc": "Block websites with content inappropriate for children",
+ "settings_safe_search": "Safe search",
+ "settings_safe_search_desc": "Block inappropriate content in search results",
+ "settings_log_dns_requests": "Log DNS requests",
+ "settings_anonymize_client_ip": "Anonymize client IP addresses",
+ "settings_anonymize_client_ip_desc": "Replace part of the IP address with zeros",
+ "settings_hours": "| %count% hour | %count% hours",
+ "settings_days": "| %count% day | %count% days",
+ "settings_custom": "Custom",
+ "settings_log_rotation_hours": "Log rotation, in hours",
+ "settings_rotation_placeholder": "Use numbers",
+ "settings_domain_names": "Domain names, one per line",
+ "settings_rule_syntax": "Rule syntax",
+ "form_error_required": "Fill out this field",
+ "form_value_value_from_error": "Enter a value from %min_value% and %max_value%",
+ "form_value_value_min_error": "Enter a value of %min_value% or greater",
+ "save": "Save",
+ "cancel": "Cancel",
+ "close": "Close",
+ "clear_query_log": "Clear Query log",
+ "settings_statistics": "Statistics",
+ "settings_statistics_desc": "Display request and client statistics on the dashboard",
+ "settings_statistics_retention": "Statistics retention",
+ "settings_statistics_retention_hours": "Statistics retention, in hours",
+ "settings_statistics_clear": "Clear statistics",
+ "settings_confirm_decrease_log_rotation_interval": "Decrease log rotation interval?",
+ "settings_confirm_decrease_log_rotation_interval_desc": "This will delete all logs older than %value%",
+ "settings_yes_decrease": "Yes, decrease",
+ "settings_confirm_decrease_stats_rotation_interval": "Decrease stats retention interval?",
+ "settings_confirm_decrease_stats_rotation_interval_desc": "This will delete all stats older than %value%",
+ "settings_confirm_clear_statistics": "Clear statistics?",
+ "settings_confirm_clear_statistics_desc": "This will clear all statistics. Query log will still be available",
+ "settings_yes_clear": "Yes, clear",
+ "settings_confirm_clear_query_log": "Clear Query log?",
+ "settings_confirm_clear_query_log_desc": "This will delete all requests from Query log . Statistics will still be available",
+ "settings_tooltip_domain_names": "Domain names, wildcards, or filtering rules",
+ "settings_tooltip_examples": "Examples:",
+ "settings_notify_changes_saved": "Changes saved",
+ "settings_notify_query_log_cleared": "Query log cleared",
+ "settings_notify_statistics_cleared": "Statistics cleared",
+ "protocols": "Protocols",
+ "encryption_title": "Encryption",
+ "encryption_encrypted_dns": "Encrypted DNS",
+ "encryption_encrypted_dns_desc": "The AdGuard Home admin interface uses HTTPS. The DNS server supports DNS-over-HTTPS, DNS-over-TLS, and DNS-over-QUIC",
+ "encryption_plain_dns": "Plain DNS",
+ "encryption_plain_dns_desc": "Plain DNS is enabled by default. Disabling it forces all devices to use encrypted DNS. To do so, specify at least one encrypted DNS protocol",
+ "encryption_server": "Server name",
+ "encryption_server_tooltip_1": "If set, AdGuard Home will detect ClientIDs, respond to DDR queries, and perform additional connection validations. If not set, these features are disabled",
+ "encryption_server_tooltip_2": "Must match a DNS name in the certificate",
+ "encryption_https": "HTTPS port",
+ "encryption_https_tooltip": "This port provides HTTPS access for the AdGuard Home admin interface. It also provides DNS-over-HTTPS at the '/dns-query' location",
+ "encryption_dot": "DNS-over-TLS port",
+ "encryption_dot_tooltip": "This port is used to run a DNS-over-TLS server",
+ "encryption_doq": "DNS-over-QUIC port",
+ "encryption_doq_tooltip": "This port is used to run a DNS-over-QUIC server",
+ "encryption_server_summary": "Server name: %value%",
+ "encryption_https_summary": "HTTPS port: %value%",
+ "encryption_dot_summary": "DNS-over-TLS port: %value%",
+ "encryption_doq_summary": "DNS-over-QUIC port: %value%",
+ "encryption_force_redirect": "Automatically redirect from HTTP to HTTPS",
+ "encryption_certificates": "Certificates",
+ "encryption_certificates_desc": "To use encryption, you must provide a valid SSL certificate chain for your domain. You can get a free SSL certificate from letsencrypt.org or buy one from a trusted Certificate Authority",
+ "encryption_key": "Private key",
+ "encryption_disable_plain_dns": "Disable plain DNS?",
+ "encryption_disable_plain_dns_desc": "Devices that do not support encrypted DNS may not work properly",
+ "disable": "Disable",
+ "disable_protection_btn": "Disable protection",
+ "encryption_chain_valid": "Certificate chain is valid",
+ "encryption_chain_invalid": "Certificate chain is invalid",
+ "encryption_key_valid": "Private key is valid",
+ "encryption_key_type": "Encryption algorithm: %value%",
+ "encryption_key_invalid": "Private key is invalid",
+ "encryption_subject": "Subject: %value%",
+ "encryption_issuer": "Issuer: %value%",
+ "encryption_expire": "Expires: %value%",
+ "encryption_hostnames": "Hostnames: %value%",
+ "encryption_certificate_has_issues": "Certificate has issues",
+ "encryption_confirm_clear": "Reset encryption settings",
+ "encryption_confirm_clear_desc": "All encryption settings will be reset to their default values",
+ "dns_protocols_title": "DNS protocols",
+ "add_tls_certificate": "Add TLS certificate",
+ "add_tls_certificate_private_key": "Add TLS certificate private key",
+ "tls_certificate": "TLS certificate",
+ "encrypted_dns_settings": "Encrypted DNS server settings",
+ "reset_dns_protocols": "Reset DNS protocols",
+ "reset_dns_confirm_title": "Reset DNS protocols?",
+ "reset_dns_confirm_text": "All DNS protocol settings will be reset to their default values",
+ "yes_reset": "Yes, reset",
+ "yes_disable": "Yes, disable",
+ "browse": "Browse",
+ "none_text": "None",
+ "path_to_file_placeholder": "Path to file: local or Internet address",
+ "tls_cert_path_option": "Enter the full path to the certificate file",
+ "tls_cert_path_label": "Full path to the certificate file",
+ "tls_key_path_option": "Enter the full path to the private key file",
+ "tls_key_path_label": "Full path to the private key file",
+ "tls_cert_modal_description": "To use encryption, you must provide a valid TLS certificate chain for your domain. You can get a free TLS certificate from letsencrypt.org or buy one from a trusted Certificate Authority",
+ "reset": "Reset",
+ "confirm": "Confirm",
+ "dns_access_settings_title": "Access settings",
+ "dns_allowed_clients": "Allowed clients",
+ "dns_allowed_clients_desc": "Only accepts requests from clients on this list",
+ "dns_allowed_clients_desc_2": "To add a client, enter its CIDR, IP address, or ClientID ",
+ "dns_allowed_clients_label": "Allowed clients, one per line",
+ "dns_allowed_clients_placeholder": "Enter allowed clients",
+ "dns_blocking_mode": "Blocking mode",
+ "dns_blocking_mode_custom_ip": "Custom IP address",
+ "dns_blocking_mode_custom_ip_desc": "Responds with a manually set IP address",
+ "dns_blocking_mode_default": "Default",
+ "dns_blocking_mode_default_desc": "Responds with 0.0.0.0 for A records and :: for AAAA records if blocked by an Adblock-style rule. Responds with the IP address specified in the rule if blocked by a hosts-style rule",
+ "dns_blocking_mode_desc": "Sets the response type to blocked requests",
+ "dns_blocking_mode_ipv4_label": "Blocking IPv4",
+ "dns_blocking_mode_ipv4_placeholder": "Enter IPv4 address",
+ "dns_blocking_mode_ipv6_label": "Blocking IPv6",
+ "dns_blocking_mode_ipv6_placeholder": "Enter IPv6 address",
+ "dns_blocking_mode_null_ip": "Null IP address",
+ "dns_blocking_mode_null_ip_desc": "Responds with 0.0.0.0 for A and :: for AAAA",
+ "dns_blocking_mode_nxdomain": "NXDOMAIN",
+ "dns_blocking_mode_nxdomain_desc": "Responds with NXDOMAIN code",
+ "dns_blocking_mode_refused": "REFUSED",
+ "dns_blocking_mode_refused_desc": "Responds with REFUSED code",
+ "dns_blocking_mode_title": "Blocking mode",
+ "dns_blocking_mode_ttl_label": "Blocked response TTL, in seconds",
+ "dns_blocking_mode_ttl_placeholder": "Enter TTL",
+ "dns_bootstrap_dns_desc": "Sets the DNS servers used to resolve DoH/DoT upstream resolver hostnames",
+ "dns_bootstrap_dns_desc_2": "Enter IP addresses only. Comments are not allowed",
+ "dns_bootstrap_dns_label": "Bootstrap DNS servers, one per line",
+ "dns_bootstrap_dns_placeholder": "IP addresses",
+ "dns_bootstrap_dns_title": "Bootstrap DNS servers",
+ "dns_bootstrap_servers": "Bootstrap DNS servers",
+ "dns_cache_desc": "Stores DNS responses locally",
+ "dns_cache_size": "Cache size",
+ "dns_cache_size_desc": "Sets the DNS cache size",
+ "dns_cache_size_label": "DNS cache size, in bytes",
+ "dns_cache_size_title": "Cache size",
+ "dns_cache_title": "DNS cache",
+ "dns_clear_cache": "Clear cache",
+ "dns_clear_cache_cancel": "Cancel",
+ "dns_clear_cache_confirm": "Yes, clear",
+ "dns_clear_cache_desc": "This will clear your device's DNS cache and prompt it to request new information from the DNS server",
+ "dns_clear_cache_title": "Clear cache?",
+ "dns_confirm_clear_cache": "Clear DNS cache?",
+ "dns_confirm_clear_cache_desc": "Are you sure you want to clear the DNS cache?",
+ "dns_clear_cache_yes": "Yes, clear",
+ "dns_disallowed_clients": "Disallowed clients",
+ "dns_disallowed_clients_desc": "Drops requests from clients on this list. This setting is ignored when there are entries in Allowed clients",
+ "dns_disallowed_clients_label": "Disallowed clients, one per line",
+ "dns_disallowed_clients_title": "Disallowed clients",
+ "dns_disallowed_domains": "Disallowed domains",
+ "dns_disallowed_domains_desc": "Drops DNS queries that match these domains, and they don’t appear in Query log",
+ "dns_disallowed_domains_desc_2": "Enter exact domain names, wildcards, or URL filter rules, such as example.org, *.example.org, or ||example.org^",
+ "dns_disallowed_domains_label": "Disallowed domains, one per line",
+ "dns_disallowed_domains_title": "Disallowed domains",
+ "dns_dnssec": "DNSSEC",
+ "dns_dnssec_desc": "Adds the DNSSEC flag in outbound DNS queries and checks the result. Requires a DNSSEC-enabled resolver",
+ "dns_edns_client_subnet": "EDNS client subnet",
+ "dns_edns_client_subnet_desc": "Adds EDNS Client Subnet (ECS) data to upstream requests and records ECS values in Query log",
+ "dns_edns_custom_label": "Custom IP address for EDNS",
+ "dns_edns_custom_placeholder": "Enter IP address",
+ "dns_edns_desc": "Add EDNS Client Subnet (ECS) data to upstream requests and record ECS values in Query log. You can use the default client subnet or specify a custom IP address for ECS",
+ "dns_edns_option_custom": "Use custom IP address",
+ "dns_edns_option_default": "Use default EDNS client subnet",
+ "dns_edns_title": "EDNS client subnet",
+ "dns_fallback_dns_desc": "Sets fallback DNS servers to use when upstream DNS servers are unavailable",
+ "dns_fallback_dns_desc_2": "Use the same syntax as for the upstream DNS servers ",
+ "dns_fallback_dns_label": "Fallback DNS servers, one per line",
+ "dns_fallback_dns_placeholder": "IP addresses, sdns://, quic://, https://, h3://, or tls://",
+ "dns_fallback_dns_title": "Fallback DNS servers",
+ "dns_fallback_servers": "Fallback DNS servers",
+ "dns_ipv6_resolution": "IPv6 resolution",
+ "dns_ipv6_resolution_desc": "Enables resolution of DNS queries for IPv6 addresses (AAAA records) and adds IPv6 hints to HTTPS responses",
+ "dns_optimistic_caching": "Optimistic caching",
+ "dns_optimistic_caching_desc": "Responds from the cache even if the entries have expired and tries to update them",
+ "dns_override_max_ttl": "Override maximum TTL",
+ "dns_override_max_ttl_desc": "Sets the maximum time DNS responses are cached. Higher TTL values are reduced to this value",
+ "dns_override_max_ttl_label": "Maximum time-to-live (TTL), in seconds",
+ "dns_override_max_ttl_placeholder": "Time-to-live (TTL)",
+ "dns_override_max_ttl_title": "Override maximum TTL",
+ "dns_override_min_ttl": "Override minimum TTL",
+ "dns_override_min_ttl_desc": "Sets the minimum time DNS responses are cached. Lower TTL values are increased to this value",
+ "dns_override_min_ttl_label": "Minimum time-to-live (TTL), in seconds",
+ "dns_override_min_ttl_placeholder": "Time-to-live (TTL)",
+ "dns_override_min_ttl_title": "Override minimum TTL",
+ "dns_private_reverse_page_desc": "Resolves PTR, SOA, and NS requests for ARPA domains with private IP addresses through private upstream servers, DHCP, and /etc/hosts. When disabled, responds to all such requests with NXDOMAIN",
+ "dns_private_reverse_resolvers": "Private reverse DNS resolvers",
+ "dns_private_reverse_resolvers_desc": "Resolves PTR, SOA, and NS requests for ARPA domains with private IP addresses through private upstream servers, DHCP, and /etc/hosts",
+ "dns_private_reverse_resolvers_disabled_desc": "When disabled, responds to all such requests with NXDOMAIN",
+ "dns_private_reverse_resolve_clients_desc": "Resolves client IP addresses to their hostnames using PTR queries. Uses private DNS servers for local clients and upstream servers for clients with public IP addresses",
+ "dns_private_reverse_resolve_clients_title": "Reverse lookup of client IP addresses",
+ "dns_private_reverse_servers_desc": "Sets the DNS servers for private PTR, SOA, and NS requests. These requests come from clients with private IP addresses and ask for ARPA domains containing subnets from private IP address ranges",
+ "dns_private_reverse_servers_desc_2": "If this field is empty, your operating system's default DNS resolvers are used, except for AdGuard Home IP addresses. By default, AdGuard Home uses the following reverse DNS resolvers: %value_1% and %value_2%",
+ "dns_private_reverse_servers_label": "Private reverse DNS servers, one per line",
+ "dns_private_reverse_servers_placeholder": "IP addresses, sdns://, quic://, https://, h3://, or tls://",
+ "dns_private_reverse_servers_title": "Private reverse DNS servers",
+ "dns_rate_limit": "Rate limit",
+ "dns_rate_limit_allowlist": "Rate-limiting allowlist",
+ "dns_rate_limit_allowlist_desc": "Specifies IP addresses excluded from rate limiting",
+ "dns_rate_limit_allowlist_label": "IP addresses, one per line",
+ "dns_rate_limit_allowlist_placeholder": "IP addresses",
+ "dns_rate_limit_allowlist_title": "Rate-limiting allowlist",
+ "dns_rate_limit_desc": "Sets the number of requests per second allowed per client",
+ "dns_rate_limit_desc_2": "A value of 0 means no limit",
+ "dns_rate_limit_no_limit": "No limit",
+ "dns_rate_limit_placeholder": "Enter rate limit",
+ "dns_rate_limit_title": "Rate limit",
+ "dns_rate_limit_value": "%value% req/s",
+ "dns_server_addresses": "Server addresses",
+ "dns_server_addresses_configured_in_file": "Server addresses are configured in the file %path% ",
+ "dns_server_addresses_desc": "Sets DNS server addresses through which AdGuard Home will be accessible",
+ "dns_server_addresses_desc_2": "View tips for configuring upstream DNS servers and our list of trusted DNS providers ",
+ "dns_server_addresses_label": "Server addresses, one per line",
+ "dns_server_addresses_placeholder": "IP addresses, sdns://, quic://, https://, h3://, or tls://",
+ "dns_server_addresses_title": "Server addresses",
+ "dns_server_config_title": "DNS server configuration",
+ "dns_subnet_prefix": "Subnet prefix length for %value% addresses",
+ "dns_subnet_prefix_desc": "Sets the subnet prefix length for %value% addresses used for rate limiting",
+ "dns_subnet_prefix_title": "Subnet prefix length for %value% addresses",
+ "dns_subnet_placeholder": "Enter prefix length",
+ "dns_test_upstreams": "Test upstreams",
+ "dns_upstream_mode": "Upstream mode",
+ "dns_upstream_mode_desc": "Determines how to route your network’s DNS queries to multiple upstream servers",
+ "dns_upstream_mode_title": "Upstream mode",
+ "dns_upstream_servers_title": "Upstream DNS servers",
+ "dns_upstream_timeout": "Upstream timeout",
+ "dns_upstream_timeout_desc": "Sets how long to wait for a response from upstream servers",
+ "dns_upstream_timeout_placeholder": "Enter timeout in seconds",
+ "dns_upstream_timeout_title": "Upstream timeout",
+ "dns_ttl_value": "%value% s",
+ "dns_upstream_validation_invalid": "Invalid format",
+ "dns_upstream_validation_invalid_lines": "Invalid format on lines {lines}",
+ "dns_settings": "DNS",
+ "upstream_dns": "Upstream DNS servers",
+ "upstream_dns_servers_title": "Upstream DNS servers",
+ "upstream_dns_placeholder": "IP addresses, sdns://, quic://, https://, h3://, or tls://",
+ "upstream_dns_addresses": "Server addresses, one per line",
+ "upstream_dns_addresses_faq": "Learn more about configuring upstream DNS servers. Here is a list of known DNS providers to choose from.",
+ "upstream_dns_load_balancing": "Load balancing",
+ "upstream_dns_load_balancing_desc": "Query one upstream server at a time
AdGuard Home uses a weighted random algorithm to select servers with the lowest number of failed lookups and the lowest average lookup time
",
+ "upstream_dns_parallel_requests": "Parallel requests",
+ "upstream_dns_parallel_requests_desc": "Use parallel queries to speed up resolving by querying all upstream servers simultaneously",
+ "upstream_dns_fastest_addr": "Fastest IP address",
+ "upstream_dns_fastest_addr_desc": "Wait for responses from all DNS servers, measure the TCP connection speed for each server, and return the IP address of the server with the fastest connection speed",
+ "upstream_dns_fastest_addr_warning": "This mode can significantly slow down DNS queries, if one or more upstream servers are not responding. Make sure that your upstream servers are stable and your upstream timeout is low",
+ "upstream_examples_title": "Examples",
+ "upstream_example_udp": "%ipv4%,%ipv6%: regular DNS (over UDP)",
+ "upstream_example_udp_port": "%ipv4%,%ipv6%: regular DNS (over UDP, with port)",
+ "upstream_example_udp_hostname": "%value%: regular DNS (over UDP, hostname)",
+ "upstream_example_tcp": "%ipv4%,%ipv6%: regular DNS (over TCP)",
+ "upstream_example_tcp_port": "%ipv4%,%ipv6%: regular DNS (over TCP, with port)",
+ "upstream_example_tcp_hostname": "%value%: regular DNS (over TCP, hostname)",
+ "upstream_example_upstream_dot": "%value%: encrypted DNS-over-TLS ",
+ "upstream_example_upstream_doh": "%value%: encrypted DNS-over-HTTPS ",
+ "upstream_example_upstream_doh3": "%value%: encrypted DNS-over-HTTPS with forced HTTP/3 and no fallback to HTTP/2",
+ "upstream_example_upstream_doq": "%value%: encrypted DNS-over-QUIC ",
+ "upstream_example_upstream_sdns": "%value%: DNS Stamps for DNSCrypt or DNS-over-HTTPS resolvers",
+ "upstream_example_upstream_reserved": "%value%: an upstream for specific domains ",
+ "upstream_example_multiple_upstreams_reserved": "%value%: multiple upstreams for specific domains ",
+ "upstream_example_upstream_comment": "%value%: a comment.",
+ "upstream_fallback_title": "Fallback DNS servers, one per line",
+ "upstream_fallback_title_faq": "Fallback DNS servers to use when upstream DNS servers are unavailable. Use the same syntax as for the upstream DNS servers",
+ "ip_addresses_placeholder": "Enter IP addresses, one per line",
+ "upstream_bootstrap_dns_title": "Bootstrap DNS servers, one per line",
+ "upstream_bootstrap_dns_faq": "IP addresses of DNS servers used to resolve the IP addresses of DoH/DoT upstream resolvers. Comments are not allowed",
+ "upstream_ptr": "Private reverse DNS servers, one per line",
+ "upstream_ptr_faq_1": "AdGuard Home uses these DNS servers for private PTR, SOA, and NS requests. Private requests come from clients with a private IP address and ask for an ARPA domain that contains a subnet from a private IP address range, such as %value%",
+ "upstream_ptr_faq_2": "If this field is empty, the default DNS resolvers of your OS will be used, except for the AdGuard Home IP addresses",
+ "upstream_ptr_faq_3": "By default, AdGuard Home uses the following reverse DNS resolvers: %value_1% and %value_2%",
+ "upstream_private_ptr_resolvers_title": "Use private reverse DNS resolvers",
+ "upstream_private_ptr_resolvers_desc": "Resolve PTR, SOA, and NS requests for ARPA domains containing private IP addresses through private upstream servers, DHCP, /etc/hosts, and so on. If disabled, AdGuard Home will respond to all such requests with NXDOMAIN",
+ "upstream_enable_reverse_lookup_title": "Enable reverse lookup of client IP addresses",
+ "upstream_enable_reverse_lookup_desc": "Reverse-resolve client IP addresses to their hostnames by sending PTR queries to appropriate resolvers: private DNS servers for local clients, or upstream servers for clients with public IP addresses",
+ "upstream_timeout": "Upstream timeout, in seconds",
+ "upstream_timeout_faq": "Time to wait for a response from the upstream server",
+ "upstream_timeout_placeholder": "Enter the upstream server timeout duration",
+ "apply": "Apply",
+ "test_upstreams": "Test upstreams",
+ "server_config_rate_limit": "Rate limit, RPS",
+ "server_config_rate_limit_faq": "The number of requests per second allowed per client. A value of 0 means no limit",
+ "server_config_rate_limit_placeholder": "Enter rate limit",
+ "server_config_subnet_len_ipv4": "Subnet prefix length for IPv4 addresses",
+ "server_config_subnet_len_ipv4_faq": "Subnet prefix length for IPv4 addresses used for rate limiting. The default is 24",
+ "server_config_subnet_len_ipv6": "Subnet prefix length for IPv6 addresses",
+ "server_config_subnet_len_ipv6_faq": "Subnet prefix length for IPv6 addresses used for rate limiting. The default is 56",
+ "server_config_subnet_len_placeholder": "Enter prefix length",
+ "server_config_rate_limit_whitelist": "Rate-limiting allowlist",
+ "server_config_rate_limit_whitelist_faq": "IP addresses excluded from rate limiting",
+ "server_config_edns_enable": "Enable EDNS client subnet",
+ "server_config_edns_cs_desc": "Add the EDNS Client Subnet (ECS) option to upstream requests and log the values sent by the clients in the query log",
+ "server_config_edns_use_custom_ip": "Use custom IP address for EDNS",
+ "server_config_edns_use_custom_ip_desc": "Replace the client IP address with the custom IP address below",
+ "enter_ip_address_placeholder": "Enter IP address",
+ "server_config_dnssec_enable": "Enable DNSSEC",
+ "server_config_dnssec_enable_desc": "Set the DNSSEC flag in outbound DNS queries and check the result. Requires a DNSSEC-enabled resolver",
+ "server_config_disable_ipv6": "Disable resolving of IPv6 addresses",
+ "server_config_disable_ipv6_desc": "Drop all DNS queries for IPv6 addresses (type AAAA) and remove IPv6 hints from HTTPS responses",
+ "server_config_blocking_mode": "Blocking mode",
+ "server_config_default": "Default",
+ "server_config_blocking_mode_default_desc": "Default: Respond with 0.0.0.0 for A and :: for AAAA if blocked by Adblock-style rule. Respond with the IP address specified in the rule if blocked by /etc/hosts-style rule",
+ "server_config_refused": "REFUSED",
+ "server_config_blocking_mode_refused_desc": "REFUSED: Respond with REFUSED code",
+ "server_config_nxdomain": "NXDOMAIN",
+ "server_config_blocking_mode_nxdomain_desc": "NXDOMAIN: Respond with NXDOMAIN code",
+ "server_config_null_ip": "Null IP address",
+ "server_config_blocking_mode_null_ip_desc": "Null IP address: Respond with 0.0.0.0 for A and :: for AAAA",
+ "server_config_custom_ip": "Custom IP address",
+ "server_config_blocking_mode_custom_ip_desc": "Custom IP address: Respond with a manually set IP address",
+ "server_config_blocking_mode_ipv4": "Blocking IPv4",
+ "server_config_blocking_mode_ipv4_placeholder": "Enter IPv4 address",
+ "server_config_blocking_mode_ipv4_faq": "IP address to return for a blocked A request",
+ "server_config_blocking_mode_ipv6": "Blocking IPv6",
+ "server_config_blocking_mode_ipv6_placeholder": "Enter IPv6 address",
+ "server_config_blocking_mode_ipv6_faq": "IP address to return for a blocked AAAA request",
+ "server_config_blocking_mode_ttl": "Blocked response TTL, in seconds",
+ "server_config_blocking_mode_ttl_faq": "Specifies how long clients should cache a filtered response",
+ "cache_config_size": "Cache size, in bytes",
+ "cache_config_size_faq": "DNS cache size. To disable caching, leave empty",
+ "cache_config_min_ttl": "Override minimum TTL, in seconds",
+ "cache_config_min_ttl_faq": "The minimum time-to-live (TTL) value for entries in the DNS cache",
+ "cache_config_min_ttl_placeholder": "Enter minimum TTL",
+ "cache_config_max_ttl": "Override maximum TTL, in seconds",
+ "cache_config_max_ttl_faq": "The maximum time-to-live (TTL) value for entries in the DNS cache",
+ "cache_config_max_ttl_placeholder": "Enter maximum TTL",
+ "cache_config_optimistic": "Optimistic caching",
+ "cache_config_optimistic_desc": "AdGuard Home will respond from the cache even if the entries have expired and will try to update them",
+ "cache_config_clear": "Clear cache",
+ "cache_config_size_validation": "The cache size must be greater than zero when enabled",
+ "cache_config_ttl_validation": "Minimum cache TTL override must be less than or equal to the maximum",
+ "cache_enabled": "Enable cache",
+ "cache_enabled_desc": "Store DNS responses locally",
+ "cache_confirm_clear_title": "Clear DNS cache",
+ "cache_confirm_clear_desc": "This will remove all cached DNS responses. Subsequent DNS queries may be slower until the cache is rebuilt",
+ "access_settings_title": "Access settings",
+ "access_settings_allowed_title": "Allowed clients, one per line",
+ "access_settings_allowed_faq": "A list of CIDRs, IP addresses, or ClientIDs . If this field has entries, AdGuard Home will accept requests only from these clients",
+ "access_settings_disallowed_title": "Disallowed clients, one per line",
+ "access_settings_disallowed_faq": "A list of CIDRs, IP addresses, or ClientIDs . If this field has entries, AdGuard Home will drop requests from these clients. This field is ignored if there are entries in Allowed clients ",
+ "access_settings_blocked_title": "Disallowed domains, one per line",
+ "access_settings_blocked_faq_1": "AdGuard Home will drop DNS queries matching these domains. Such queries don't appear in the query log. You can enter exact domain names, wildcards, or URL filter rules, such as example.org, *.example.org, or ||example.org^",
+ "access_settings_blocked_faq_2": "Not to be confused with filters",
+ "access_settings_allowed_placeholder": "Enter allowed clients",
+ "access_settings_disallowed_placeholder": "Enter disallowed clients",
+ "access_settings_blocked_placeholder": "Enter disallowed domains",
+ "blocklists_title": "Blocklists",
+ "blocklists_desc": "Domains from these lists are blocked",
+ "blocklists_add": "Add blocklists",
+ "blocklists_add_desc": "You can select blocklists from the list or add them manually",
+ "blocklists_empty": "No blocklists yet — add your first one ",
+ "allowlists_desc": "Domains on these allowlists are allowed even if they are on a blocklist",
+ "name_label": "Name",
+ "url_label": "URL",
+ "rules_label": "Rules",
+ "last_updated_label": "Last updated",
+ "rows_per_page": "Rows per page %value%",
+ "update_filters_title": "Update filters automatically",
+ "update_filters_desc": "Sets the auto-update interval for filters. Applies to both blocklists and allowlists added via external links",
+ "update_filters_disable": "Disable",
+ "update_filters_interval_hourly": "Every hour",
+ "update_filters_interval_daily": "Every day",
+ "update_filters_interval_weekly": "Every week",
+ "update_filters_interval_custom": "Custom",
+ "update_filters_custom_hours": "Custom interval, in hours",
+ "blocklist_add_from_list": "Add from list",
+ "blocklist_add_manual": "Add manually",
+ "blocklist_homepage": "Homepage",
+ "blocklist_contents": "Contents (.%value%)",
+ "add": "Add",
+ "blocklist_edit": "Edit blocklist",
+ "blocklist_url_file_path": "URL or file path",
+ "blocklist_remove": "Remove blocklist?",
+ "blocklist_remove_desc": "%value% will be removed",
+ "remove": "Remove",
+ "yes_remove": "Yes, remove",
+ "changes_saved_success": "Changes saved",
+ "blocklist_placeholder_example": "My blocklist",
+ "allowlists_title": "Allowlists",
+ "allowlist_remove": "Remove allowlist?",
+ "allowlist_remove_desc": "%value% will be removed",
+ "allowlist_placeholder_example": "My allowlist",
+ "allowlist_add": "Add allowlist",
+ "allowlist_edit": "Edit allowlist",
+ "allowlist_empty": "No allowlists yet — add your first one ",
+ "form_error_range": "Value must be between %min% and %max%",
+ "dns_rewrites": "DNS rewrites",
+ "dns_rewrite_removed": "DNS rewrite removed",
+ "dns_rewrite_exists": "This DNS rewrite already exists",
+ "dns_rewrite_same": "You can’t rewrite to the same domain or wildcard",
+ "dns_rewrites_desc": "Lists your custom DNS responses for specific domain names",
+ "rewrite_add": "Add DNS rewrite",
+ "rewrite_domain": "Domain name or wildcard",
+ "rewrites_empty": "No rewrites yet — add your first one ",
+ "domain": "Domain",
+ "result": "Result",
+ "disable_dns_rewrites": "Disable DNS rewrites?",
+ "rewrite_domain_input_placeholder": "Enter the domain name or wildcard",
+ "instructions": "Instructions",
+ "rewrites_answer_input_placeholder": "Enter IP address, CNAME record, A, or AAAA",
+ "rewrites_remove_title": "Remove DNS rewrite?",
+ "rewrites_remove_desc": "The DNS rewrite for %value% will be removed",
+ "rewrites_tooltip_examples_item1": "Rewrite DNS responses for this domain name only",
+ "rewrites_tooltip_examples_item2": "Rewrite DNS responses for all subdomains of example.org",
+ "rewrites_tooltip_instructions_item1": "To resolve the domain name to an IP address, enter an IPv4 address for A records and an IPv6 address for AAAA records",
+ "rewrites_tooltip_instructions_item2": "To redirect the domain name, enter the CNAME record of the target domain",
+ "rewrites_tooltip_instructions_item3": "To keep only A records, enter",
+ "rewrites_tooltip_instructions_item4": "To keep only AАAA records, enter",
+ "rewrite_settings_updated": "DNS rewrite settings successfully updated",
+ "all_rewrites_disabled": "All custom DNS responses will be disabled",
+ "enable_dns_rewrites": "Enable DNS rewrites?",
+ "all_rewrites_enabled": "All custom DNS responses will be enabled",
+ "enable": "Enable",
+ "sort_asc": "Sort from A-Z",
+ "sort_desc": "Sort from Z-A",
+ "all_domains": "All domains",
+ "setup_guide_desc": "Configure your devices to use AdGuard Home",
+ "setup_guide_device_type": "Select your device type",
+ "home_dns_addresses": "AdGuard Home DNS addresses",
+ "home_dns_addresses_desc": "The AdGuard Home DNS server listens on the following interfaces/ports:",
+ "encrypted_dns_addresses": "Encrypted DNS server addresses",
+ "plain_dns_addresses": "Plain DNS server addresses",
+ "setup_devices_macos_list_1": "Click Apple menu → System Preferences → Network .",
+ "setup_devices_macos_list_2": "Select the required connection and click Advanced .",
+ "setup_devices_macos_list_3": "Select the DNS tab and enter the addresses of your AdGuard Home server.",
+ "setup_devices_android_list_1": "Open your device's Settings app and select Wi-Fi .",
+ "setup_devices_android_list_2": "Long press your current network, then select Modify network . It is not possible to set custom DNS for mobile Internet connections.",
+ "setup_devices_android_list_3": "On some devices, you may need to select the Advanced checkbox to see more settings. To customize your Android DNS settings, you need to change IP settings from DHCP to Static .",
+ "setup_devices_android_list_4": "Change the DNS 1 and DNS 2 values to the addresses of your AdGuard Home server.",
+ "setup_devices_ios_list_1": "Open your device's Settings app and select Wi-Fi . It is not possible to set custom DNS for mobile Internet connections.",
+ "setup_devices_ios_list_2": "Tap the name of the currently active network.",
+ "setup_devices_ios_list_3": "Go to DNS → Configure DNS → Manual and add the addresses of your AdGuard Home server.",
+ "setup_devices_ios_list_4": "Tap Save .",
+ "setup_devices_router_desc_1": "Set up AdGuard Home on your home network router and protect all devices connected to it. No need to manually configure each device.",
+ "setup_devices_router_list_1_title": "Open your router's settings.",
+ "setup_devices_router_list_1": "You can usually access them from your browser via a URL, such as http://192.168.0.1 or http://192.168.1.1 . You may be asked to enter a password. You can often reset the password by pressing a button on the router itself, but be aware that you could lose your entire router configuration. If your router is to be set up through an app, install the app on your phone or PC and use it to access the router's settings.",
+ "setup_devices_router_list_2_title": "In the DHCP/DNS settings, enter the addresses of your AdGuard Home server.",
+ "setup_devices_router_list_2": "To find the settings, look for DNS next to a field that allows two or three sets of numbers, each divided into four groups of one to three digits.",
+ "setup_devices_router_desc_2": "Some routers do not allow you to set up a custom DNS server. In this case, you can try to set up AdGuard Home as a DHCP server . If this does not work, please refer to your router manual for instructions on how to customize DNS servers for your specific router.",
+ "setup_devices_windows_list_1": "Open Control Panel from the Start menu or Windows Search .",
+ "setup_devices_windows_list_2": "Click Network and Internet → Network and Sharing Center → Change adapter settings .",
+ "setup_devices_windows_list_3": "Right-click on your active connection and select Properties .",
+ "setup_devices_windows_list_4": "In the list, select Internet Protocol Version 4 (TCP/IPv4) for IPv4 or Internet Protocol Version 6 (TCP/IPv6) for IPv6.",
+ "setup_devices_windows_list_5": "Click Properties → Use the following DNS server addresses .",
+ "setup_devices_windows_list_6": "Enter the addresses of your AdGuard Home server.",
+ "setup_devices_dns_list_1": "For DNS-over-TLS , use ",
+ "setup_devices_dns_list_2": "For DNS-over-HTTPS , use ",
+ "setup_devices_dns_list_3": "For DNS-over-QUIC , use ",
+ "setup_devices_dns_list_devices": "Here’s a list of software you can use.",
+ "setup_devices_dns_android_list_1": "Android 9 natively supports DNS-over-TLS . To configure it, go to Settings → Network & Internet → Advanced → Private DNS and enter your domain name.",
+ "setup_devices_dns_android_list_2": "AdGuard for Android supports DNS-over-HTTPS , DNS-over-TLS , and DNS-over-QUIC setup .",
+ "setup_devices_dns_android_list_3": "Intra adds DNS-over-HTTPS support to Android.",
+ "setup_devices_dns_ios_list_1": "AdGuard for iOS supports DNS-over-HTTPS , DNS-over-TLS , and DNS-over-QUIC setup.",
+ "setup_devices_dns_ios_list_2": "DNSCloak supports DNS-over-HTTPS , but to configure it to use your own server, you'll need to generate a DNS Stamp for it.",
+ "setup_devices_dns_other_list_1": "AdGuard Home itself can be a secure DNS client on any platform.",
+ "setup_devices_dns_other_list_2": "dnsproxy supports all known secure DNS protocols.",
+ "setup_devices_dns_other_list_3": "dnscrypt-proxy supports DNS-over-HTTPS .",
+ "setup_devices_dns_other_list_4": "Mozilla Firefox supports DNS-over-HTTPS .",
+ "setup_devices_dns_other_list_5": "For more implementations, see DNSCrypt and DNS privacy .",
+ "setup_devices_dns_macos_desc": "On an iOS 14 or macOS Big Sur device, you can download a special mobileconfig file that adds DNS-over-HTTPS or DNS-over-TLS servers to the DNS settings.",
+ "client_id_faq": "Clients can be identified by their ClientID. To learn more, visit the AdGuard Home GitHub ",
+ "device_type": "Device type",
+ "setup_guide": "Setup guide",
+ "setup_devices_router_title": "Router",
+ "setup_devices_windows_title": "Windows",
+ "setup_devices_macos_title": "macOS",
+ "setup_devices_android_title": "Android",
+ "setup_devices_ios_title": "iOS",
+ "protection": "Protection",
+ "pause_for_seconds": "| Pause for %count% second | Pause for %count% seconds",
+ "pause_for_minutes": "| Pause for %count% minute | Pause for %count% minutes",
+ "pause_for_hour": "Pause for %count% hour",
+ "pause_until_tomorrow": "Pause until tomorrow, %time%",
+ "resume_protection_timer": "Protection will resume in %time%",
+ "no_stats_yet": "No stats yet — keep using AdGuard Home to view them",
+ "blocked_threats_chart": "Blocked threats",
+ "ads_blocked_card": "Ads blocked",
+ "queries_total": "| %count% query total | %count% queries total",
+ "queries": "Queries",
+ "blocked_queries": "Blocked queries",
+ "confirm_client_block_title": "Block client?",
+ "confirm_client_block_desc": "AdGuard Home will be dropping all DNS requests from the client %ip% ",
+ "confirm_client_unblock_title": "Unblock client?",
+ "confirm_client_unblock_desc": "DNS requests from the client %ip% will be allowed",
+ "client_already_blocked": "Client %ip% is already blocked",
+ "blocked": "Blocked",
+ "block_client": "Block client",
+ "unblock_client": "Unblock client",
+ "client_blocked_flash": "Client blocked",
+ "client_unblocked_flash": "Client unblocked",
+ "dns_queries": "DNS queries",
+ "dns_queries_tooltip": "The number of DNS queries processed for the last 24 hours",
+ "ads_blocked": "Ads blocked",
+ "ads_blocked_tooltip": "The number of DNS queries blocked by adblock filters and hosts blocklist",
+ "threats_blocked": "Threats blocked",
+ "threats_blocked_tooltip": "The number of DNS queries blocked by the AdGuard browsing security module",
+ "adult_websites_blocked": "Adult websites blocked",
+ "adult_websites_blocked_tooltip": "The number of adult websites blocked",
+ "safe_search_used": "Safe search used",
+ "safe_search_used_tooltip": "The number of DNS queries to search engines for which Safe Search was enforced",
+ "average_time_processing": "Average processing time",
+ "average_time_processing_tooltip": "Average time in milliseconds onprocessing a DNS query",
+ "found_in_known_domains": "Found in the known domains",
+ "name_tooltip": "Name",
+ "category_tooltip": "Category",
+ "source_tooltip": "Source",
+ "blocked_total": "%value% blocked total",
+ "copied": "Copied to clipboard",
+ "copy": "Copy",
+ "total": "Total",
+ "setup_dns_notice_new": "In order to use DNS-over-HTTPS or DNS-over-TLS , you need to configure Encryption in AdGuard Home settings.",
+ "setup_guide_greeting_title": "Set up AdGuard Home",
+ "setup_guide_greeting_desc": "This wizard will help you configure AdGuard Home:",
+ "setup_guide_greeting_list_1": "Create credentials to access the admin panel",
+ "setup_guide_greeting_list_2": "Set the Web UI address for AdGuard Home",
+ "setup_guide_greeting_list_3": "Set the AdGuard Home DNS server address",
+ "setup_guide_greeting_list_4": "Get instructions for your individual devices",
+ "setup_guide_greeting_list_5": "Get access to the AdGuard Home dashboard ",
+ "setup_guide_greeting_button": "Run wizard",
+ "setup_guide_auth_title": "Set up password protection",
+ "setup_guide_auth_subtitle": "This is required to protect the AdGuard Home admin panel from unauthorized access",
+ "setup_guide_auth_privacy": "I accept the Privacy policy and Terms and conditions of AdGuard websites",
+ "password_requirements": "Password requirements",
+ "password_requirements_characters": "Minimum 8 characters",
+ "password_requirements_special": "English letters, numbers, and special characters",
+ "password_requirements_lowercase": "At least 1 lowercase letter",
+ "password_requirements_uppercase": "At least 1 uppercase letter",
+ "password_requirements_match": "Passwords match",
+ "setup_complete": "Setup is complete and AdGuard Home is ready to use",
+ "setup_ui_title": "Set the Web UI address for AdGuard Home",
+ "setup_ui_desc": "Use these addresses to access the Web user interface of AdGuard Home. If necessary, change the port",
+ "setup_ui_title_banner": "Web UI settings",
+ "setup_dns_title": "Set the AdGuard Home DNS server address",
+ "setup_dns_title_banner": "DNS server settings",
+ "network_interface": "Network interface",
+ "setup_dns_desc": "Configure your router or other devices to use the following DNS server addresses:",
+ "setup_dns_quote_title": "Assign a static IP address to the AdGuard Home DNS server on your local network",
+ "setup_dns_quote_desc": "To function properly, the AdGuard Home DNS server requires a static IP address on your local network. Otherwise, your router may eventually assign it a different IP address and disrupt the DNS protection",
+ "setup_guide_title": "Set up your devices to use AdGuard Home",
+ "port_in_use": "This port is already in use",
+ "forgot_password": "Forgot password?",
+ "forgot_password_page_desc": "AdGuard Home stores passwords as BCrypt-encrypted hashes",
+ "forgot_password_list_title": "To change your password:",
+ "forgot_password_list_item_1": "Create a new password using the htpasswd tool or any online BCrypt generator.",
+ "forgot_password_list_item_2": "Stop AdGuard Home.",
+ "forgot_password_list_item_3": "Open AdGuardHome.yaml, find the password field, and replace it with the new value.",
+ "forgot_password_list_item_4": "Start AdGuard Home.",
+ "forgot_password_list_item_5": "Log in to the web interface with your new password.",
+ "login": "Log in",
+ "period_notify": "To change the displayed periods, edit Statistics retention in General settings ",
+ "password_login_error": "Your username and password do not match",
+ "last_hour": "Last hour",
+ "last_hours": "| Last %count% hour | Last %count% hours",
+ "last_days": "| Last %count% day | Last %count% days",
+ "dhcp": "DHCP",
+ "dhcp_settings": "DHCP settings",
+ "dhcp_enable": "Use AdGuard's own built-in DHCP server",
+ "enabled_dhcp": "DHCP server enabled",
+ "disabled_dhcp": "DHCP server disabled",
+ "unavailable_dhcp": "DHCP is unavailable",
+ "unavailable_dhcp_desc": "AdGuard Home cannot run a DHCP server on your OS",
+ "check_dhcp_servers": "Check for DHCP servers",
+ "dhcp_ipv4_settings": "DHCP IPv4 settings",
+ "dhcp_ipv6_settings": "DHCP IPv6 settings",
+ "dhcp_leases": "Dynamic leases",
+ "dhcp_static_leases": "Static leases",
+ "dhcp_leases_not_found": "No DHCP leases found",
+ "dhcp_static_leases_not_found": "No DHCP static leases found",
+ "dhcp_config_saved": "DHCP configuration successfully saved",
+ "dhcp_not_found": "DHCP server is active",
+ "dhcp_found": "DHCP server can’t be activated",
+ "dhcp_warning": "To enable this DHCP server, deactivate all other DHCP servers on your network. Otherwise, devices may lose Internet access",
+ "dhcp_error": "Failed to check the DHCP server. Wait a bit before retrying",
+ "dhcp_static_ip_error": "The DHCP server requires a static IP address. We could not confirm that this network interface has one",
+ "dhcp_dynamic_ip_found": "Your system uses dynamic IP address configuration for interface %interface_name%. In order to use DHCP server, a static IP address must be set. Your current IP address is %ip%. AdGuard Home will automatically set this IP address as static if you press the \"Enable DHCP server\" button.",
+ "dhcp_lease_added": "Static lease \"%key%\" successfully added",
+ "dhcp_lease_deleted": "Static lease \"%key%\" successfully deleted",
+ "dhcp_lease_updated": "Static lease \"%key%\" successfully updated",
+ "dhcp_new_static_lease": "New static lease",
+ "dhcp_edit_static_lease": "Edit static lease",
+ "dhcp_add_static_lease": "Add static lease",
+ "dhcp_reset_leases": "Reset all leases",
+ "dhcp_reset_leases_confirm": "This will reset all leases",
+ "dhcp_reset_leases_success": "DHCP leases successfully reset",
+ "dhcp_reset": "This will reset the DHCP configuration. All changes will be lost",
+ "dhcp_form_gateway_input": "Gateway IP",
+ "dhcp_form_gateway_address": "Gateway IP address",
+ "dhcp_form_gateway_address_value": "Gateway IP address: %value%",
+ "dhcp_form_subnet_input": "Subnet mask",
+ "dhcp_form_range_title": "IP address range",
+ "dhcp_form_range_start": "Start IP address",
+ "dhcp_form_lease_title": "DHCP lease time, in seconds",
+ "dhcp_interface_select": "DHCP interface",
+ "dhcp_hardware_address_value": "Hardware address: %value%",
+ "dhcp_ip_addresses_value": "IP addresses: %value%",
+ "dhcp_table_mac_address": "MAC address",
+ "dhcp_table_ip_address": "IP address",
+ "dhcp_table_hostname": "Hostname",
+ "dhcp_mac_address_already_added": "This MAC address is already added",
+ "reset_settings": "Reset settings",
+ "reset_settings_confirm": "Yes, reset",
+ "actions_table_header": "Actions",
+ "edit_table_action": "Edit",
+ "delete_table_action": "Delete",
+ "delete_table_action_confirm": "Yes, delete",
+ "refresh_btn": "Refresh",
+ "make_static": "Make static",
+ "form_enter_mac": "Enter a MAC address",
+ "form_enter_ip": "Enter an IP address",
+ "form_enter_hostname": "Enter a hostname",
+ "delete_confirm": "Delete lease?",
+ "delete_confirm_desc": "This will free %ip% ",
+ "set_static_ip_manually": "Set static IP address",
+ "try_again": "Try again",
+ "form_error_ip_already_added": "This IP address is already added",
+ "form_error_mac_already_added": "This MAC address is already added",
+ "form_error_hostname_format": "Use numbers, lowercase English letters, or hyphens",
+ "make_static_desc": "This action converts a dynamic address to a static one",
+ "show_more_count": "+ %count% more",
+ "dhcp_leases_title": "DHCP leases",
+ "dhcp_v6_unavailable": "Unavailable",
+ "reset_dhcp_settings": "Reset DHCP settings",
+ "query_log": "Query log",
+ "domain_or_client": "Domain or client",
+ "query_log_strict_search": "Use double quotes for strict search",
+ "yes": "Yes",
+ "no": "No",
+ "error": "Error",
+ "invalid": "Invalid",
+ "disallow_client_confirm_title": "Disallow this client?",
+ "disallow_client_confirm_text": "This will drop all future DNS queries from client %ip% and remove it from the list of allowed clients",
+ "disallow_client_confirm_allowlist_note": "This action also will exclude the rule %ip% from the list of allowed clients.",
+ "yes_disallow": "Yes, disallow",
+ "type_value": "Type: %value%",
+ "query_log_detail_time": "Time: %value% ",
+ "query_log_detail_date": "Date: %value% ",
+ "query_log_detail_domain": "Domain: %value% ",
+ "query_log_detail_ecs": "ECS: %value% ",
+ "query_log_detail_type": "Type: %value% ",
+ "query_log_detail_protocol": "Protocol: %value% ",
+ "query_log_detail_name": "Name: %value% ",
+ "query_log_detail_category": "Category: %value% ",
+ "query_log_detail_source": "Source: %value% ",
+ "query_log_detail_served_from_cache": "Served from cache: %value% ",
+ "query_log_detail_status": "Status: %value% ",
+ "query_log_detail_dns_server": "DNS server: %value% ",
+ "query_log_detail_elapsed": "Elapsed: %value% ",
+ "query_log_detail_response_code": "Response code: %value% ",
+ "query_log_detail_response": "Response:",
+ "query_log_detail_service_name": "Service name: %value% ",
+ "query_log_detail_rules": "Rule(s):",
+ "query_log_detail_original_response": "Original response:",
+ "query_log_detail_address": "Address: %value% ",
+ "query_log_detail_country": "Country: %value% ",
+ "query_log_detail_network": "Network: %value% ",
+ "query_log_detail_reason": "Reason:",
+ "query_log_all_reasons": "All reasons",
+ "query_log_blocked_by_filter": "Blocked by filter",
+ "query_log_blocked_by_parental_control": "Blocked by parental control",
+ "query_log_blocked_services": "Blocked services",
+ "query_log_blocked_threats": "Blocked threats",
+ "query_log_custom_filtering_rules": "Custom filtering rules",
+ "query_log_rewritten": "Rewritten",
+ "query_log_safe_search": "Safe search",
+ "query_log_all_statuses": "All statuses",
+ "query_log_allowed": "Allowed",
+ "query_log_blocked": "Blocked",
+ "query_log_processed": "Processed",
+ "status_table_header": "Status",
+ "reason_table_header": "Reason",
+ "query_details": "Query details",
+ "client_ip": "Client IP",
+ "client_location": "Client location",
+ "query_log_nothing_available": "Nothing found",
+ "query_log_nothing_available_rotation": "Query log is disabled in General settings",
+ "allow_service": "Allow service",
+ "add_to_allowlist": "Add to allowlist",
+ "notify_updated": "Updated",
+ "blocked_services": "Blocked services",
+ "blocked_services_desc": "Select popular websites and services that should be blocked",
+ "blocked_service": "Blocked service",
+ "inactivity_schedule": "Inactivity schedule",
+ "inactivity_schedule_desc": "Set inactivity periods for blocking services",
+ "parental_group_ai": "AI",
+ "parental_group_cdn": "CDN",
+ "parental_group_dating": "Dating",
+ "parental_group_gambling": "Gambling, betting",
+ "parental_group_gaming": "Gaming",
+ "parental_group_messaging": "Messaging",
+ "parental_group_privacy": "Privacy tools",
+ "parental_group_shopping": "Shopping",
+ "parental_group_social": "Social networks",
+ "parental_group_development": "Software development",
+ "parental_group_streaming": "Streaming",
+ "parental_group_hosting": "Web hosting",
+ "parental_group_messenger": "Messaging",
+ "parental_group_social_network": "Social networks",
+ "parental_group_software": "Software development",
+ "search_placeholder": "Search",
+ "sunday": "Sunday",
+ "monday": "Monday",
+ "tuesday": "Tuesday",
+ "wednesday": "Wednesday",
+ "thursday": "Thursday",
+ "friday": "Friday",
+ "saturday": "Saturday",
+ "inactivity_schedule_no_schedules": "No schedule",
+ "inactivity_schedule_all_day": "All day",
+ "inactivity_schedule_delete_desc": "Service blocking will be active all day on %value% ",
+ "inactivity_schedule_add": "Add schedule",
+ "inactivity_schedule_edit": "Edit schedule",
+ "inactivity_schedule_delete": "Delete schedule",
+ "inactivity_schedule_time_from": "Time from",
+ "inactivity_schedule_time_to": "Time to",
+ "inactivity_schedule_replace_desc": "This schedule will replace any existing schedules for the same day of the week. Each day can have only one inactivity period",
+ "inactivity_schedule_timezone": "Timezone",
+ "save_btn": "Save",
+ "cancel_btn": "Cancel",
+ "delete_btn": "Delete",
+ "static_dhcp_leases_not_found": "Static DHCP leases not found",
+ "dynamic_dhcp_leases_not_found": "Dynamic DHCP leases not found",
+ "user_rules_title": "Custom rules",
+ "user_rules_desc": "Enter one rule per line. Use Adblock-style or hosts file syntax",
+ "user_rules_placeholder": "This is a placeholder",
+ "user_rules_example_block": "||%domain% Block access to %domain% and all its subdomains",
+ "user_rules_example_unblock": "@@||%domain% Unblock access to %domain% and all its subdomains",
+ "user_rules_example_response": "127.0.0.1 %domain% Respond with 127.0.0.1 for %domain% only, not for its subdomains",
+ "user_rules_example_comment": "! This is a comment To comment, start the line with ! or #",
+ "user_rules_example_comment_2": "# This is also a comment To comment, start the line with ! or #",
+ "user_rules_example_regex": "/REGEX/ Block access to domains that match the specified regular expression",
+ "user_rules_learn_more": "Learn more about creating your own hosts lists.",
+ "user_rules_check_title": "Check domain filtering globally or for a client",
+ "user_rules_check_hostname_label": "Hostname or domain name",
+ "user_rules_check_hostname_placeholder": "Enter target",
+ "user_rules_check_client_label": "Client identifier (name, ClientID, or IP address)",
+ "user_rules_check_client_placeholder": "Enter client identifier",
+ "user_rules_check_dns_record_type_label": "DNS record type",
+ "user_rules_check_button": "Check",
+ "user_rules_domain_blocked": "Domain is blocked",
+ "user_rules_rule": "Rule: %rule%",
+ "user_rules_ip": "IP addresses: %ip%",
+ "user_rules_cname": "CNAME: %cname%",
+ "user_rules_reason": "Reason: %reason%",
+ "user_rules_status": "Status: %reason%",
+ "user_rules_service": "Service: %service%",
+ "user_rules_dns_record_type_placeholder": "Select DNS record type",
+ "user_rules_add_to_allowlist": "Add to allowlist",
+ "user_rules_domain": "Domain: %value%",
+ "user_rules_redirected_to": "Redirected to: %value%",
+ "user_rules_rewritten_to": "Rewritten to: %value%",
+ "user_rules_source": "Source: %value%",
+ "user_rules_source_blocklist": "Blocklist: %value%",
+ "user_rules_source_allowlist": "Allowlist: %value%",
+ "user_rules_reason_blocked_by": "Blocked by %source%",
+ "user_rules_reason_filtered_by": "Filtered by %source%",
+ "user_rules_reason_allowed_by": "Allowed by %source%",
+ "notify_undo": "Undo",
+ "user_rules_domain_is_allowed": "Domain is allowed",
+ "user_rules_rule_added_to_custom_filtering_rules": "Rule added to custom filtering rules",
+ "user_rules_rule_added": "User rule added: %rule% ",
+ "user_rules_disable_parental_control": "Disable Parental control",
+ "user_rules_parental_control_disabled": "Parental control disabled",
+ "user_rules_disable_browsing_security": "Disable Browsing security",
+ "user_rules_browsing_security_disabled": "Browsing security disabled",
+ "user_rules_disable_safe_search": "Disable Safe Search",
+ "user_rules_disable_filter": "Disable filter",
+ "user_rules_filter_was_disabled": "%value% was disabled",
+ "user_rules_rewrite_rule_is_applied": "Rewrite rule is applied",
+ "user_rules_safe_search_disabled": "Safe search disabled",
+ "user_rules_domain_is_processed": "Domain is processed",
+ "user_rules_no_rules_matched": "Result: No rules matched",
+ "user_rules_allow_service": "Allow service",
+ "user_rules_rule_removed": "Rule removed from custom filtering rules",
+ "close_result": "Close result",
+ "user_rules_service_allowed": "The %value% service was allowed",
+ "user_rules_remove_dns_rewrite": "Remove DNS rewrite",
+ "user_rules_edit_dns_rewrite": "Edit DNS rewrite",
+ "user_rules_dns_rewrite_removed": "Rule removed from DNS rewrite",
+ "clients_add": "Add client",
+ "clients_edit": "Edit client",
+ "requests_table_header": "Requests",
+ "clients_remove_title": "Remove client?",
+ "clients_remove_desc": "%value% will be removed from Persistent clients along with its settings",
+ "clients_add_name": "Client name",
+ "clients_add_placeholder": "Enter client name",
+ "clients_identifiers": "Identifiers",
+ "clients_identifiers_desc": "You can use the client’s IP address, CIDR, MAC address, or ClientID (for DoT, DoH, and DoQ). Learn more ",
+ "clients_add_identifier": "Add identifier",
+ "clients_identifier_format_error": "Use numbers, lowercase English letters, or hyphens",
+ "clients_identifier_already_used": "This identifier is already used",
+ "clients_tags": "Tags",
+ "clients_tags_desc": "You can add tags for this client and include them in filtering rules. Learn more ",
+ "clients_tags_placeholder": "Add tags",
+ "clients_use_global_settings": "Use global settings",
+ "clients_use_global_settings_desc": "Inherit protection settings from your AdGuard Home account",
+ "clients_protection": "Protection",
+ "clients_protection_desc": "Enable or disable protection features, logging, and statistics",
+ "clients_upstreams_desc": "Upstream DNS servers, one per line",
+ "clients_use_dns_cache": "Use DNS cache",
+ "clients_dns_cache_size": "DNS cache size, in bytes",
+ "clients_dns_cache_size_placeholder": "Use numbers",
+ "remove_tag": "Remove %value%",
+ "clients_logs_and_statistics": "Logs and statistics",
+ "clients_dont_log": "Don't log this client",
+ "clients_dont_collect_stats": "Don't collect stats for this client",
+ "clients": "Clients",
+ "aria_clear_input": "Clear input",
+ "aria_previous_page": "Previous page",
+ "aria_next_page": "Next page",
+ "update_available": "Version %version% is available. Release notes ",
+ "update_button": "Update",
+ "update_how_to": "How to update",
+ "tls_certificate_expiring": "Your TLS certificate is about to expire",
+ "tls_certificate_expired": "Your TLS certificate has expired",
+ "version_number": "Version %value%",
+ "update_failed": "Auto-update failed. Please follow these steps to update manually"
+}
diff --git a/client_v2/src/__locales/es.json b/client_v2/src/__locales/es.json
new file mode 100644
index 00000000000..2404d827308
--- /dev/null
+++ b/client_v2/src/__locales/es.json
@@ -0,0 +1,864 @@
+{
+ "access_settings_allowed_faq": "Una lista de CIDR, direcciones IP o ClientID . Si este campo contiene entradas, AdGuard Home solo aceptará solicitudes de estos clientes",
+ "access_settings_allowed_placeholder": "Ingresa los clientes permitidos",
+ "access_settings_allowed_title": "Clientes permitidos, uno por línea",
+ "access_settings_blocked_faq_1": "AdGuard Home descartará las consultas DNS que coincidan con estos dominios. Dichas consultas no aparecen en el registro de consultas. Puedes introducir nombres de dominio exactos, comodines o reglas de filtrado de URL, como example.org, *.example.org o ||example.org^",
+ "access_settings_blocked_faq_2": "No debe confundirse con los filtros",
+ "access_settings_blocked_placeholder": "Introduce dominios no permitidos",
+ "access_settings_blocked_title": "Dominios no permitidos, uno por línea",
+ "access_settings_disallowed_faq": "Una lista de CIDR, direcciones IP o ClientID . Si este campo contiene entradas, AdGuard Home rechazará las solicitudes procedentes de estos clientes. Este campo se ignora si hay entradas en «Clientes permitidos ",
+ "access_settings_disallowed_placeholder": "Introduce los clientes no autorizados",
+ "access_settings_disallowed_title": "Clientes no autorizados, uno por línea",
+ "access_settings_saved_toast": "Configuración de acceso guardada",
+ "access_settings_title": "Configuración de acceso",
+ "actions_table_header": "Acciones",
+ "add": "Añadir",
+ "add_allowlist": "Añadir lista de permitido",
+ "add_blocklist": "Añadir lista de bloqueo",
+ "add_persistent_client": "Añadir como cliente persistente",
+ "add_to_allowlist": "Añadir a la lista de permitido",
+ "ads_blocked": "Anuncios bloqueados",
+ "ads_blocked_card": "Anuncios bloqueados",
+ "ads_blocked_tooltip": "El número de consultas DNS bloqueadas por los filtros de bloqueo de anuncios y la lista negra de hosts",
+ "adult_websites_blocked": "Sitios web para adultos bloqueados",
+ "adult_websites_blocked_tooltip": "Número de sitios web para adultos bloqueado",
+ "all_domains": "Todos los dominios",
+ "all_lists_up_to_date_toast": "Todas las listas ya están actualizadas",
+ "all_rewrites_disabled": "Se desactivarán todas las respuestas DNS personalizadas",
+ "all_rewrites_enabled": "Se habilitarán todas las respuestas DNS personalizadas",
+ "allow_service": "Permitir servicio",
+ "allowed": "Permitido",
+ "allowlist_add": "Añadir lista de permitido",
+ "allowlist_edit": "Editar lista de permitido",
+ "allowlist_empty": "Aún no hay listas de permitidos: añade la primera ",
+ "allowlist_placeholder_example": "Mi lista de permitidos",
+ "allowlist_remove": "¿Eliminar lista de permitidos?",
+ "allowlist_remove_desc": "%value% será eliminado",
+ "allowlists": "Listas de permitidos",
+ "allowlists_title": "Listas de permitidos",
+ "apply": "Aplicar",
+ "aria_clear_input": "Borrar entrada",
+ "autofix_warning_list": "Realizará estas tareas:
Desactivar el DNSStubListener del sistema
Establecer la dirección del servidor DNS en 127.0.0.1
Reemplazar el destino del enlace simbólico de /etc/resolv.conf con /run/systemd/resolve/resolv.conf
Detener el DNSStubListener (recargar el servicio systemd-resolved)
",
+ "autofix_warning_result": "Como resultado, todas las peticiones DNS de tu sistema serán procesadas por AdGuard Home de manera predeterminada.",
+ "autofix_warning_text": "Si haces clic en \"Corregir\", AdGuard Home configurará tu sistema para utilizar el servidor DNS de AdGuard Home.",
+ "average_time_processing": "Tiempo promedio de procesamiento",
+ "average_time_processing_tooltip": "Tiempo medio, en milisegundos, de procesamiento de una consulta DNS",
+ "average_upstream_response_time": "Tiempo promedio de respuesta del proveedor DNS",
+ "back": "Atrás",
+ "block": "Bloquear",
+ "block_client": "Bloquear cliente",
+ "block_for_this_client_only": "Bloquear solo para este cliente",
+ "blocked": "Bloqueado",
+ "blocked_adult_websites": "Bloqueado por el control parental",
+ "blocked_by_cname_or_ip": "Bloqueado por CNAME o IP",
+ "blocked_queries": "Consultas bloqueadas",
+ "blocked_service": "Servicio bloqueado",
+ "blocked_services": "Servicios bloqueados",
+ "blocked_services_desc": "Selecciona sitios web y servicios populares que deberían bloquearse",
+ "blocked_threats": "Amenazas bloqueadas",
+ "blocked_threats_chart": "Amenazas bloqueadas",
+ "blocked_total": "%value% total bloqueado",
+ "blocklist_add_from_list": "Añadir de la lista",
+ "blocklist_add_manual": "Añadir manualmente",
+ "blocklist_contents": "Contenido (.%value%)",
+ "blocklist_edit": "Editar lista de bloqueo",
+ "blocklist_homepage": "Página de inicio",
+ "blocklist_placeholder_example": "Mi lista de bloqueo",
+ "blocklist_remove": "¿Eliminar lista de bloqueo?",
+ "blocklist_remove_desc": "%value% será eliminado",
+ "blocklist_url_file_path": "URL o ruta del archivo",
+ "blocklists_add": "Añadir listas de bloqueo",
+ "blocklists_add_desc": "Puedes seleccionar listas de bloqueo de la lista o añadirlas manualmente",
+ "blocklists_desc": "Los dominios de estas listas están bloqueados",
+ "blocklists_empty": "Aún no hay listas de bloqueo — añade la primera ",
+ "blocklists_title": "Listas de bloqueo",
+ "bootstrap_dns": "Servidores DNS de arranque",
+ "browse": "Buscar",
+ "cache_cleared": "Caché DNS borrado correctamente",
+ "cache_config_clear": "Borrar caché",
+ "cache_config_max_ttl": "Anulación máxima TTL, en segundos",
+ "cache_config_max_ttl_faq": "El valor máximo de tiempo de vida (TTL) para las entradas en la caché DNS",
+ "cache_config_max_ttl_placeholder": "Ingresa el TTL máximo",
+ "cache_config_min_ttl": "Anulación mínima de TTL, en segundos",
+ "cache_config_min_ttl_faq": "El valor mínimo de tiempo de vida (TTL) para las entradas de la caché del DNS",
+ "cache_config_min_ttl_placeholder": "Introduce el TTL mínimo",
+ "cache_config_optimistic": "Caché optimista",
+ "cache_config_optimistic_desc": "AdGuard Home responderá desde la caché incluso si las entradas han caducado e intentará actualizarlas",
+ "cache_config_size": "Tamaño de la caché, en bytes",
+ "cache_config_size_faq": "Tamaño de la caché DNS. Para desactivar el almacenamiento en caché, déjalo en blanco",
+ "cache_config_size_validation": "El tamaño de la cache debe ser mayor que cero cuando está habilitado",
+ "cache_config_ttl_validation": "La anulación TTL mínimo de la caché debe ser menor o igual al máximo",
+ "cache_confirm_clear_desc": "Esto eliminará todas las respuestas DNS almacenadas en la caché. Las consultas DNS posteriores pueden tardar más en responderse hasta que se vuelva a generar la caché",
+ "cache_confirm_clear_title": "Borrar la caché de DNS",
+ "cache_enabled": "Habilitar caché",
+ "cache_enabled_desc": "Almacenar las respuestas DNS localmente",
+ "cancel": "Cancelar",
+ "cancel_btn": "Cancelar",
+ "category_tooltip": "Categoría",
+ "changes_saved_success": "Cambios guardados",
+ "check_dhcp_servers": "Comprobar si hay servidores DHCP",
+ "check_not_found": "No se ha encontrado en tus listas de filtros",
+ "check_updates_btn": "Buscar actualizaciones",
+ "clear_btn": "Borrar",
+ "clear_query_log": "Borrar registro de consultas",
+ "client_add": "Añadir cliente",
+ "client_added": "Cliente añadido",
+ "client_already_blocked": "El cliente %ip% ya está bloqueado",
+ "client_blocked_flash": "Cliente bloqueado",
+ "client_details": "Detalles del cliente",
+ "client_id": "ID de cliente",
+ "client_id_faq": "Los clientes pueden identificarse mediante su ClientID. Para obtener más información, visita la página de AdGuard Home en GitHub ",
+ "client_id_placeholder": "Ingresa el ID del cliente",
+ "client_identifier": "Identificador",
+ "client_ip": "IP de cliente",
+ "client_location": "Ubicación del cliente",
+ "client_removed": "Se ha eliminado el cliente",
+ "client_settings": "Configuración de clientes",
+ "client_table_header": "Cliente",
+ "client_unblocked_flash": "Cliente desbloqueado",
+ "clients": "Clientes",
+ "clients_add": "Añadir cliente",
+ "clients_add_identifier": "Añadir identificador",
+ "clients_add_name": "Nombre del cliente",
+ "clients_add_placeholder": "Ingresa el nombre del cliente",
+ "clients_desc": "Reglas para dispositivos conectados a AdGuard Home",
+ "clients_dns_cache_size": "Tamaño de la caché DNS, en bytes",
+ "clients_dns_cache_size_placeholder": "Usa números",
+ "clients_dont_collect_stats": "No recopilar estadísticas para este cliente",
+ "clients_dont_log": "No registrar a este cliente",
+ "clients_edit": "Editar cliente",
+ "clients_identifier_already_used": "Este identificador ya está en uso",
+ "clients_identifier_format_error": "Utiliza números, letras minúsculas o guiones",
+ "clients_identifiers": "Identificadores",
+ "clients_identifiers_desc": "Puede usar la dirección IP del cliente, CIDR, la dirección MAC o el ClientID (para DoT, DoH y DoQ). Más información ",
+ "clients_logs_and_statistics": "Registros y estadísticas",
+ "clients_not_found": "No se han encontrado clientes",
+ "clients_protection": "Protección",
+ "clients_protection_desc": "Habilitar o deshabilitar las funciones de protección, el registro y las estadísticas",
+ "clients_remove_desc": "%value% se eliminará de los clientes persistentes junto con su configuración",
+ "clients_remove_title": "¿Eliminar cliente?",
+ "clients_tags": "Etiquetas",
+ "clients_tags_desc": "Puedes añadir etiquetas para este cliente e incluirlas en las reglas de filtrado. Más información ",
+ "clients_upstreams_desc": "Servidores DNS upstream, uno por línea",
+ "clients_use_dns_cache": "Utilizar la caché de DNS",
+ "clients_use_global_settings": "Usar configuración global",
+ "clients_use_global_settings_desc": "Heredar la configuración de protección de tu cuenta de AdGuard Home",
+ "close": "Cerrar",
+ "close_result": "Cerrar resultado",
+ "confirm": "Confirmar",
+ "confirm_client_block_desc": "AdGuard Home descartará todas las solicitudes DNS procedentes del cliente %ip% ",
+ "confirm_client_block_title": "¿Bloquear al cliente?",
+ "confirm_client_unblock_desc": "Las solicitudes DNS procedentes del cliente %ip% se permitirán",
+ "confirm_client_unblock_title": "¿Desbloquear al cliente?",
+ "confirm_static_ip": "AdGuard Home configurará %ip% como tu dirección IP estática. ¿Quieres continuar?",
+ "copied": "Copiado al portapapeles",
+ "copy": "Copiar",
+ "copy_to_clipboard": "Copiar al portapapeles",
+ "custom_filtering_rules": "Reglas de filtrado personalizado",
+ "dark_theme": "Tema oscuro",
+ "dashboard": "Panel de control",
+ "delete_btn": "Eliminar",
+ "delete_confirm": "¿Eliminar la concesión?",
+ "delete_confirm_desc": "Esto liberará %ip% ",
+ "delete_table_action": "Eliminar",
+ "delete_table_action_confirm": "Sí, borrar",
+ "device_type": "Tipo de dispositivo",
+ "dhcp_add_static_lease": "Añadir asignación estática",
+ "dhcp_config_saved": "Configuración DHCP guardado correctamente",
+ "dhcp_edit_static_lease": "Editar asignación estática",
+ "dhcp_error": "No se pudo comprobar el servidor DHCP. Espere un momento antes de volver a intentarlo",
+ "dhcp_form_gateway_address": "Dirección IP de gateway",
+ "dhcp_form_gateway_input": "IP de puerta de enlace",
+ "dhcp_form_range_start": "Dirección IP inicial",
+ "dhcp_form_range_title": "Rango de direcciones IP",
+ "dhcp_form_subnet_input": "Máscara de subred",
+ "dhcp_found": "No se puede activar el servidor DHCP",
+ "dhcp_ipv4_settings": "Configuración DHCP IPv4",
+ "dhcp_ipv6_settings": "Configuración DHCP IPv6",
+ "dhcp_lease_added": "Concesión estática \"%key%\" agregada correctamente",
+ "dhcp_lease_deleted": "Concesión estática \"%key%\" eliminada correctamente",
+ "dhcp_lease_updated": "El contrato de arrendamiento estático «%key%» se ha actualizado correctamente",
+ "dhcp_leases_not_found": "No se han encontrado asignaciones DHCP",
+ "dhcp_leases_title": "Asignaciones DHCP",
+ "dhcp_mac_address_already_added": "Esta dirección MAC ya está añadida",
+ "dhcp_new_static_lease": "Nueva asignación estática",
+ "dhcp_not_found": "El servidor DHCP está activo",
+ "dhcp_reset": "Esto restablecerá la configuración DHCP. Se perderán todos los cambios",
+ "dhcp_reset_leases": "Restablecer todas las asignaciones",
+ "dhcp_reset_leases_confirm": "Esto restablecerá todas las concesiones",
+ "dhcp_reset_leases_success": "Asignaciones DHCP restablecidas correctamente",
+ "dhcp_settings": "Configuración DHCP",
+ "dhcp_static_ip_error": "El servidor DHCP requiere una dirección IP estática. No hemos podido confirmar que esta interfaz de red disponga de una",
+ "dhcp_static_leases_not_found": "No se han encontrado asignaciones DHCP estáticas",
+ "dhcp_table_hostname": "Nombre del host",
+ "dhcp_table_ip_address": "Dirección IP",
+ "dhcp_table_mac_address": "Dirección MAC",
+ "disable": "Deshabilitar",
+ "disable_protection_btn": "Deshabilitar protección",
+ "disabled_dhcp": "Servidor DHCP deshabilitado",
+ "disallow_client_confirm_allowlist_note": "Esta acción también excluirá la regla %ip% de la lista de clientes permitidos.",
+ "disallow_client_confirm_title": "¿Bloquear este cliente?",
+ "disallow_this_client": "No permitir a este cliente",
+ "dns_access_settings_title": "Configuración de acceso",
+ "dns_allowed_clients": "Clientes permitidos",
+ "dns_allowed_clients_label": "Clientes permitidos, uno por línea",
+ "dns_allowed_clients_placeholder": "Ingresa los clientes permitidos",
+ "dns_blocking_mode": "Modo de bloqueo",
+ "dns_blocking_mode_custom_ip": "Dirección IP personalizada",
+ "dns_blocking_mode_default": "Predeterminado",
+ "dns_blocking_mode_ipv4_label": "Bloqueo de IPv4",
+ "dns_blocking_mode_ipv4_placeholder": "Ingresa la dirección IPv4",
+ "dns_blocking_mode_ipv6_label": "Bloqueo de IPv6",
+ "dns_blocking_mode_ipv6_placeholder": "Ingresa la dirección IPv6",
+ "dns_blocking_mode_null_ip": "Dirección IP nula",
+ "dns_blocking_mode_nxdomain": "NXDOMAIN",
+ "dns_blocking_mode_refused": "REFUSED",
+ "dns_blocking_mode_title": "Modo de bloqueo",
+ "dns_blocking_mode_ttl_label": "TTL de respuesta bloqueada, en segundos",
+ "dns_bootstrap_dns_label": "Servidores DNS de arranque, uno por línea",
+ "dns_bootstrap_dns_placeholder": "Direcciones IP",
+ "dns_bootstrap_dns_title": "Servidores DNS de arranque",
+ "dns_bootstrap_servers": "Servidores DNS de arranque",
+ "dns_cache_config": "Configuración de la caché DNS",
+ "dns_cache_configuration_saved_toast": "Configuración de la caché DNS guardada",
+ "dns_cache_size": "Tamaño de la caché",
+ "dns_cache_size_label": "Tamaño de la caché DNS, en bytes",
+ "dns_cache_size_title": "Tamaño de la caché",
+ "dns_clear_cache": "Borrar caché",
+ "dns_clear_cache_cancel": "Cancelar",
+ "dns_clear_cache_confirm": "Sí, borrar",
+ "dns_clear_cache_yes": "Sí, borrar",
+ "dns_config": "Configuración del servidor DNS",
+ "dns_disallowed_clients": "Clientes no permitidos",
+ "dns_disallowed_clients_label": "Clientes no autorizados, uno por línea",
+ "dns_disallowed_clients_title": "Clientes no permitidos",
+ "dns_disallowed_domains": "Dominios no permitidos",
+ "dns_disallowed_domains_label": "Dominios no permitidos, uno por línea",
+ "dns_disallowed_domains_title": "Dominios no permitidos",
+ "dns_edns_custom_placeholder": "Ingrese la dirección IP",
+ "dns_fallback_dns_label": "Servidores DNS fallback, uno por línea",
+ "dns_fallback_dns_placeholder": "Direcciones IP, sdns://, quic://, https://, h3:// o tls://",
+ "dns_fallback_dns_title": "Servidores DNS alternativos",
+ "dns_fallback_servers": "Servidores DNS alternativos",
+ "dns_optimistic_caching": "Caché optimista",
+ "dns_over_https": "DNS mediante HTTPS",
+ "dns_over_quic": "DNS-over-QUIC",
+ "dns_over_tls": "DNS mediante TLS",
+ "dns_override_max_ttl": "Anular TTL máximo",
+ "dns_override_max_ttl_title": "Anular TTL máximo",
+ "dns_override_min_ttl": "Anular TTL mínimo",
+ "dns_override_min_ttl_title": "Anular TTL mínimo",
+ "dns_privacy": "DNS cifrado",
+ "dns_private_reverse_servers_label": "Servidores DNS inversos privados, uno por línea",
+ "dns_private_reverse_servers_placeholder": "Direcciones IP, sdns://, quic://, https://, h3:// o tls://",
+ "dns_private_reverse_servers_title": "Servidores DNS inversos y privados",
+ "dns_queries": "Consultas DNS",
+ "dns_queries_tooltip": "El número de consultas DNS procesadas en las últimas 24 horas",
+ "dns_query": "Consultas DNS",
+ "dns_rate_limit": "Límite de cantidad",
+ "dns_rate_limit_allowlist": "Lista de permisos de limitación de velocidad",
+ "dns_rate_limit_allowlist_placeholder": "Direcciones IP",
+ "dns_rate_limit_allowlist_title": "Lista de permisos de limitación de velocidad",
+ "dns_rate_limit_placeholder": "Ingresa el límite de cantidad",
+ "dns_rate_limit_title": "Límite de cantidad",
+ "dns_rewrites": "Reescrituras DNS",
+ "dns_server_addresses": "Direcciones del servidor",
+ "dns_server_addresses_label": "Direcciones de servidor, una por línea",
+ "dns_server_addresses_placeholder": "Direcciones IP, sdns://, quic://, https://, h3:// o tls://",
+ "dns_server_addresses_title": "Direcciones del servidor",
+ "dns_server_config_title": "Configuración del servidor DNS",
+ "dns_server_configuration_saved_toast": "Configuración del servidor DNS guardada",
+ "dns_settings": "DNS",
+ "dns_subnet_placeholder": "Introduce la longitud del prefijo",
+ "dns_test_upstreams": "Probar proveedores DNS",
+ "dns_upstream_servers_title": "Proveedores DNS",
+ "dns_upstream_timeout": "Tiempo de espera del proveedor DNS",
+ "dns_upstream_timeout_title": "Tiempo de espera del proveedor DNS",
+ "dns_upstream_validation_invalid": "Formato no válido",
+ "domain": "Dominio",
+ "domain_or_client": "Dominio o cliente",
+ "download_mobileconfig": "Descargar archivo de configuración",
+ "dynamic_dhcp_leases_not_found": "No se encontraron concesiones DHCP dinámicas",
+ "edit_table_action": "Editar",
+ "enable": "Habilitar",
+ "enabled_dhcp": "Servidor DHCP habilitado",
+ "encrypted_dns_addresses": "Direcciones de servidores DNS cifradas",
+ "encryption_certificate_has_issues": "El certificado tiene problemas",
+ "encryption_certificate_path": "Ruta de acceso al certificado",
+ "encryption_certificates": "Certificados",
+ "encryption_certificates_desc": "Para usar el cifrado, debes proporcionar una cadena de certificados SSL válida para tu dominio. Puedes obtener un certificado SSL gratuito de letsencrypt.org o comprar uno de una Autoridad de Certificación de confianza",
+ "encryption_certificates_input": "Copia/pega aquí tu certificado codificado PEM.",
+ "encryption_certificates_source_content": "Pegar el contenido del certificado",
+ "encryption_certificates_source_path": "Establecer una ruta para el archivo de certificado",
+ "encryption_chain_invalid": "La cadena de certificado no es válida",
+ "encryption_chain_valid": "La cadena de certificado es válida",
+ "encryption_config_saved": "Configuración de cifrado guardado",
+ "encryption_confirm_clear": "Restablecer la configuración de cifrado",
+ "encryption_confirm_clear_desc": "Todos los ajustes de cifrado se restablecerán a sus valores predeterminados",
+ "encryption_disable_plain_dns": "¿Deshabilitar el DNS sin cifrado?",
+ "encryption_disable_plain_dns_desc": "Los dispositivos que no admiten DNS cifrado pueden no funcionar correctamente",
+ "encryption_doq": "Puerto DNS mediante QUIC",
+ "encryption_doq_tooltip": "Este puerto se utiliza para ejecutar un servidor DNS-over-QUIC",
+ "encryption_dot": "Puerto DNS mediante TLS",
+ "encryption_dot_tooltip": "Este puerto se utiliza para ejecutar un servidor DNS-over-TLS",
+ "encryption_encrypted_dns": "DNS cifrado",
+ "encryption_encrypted_dns_desc": "La interfaz de administración de AdGuard Home utiliza HTTPS. El servidor DNS admite DNS-over-HTTPS, DNS-over-TLS y DNS-over-QUIC",
+ "encryption_expire": "Expira: %value%",
+ "encryption_force_redirect": "Redirigir automáticamente de HTTP a HTTPS",
+ "encryption_hostnames": "Nombres de host: %value%",
+ "encryption_https": "Puerto HTTPS",
+ "encryption_https_tooltip": "Este puerto proporciona acceso HTTPS para la interfaz de administración de AdGuard Home. También proporciona DNS-over-HTTPS en la ubicación '/dns-query'",
+ "encryption_issuer": "Emisor: %value%",
+ "encryption_key": "Clave privada",
+ "encryption_key_input": "Copia/pega aquí tu clave privada codificada PEM para tu certificado.",
+ "encryption_key_invalid": "La clave privada no es válida",
+ "encryption_key_source_content": "Pegar el contenido de la clave privada",
+ "encryption_key_source_path": "Establecer un archivo de clave privada",
+ "encryption_key_type": "Algoritmo de cifrado: %value%",
+ "encryption_key_valid": "La clave privada es válida",
+ "encryption_plain_dns": "DNS simple",
+ "encryption_plain_dns_error": "Para deshabilitar el DNS simple, habilita al menos un protocolo DNS cifrado",
+ "encryption_private_key_path": "Ruta de acceso a la clave privada",
+ "encryption_server": "Nombre del servidor",
+ "encryption_server_enter": "Ingresa el nombre del dominio",
+ "encryption_server_tooltip_1": "Si se establece, AdGuard Home detectará ClientIDs, responderá a consultas DDR y realizará validaciones adicionales de conexión. Si no se establece, estas funciones estarán deshabilitadas",
+ "encryption_server_tooltip_2": "Debe coincidir con un nombre DNS que figure en el certificado",
+ "encryption_subject": "Asunto: %value%",
+ "encryption_title": "Cifrado",
+ "enter_cache_size": "Ingresa el tamaño de la caché (bytes)",
+ "enter_ip_address_placeholder": "Ingrese la dirección IP",
+ "error": "Hubo un error",
+ "fallback_dns": "Servidores DNS alternativos",
+ "filter_added_successfully": "Se ha añadido a la lista de bloqueo: %value% ",
+ "filter_added_successfully_allowlist": "Lista de permitidos añadida: %value% ",
+ "filter_added_successfully_more": "Listas de bloqueo añadidas: %value% y %more% más",
+ "filter_category_general": "General",
+ "filter_category_general_desc": "Listas que bloquean rastreadores y anuncios en la mayoría de los dispositivos",
+ "filter_category_other": "Otros",
+ "filter_category_other_desc": "Otras listas de bloqueo",
+ "filter_category_regional": "Regional",
+ "filter_category_regional_desc": "Listas que se centran en anuncios regionales y servidores de rastreo",
+ "filter_category_security": "Seguridad",
+ "filter_category_security_desc": "Listas diseñadas específicamente para bloquear dominios de malware, phishing y estafa",
+ "filter_removed_successfully": "Lista de bloqueo eliminada: %value% ",
+ "filter_removed_successfully_allowlist": "Lista de permitidos eliminada: %value% ",
+ "filters": "Suscripciones a filtros",
+ "forgot_password": "¿Olvidaste tu contraseña?",
+ "forgot_password_list_item_1": "Crea una nueva contraseña con la herramienta htpasswd o con cualquier generador de BCrypt en línea.",
+ "forgot_password_list_item_2": "Detén AdGuard Home.",
+ "forgot_password_list_item_3": "Abre el archivo AdGuardHome.yaml, busca el campo de la contraseña y sustitúyelo por el nuevo valor.",
+ "forgot_password_list_item_4": "Inicia AdGuard Home.",
+ "forgot_password_list_item_5": "Inicia sesión en la interfaz web con tu nueva contraseña.",
+ "forgot_password_list_title": "Para cambiar tu contraseña:",
+ "forgot_password_page_desc": "AdGuard Home almacena las contraseñas como hash cifrados con BCrypt",
+ "form_enter_blocked_response_ttl": "Ingresa el TTL de respuesta bloqueada (en segundos)",
+ "form_enter_hostname": "Introduce un nombre de host",
+ "form_enter_ip": "Introduce una dirección IP",
+ "form_enter_mac": "Introduce una dirección MAC",
+ "form_error_answer_format": "Formato de resultado no válido",
+ "form_error_client_id_format": "El ID de cliente debe contener solo números, letras minúsculas y guiones",
+ "form_error_domain_format": "Formato de dominio no válido",
+ "form_error_equal": "No debe ser igual",
+ "form_error_format": "Formato no válido",
+ "form_error_gateway_ip": "La asignación no puede tener la dirección IP de la puerta de enlace",
+ "form_error_hostname_format": "Utiliza números, letras minúsculas o guiones",
+ "form_error_ip4_format": "Dirección IPv4 no válida",
+ "form_error_ip6_format": "Dirección IPv6 no válida",
+ "form_error_ip_already_added": "Esta dirección IP ya está añadida",
+ "form_error_ip_format": "Dirección IP no válida",
+ "form_error_mac_already_added": "Esta dirección MAC ya está añadida",
+ "form_error_mac_format": "Dirección MAC no válida",
+ "form_error_password": "La contraseña no coincide",
+ "form_error_port": "Ingresa un número de puerto válido",
+ "form_error_port_range": "Ingresa el número del puerto en el rango de 80 a 65535",
+ "form_error_port_unsafe": "Puerto inseguro",
+ "form_error_range": "El valor debe estar comprendido entre %min% y %max%",
+ "form_error_server_name": "Nombre de servidor no válido",
+ "form_error_url_format": "Ingresa una URL o una ruta de archivo válida",
+ "found_in_known_domains": "Encontrado en los dominios conocidos",
+ "friday": "Viernes",
+ "gateway_or_subnet_invalid": "Máscara de subred no válida",
+ "general_statistics": "Estadísticas generales",
+ "home_dns_addresses": "Direcciones DNS de AdGuard Home",
+ "home_dns_addresses_desc": "El servidor DNS de AdGuard Home escucha en las siguientes interfaces/puertos:",
+ "ignore_domains_desc_log": "Las solicitudes que coinciden con estas reglas no se muestran en el registro de consultas",
+ "ignore_domains_desc_stats": "Las solicitudes que coinciden con estas reglas no se incluyen en las estadísticas",
+ "ignore_domains_title": "Dominios ignorados",
+ "inactivity_schedule": "Calendario de inactividad",
+ "inactivity_schedule_add": "Añadir horario",
+ "inactivity_schedule_all_day": "Todo el dia",
+ "inactivity_schedule_delete": "Eliminar programación",
+ "inactivity_schedule_delete_desc": "El bloqueo del servicio estará activo todo el día en %value% ",
+ "inactivity_schedule_desc": "Establece períodos de inactividad para bloquear servicios",
+ "inactivity_schedule_edit": "Editar horario",
+ "inactivity_schedule_no_schedules": "No programado",
+ "inactivity_schedule_replace_desc": "Este horario sustituirá cualquier horario existente para el mismo día de la semana. Cada día solo podrá tener un período de inactividad",
+ "inactivity_schedule_time_from": "Hora del inicio",
+ "inactivity_schedule_time_to": "Hora del fin",
+ "inactivity_schedule_timezone": "Zona horaria",
+ "install_auth_confirm": "Confirmar contraseña",
+ "install_auth_password": "Contraseña",
+ "install_auth_password_enter": "Ingresa tu contraseña",
+ "install_auth_username": "Nombre de usuario",
+ "install_auth_username_enter": "Ingresa tu nombre de usuario",
+ "install_saved": "Guardado correctamente",
+ "install_settings_all_interfaces": "Todas las interfaces",
+ "install_settings_port": "Puerto",
+ "install_static_configure": "AdGuard Home ha detectado que se está utilizando la dirección IP dinámica %ip%. ¿Deseas que se configure como tu dirección estática?",
+ "install_static_error": "AdGuard Home no puede configurarlo automáticamente para esta interfaz de red. Busca instrucciones sobre cómo hacer esto manualmente.",
+ "install_static_ok": "¡Buenas noticias! La dirección IP estática ya está configurada",
+ "install_step": "Paso",
+ "install_submit_title": "¡Felicitaciones!",
+ "instructions": "Instrucciones",
+ "invalid": "No válido",
+ "ip_address": "Dirección IP",
+ "ip_addresses_placeholder": "Introduce las direcciones IP, una por línea",
+ "known_tracker": "Rastreador conocido",
+ "language": "Idioma",
+ "last_days": "| Último %count% día | Últimos %count% días",
+ "last_hour": "Última hora",
+ "last_hours": "| Última %count% hora | Últimas %count% horas",
+ "last_updated_label": "Última actualización",
+ "light_theme": "Tema claro",
+ "list_updated": "| %count% lista actualizada | %count% listas actualizadas",
+ "login": "Iniciar sesión",
+ "logout": "Cerrar sesión",
+ "make_static": "Hacer estático",
+ "make_static_desc": "Esta acción convierte una dirección dinámica en una estática",
+ "milliseconds_abbreviation": "ms",
+ "monday": "Lunes",
+ "name": "Nombre",
+ "name_label": "Nombre",
+ "name_table_header": "Nombre",
+ "name_tooltip": "Nombre",
+ "network_interface": "Interfaz de red",
+ "next": "Siguiente",
+ "no": "No",
+ "no_stats_yet": "Aún no hay estadísticas: sigue utilizando AdGuard Home para verlas",
+ "none_text": "Ninguno",
+ "nothing_found": "No se ha encontrado nada",
+ "notify_undo": "Deshacer",
+ "notify_updated": "Actualizado",
+ "open_dashboard": "Abrir el panel de control",
+ "parental_control": "Control parental",
+ "parental_group_ai": "IA",
+ "parental_group_cdn": "CDN",
+ "parental_group_dating": "Citas",
+ "parental_group_development": "Desarrollo de software",
+ "parental_group_gambling": "Juegos de azar y apuestas",
+ "parental_group_gaming": "Juegos",
+ "parental_group_hosting": "Web hosting",
+ "parental_group_messaging": "",
+ "parental_group_messenger": "Mensajería",
+ "parental_group_privacy": "Herramientas de privacidad",
+ "parental_group_shopping": "Compras",
+ "parental_group_social": "Redes sociales",
+ "parental_group_social_network": "Redes sociales",
+ "parental_group_software": "Desarrollo de software",
+ "parental_group_streaming": "Streaming",
+ "password_label": "Contraseña",
+ "password_placeholder": "Ingresa tu contraseña",
+ "password_requirements": "Requisitos de contraseña",
+ "password_requirements_characters": "Mínimo 8 caracteres",
+ "password_requirements_lowercase": "Al menos 1 letra minúscula",
+ "password_requirements_match": "Las contraseñas coinciden",
+ "password_requirements_special": "Letras, números y caracteres especiales",
+ "password_requirements_uppercase": "Al menos 1 letra mayúscula",
+ "pause_for_hour": "Pausa durante %count% hora",
+ "pause_for_minutes": "| Pausa de %count% minuto | Pausa de %count% minutos",
+ "pause_for_seconds": "| Pausa de %count% segundo | Pausa de %count% segundos",
+ "pause_until_tomorrow": "Haz una pausa hasta mañana, %time%",
+ "period_notify": "Para cambiar los períodos mostrados, edita Retención de estadísticas en Configuración general ",
+ "plain_dns": "DNS simple",
+ "plain_dns_addresses": "Direcciones de servidores DNS sin formato",
+ "port_53_faq_link": "El puerto 53 suele estar ocupado por los servicios «DNSStubListener» o «systemd-resolved». Lee estas instrucciones para saber cómo solucionar este problema.",
+ "port_in_use": "Este puerto ya está en uso",
+ "privacy_policy": "Política de privacidad",
+ "processing_time_ms": "%value% ms",
+ "protection": "Protección",
+ "protocol": "Protocolo",
+ "queries": "Consultas",
+ "queries_total": "| %count% consulta en total | %count% consultas en total",
+ "query_details": "Detalles de la consulta",
+ "query_log": "Registro de consultas",
+ "query_log_all_reasons": "Todos los motivos",
+ "query_log_all_statuses": "Todos los estados",
+ "query_log_allowed": "Permitido",
+ "query_log_blocked": "Bloqueado",
+ "query_log_blocked_by_filter": "Bloqueado por filtro",
+ "query_log_blocked_by_parental_control": "Bloqueado por el control parental",
+ "query_log_blocked_services": "Servicios bloqueados",
+ "query_log_blocked_threats": "Amenazas bloqueadas",
+ "query_log_custom_filtering_rules": "Reglas de filtrado personalizado",
+ "query_log_detail_address": "Dirección: %value% ",
+ "query_log_detail_category": "Categoría: %value% ",
+ "query_log_detail_country": "País: %value% ",
+ "query_log_detail_date": "Fecha: %value% ",
+ "query_log_detail_dns_server": "Servidor DNS: %value% ",
+ "query_log_detail_domain": "Dominio: %value% ",
+ "query_log_detail_ecs": "ECS: %value% ",
+ "query_log_detail_elapsed": "Transcurrido: %value% ",
+ "query_log_detail_name": "Nombre: %value% ",
+ "query_log_detail_network": "Red: %value% ",
+ "query_log_detail_original_response": "Respuesta original:",
+ "query_log_detail_protocol": "Protocolo: %value% ",
+ "query_log_detail_reason": "Motivo:",
+ "query_log_detail_response": "Respuesta:",
+ "query_log_detail_response_code": "Código de respuesta: %value% ",
+ "query_log_detail_rules": "Regla(s):",
+ "query_log_detail_served_from_cache": "Servido desde la caché: %value% ",
+ "query_log_detail_service_name": "Nombre del servicio: %value% ",
+ "query_log_detail_source": "Origen: %value% ",
+ "query_log_detail_status": "Estado: %value% ",
+ "query_log_detail_time": "Tiempo: %value% ",
+ "query_log_detail_type": "Tipo: %value% ",
+ "query_log_nothing_available": "No se ha encontrado nada",
+ "query_log_processed": "Procesado",
+ "query_log_retention": "Rotación de los registros",
+ "query_log_rewritten": "Reescrito",
+ "query_log_safe_search": "Búsqueda segura",
+ "query_log_strict_search": "Usar comillas dobles para una búsqueda estricta",
+ "rate_limit_subnet_len_ipv4_error": "La longitud del prefijo de subred IPv4 debe estar entre 0 y 32",
+ "rate_limit_subnet_len_ipv6_error": "La longitud del prefijo de subred IPv6 debe estar entre 0 y 128",
+ "reason_table_header": "Motivo",
+ "refresh_btn": "Actualizar",
+ "refresh_statics": "Actualizar estadísticas",
+ "release_notes": "Notas de lanzamiento",
+ "remove": "Eliminar",
+ "remove_tag": "Elimina %value%",
+ "report_an_issue": "Informar de un problema",
+ "request_table_header": "Petición",
+ "requests_table_header": "Peticiones",
+ "reset": "Restablecer",
+ "reset_settings": "Restablecer configuración",
+ "reset_settings_confirm": "Sí, reiniciar",
+ "response_details": "Detalles de la respuesta",
+ "response_time": "Tiempo de respuesta",
+ "result": "Resultado",
+ "resume_protection_timer": "La protección se reanudará en %time%",
+ "rewrite_add": "Añadir reescritura DNS",
+ "rewrite_domain_input_placeholder": "Introduce el nombre de dominio o el wildcard",
+ "rewrite_edit": "Editar reescritura DNS",
+ "rewrite_hosts_applied": "Reescrito por la regla del archivo hosts",
+ "rewrite_settings_updated": "La configuración de reescritura de DNS se actualizó correctamente",
+ "rewrite_updated": "Reescritura DNS actualizada correctamente",
+ "rewrites_answer_input_placeholder": "Introduce una dirección IP, un registro CNAME, un registro A o un registro AAAA",
+ "rewrites_empty": "Aún no hay reescrituras — agrega la primera ",
+ "rewrites_remove_desc": "Se eliminará la reescritura DNS para %value% ",
+ "rewrites_remove_title": "¿Eliminar la reescritura de DNS?",
+ "rewrites_tooltip_examples_item1": "Reescribe las respuestas DNS solo para este nombre de dominio",
+ "rewrites_tooltip_examples_item2": "Reescribe las respuestas DNS de todos los subdominios de example.org",
+ "rewrites_tooltip_instructions_item1": "Para resolver el nombre de dominio en una dirección IP, introduce una dirección IPv4 para los registros A y una dirección IPv6 para los registros AAAA",
+ "rewrites_tooltip_instructions_item2": "Para redirigir el nombre de dominio, introduce el registro CNAME del dominio de destino",
+ "rewrites_tooltip_instructions_item3": "Para conservar únicamente los registros A, introduce",
+ "rewrites_tooltip_instructions_item4": "Para conservar únicamente los registros AАAA, introduce",
+ "rewritten": "Reescrito",
+ "rows_per_page": "Filas por página %value%",
+ "rules_label": "Reglas",
+ "safe_browsing": "Navegación segura",
+ "safe_search": "Búsqueda segura",
+ "safe_search_used": "Se ha utilizado la búsqueda segura",
+ "safe_search_used_tooltip": "El número de consultas DNS a motores de búsqueda en las que se aplicó la función \"Búsqueda segura\"",
+ "saturday": "Sábado",
+ "save": "Guardar",
+ "save_btn": "Guardar",
+ "search_placeholder": "Buscar",
+ "selected": "%value% seleccionado",
+ "server_config_blocking_mode": "Modo de bloqueo",
+ "server_config_blocking_mode_custom_ip_desc": "Dirección IP personalizada: Responder con una dirección IP establecida manualmente",
+ "server_config_blocking_mode_default_desc": "Predeterminado: Responder con 0.0.0.0 para A y :: para AAAA si está bloqueado por una regla de estilo bloqueo de anuncios. Responder con la dirección IP especificada en la regla si está bloqueado por una regla de estilo /etc/hosts",
+ "server_config_blocking_mode_ipv4": "Bloqueo de IPv4",
+ "server_config_blocking_mode_ipv4_faq": "Dirección IP a devolver para una solicitud A bloqueada",
+ "server_config_blocking_mode_ipv4_placeholder": "Ingresa la dirección IPv4",
+ "server_config_blocking_mode_ipv6": "Bloqueo de IPv6",
+ "server_config_blocking_mode_ipv6_faq": "Dirección IP que se devolverá para una solicitud AAAA bloqueada",
+ "server_config_blocking_mode_ipv6_placeholder": "Ingresa la dirección IPv6",
+ "server_config_blocking_mode_null_ip_desc": "Dirección IP nula: Responder con 0.0.0.0 para A y :: para AAAA",
+ "server_config_blocking_mode_nxdomain_desc": "NXDOMAIN: Responder con el código NXDOMAIN",
+ "server_config_blocking_mode_refused_desc": "REFUSED: Responder con el código REFUSED",
+ "server_config_blocking_mode_ttl": "TTL de respuesta bloqueada, en segundos",
+ "server_config_blocking_mode_ttl_faq": "Especifica cuánto tiempo los clientes deben almacenar en caché una respuesta filtrada",
+ "server_config_custom_ip": "Dirección IP personalizada",
+ "server_config_default": "Predeterminado",
+ "server_config_disable_ipv6": "Deshabilitar resolución de direcciones IPv6",
+ "server_config_disable_ipv6_desc": "Descarta todas las consultas DNS para direcciones IPv6 (tipo AAAA) y elimina las sugerencias IPv6 de las respuestas HTTPS",
+ "server_config_dnssec_enable": "Habilitar DNSSEC",
+ "server_config_dnssec_enable_desc": "Activa la opción DNSSEC en las consultas DNS salientes y comprueba el resultado. Se necesita un resolutor compatible con DNSSEC",
+ "server_config_edns_cs_desc": "Agrega la opción EDNS Client Subnet (ECS) a las solicitudes upstream y registra los valores enviados por los clientes en el registro de consultas",
+ "server_config_edns_enable": "Habilitar subred de cliente EDNS",
+ "server_config_edns_use_custom_ip": "Utilice una dirección IP personalizada para EDNS",
+ "server_config_edns_use_custom_ip_desc": "Reemplazar la dirección IP del cliente con la dirección IP personalizada a continuación",
+ "server_config_null_ip": "Dirección IP nula",
+ "server_config_nxdomain": "NXDOMAIN",
+ "server_config_rate_limit": "Límite de frecuencia, RPS",
+ "server_config_rate_limit_faq": "El número de solicitudes por segundo permitidas por cliente. Un valor de 0 significa que no hay límite",
+ "server_config_rate_limit_placeholder": "Ingresa el límite de cantidad",
+ "server_config_rate_limit_whitelist": "Lista de permisos de limitación de velocidad",
+ "server_config_rate_limit_whitelist_faq": "Direcciones IP excluidas del límite de cantidad",
+ "server_config_refused": "REFUSED",
+ "server_config_subnet_len_ipv4": "Longitud del prefijo de subred para direcciones IPv4",
+ "server_config_subnet_len_ipv4_faq": "Longitud del prefijo de subred para direcciones IPv4 utilizadas para limitar la cantidad. El valor predeterminado es 24",
+ "server_config_subnet_len_ipv6": "Longitud del prefijo de subred para direcciones IPv6",
+ "server_config_subnet_len_ipv6_faq": "Longitud del prefijo de subred para direcciones IPv6 utilizadas para limitar la cantidad. El valor predeterminado es 56",
+ "server_config_subnet_len_placeholder": "Introduce la longitud del prefijo",
+ "set_static_ip": "Establecer una dirección IP estática",
+ "settings": "Configuración",
+ "settings_anonymize_client_ip": "Anonimizar las direcciones IP de los clientes",
+ "settings_anonymize_client_ip_desc": "Sustituye parte de la dirección IP por ceros",
+ "settings_browsing_security": "Seguridad de navegación",
+ "settings_browsing_security_desc": "Bloquear el acceso a los dominios que figuran en la base de datos de dominios maliciosos y de phishing",
+ "settings_confirm_clear_query_log": "¿Borrar el registro de consultas?",
+ "settings_confirm_clear_query_log_desc": "Esto eliminará todas las solicitudes de Registro de consultas . Las estadísticas seguirán estando disponibles",
+ "settings_confirm_clear_statistics": "¿Borrar estadísticas?",
+ "settings_confirm_clear_statistics_desc": "Esto borrará todas las estadísticas. El registro de consultas seguirá estando disponible",
+ "settings_confirm_decrease_log_rotation_interval": "¿Disminuir el intervalo de rotación de registros?",
+ "settings_confirm_decrease_log_rotation_interval_desc": "Esto eliminará todos los registros anteriores a %value%",
+ "settings_confirm_decrease_stats_rotation_interval": "¿Reducir el intervalo de retención de estadísticas?",
+ "settings_confirm_decrease_stats_rotation_interval_desc": "Esto eliminará todas las estadísticas anteriores a %value%",
+ "settings_custom": "Personalizado",
+ "settings_days": "| %count% día | %count% días",
+ "settings_domain_names": "Nombres de dominio, uno por línea",
+ "settings_filter_requests": "Filtrar peticiones",
+ "settings_filter_requests_desc": "Puedes utilizar listas de bloqueo , listas de permitidos y reglas de usuario para configurar reglas de filtrado",
+ "settings_filtering_and_security": "Filtrado y seguridad",
+ "settings_general_short": "General",
+ "settings_global": "Global",
+ "settings_hours": "| %count% hora | %count% horas",
+ "settings_log_dns_requests": "Registrar peticiones DNS",
+ "settings_log_rotation_hours": "Rotación de registros, en horas",
+ "settings_notify_changes_saved": "Cambios guardados",
+ "settings_notify_query_log_cleared": "Registro de consultas borrado",
+ "settings_notify_statistics_cleared": "Estadísticas borradas",
+ "settings_parental_control": "Control parental",
+ "settings_parental_control_desc": "Bloquear sitios web con contenidos inadecuados para los niños",
+ "settings_rotation_placeholder": "Usar números",
+ "settings_rule_syntax": "Sintaxis de regla",
+ "settings_safe_search": "Búsqueda segura",
+ "settings_safe_search_desc": "Bloquear el contenido inapropiado en los resultados de búsqueda",
+ "settings_statistics": "Estadísticas",
+ "settings_statistics_clear": "Borrar estadísticas",
+ "settings_statistics_desc": "Mostrar estadísticas de solicitudes y clientes en el panel",
+ "settings_statistics_retention": "Retención de estadísticas",
+ "settings_statistics_retention_hours": "Retención de estadísticas, en horas",
+ "settings_tooltip_domain_names": "Nombres de dominio, wildcards o reglas de filtrado",
+ "settings_tooltip_examples": "Ejemplos:",
+ "settings_yes_clear": "Sí, borrar",
+ "settings_yes_decrease": "Sí, reducir",
+ "setup_complete": "La configuración se ha completado y AdGuard Home está listo para usar",
+ "setup_devices_android_list_1": "Abre la aplicación Ajustes de tu dispositivo y selecciona Wi-Fi .",
+ "setup_devices_android_list_2": "Mantén pulsada tu red actual y, a continuación, selecciona Modificar red . No es posible configurar un DNS personalizado para conexiones a Internet móviles.",
+ "setup_devices_android_list_3": "En algunos dispositivos, es posible que tengas que marcar la casilla «Avanzado» para ver más opciones de configuración. Para personalizar la configuración de DNS de tu dispositivo Android, debes cambiar la configuración de IP de «DHCP» a «Estática ».",
+ "setup_devices_android_list_4": "Cambia los valores DNS 1 y DNS 2 por las direcciones de tu servidor AdGuard Home.",
+ "setup_devices_android_title": "Android",
+ "setup_devices_dns_android_list_1": "Android 9 es compatible de forma nativa con DNS-over-TLS . Para configurarlo, ve a Ajustes → Red e Internet → Avanzado → DNS privado e introduce tu nombre de dominio.",
+ "setup_devices_dns_android_list_2": "AdGuard para Android admite la configuración de DNS-over-HTTPS , DNS-over-TLS y DNS-over-QUIC .",
+ "setup_devices_dns_android_list_3": "Intra agrega soporte para DNS-over-HTTPS en Android.",
+ "setup_devices_dns_ios_list_1": "AdGuard para iOS admite la configuración de DNS-over-HTTPS , DNS-over-TLS y DNS-over-QUIC .",
+ "setup_devices_dns_ios_list_2": "DNSCloak es compatible con DNS sobre HTTPS , pero para configurarlo para que utilice tu propio servidor, tendrás que generar un «DNS Stamp» para él.",
+ "setup_devices_dns_list_1": "Para DNS-over-TLS , utiliza ",
+ "setup_devices_dns_list_2": "Para DNS-over-HTTPS , usa ",
+ "setup_devices_dns_list_3": "Para DNS-over-QUIC , utiliza ",
+ "setup_devices_dns_list_devices": "Aquí tienes una lista de programas que puedes utilizar.",
+ "setup_devices_dns_macos_desc": "En un dispositivo con iOS 14 o macOS Big Sur, puedes descargar un archivo mobileconfig especial que añade servidores DNS-over-HTTPS o DNS-over-TLS a la configuración de DNS.",
+ "setup_devices_dns_other_list_1": "El propio AdGuard Home puede funcionar como cliente DNS seguro en cualquier plataforma.",
+ "setup_devices_dns_other_list_2": "dnsproxy es compatible con todos los protocolos DNS seguros conocidos.",
+ "setup_devices_dns_other_list_3": "dnscrypt-proxy es compatible con DNS-over-HTTPS .",
+ "setup_devices_dns_other_list_4": "Mozilla Firefox admite DNS-over-HTTPS .",
+ "setup_devices_dns_other_list_5": "Para conocer otras implementaciones, consulta DNSCrypt y la privacidad del DNS .",
+ "setup_devices_ios_list_1": "Abre la aplicación «Ajustes» de tu dispositivo y selecciona «Wi-Fi ». No es posible configurar un DNS personalizado para las conexiones a Internet móvil.",
+ "setup_devices_ios_list_2": "Pulsa el nombre de la red que esté activa actualmente.",
+ "setup_devices_ios_list_3": "Ve a DNS → Configurar DNS → Manual y añade las direcciones de tu servidor de AdGuard Home.",
+ "setup_devices_ios_list_4": "Toca Guardar .",
+ "setup_devices_ios_title": "iOS",
+ "setup_devices_macos_list_1": "Haz clic en el menú Apple → Preferencias del Sistema → Red .",
+ "setup_devices_macos_list_2": "Selecciona la conexión deseada y haz clic en «Avanzado ».",
+ "setup_devices_macos_list_3": "Selecciona la pestaña «DNS» e introduce las direcciones de tu servidor AdGuard Home.",
+ "setup_devices_macos_title": "macOS",
+ "setup_devices_router_desc_1": "Instala AdGuard Home en el router de tu red doméstica y protege todos los dispositivos conectados. No es necesario configurar cada dispositivo manualmente.",
+ "setup_devices_router_desc_2": "Algunos routers no permiten configurar un servidor DNS personalizado. En ese caso, puedes intentar configurar AdGuard Home como servidor DHCP . Si esto no funciona, consulta el manual de tu router para obtener instrucciones sobre cómo personalizar los servidores DNS para tu router concreto.",
+ "setup_devices_router_list_1": "Normalmente puedes acceder a ellos desde tu navegador mediante una URL, como http://192.168.0.1 o http://192.168.1.1 . Es posible que se te pida que introduzcas una contraseña. A menudo puedes restablecer la contraseña pulsando un botón en el propio router, pero ten en cuenta que podrías perder toda la configuración del router. Si vas a configurar el router mediante una aplicación, instálala en tu teléfono o PC y úsala para acceder a la configuración del router.",
+ "setup_devices_router_list_1_title": "Abre la configuración de tu router.",
+ "setup_devices_router_list_2": "Para encontrar la configuración, busca «DNS» junto a un campo en el que se puedan introducir dos o tres series de números, cada una dividida en cuatro grupos de entre uno y tres dígitos.",
+ "setup_devices_router_list_2_title": "En la configuración de DHCP/DNS, introduce las direcciones de tu servidor AdGuard Home.",
+ "setup_devices_router_title": "Router",
+ "setup_devices_windows_list_1": "Abre el Panel de control desde el menú Inicio o mediante la búsqueda de Windows .",
+ "setup_devices_windows_list_2": "Haz clic en «Red e Internet » → «Centro de redes y recursos compartidos » → «Cambiar configuración del adaptador ».",
+ "setup_devices_windows_list_3": "Haz clic con el botón derecho del ratón en la conexión activa y selecciona «Propiedades ».",
+ "setup_devices_windows_list_4": "En la lista, selecciona «Protocolo de Internet versión 4 (TCP/IPv4) » para IPv4 o «Protocolo de Internet versión 6 (TCP/IPv6) » para IPv6.",
+ "setup_devices_windows_list_5": "Haz clic en «Propiedades » → «Utilizar las siguientes direcciones de servidor DNS ».",
+ "setup_devices_windows_list_6": "Introduce las direcciones de tu servidor de AdGuard Home.",
+ "setup_devices_windows_title": "Windows",
+ "setup_dns_desc": "Configura tu router u otros dispositivos para que utilicen las siguientes direcciones de servidores DNS:",
+ "setup_dns_notice_new": "Para poder utilizar DNS-over-HTTPS o DNS-over-TLS , debes configurar el cifrado en los ajustes de AdGuard Home.",
+ "setup_dns_privacy_ioc_mac": "Configuración de iOS y macOS",
+ "setup_dns_privacy_other_title": "Otras implementaciones",
+ "setup_dns_quote_desc": "Para funcionar correctamente, el servidor DNS de AdGuard Home requiere una dirección IP estática en tu red local. De lo contrario, tu router podría acabar asignándole una dirección IP diferente y romper la protección DNS",
+ "setup_dns_quote_title": "Asigna una dirección IP estática al servidor DNS de AdGuard Home en tu red local",
+ "setup_dns_title": "Configura la dirección del servidor DNS de AdGuard Home",
+ "setup_dns_title_banner": "Configuración del servidor DNS",
+ "setup_guide": "Guía de configuración",
+ "setup_guide_auth_privacy": "Acepto la Política de privacidad y los Términos y condiciones de los sitios web de AdGuard",
+ "setup_guide_auth_subtitle": "Esto es necesario para proteger el panel de administración de AdGuard Home frente a accesos no autorizados",
+ "setup_guide_auth_title": "Configurar la protección con contraseña",
+ "setup_guide_desc": "Configura tus dispositivos para usar AdGuard Home",
+ "setup_guide_device_type": "Selecciona tu tipo de dispositivo",
+ "setup_guide_greeting_button": "Ejecutar el asistente",
+ "setup_guide_greeting_desc": "Este asistente te ayudará a configurar AdGuard Home:",
+ "setup_guide_greeting_list_1": "Crea credenciales para acceder al panel de administración",
+ "setup_guide_greeting_list_2": "Configura la dirección de la interfaz de usuario de AdGuard Home",
+ "setup_guide_greeting_list_3": "Configura la dirección del servidor DNS de AdGuard Home",
+ "setup_guide_greeting_list_4": "Obtén instrucciones para tus dispositivos individuales",
+ "setup_guide_greeting_list_5": "Accede al panel de control de AdGuard Home ",
+ "setup_guide_greeting_title": "Configura AdGuard Home",
+ "setup_guide_title": "Configura tus dispositivos para usar AdGuard Home",
+ "setup_ui_desc": "Utiliza estas direcciones para acceder a la interfaz de usuario web de AdGuard Home. Si es necesario, cambia el puerto",
+ "setup_ui_title": "Configura la dirección de la interfaz de usuario web de AdGuard Home",
+ "setup_ui_title_banner": "Configuración de la interfaz de usuario web",
+ "show_blocked_responses": "Bloqueado",
+ "show_more_count": "+ %count% más",
+ "show_processed_responses": "Procesado",
+ "sort_asc": "Ordenar de la A a la Z",
+ "sort_desc": "Ordenar de la Z a la A",
+ "source_label": "Fuente",
+ "source_tooltip": "Fuente",
+ "static_dhcp_leases_not_found": "No se encontraron concesiones DHCP estáticas",
+ "stats_adult": "Sitios web para adultos bloqueado",
+ "stats_query_domain": "Dominios más consultados",
+ "status_table_header": "Estado",
+ "sunday": "Domingo",
+ "system_host_files": "Archivos hosts del sistema",
+ "system_theme": "Tema del sistema",
+ "table_client": "Cliente",
+ "tags_title": "Etiquetas",
+ "test_upstreams": "Probar proveedores DNS",
+ "threats_blocked": "Amenazas bloqueadas",
+ "threats_blocked_tooltip": "El número de consultas DNS bloqueadas por el módulo de seguridad de navegación de AdGuard",
+ "thursday": "Jueves",
+ "time_table_header": "Hora",
+ "top_blocked_domains": "Dominios más bloqueados",
+ "top_clients": "Clientes más frecuentes",
+ "top_upstreams": "Proveedores DNS más frecuentes",
+ "total": "Total",
+ "try_again": "Volver a intentar",
+ "tuesday": "Martes",
+ "type_value": "Tipo: %value%",
+ "unavailable_dhcp": "DHCP no disponible",
+ "unavailable_dhcp_desc": "AdGuard Home no puede ejecutar un servidor DHCP en tu SO",
+ "unblock": "Desbloquear",
+ "unblock_client": "Desbloquear cliente",
+ "unknown_filter": "Filtro desconocido %filterId%",
+ "update_button": "Actualizar",
+ "update_filters_custom_hours": "Intervalo personalizado, en horas",
+ "update_filters_disable": "Deshabilitar",
+ "update_filters_interval_custom": "Personalizado",
+ "update_filters_interval_daily": "Todos los días",
+ "update_filters_interval_hourly": "Cada hora",
+ "update_filters_interval_weekly": "Cada semana",
+ "update_filters_title": "Actualizar filtros automáticamente",
+ "updated_upstream_dns_toast": "Servidores upstream guardados",
+ "updates_checked": "La nueva versión de AdGuard Home está disponible",
+ "updates_version_equal": "AdGuard Home está actualizado",
+ "upstream": "Proveedor DNS",
+ "upstream_bootstrap_dns_faq": "Direcciones IP de los servidores DNS utilizados para resolver las direcciones IP de los resolvedores ascendentes de DoH/DoT. No se permiten comentarios",
+ "upstream_bootstrap_dns_title": "Servidores DNS de arranque, uno por línea",
+ "upstream_dns": "Proveedores DNS",
+ "upstream_dns_addresses": "Direcciones de servidor, una por línea",
+ "upstream_dns_addresses_faq": "Más información sobre cómo configurar los servidores DNS de origen. Aquí tienes unalista de proveedores de DNS conocidos entre los que puedes elegir.",
+ "upstream_dns_configured_in_file": "Configurado en %path%",
+ "upstream_dns_fastest_addr": "Dirección IP más rápida",
+ "upstream_dns_parallel_requests": "Consultas paralelas",
+ "upstream_dns_placeholder": "Direcciones IP, sdns://, quic://, https://, h3:// o tls://",
+ "upstream_dns_servers_title": "Proveedores DNS",
+ "upstream_enable_reverse_lookup_desc": "Resolver de forma inversa las direcciones IP de los clientes a sus nombres de host enviando consultas PTR a los resolutores adecuados: servidores DNS privados para los clientes locales, o servidores de nivel superior para los clientes con direcciones IP públicas",
+ "upstream_enable_reverse_lookup_title": "Habilitar la búsqueda inversa de direcciones IP de los clientes",
+ "upstream_example_multiple_upstreams_reserved": "%value%: múltiples upstreams para dominios específicos ",
+ "upstream_example_tcp": "%ipv4%,%ipv6%: DNS regular (sobre TCP)",
+ "upstream_example_tcp_hostname": "%value%: DNS regular (sobre TCP, nombre de host)",
+ "upstream_example_tcp_port": "%ipv4%,%ipv6%: DNS regular (sobre TCP, con puerto)",
+ "upstream_example_udp": "%ipv4%,%ipv6%: DNS regular (sobre UDP)",
+ "upstream_example_udp_hostname": "%value%: DNS regular (sobre UDP, nombre de host)",
+ "upstream_example_udp_port": "%ipv4%,%ipv6%: DNS regular (sobre UDP, con puerto)",
+ "upstream_example_upstream_comment": "%value%: un comentario.",
+ "upstream_example_upstream_doh": "%value%: DNS-over-HTTPS cifrado",
+ "upstream_example_upstream_doh3": "%value%: DNS-over-HTTPS cifrado con HTTP/3 HTTP/3 forzado y sin respaldo a HTTP/2",
+ "upstream_example_upstream_doq": "%value%: DNS-over-QUIC cifrado",
+ "upstream_example_upstream_dot": "%value%: cifrado DNS-over-TLS ",
+ "upstream_example_upstream_reserved": "%value%: un upstream para dominios específicos ",
+ "upstream_example_upstream_sdns": "%value%: Sellos DNS para resolutores DNSCrypt o DNS-over-HTTPS ",
+ "upstream_examples_title": "Ejemplos",
+ "upstream_fallback_title": "Servidores DNS fallback, uno por línea",
+ "upstream_fallback_title_faq": "Servidores DNS de reserva que se utilizarán cuando los servidores DNS principales no estén disponibles. Utiliza la misma sintaxis que para los servidores DNS principales",
+ "upstream_private_ptr_resolvers_desc": "Resuelve las solicitudes PTR, SOA y NS para dominios ARPA que contengan direcciones IP privadas a través de servidores upstream privados, DHCP, /etc/hosts, etc. Si se desactiva, AdGuard Home responderá a todas esas solicitudes con NXDOMAIN",
+ "upstream_private_ptr_resolvers_title": "Usar resolutores DNS inversos y privados",
+ "upstream_ptr": "Servidores DNS inversos privados, uno por línea",
+ "upstream_ptr_faq_1": "AdGuard Home utiliza estos servidores DNS para las solicitudes privadas de PTR, SOA y NS. Las solicitudes privadas proceden de clientes con una dirección IP privada y solicitan un dominio ARPA que contenga una subred perteneciente a un rango de direcciones IP privadas, como por ejemplo %value%",
+ "upstream_ptr_faq_2": "Si este campo está vacío, se utilizarán los resolutores DNS predeterminados de tu sistema operativo, salvo en el caso de las direcciones IP de AdGuard Home",
+ "upstream_ptr_faq_3": "De forma predeterminada, AdGuard Home utiliza los siguientes servidores de resolución DNS inversa: %value_1% y %value_2%",
+ "upstream_timeout": "Tiempo de espera de entrada, en segundos",
+ "upstream_timeout_faq": "Tiempo de espera para recibir una respuesta del servidor de origen",
+ "upstream_timeout_placeholder": "Introduce el tiempo de espera del servidor de origen",
+ "upstreams": "Proveedores DNS",
+ "url_label": "URL",
+ "use_saved_key": "Utiliza una clave privada ya existente",
+ "user_rules": "Reglas del usuario",
+ "user_rules_add_to_allowlist": "Añadir a la lista de permitido",
+ "user_rules_allow_service": "Permitir servicio",
+ "user_rules_browsing_security_disabled": "Seguridad de navegación desactivada",
+ "user_rules_check_button": "Comprobar",
+ "user_rules_check_client_label": "Identificador del cliente (nombre, ID de cliente o dirección IP)",
+ "user_rules_check_client_placeholder": "Ingresa el identificador del cliente",
+ "user_rules_check_dns_record_type_label": "Tipo de registro DNS",
+ "user_rules_check_hostname_label": "Nombre de host o nombre de dominio",
+ "user_rules_check_hostname_placeholder": "Introduce el destino",
+ "user_rules_check_title": "Verifica el filtrado de dominios a nivel global o para un cliente",
+ "user_rules_cname": "CNAME: %cname%",
+ "user_rules_disable_browsing_security": "Desactivar la seguridad de navegación",
+ "user_rules_disable_filter": "Deshabilitar filtro",
+ "user_rules_disable_parental_control": "Deshabilitar control parental",
+ "user_rules_disable_safe_search": "Desactivar la búsqueda segura",
+ "user_rules_dns_record_type_placeholder": "Selecciona el tipo de registro DNS",
+ "user_rules_dns_rewrite_removed": "Regla eliminada de la reescritura de DNS",
+ "user_rules_domain": "Dominio: %value%",
+ "user_rules_domain_blocked": "Dominio bloqueado",
+ "user_rules_domain_is_allowed": "El dominio está permitido",
+ "user_rules_domain_is_processed": "Dominio procesado",
+ "user_rules_edit_dns_rewrite": "Editar reescritura DNS",
+ "user_rules_example_block": "||%domain% Bloquea el acceso a %domain% y todos sus subdominios",
+ "user_rules_example_comment": "! Este es un comentario Para comentar, empieza la línea con ! o #",
+ "user_rules_example_comment_2": "# Esto también es un comentario Para comentar, empieza la línea con ! o #",
+ "user_rules_example_regex": "/REGEX/ Bloquea el acceso a los dominios que coincidan con la expresión regular especificada",
+ "user_rules_example_response": "127.0.0.1 %domain% Responde con 127.0.0.1 únicamente para %domain%, no para sus subdominios",
+ "user_rules_example_unblock": "@@||%domain% Desbloquea el acceso a %domain% y todos sus subdominios",
+ "user_rules_filter_was_disabled": "%value% se deshabilitó",
+ "user_rules_ip": "Direcciones IP: %ip%",
+ "user_rules_learn_more": "Más información sobre crear tus propias listas de host.",
+ "user_rules_no_rules_matched": "Resultado: No se encontraron reglas coincidentes",
+ "user_rules_parental_control_disabled": "Control parental deshabilitado",
+ "user_rules_placeholder": "Esto es un marcador de posición",
+ "user_rules_reason": "Motivo: %reason%",
+ "user_rules_reason_allowed_by": "Permitido por %source%",
+ "user_rules_reason_blocked_by": "Bloqueado por %source%",
+ "user_rules_reason_filtered_by": "Filtrado por %source%",
+ "user_rules_redirected_to": "Redirigido a: %value%",
+ "user_rules_remove_dns_rewrite": "Eliminar reescritura DNS",
+ "user_rules_rewrite_rule_is_applied": "Regla de reescritura aplicada",
+ "user_rules_rewritten_to": "Reescrito a: %value%",
+ "user_rules_rule": "Regla: %rule%",
+ "user_rules_rule_added_to_custom_filtering_rules": "Regla añadida a las reglas de filtrado personalizadas",
+ "user_rules_rule_removed": "Regla eliminada de las reglas de filtrado personalizadas",
+ "user_rules_safe_search_disabled": "Búsqueda segura deshabilitada",
+ "user_rules_service": "Servicio: %service%",
+ "user_rules_service_allowed": "Se permitió el servicio %value% ",
+ "user_rules_source": "Fuente: %value%",
+ "user_rules_source_allowlist": "Lista de permitidos: %value%",
+ "user_rules_source_blocklist": "Lista de bloqueo: %value%",
+ "user_rules_status": "Estado: %reason%",
+ "username_label": "Nombre de usuario",
+ "username_placeholder": "Ingresa tu nombre de usuario",
+ "validated_with_dnssec": "Validado con DNSSEC",
+ "wednesday": "Miércoles",
+ "whois": "WHOIS",
+ "yes": "Sí",
+ "yes_disable": "Sí, desactivar",
+ "yes_reset": "Sí, reiniciar"
+}
diff --git a/client_v2/src/__locales/fa.json b/client_v2/src/__locales/fa.json
new file mode 100644
index 00000000000..46912bb4bf7
--- /dev/null
+++ b/client_v2/src/__locales/fa.json
@@ -0,0 +1,388 @@
+{
+ "access_settings_title": "تنظیمات دسترسی",
+ "actions_table_header": "اقدامات",
+ "add": "افزودن",
+ "add_allowlist": "افزودن لیست مجاز",
+ "add_blocklist": "افزودن لیست سیاه",
+ "add_persistent_client": "به عنوان مشتری دائمی اضافه کنید",
+ "add_to_allowlist": "افزودن به لیست سفید",
+ "ads_blocked": "تبلیغ مسدود شد",
+ "ads_blocked_card": "تبلیغ مسدود شد",
+ "adult_websites_blocked_tooltip": "تعداد وبسایت های غیر اخلاقی مسدود شده",
+ "all_lists_up_to_date_toast": "همه لیست ها از قبل بروز اند",
+ "allowed": "لیست سفید",
+ "allowlist_add": "افزودن لیست مجاز",
+ "allowlist_edit": "ویرایش لیست مجاز",
+ "apply": "اِعمال",
+ "autofix_warning_result": "در نتیجه همه درخواست های DNS از سیستم شما بطور پیش فرض با AdGuardHome پردازش خواهد شد.",
+ "autofix_warning_text": "اگر روی \"تعمیر\" کلیک کنید، AdGuardHome سیستم شما را برای استفاده از DNS سرور AdGuardHome پیکربندی می کند.",
+ "average_time_processing": "میانگین زمان پردازش",
+ "average_upstream_response_time": "میانگین زمان پاسخ دهی بالادست",
+ "back": "قبلی",
+ "block": "مسدود کن",
+ "block_for_this_client_only": "مسدود کردن فقط برای این مشتری",
+ "blocked": "مسدود شده",
+ "blocked_by_cname_or_ip": "توسط CNAME یا IP مسدود شده است",
+ "blocked_service": "سرویس مسدود شده",
+ "blocked_services": "سرویس های مسدود شده",
+ "blocked_threats": "تهدید مسدود شده",
+ "blocked_threats_chart": "تهدید مسدود شده",
+ "blocklist_add_manual": "به طور دستی اضافه کنید",
+ "blocklist_edit": "ویرایش لیست سیاه",
+ "blocklist_homepage": "صفحه خانگی",
+ "blocklist_url_file_path": "آدرس URL یا مسیر فایل",
+ "bootstrap_dns": "خودراه انداز سرورهای DNS",
+ "browse": "مرور",
+ "cache_cleared": "حافظه پنهان DNS با موفقیت حذف شد",
+ "cache_config_clear": "پاک کردن کش",
+ "cache_config_optimistic": "حالت ویژه پردازش",
+ "cache_config_ttl_validation": "حداقل لغو TTL حافظه نهان باید کمتر یا مساوی حداکثر باشد",
+ "cache_enabled": "فعال کردن حافظه پنهان",
+ "cancel": "لغو",
+ "cancel_btn": "لغو",
+ "category_tooltip": "دسته بندی",
+ "changes_saved_success": "تغییرات ذخیره شد",
+ "check_dhcp_servers": "بررسی برای سرورهای DHCP",
+ "check_not_found": "در لیست فیلترهای شما یافت نشد",
+ "check_updates_btn": "بررسی بروز رسانی",
+ "clear_btn": "پاکسازی",
+ "client_add": "افزودن کلاینت",
+ "client_details": "جزئیات کلاینت",
+ "client_id": "آیدی کاربر",
+ "client_id_placeholder": "آیدی کاربر را وارد کنید",
+ "client_identifier": "احراز با",
+ "client_settings": "تنظیمات کلاینت",
+ "client_table_header": "کلاینت",
+ "clients_add_identifier": "افزودن احرازکننده",
+ "clients_add_placeholder": "نام کلاینت را وارد کنید",
+ "clients_dns_cache_size": "اندازه کش DNS، بر حسب بایت",
+ "clients_not_found": "کلاینتی یافت نشد",
+ "clients_protection": "حفاظت",
+ "clients_tags": "برچسب ها",
+ "clients_use_global_settings": "استفاده از تنظیمات سراسری",
+ "close": "بستن",
+ "confirm": "تایید",
+ "copied": "کپی به کلیپبورد",
+ "copy": "کپی",
+ "copy_to_clipboard": "کپی به کلیپبورد",
+ "custom_filtering_rules": "دستورات فیلترینگ دستی",
+ "dark_theme": "پوسته تیره",
+ "dashboard": "داشبورد",
+ "delete_btn": "حذف",
+ "delete_table_action": "حذف",
+ "dhcp_add_static_lease": "افزودن اجاره ایستا",
+ "dhcp_config_saved": "پیکربندی سرور DHCP ذخیره شده است",
+ "dhcp_edit_static_lease": "ویرایش اجاره ایستا",
+ "dhcp_form_gateway_input": "آی پی دروازه",
+ "dhcp_form_subnet_input": "ماسک زیر شبکه",
+ "dhcp_leases_not_found": "اجاره DHCP یافت نشد",
+ "dhcp_leases_title": "اجاره DHCP",
+ "dhcp_new_static_lease": "اجاره ایستا جدید",
+ "dhcp_reset_leases": "بازگردانی همه مجوزهای منابع",
+ "dhcp_reset_leases_success": "مجوزهای منابع DHCP با موفقیت بازگردانی شد",
+ "dhcp_settings": "تنظیمات DHCP",
+ "dhcp_static_leases_not_found": "هیچ اجاره DHCP ایستا یافت نشد",
+ "dhcp_table_hostname": "نام میزبان",
+ "dhcp_table_ip_address": "آدرس آی پی",
+ "disable": "غيرفعالسازي",
+ "disable_protection_btn": "غيرفعالسازي حفاظت",
+ "disabled_dhcp": "سرور DHCP غیرفعال شده است",
+ "disallow_this_client": "این مشتری را رد کنید",
+ "dns_access_settings_title": "تنظیمات دسترسی",
+ "dns_allowed_clients": "کلاینت های مجاز",
+ "dns_blocking_mode": "حالت مسدودسازی",
+ "dns_blocking_mode_custom_ip": "آدرس IP دستی",
+ "dns_blocking_mode_default": "پيش فرض",
+ "dns_blocking_mode_ipv4_label": "مسدودسازی IPv4",
+ "dns_blocking_mode_ipv6_label": "مسدودسازی IPv6",
+ "dns_blocking_mode_nxdomain": "NXDOMAIN",
+ "dns_blocking_mode_refused": "مرفوض",
+ "dns_blocking_mode_title": "حالت مسدودسازی",
+ "dns_bootstrap_dns_placeholder": "آدرس آی پی",
+ "dns_bootstrap_dns_title": "خودراه انداز سرورهای DNS",
+ "dns_bootstrap_servers": "خودراه انداز سرورهای DNS",
+ "dns_cache_config": "تنظیمات کش DNS",
+ "dns_cache_size": "اندازه کش",
+ "dns_cache_size_label": "اندازه کش DNS، بر حسب بایت",
+ "dns_cache_size_title": "اندازه کش",
+ "dns_clear_cache": "پاک کردن کش",
+ "dns_clear_cache_cancel": "لغو",
+ "dns_clear_cache_confirm": "بله",
+ "dns_clear_cache_yes": "بله",
+ "dns_config": "پیکربندی DNS سرور",
+ "dns_disallowed_clients": "کلاینت های غیرمجاز",
+ "dns_disallowed_clients_title": "کلاینت های غیرمجاز",
+ "dns_disallowed_domains": "دامنه های مسدود شده",
+ "dns_disallowed_domains_title": "دامنه های مسدود شده",
+ "dns_edns_custom_placeholder": "آدرس آی پی را وارد کنید",
+ "dns_fallback_dns_title": "سرورهای DNS بازگشتی",
+ "dns_fallback_servers": "سرورهای DNS بازگشتی",
+ "dns_optimistic_caching": "حالت ویژه پردازش",
+ "dns_over_https": "DNS-over-HTTPS",
+ "dns_over_quic": "DNS-over-QUIC",
+ "dns_over_tls": "DNS-over-TLS",
+ "dns_override_max_ttl": "نادیده گرفتن حداکثر TTL",
+ "dns_override_max_ttl_title": "نادیده گرفتن حداکثر TTL",
+ "dns_override_min_ttl": "نادیده گرفتن حداقل TTL",
+ "dns_override_min_ttl_title": "نادیده گرفتن حداقل TTL",
+ "dns_privacy": "حریم خصوصی DNS",
+ "dns_private_reverse_servers_title": "سرورهای خصوصی DNS",
+ "dns_rate_limit": "میزان محدودیت",
+ "dns_rate_limit_allowlist_placeholder": "آدرس آی پی",
+ "dns_rate_limit_placeholder": "میزان محدودیت را وارد کنید",
+ "dns_rate_limit_title": "میزان محدودیت",
+ "dns_rewrites": "بازنویسی های DNS",
+ "dns_server_addresses": "آدرسهای سرور",
+ "dns_server_addresses_label": "آدرس های سرور، یکی در هر خط",
+ "dns_server_addresses_title": "آدرسهای سرور",
+ "dns_server_config_title": "پیکربندی DNS سرور",
+ "dns_settings": "DNS",
+ "dns_test_upstreams": "تست جریان ارسالی",
+ "dns_upstream_servers_title": "سرورهای DNS جریان ارسالی",
+ "dns_upstream_timeout": "مهلت زمانی بالادست",
+ "dns_upstream_timeout_title": "مهلت زمانی بالادست",
+ "dns_upstream_validation_invalid": "قالب نامعتبر است",
+ "domain": "دامنه",
+ "domain_or_client": "دامنه یا کلاینت",
+ "download_mobileconfig": "دانلود فایل تنظیمات",
+ "edit_table_action": "ويرايش",
+ "enable": "فعالسازي",
+ "enabled_dhcp": "سرور DHCP فعال شده است",
+ "encryption_certificate_path": "مسیر گواهینامه",
+ "encryption_certificates": "گواهینامه ها",
+ "encryption_certificates_input": "کپی/چسباندن گواهینامه پی ای اِم کد گذاری شده در اینجا.",
+ "encryption_certificates_source_content": "چسباندن محتوای گواهینامه",
+ "encryption_certificates_source_path": "تنظیم مسیر فایل گواهینامه",
+ "encryption_chain_invalid": "زنجیره گواهینامه نامعتبر است",
+ "encryption_chain_valid": "زنجیره گواهینامه معتبر است",
+ "encryption_config_saved": "پیکربندی رمزگذاری ذخیره شد",
+ "encryption_doq": "درگاه DNS-over-QUIC",
+ "encryption_dot": "پورت DNS-over-TLS",
+ "encryption_https": "پورت HTTPS",
+ "encryption_key": "کلید خصوصی",
+ "encryption_key_input": "کپی/چسباندن کلید گواهینامه پی ای اِم کد گذاری شده در اینجا.",
+ "encryption_key_source_content": "چسباندن محتوای کلید خصوصی",
+ "encryption_plain_dns": "DNS ساده",
+ "encryption_plain_dns_error": "برای غیرفعال کردن DNS ساده، حداقل یک پروتکل DNS رمزگذاری شده را فعال کنید",
+ "encryption_private_key_path": "مسیر کلید خصوصی",
+ "encryption_server": "نام سرور",
+ "encryption_server_enter": "نام دامنه خود را وارد کنید",
+ "encryption_title": "رمزگُذاری",
+ "enter_cache_size": "اندازه حافظه پنهان را وارد کنید (بایت)",
+ "enter_ip_address_placeholder": "آدرس آی پی را وارد کنید",
+ "error": "خطا",
+ "fallback_dns": "سرورهای DNS بازگشتی",
+ "filter_category_general": "General",
+ "filter_category_general_desc": "فهرستهایی که ردیابی و تبلیغات را بر روی اکثر دستگاهها مسدود میکنند",
+ "filter_category_other": "ساير",
+ "filter_category_other_desc": "سایر لیست های بلاک شده",
+ "filter_category_regional": "منطقهای",
+ "filter_category_regional_desc": "فهرستهایی که بر روی تبلیغات منطقهای و سرورهای ردیابی تمرکز دارند",
+ "filter_category_security": "مسدودسازی بدافزار و فیشینگ",
+ "filter_category_security_desc": "فهرستهایی طراحی شده است که به طور خاص برای مسدودسازی دامنههای مخرب، طعمهگذاری و کلاهبرداری هستند",
+ "filters": "فيلترها",
+ "forgot_password": "رمزعبور را فراموش کرده اید؟",
+ "form_enter_blocked_response_ttl": "پاسخ مسدود شده TTL (ثانیه) را وارد کنید",
+ "form_error_client_id_format": "فرمت شناسه کلاینت نامعتبر است",
+ "form_error_domain_format": "فرمت دامنه اشتباه است",
+ "form_error_equal": "نباید برابر باشد",
+ "form_error_format": "قالب نامعتبر است",
+ "form_error_gateway_ip": "Lease نمی تواند آدرس IP دروازه را داشته باشد",
+ "form_error_ip4_format": "فرمت نامعتبر IPv4",
+ "form_error_ip6_format": "فرمت نامعتبر IPv6",
+ "form_error_ip_format": "آدرس آی پی نامعتبر است",
+ "form_error_mac_format": "فرمت مَک نامعتبر است",
+ "form_error_password": "رمزعبور تطبیق ندارد",
+ "form_error_port": "مقدار پورت معتبر وارد کنید",
+ "form_error_port_range": "مقدار پورت را در محدوده 80-65535 وارد کنید",
+ "form_error_port_unsafe": "این پورت غیر ایمن است",
+ "form_error_server_name": "نام سرور نامعتبر است",
+ "form_error_url_format": "یک نشانی وب یا مسیر پرونده معتبر وارد کنید",
+ "friday": "جمعه",
+ "gateway_or_subnet_invalid": "پوشش زیرشبکه نامعتبر است",
+ "general_statistics": "آمار عمومی",
+ "ignore_domains_title": "دامنههای نادیده گرفته شده",
+ "inactivity_schedule_add": "برنامه اضافه کنید",
+ "inactivity_schedule_all_day": "تمام روز",
+ "inactivity_schedule_edit": "ویرایش برنامه",
+ "install_auth_confirm": "تأیید رمزعبور",
+ "install_auth_password": "رمزعبور",
+ "install_auth_password_enter": "رمزعبور را وارد کنید",
+ "install_auth_username": "نام کاربر",
+ "install_auth_username_enter": "نام کاربر را وارد کنید",
+ "install_saved": "با موفقیت ذخیره نشد",
+ "install_settings_all_interfaces": "همه رابط ها",
+ "install_settings_port": "پورت",
+ "install_static_error": "AdGuard Home نمیتواند رابط این شبکه را خودکار پیکربندی کند. لطفا دستورالعمل چگونگی انجام دستی آن را مطالعه کنید.",
+ "install_static_ok": "خبر خوب! آدرس آی پی ثابت از قبل پیکربندی شده است",
+ "install_step": "گام",
+ "install_submit_title": "تبریک می گوییم!",
+ "ip_address": "آدرس آی پی",
+ "known_tracker": "ردیاب های شناخته شده",
+ "language": "زبان",
+ "light_theme": "پوسته روشن",
+ "login": "ورود",
+ "logout": "خروج",
+ "make_static": "ایستا کنید",
+ "milliseconds_abbreviation": "هـ ثـ",
+ "monday": "دوشنبه",
+ "name": "نام",
+ "name_label": "نام",
+ "name_table_header": "نام",
+ "name_tooltip": "نام",
+ "next": "بعدي",
+ "no": "خیر",
+ "none_text": "هيچکدام",
+ "nothing_found": "هیچ چیز یافت نشد",
+ "notify_undo": "آندو",
+ "notify_updated": "بروز رسانی شده",
+ "parental_control": "نظارت والدین",
+ "parental_group_cdn": "CDN",
+ "parental_group_development": "توسعه نرمافزار",
+ "parental_group_gaming": "بازی",
+ "parental_group_hosting": "میزبانی وب",
+ "parental_group_privacy": "ابزارهای حریم خصوصی",
+ "parental_group_shopping": "خرید",
+ "parental_group_social": "شبکه اجتماعی",
+ "parental_group_social_network": "شبکه اجتماعی",
+ "parental_group_software": "توسعه نرمافزار",
+ "password_label": "رمزعبور",
+ "password_placeholder": "رمزعبور را وارد کنید",
+ "plain_dns": "DNS ساده",
+ "privacy_policy": "سیاست حریم خصوصی",
+ "protection": "حفاظت",
+ "protocol": "انتخاب پروتکل",
+ "query_log_allowed": "لیست سفید",
+ "query_log_blocked": "مسدود شده",
+ "query_log_blocked_services": "سرویس های مسدود شده",
+ "query_log_blocked_threats": "تهدید مسدود شده",
+ "query_log_custom_filtering_rules": "دستورات فیلترینگ دستی",
+ "query_log_nothing_available": "هیچ چیز یافت نشد",
+ "query_log_processed": "پردازش شده",
+ "query_log_rewritten": "بازنویسی شده",
+ "query_log_safe_search": "فعالسازی جستجوی اَمن",
+ "query_log_strict_search": "برای جستجوی موکد از علامت نقل قول دوتایی استفاده کنید",
+ "rate_limit_subnet_len_ipv4_error": "طول پیشوند زیرشبکه IPv4 باید بین ۰ و ۳۲ باشد",
+ "rate_limit_subnet_len_ipv6_error": "طول پیشوند زیرشبکه IPv6 باید بین ۰ تا ۱۲۸ باشد",
+ "refresh_btn": "تازه سازی",
+ "refresh_statics": "تازه سازی آمار",
+ "release_notes": "یادداشت های انتشار",
+ "remove": "حذف",
+ "report_an_issue": "گزارش یک مشکل",
+ "request_table_header": "درخواست",
+ "requests_table_header": "درخواست",
+ "reset": "ريست",
+ "reset_settings": "ریست تنظیمات",
+ "reset_settings_confirm": "بله",
+ "response_details": "جزئیات پاسخ",
+ "response_time": "زمان پاسخ",
+ "result": "نتیجه",
+ "rewrite_add": "افزودن بازنویسی DNS",
+ "rewrite_edit": "بازنویسی DNS را ویرایش کنید",
+ "rewrite_hosts_applied": "بازنویسی با دستور فایل میزبان",
+ "rewrite_settings_updated": "تنظيمات بازنویسی DNS با موفقیت بهروزرسانی شد",
+ "rewrite_updated": "بازنویسی DNS با موفقیت به روز شد",
+ "rewritten": "بازنویسی شده",
+ "rules_label": "دستورات",
+ "safe_browsing": "وب گردی اَمن",
+ "safe_search": "فعالسازی جستجوی اَمن",
+ "saturday": "شنبه",
+ "save": "ذخیره",
+ "save_btn": "ذخیره",
+ "search_placeholder": "جستجو",
+ "server_config_blocking_mode": "حالت مسدودسازی",
+ "server_config_blocking_mode_ipv4": "مسدودسازی IPv4",
+ "server_config_blocking_mode_ipv6": "مسدودسازی IPv6",
+ "server_config_custom_ip": "آدرس IP دستی",
+ "server_config_default": "پيش فرض",
+ "server_config_disable_ipv6": "غیرفعالسازی IPv6",
+ "server_config_dnssec_enable": "فعالسازی DNSSEC",
+ "server_config_edns_enable": "فعالسازی زیرشبکه کلاینت EDNS",
+ "server_config_nxdomain": "NXDOMAIN",
+ "server_config_rate_limit_placeholder": "میزان محدودیت را وارد کنید",
+ "server_config_rate_limit_whitelist_faq": "نشانیهای آی پی استثناء شده از محدودیت نرخ",
+ "server_config_refused": "مرفوض",
+ "server_config_subnet_len_ipv4": "طول پیشوند زیرشبکه برای نشانیهای IPv4",
+ "server_config_subnet_len_ipv4_faq": "طول پیشوند زیرشبکه برای نشانیهای IPv4 استفاده شده برای محدودیت نرخ. پیشفرض ۲۴ است",
+ "server_config_subnet_len_ipv6": "طول پیشوند زیرشبکه برای نشانیهای IPv6",
+ "server_config_subnet_len_ipv6_faq": "طول پیشوند زیرشبکه برای نشانیهای IPv6 استفاده شده برای محدودیت نرخ. پیشفرض ۵۶ است",
+ "set_static_ip": "تنظیم یک آدرس آی پی ثابت",
+ "settings": "تنظیمات",
+ "settings_browsing_security": "امنیت وب گردی",
+ "settings_confirm_clear_statistics": "آمار پاک شود؟",
+ "settings_custom": "دستي",
+ "settings_general_short": "General",
+ "settings_global": "سراسری",
+ "settings_notify_changes_saved": "تغییرات ذخیره شد",
+ "settings_notify_statistics_cleared": "آمار پاک شد",
+ "settings_parental_control": "نظارت والدین",
+ "settings_rule_syntax": "نحو قوانین",
+ "settings_safe_search": "فعالسازی جستجوی اَمن",
+ "settings_statistics": "آمارها",
+ "settings_statistics_clear": "بازنشانی آمار",
+ "settings_statistics_retention": "مدت حفظ آمارها",
+ "settings_yes_clear": "بله",
+ "setup_devices_router_title": "روتر",
+ "setup_dns_privacy_ioc_mac": "پیکربندی iOS و macOS",
+ "setup_dns_privacy_other_title": "سایر راه کارها",
+ "show_blocked_responses": "مسدود شده",
+ "show_processed_responses": "پردازش شده",
+ "source_label": "منبع",
+ "source_tooltip": "منبع",
+ "stats_adult": "وبسایت غیراخلاقی مسدود شده است",
+ "stats_query_domain": "دامنه جستار بالا",
+ "status_table_header": "وضعیت",
+ "sunday": "یکشنبه",
+ "system_host_files": "فایل های هوست سیستم",
+ "table_client": "کلاینت",
+ "tags_title": "برچسب ها",
+ "test_upstreams": "تست جریان ارسالی",
+ "threats_blocked": "تهدید مسدود شد",
+ "thursday": "پنج شنبه",
+ "time_table_header": "زمان",
+ "top_blocked_domains": "دامنه های بیشتر مسدود شده",
+ "top_clients": "بالاترین کلاینت ها",
+ "top_upstreams": "سرورهای بالادست بالا",
+ "total": "مجموع",
+ "try_again": "امتحان دوباره",
+ "tuesday": "سهشنبه",
+ "unavailable_dhcp": "DHCP در دسترس نیست",
+ "unavailable_dhcp_desc": "AdGuard Home نمی تواند سرور DHCP را روی سیستم عامل شما اجرا کند",
+ "unblock": "باز کن",
+ "update_button": "بروز رسانی",
+ "update_filters_disable": "غيرفعالسازي",
+ "update_filters_interval_custom": "دستي",
+ "update_filters_interval_daily": "هر روز",
+ "update_filters_interval_hourly": "هر ساعت",
+ "update_filters_title": "بروز رسانی خودکار فیلترها",
+ "updates_checked": "نسخه جدیدی از AdGuard Home در دسترس است",
+ "updates_version_equal": "AdGuard Home بروز است",
+ "upstream": "سرور مادر",
+ "upstream_dns": "سرورهای DNS جریان ارسالی",
+ "upstream_dns_addresses": "آدرس های سرور، یکی در هر خط",
+ "upstream_dns_fastest_addr": "سریعترین آدرس آی پی",
+ "upstream_dns_parallel_requests": "درخواست های موازی",
+ "upstream_dns_servers_title": "سرورهای DNS جریان ارسالی",
+ "upstream_examples_title": "مثال ها",
+ "upstream_private_ptr_resolvers_title": "از تعیین کننده های rDNS محرمانه استفاده کنید",
+ "upstreams": "جریان ارسالی",
+ "url_label": "آدرس وب",
+ "user_rules": "فیلتر کاربر",
+ "user_rules_add_to_allowlist": "افزودن به لیست سفید",
+ "user_rules_check_button": "بررسی",
+ "user_rules_check_client_placeholder": "شناسه مشتری را وارد کنید",
+ "user_rules_check_hostname_label": "نام میزبان یا نام دامنه",
+ "user_rules_disable_filter": "غیرفعالسازی فیلتر",
+ "user_rules_disable_parental_control": "غيرفعالسازي نظارت والدین",
+ "user_rules_dns_record_type_placeholder": "نوع رکورد DNS را انتخاب کنید",
+ "user_rules_edit_dns_rewrite": "بازنویسی DNS را ویرایش کنید",
+ "user_rules_rewrite_rule_is_applied": "دستور بازنویسی اِعمال شد",
+ "username_label": "نام کاربر",
+ "username_placeholder": "نام کاربر را وارد کنید",
+ "validated_with_dnssec": "معتبر سازی با DNSSEC",
+ "wednesday": "چهار شنبه",
+ "whois": "هوئیز",
+ "yes": "بله",
+ "yes_disable": "بله",
+ "yes_reset": "بله"
+}
diff --git a/client_v2/src/__locales/fi.json b/client_v2/src/__locales/fi.json
new file mode 100644
index 00000000000..b1a820f74b9
--- /dev/null
+++ b/client_v2/src/__locales/fi.json
@@ -0,0 +1,390 @@
+{
+ "access_settings_title": "Käytön asetukset",
+ "actions_table_header": "Toiminnot",
+ "add": "Lisää",
+ "add_allowlist": "Lisää sallittujen lista",
+ "add_blocklist": "Lisää estolista",
+ "add_persistent_client": "Lisää pysyvänä päätelaitteena",
+ "add_to_allowlist": "Lisää sallittuihin",
+ "ads_blocked": "Mainokset",
+ "ads_blocked_card": "Mainokset",
+ "adult_websites_blocked_tooltip": "Estettyjen aikuisille tarkoitettujen sivustojen määrä",
+ "all_lists_up_to_date_toast": "Kaikki listat ovat ajan tasalla",
+ "allowed": "Sallitut",
+ "allowlist_add": "Lisää sallittujen lista",
+ "allowlist_edit": "Muokkaa sallittujen listaa",
+ "apply": "Käytä",
+ "autofix_warning_result": "Tämän jälkeen järjestelmäsi kaikki DNS-pyynnöt käsittelee oletusarvoisesti AdGuard Home.",
+ "autofix_warning_text": "Jos painat \"Korjaa\", AdGuard Home määrittää järjestelmäsi käyttämään AdGuard Homen DNS-palvelinta.",
+ "average_time_processing": "Keskimääräinen käsittelyaika",
+ "average_upstream_response_time": "Ylävirran keskimääräinen vasteaika",
+ "back": "Palaa takaisin",
+ "block": "Estä",
+ "block_for_this_client_only": "Estä vain tältä päätelaitteelta",
+ "blocked": "Estetty",
+ "blocked_by_cname_or_ip": "Estetty CNAME:n tai IP:n perusteella",
+ "blocked_service": "Estetty palvelu",
+ "blocked_services": "Estetyt palvelut",
+ "blocked_threats": "Estettyä uhkaa",
+ "blocked_threats_chart": "Estettyä uhkaa",
+ "blocklist_add_manual": "Lisää manuaalisesti",
+ "blocklist_edit": "Muokkaa estolistaa",
+ "blocklist_homepage": "Verkkosivusto",
+ "blocklist_url_file_path": "URL-osoite tai tiedostosijainti",
+ "bootstrap_dns": "Bootstrap DNS-palvelimet",
+ "browse": "Selaa",
+ "cache_cleared": "DNS-välimuistin tyhjennys onnistui",
+ "cache_config_clear": "Tyhjennä välimuisti",
+ "cache_config_optimistic": "Optimistinen välimuisti",
+ "cache_config_ttl_validation": "Välimuistin vähimmäiselinajan on oltava pienempi tai sama kuin enimmäiselinajan",
+ "cache_enabled": "Ota välimuisti käyttöön",
+ "cancel": "Peruuta",
+ "cancel_btn": "Peruuta",
+ "category_tooltip": "Luokitus",
+ "changes_saved_success": "Muutokset tallennettu",
+ "check_dhcp_servers": "Etsi DHCP-palvelimia",
+ "check_not_found": "Ei löytynyt suodatinlistoilta",
+ "check_updates_btn": "Tarkista päivitykset",
+ "clear_btn": "Tyhjennä",
+ "client_add": "Lisää päätelaite",
+ "client_details": "Päätelaitteen tiedot",
+ "client_id": "ClientID",
+ "client_id_placeholder": "Syötä ClientID",
+ "client_identifier": "Tunniste",
+ "client_settings": "Päätelaiteasetukset",
+ "client_table_header": "Asiakas",
+ "clients_add_identifier": "Lisää tunniste",
+ "clients_add_placeholder": "Syötä päätelaitteen nimi",
+ "clients_dns_cache_size": "DNS-välimuistin koko tavuina",
+ "clients_not_found": "Päätelaitteita ei löytynyt",
+ "clients_protection": "Suojaus",
+ "clients_tags": "Tunnisteet",
+ "clients_use_global_settings": "Käytä yleisiä asetuksia",
+ "close": "Sulje",
+ "confirm": "Vahvista",
+ "copied": "Kopioitiin leikepöydälle",
+ "copy": "Kopioi",
+ "copy_to_clipboard": "Kopioi leikepöydälle",
+ "custom_filtering_rules": "Omat suodatussäännöt",
+ "dark_theme": "Tumma teema",
+ "dashboard": "Tila",
+ "delete_btn": "Poista",
+ "delete_table_action": "Poista",
+ "dhcp_add_static_lease": "Lisää kiinteä laina",
+ "dhcp_config_saved": "DHCP-asetukset tallennettiin",
+ "dhcp_edit_static_lease": "Muokkaa kiinteää laina",
+ "dhcp_form_gateway_input": "Yhdyskäytävän IP-osoite",
+ "dhcp_form_subnet_input": "Aliverkon peite",
+ "dhcp_leases_not_found": "DHCP-lainoja ei löytynyt",
+ "dhcp_leases_title": "DHCP-lainat",
+ "dhcp_new_static_lease": "Uusi kiinteä laina",
+ "dhcp_reset_leases": "Tyhjennä kaikki lainat",
+ "dhcp_reset_leases_success": "DHCP-lainojen tyhjennys onnistui",
+ "dhcp_settings": "DHCP-asetukset",
+ "dhcp_static_leases_not_found": "Kiinteitä DHCP-lainoja ei löytynyt",
+ "dhcp_table_hostname": "Isäntänimi",
+ "dhcp_table_ip_address": "IP-osoite",
+ "disable": "Poista käytöstä",
+ "disable_protection_btn": "Poista suojaus käytöstä",
+ "disabled_dhcp": "DHCP-palvelin poistettiin käytöstä",
+ "disallow_this_client": "Estä tämä päätelaite",
+ "dns_access_settings_title": "Käytön asetukset",
+ "dns_allowed_clients": "Sallitut päätelaitteet",
+ "dns_blocking_mode": "Estotila",
+ "dns_blocking_mode_custom_ip": "Oma IP-osoite",
+ "dns_blocking_mode_default": "Oletus",
+ "dns_blocking_mode_ipv4_label": "IPv4-esto",
+ "dns_blocking_mode_ipv6_label": "IPv6-esto",
+ "dns_blocking_mode_nxdomain": "NXDOMAIN",
+ "dns_blocking_mode_refused": "REFUSED",
+ "dns_blocking_mode_title": "Estotila",
+ "dns_bootstrap_dns_placeholder": "IP-osoitteet",
+ "dns_bootstrap_dns_title": "Bootstrap DNS-palvelimet",
+ "dns_bootstrap_servers": "Bootstrap DNS-palvelimet",
+ "dns_cache_config": "DNS-välimuistin määritys",
+ "dns_cache_size": "Välimuistin koko",
+ "dns_cache_size_label": "DNS-välimuistin koko tavuina",
+ "dns_cache_size_title": "Välimuistin koko",
+ "dns_clear_cache": "Tyhjennä välimuisti",
+ "dns_clear_cache_cancel": "Peruuta",
+ "dns_clear_cache_confirm": "Kyllä, tyhjennä",
+ "dns_clear_cache_yes": "Kyllä, tyhjennä",
+ "dns_config": "DNS-palvelimen määritys",
+ "dns_disallowed_clients": "Estetyt päätelaitteet",
+ "dns_disallowed_clients_title": "Estetyt päätelaitteet",
+ "dns_disallowed_domains": "Estetyt verkkotunnukset",
+ "dns_disallowed_domains_title": "Estetyt verkkotunnukset",
+ "dns_edns_custom_placeholder": "Syötä IP-osoite",
+ "dns_fallback_dns_title": "DNS-varapalvelimet",
+ "dns_fallback_servers": "DNS-varapalvelimet",
+ "dns_optimistic_caching": "Optimistinen välimuisti",
+ "dns_over_https": "DNS-over-HTTPS",
+ "dns_over_quic": "DNS-over-QUIC",
+ "dns_over_tls": "DNS-over-TLS",
+ "dns_override_max_ttl": "Korvaa enimmäis-TTL",
+ "dns_override_max_ttl_title": "Korvaa enimmäis-TTL",
+ "dns_override_min_ttl": "Korvaa vähimmäis-TTL",
+ "dns_override_min_ttl_title": "Korvaa vähimmäis-TTL",
+ "dns_privacy": "DNS-tietosuoja",
+ "dns_private_reverse_servers_title": "Yksityiset käänteis-DNS-palvelimet",
+ "dns_rate_limit": "Pyyntöajoitus",
+ "dns_rate_limit_allowlist_placeholder": "IP-osoitteet",
+ "dns_rate_limit_placeholder": "Syötä pyyntörajoitus",
+ "dns_rate_limit_title": "Pyyntöajoitus",
+ "dns_rewrites": "DNS-uudelleenohjaukset",
+ "dns_server_addresses": "Palvelimen osoite",
+ "dns_server_addresses_label": "Palvelinten osoitteet, yksi riviä kohden",
+ "dns_server_addresses_title": "Palvelimen osoite",
+ "dns_server_config_title": "DNS-palvelimen määritys",
+ "dns_settings": "DNS",
+ "dns_test_upstreams": "Testaa ylävirtoja",
+ "dns_upstream_servers_title": "Ylävirran DNS-palvelimet",
+ "dns_upstream_timeout": "Ylöspäin suuntautuva aikakatkaisu",
+ "dns_upstream_timeout_title": "Ylöspäin suuntautuva aikakatkaisu",
+ "dns_upstream_validation_invalid": "Virheellinen muoto.",
+ "domain": "Verkkotunnus",
+ "domain_or_client": "Verkkotunnus tai päätelaite",
+ "download_mobileconfig": "Lataa asetustiedosto",
+ "edit_table_action": "Muokkaa",
+ "enable": "Ota käyttöön",
+ "enabled_dhcp": "DHCP-palvelin otettiin käyttöön",
+ "encryption_certificate_path": "Varmenteen sijainti",
+ "encryption_certificates": "Varmenteet",
+ "encryption_certificates_input": "Kopioi/liitä PEM-koodatut varmenteesi tähän.",
+ "encryption_certificates_source_content": "Liitä varmenteen sisältö",
+ "encryption_certificates_source_path": "Määritä varmennetiedoston sijainti",
+ "encryption_chain_invalid": "Varmenneketju ei kelpaa",
+ "encryption_chain_valid": "Varmenneketju on kelvollinen",
+ "encryption_config_saved": "Salausasetukset tallennettiin",
+ "encryption_doq": "DNS-over-QUIC-portti",
+ "encryption_dot": "DNS-over-TLS -portti",
+ "encryption_https": "HTTPS-portti",
+ "encryption_key": "Yksityinen avain",
+ "encryption_key_input": "Kopioi/liitä tähän varmenteesi PEM-koodattu yksityinen avain.",
+ "encryption_key_source_content": "Liitä yksityisen avaimen sisältö",
+ "encryption_plain_dns": "Tavallinen DNS",
+ "encryption_plain_dns_error": "Voit poistaa tavallisen DNS:n käytöstä ottamalla käyttöön ainakin yhden salatun DNS-protokollan.",
+ "encryption_private_key_path": "Yksityisen avaimen sijainti",
+ "encryption_server": "Palvelimen nimi",
+ "encryption_server_enter": "Syötä verkkotunnuksesi",
+ "encryption_title": "Salaus",
+ "enter_cache_size": "Syötä välimuistin koko (tavuina)",
+ "enter_ip_address_placeholder": "Syötä IP-osoite",
+ "error": "Virhe",
+ "fallback_dns": "DNS-varapalvelimet",
+ "filter_category_general": "Yleiset",
+ "filter_category_general_desc": "Listat, jotka estävät seurannan ja mainokset useimmilla laitteilla",
+ "filter_category_other": "Muut",
+ "filter_category_other_desc": "Muut estolistat",
+ "filter_category_regional": "Alueelliset",
+ "filter_category_regional_desc": "Listat, jotka painottavat alueellisia mainoksia ja seurantapalvelimia",
+ "filter_category_security": "Tietoturva",
+ "filter_category_security_desc": "Tietojenkalastelu-, huijaus- ja muiden haitallisten verkkotunnusten estoon erikoistuneet listat",
+ "filters": "Suodattimet",
+ "forgot_password": "Salasana unohtunut?",
+ "form_enter_blocked_response_ttl": "Syötä estetyn vastauksen elinaika (sekuntia)",
+ "form_error_client_id_format": "ClientID-tunniste voi sisältää ainoastaan numeroita, pieniä kirjaimia sekä yhdysviivoja",
+ "form_error_domain_format": "Virheellinen verkkotunnuksen muoto",
+ "form_error_equal": "Ei voi olla sama",
+ "form_error_format": "Virheellinen muoto.",
+ "form_error_gateway_ip": "Lainalla ei voi olla yhdyskäytävän IP-osoitetta",
+ "form_error_ip4_format": "Virheellinen IPv4-osoite",
+ "form_error_ip6_format": "Virheellinen IPv6-osoite",
+ "form_error_ip_format": "Virheellinen IP-osoite",
+ "form_error_mac_format": "Virheellinen MAC-osoite",
+ "form_error_password": "Salasanat eivät täsmää",
+ "form_error_port": "Syötä kelvollinen portti",
+ "form_error_port_range": "Syötä portti väliltä 80-65535",
+ "form_error_port_unsafe": "Portti ei ole turvallinen",
+ "form_error_server_name": "Virheellinen palvelimen nimi",
+ "form_error_url_format": "Syötä kelvollinen URL-osoite tai tiedostosijainti.",
+ "friday": "Perjantai",
+ "gateway_or_subnet_invalid": "Virheellinen aliverkon peite",
+ "general_statistics": "Yleiset tilastot",
+ "ignore_domains_title": "Ohitettavat verkkotunnukset",
+ "inactivity_schedule_add": "Lisää ajoitus",
+ "inactivity_schedule_all_day": "Koko päivän",
+ "inactivity_schedule_edit": "Muokkaa ajoitus",
+ "install_auth_confirm": "Vahvista salasana",
+ "install_auth_password": "Salasana",
+ "install_auth_password_enter": "Syötä salasana",
+ "install_auth_username": "Käyttäjätunnus",
+ "install_auth_username_enter": "Syötä käyttäjätunnus",
+ "install_saved": "Tallenus onnistui",
+ "install_settings_all_interfaces": "Kaikki verkkosovittimet",
+ "install_settings_port": "Portti",
+ "install_static_error": "AdGuard Home ei voi määrittää sitä tälle verkkosovittimelle automaattisesti. Etsi ohjeita tämän suorittamiseksi itse.",
+ "install_static_ok": "Hyviä uutisia! Kiinteä IP-osoite on jo määritetty.",
+ "install_step": "Vaihe",
+ "install_submit_title": "Onnittelut!",
+ "ip_address": "IP-osoite",
+ "known_tracker": "Tunnettu seuranta",
+ "language": "Kieli",
+ "light_theme": "Vaalea teema",
+ "login": "Kirjaudu",
+ "logout": "Kirjaudu ulos",
+ "make_static": "Tallenna kiinteäksi",
+ "milliseconds_abbreviation": "ms",
+ "monday": "Maanantai",
+ "name": "Nimi",
+ "name_label": "Nimi",
+ "name_table_header": "Nimi",
+ "name_tooltip": "Nimi",
+ "next": "Seuraava",
+ "no": "Ei",
+ "none_text": "Ei määritetty",
+ "nothing_found": "Ei tuloksia",
+ "notify_undo": "Kumoa",
+ "notify_updated": "Päivitetty",
+ "parental_control": "Lapsilukko",
+ "parental_group_cdn": "Sisällönjakelu (CDN)",
+ "parental_group_development": "Ohjelmistokehitys",
+ "parental_group_gaming": "Pelaaminen",
+ "parental_group_hosting": "Web-hosting",
+ "parental_group_privacy": "Yksityisyyden työkalut",
+ "parental_group_shopping": "Verkkokaupat",
+ "parental_group_social": "Sosiaaliset verkostot",
+ "parental_group_social_network": "Sosiaaliset verkostot",
+ "parental_group_software": "Ohjelmistokehitys",
+ "parental_group_streaming": "Suoratoisto",
+ "password_label": "Salasana",
+ "password_placeholder": "Syötä salasana",
+ "plain_dns": "Tavallinen DNS",
+ "privacy_policy": "Tietosuojakäytäntö",
+ "protection": "Suojaus",
+ "protocol": "Protokolla",
+ "query_log_allowed": "Sallitut",
+ "query_log_blocked": "Estetty",
+ "query_log_blocked_services": "Estetyt palvelut",
+ "query_log_blocked_threats": "Estettyä uhkaa",
+ "query_log_custom_filtering_rules": "Omat suodatussäännöt",
+ "query_log_nothing_available": "Ei tuloksia",
+ "query_log_processed": "Käsitelty",
+ "query_log_rewritten": "Uudelleenohjatut",
+ "query_log_safe_search": "Turvallinen haku",
+ "query_log_strict_search": "Käytä tarkalle haulle lainausmerkkejä",
+ "rate_limit_subnet_len_ipv4_error": "IPv4-aliverkon etuliitteen pituuden tulee olla väliltä 0–32.",
+ "rate_limit_subnet_len_ipv6_error": "IPv6-aliverkon etuliitteen pituuden tulee olla väliltä 0–128.",
+ "refresh_btn": "Päivitä",
+ "refresh_statics": "Päivitä tilastot",
+ "release_notes": "Julkaisutiedot",
+ "remove": "Poista",
+ "report_an_issue": "Ilmoita ongelmasta",
+ "request_table_header": "Pyyntö",
+ "requests_table_header": "Pyynnöt",
+ "reset": "Palauta",
+ "reset_settings": "Tyhjennä asetukset",
+ "reset_settings_confirm": "Kyllä, poista",
+ "response_details": "Vastauksen tiedot",
+ "response_time": "Vasteaika",
+ "result": "Tulos",
+ "rewrite_add": "Lisää DNS-uudelleenohjaus",
+ "rewrite_edit": "Muokkaa DNS-uudelleenohjausta",
+ "rewrite_hosts_applied": "Hosts-tiedoston säännön korvaama",
+ "rewrite_settings_updated": "DNS-uudelleenkirjoitusasetukset päivitetty onnistuneesti",
+ "rewrite_updated": "DNS-uudelleenohjaukset päivitettiin",
+ "rewritten": "Uudelleenohjatut",
+ "rules_label": "Sääntöjä",
+ "safe_browsing": "Turvallinen selaus",
+ "safe_search": "Turvallinen haku",
+ "saturday": "Lauantai",
+ "save": "Tallenna",
+ "save_btn": "Tallenna",
+ "search_placeholder": "Haku",
+ "server_config_blocking_mode": "Estotila",
+ "server_config_blocking_mode_ipv4": "IPv4-esto",
+ "server_config_blocking_mode_ipv6": "IPv6-esto",
+ "server_config_custom_ip": "Oma IP-osoite",
+ "server_config_default": "Oletus",
+ "server_config_disable_ipv6": "Älä selvitä IPv6-osoitteita",
+ "server_config_dnssec_enable": "Ota DNSSEC käyttöön",
+ "server_config_edns_enable": "Käytä EDNS-päätelaitealivekkoa",
+ "server_config_nxdomain": "NXDOMAIN",
+ "server_config_rate_limit_placeholder": "Syötä pyyntörajoitus",
+ "server_config_rate_limit_whitelist_faq": "IP-osoitteet, jotka eivät kuulu pyyntörajoituksen piiriin.",
+ "server_config_refused": "REFUSED",
+ "server_config_subnet_len_ipv4": "IPv4-osoitteiden aliverkon etuliitteen pituus",
+ "server_config_subnet_len_ipv4_faq": "Pyyntörajoitukseen käytettävien IPv4-osoitteiden aliverkon etuliitteen pituus. Oletusarvo on 24.",
+ "server_config_subnet_len_ipv6": "IPv6-osoitteiden aliverkon etuliitteen pituus",
+ "server_config_subnet_len_ipv6_faq": "Pyyntörajoitukseen käytettävien IPv6-osoitteiden aliverkon etuliitteen pituus. Oletusarvo on 56.",
+ "set_static_ip": "Määritä kiinteä IP-osoite",
+ "settings": "Asetukset",
+ "settings_browsing_security": "Turvallinen selaus",
+ "settings_confirm_clear_statistics": "Haluatko tyhjentää tilastot?",
+ "settings_custom": "Mukautettu",
+ "settings_general_short": "Yleiset",
+ "settings_global": "Yleinen",
+ "settings_notify_changes_saved": "Muutokset tallennettu",
+ "settings_notify_statistics_cleared": "Tilastot tyhjennetty",
+ "settings_parental_control": "Lapsilukko",
+ "settings_rule_syntax": "Sääntösyntaksi",
+ "settings_safe_search": "Turvallinen haku",
+ "settings_statistics": "Tilastot",
+ "settings_statistics_clear": "Tyhjennä tilastot",
+ "settings_statistics_retention": "Tilastojen säilytys",
+ "settings_yes_clear": "Kyllä, tyhjennä",
+ "setup_devices_router_title": "Reititin",
+ "setup_dns_privacy_ioc_mac": "iOS ja macOS -asetukset",
+ "setup_dns_privacy_other_title": "Muita toteutuksia",
+ "show_blocked_responses": "Estetty",
+ "show_processed_responses": "Käsitelty",
+ "source_label": "Lähde",
+ "source_tooltip": "Lähde",
+ "stats_adult": "Estetyt aikuisille tarkoitetut sivustot",
+ "stats_query_domain": "Kysytyimmät verkkotunnukset",
+ "status_table_header": "Tila",
+ "sunday": "Sunnuntai",
+ "system_host_files": "Järjestelmän hosts-tiedostot",
+ "table_client": "Asiakas",
+ "tags_title": "Tunnisteet",
+ "test_upstreams": "Testaa ylävirtoja",
+ "threats_blocked": "Uhkia estetty",
+ "thursday": "Torstai",
+ "time_table_header": "Aika",
+ "top_blocked_domains": "Estetyimmät verkkotunnukset",
+ "top_clients": "Käytetyimmät päätelaitteet",
+ "top_upstreams": "Käytetyimmät ylävirrat",
+ "total": "Kaikki yhteensä",
+ "try_again": "Yritä uudelleen",
+ "tuesday": "Tiistai",
+ "unavailable_dhcp": "DHCP ei ole käytettävissä",
+ "unavailable_dhcp_desc": "AdGuard Home ei voi suorittaa DHCP-palvelinta käyttöjärjestelmässäsi",
+ "unblock": "Salli",
+ "update_button": "Päivitä",
+ "update_filters_disable": "Poista käytöstä",
+ "update_filters_interval_custom": "Mukautettu",
+ "update_filters_interval_daily": "Joka päivä",
+ "update_filters_interval_hourly": "Tunnin välein",
+ "update_filters_title": "Pitää suodattimet automaattisesti ajan tasalla",
+ "updates_checked": "Uusi versio AdGuard Home -ohjelmasta on saatavana\n",
+ "updates_version_equal": "AdGuard Home on ajan tasalla",
+ "upstream": "Ylävirta",
+ "upstream_dns": "Ylävirran DNS-palvelimet",
+ "upstream_dns_addresses": "Palvelinten osoitteet, yksi riviä kohden",
+ "upstream_dns_fastest_addr": "Nopein IP-osoite",
+ "upstream_dns_parallel_requests": "Rinnakkaiset pyynnöt",
+ "upstream_dns_servers_title": "Ylävirran DNS-palvelimet",
+ "upstream_examples_title": "Esimerkkejä",
+ "upstream_private_ptr_resolvers_title": "Käytä yksityisiä käänteis-DNS-resolvereita",
+ "upstreams": "Ylävirrat",
+ "url_label": "URL-osoite",
+ "user_rules": "Käyttäjän säännöt",
+ "user_rules_add_to_allowlist": "Lisää sallittuihin",
+ "user_rules_check_button": "Tarkasta",
+ "user_rules_check_client_placeholder": "Syötä asiakastunniste",
+ "user_rules_check_hostname_label": "Isäntänimi tai verkkotunnus",
+ "user_rules_disable_filter": "Poista suodatin käytöstä",
+ "user_rules_disable_parental_control": "Poista lapsilukko käytöstä",
+ "user_rules_dns_record_type_placeholder": "Valitse DNS-tietueen tyyppi",
+ "user_rules_domain_blocked": "Verkkotunnus on estetty",
+ "user_rules_edit_dns_rewrite": "Muokkaa DNS-uudelleenohjausta",
+ "user_rules_rewrite_rule_is_applied": "Uudelleenohjattu säännöllä",
+ "username_label": "Käyttäjätunnus",
+ "username_placeholder": "Syötä käyttäjätunnus",
+ "validated_with_dnssec": "DNSSEC-vahvistettu",
+ "wednesday": "Keskiviikko",
+ "whois": "WHOIS",
+ "yes": "Kyllä",
+ "yes_disable": "Kyllä, lopetetaan",
+ "yes_reset": "Kyllä, poista"
+}
diff --git a/client_v2/src/__locales/fr.json b/client_v2/src/__locales/fr.json
new file mode 100644
index 00000000000..6d45b3a3851
--- /dev/null
+++ b/client_v2/src/__locales/fr.json
@@ -0,0 +1,604 @@
+{
+ "access_settings_allowed_faq": "Une liste de CIDR, d'adresses IP ou de ClientID . Si ce champ contient des entrées, AdGuard Home n'acceptera que les requêtes provenant de ces clients",
+ "access_settings_allowed_placeholder": "Saisir les clients autorisés",
+ "access_settings_allowed_title": "Clients autorisés, un par ligne",
+ "access_settings_blocked_faq_1": "AdGuard Home ignorera les requêtes DNS correspondant à ces domaines. Ces requêtes n'apparaissent pas dans le journal des requêtes. Vous pouvez saisir des noms de domaine exacts, des caractères génériques ou des règles de filtrage d'URL, par exemple example.org, *.example.org ou ||example.org^",
+ "access_settings_blocked_faq_2": "À ne pas confondre avec les filtres",
+ "access_settings_blocked_placeholder": "Saisir les domaines interdits",
+ "access_settings_blocked_title": "Domaines interdits, un par ligne",
+ "access_settings_disallowed_placeholder": "Saisir les clients refusés",
+ "access_settings_disallowed_title": "Clients refusés, un par ligne",
+ "access_settings_saved_toast": "Paramètres d'accès enregistrés",
+ "access_settings_title": "Paramètres d'accès",
+ "actions_table_header": "Actions",
+ "add": "Ajouter",
+ "add_allowlist": "Ajouter liste d’autorisation",
+ "add_blocklist": "Ajouter liste de blocage",
+ "add_persistent_client": "Ajouter comme client persistant",
+ "add_to_allowlist": "Ajouter à la liste blanche",
+ "ads_blocked": "Annonces bloquées",
+ "ads_blocked_card": "Annonces bloquées",
+ "adult_websites_blocked_tooltip": "Le nombre de sites à contenu adulte bloqués",
+ "all_lists_up_to_date_toast": "Toutes les listes sont déjà à jour",
+ "allow_service": "Autoriser le service",
+ "allowed": "En Liste blanche",
+ "allowlist_add": "Ajouter liste d’autorisation",
+ "allowlist_edit": "Modifier la liste d’autorisation",
+ "allowlist_remove": "Supprimer la liste d’autorisation ?",
+ "allowlists_title": "Listes d'autorisation",
+ "apply": "Appliquer",
+ "aria_clear_input": "Effacer la saisie",
+ "autofix_warning_result": "Par conséquent, toutes les demandes DNS de votre système seront traitées par AdGuardHome par défaut.",
+ "autofix_warning_text": "Si vous cliquez sur « Réparer », AdGuard Home configurera votre système pour utiliser le serveur DNS AdGuard Home.",
+ "average_time_processing": "Temps moyen de traitement",
+ "average_upstream_response_time": "Temps de réponse moyen en amont",
+ "back": "Retour",
+ "block": "Bloquer",
+ "block_client": "Bloquer un client",
+ "block_for_this_client_only": "Bloquer uniquement pour ce client",
+ "blocked": "Bloqué",
+ "blocked_by_cname_or_ip": "Bloqué par CNAME ou adresse IP",
+ "blocked_queries": "Requêtes bloquées",
+ "blocked_service": "Service bloqué",
+ "blocked_services": "Services bloqués",
+ "blocked_threats": "Menaces bloquées",
+ "blocked_threats_chart": "Menaces bloquées",
+ "blocklist_add_from_list": "Ajouter à partir de la liste",
+ "blocklist_add_manual": "Ajouter manuellement",
+ "blocklist_edit": "Modifier la liste de blocage",
+ "blocklist_homepage": "Page d'accueil",
+ "blocklist_placeholder_example": "Ma liste de blocage",
+ "blocklist_url_file_path": "URL ou chemin du fichier ",
+ "blocklists_add": "Ajouter des listes de blocage",
+ "blocklists_add_desc": "Vous pouvez sélectionner des listes de blocage dans la liste ou les ajouter manuellement",
+ "blocklists_desc": "Les domaines figurant dans ces listes sont bloqués",
+ "blocklists_empty": "Il n'y a pas encore de listes de blocage — ajoutez la première ",
+ "bootstrap_dns": "Serveurs DNS d'amorçage",
+ "browse": "Parcourir",
+ "cache_cleared": "Le cache DNS a été vidé",
+ "cache_config_clear": "Vider le cache",
+ "cache_config_max_ttl": "Remplacer la valeur maximale de TTL, en secondes",
+ "cache_config_max_ttl_faq": "La valeur maximale de la durée de vie (TTL) des entrées du cache DNS",
+ "cache_config_min_ttl_placeholder": "Saisissez la valeur minimale de TTL",
+ "cache_config_optimistic": "Caching optimiste",
+ "cache_config_optimistic_desc": "AdGuard Home répondra à partir du cache même si les entrées ont expiré et tentera de les mettre à jour",
+ "cache_config_size_validation": "La taille du cache doit être supérieure à zéro lorsqu'il est activé",
+ "cache_config_ttl_validation": "La valeur TTL minimale du cache doit être inférieure ou égale à la valeur maximale",
+ "cache_confirm_clear_desc": "Cette opération supprimera toutes les réponses DNS mises en cache. Les requêtes DNS suivantes pourraient être plus lentes jusqu'à ce que le cache soit reconstitué",
+ "cache_confirm_clear_title": "Vider le cache DNS",
+ "cache_enabled": "Activer le cache",
+ "cache_enabled_desc": "Enregistrer les réponses DNS localement",
+ "cancel": "Annuler",
+ "cancel_btn": "Annuler",
+ "category_tooltip": "Catégorie",
+ "changes_saved_success": "Modifications enregistrées",
+ "check_dhcp_servers": "Rechercher les serveurs DHCP",
+ "check_not_found": "Introuvable dans vos listes de filtres",
+ "check_updates_btn": "Vérifier les mises à jour",
+ "clear_btn": "Effacer",
+ "client_add": "Ajouter un client",
+ "client_added": "Client ajouté",
+ "client_blocked_flash": "Client bloqué",
+ "client_details": "Détails du client",
+ "client_id": "ClientID",
+ "client_id_faq": "Les clients peuvent être identifiés grâce à leur ClientID. Pour en savoir plus, rendez-vous sur la page GitHub d'AdGuard Home ",
+ "client_id_placeholder": "Saisissez le ClientID",
+ "client_identifier": "Identifiant",
+ "client_ip": "IP du client",
+ "client_location": "Localisation du client",
+ "client_removed": "Client supprimé",
+ "client_settings": "Paramètres du client",
+ "client_table_header": "Client",
+ "clients": "Clients",
+ "clients_add": "Ajouter un client",
+ "clients_add_identifier": "Ajouter identifiant",
+ "clients_add_name": "Nom du client",
+ "clients_add_placeholder": "Saisissez le nom du client",
+ "clients_desc": "Règles applicables aux appareils connectés à AdGuard Home",
+ "clients_dns_cache_size": "Taille du cache DNS, en bytes",
+ "clients_dns_cache_size_placeholder": "Utilisez des chiffres",
+ "clients_dont_collect_stats": "Ne pas collecter de statistiques pour ce client",
+ "clients_dont_log": "Ne pas enregistrer ce client",
+ "clients_edit": "Modifier le client",
+ "clients_identifier_already_used": "Cet identifiant est déjà utilisé",
+ "clients_identifier_format_error": "Utilisez des chiffres, des lettres minuscules de l'alphabet anglais ou des tirets",
+ "clients_identifiers": "Identifiants",
+ "clients_identifiers_desc": "Vous pouvez utiliser l'adresse IP du client, son adresse CIDR, son adresse MAC ou son ClientID (pour les protocoles DoT, DoH et DoQ). En savoir plus ",
+ "clients_logs_and_statistics": "Journaux et statistiques",
+ "clients_not_found": "Pas de clients trouvés",
+ "clients_protection": "Protection",
+ "clients_protection_desc": "Activer ou désactiver les fonctions de protection, la journalisation et les statistiques",
+ "clients_remove_desc": "%value% sera supprimé des clients Persistent, ainsi que ses paramètres",
+ "clients_remove_title": "Supprimer le client ?",
+ "clients_tags": "Mots clés",
+ "clients_tags_desc": "Vous pouvez ajouter des balises pour ce client et les inclure dans vos règles de filtrage. En savoir plus ",
+ "clients_upstreams_desc": "Serveurs DNS en amont, un par ligne",
+ "clients_use_dns_cache": "Utiliser le cache DNS",
+ "clients_use_global_settings": "Utiliser les paramètres généraux",
+ "clients_use_global_settings_desc": "Hériter des paramètres de protection de votre compte AdGuard Home",
+ "close": "Fermer",
+ "close_result": "Fermer le résultat",
+ "confirm": "Confirmer",
+ "confirm_client_block_title": "Bloquer un client ?",
+ "confirm_client_unblock_title": "Débloquer le client ?",
+ "copied": "Copié dans le Presse-papiers",
+ "copy": "Copier",
+ "copy_to_clipboard": "Copier dans le clipboard",
+ "custom_filtering_rules": "Règles de filtrage d'utilisateur",
+ "dark_theme": "Thème sombre",
+ "dashboard": "Tableau de bord",
+ "delete_btn": "Supprimer",
+ "delete_table_action": "Supprimer",
+ "device_type": "Type d'appareil",
+ "dhcp_add_static_lease": "Ajoutez un bail statique",
+ "dhcp_config_saved": "Configuration du serveur DHCP sauvegardée",
+ "dhcp_edit_static_lease": "Modifier le bail statique",
+ "dhcp_form_gateway_input": "IP de la passerelle",
+ "dhcp_form_subnet_input": "Masque de sous-réseau",
+ "dhcp_leases_not_found": "Aucun bail DHCP trouvé",
+ "dhcp_leases_title": "Locations des serveurs DHCP",
+ "dhcp_new_static_lease": "Nouveau bail statique",
+ "dhcp_reset_leases": "Réinitialiser tous les baux",
+ "dhcp_reset_leases_success": "Les baux DHCP ont été réinitialisés avec succès",
+ "dhcp_settings": "Paramètres DHCP",
+ "dhcp_static_leases_not_found": "Aucun bail statique DHCP trouvé",
+ "dhcp_table_hostname": "Nom d'hôte",
+ "dhcp_table_ip_address": "Adresse IP",
+ "disable": "Désactiver",
+ "disable_protection_btn": "Désactiver la protection",
+ "disabled_dhcp": "Serveur DHCP désactivé",
+ "disallow_client_confirm_allowlist_note": "Cette action exclura également la règle %ip% de la liste des clients autorisés.",
+ "disallow_this_client": "Interdire ce client",
+ "dns_access_settings_title": "Paramètres d'accès",
+ "dns_allowed_clients": "Clients autorisés",
+ "dns_allowed_clients_label": "Clients autorisés, un par ligne",
+ "dns_allowed_clients_placeholder": "Saisir les clients autorisés",
+ "dns_blocking_mode": "Mode du blocage",
+ "dns_blocking_mode_custom_ip": "Adresse IP personnalisée",
+ "dns_blocking_mode_default": "Défaut",
+ "dns_blocking_mode_ipv4_label": "Blocage IPv4",
+ "dns_blocking_mode_ipv6_label": "Blocage IPv6",
+ "dns_blocking_mode_nxdomain": "NXDOMAIN",
+ "dns_blocking_mode_refused": "REFUSED",
+ "dns_blocking_mode_title": "Mode du blocage",
+ "dns_bootstrap_dns_label": "Serveurs DNS de démarrage, un par ligne",
+ "dns_bootstrap_dns_placeholder": "Adresses IP",
+ "dns_bootstrap_dns_title": "Serveurs DNS d'amorçage",
+ "dns_bootstrap_servers": "Serveurs DNS d'amorçage",
+ "dns_cache_config": "Configuration du cache DNS",
+ "dns_cache_configuration_saved_toast": "Configuration du cache DNS enregistrée",
+ "dns_cache_size": "Taille du cache",
+ "dns_cache_size_label": "Taille du cache DNS, en bytes",
+ "dns_cache_size_title": "Taille du cache",
+ "dns_clear_cache": "Vider le cache",
+ "dns_clear_cache_cancel": "Annuler",
+ "dns_clear_cache_confirm": "Oui, effacer",
+ "dns_clear_cache_yes": "Oui, effacer",
+ "dns_config": "Configuration du serveur DNS",
+ "dns_disallowed_clients": "Clients non autorisés",
+ "dns_disallowed_clients_label": "Clients refusés, un par ligne",
+ "dns_disallowed_clients_title": "Clients non autorisés",
+ "dns_disallowed_domains": "Domaines interdits",
+ "dns_disallowed_domains_label": "Domaines interdits, un par ligne",
+ "dns_disallowed_domains_title": "Domaines interdits",
+ "dns_edns_custom_placeholder": "Saisissez l'adresse IP",
+ "dns_fallback_dns_title": "Serveurs DNS de repli",
+ "dns_fallback_servers": "Serveurs DNS de repli",
+ "dns_optimistic_caching": "Caching optimiste",
+ "dns_over_https": "DNS-over-HTTPS",
+ "dns_over_quic": "DNS-over-QUIC",
+ "dns_over_tls": "DNS-over-TLS",
+ "dns_override_max_ttl": "Remplacer le TTL maximum",
+ "dns_override_max_ttl_title": "Remplacer le TTL maximum",
+ "dns_override_min_ttl": "Remplacer le TTL minimum",
+ "dns_override_min_ttl_title": "Remplacer le TTL minimum",
+ "dns_privacy": "Confidentialité DNS",
+ "dns_private_reverse_servers_label": "Serveurs DNS inversés privés, un par ligne",
+ "dns_private_reverse_servers_title": "Serveurs DNS privés inverses",
+ "dns_queries": "Requêtes DNS",
+ "dns_query": "Requêtes DNS",
+ "dns_rate_limit": "Limite de taux",
+ "dns_rate_limit_allowlist_placeholder": "Adresses IP",
+ "dns_rate_limit_placeholder": "Entrez la limite de taux",
+ "dns_rate_limit_title": "Limite de taux",
+ "dns_rewrites": "Réécritures DNS",
+ "dns_server_addresses": "Adresses des serveurs",
+ "dns_server_addresses_label": "Adresses du serveur, une par ligne",
+ "dns_server_addresses_title": "Adresses des serveurs",
+ "dns_server_config_title": "Configuration du serveur DNS",
+ "dns_server_configuration_saved_toast": "Configuration du serveur DNS enregistrée",
+ "dns_settings": "DNS",
+ "dns_test_upstreams": "Tester les upstreams",
+ "dns_upstream_servers_title": "Serveurs DNS upstream",
+ "dns_upstream_timeout": "Délai d'attente en amont",
+ "dns_upstream_timeout_title": "Délai d'attente en amont",
+ "dns_upstream_validation_invalid": "Format invalide",
+ "domain": "Domaine",
+ "domain_or_client": "Domaine ou client",
+ "download_mobileconfig": "Télécharger le fichier de configuration",
+ "dynamic_dhcp_leases_not_found": "Aucun bail DHCP dynamique n'a été trouvé",
+ "edit_table_action": "Éditer",
+ "enable": "Activer",
+ "enabled_dhcp": "Serveur DHCP activé",
+ "encryption_certificate_has_issues": "Le certificat présente des problèmes",
+ "encryption_certificate_path": "Emplacement du certificat",
+ "encryption_certificates": "Certificats",
+ "encryption_certificates_desc": "Pour utiliser le chiffrement, vous devez fournir une chaîne de certificats SSL valide pour votre domaine. Vous pouvez obtenir un certificat SSL gratuit sur letsencrypt.org ou en acheter un auprès d'une autorité de certification de confiance",
+ "encryption_certificates_input": "Copiez/coller vos certificats encodés PEM ici.",
+ "encryption_certificates_source_content": "Coller les contenus des certificats",
+ "encryption_certificates_source_path": "Définir un emplacement du fichier des certificats",
+ "encryption_chain_invalid": "Chaîne de certificat invalide",
+ "encryption_chain_valid": "Chaîne de certificat valide.",
+ "encryption_config_saved": "Configuration de chiffrement enregistrée",
+ "encryption_confirm_clear": "Réinitialiser les paramètres de chiffrement",
+ "encryption_confirm_clear_desc": "Tous les paramètres de chiffrement seront réinitialisés à leurs valeurs par défaut",
+ "encryption_disable_plain_dns": "Désactiver le DNS simple ?",
+ "encryption_doq": "Port DNS sur QUIC",
+ "encryption_doq_tooltip": "Ce port sert à faire fonctionner un serveur DNS-over-QUIC",
+ "encryption_dot": "Port DNS-over-TLS",
+ "encryption_dot_tooltip": "Ce port sert à faire fonctionner un serveur DNS-over-TLS",
+ "encryption_force_redirect": "Rediriger automatiquement de HTTP vers HTTPS",
+ "encryption_hostnames": "Noms d'hôtes : %value%",
+ "encryption_https": "Port HTTPS",
+ "encryption_issuer": "Émetteur : %value%",
+ "encryption_key": "Clé privée",
+ "encryption_key_input": "Copiez/coller votre clé privée PEM encodée pour votre certificat ici.",
+ "encryption_key_invalid": "La clé privée n'est pas valide",
+ "encryption_key_source_content": "Coller les contenus de la clef privée",
+ "encryption_key_source_path": "Définir un fichier de clé privée",
+ "encryption_key_valid": "La clé privée est valide",
+ "encryption_plain_dns": "DNS brut",
+ "encryption_plain_dns_error": "Pour désactiver le DNS simple, activez au moins un protocole DNS crypté",
+ "encryption_private_key_path": "Emplacement de la clef privée",
+ "encryption_server": "Nom du serveur",
+ "encryption_server_enter": "Entrez votre nom de domaine",
+ "encryption_server_tooltip_1": "Si cette option est activée, AdGuard Home détectera les ClientID, répondra aux requêtes DDR et effectuera des validations supplémentaires de la connexion. Si elle n'est pas activée, ces fonctionnalités sont désactivées",
+ "encryption_server_tooltip_2": "Doit correspondre à un nom DNS figurant dans le certificat",
+ "encryption_subject": "Objet : %value%",
+ "encryption_title": "Chiffrement",
+ "enter_cache_size": "Entrer la taille du cache (octets)",
+ "enter_ip_address_placeholder": "Saisissez l'adresse IP",
+ "error": "Erreur",
+ "fallback_dns": "Serveurs DNS de repli",
+ "filter_added_successfully": "Liste de blocage ajoutée : %value% ",
+ "filter_added_successfully_allowlist": "Liste d’autorisation ajoutée : %value% ",
+ "filter_added_successfully_more": "Listes de blocage ajoutées : %value% et %more% plus",
+ "filter_category_general": "General",
+ "filter_category_general_desc": "Listes qui bloquent le suivi et la publicité sur la plupart des appareils",
+ "filter_category_other": "Autres",
+ "filter_category_other_desc": "Autres listes noires",
+ "filter_category_regional": "Régional",
+ "filter_category_regional_desc": "Listes axées sur les annonces régionales et les serveurs de suivi",
+ "filter_category_security": "Sécurité",
+ "filter_category_security_desc": "Listes créées exprès pour bloquer les logiciels malveillants, des domaines hameçonneurs ou frauduleux",
+ "filter_removed_successfully": "Liste de blocage supprimée : %value% ",
+ "filter_removed_successfully_allowlist": "Liste d'autorisation supprimée : %value% ",
+ "filters": "Filtres",
+ "forgot_password": "Mot de passe oublié ?",
+ "forgot_password_list_item_2": "Arrêter AdGuard Home.",
+ "forgot_password_list_item_4": "Démarrer AdGuard Home.",
+ "form_enter_blocked_response_ttl": "Saisir le TTL de la réponse bloquée (secondes)",
+ "form_error_answer_format": "Format de résultat invalide",
+ "form_error_client_id_format": "L'ID du client ne doit contenir que des chiffres, des lettres minuscules et des traits d'union",
+ "form_error_domain_format": "Format de domaine invalide",
+ "form_error_equal": "Ne doit pas être égal",
+ "form_error_format": "Format invalide",
+ "form_error_gateway_ip": "Le bail ne peut pas avoir d'adresse IP de la passerelle",
+ "form_error_ip4_format": "Adresse IPv4 invalide",
+ "form_error_ip6_format": "Adresse IPv6 invalide",
+ "form_error_ip_format": "Adresse e-mail non valide",
+ "form_error_mac_format": "Adresse MAC invalide",
+ "form_error_password": "Mots de passe différents",
+ "form_error_port": "Saisissez un numéro de port valide",
+ "form_error_port_range": "Saisissez une valeur de port entre 80 et 65535",
+ "form_error_port_unsafe": "Port non fiable",
+ "form_error_server_name": "Nom de serveur invalide",
+ "form_error_url_format": "Saisissez une URL ou un chemin d\\'accès au fichier valide",
+ "friday": "Vendredi",
+ "gateway_or_subnet_invalid": "Masque de sous-réseau invalide.",
+ "general_statistics": "Statistiques générales",
+ "ignore_domains_title": "Domaines ignorés",
+ "inactivity_schedule_add": "Ajouter un horaire",
+ "inactivity_schedule_all_day": "Toute la journée",
+ "inactivity_schedule_edit": "Modifier l'horaire",
+ "inactivity_schedule_replace_desc": "Cet horaire remplacera tout autre planning existant pour le même jour de la semaine. Chaque jour ne peut avoir qu'une seule période d'inactivité",
+ "inactivity_schedule_timezone": "Fuseau horaire",
+ "install_auth_confirm": "Confirmer le mot de passe",
+ "install_auth_password": "Mot de passe",
+ "install_auth_password_enter": "Entrer le mot de passe",
+ "install_auth_username": "Nom d'utilisateur",
+ "install_auth_username_enter": "Saisir un nom d'utilisateur",
+ "install_saved": "Enregistré avec succès",
+ "install_settings_all_interfaces": "Toutes les interfaces",
+ "install_settings_port": "Port",
+ "install_static_error": "AdGuard Home ne peut pas le configurer automatiquement pour cette interface réseau. Veuillez rechercher une instruction sur la façon de procéder manuellement.",
+ "install_static_ok": "Bonne nouvelle ! L’adresse IP statique est déjà configurée",
+ "install_step": "Étape",
+ "install_submit_title": "Félicitations !",
+ "instructions": "Instructions",
+ "ip_address": "Adresse IP",
+ "ip_addresses_placeholder": "Saisissez les adresses IP, une par ligne",
+ "known_tracker": "Traqueur connu",
+ "language": "Langue",
+ "last_updated_label": "Dernière mise à jour",
+ "light_theme": "Thème clair",
+ "login": "Connexion",
+ "logout": "Déconnexion",
+ "make_static": "Rendre statique",
+ "milliseconds_abbreviation": "ms",
+ "monday": "Lundi",
+ "name": "Nom",
+ "name_label": "Nom",
+ "name_table_header": "Nom",
+ "name_tooltip": "Nom",
+ "network_interface": "Interface réseau",
+ "next": "Suivant",
+ "no": "Non",
+ "none_text": "Aucune",
+ "nothing_found": "Rien n'a été trouvé",
+ "notify_undo": "Annuler",
+ "notify_updated": "Mis à jour",
+ "open_dashboard": "Ouvrir le tableau de bord",
+ "parental_control": "Contrôle Parental",
+ "parental_group_cdn": "CDN",
+ "parental_group_development": "Développement de logiciels",
+ "parental_group_gaming": "Jeux vidéo",
+ "parental_group_hosting": "Hébergement Web",
+ "parental_group_privacy": "Outils de confidentialité",
+ "parental_group_shopping": "Shopping",
+ "parental_group_social": "Réseaux sociaux",
+ "parental_group_social_network": "Réseaux sociaux",
+ "parental_group_software": "Développement de logiciels",
+ "parental_group_streaming": "Streaming",
+ "password_label": "Mot de passe",
+ "password_placeholder": "Entrer le mot de passe",
+ "pause_for_hour": "Suspendre pour %count% heure",
+ "pause_until_tomorrow": "Mettre en pause jusqu'à demain, %time%",
+ "plain_dns": "DNS brut",
+ "privacy_policy": "Politique de confidentialité",
+ "protection": "Protection",
+ "protocol": "Protocole",
+ "queries": "Requêtes",
+ "query_details": "Détails de la requête",
+ "query_log_all_reasons": "Toutes les raisons",
+ "query_log_all_statuses": "Tous les états",
+ "query_log_allowed": "En Liste blanche",
+ "query_log_blocked": "Bloqué",
+ "query_log_blocked_by_filter": "Bloqué par le filtre",
+ "query_log_blocked_by_parental_control": "Bloqué par le contrôle parental",
+ "query_log_blocked_services": "Services bloqués",
+ "query_log_blocked_threats": "Menaces bloquées",
+ "query_log_custom_filtering_rules": "Règles de filtrage d'utilisateur",
+ "query_log_detail_address": "Adresse : %value% ",
+ "query_log_detail_category": "Catégorie : %value% ",
+ "query_log_detail_country": "Pays : %value% ",
+ "query_log_detail_date": "Date : %value% ",
+ "query_log_detail_dns_server": "Serveur DNS : %value% ",
+ "query_log_detail_domain": "Domaine : %value% ",
+ "query_log_detail_ecs": "ECS : %value% ",
+ "query_log_detail_elapsed": "Temps écoulé : %value% ",
+ "query_log_detail_name": "Nom : %value% ",
+ "query_log_detail_network": "Réseau : %value% ",
+ "query_log_detail_original_response": "Réponse originale :",
+ "query_log_detail_protocol": "Protocole : %value% ",
+ "query_log_detail_reason": "Raison :",
+ "query_log_detail_response": "Réponse :",
+ "query_log_detail_response_code": "Code de réponse : %value% ",
+ "query_log_detail_rules": "Règle(s) :",
+ "query_log_detail_served_from_cache": "Servi depuis le cache : %value% ",
+ "query_log_detail_service_name": "Nom du service : %value% ",
+ "query_log_detail_source": "Source : %value% ",
+ "query_log_detail_status": "État : %value% ",
+ "query_log_detail_time": "Heure : %value% ",
+ "query_log_detail_type": "Type : %value% ",
+ "query_log_nothing_available": "Rien n'a été trouvé",
+ "query_log_processed": "Traités",
+ "query_log_retention": "Rotation des journaux",
+ "query_log_rewritten": "Réécrit",
+ "query_log_safe_search": "Recherche sécurisée",
+ "query_log_strict_search": "Utilisez les doubles guillemets pour une recherche stricte",
+ "rate_limit_subnet_len_ipv4_error": "La longueur du préfixe du sous-réseau IPv4 doit être entre 0 et 32",
+ "rate_limit_subnet_len_ipv6_error": "La longueur du préfixe du sous-réseau IPv6 doit être entre 0 et 128",
+ "reason_table_header": "Raison",
+ "refresh_btn": "Actualiser",
+ "refresh_statics": "Actualiser les statistiques",
+ "release_notes": "Notes de mise à jour",
+ "remove": "Supprimer",
+ "remove_tag": "Supprimer %value%",
+ "report_an_issue": "Signaler un problème",
+ "request_table_header": "Requête",
+ "requests_table_header": "Requêtes",
+ "reset": "Réinitialiser",
+ "reset_settings": "Réinitialiser les paramètres",
+ "reset_settings_confirm": "Oui, réinitialiser",
+ "response_details": "Détails de la réponse",
+ "response_time": "Temps de réponse",
+ "result": "Résultat",
+ "resume_protection_timer": "La protection reprendra dans %time%",
+ "rewrite_add": "Ajouter une réécriture DNS",
+ "rewrite_edit": "Modifier la réécriture DNS",
+ "rewrite_hosts_applied": "Réécrit par la règle du fichier hosts",
+ "rewrite_settings_updated": "Les paramètres de réécriture DNS ont été mis à jour avec succès",
+ "rewrite_updated": "Réécriture DNS mise à jour",
+ "rewrites_answer_input_placeholder": "Saisissez une adresse IP, un enregistrement CNAME, un enregistrement A ou un enregistrement AAAA",
+ "rewrites_remove_desc": "La réécriture DNS pour %value% sera supprimée",
+ "rewrites_remove_title": "Supprimer la réécriture DNS ?",
+ "rewritten": "Réécrit",
+ "rows_per_page": "Nombre de lignes par page %value%",
+ "rules_label": "Règles",
+ "safe_browsing": "Navigation sécurisée",
+ "safe_search": "Recherche sécurisée",
+ "saturday": "Samedi",
+ "save": "Sauvegarder",
+ "save_btn": "Sauvegarder",
+ "search_placeholder": "Rechercher",
+ "server_config_blocking_mode": "Mode du blocage",
+ "server_config_blocking_mode_ipv4": "Blocage IPv4",
+ "server_config_blocking_mode_ipv6": "Blocage IPv6",
+ "server_config_custom_ip": "Adresse IP personnalisée",
+ "server_config_default": "Défaut",
+ "server_config_disable_ipv6": "Désactiver la résolution des adresses IPv6",
+ "server_config_dnssec_enable": "Activer DNSSEC",
+ "server_config_edns_enable": "Activer le sous-réseau du client EDNS",
+ "server_config_nxdomain": "NXDOMAIN",
+ "server_config_rate_limit_placeholder": "Entrez la limite de taux",
+ "server_config_rate_limit_whitelist_faq": "Adresses IP exclues de la limitation du débit",
+ "server_config_refused": "REFUSED",
+ "server_config_subnet_len_ipv4": "Longueur du préfixe de sous-réseau pour les adresses IPv4",
+ "server_config_subnet_len_ipv4_faq": "Longueur du préfixe de sous-réseau pour les adresses IPv4 utilisé pour la limitation de vitesse. La valeur par défaut est 24",
+ "server_config_subnet_len_ipv6": "Longueur du préfixe de sous-réseau pour les adresses IPv6",
+ "server_config_subnet_len_ipv6_faq": "Longueur du préfixe de sous-réseau pour les adresses IPv6 utilisé pour la limitation de débit. La valeur par défaut est 56",
+ "set_static_ip": "Définir une adresse IP statique",
+ "settings": "Paramètres",
+ "settings_browsing_security": "Navigation sécurisée",
+ "settings_confirm_clear_statistics": "Effacer les statistiques?",
+ "settings_confirm_decrease_log_rotation_interval_desc": "Cette opération supprimera tous les journaux plus anciens que %value%",
+ "settings_custom": "Personnalisé",
+ "settings_general_short": "General",
+ "settings_global": "Général",
+ "settings_notify_changes_saved": "Modifications enregistrées",
+ "settings_notify_statistics_cleared": "Statistiques effacées",
+ "settings_parental_control": "Contrôle parental",
+ "settings_rule_syntax": "Syntaxe des règles",
+ "settings_safe_search": "Recherche sécurisée",
+ "settings_statistics": "Statistiques",
+ "settings_statistics_clear": "Réinitialiser les statistiques",
+ "settings_statistics_retention": "Maintien des statistiques",
+ "settings_statistics_retention_hours": "Durée de conservation des statistiques, en heures",
+ "settings_yes_clear": "Oui, effacer",
+ "settings_yes_decrease": "Oui, diminuer",
+ "setup_devices_android_title": "Android",
+ "setup_devices_dns_android_list_1": "Android 9 prend en charge nativement DNS-over-TLS . Pour le configurer, rendez-vous dans Paramètres → Réseau et Internet → Avancé → DNS privé et saisissez votre nom de domaine.",
+ "setup_devices_dns_list_devices": "Voici une liste des logiciels que vous pouvez utiliser.",
+ "setup_devices_dns_macos_desc": "Sur un appareil fonctionnant sous iOS 14 ou macOS Big Sur, vous pouvez télécharger un fichier mobileconfig spécial qui ajoute des serveurs DNS-over-HTTPS ou DNS-over-TLS aux paramètres DNS.",
+ "setup_devices_ios_title": "iOS",
+ "setup_devices_router_desc_2": "Certains routeurs ne permettent pas de configurer un serveur DNS personnalisé. Dans ce cas, vous pouvez essayer de configurer AdGuard Home en tant que serveur DHCP . Si cela ne fonctionne pas, veuillez consulter le manuel de votre routeur pour savoir comment personnaliser les serveurs DNS sur votre modèle spécifique.",
+ "setup_devices_router_list_2": "Pour trouver ces paramètres, repérez la rubrique « DNS » à côté d'un champ permettant de saisir deux ou trois séries de chiffres, chacune divisée en quatre groupes de un à trois chiffres.",
+ "setup_devices_router_title": "Routeur",
+ "setup_devices_windows_list_6": "Saisissez les adresses de votre serveur AdGuard Home.",
+ "setup_devices_windows_title": "Windows",
+ "setup_dns_desc": "Configurez votre routeur ou vos autres appareils pour qu'ils utilisent les adresses de serveurs DNS suivantes :",
+ "setup_dns_privacy_ioc_mac": "Configuration sur iOS et macOS",
+ "setup_dns_privacy_other_title": "Autres implémentations",
+ "setup_dns_quote_title": "Attribuez une adresse IP statique au serveur DNS d'AdGuard Home sur votre réseau local",
+ "show_blocked_responses": "Bloqué",
+ "show_processed_responses": "Traités",
+ "source_label": "Source",
+ "source_tooltip": "Source",
+ "static_dhcp_leases_not_found": "Aucun bail DHCP statique n'a été trouvé",
+ "stats_adult": "Sites à contenu adulte bloqués",
+ "stats_query_domain": "Domaines les plus recherchés",
+ "status_table_header": "État",
+ "sunday": "Dimanche",
+ "system_host_files": "Fichier d'hôtes système",
+ "table_client": "Client",
+ "tags_title": "Mots clés",
+ "test_upstreams": "Tester les upstreams",
+ "threats_blocked": "Menaces bloquées",
+ "thursday": "Jeudi",
+ "time_table_header": "Temps",
+ "top_blocked_domains": "Les domaines les plus fréquemment bloqués",
+ "top_clients": "Meilleurs clients",
+ "top_upstreams": "Top amonts",
+ "total": "Total",
+ "try_again": "Essayer à nouveau",
+ "tuesday": "Mardi",
+ "type_value": "Type : %value%",
+ "unavailable_dhcp": "Le DHCP n’est pas disponible",
+ "unavailable_dhcp_desc": "AdGuard Home ne peut pas exécuter un serveur DHCP sur votre système d’exploitation",
+ "unblock": "Débloquer",
+ "unblock_client": "Débloquer le client",
+ "unknown_filter": "Filtre inconnu %filterId%",
+ "update_button": "Mettre à jour",
+ "update_filters_custom_hours": "Intervalle personnalisé, en heures",
+ "update_filters_disable": "Désactiver",
+ "update_filters_interval_custom": "Personnalisé",
+ "update_filters_interval_daily": "Tous les jours",
+ "update_filters_interval_hourly": "Chaque heure",
+ "update_filters_interval_weekly": "Chaque semaine",
+ "update_filters_title": "Mettre à jour automatiquement les filtres",
+ "updated_upstream_dns_toast": "Serveurs en amont enregistrés",
+ "updates_checked": "Une nouvelle version de AdGuard Home est disponible",
+ "updates_version_equal": "AdGuard Home est à jour",
+ "upstream": "Amont",
+ "upstream_bootstrap_dns_faq": "Adresses IP des serveurs DNS utilisés pour résoudre les adresses IP des résolveurs en amont DoH/DoT. Les commentaires sont désactivés",
+ "upstream_bootstrap_dns_title": "Serveurs DNS de démarrage, un par ligne",
+ "upstream_dns": "Serveurs DNS upstream",
+ "upstream_dns_addresses": "Adresses du serveur, une par ligne",
+ "upstream_dns_addresses_faq": "En savoir plus sur la configuration des serveurs DNS en amont. Voici une liste des fournisseurs DNS connus parmi lesquels vous pouvez choisir.",
+ "upstream_dns_configured_in_file": "Configuré dans %path%",
+ "upstream_dns_fastest_addr": "Adresse IP la plus rapide",
+ "upstream_dns_parallel_requests": "Requêtes en parallèle",
+ "upstream_dns_servers_title": "Serveurs DNS upstream",
+ "upstream_examples_title": "Exemples",
+ "upstream_private_ptr_resolvers_desc": "Traiter les requêtes PTR, SOA et NS pour les domaines ARPA contenant des adresses IP privées via des serveurs en amont privés, le protocole DHCP, le fichier /etc/hosts, etc. Si cette option est désactivée, AdGuard Home répondra à toutes ces requêtes par un code NXDOMAIN",
+ "upstream_private_ptr_resolvers_title": "Utiliser des résolveurs DNS inversés privés",
+ "upstream_ptr": "Serveurs DNS inversés privés, un par ligne",
+ "upstream_ptr_faq_1": "AdGuard Home utilise ces serveurs DNS pour les requêtes PTR, SOA et NS privées. Les requêtes privées proviennent de clients disposant d'une adresse IP privée et concernent un domaine ARPA contenant un sous-réseau issu d'une plage d'adresses IP privées, tel que %value%",
+ "upstream_ptr_faq_2": "Si ce champ est vide, les résolveurs DNS par défaut de votre système d'exploitation seront utilisés, à l'exception des adresses IP d'AdGuard Home",
+ "upstream_ptr_faq_3": "Par défaut, AdGuard Home utilise les résolveurs DNS inversés suivants : %value_1% et %value_2%",
+ "upstreams": "En amont",
+ "url_label": "URL",
+ "use_saved_key": "Utiliser une clé privée existante",
+ "user_rules": "Règles d'utilisateur",
+ "user_rules_add_to_allowlist": "Ajouter à la liste blanche",
+ "user_rules_allow_service": "Autoriser le service",
+ "user_rules_browsing_security_disabled": "La Navigation sécurisée est désactivée",
+ "user_rules_check_button": "Vérifier",
+ "user_rules_check_client_label": "Identifiant du client (nom, ClientID ou adresse IP)",
+ "user_rules_check_client_placeholder": "Saisissez l'identifiant du client",
+ "user_rules_check_dns_record_type_label": "Type d’enregistrement DNS",
+ "user_rules_check_hostname_label": "Nom d'hôte ou nom de domaine",
+ "user_rules_check_hostname_placeholder": "Saisir la cible",
+ "user_rules_check_title": "Vérifier le filtrage de domaine globalement ou pour un client",
+ "user_rules_cname": "CNAME : %cname%",
+ "user_rules_disable_browsing_security": "Désactiver la sécurité de navigation",
+ "user_rules_disable_filter": "Désactiver le filtre",
+ "user_rules_disable_parental_control": "Désactiver le contrôle parental",
+ "user_rules_disable_safe_search": "Désactiver la recherche sécurisée",
+ "user_rules_dns_record_type_placeholder": "Sélectionnez le type d'enregistrement DNS",
+ "user_rules_dns_rewrite_removed": "Règle supprimée de la réécriture DNS",
+ "user_rules_domain": "Domaine : %value%",
+ "user_rules_domain_blocked": "Domaine bloqué",
+ "user_rules_domain_is_allowed": "Le domaine est autorisé",
+ "user_rules_domain_is_processed": "Le domaine est traité",
+ "user_rules_edit_dns_rewrite": "Modifier la réécriture DNS",
+ "user_rules_example_block": "||%domain% Bloquer l'accès à %domain% et à tous ses sous-domaines",
+ "user_rules_example_comment": "! Ceci est un commentaire Pour commenter, commencez la ligne par ! ou #",
+ "user_rules_example_comment_2": "# Ceci est aussi un commentaire Pour commenter, commencez la ligne par ! ou #",
+ "user_rules_example_regex": "/REGEX/ Bloquer l'accès aux domaines correspondant à l'expression régulière spécifiée",
+ "user_rules_example_response": "127.0.0.1 %domain% Répondre avec 127.0.0.1 uniquement pour %domain% , et non pour ses sous-domaines",
+ "user_rules_example_unblock": "@@||%domain% Débloquer l'accès à %domain% et à tous ses sous-domaines",
+ "user_rules_filter_was_disabled": "%value% a été désactivé",
+ "user_rules_ip": "Adresses IP : %ip%",
+ "user_rules_learn_more": "Apprenez plus sur la création des listes d'hôtes personnalisées.",
+ "user_rules_no_rules_matched": "Résultat : Aucune règle ne correspond",
+ "user_rules_parental_control_disabled": "Contrôle parental désactivé",
+ "user_rules_placeholder": "Ceci est un espace réservé",
+ "user_rules_reason": "Motif : %reason%",
+ "user_rules_reason_allowed_by": "Autorisé par %source%",
+ "user_rules_reason_blocked_by": "Bloqué par %source%",
+ "user_rules_reason_filtered_by": "Filtré par %source%",
+ "user_rules_redirected_to": "Redirigé vers : %value%",
+ "user_rules_remove_dns_rewrite": "Supprimer la réécriture DNS",
+ "user_rules_rewrite_rule_is_applied": "Règle de réécriture appliquée",
+ "user_rules_rewritten_to": "Réécrit en : %value%",
+ "user_rules_rule": "Règle : %rule%",
+ "user_rules_rule_added_to_custom_filtering_rules": "Règle ajoutée aux règles de filtrage personnalisées",
+ "user_rules_rule_removed": "Règle supprimée des règles de filtrage personnalisées",
+ "user_rules_safe_search_disabled": "Recherche sécurisée désactivée",
+ "user_rules_service": "Service : %service%",
+ "user_rules_service_allowed": "Le service %value% a été autorisé",
+ "user_rules_source": "Source : %value%",
+ "user_rules_source_allowlist": "Liste d’autorisation : %value%",
+ "user_rules_source_blocklist": "Liste de blocage : %value%",
+ "user_rules_status": "État : %reason%",
+ "username_label": "Nom d'utilisateur",
+ "username_placeholder": "Saisir un nom d'utilisateur",
+ "validated_with_dnssec": "Validé avec DNSSEC",
+ "wednesday": "Mercredi",
+ "whois": "WHOIS",
+ "yes": "Oui",
+ "yes_disable": "Oui, désactiver",
+ "yes_reset": "Oui, réinitialiser"
+}
diff --git a/client_v2/src/__locales/hr.json b/client_v2/src/__locales/hr.json
new file mode 100644
index 00000000000..b3a02ce82dd
--- /dev/null
+++ b/client_v2/src/__locales/hr.json
@@ -0,0 +1,390 @@
+{
+ "access_settings_title": "Postavke pristupa",
+ "actions_table_header": "Radnje",
+ "add": "DODAJ",
+ "add_allowlist": "Dodaj popis dopuštenih",
+ "add_blocklist": "Dodaj popis nedopuštenih",
+ "add_persistent_client": "Dodaj u spremljene klijente",
+ "add_to_allowlist": "Dodaj na popis dopuštenih",
+ "ads_blocked": "Oglasa blokirano",
+ "ads_blocked_card": "Oglasa blokirano",
+ "adult_websites_blocked_tooltip": "Broj blokiranih stranica s sadržajem za odrasle",
+ "all_lists_up_to_date_toast": "Svi popisi su ažurirani",
+ "allowed": "Na popisu dopuštenih",
+ "allowlist_add": "Dodaj popis dopuštenih",
+ "allowlist_edit": "Uredi popis dopuštenih",
+ "apply": "Primijeni",
+ "autofix_warning_result": "Kao rezultat toga, sve DNS zahtjeve iz vašeg sustava će AdGuard Home obraditi prema zadanim postavkama.",
+ "autofix_warning_text": "Ako pritisnete \"Popravi\", AdGuard Home će postaviti vaš sustav da koristi AdGuardHome DNS poslužitelj.",
+ "average_time_processing": "Prosječno vrijeme obrade",
+ "average_upstream_response_time": "Prosječno vrijeme odziva upstream poslužitelja",
+ "back": "Natrag",
+ "block": "Blokiraj",
+ "block_for_this_client_only": "Blokiraj samo za ovog klijenta",
+ "blocked": "Blokirano",
+ "blocked_by_cname_or_ip": "Blokirao CNAME ili IP",
+ "blocked_service": "Blokirane usluge",
+ "blocked_services": "Blokirane usluge",
+ "blocked_threats": "Blokirane prijetnje",
+ "blocked_threats_chart": "Blokirane prijetnje",
+ "blocklist_add_manual": "Ručno dodavanje",
+ "blocklist_edit": "Uredi popis nedopuštenih",
+ "blocklist_homepage": "Početna stranica",
+ "blocklist_url_file_path": "URL ili putanja datoteke",
+ "bootstrap_dns": "Bootstrap DNS poslužitelji",
+ "browse": "Pregledaj",
+ "cache_cleared": "DNS predmemorija je uspješno izbrisana",
+ "cache_config_clear": "Očisti predmemoriju",
+ "cache_config_optimistic": "Optimistično predmemoriranje",
+ "cache_config_ttl_validation": "Minimalno nadjačavanje TTL-a predmemorije mora biti manje ili jednako maksimalnom",
+ "cache_enabled": "Omogući predmemoriju",
+ "cancel": "Poništi",
+ "cancel_btn": "Poništi",
+ "category_tooltip": "Kategorija",
+ "changes_saved_success": "Promjene su spremljene",
+ "check_dhcp_servers": "Provjera DHCP poslužitelja",
+ "check_not_found": "Nije pronađeno na vašoj listi filtara",
+ "check_updates_btn": "Provjeri ažuriranja",
+ "clear_btn": "Očisti",
+ "client_add": "Dodaj klijenta",
+ "client_details": "Detalji o klijentu",
+ "client_id": "ClientID",
+ "client_id_placeholder": "Unesite ClientID",
+ "client_identifier": "Identifikator",
+ "client_settings": "Postavke klijenta",
+ "client_table_header": "Klijent",
+ "clients_add_identifier": "Dodaj identifikator",
+ "clients_add_placeholder": "Unesite naziv klijenta",
+ "clients_dns_cache_size": "Veličina DNS predmemorije, u bajtovima",
+ "clients_not_found": "Nema pronađenih klijenata",
+ "clients_protection": "Zaštita",
+ "clients_tags": "Oznake",
+ "clients_use_global_settings": "Koristi globalne postavke",
+ "close": "Zatvori",
+ "confirm": "POTVRDI",
+ "copied": "Kopirano u međuspremnik",
+ "copy": "Kopiraj",
+ "copy_to_clipboard": "Kopirati u međuspremnikq",
+ "custom_filtering_rules": "Prilagođena pravila filtriranja",
+ "dark_theme": "Tamna tema",
+ "dashboard": "Upravljačka ploča",
+ "delete_btn": "Ukloni",
+ "delete_table_action": "Ukloni",
+ "dhcp_add_static_lease": "Dodaj static lease",
+ "dhcp_config_saved": "Postavke DHCP poslužitelja su uspješno spremljene",
+ "dhcp_edit_static_lease": "Uredi statični lease",
+ "dhcp_form_gateway_input": "Gateway IP",
+ "dhcp_form_subnet_input": "Subnet maskiranje",
+ "dhcp_leases_not_found": "Nisu pronađeni DHCP leases",
+ "dhcp_leases_title": "DHCP leases",
+ "dhcp_new_static_lease": "Novi static lease",
+ "dhcp_reset_leases": "Ponovno postavljanje svih najmova",
+ "dhcp_reset_leases_success": "DHCP najmovi uspješno se resetiraju",
+ "dhcp_settings": "DHCP postavke",
+ "dhcp_static_leases_not_found": "Nisu pronađeni statični DHCP leases",
+ "dhcp_table_hostname": "Naziv računala",
+ "dhcp_table_ip_address": "IP adresa",
+ "disable": "Onemogući",
+ "disable_protection_btn": "Onemogući zaštitu",
+ "disabled_dhcp": "DHCP poslužitelj je onemogućen",
+ "disallow_this_client": "Onemogući ovog klijenta",
+ "dns_access_settings_title": "Postavke pristupa",
+ "dns_allowed_clients": "Dopušteni klijenti",
+ "dns_blocking_mode": "Način blokiranja",
+ "dns_blocking_mode_custom_ip": "Prilagođena IP adresa",
+ "dns_blocking_mode_default": "Zadano",
+ "dns_blocking_mode_ipv4_label": "Blokiranje IPv4",
+ "dns_blocking_mode_ipv6_label": "Blokiranje IPv6",
+ "dns_blocking_mode_nxdomain": "NXDOMAIN",
+ "dns_blocking_mode_refused": "REFUSED",
+ "dns_blocking_mode_title": "Način blokiranja",
+ "dns_bootstrap_dns_placeholder": "IP adrese",
+ "dns_bootstrap_dns_title": "Bootstrap DNS poslužitelji",
+ "dns_bootstrap_servers": "Bootstrap DNS poslužitelji",
+ "dns_cache_config": "DNS predmemorija poslužitelja",
+ "dns_cache_size": "Veličina predmemorije",
+ "dns_cache_size_label": "Veličina DNS predmemorije, u bajtovima",
+ "dns_cache_size_title": "Veličina predmemorije",
+ "dns_clear_cache": "Očisti predmemoriju",
+ "dns_clear_cache_cancel": "Poništi",
+ "dns_clear_cache_confirm": "Da",
+ "dns_clear_cache_yes": "Da",
+ "dns_config": "DNS postavke poslužitelja",
+ "dns_disallowed_clients": "Nedopušteni klijenti",
+ "dns_disallowed_clients_title": "Nedopušteni klijenti",
+ "dns_disallowed_domains": "Nedopuštene domene",
+ "dns_disallowed_domains_title": "Nedopuštene domene",
+ "dns_edns_custom_placeholder": "Unesite IP adresu",
+ "dns_fallback_dns_title": "Rezervni DNS poslužitelji",
+ "dns_fallback_servers": "Rezervni DNS poslužitelji",
+ "dns_optimistic_caching": "Optimistično predmemoriranje",
+ "dns_over_https": "DNS-over-HTTPS",
+ "dns_over_quic": "DNS-over-QUIC",
+ "dns_over_tls": "DNS-over-TLS",
+ "dns_override_max_ttl": "Nadjačaj maksimum TTL-a",
+ "dns_override_max_ttl_title": "Nadjačaj maksimum TTL-a",
+ "dns_override_min_ttl": "Nadjačaj minimum TTL-a",
+ "dns_override_min_ttl_title": "Nadjačaj minimum TTL-a",
+ "dns_privacy": "DNS privatnost",
+ "dns_private_reverse_servers_title": "Privatni obrnuti DNS poslužitelji",
+ "dns_rate_limit": "Ograničenje",
+ "dns_rate_limit_allowlist_placeholder": "IP adrese",
+ "dns_rate_limit_placeholder": "Unesite ograničenje",
+ "dns_rate_limit_title": "Ograničenje",
+ "dns_rewrites": "DNS prijepisi",
+ "dns_server_addresses": "Adrese poslužitelja",
+ "dns_server_addresses_label": "Adrese poslužitelja, jedna po retku",
+ "dns_server_addresses_title": "Adrese poslužitelja",
+ "dns_server_config_title": "DNS postavke poslužitelja",
+ "dns_settings": "DNS",
+ "dns_test_upstreams": "Testiraj upstream-ove",
+ "dns_upstream_servers_title": "Upstream DNS poslužitelji",
+ "dns_upstream_timeout": "Vrijeme čekanja na odgovore od upstream poslužitelja",
+ "dns_upstream_timeout_title": "Vrijeme čekanja na odgovore od upstream poslužitelja",
+ "dns_upstream_validation_invalid": "Oblik nije valjan",
+ "domain": "Domena",
+ "domain_or_client": "Domena ili klijent",
+ "download_mobileconfig": "Preuzmite konfiguracijsku datoteku",
+ "edit_table_action": "Uredi",
+ "enable": "Omogući",
+ "enabled_dhcp": "DHCP poslužitelj je omogućen",
+ "encryption_certificate_path": "Putanja certifikata",
+ "encryption_certificates": "Certifikati",
+ "encryption_certificates_input": "Zalijepite svoje PEM-kodirane certifikate ovdje.",
+ "encryption_certificates_source_content": "Zalijepi sadržaj certifikata",
+ "encryption_certificates_source_path": "Dodajte datoteku certifikata",
+ "encryption_chain_invalid": "Lanac certifikata nije valjan",
+ "encryption_chain_valid": "Lanac certifikata je valjan",
+ "encryption_config_saved": "Konfiguracija šifriranja spremljena",
+ "encryption_doq": "DNS-over-QUIC port (eksperimentalno)",
+ "encryption_dot": "DNS-over-TLS port",
+ "encryption_https": "HTTPS port",
+ "encryption_key": "Privatni ključ",
+ "encryption_key_input": "Zalijepite svoj PEM-kodiran privatni ključ certifikata ovdje.",
+ "encryption_key_source_content": "Zalijepi sadržaj privatnog ključa",
+ "encryption_plain_dns": "Obični DNS",
+ "encryption_plain_dns_error": "Da biste onemogućili obični DNS, omogućite barem jedan kriptirani DNS protokol",
+ "encryption_private_key_path": "Putanja privatnog ključa",
+ "encryption_server": "Naziv poslužitelja",
+ "encryption_server_enter": "Unesite naziv domene",
+ "encryption_title": "Šifriranje",
+ "enter_cache_size": "Unesite veličinu predmemorije (u bajtovima)",
+ "enter_ip_address_placeholder": "Unesite IP adresu",
+ "error": "Pogreška",
+ "fallback_dns": "Rezervni DNS poslužitelji",
+ "filter_category_general": "Općenito",
+ "filter_category_general_desc": "Popisi koji blokiraju pratitelje i oglase na većini uređaja",
+ "filter_category_other": "Ostalo",
+ "filter_category_other_desc": "Ostali popisi nedopuštenih",
+ "filter_category_regional": "Regionalno",
+ "filter_category_regional_desc": "Popisi koji se fokusiraju na regionalne oglase i poslužitelje za praćenje",
+ "filter_category_security": "Sigurnost",
+ "filter_category_security_desc": "Popisi posebno dizajnirani za blokiranje zlonamjernih domena, domena za krađu identiteta i prijevare",
+ "filters": "Filtri",
+ "forgot_password": "Zaboravljena lozinka?",
+ "form_enter_blocked_response_ttl": "Unesite TTL blokiranog odgovora (sekunde)",
+ "form_error_client_id_format": "ID klijenta može sadržavati samo brojeve, mala slova i crtice",
+ "form_error_domain_format": "Nevažeći format domene",
+ "form_error_equal": "Ne bi trebalo biti jednako",
+ "form_error_format": "Oblik nije valjan",
+ "form_error_gateway_ip": "Najam ne može imati IP adresu pristupnika",
+ "form_error_ip4_format": "Nevažeća IPv4 adresa",
+ "form_error_ip6_format": "Nevažeći IPv6 adresa",
+ "form_error_ip_format": "Nepravilna IP adresa",
+ "form_error_mac_format": "Nevažeći MAC adresa",
+ "form_error_password": "Lozinka se ne podudara",
+ "form_error_port": "Unesite važeći broj porta",
+ "form_error_port_range": "Unesite broj porta od 80 do 65536",
+ "form_error_port_unsafe": "Nesigurna port",
+ "form_error_server_name": "Nevažeće ime poslužitelja",
+ "form_error_url_format": "Unesite valjani URL ili put datoteke",
+ "friday": "Petak",
+ "gateway_or_subnet_invalid": "Nevažeća podmrežna maska",
+ "general_statistics": "Opća statistika",
+ "ignore_domains_title": "Zanemarene domene",
+ "inactivity_schedule_add": "Dodaj raspored",
+ "inactivity_schedule_all_day": "Cijeli dan",
+ "inactivity_schedule_edit": "Uredi raspored",
+ "install_auth_confirm": "Potvrdi lozinku",
+ "install_auth_password": "Lozinka",
+ "install_auth_password_enter": "Unesite lozinku",
+ "install_auth_username": "Korisničko ime",
+ "install_auth_username_enter": "Unesite korisničko ime",
+ "install_saved": "Uspješno spremljeno",
+ "install_settings_all_interfaces": "Sva sučelja",
+ "install_settings_port": "Port",
+ "install_static_error": "AdGuard Home ne može je automatski postaviti za ovo mrežno sučelje. Molimo potražite upute kako to učiniti ručno.",
+ "install_static_ok": "Dobre vijesti! Statička IP adresa već je postavljena",
+ "install_step": "Korak",
+ "install_submit_title": "Čestitamo!",
+ "ip_address": "IP adresa",
+ "known_tracker": "Poznati pratitelj",
+ "language": "Jezik",
+ "light_theme": "Svijetla tema",
+ "login": "PRIJAVA",
+ "logout": "Odjava",
+ "make_static": "Učini statičnim",
+ "milliseconds_abbreviation": "ms",
+ "monday": "Ponedjeljak",
+ "name": "Naziv",
+ "name_label": "Naziv",
+ "name_table_header": "Naziv",
+ "name_tooltip": "Naziv",
+ "next": "Sljedeće",
+ "no": "Ne",
+ "none_text": "Ništa",
+ "nothing_found": "Nema rezultata",
+ "notify_undo": "Poništi",
+ "notify_updated": "Ažurirano",
+ "parental_control": "Roditeljska zaštita",
+ "parental_group_cdn": "CDN",
+ "parental_group_development": "Razvoj softwarea",
+ "parental_group_gaming": "Igre",
+ "parental_group_hosting": "Web hosting",
+ "parental_group_privacy": "Alati za privatnost",
+ "parental_group_shopping": "Kupovina",
+ "parental_group_social": "Društvene mreže",
+ "parental_group_social_network": "Društvene mreže",
+ "parental_group_software": "Razvoj softwarea",
+ "parental_group_streaming": "Strujanje",
+ "password_label": "Lozinka",
+ "password_placeholder": "Unesite lozinku",
+ "plain_dns": "Obični DNS",
+ "privacy_policy": "Politika privatnosti",
+ "protection": "Zaštita",
+ "protocol": "Protokol",
+ "query_log_allowed": "Na popisu dopuštenih",
+ "query_log_blocked": "Blokirano",
+ "query_log_blocked_services": "Blokirane usluge",
+ "query_log_blocked_threats": "Blokirane prijetnje",
+ "query_log_custom_filtering_rules": "Prilagođena pravila filtriranja",
+ "query_log_nothing_available": "Nema rezultata",
+ "query_log_processed": "Obrađeno",
+ "query_log_rewritten": "Prepisano",
+ "query_log_safe_search": "Sigurno pretraživanje",
+ "query_log_strict_search": "Koristite dvostruke navodnike za strogo pretraživanje",
+ "rate_limit_subnet_len_ipv4_error": "Dužina IPv4 prefiksa podmreže trebala bi biti između 0 i 32",
+ "rate_limit_subnet_len_ipv6_error": "Dužina IPv6 prefiksa podmreže trebala bi biti između 0 i 128",
+ "refresh_btn": "Osvježi",
+ "refresh_statics": "Osvježi statistiku",
+ "release_notes": "Bilješke o izdanju",
+ "remove": "Ukloni",
+ "report_an_issue": "Prijava problema",
+ "request_table_header": "Zahtjev",
+ "requests_table_header": "Zahtjevi",
+ "reset": "Poništi",
+ "reset_settings": "Poništi postavke",
+ "reset_settings_confirm": "Da, resetiraj",
+ "response_details": "Detalji odgovora",
+ "response_time": "Vrijeme odziva",
+ "result": "Rezultat",
+ "rewrite_add": "Dodaj DNS prijepis",
+ "rewrite_edit": "Uredite prepisivanje DNS-a",
+ "rewrite_hosts_applied": "Prepisano od strane pravila hosts datoteke",
+ "rewrite_settings_updated": "Postavke prepisivanja DNS-a uspješno su ažurirane",
+ "rewrite_updated": "Prepisivanje DNS-a uspješno ažurirano",
+ "rewritten": "Prepisano",
+ "rules_label": "Pravila",
+ "safe_browsing": "Sigurno surfanje",
+ "safe_search": "Sigurno pretraživanje",
+ "saturday": "Subota",
+ "save": "Spremi",
+ "save_btn": "Spremi",
+ "search_placeholder": "Pretraga",
+ "server_config_blocking_mode": "Način blokiranja",
+ "server_config_blocking_mode_ipv4": "Blokiranje IPv4",
+ "server_config_blocking_mode_ipv6": "Blokiranje IPv6",
+ "server_config_custom_ip": "Prilagođena IP adresa",
+ "server_config_default": "Zadano",
+ "server_config_disable_ipv6": "Onemogući razrješavanje IPv6 adresa",
+ "server_config_dnssec_enable": "Omogući DNSSEC",
+ "server_config_edns_enable": "Omogući podmrežu klijenta EDNS-a",
+ "server_config_nxdomain": "NXDOMAIN",
+ "server_config_rate_limit_placeholder": "Unesite ograničenje",
+ "server_config_rate_limit_whitelist_faq": "IP adrese isključene iz ograničenja brzine",
+ "server_config_refused": "REFUSED",
+ "server_config_subnet_len_ipv4": "Duljina prefiksa podmreže za IPv4 adrese",
+ "server_config_subnet_len_ipv4_faq": "Duljina prefiksa podmreže za IPv4 adrese koje se koriste za ograničavanje brzine. Zadana vrijednost je 24",
+ "server_config_subnet_len_ipv6": "Duljina prefiksa podmreže za IPv6 adrese",
+ "server_config_subnet_len_ipv6_faq": "Duljina prefiksa podmreže za IPv6 adrese koje se koriste za ograničavanje brzine. Zadana vrijednost je 56",
+ "set_static_ip": "Postavite statičku IP adresu",
+ "settings": "Postavke",
+ "settings_browsing_security": "Zaštita pregledavanja",
+ "settings_confirm_clear_statistics": " Poništi statistiku",
+ "settings_custom": "Prilagođeno",
+ "settings_general_short": "Općenito",
+ "settings_global": "Globalno",
+ "settings_notify_changes_saved": "Promjene su spremljene",
+ "settings_notify_statistics_cleared": "Statistika je očišćena",
+ "settings_parental_control": "Roditeljska zaštita",
+ "settings_rule_syntax": "Sintaksa",
+ "settings_safe_search": "Sigurno pretraživanje",
+ "settings_statistics": "Statistika",
+ "settings_statistics_clear": "Poništi statistiku",
+ "settings_statistics_retention": "Spremanje statistike",
+ "settings_yes_clear": "Da",
+ "setup_devices_router_title": "Usmjerivač (Router)",
+ "setup_dns_privacy_ioc_mac": "iOS i macOS konfiguracija",
+ "setup_dns_privacy_other_title": "Ostale implementacije",
+ "show_blocked_responses": "Blokirano",
+ "show_processed_responses": "Obrađeno",
+ "source_label": "Izvor",
+ "source_tooltip": "Izvor",
+ "stats_adult": "Blokirane web stranice za odrasle",
+ "stats_query_domain": "Top tražene domene",
+ "status_table_header": "Status",
+ "sunday": "Nedjelja",
+ "system_host_files": "Datoteke host sustava",
+ "table_client": "Klijent",
+ "tags_title": "Oznake",
+ "test_upstreams": "Testiraj upstream-ove",
+ "threats_blocked": "Prijetnji blokirano",
+ "thursday": "Četvrtak",
+ "time_table_header": "Vrijeme",
+ "top_blocked_domains": "Top blokirane domene",
+ "top_clients": "Top klijenti",
+ "top_upstreams": "Top upstream poslužitelji",
+ "total": "Ukupno",
+ "try_again": "Pokušajte ponovno",
+ "tuesday": "Utorak",
+ "unavailable_dhcp": "DHCP je nedostupan",
+ "unavailable_dhcp_desc": "AdGuard Home ne može pokrenuti DHCP poslužitelj na vašem OS-u",
+ "unblock": "Odblokiraj",
+ "update_button": "Ažuriraj",
+ "update_filters_disable": "Onemogući",
+ "update_filters_interval_custom": "Prilagođeno",
+ "update_filters_interval_daily": "Svaki dan",
+ "update_filters_interval_hourly": "Svaki sat",
+ "update_filters_title": "Automatski ažurirajte filtre",
+ "updates_checked": "Dostupna je nova verzija AdGuard Home-a",
+ "updates_version_equal": "AdGuard Home je ažuriran",
+ "upstream": "Upstream poslužitelj",
+ "upstream_dns": "Upstream DNS poslužitelji",
+ "upstream_dns_addresses": "Adrese poslužitelja, jedna po retku",
+ "upstream_dns_fastest_addr": "Najbrža IP adresa",
+ "upstream_dns_parallel_requests": "Paralelni zahtjevi",
+ "upstream_dns_servers_title": "Upstream DNS poslužitelji",
+ "upstream_examples_title": "Primjeri",
+ "upstream_private_ptr_resolvers_title": "Koristi privatne reverzne DNS razrješivače",
+ "upstreams": "Upstreams",
+ "url_label": "URL",
+ "user_rules": "Korisnička pravila",
+ "user_rules_add_to_allowlist": "Dodaj na popis dopuštenih",
+ "user_rules_check_button": "Provjeri",
+ "user_rules_check_client_placeholder": "Unesite identifikator klijenta",
+ "user_rules_check_hostname_label": "Naziv poslužitelja ili naziv domene",
+ "user_rules_disable_filter": "Onemogući filtar",
+ "user_rules_disable_parental_control": "Onemogući roditeljsku zaštitu",
+ "user_rules_dns_record_type_placeholder": "Odaberite tip DNS zapisa",
+ "user_rules_domain_blocked": "blokirana domena",
+ "user_rules_edit_dns_rewrite": "Uredite prepisivanje DNS-a",
+ "user_rules_rewrite_rule_is_applied": "Pravilo prijepisa je primjenjeno",
+ "username_label": "Korisničko ime",
+ "username_placeholder": "Unesite korisničko ime",
+ "validated_with_dnssec": "Potvrđeno s DNSSEC-om",
+ "wednesday": "Srijeda",
+ "whois": "WHOIS",
+ "yes": "Da",
+ "yes_disable": "Da",
+ "yes_reset": "Da, resetiraj"
+}
diff --git a/client_v2/src/__locales/hu.json b/client_v2/src/__locales/hu.json
new file mode 100644
index 00000000000..3faf954e9fb
--- /dev/null
+++ b/client_v2/src/__locales/hu.json
@@ -0,0 +1,390 @@
+{
+ "access_settings_title": "Hozzáférési beállítások",
+ "actions_table_header": "Műveletek",
+ "add": "Hozzáad",
+ "add_allowlist": "Engedélyezési lista hozzáadása",
+ "add_blocklist": "Blokkolási lista hozzáadása",
+ "add_persistent_client": "Hozzáadás állandó ügyfélként",
+ "add_to_allowlist": "HOZZÁADÁS A KIVÉTELEKHEZ",
+ "ads_blocked": "Blokkolt hirdetések",
+ "ads_blocked_card": "Blokkolt hirdetések",
+ "adult_websites_blocked_tooltip": "Blokkolt felnőtt tartalmak száma",
+ "all_lists_up_to_date_toast": "Már minden lista naprakész",
+ "allowed": "Kivételezett",
+ "allowlist_add": "Engedélyezési lista hozzáadása",
+ "allowlist_edit": "Engedélyezési lista módosítása",
+ "apply": "Alkalmaz",
+ "autofix_warning_result": "Mindennek eredményeként az Ön rendszeréből származó összes DNS-kérést alapértelmezés szerint az AdGuard Home dolgozza fel.",
+ "autofix_warning_text": "Ha a \"Javítás\" lehetőségre kattint, az AdGuard Home megpróbálja beállítani a rendszerét, hogy használja az AdGuard Home DNS szervert.",
+ "average_time_processing": "Átlagos feldolgozási idő",
+ "average_upstream_response_time": "Átlagos upstream válaszidő",
+ "back": "Vissza",
+ "block": "Blokkolás",
+ "block_for_this_client_only": "Tiltás csak ennek a kliensnek",
+ "blocked": "Tiltva",
+ "blocked_by_cname_or_ip": "CNAME vagy IP által blokkolva",
+ "blocked_service": "Blokkolt szolgáltatás",
+ "blocked_services": "Blokkolt szolgáltatások",
+ "blocked_threats": "Tiltott fenyegetések",
+ "blocked_threats_chart": "Tiltott fenyegetések",
+ "blocklist_add_manual": "Kézi hozzáadás",
+ "blocklist_edit": "Blokkolási lista módosítása",
+ "blocklist_homepage": "Honlap",
+ "blocklist_url_file_path": "URL vagy fájl elérési út",
+ "bootstrap_dns": "Bootstrap DNS kiszolgálók",
+ "browse": "Tallózás",
+ "cache_cleared": "A DNS gyorsítótár sikeresen törlődött",
+ "cache_config_clear": "Gyorsítótár törlése",
+ "cache_config_optimistic": "Optimista gyorsítótár",
+ "cache_config_ttl_validation": "A minimális gyorsítótár TTL értéknek kisebbnek vagy egyenlőnek kell lennie a maximum értékkel",
+ "cache_enabled": "Gyorsítótár engedélyezése",
+ "cancel": "Mégsem",
+ "cancel_btn": "Mégsem",
+ "category_tooltip": "Kategória",
+ "changes_saved_success": "Változások mentve",
+ "check_dhcp_servers": "DHCP szerverek keresése",
+ "check_not_found": "Nem található az Ön szűrőlistái között",
+ "check_updates_btn": "Frissítések keresése",
+ "clear_btn": "Törlés",
+ "client_add": "Kliens hozzáadása",
+ "client_details": "Kliens részletei",
+ "client_id": "Kliens azonosító (ClientID)",
+ "client_id_placeholder": "Kliens azonosító (ClientID) megadása",
+ "client_identifier": "Azonosító",
+ "client_settings": "Kliens beállítások",
+ "client_table_header": "Kliens",
+ "clients_add_identifier": "Azonosító hozzáadása",
+ "clients_add_placeholder": "Adja meg a kliens nevét",
+ "clients_dns_cache_size": "DNS gyorsítótár mérete, bájtokban",
+ "clients_not_found": "Nem található kliens",
+ "clients_protection": "Védelem",
+ "clients_tags": "Címkék",
+ "clients_use_global_settings": "Globális beállítások használata",
+ "close": "Bezárás",
+ "confirm": "Megerősítés",
+ "copied": "Vágólapra másolva",
+ "copy": "Másolás",
+ "copy_to_clipboard": "Másolás vágólapra",
+ "custom_filtering_rules": "Egyéni szűrési szabályok",
+ "dark_theme": "Sötét téma",
+ "dashboard": "Irányítópult",
+ "delete_btn": "Törlés",
+ "delete_table_action": "Törlés",
+ "dhcp_add_static_lease": "Statikus bérlet hozzáadása",
+ "dhcp_config_saved": "DHCP beállítások sikeresen el lettek mentve",
+ "dhcp_edit_static_lease": "Statikus bérlet szerkesztése",
+ "dhcp_form_gateway_input": "Átjáró IP",
+ "dhcp_form_subnet_input": "Alhálózati maszk",
+ "dhcp_leases_not_found": "Nem találhatóak DHCP bérletek",
+ "dhcp_leases_title": "DHCP bérletek",
+ "dhcp_new_static_lease": "Új statikus bérlet",
+ "dhcp_reset_leases": "Bérletek alaphelyzetbe",
+ "dhcp_reset_leases_success": "DHCP bérletek alaphelyzetbe állítva",
+ "dhcp_settings": "DHCP beállítások",
+ "dhcp_static_leases_not_found": "Nem találhatóak statikus DHCP bérletek",
+ "dhcp_table_hostname": "Hosztnév",
+ "dhcp_table_ip_address": "IP cím",
+ "disable": "Kikapcsolás",
+ "disable_protection_btn": "Védelem kikapcsolása",
+ "disabled_dhcp": "DHCP szerver letiltva",
+ "disallow_this_client": "Tiltás ennek a kliensnek",
+ "dns_access_settings_title": "Hozzáférési beállítások",
+ "dns_allowed_clients": "Engedélyezett kliensek",
+ "dns_blocking_mode": "Blokkolás módja",
+ "dns_blocking_mode_custom_ip": "Egyéni IP cím",
+ "dns_blocking_mode_default": "Alapértelmezett",
+ "dns_blocking_mode_ipv4_label": "IPv4 blokkolása",
+ "dns_blocking_mode_ipv6_label": "IPv6 blokkolása",
+ "dns_blocking_mode_nxdomain": "NXDOMAIN",
+ "dns_blocking_mode_refused": "REFUSED",
+ "dns_blocking_mode_title": "Blokkolás módja",
+ "dns_bootstrap_dns_placeholder": "IP-címek",
+ "dns_bootstrap_dns_title": "Bootstrap DNS kiszolgálók",
+ "dns_bootstrap_servers": "Bootstrap DNS kiszolgálók",
+ "dns_cache_config": "DNS gyorsítótár beállításai",
+ "dns_cache_size": "Gyorsítótár mérete",
+ "dns_cache_size_label": "DNS gyorsítótár mérete, bájtokban",
+ "dns_cache_size_title": "Gyorsítótár mérete",
+ "dns_clear_cache": "Gyorsítótár törlése",
+ "dns_clear_cache_cancel": "Mégsem",
+ "dns_clear_cache_confirm": "Igen",
+ "dns_clear_cache_yes": "Igen",
+ "dns_config": "DNS szerver beállításai",
+ "dns_disallowed_clients": "Nem engedélyezett kliensek",
+ "dns_disallowed_clients_title": "Nem engedélyezett kliensek",
+ "dns_disallowed_domains": "Nem engedélyezett domainek",
+ "dns_disallowed_domains_title": "Nem engedélyezett domainek",
+ "dns_edns_custom_placeholder": "IP-cím megadása",
+ "dns_fallback_dns_title": "Tartalék DNS-szerverek",
+ "dns_fallback_servers": "Tartalék DNS-szerverek",
+ "dns_optimistic_caching": "Optimista gyorsítótár",
+ "dns_over_https": "DNS-over-HTTPS",
+ "dns_over_quic": "DNS-over-QUIC",
+ "dns_over_tls": "DNS-over-TLS",
+ "dns_override_max_ttl": "A maximális TTL felülírása",
+ "dns_override_max_ttl_title": "A maximális TTL felülírása",
+ "dns_override_min_ttl": "A minimális TTL felülírása",
+ "dns_override_min_ttl_title": "A minimális TTL felülírása",
+ "dns_privacy": "DNS Adatvédelem",
+ "dns_private_reverse_servers_title": "Privát DNS szerverek",
+ "dns_rate_limit": "Kérések korlátozása",
+ "dns_rate_limit_allowlist_placeholder": "IP-címek",
+ "dns_rate_limit_placeholder": "Adja meg a kérések maximális számát",
+ "dns_rate_limit_title": "Kérések korlátozása",
+ "dns_rewrites": "DNS átírások",
+ "dns_server_addresses": "Kiszolgáló címek",
+ "dns_server_addresses_label": "Szervercímek, soronként egy",
+ "dns_server_addresses_title": "Kiszolgáló címek",
+ "dns_server_config_title": "DNS szerver beállításai",
+ "dns_settings": "DNS",
+ "dns_test_upstreams": "Upstreamek tesztelése",
+ "dns_upstream_servers_title": "Upstream DNS-kiszolgálók",
+ "dns_upstream_timeout": "Upstream időtúllépés",
+ "dns_upstream_timeout_title": "Upstream időtúllépés",
+ "dns_upstream_validation_invalid": "Érvénytelen formátum",
+ "domain": "Domain",
+ "domain_or_client": "Domain vagy kliens",
+ "download_mobileconfig": "Konfigurációs fájl letöltése",
+ "edit_table_action": "Szerkesztés",
+ "enable": "Bekapcsolás",
+ "enabled_dhcp": "DHCP szerver engedélyezve",
+ "encryption_certificate_path": "Tanúsítvány útvonala",
+ "encryption_certificates": "Tanúsítványok",
+ "encryption_certificates_input": "Másolja be ide a PEM-kódolt tanúsítványt.",
+ "encryption_certificates_source_content": "Tanúsítvány tartalmának megadása",
+ "encryption_certificates_source_path": "Tanúsítványfájl útvonalának megadása",
+ "encryption_chain_invalid": "A tanúsítványlánc érvénytelen",
+ "encryption_chain_valid": "A tanúsítványlánc érvényes",
+ "encryption_config_saved": "Titkosítási beállítások mentve",
+ "encryption_doq": "DNS-over-QUIC port",
+ "encryption_dot": "DNS-over-TLS port",
+ "encryption_https": "HTTPS port",
+ "encryption_key": "Privát kulcs",
+ "encryption_key_input": "Másolja ki és illessze be ide a tanúsítványa PEM-kódolt privát kulcsát.",
+ "encryption_key_source_content": "Privát kulcs tartalmának megadása",
+ "encryption_plain_dns": "Egyszerű DNS",
+ "encryption_plain_dns_error": "Az egyszerű DNS letiltásához engedélyezzen legalább egy titkosított DNS protokollt",
+ "encryption_private_key_path": "Privát kulcs útvonala",
+ "encryption_server": "Szerver neve",
+ "encryption_server_enter": "Adja meg az Ön domain címét",
+ "encryption_title": "Titkosítás",
+ "enter_cache_size": "Adja meg a gyorsítótár méretét",
+ "enter_ip_address_placeholder": "IP-cím megadása",
+ "error": "Hiba",
+ "fallback_dns": "Tartalék DNS-szerverek",
+ "filter_category_general": "Általános",
+ "filter_category_general_desc": "Olyan listák, amelyek blokkolják a nyomkövetést és a hirdetéseket a legtöbb eszközön",
+ "filter_category_other": "Egyéb",
+ "filter_category_other_desc": "További tiltólisták",
+ "filter_category_regional": "Regionális",
+ "filter_category_regional_desc": "Olyan listák, amelyek a regionális hirdetések, valamint a nyomkövető szerverek ellen vannak kifejlesztve",
+ "filter_category_security": "Biztonság",
+ "filter_category_security_desc": "Kifejezetten a rosszindulatú, adathalász és átverős domainek blokkolására tervezett listák",
+ "filters": "Szűrők",
+ "forgot_password": "Elfelejtette a jelszót?",
+ "form_enter_blocked_response_ttl": "Írja be a blokkolt válasz TTL-jét (másodpercben)",
+ "form_error_client_id_format": "A ClientID (kliens azonosító) csak számokat, kisbetűket és kötőjeleket tartalmazhat",
+ "form_error_domain_format": "Érvénytelen domain formátum",
+ "form_error_equal": "Nem egyezhetnek",
+ "form_error_format": "Érvénytelen formátum",
+ "form_error_gateway_ip": "A bérleti szerződés nem tartalmazhatja az átjáró IP-címét",
+ "form_error_ip4_format": "Érvénytelen IPv4 cím",
+ "form_error_ip6_format": "Érvénytelen IPv6 cím",
+ "form_error_ip_format": "Érvénytelen IP-cím",
+ "form_error_mac_format": "Érvénytelen MAC cím",
+ "form_error_password": "A jelszavak nem egyeznek",
+ "form_error_port": "Adjon meg egy érvényes portot",
+ "form_error_port_range": "Adjon meg egy portszámot a 80-65535 tartományon belül",
+ "form_error_port_unsafe": "Ez a port nem biztonságos",
+ "form_error_server_name": "Érvénytelen szervernév",
+ "form_error_url_format": "Adjon meg egy érvényes URL-t vagy fájl elérési utat",
+ "friday": "Péntek",
+ "gateway_or_subnet_invalid": "Az alhálózati maszk érvénytelen",
+ "general_statistics": "Általános statisztikák",
+ "ignore_domains_title": "Figyelmen kívül hagyott domainek",
+ "inactivity_schedule_add": "Ütemezés hozzáadása",
+ "inactivity_schedule_all_day": "Egész nap",
+ "inactivity_schedule_edit": "Ütemezés szerkesztése",
+ "install_auth_confirm": "Jelszó megerősítése",
+ "install_auth_password": "Jelszó",
+ "install_auth_password_enter": "Írja be a jelszót",
+ "install_auth_username": "Felhasználónév",
+ "install_auth_username_enter": "Felhasználónév megadása",
+ "install_saved": "Sikeres mentés",
+ "install_settings_all_interfaces": "Minden felület",
+ "install_settings_port": "Port",
+ "install_static_error": "Az AdGuard Home nem tudja automatikusan konfigurálni ezt a hálózati felületet. Kérjük, nézzen utána, hogyan kell ezt manuálisan elvégezni.",
+ "install_static_ok": "Jó hír! A statikus IP-cím már be van állítva",
+ "install_step": "Lépés",
+ "install_submit_title": "Gratulálunk!",
+ "ip_address": "IP cím",
+ "known_tracker": "Ismert követő",
+ "language": "Nyelv",
+ "light_theme": "Világos téma",
+ "login": "Belépés",
+ "logout": "Kijelentkezés",
+ "make_static": "Statikussá tétel",
+ "milliseconds_abbreviation": "ms",
+ "monday": "Hétfő",
+ "name": "Név",
+ "name_label": "Név",
+ "name_table_header": "Név",
+ "name_tooltip": "Név",
+ "next": "Következő",
+ "no": "Nem",
+ "none_text": "Semmi",
+ "nothing_found": "Nincs találat",
+ "notify_undo": "Visszavonás",
+ "notify_updated": "Frissítve",
+ "parental_control": "Szülői felügyelet",
+ "parental_group_cdn": "CDN",
+ "parental_group_development": "Szoftverfejlesztés",
+ "parental_group_gaming": "Játék",
+ "parental_group_hosting": "Webtárhely",
+ "parental_group_privacy": "Adatvédelmi eszközök",
+ "parental_group_shopping": "Vásárlás",
+ "parental_group_social": "Közösségi háló",
+ "parental_group_social_network": "Közösségi háló",
+ "parental_group_software": "Szoftverfejlesztés",
+ "parental_group_streaming": "Streaming",
+ "password_label": "Jelszó",
+ "password_placeholder": "Írja be a jelszót",
+ "plain_dns": "Egyszerű DNS",
+ "privacy_policy": "Adatvédelmi irányelvek",
+ "protection": "Védelem",
+ "protocol": "Protokoll",
+ "query_log_allowed": "Kivételezett",
+ "query_log_blocked": "Tiltva",
+ "query_log_blocked_services": "Blokkolt szolgáltatások",
+ "query_log_blocked_threats": "Tiltott fenyegetések",
+ "query_log_custom_filtering_rules": "Egyéni szűrési szabályok",
+ "query_log_nothing_available": "Nincs találat",
+ "query_log_processed": "Feldolgozott",
+ "query_log_rewritten": "Átírt",
+ "query_log_safe_search": "Biztonságos keresés",
+ "query_log_strict_search": "Használjon \"dupla idézőjelet\" a pontos kereséshez",
+ "rate_limit_subnet_len_ipv4_error": "Az IPv4 alhálózati előtag hosszának 0 és 32 között kell lennie",
+ "rate_limit_subnet_len_ipv6_error": "Az IPv6 alhálózati előtag hosszának 0 és 128 között kell lennie",
+ "refresh_btn": "Frissítés",
+ "refresh_statics": "Statisztikák frissítése",
+ "release_notes": "Kiadási megjegyzések",
+ "remove": "Eltávolítás",
+ "report_an_issue": "Hiba jelentése",
+ "request_table_header": "Kérés",
+ "requests_table_header": "Kérések",
+ "reset": "Visszaállítás",
+ "reset_settings": "Beállítások visszaállítása",
+ "reset_settings_confirm": "Igen",
+ "response_details": "Válasz részletei",
+ "response_time": "Válaszidő",
+ "result": "Eredmény",
+ "rewrite_add": "DNS átírás hozzáadása",
+ "rewrite_edit": "DNS újraírás szerkesztése",
+ "rewrite_hosts_applied": "Átírva egy hoszt szabály által",
+ "rewrite_settings_updated": "A DNS újraírási beállítások sikeresen frissítve lettek",
+ "rewrite_updated": "A DNS újraírása sikeresen frissítve",
+ "rewritten": "Átírt",
+ "rules_label": "Szabályok:",
+ "safe_browsing": "Biztonságos böngészés",
+ "safe_search": "Biztonságos keresés",
+ "saturday": "Szombat",
+ "save": "Mentés",
+ "save_btn": "Mentés",
+ "search_placeholder": "Keresés",
+ "server_config_blocking_mode": "Blokkolás módja",
+ "server_config_blocking_mode_ipv4": "IPv4 blokkolása",
+ "server_config_blocking_mode_ipv6": "IPv6 blokkolása",
+ "server_config_custom_ip": "Egyéni IP cím",
+ "server_config_default": "Alapértelmezett",
+ "server_config_disable_ipv6": "IPv6 címek feloldásának tiltása",
+ "server_config_dnssec_enable": "DNSSEC engedélyezése",
+ "server_config_edns_enable": "EDNS kliens alhálózat engedélyezése",
+ "server_config_nxdomain": "NXDOMAIN",
+ "server_config_rate_limit_placeholder": "Adja meg a kérések maximális számát",
+ "server_config_rate_limit_whitelist_faq": "A sebességkorlátozásból kizárt IP-címek",
+ "server_config_refused": "REFUSED",
+ "server_config_subnet_len_ipv4": "Az IPv4-címek alhálózati előtagjának hossza",
+ "server_config_subnet_len_ipv4_faq": "A sebességkorlátozáshoz használt IPv4-címek alhálózati előtagjának hossza. Az alapértelmezett érték 24",
+ "server_config_subnet_len_ipv6": "Az IPv6-címek alhálózati előtagjának hossza",
+ "server_config_subnet_len_ipv6_faq": "A sebességkorlátozáshoz használt IPv6-címek alhálózati előtagjának hossza. Az alapértelmezett érték 56",
+ "set_static_ip": "Statikus IP-cím beállítása",
+ "settings": "Beállítások",
+ "settings_browsing_security": "Böngészési biztonság",
+ "settings_confirm_clear_statistics": "Statisztika törlése?",
+ "settings_custom": "Egyéb",
+ "settings_general_short": "Általános",
+ "settings_global": "Globális",
+ "settings_notify_changes_saved": "Változások mentve",
+ "settings_notify_statistics_cleared": "A statisztikák törölve lettek",
+ "settings_parental_control": "Szülői felügyelet",
+ "settings_rule_syntax": "Szabály szintaxis",
+ "settings_safe_search": "Biztonságos keresés",
+ "settings_statistics": "Statisztikák",
+ "settings_statistics_clear": "Statisztikák visszaállítása",
+ "settings_statistics_retention": "Statisztika megőrzése",
+ "settings_yes_clear": "Igen",
+ "setup_devices_router_title": "Router",
+ "setup_dns_privacy_ioc_mac": "iOS és macOS konfiguráció",
+ "setup_dns_privacy_other_title": "Egyéb megvalósítások",
+ "show_blocked_responses": "Tiltva",
+ "show_processed_responses": "Feldolgozott",
+ "source_label": "Forrás",
+ "source_tooltip": "Forrás",
+ "stats_adult": "Blokkolt felnőtt tartalom",
+ "stats_query_domain": "Leglátogatottabb domainek",
+ "status_table_header": "Állapot",
+ "sunday": "Vasárnap",
+ "system_host_files": "Rendszer hosztfájlok",
+ "table_client": "Kliens",
+ "tags_title": "Címkék",
+ "test_upstreams": "Upstreamek tesztelése",
+ "threats_blocked": "Blokkolt fenyegetések",
+ "thursday": "Csütörtök",
+ "time_table_header": "Idő",
+ "top_blocked_domains": "Legtöbbet blokkolt domainek",
+ "top_clients": "Legaktívabb kliensek",
+ "top_upstreams": "Top upstream szerverek",
+ "total": "Összesen",
+ "try_again": "Újrapróbálkozás",
+ "tuesday": "Kedd",
+ "unavailable_dhcp": "A DHCP nem elérhető",
+ "unavailable_dhcp_desc": "Az AdGuard Home nem tud DHCP szervert futtatni az operációs rendszerén",
+ "unblock": "Feloldás",
+ "update_button": "Frissítés",
+ "update_filters_disable": "Kikapcsolás",
+ "update_filters_interval_custom": "Egyéb",
+ "update_filters_interval_daily": "Minden nap",
+ "update_filters_interval_hourly": "Óránként",
+ "update_filters_title": "Frissíti a szűrőket automatikusan",
+ "updates_checked": "Elérhető az AdGuard Home új verziója",
+ "updates_version_equal": "Az AdGuard Home naprakész",
+ "upstream": "Upstream szerver",
+ "upstream_dns": "Upstream DNS-kiszolgálók",
+ "upstream_dns_addresses": "Szervercímek, soronként egy",
+ "upstream_dns_fastest_addr": "Leggyorsabb IP-cím",
+ "upstream_dns_parallel_requests": "Párhuzamos lekérések",
+ "upstream_dns_servers_title": "Upstream DNS-kiszolgálók",
+ "upstream_examples_title": "Példák",
+ "upstream_private_ptr_resolvers_title": "Privát reverse DNS feloldók használata",
+ "upstreams": "Upstream-ek",
+ "url_label": "URL",
+ "user_rules": "Felhasználói szűrő",
+ "user_rules_add_to_allowlist": "HOZZÁADÁS A KIVÉTELEKHEZ",
+ "user_rules_check_button": "Ellenőrzés",
+ "user_rules_check_client_placeholder": "Adja meg a kliens azonosítót",
+ "user_rules_check_hostname_label": "Gazda név vagy domain név",
+ "user_rules_disable_filter": "Szűrő kikapcsolása",
+ "user_rules_disable_parental_control": "Szülői felügyelet kikapcsolása",
+ "user_rules_dns_record_type_placeholder": "DNS rekord típusának kiválasztása",
+ "user_rules_domain_blocked": "Domain blokkolva",
+ "user_rules_edit_dns_rewrite": "DNS újraírás szerkesztése",
+ "user_rules_rewrite_rule_is_applied": "Alkalmazott átírási szabály",
+ "username_label": "Felhasználónév",
+ "username_placeholder": "Felhasználónév megadása",
+ "validated_with_dnssec": "DNSSEC által ellenőrizve",
+ "wednesday": "Szerda",
+ "whois": "WHOIS",
+ "yes": "Igen",
+ "yes_disable": "Igen",
+ "yes_reset": "Igen"
+}
diff --git a/client_v2/src/__locales/id.json b/client_v2/src/__locales/id.json
new file mode 100644
index 00000000000..5f1c82a6cc7
--- /dev/null
+++ b/client_v2/src/__locales/id.json
@@ -0,0 +1,390 @@
+{
+ "access_settings_title": "Pengaturan akses",
+ "actions_table_header": "Aksi",
+ "add": "Tambahkan",
+ "add_allowlist": "Tambahkan daftar putih",
+ "add_blocklist": "Tambahkan daftar hitam",
+ "add_persistent_client": "Tambahkan sebagai klien persisten",
+ "add_to_allowlist": "Tambahkan ke daftar putih",
+ "ads_blocked": "Iklan diblokir",
+ "ads_blocked_card": "Iklan diblokir",
+ "adult_websites_blocked_tooltip": "Jumlah situs web dewasa yang diblokir",
+ "all_lists_up_to_date_toast": "Semua daftar sudah diperbarui",
+ "allowed": "Dalam Daftar Putih",
+ "allowlist_add": "Tambahkan daftar putih",
+ "allowlist_edit": "Edit daftar putih",
+ "apply": "Terapkan",
+ "autofix_warning_result": "Hasilnya, semua permintaan DNS dari sistem anda akan diproses oleh AdGuardHome secara standar.",
+ "autofix_warning_text": "Apabila anda menekan \"Perbaiki\", AdGuardHome akan mengatur sistem anda untuk menggunakan server DNS AdGuardHome.",
+ "average_time_processing": "Rata-rata waktu pemrosesan",
+ "average_upstream_response_time": "Rata-rata waktu respons hulu",
+ "back": "Back",
+ "block": "Blokir",
+ "block_for_this_client_only": "Blok hanya untuk klien ini",
+ "blocked": "Diblokir",
+ "blocked_by_cname_or_ip": "Diblokir oleh CNAME atau IP",
+ "blocked_service": "Layanan terblokir",
+ "blocked_services": "Layanan terblokir",
+ "blocked_threats": "Ancaman terblokir",
+ "blocked_threats_chart": "Ancaman terblokir",
+ "blocklist_add_manual": "Tambahkan secara manual",
+ "blocklist_edit": "Edit daftar hitam",
+ "blocklist_homepage": "Beranda",
+ "blocklist_url_file_path": "URL atau lokasi path berkas",
+ "bootstrap_dns": "Server DNS bootstrap",
+ "browse": "Jelajahi",
+ "cache_cleared": "Cache DNS berhasil dihapus",
+ "cache_config_clear": "Hapus cache",
+ "cache_config_optimistic": "Caching yang optimis",
+ "cache_config_ttl_validation": "Nilai TTL cache minimum harus kurang dari atau sama dengan nilai maksimum",
+ "cache_enabled": "Aktifkan cache",
+ "cancel": "Batal",
+ "cancel_btn": "Batal",
+ "category_tooltip": "Kategori",
+ "changes_saved_success": "Perubahan disimpan",
+ "check_dhcp_servers": "Cek untuk server DHCP",
+ "check_not_found": "Tidak di temukan di daftar penyaringan anda",
+ "check_updates_btn": "Cek pembaruan",
+ "clear_btn": "Hapus",
+ "client_add": "Tambahkan Klien",
+ "client_details": "Detail klien",
+ "client_id": "ClientID",
+ "client_id_placeholder": "Masukkan ClientID",
+ "client_identifier": "Identifikasi",
+ "client_settings": "Pengaturan klien",
+ "client_table_header": "Klien",
+ "clients_add_identifier": "Tambahkan pengenal",
+ "clients_add_placeholder": "Masukkan nama klien",
+ "clients_dns_cache_size": "Ukuran cache DNS, dalam byte",
+ "clients_not_found": "Tidak ditemukan klien",
+ "clients_protection": "Perlindungan",
+ "clients_tags": "Tag",
+ "clients_use_global_settings": "Gunakan pengaturan global",
+ "close": "Tutup",
+ "confirm": "Konfirmasi",
+ "copied": "Disalin ke papan klip",
+ "copy": "Salin",
+ "copy_to_clipboard": "Salin ke papan klip",
+ "custom_filtering_rules": "Aturan penyaringan khusus",
+ "dark_theme": "Tema gelap",
+ "dashboard": "Beranda",
+ "delete_btn": "Hapus",
+ "delete_table_action": "Hapus",
+ "dhcp_add_static_lease": "Tambah static lease",
+ "dhcp_config_saved": "Pengaturan server DHCP tersimpan",
+ "dhcp_edit_static_lease": "Mengedit static lease",
+ "dhcp_form_gateway_input": "IP gateway",
+ "dhcp_form_subnet_input": "Subnet mask",
+ "dhcp_leases_not_found": "DHCP lease tidak ditemukan",
+ "dhcp_leases_title": "DHCP leases",
+ "dhcp_new_static_lease": "Static lease baru",
+ "dhcp_reset_leases": "Atur ulang semua kontrak",
+ "dhcp_reset_leases_success": "Kontrak DHCP berhasil diatur ulang",
+ "dhcp_settings": "Pengaturan DHCP",
+ "dhcp_static_leases_not_found": "DHCP static lease tidak ditemukan",
+ "dhcp_table_hostname": "Nama host",
+ "dhcp_table_ip_address": "Alamat IP",
+ "disable": "Nonaktifkan",
+ "disable_protection_btn": "Matikan perlindungan",
+ "disabled_dhcp": "Server DHCP dinonaktifkan",
+ "disallow_this_client": "Cabut ijin untuk klien ini",
+ "dns_access_settings_title": "Pengaturan akses",
+ "dns_allowed_clients": "Klien yang diizinkan",
+ "dns_blocking_mode": "Mode blokir",
+ "dns_blocking_mode_custom_ip": "Alamat IP kustom",
+ "dns_blocking_mode_default": "Standar",
+ "dns_blocking_mode_ipv4_label": "Blokiran IPv4",
+ "dns_blocking_mode_ipv6_label": "Blokiran IPv6",
+ "dns_blocking_mode_nxdomain": "NXDOMAIN",
+ "dns_blocking_mode_refused": "DITOLAK",
+ "dns_blocking_mode_title": "Mode blokir",
+ "dns_bootstrap_dns_placeholder": "Alamat IP",
+ "dns_bootstrap_dns_title": "Server DNS bootstrap",
+ "dns_bootstrap_servers": "Server DNS bootstrap",
+ "dns_cache_config": "Konfigurasi cache DNS",
+ "dns_cache_size": "Ukuran cache",
+ "dns_cache_size_label": "Ukuran cache DNS, dalam byte",
+ "dns_cache_size_title": "Ukuran cache",
+ "dns_clear_cache": "Hapus cache",
+ "dns_clear_cache_cancel": "Batal",
+ "dns_clear_cache_confirm": "Ya, hapus",
+ "dns_clear_cache_yes": "Ya, hapus",
+ "dns_config": "Konfigurasi server DNS",
+ "dns_disallowed_clients": "Klien yang tidak diizinkan",
+ "dns_disallowed_clients_title": "Klien yang tidak diizinkan",
+ "dns_disallowed_domains": "Domain yang diblokir",
+ "dns_disallowed_domains_title": "Domain yang diblokir",
+ "dns_edns_custom_placeholder": "Masukkan alamat IP",
+ "dns_fallback_dns_title": "Server DNS cadangan",
+ "dns_fallback_servers": "Server DNS cadangan",
+ "dns_optimistic_caching": "Caching yang optimis",
+ "dns_over_https": "DNS melaui HTTPS",
+ "dns_over_quic": "DNS-over-QUIC",
+ "dns_over_tls": "DNS melalui TLS",
+ "dns_override_max_ttl": "Tumpuk TTL maksimum",
+ "dns_override_max_ttl_title": "Tumpuk TTL maksimum",
+ "dns_override_min_ttl": "Tumpuk TTL minimum",
+ "dns_override_min_ttl_title": "Tumpuk TTL minimum",
+ "dns_privacy": "DNS Privasi",
+ "dns_private_reverse_servers_title": "Server pembalik DNS pribadi",
+ "dns_rate_limit": "Batas nilai",
+ "dns_rate_limit_allowlist_placeholder": "Alamat IP",
+ "dns_rate_limit_placeholder": "Masukkan batas nilai",
+ "dns_rate_limit_title": "Batas nilai",
+ "dns_rewrites": "DNS rewrite",
+ "dns_server_addresses": "Alamat server",
+ "dns_server_addresses_label": "Alamat server, satu per baris",
+ "dns_server_addresses_title": "Alamat server",
+ "dns_server_config_title": "Konfigurasi server DNS",
+ "dns_settings": "DNS",
+ "dns_test_upstreams": "Uji hulu",
+ "dns_upstream_servers_title": "Server DNS hulu",
+ "dns_upstream_timeout": "Batas waktu hulu",
+ "dns_upstream_timeout_title": "Batas waktu hulu",
+ "dns_upstream_validation_invalid": "Format tidak valid",
+ "domain": "Domain",
+ "domain_or_client": "Domain atau klien",
+ "download_mobileconfig": "Unduh berkas konfigurasi",
+ "edit_table_action": "Edit",
+ "enable": "Aktifkan",
+ "enabled_dhcp": "Server DHCP diaktifkan",
+ "encryption_certificate_path": "Path sertifikat",
+ "encryption_certificates": "Sertifikat",
+ "encryption_certificates_input": "Salin / rekatkan sertifikat PEM yang disandikan di sini.",
+ "encryption_certificates_source_content": "Tempel konten sertifikat",
+ "encryption_certificates_source_path": "Tetapkan path berkas sertifikat",
+ "encryption_chain_invalid": "Rantai sertifikat tidak valid",
+ "encryption_chain_valid": "Rantai sertifikat valid",
+ "encryption_config_saved": "Pengaturan enkripsi telah tersimpan",
+ "encryption_doq": "Port DNS-over-QUIC ",
+ "encryption_dot": "Port DNS-over-TLS",
+ "encryption_https": "Port HTTPS",
+ "encryption_key": "Kunci privat",
+ "encryption_key_input": "Salin / rekatkan kunci pribadi PEM berkode untuk sertifikat Anda di sini.",
+ "encryption_key_source_content": "Tempel konten kunci pribadi",
+ "encryption_plain_dns": "Plain DNS",
+ "encryption_plain_dns_error": "Untuk menonaktifkan DNS biasa, aktifkan setidaknya satu protokol DNS terenkripsi",
+ "encryption_private_key_path": "Path kunci pribadi",
+ "encryption_server": "Nama server",
+ "encryption_server_enter": "Masukkan nama domain anda",
+ "encryption_title": "Enkripsi",
+ "enter_cache_size": "Masukkan ukuran cache (bytes)",
+ "enter_ip_address_placeholder": "Masukkan alamat IP",
+ "error": "Kesalahan",
+ "fallback_dns": "Server DNS cadangan",
+ "filter_category_general": "General",
+ "filter_category_general_desc": "Daftar yang memblokir pelacakan dan iklan di sebagian besar perangkat",
+ "filter_category_other": "Lainnya",
+ "filter_category_other_desc": "Daftar hitam lain",
+ "filter_category_regional": "Wilayah",
+ "filter_category_regional_desc": "Daftar yang berfokus pada iklan regional dan server pelacakan",
+ "filter_category_security": "Blokir program berbahaya dan phishing",
+ "filter_category_security_desc": "Daftar yang khusus pada pemblokiran malware, phishing, atau domain penipuan",
+ "filters": "Filter",
+ "forgot_password": "Lupa kata sandi?",
+ "form_enter_blocked_response_ttl": "Masukkan TTL respons yang diblokir (detik)",
+ "form_error_client_id_format": "ClientID hanya boleh berisi angka, huruf kecil, dan tanda hubung",
+ "form_error_domain_format": "Format domain tidak valid",
+ "form_error_equal": "Tidak boleh sama",
+ "form_error_format": "Format tidak valid",
+ "form_error_gateway_ip": "Lease tidak dapat memiliki gerbang alamat IP",
+ "form_error_ip4_format": "Alamat IPv4 tidak valid",
+ "form_error_ip6_format": "Alamat IPv6 tidak valid",
+ "form_error_ip_format": "Alamat IP salah",
+ "form_error_mac_format": "Alamat MAC tidak valid",
+ "form_error_password": "Kata sandi tidak cocok",
+ "form_error_port": "Masukkan nomor port yang valid",
+ "form_error_port_range": "Masukkan nomor port di kisaran 80-65535",
+ "form_error_port_unsafe": "Port tidak aman",
+ "form_error_server_name": "Nama server tidak valid",
+ "form_error_url_format": "Masukkan URL atau lokasi berkas yang valid",
+ "friday": "Jumat",
+ "gateway_or_subnet_invalid": "Subnet samaran tidak valid",
+ "general_statistics": "Statistik umum",
+ "ignore_domains_title": "Domain yang diabaikan",
+ "inactivity_schedule_add": "Tambahkan jadwal",
+ "inactivity_schedule_all_day": "Sepanjang hari",
+ "inactivity_schedule_edit": "Edit jadwal",
+ "install_auth_confirm": "Konfirmasi sandi",
+ "install_auth_password": "Kata sandi",
+ "install_auth_password_enter": "Masukkan sandi",
+ "install_auth_username": "Nama pengguna",
+ "install_auth_username_enter": "Masukkan nama pengguna",
+ "install_saved": "Berhasil disimpan",
+ "install_settings_all_interfaces": "Semua antarmuka",
+ "install_settings_port": "Port",
+ "install_static_error": "AdGuard Home tidak dapat mengonfigurasinya secara otomatis untuk antarmuka jaringan ini. Silakan cari instruksi tentang cara melakukan ini secara manual.",
+ "install_static_ok": "Kabar baik! Alamat IP statis sudah dikonfigurasi",
+ "install_step": "langkah",
+ "install_submit_title": "Selamat!",
+ "ip_address": "Alamat IP",
+ "known_tracker": "Pelacak yang dikenal",
+ "language": "Bahasa",
+ "light_theme": "Tema terang",
+ "login": "Masuk",
+ "logout": "Keluar",
+ "make_static": "Jadikan statis",
+ "milliseconds_abbreviation": "ms",
+ "monday": "Senin",
+ "name": "Nama",
+ "name_label": "Nama",
+ "name_table_header": "Nama",
+ "name_tooltip": "Nama",
+ "next": "Selanjutnya",
+ "no": "Tidak",
+ "none_text": "Kosong",
+ "nothing_found": "Tidak ditemukan",
+ "notify_undo": "Urungkan",
+ "notify_updated": "Diperbarui",
+ "parental_control": "Pengawasan Orang Tua",
+ "parental_group_cdn": "CDN",
+ "parental_group_development": "Pengembangan software",
+ "parental_group_gaming": "Permainan",
+ "parental_group_hosting": "Hosting web",
+ "parental_group_privacy": "Alat privasi",
+ "parental_group_shopping": "Belanja",
+ "parental_group_social": "Jaringan sosial",
+ "parental_group_social_network": "Jaringan sosial",
+ "parental_group_software": "Pengembangan software",
+ "parental_group_streaming": "Streaming",
+ "password_label": "Kata sandi",
+ "password_placeholder": "Masukkan sandi",
+ "plain_dns": "Plain DNS",
+ "privacy_policy": "Kebijakan privasi",
+ "protection": "Perlindungan",
+ "protocol": "Protokol",
+ "query_log_allowed": "Dalam Daftar Putih",
+ "query_log_blocked": "Diblokir",
+ "query_log_blocked_services": "Layanan terblokir",
+ "query_log_blocked_threats": "Ancaman terblokir",
+ "query_log_custom_filtering_rules": "Aturan penyaringan khusus",
+ "query_log_nothing_available": "Tidak ditemukan",
+ "query_log_processed": "Terproses",
+ "query_log_rewritten": "Tulis ulang",
+ "query_log_safe_search": "Aktifkan Pencarian Aman",
+ "query_log_strict_search": "Gunakan tanda kutip ganda untuk pencarian ketat",
+ "rate_limit_subnet_len_ipv4_error": "Panjang awalan subnet IPv4 harus antara 0 dan 32",
+ "rate_limit_subnet_len_ipv6_error": "Panjang awalan subnet IPv6 harus antara 0 dan 128",
+ "refresh_btn": "Segarkan",
+ "refresh_statics": "Segarkan statistik",
+ "release_notes": "Catatan rilis",
+ "remove": "Hapus",
+ "report_an_issue": "Laporkan bug",
+ "request_table_header": "Permintaan",
+ "requests_table_header": "Permintaan",
+ "reset": "Atur ulang",
+ "reset_settings": "Setel ulang pengaturan",
+ "reset_settings_confirm": "Ya, atur ulang",
+ "response_details": "Detail respon",
+ "response_time": "Waktu respons",
+ "result": "Hasil",
+ "rewrite_add": "Tambahkan penulisan ulang DNS",
+ "rewrite_edit": "Edit penulisan ulang DNS",
+ "rewrite_hosts_applied": "Ditulis ulang oleh aturan berkas host",
+ "rewrite_settings_updated": "Pengaturan penulisan ulang DNS berhasil diperbarui",
+ "rewrite_updated": "Penulisan ulang DNS berhasil diperbarui",
+ "rewritten": "Tulis ulang",
+ "rules_label": "Aturan",
+ "safe_browsing": "Penjelajahan Aman",
+ "safe_search": "Aktifkan Pencarian Aman",
+ "saturday": "Sabtu",
+ "save": "Simpan",
+ "save_btn": "Simpan",
+ "search_placeholder": "Cari",
+ "server_config_blocking_mode": "Mode blokir",
+ "server_config_blocking_mode_ipv4": "Blokiran IPv4",
+ "server_config_blocking_mode_ipv6": "Blokiran IPv6",
+ "server_config_custom_ip": "Alamat IP kustom",
+ "server_config_default": "Standar",
+ "server_config_disable_ipv6": "Nonaktifkan penyelesaian alamat IPv6",
+ "server_config_dnssec_enable": "Aktifkan DNSSEC",
+ "server_config_edns_enable": "Aktifkan EDNS Klien Subnet",
+ "server_config_nxdomain": "NXDOMAIN",
+ "server_config_rate_limit_placeholder": "Masukkan batas nilai",
+ "server_config_rate_limit_whitelist_faq": "Alamat IP dikecualikan dari pembatasan tarif",
+ "server_config_refused": "DITOLAK",
+ "server_config_subnet_len_ipv4": "Panjang awalan subnet untuk alamat IPv4",
+ "server_config_subnet_len_ipv4_faq": "Panjang awalan subnet untuk alamat IPv4 yang digunakan untuk pembatasan kecepatan. Standarnya adalah 24",
+ "server_config_subnet_len_ipv6": "Panjang awalan subnet untuk alamat IPv6",
+ "server_config_subnet_len_ipv6_faq": "Panjang awalan subnet untuk alamat IPv6 yang digunakan untuk pembatasan kecepatan. Standarnya adalah 56",
+ "set_static_ip": "Atur alamat IP statik",
+ "settings": "Pengaturan",
+ "settings_browsing_security": "Perlindungan menjelajah",
+ "settings_confirm_clear_statistics": "Hapus statistik?",
+ "settings_custom": "Buatan",
+ "settings_general_short": "General",
+ "settings_global": "Global",
+ "settings_notify_changes_saved": "Perubahan disimpan",
+ "settings_notify_statistics_cleared": "Statistik dihapus",
+ "settings_parental_control": "Kontrol Orang Tua",
+ "settings_rule_syntax": "Sintaks aturan",
+ "settings_safe_search": "Aktifkan Pencarian Aman",
+ "settings_statistics": "Statistik",
+ "settings_statistics_clear": "Hapus statistik",
+ "settings_statistics_retention": "Statistik disimpan",
+ "settings_yes_clear": "Ya, hapus",
+ "setup_devices_router_title": "Router",
+ "setup_dns_privacy_ioc_mac": "Konfigurasi iOS dan macOS",
+ "setup_dns_privacy_other_title": "Implementasi lain",
+ "show_blocked_responses": "Diblokir",
+ "show_processed_responses": "Terproses",
+ "source_label": "Sumber",
+ "source_tooltip": "Sumber",
+ "stats_adult": "Situs dewasa terblokir",
+ "stats_query_domain": "Kueri domain teratas",
+ "status_table_header": "Status",
+ "sunday": "Minggu",
+ "system_host_files": "Berkas host sistem",
+ "table_client": "Klien",
+ "tags_title": "Tag",
+ "test_upstreams": "Uji hulu",
+ "threats_blocked": "Ancaman diblokir",
+ "thursday": "Kamis",
+ "time_table_header": "Waktu",
+ "top_blocked_domains": "Domain diblokir teratas",
+ "top_clients": "Klien teratas",
+ "top_upstreams": "Hulu teratas",
+ "total": "Jumlah",
+ "try_again": "Coba lagi",
+ "tuesday": "Selasa",
+ "unavailable_dhcp": "DHCP tidak tersedia",
+ "unavailable_dhcp_desc": "Adguard Home tidak dapat menjalankan server DHCP pada OS Anda",
+ "unblock": "Buka blokir",
+ "update_button": "Perbarui",
+ "update_filters_disable": "Nonaktifkan",
+ "update_filters_interval_custom": "Buatan",
+ "update_filters_interval_daily": "Setiap hari",
+ "update_filters_interval_hourly": "Setiap jam",
+ "update_filters_title": "Memperbarui filter secara otomatis",
+ "updates_checked": "Versi baru AdGuard Home tersedia",
+ "updates_version_equal": "AdGuard Home sudah tebaru",
+ "upstream": "Hulu",
+ "upstream_dns": "Server DNS hulu",
+ "upstream_dns_addresses": "Alamat server, satu per baris",
+ "upstream_dns_fastest_addr": "Alamat IP tercepat",
+ "upstream_dns_parallel_requests": "Permintaan paralel",
+ "upstream_dns_servers_title": "Server DNS hulu",
+ "upstream_examples_title": "Contoh",
+ "upstream_private_ptr_resolvers_title": "Gunakan server pembalik DNS pribadi",
+ "upstreams": "Hulu",
+ "url_label": "URL",
+ "user_rules": "Aturan pengguna",
+ "user_rules_add_to_allowlist": "Tambahkan ke daftar putih",
+ "user_rules_check_button": "Periksa",
+ "user_rules_check_client_placeholder": "Masukkan pengenal klien",
+ "user_rules_check_hostname_label": "Nama host atau nama domain",
+ "user_rules_disable_filter": "Nonaktifkan filter",
+ "user_rules_disable_parental_control": "Disable ParentalControl",
+ "user_rules_dns_record_type_placeholder": "Pilih tipe catatan DNS",
+ "user_rules_domain_blocked": "Domain diblokir",
+ "user_rules_edit_dns_rewrite": "Edit penulisan ulang DNS",
+ "user_rules_rewrite_rule_is_applied": "Aturan Rewrite yang diterapkan",
+ "username_label": "Nama pengguna",
+ "username_placeholder": "Masukkan nama pengguna",
+ "validated_with_dnssec": "Tervalidasi dengan DNSSEC",
+ "wednesday": "Rabu",
+ "whois": "WHOIS",
+ "yes": "Ya",
+ "yes_disable": "Ya, nonaktifkan",
+ "yes_reset": "Ya, atur ulang"
+}
diff --git a/client_v2/src/__locales/it.json b/client_v2/src/__locales/it.json
new file mode 100644
index 00000000000..c6088eae17e
--- /dev/null
+++ b/client_v2/src/__locales/it.json
@@ -0,0 +1,864 @@
+{
+ "access_settings_allowed_faq": "Un elenco di CIDR, indirizzi IP, o ClientID . Se questo campo conterrà elementi, AdGuard Home accetterà richieste solo da questi client",
+ "access_settings_allowed_placeholder": "Inserisci i clienti consentiti",
+ "access_settings_allowed_title": "Clienti consentiti, uno per riga",
+ "access_settings_blocked_faq_1": "AdGuard Home ignorerà le richieste DNS corrispondenti a questi domini. Tali richieste non compaiono nel registro delle richieste. È possibile inserire nomi di dominio esatti, caratteri jolly o regole di filtro degli URL, come ad esempio example.org, *.example.org o ||example.org^",
+ "access_settings_blocked_faq_2": "Da non confondere con i filtri",
+ "access_settings_blocked_placeholder": "Inserisci i domini non consentiti",
+ "access_settings_blocked_title": "Domini non autorizzati, uno per riga",
+ "access_settings_disallowed_faq": "Un elenco di CIDR, indirizzi IP o ClientID . Se l'elenco conterrà degli elementi, AdGuard Home rifiuterà richieste da questi client. Questo campo verrà ignorato se ci saranno elementi nei client consentiti .",
+ "access_settings_disallowed_placeholder": "Inserisci i clienti non consentiti",
+ "access_settings_disallowed_title": "Clienti non autorizzati, uno per riga",
+ "access_settings_saved_toast": "Impostazioni di accesso salvate",
+ "access_settings_title": "Impostazioni di accesso",
+ "actions_table_header": "Azioni",
+ "add": "Aggiungi",
+ "add_allowlist": "Aggiungi lista bianca",
+ "add_blocklist": "Aggiungi lista nera",
+ "add_persistent_client": "Aggiungi come client persistente",
+ "add_to_allowlist": "Aggiungi alla lista bianca",
+ "ads_blocked": "Annunci bloccati",
+ "ads_blocked_card": "Annunci bloccati",
+ "ads_blocked_tooltip": "Il numero di richieste DNS bloccate dai filtri di blocco inserzioni e dalle liste di blocco degli host",
+ "adult_websites_blocked": "Siti web per adulti bloccati",
+ "adult_websites_blocked_tooltip": "Numero di siti web per adulti bloccati",
+ "all_domains": "Tutti i domini",
+ "all_lists_up_to_date_toast": "Tutti gli elenchi sono aggiornati",
+ "all_rewrites_disabled": "Tutte le risposte DNS personalizzate saranno disabilitate",
+ "all_rewrites_enabled": "Tutte le risposte DNS personalizzate saranno abilitate",
+ "allow_service": "Consenti il servizio",
+ "allowed": "Consentito",
+ "allowlist_add": "Aggiungi lista bianca",
+ "allowlist_edit": "Modifica lista bianca",
+ "allowlist_empty": "Nessuna lista consensi ancora — aggiungi la tua prima lista ",
+ "allowlist_placeholder_example": "La mia lista consensi",
+ "allowlist_remove": "Rimuovi lista consentita?",
+ "allowlist_remove_desc": "%value% verrà rimosso",
+ "allowlists": "Liste consensi",
+ "allowlists_title": "Liste consentite",
+ "apply": "Applica",
+ "aria_clear_input": "Cancella input",
+ "autofix_warning_list": "Eseguirà queste operazioni:
Disattivare DNSStubListener di sistema
Impostare l'indirizzo del server DNS su 127.0.0.1
Sostituire la destinazione del collegamento simbolico di /etc/resolv.conf con /run/systemd/resolve/resolv.conf
Arrestare DNSStubListener (riavviare il servizio systemd-resolved)
",
+ "autofix_warning_result": "Di conseguenza, tutte le richieste DNS dal sistema verranno elaborate da AdGuardHome per impostazione predefinita.",
+ "autofix_warning_text": "Se fai clic su \"Correggi\", AdGuardHome configurerà il tuo sistema per utilizzare il server DNS AdGuardHome.",
+ "average_time_processing": "Tempo di elaborazione medio",
+ "average_time_processing_tooltip": "Tempo medio in millisecondi per elaborare una richiesta DNS",
+ "average_upstream_response_time": "Tempo medio di risposta upstream",
+ "back": "Indietro",
+ "block": "Blocca",
+ "block_client": "Blocca client",
+ "block_for_this_client_only": "Blocca solo per questo client",
+ "blocked": "Bloccato",
+ "blocked_adult_websites": "Bloccato dal controllo parentale",
+ "blocked_by_cname_or_ip": "Bloccato da CNAME o IP",
+ "blocked_queries": "Richieste bloccate",
+ "blocked_service": "Servizio bloccato",
+ "blocked_services": "Servizi bloccati",
+ "blocked_services_desc": "Seleziona i siti web e i servizi più diffusi che dovrebbero essere bloccati",
+ "blocked_threats": "Minacce bloccate",
+ "blocked_threats_chart": "Minacce bloccate",
+ "blocked_total": "%value% totale bloccato",
+ "blocklist_add_from_list": "Aggiungi dalla lista",
+ "blocklist_add_manual": "Aggiungi manualmente",
+ "blocklist_contents": "Contenuti (.%value%)",
+ "blocklist_edit": "Modifica lista nera",
+ "blocklist_homepage": "Pagina principale",
+ "blocklist_placeholder_example": "La mia lista dei blocchi",
+ "blocklist_remove": "Rimuovi lista blocchi?",
+ "blocklist_remove_desc": "%value% verrà rimosso",
+ "blocklist_url_file_path": "URL o percorso del file",
+ "blocklists_add": "Aggiungi liste blocchi",
+ "blocklists_add_desc": "Puoi selezionare le liste blocchi dall'elenco o aggiungerle manualmente",
+ "blocklists_desc": "I domini di queste liste sono bloccati",
+ "blocklists_empty": "Nessuna lista di blocco ancora — aggiungi la tua prima lista ",
+ "blocklists_title": "Liste blocchi",
+ "bootstrap_dns": "Server DNS bootstrap",
+ "browse": "Naviga",
+ "cache_cleared": "Cache DNS è stata cancellata correttamente",
+ "cache_config_clear": "Cancella cache",
+ "cache_config_max_ttl": "Sovrascrivi il valore massimo di TTL, in secondi",
+ "cache_config_max_ttl_faq": "Il valore massimo di tempo di vita (TTL) per le voci nella cache DNS",
+ "cache_config_max_ttl_placeholder": "Inserisci il TTL massimo",
+ "cache_config_min_ttl": "Sovrascrivi il TTL minimo, in secondi",
+ "cache_config_min_ttl_faq": "Il valore minimo di tempo di vita (TTL) per le voci nella cache DNS",
+ "cache_config_min_ttl_placeholder": "Inserisci il TTL minimo",
+ "cache_config_optimistic": "Optimistic caching",
+ "cache_config_optimistic_desc": "AdGuard Home risponderà dalla cache anche se le voci sono scadute e tenterà di aggiornarle",
+ "cache_config_size": "Dimensione della cache, in byte",
+ "cache_config_size_faq": "Dimensione della cache DNS. Per disabilitare la memorizzazione nella cache, lascia vuoto",
+ "cache_config_size_validation": "La dimensione della cache deve essere maggiore di zero quando abilitata",
+ "cache_config_ttl_validation": "La sovrascrittura del valore TTL minimo della cache deve essere inferiore o uguale a quello massimo",
+ "cache_confirm_clear_desc": "Questa operazione rimuoverà tutte le risposte DNS memorizzate nella cache. Le richieste successive DNS potrebbero risultare più lente fino alla ricostruzione della cache.",
+ "cache_confirm_clear_title": "Svuota la cache DNS",
+ "cache_enabled": "Abilita la cache",
+ "cache_enabled_desc": "Memorizza le risposte DNS localmente",
+ "cancel": "Annulla",
+ "cancel_btn": "Annulla",
+ "category_tooltip": "Categoria",
+ "changes_saved_success": "Modifiche salvate",
+ "check_dhcp_servers": "Controlla la presenza di server DHCP",
+ "check_not_found": "Non trovato negli elenchi dei filtri",
+ "check_updates_btn": "Ricerca aggiornamenti",
+ "clear_btn": "Cancellare",
+ "clear_query_log": "Cancella il Registro richieste",
+ "client_add": "Aggiungi Client",
+ "client_added": "Client aggiunto",
+ "client_already_blocked": "Il client %ip% è già bloccato",
+ "client_blocked_flash": "Client bloccato",
+ "client_details": "Dettagli client",
+ "client_id": "ClientID",
+ "client_id_faq": "I clienti possono essere identificati tramite il loro ClientID. Per saperne di più, visita la pagina GitHub di AdGuard Home ",
+ "client_id_placeholder": "Inserisci un ClientID",
+ "client_identifier": "Identificatore",
+ "client_ip": "IP client",
+ "client_location": "Posizione del cliente",
+ "client_removed": "Client rimosso",
+ "client_settings": "Impostazioni client",
+ "client_table_header": "Client",
+ "client_unblocked_flash": "Cliente sbloccato",
+ "clients": "Clienti",
+ "clients_add": "Aggiungi client",
+ "clients_add_identifier": "Aggiungi identificatore",
+ "clients_add_name": "Nome client",
+ "clients_add_placeholder": "Inserisci nome client",
+ "clients_desc": "Regole per i dispositivi connessi ad AdGuard Home",
+ "clients_dns_cache_size": "Dimensioni cache DNS (in byte)",
+ "clients_dns_cache_size_placeholder": "Usa numeri",
+ "clients_dont_collect_stats": "Non raccogliere statistiche per questo client",
+ "clients_dont_log": "Non registrare questo client",
+ "clients_edit": "Modifica client",
+ "clients_identifier_already_used": "Questo identificatore è già in uso",
+ "clients_identifier_format_error": "Usa numeri, lettere inglesi minuscole o trattini",
+ "clients_identifiers": "Identificatori",
+ "clients_identifiers_desc": "Puoi utilizzare l'indirizzo IP del client, CIDR, l'indirizzo MAC o il ClientID (per DoT, DoH e DoQ). Scopri di più ",
+ "clients_logs_and_statistics": "Registri e statistiche",
+ "clients_not_found": "Nessun client trovato",
+ "clients_protection": "Protezione",
+ "clients_protection_desc": "Abilita o disabilita le funzionalità di protezione, la registrazione e le statistiche",
+ "clients_remove_desc": "%value% verrà rimosso dai client persistenti insieme alle sue impostazioni",
+ "clients_remove_title": "Rimuovere il client?",
+ "clients_tags": "Tag",
+ "clients_tags_desc": "Puoi aggiungere tag per questo client e includerli nelle regole di filtraggio. Scopri di più ",
+ "clients_upstreams_desc": "Server DNS upstream, uno per riga",
+ "clients_use_dns_cache": "Usa la cache DNS",
+ "clients_use_global_settings": "Utilizza le impostazioni globali",
+ "clients_use_global_settings_desc": "Eredita le impostazioni di protezione dal tuo account AdGuard Home",
+ "close": "Chiudi",
+ "close_result": "Chiudi risultato",
+ "confirm": "Conferma",
+ "confirm_client_block_desc": "AdGuard Home ignorerà tutte le richieste DNS provenienti dal client %ip% ",
+ "confirm_client_block_title": "Bloccare il client?",
+ "confirm_client_unblock_desc": "Le richieste DNS dal client %ip% saranno consentite",
+ "confirm_client_unblock_title": "Sbloccare il client?",
+ "confirm_static_ip": "AdGuard Home configurerà %ip% come tuo indirizzo IP statico. Vuoi procedere?",
+ "copied": "Copiato negli appunti",
+ "copy": "Copia",
+ "copy_to_clipboard": "Copia negli appunti",
+ "custom_filtering_rules": "Regole filtri personalizzate",
+ "dark_theme": "Tema scuro",
+ "dashboard": "Cruscotto",
+ "delete_btn": "Elimina",
+ "delete_confirm": "Elimina il lease?",
+ "delete_confirm_desc": "Questo libererà %ip% ",
+ "delete_table_action": "Elimina",
+ "delete_table_action_confirm": "Sì, elimina",
+ "device_type": "Tipo di dispositivo",
+ "dhcp_add_static_lease": "Aggiungi lease statico",
+ "dhcp_config_saved": "Salvataggio configurazione server DHCP riuscito",
+ "dhcp_edit_static_lease": "Modifica locazione statica",
+ "dhcp_error": "Impossibile verificare il server DHCP. Aspetta un po' prima di riprovare",
+ "dhcp_form_gateway_address": "Indirizzo IP di gateway",
+ "dhcp_form_gateway_input": "IP Gateway",
+ "dhcp_form_range_start": "Indirizzo IP iniziale",
+ "dhcp_form_range_title": "Intervallo di indirizzi IP",
+ "dhcp_form_subnet_input": "Maschera di sottorete",
+ "dhcp_found": "Impossibile attivare il server DHCP",
+ "dhcp_ipv4_settings": "Impostazioni DHCP IPv4",
+ "dhcp_ipv6_settings": "Impostazioni DHCP IPv6",
+ "dhcp_lease_added": "Lease statico \"%key%\" aggiunta correttamente",
+ "dhcp_lease_deleted": "Lease statico \"%key%\" eliminato correttamente",
+ "dhcp_lease_updated": "Lease statico \"%key%\" aggiornato correttamente",
+ "dhcp_leases_not_found": "Nessun lease DHCP trovato",
+ "dhcp_leases_title": "Leases DHCP",
+ "dhcp_mac_address_already_added": "Questo indirizzo MAC è già stato aggiunto",
+ "dhcp_new_static_lease": "Nuovo lease statico",
+ "dhcp_not_found": "Il server DHCP è attivo",
+ "dhcp_reset": "Questa operazione ripristinerà la configurazione DHCP. Tutte le modifiche andranno perse",
+ "dhcp_reset_leases": "Reimposta tutti i temporanei",
+ "dhcp_reset_leases_confirm": "Questo reimposterà tutti i contratti di leasing",
+ "dhcp_reset_leases_success": "DHCP temporanei reimpostati correttamente",
+ "dhcp_settings": "Impostazioni DHCP",
+ "dhcp_static_ip_error": "Il server DHCP richiede un indirizzo IP statico. Non è stato possibile confermare che questa interfaccia di rete ne abbia uno",
+ "dhcp_static_leases_not_found": "Non è stato trovato nessun leases statico DHCP",
+ "dhcp_table_hostname": "Nome host",
+ "dhcp_table_ip_address": "Indirizzo IP",
+ "dhcp_table_mac_address": "Indirizzo MAC",
+ "disable": "Disattiva",
+ "disable_protection_btn": "Disattiva protezione",
+ "disabled_dhcp": "Server DHCP disattivato",
+ "disallow_client_confirm_allowlist_note": "Questa azione escluderà anche la regola %ip% dall'elenco dei client consentiti.",
+ "disallow_client_confirm_title": "Rifiutare questo cliente?",
+ "disallow_this_client": "Blocca questo client",
+ "dns_access_settings_title": "Impostazioni di accesso",
+ "dns_allowed_clients": "Client permessi",
+ "dns_allowed_clients_label": "Clienti consentiti, uno per riga",
+ "dns_allowed_clients_placeholder": "Inserisci i clienti consentiti",
+ "dns_blocking_mode": "Modalità di blocco",
+ "dns_blocking_mode_custom_ip": "Indirizzo IP personalizzato",
+ "dns_blocking_mode_default": "Predefinito",
+ "dns_blocking_mode_ipv4_label": "Blocca IPv4",
+ "dns_blocking_mode_ipv4_placeholder": "Inserisci indirizzo IPv4",
+ "dns_blocking_mode_ipv6_label": "Blocca IPv6",
+ "dns_blocking_mode_ipv6_placeholder": "Inserisci indirizzo IPv6",
+ "dns_blocking_mode_null_ip": "Indirizzo IP nullo",
+ "dns_blocking_mode_nxdomain": "NXDOMAIN",
+ "dns_blocking_mode_refused": "REFUSED",
+ "dns_blocking_mode_title": "Modalità di blocco",
+ "dns_blocking_mode_ttl_label": "TTL di risposta bloccata, in secondi",
+ "dns_bootstrap_dns_label": "Server DNS Bootstrap, uno per riga",
+ "dns_bootstrap_dns_placeholder": "Indirizzi IP",
+ "dns_bootstrap_dns_title": "Server DNS bootstrap",
+ "dns_bootstrap_servers": "Server DNS bootstrap",
+ "dns_cache_config": "Configurazione cache DNS",
+ "dns_cache_configuration_saved_toast": "Configurazione della cache DNS salvata",
+ "dns_cache_size": "Dimensioni cache",
+ "dns_cache_size_label": "Dimensioni cache DNS (in byte)",
+ "dns_cache_size_title": "Dimensioni cache",
+ "dns_clear_cache": "Cancella cache",
+ "dns_clear_cache_cancel": "Annulla",
+ "dns_clear_cache_confirm": "Sì, cancella",
+ "dns_clear_cache_yes": "Sì, cancella",
+ "dns_config": "Configurazione server DNS",
+ "dns_disallowed_clients": "Client non permessi",
+ "dns_disallowed_clients_label": "Clienti non autorizzati, uno per riga",
+ "dns_disallowed_clients_title": "Client non permessi",
+ "dns_disallowed_domains": "Domini bloccati",
+ "dns_disallowed_domains_label": "Domini non autorizzati, uno per riga",
+ "dns_disallowed_domains_title": "Domini bloccati",
+ "dns_edns_custom_placeholder": "Inserire il indirizzo IP",
+ "dns_fallback_dns_label": "Server DNS di riserva, uno per riga",
+ "dns_fallback_dns_placeholder": "Indirizzi IP, sdns://, quic://, https://, h3:// o tls://",
+ "dns_fallback_dns_title": "Server DNS di fallback",
+ "dns_fallback_servers": "Server DNS di fallback",
+ "dns_optimistic_caching": "Optimistic caching",
+ "dns_over_https": "DNS su HTTPS",
+ "dns_over_quic": "DNS su QUIC",
+ "dns_over_tls": "DNS su TLS",
+ "dns_override_max_ttl": "Sovrascrivi TTL massimo",
+ "dns_override_max_ttl_title": "Sovrascrivi TTL massimo",
+ "dns_override_min_ttl": "Sovrascrivi TTL minimo",
+ "dns_override_min_ttl_title": "Sovrascrivi TTL minimo",
+ "dns_privacy": "Privacy DNS",
+ "dns_private_reverse_servers_label": "Server DNS inversi privati, uno per riga",
+ "dns_private_reverse_servers_placeholder": "Indirizzi IP, sdns://, quic://, https://, h3:// o tls://",
+ "dns_private_reverse_servers_title": "Server DNS privati inversi",
+ "dns_queries": "Richieste DNS",
+ "dns_queries_tooltip": "Numero di richieste DNS elaborate nelle ultime 24 ore",
+ "dns_query": "Richieste DNS",
+ "dns_rate_limit": "Limite delle richieste",
+ "dns_rate_limit_allowlist": "Lista consensi per limitazione velocità",
+ "dns_rate_limit_allowlist_placeholder": "Indirizzi IP",
+ "dns_rate_limit_allowlist_title": "Lista consensi per limitazione velocità",
+ "dns_rate_limit_placeholder": "Imposta limite delle richieste",
+ "dns_rate_limit_title": "Limite delle richieste",
+ "dns_rewrites": "Riscrittura DNS",
+ "dns_server_addresses": "Indirizzi server",
+ "dns_server_addresses_label": "Indirizzi server, uno per riga",
+ "dns_server_addresses_placeholder": "Indirizzi IP, sdns://, quic://, https://, h3:// o tls://",
+ "dns_server_addresses_title": "Indirizzi server",
+ "dns_server_config_title": "Configurazione server DNS",
+ "dns_server_configuration_saved_toast": "Configurazione del server DNS salvata",
+ "dns_settings": "DNS",
+ "dns_subnet_placeholder": "Inserire la lunghezza del prefisso",
+ "dns_test_upstreams": "Testa gli upstream",
+ "dns_upstream_servers_title": "Server DNS upstream",
+ "dns_upstream_timeout": "Timeout upstream",
+ "dns_upstream_timeout_title": "Timeout upstream",
+ "dns_upstream_validation_invalid": "Formato non valido",
+ "domain": "Dominio",
+ "domain_or_client": "Dominio o client",
+ "download_mobileconfig": "Scarica file di configurazione",
+ "dynamic_dhcp_leases_not_found": "Non sono stati trovati lease DHCP dinamici",
+ "edit_table_action": "Modifica",
+ "enable": "Attiva",
+ "enabled_dhcp": "Server DHCP attivo",
+ "encrypted_dns_addresses": "Indirizzi server DNS crittografati",
+ "encryption_certificate_has_issues": "Il certificato presenta problemi",
+ "encryption_certificate_path": "Percorso di certificato",
+ "encryption_certificates": "Certificati",
+ "encryption_certificates_desc": "Per utilizzare la crittografia, è necessario fornire una catena di certificati SSL valida per il tuo dominio. Puoi ottenere un certificato SSL gratuito da letsencrypt.org oppure acquistarne uno da un'autorità di certificazione affidabile",
+ "encryption_certificates_input": "Copia / incolla qui i certificati codificati PEM.",
+ "encryption_certificates_source_content": "Incolla i contenuti di certificato",
+ "encryption_certificates_source_path": "Definisci un percorso alle file dei certificati",
+ "encryption_chain_invalid": "La catena di certificati non è valida",
+ "encryption_chain_valid": "La catena di certificati è valida",
+ "encryption_config_saved": "Configurazione crittografia salvata",
+ "encryption_confirm_clear": "Reimposta le impostazioni di crittografia",
+ "encryption_confirm_clear_desc": "Tutte le impostazioni di crittografia verranno ripristinate ai valori predefiniti",
+ "encryption_disable_plain_dns": "Disabilitare il DNS semplice?",
+ "encryption_disable_plain_dns_desc": "I dispositivi che non supportano il DNS crittografato potrebbero non funzionare correttamente",
+ "encryption_doq": "Porta DNS su QUIC",
+ "encryption_doq_tooltip": "Questa porta è utilizzata per eseguire un server DNS-over-QUIC",
+ "encryption_dot": "DNS su porta TLS",
+ "encryption_dot_tooltip": "Questa porta è utilizzata per eseguire un server DNS-over-TLS",
+ "encryption_encrypted_dns": "DNS crittografato",
+ "encryption_encrypted_dns_desc": "L'interfaccia di amministrazione di AdGuard Home utilizza HTTPS. Il server DNS supporta DNS-over-HTTPS, DNS-over-TLS e DNS-over-QUIC",
+ "encryption_expire": "Scade: %value%",
+ "encryption_force_redirect": "Reindirizza automaticamente da HTTP a HTTPS",
+ "encryption_hostnames": "Nomi host: %value%",
+ "encryption_https": "Porta HTTPS",
+ "encryption_https_tooltip": "Questa porta fornisce accesso HTTPS per l'interfaccia di amministrazione di AdGuard Home. Fornisce anche DNS-over-HTTPS all'indirizzo '/dns-query'",
+ "encryption_issuer": "Emittente: %value%",
+ "encryption_key": "Chiave privata",
+ "encryption_key_input": "Copia/Incolla qui la tua chiave privata codificata PEM per il tuo certificato.",
+ "encryption_key_invalid": "La chiave privata non è valida",
+ "encryption_key_source_content": "Incolla i contenuti della chiave privata",
+ "encryption_key_source_path": "Imposta un file chiave privata",
+ "encryption_key_type": "Algoritmo di crittografia: %value%",
+ "encryption_key_valid": "La chiave privata è valida",
+ "encryption_plain_dns": "DNS semplice",
+ "encryption_plain_dns_error": "Per disabilitare il DNS semplice, abilitare almeno un protocollo DNS crittografato",
+ "encryption_private_key_path": "Percorso della chiave privata",
+ "encryption_server": "Nome server",
+ "encryption_server_enter": "Inserisci il tuo nome di dominio",
+ "encryption_server_tooltip_1": "Se impostato, AdGuard Home rileverà i ClientID, risponderà alle richieste DDR ed eseguirà ulteriori convalide della connessione. Se non impostato, queste funzionalità sono disabilitate",
+ "encryption_server_tooltip_2": "Deve corrispondere a un nome DNS nel certificato",
+ "encryption_subject": "Oggetto: %value%",
+ "encryption_title": "crittografia",
+ "enter_cache_size": "Immetti dimensioni cache (in byte)",
+ "enter_ip_address_placeholder": "Inserire il indirizzo IP",
+ "error": "Errore",
+ "fallback_dns": "Server DNS di fallback",
+ "filter_added_successfully": "Lista blocchi aggiunta: %value% ",
+ "filter_added_successfully_allowlist": "Lista consensi aggiunta: %value% ",
+ "filter_added_successfully_more": "Liste di blocchi aggiunte: %value% e %more% altri",
+ "filter_category_general": "General",
+ "filter_category_general_desc": "Liste che bloccano tracciatori e inserzioni nella maggioranza dei dispositivi",
+ "filter_category_other": "Altro",
+ "filter_category_other_desc": "Altre liste nere",
+ "filter_category_regional": "Regionale",
+ "filter_category_regional_desc": "Elenchi focalizzati su annunci regionali e server tracciatori",
+ "filter_category_security": "Sicurezza",
+ "filter_category_security_desc": "Elenchi progettati specificamente per bloccare domini malevoli, di phishing o truffa",
+ "filter_removed_successfully": "Lista blocchi rimossa: %value% ",
+ "filter_removed_successfully_allowlist": "Lista consensi rimossa: %value% ",
+ "filters": "Filtri",
+ "forgot_password": "Hai perso la password?",
+ "forgot_password_list_item_1": "Crea una nuova password utilizzando lo strumento htpasswd o qualsiasi generatore BCrypt online.",
+ "forgot_password_list_item_2": "Chiudi AdGuard Home.",
+ "forgot_password_list_item_3": "Apri il file AdGuardHome.yaml, individua il campo password e sostituiscilo con il nuovo valore.",
+ "forgot_password_list_item_4": "Avvia AdGuard Home.",
+ "forgot_password_list_item_5": "Accedi all'interfaccia web con la tua nuova password.",
+ "forgot_password_list_title": "Per cambiare la password:",
+ "forgot_password_page_desc": "AdGuard Home memorizza le password sotto forma di hash crittografati con BCrypt",
+ "form_enter_blocked_response_ttl": "Inserisci tempo di vita (TTL) della risposta bloccata (secondi)",
+ "form_enter_hostname": "Inserisci un nome host",
+ "form_enter_ip": "Inserisci un indirizzo IP",
+ "form_enter_mac": "Inserisci un indirizzo MAC",
+ "form_error_answer_format": "Formato del risultato non valido",
+ "form_error_client_id_format": "Il ClientID deve contenere solo numeri, lettere minuscole, e trattini",
+ "form_error_domain_format": "Formato del dominio non valido",
+ "form_error_equal": "Non deve essere uguale",
+ "form_error_format": "Formato non valido",
+ "form_error_gateway_ip": "Il leasing non può avere l'indirizzo IP del gateway",
+ "form_error_hostname_format": "Usa numeri, lettere inglesi minuscole o trattini",
+ "form_error_ip4_format": "Indirizzo IPv4 non valido",
+ "form_error_ip6_format": "Indirizzo IPv6 non valido",
+ "form_error_ip_already_added": "Questo indirizzo IP è già in uso",
+ "form_error_ip_format": "Indirizzo IP non valido",
+ "form_error_mac_already_added": "Questo indirizzo MAC è già stato aggiunto",
+ "form_error_mac_format": "Indirizzo MAC non valido",
+ "form_error_password": "Password non corrispondente",
+ "form_error_port": "Immettere un valore di porta valido",
+ "form_error_port_range": "Immettere il valore della porta nell'intervallo 80-65535",
+ "form_error_port_unsafe": "Questa porta non è sicura",
+ "form_error_range": "Il valore deve essere compreso tra %min% e %max%",
+ "form_error_server_name": "Nome server non valido",
+ "form_error_url_format": "Inserisci un URL valido o un percorso di file corretto",
+ "found_in_known_domains": "Trovato nei domini conosciuti",
+ "friday": "Venerdì",
+ "gateway_or_subnet_invalid": "Maschera di sottorete non valida",
+ "general_statistics": "Statistiche generali",
+ "home_dns_addresses": "Indirizzi DNS di AdGuard Home",
+ "home_dns_addresses_desc": "Il server DNS di AdGuard Home è in ascolto sulle seguenti interfacce/porte:",
+ "ignore_domains_desc_log": "Le richieste che corrispondono a queste regole non vengono visualizzate nel Registro richieste",
+ "ignore_domains_desc_stats": "Le richieste che corrispondono a queste regole non vengono incluse nelle statistiche",
+ "ignore_domains_title": "Domini ignorati",
+ "inactivity_schedule": "Programma di inattività",
+ "inactivity_schedule_add": "Aggiungere programma",
+ "inactivity_schedule_all_day": "Tutto il giorno",
+ "inactivity_schedule_delete": "Elimina il programma",
+ "inactivity_schedule_delete_desc": "Il blocco del servizio sarà attivo tutto il giorno su %value% ",
+ "inactivity_schedule_desc": "Impostare periodi di inattività per i servizi di blocco",
+ "inactivity_schedule_edit": "Modificare programma",
+ "inactivity_schedule_no_schedules": "Nessun programma",
+ "inactivity_schedule_replace_desc": "Questo programma rimpiazzerà tutti i programmi esistenti per lo stesso giorno della settimana. Ogni giorno della settimana può avere un solo periodo d'inattività",
+ "inactivity_schedule_time_from": "Tempo da",
+ "inactivity_schedule_time_to": "Tempo a",
+ "inactivity_schedule_timezone": "Fuso orario",
+ "install_auth_confirm": "Conferma password",
+ "install_auth_password": "Password",
+ "install_auth_password_enter": "Inserisci password",
+ "install_auth_username": "Nome utente",
+ "install_auth_username_enter": "Inserisci nome utente",
+ "install_saved": "Salvataggio riuscito",
+ "install_settings_all_interfaces": "Tutte le interfacce",
+ "install_settings_port": "Porta",
+ "install_static_configure": "AdGuard Home ha rilevato l'utilizzo dell'indirizzo IP dinamico %ip%. Desideri impostarlo come indirizzo statico?",
+ "install_static_error": "AdGuard Home non può configurarlo automaticamente per questa interfaccia di rete. Ti suggeriamo di cercare un metodo alternativo per effettuare tale operazione manualmente.",
+ "install_static_ok": "Buone notizie! L'indirizzo IP statico è già configurato",
+ "install_step": "Passo",
+ "install_submit_title": "Felicitazioni!",
+ "instructions": "Istruzioni",
+ "invalid": "Non valido",
+ "ip_address": "Indirizzo IP",
+ "ip_addresses_placeholder": "Inserisci gli indirizzi IP, uno per riga",
+ "known_tracker": "Tracciatore noto",
+ "language": "Lingua",
+ "last_days": "| Ultimo %count% giorno | Ultimo %count% giorni",
+ "last_hour": "Ultima ora",
+ "last_hours": "| Ultima %count% ora | Ultime %count% ore",
+ "last_updated_label": "Ultimo aggiornamento",
+ "light_theme": "Tema chiaro",
+ "list_updated": "| %count% lista aggiornata | %count% liste aggiornate",
+ "login": "Accedi",
+ "logout": "Disconnetti",
+ "make_static": "Rendere statico",
+ "make_static_desc": "Questa azione converte un indirizzo dinamico in un indirizzo statico",
+ "milliseconds_abbreviation": "ms",
+ "monday": "Lunedi",
+ "name": "Nome",
+ "name_label": "Nome",
+ "name_table_header": "Nome",
+ "name_tooltip": "Nome",
+ "network_interface": "Interfaccia di rete",
+ "next": "Avanti",
+ "no": "No",
+ "no_stats_yet": "Non ci sono ancora statistiche: continua a usare AdGuard Home per visualizzarle",
+ "none_text": "Nessuno",
+ "nothing_found": "Non è stato trovato nulla",
+ "notify_undo": "Annulla",
+ "notify_updated": "Aggiornato",
+ "open_dashboard": "Apri il quadro comandi",
+ "parental_control": "Controllo Parentale",
+ "parental_group_ai": "IA",
+ "parental_group_cdn": "CDN",
+ "parental_group_dating": "Incontri",
+ "parental_group_development": "Sviluppo di programmi",
+ "parental_group_gambling": "Giochi d'azzardo e scommesse",
+ "parental_group_gaming": "Giochi",
+ "parental_group_hosting": "Hosting web",
+ "parental_group_messaging": "Messaggistica",
+ "parental_group_messenger": "Messaggistica",
+ "parental_group_privacy": "Strumenti per riservatezza",
+ "parental_group_shopping": "Shopping",
+ "parental_group_social": "Reti social",
+ "parental_group_social_network": "Reti social",
+ "parental_group_software": "Sviluppo di programmi",
+ "parental_group_streaming": "Streaming",
+ "password_label": "Password",
+ "password_placeholder": "Inserisci password",
+ "password_requirements": "Requisiti per password",
+ "password_requirements_characters": "Almeno 8 caratteri",
+ "password_requirements_lowercase": "Almeno 1 lettera minuscola",
+ "password_requirements_match": "Le password corrispondono",
+ "password_requirements_special": "Lettere, numeri e caratteri speciali inglesi",
+ "password_requirements_uppercase": "Almeno 1 lettera maiuscola",
+ "pause_for_hour": "Pausa per %count% ora",
+ "pause_for_minutes": "| Pausa per %count% minuto | Pausa per %count% minuti",
+ "pause_for_seconds": "| Pausa per %count% secondo | Pausa per %count% secondi",
+ "pause_until_tomorrow": "Metti in pausa fino a domani, %time%",
+ "period_notify": "Per modificare i periodi visualizzati, modifica Conservazione statistiche in Impostazioni generali ",
+ "plain_dns": "DNS semplice",
+ "plain_dns_addresses": "Indirizzi di server DNS semplici",
+ "port_53_faq_link": "La porta 53 è spesso occupata dai servizi \"DNSStubListener\" o \"systemd-resolved\". Leggi questa istruzione su come risolvere questo problema.",
+ "port_in_use": "Questa porta è già in uso",
+ "privacy_policy": "Politica sulla Riservatezza",
+ "processing_time_ms": "%value% msec",
+ "protection": "Protezione",
+ "protocol": "Protocollo",
+ "queries": "Richieste",
+ "queries_total": "| %count% richiesta totale | %count% richieste totali",
+ "query_details": "Dettagli della richiesta",
+ "query_log": "Registro richieste",
+ "query_log_all_reasons": "Tutti i motivi",
+ "query_log_all_statuses": "Tutti gli stati",
+ "query_log_allowed": "Consentito",
+ "query_log_blocked": "Bloccato",
+ "query_log_blocked_by_filter": "Bloccato dal filtro",
+ "query_log_blocked_by_parental_control": "Bloccato dal controllo parentale",
+ "query_log_blocked_services": "Servizi bloccati",
+ "query_log_blocked_threats": "Minacce bloccate",
+ "query_log_custom_filtering_rules": "Regole filtri personalizzate",
+ "query_log_detail_address": "Indirizzo: %value% ",
+ "query_log_detail_category": "Categoria: %value% ",
+ "query_log_detail_country": "Paese: %value% ",
+ "query_log_detail_date": "Data: %value% ",
+ "query_log_detail_dns_server": "Server DNS: %value% ",
+ "query_log_detail_domain": "Dominio: %value% ",
+ "query_log_detail_ecs": "ECS: %value% ",
+ "query_log_detail_elapsed": "Tempo trascorso: %value% ",
+ "query_log_detail_name": "Nome: %value% ",
+ "query_log_detail_network": "Rete: %value% ",
+ "query_log_detail_original_response": "Risposta originale:",
+ "query_log_detail_protocol": "Protocollo: %value% ",
+ "query_log_detail_reason": "Motivo:",
+ "query_log_detail_response": "Risposta:",
+ "query_log_detail_response_code": "Codice di risposta: %value% ",
+ "query_log_detail_rules": "Regola(e):",
+ "query_log_detail_served_from_cache": "Servito dalla cache: %value% ",
+ "query_log_detail_service_name": "Nome del servizio: %value% ",
+ "query_log_detail_source": "Fonte: %value% ",
+ "query_log_detail_status": "Stato: %value% ",
+ "query_log_detail_time": "Tempo: %value% ",
+ "query_log_detail_type": "Tipo: %value% ",
+ "query_log_nothing_available": "Non è stato trovato nulla",
+ "query_log_processed": "Elaborato",
+ "query_log_retention": "Rotazione dei registri",
+ "query_log_rewritten": "Riscritto",
+ "query_log_safe_search": "Ricerca sicura",
+ "query_log_strict_search": "Utilizzare le doppie virgolette per una ricerca precisa",
+ "rate_limit_subnet_len_ipv4_error": "La lunghezza del prefisso di sottorete IPv4 deve essere compresa tra 0 e 32",
+ "rate_limit_subnet_len_ipv6_error": "La lunghezza del prefisso di sottorete IPv6 deve essere compresa tra 0 e 128",
+ "reason_table_header": "Motivo",
+ "refresh_btn": "Aggiorna",
+ "refresh_statics": "Aggiorna statistiche",
+ "release_notes": "Note di rilascio",
+ "remove": "Rimuovi",
+ "remove_tag": "Rimuovi %value%",
+ "report_an_issue": "Fai rapporto per un problema",
+ "request_table_header": "Richiesta",
+ "requests_table_header": "Richieste",
+ "reset": "Reimposta",
+ "reset_settings": "Reimposta impostazioni",
+ "reset_settings_confirm": "Sì, ripristina",
+ "response_details": "Dettagli di Risposta",
+ "response_time": "Tempo di risposta",
+ "result": "Risultato",
+ "resume_protection_timer": "La protezione riprenderà tra %time%",
+ "rewrite_add": "Aggiungi la riscrittura DNS",
+ "rewrite_domain_input_placeholder": "Inserisci il nome di dominio o il carattere universale",
+ "rewrite_edit": "Modifica della riscrittura DNS",
+ "rewrite_hosts_applied": "Riscritto dal file delle regole host",
+ "rewrite_settings_updated": "Impostazioni di riscrittura DNS aggiornate correttamente",
+ "rewrite_updated": "Riscrittura DNS aggiornata correttamente",
+ "rewrites_answer_input_placeholder": "Inserisci l'indirizzo IP, il record CNAME, A o AAAA",
+ "rewrites_empty": "Nessuna riscrittura ancora — aggiungi la tua prima ",
+ "rewrites_remove_desc": "La riscrittura DNS per %value% verrà rimossa",
+ "rewrites_remove_title": "Rimuovi la riscrittura DNS?",
+ "rewrites_tooltip_examples_item1": "Riscrivi risposte DNS per questo dominio soltanto",
+ "rewrites_tooltip_examples_item2": "Riscrivi le risposte DNS per tutti i sottodomini di example.org",
+ "rewrites_tooltip_instructions_item1": "Per risolvere il nome di dominio in un indirizzo IP, inserisci un indirizzo IPv4 per i record A e un indirizzo IPv6 per i record AAAA",
+ "rewrites_tooltip_instructions_item2": "Per reindirizzare il nome di dominio, inserisci il record CNAME del dominio di destinazione",
+ "rewrites_tooltip_instructions_item3": "Per conservare solo i record di tipo A, digita",
+ "rewrites_tooltip_instructions_item4": "Per conservare solo i record di tipo AAA, digita",
+ "rewritten": "Riscritto",
+ "rows_per_page": "Righe per pagina %value%",
+ "rules_label": "Regole",
+ "safe_browsing": "Navigazione Sicura",
+ "safe_search": "Ricerca sicura",
+ "safe_search_used": "Ricerca sicura utilizzata",
+ "safe_search_used_tooltip": "Numero di richieste DNS dai motori di ricerca per i quali la Ricerca Sicura è stata forzata",
+ "saturday": "Sabato",
+ "save": "Salva",
+ "save_btn": "Salva",
+ "search_placeholder": "Cerca",
+ "selected": "%value% scelto",
+ "server_config_blocking_mode": "Modalità di blocco",
+ "server_config_blocking_mode_custom_ip_desc": "Indirizzo IP personalizzato: Rispondi con un indirizzo IP impostato manualmente",
+ "server_config_blocking_mode_default_desc": "Predefinito: Rispondi con 0.0.0.0 per A e :: per AAAA se bloccato da una regola in stile Adblock. Rispondi con l'indirizzo IP specificato nella regola se bloccato da una regola in stile /etc/hosts",
+ "server_config_blocking_mode_ipv4": "Blocca IPv4",
+ "server_config_blocking_mode_ipv4_faq": "Indirizzo IP da restituire per una richiesta A bloccata",
+ "server_config_blocking_mode_ipv4_placeholder": "Inserisci indirizzo IPv4",
+ "server_config_blocking_mode_ipv6": "Blocca IPv6",
+ "server_config_blocking_mode_ipv6_faq": "Indirizzo IP da restituire per una richiesta AAAA bloccata",
+ "server_config_blocking_mode_ipv6_placeholder": "Inserisci indirizzo IPv6",
+ "server_config_blocking_mode_null_ip_desc": "Indirizzo IP nullo: Rispondi con 0.0.0.0 per A e :: per AAAA",
+ "server_config_blocking_mode_nxdomain_desc": "NXDOMAIN : Rispondi con codice NXDOMAIN",
+ "server_config_blocking_mode_refused_desc": "REFUSED : Rispondi con codice REFUSED",
+ "server_config_blocking_mode_ttl": "TTL di risposta bloccata, in secondi",
+ "server_config_blocking_mode_ttl_faq": "Specifica per quanto tempo i client devono memorizzare nella cache una risposta filtrata",
+ "server_config_custom_ip": "Indirizzo IP personalizzato",
+ "server_config_default": "Predefinito",
+ "server_config_disable_ipv6": "Disattiva risoluzione indirizzi IPv6",
+ "server_config_disable_ipv6_desc": "Elimina tutte le richieste DNS per gli indirizzi IPv6 (tipo AAAA) e rimuovi i suggerimenti IPv6 dalle risposte HTTPS",
+ "server_config_dnssec_enable": "Attiva DNSSEC",
+ "server_config_dnssec_enable_desc": "Imposta il flag DNSSEC nelle query DNS in uscita e verifica il risultato. Richiede un resolver con DNSSEC abilitato",
+ "server_config_edns_cs_desc": "Aggiungi l'opzione EDNS Client Subnet (ECS) alle richieste upstream e registra i valori inviati dai client nel registro richieste",
+ "server_config_edns_enable": "Attiva client di sottorete EDNS",
+ "server_config_edns_use_custom_ip": "Usa indirizzo IP personalizzato per EDNS",
+ "server_config_edns_use_custom_ip_desc": "Sostituisci l'indirizzo IP del client con l'indirizzo IP personalizzato riportato di seguito",
+ "server_config_null_ip": "Indirizzo IP nullo",
+ "server_config_nxdomain": "NXDOMAIN",
+ "server_config_rate_limit": "Limite di frequenza, RPS",
+ "server_config_rate_limit_faq": "Il numero di richieste al secondo consentite da un singolo client. La valore a 0 significa che non sono limite",
+ "server_config_rate_limit_placeholder": "Imposta limite delle richieste",
+ "server_config_rate_limit_whitelist": "Lista consensi per limitazione velocità",
+ "server_config_rate_limit_whitelist_faq": "Indirizzi IP esclusi dalla limitazione della velocità",
+ "server_config_refused": "REFUSED",
+ "server_config_subnet_len_ipv4": "Lunghezza prefisso di sottorete per indirizzi IPv4",
+ "server_config_subnet_len_ipv4_faq": "Lunghezza prefisso sottorete per indirizzi IPv4 usati per la limitazione della velocità. Valore predefinito 24",
+ "server_config_subnet_len_ipv6": "Lunghezza prefisso di sottorete per indirizzi IPv6",
+ "server_config_subnet_len_ipv6_faq": "Lunghezza prefisso di sottorete per indirizzi IPv6 usati per la limitazione della velocità. Valore predefinito 56",
+ "server_config_subnet_len_placeholder": "Inserire la lunghezza del prefisso",
+ "set_static_ip": "Imposta un indirizzo IP statico",
+ "settings": "Impostazioni",
+ "settings_anonymize_client_ip": "Anonimizzare gli indirizzi IP dei client",
+ "settings_anonymize_client_ip_desc": "Sostituisci una parte dell'indirizzo IP con zeri",
+ "settings_browsing_security": "Sicurezza di navigazione",
+ "settings_browsing_security_desc": "Blocca l'accesso ai domini presenti nel database dei domini dannosi e di phishing",
+ "settings_confirm_clear_query_log": "Cancella il Registro richieste?",
+ "settings_confirm_clear_query_log_desc": "Questo eliminerà tutte le richieste dal Registro richieste . Le statistiche saranno ancora disponibili",
+ "settings_confirm_clear_statistics": "Azzerare statistiche?",
+ "settings_confirm_clear_statistics_desc": "Questo cancellerà tutte le statistiche. Il Registro richieste sarà ancora disponibile",
+ "settings_confirm_decrease_log_rotation_interval": "Ridurre l'intervallo di rotazione degli registri?",
+ "settings_confirm_decrease_log_rotation_interval_desc": "Questo eliminerà tutti gli registri più vecchie di %value%",
+ "settings_confirm_decrease_stats_rotation_interval": "Ridurre l'intervallo di conservazione delle statistiche?",
+ "settings_confirm_decrease_stats_rotation_interval_desc": "Questo eliminerà tutte le statistiche più vecchie di %value%",
+ "settings_custom": "Personalizzato",
+ "settings_days": "| %count% giorno | %count% giorni",
+ "settings_domain_names": "Nomi di dominio, uno per riga",
+ "settings_filter_requests": "Filtra richieste",
+ "settings_filter_requests_desc": "È possibile utilizzare le Liste blocchi , le Liste consensi e le Regole utente per impostare le regole di filtro",
+ "settings_filtering_and_security": "Filtraggio e sicurezza",
+ "settings_general_short": "General",
+ "settings_global": "Globale",
+ "settings_hours": "| %count% ora | %count% ore",
+ "settings_log_dns_requests": "Registra le richieste DNS",
+ "settings_log_rotation_hours": "Rotazione degli registri, in ore",
+ "settings_notify_changes_saved": "Modifiche salvate",
+ "settings_notify_query_log_cleared": "Registro richieste svuotato",
+ "settings_notify_statistics_cleared": "Statistiche azzerate",
+ "settings_parental_control": "Controllo Parentale",
+ "settings_parental_control_desc": "Blocca i siti web con contenuti inappropriati per i bambini",
+ "settings_rotation_placeholder": "Usa numeri",
+ "settings_rule_syntax": "Sintassi delle regole",
+ "settings_safe_search": "Ricerca sicura",
+ "settings_safe_search_desc": "Blocca contenuti inappropriati nei risultati di ricerca",
+ "settings_statistics": "Statistiche",
+ "settings_statistics_clear": "Azzera statistiche",
+ "settings_statistics_desc": "Mostra le statistiche delle richieste e dei client nel quadro comandi",
+ "settings_statistics_retention": "Conservazione delle statistiche",
+ "settings_statistics_retention_hours": "Conservazione delle statistiche, in ore",
+ "settings_tooltip_domain_names": "Nomi di dominio, caratteri universali o regole di filtraggio",
+ "settings_tooltip_examples": "Esempi:",
+ "settings_yes_clear": "Sì, cancella",
+ "settings_yes_decrease": "Sì, diminuire",
+ "setup_complete": "La configurazione è completa e AdGuard Home è pronto per l'uso",
+ "setup_devices_android_list_1": "Apri l'app Impostazioni del tuo dispositivo e seleziona Wi-Fi .",
+ "setup_devices_android_list_2": "Tieni premuto a lungo sulla tua rete attuale, quindi seleziona Modifica rete . Non è possibile impostare DNS personalizzati per le connessioni Internet mobili.",
+ "setup_devices_android_list_3": "Su alcuni dispositivi, potrebbe essere necessario selezionare la casella di controllo \"Avanzate\" per visualizzare ulteriori impostazioni. Per personalizzare le impostazioni DNS di Android, è necessario modificare le impostazioni IP passando da DHCP a Statico .",
+ "setup_devices_android_list_4": "Modifica i valori DNS 1 e DNS 2 con gli indirizzi del tuo server AdGuard Home.",
+ "setup_devices_android_title": "Android",
+ "setup_devices_dns_android_list_1": "Android 9 supporta nativamente DNS-over-TLS . Per configurarlo, vai su Impostazioni → Rete e Internet → Avanzate → DNS privato e inserisci il tuo nome di dominio.",
+ "setup_devices_dns_android_list_2": "AdGuard per Android supporta DNS-over-HTTPS , DNS-over-TLS e DNS-over-QUIC setup .",
+ "setup_devices_dns_android_list_3": "Intra aggiunge il supporto DNS-over-HTTPS ad Android.",
+ "setup_devices_dns_ios_list_1": "AdGuard per iOS supporta la configurazione di DNS-over-HTTPS , DNS-over-TLS e DNS-over-QUIC .",
+ "setup_devices_dns_ios_list_2": "DNSCloak supporta DNS-over-HTTPS , ma per configurarlo in modo che utilizzi il tuo server, dovrai generare un DNS Stamp per esso.",
+ "setup_devices_dns_list_1": "Per DNS-over-TLS , usa ",
+ "setup_devices_dns_list_2": "Per DNS-over-HTTPS , usa ",
+ "setup_devices_dns_list_3": "Per DNS-over-QUIC , usa ",
+ "setup_devices_dns_list_devices": "Ecco un elenco di software che puoi usare.",
+ "setup_devices_dns_macos_desc": "Su un dispositivo iOS 14 o macOS Big Sur, è possibile scaricare uno speciale file mobileconfig che aggiunge i server DNS-over-HTTPS o DNS-over-TLS alle impostazioni DNS.",
+ "setup_devices_dns_other_list_1": "AdGuard Home può essere un client DNS sicuro su qualsiasi piattaforma.",
+ "setup_devices_dns_other_list_2": "dnsproxy supporta tutti i protocolli DNS sicuri conosciuti.",
+ "setup_devices_dns_other_list_3": "dnscrypt-proxy supporta DNS-over-HTTPS .",
+ "setup_devices_dns_other_list_4": "Mozilla Firefox supporta DNS-over-HTTPS .",
+ "setup_devices_dns_other_list_5": "Per ulteriori implementazioni, consulta DNSCrypt e DNS Privacy .",
+ "setup_devices_ios_list_1": "Apri l'app Impostazioni del tuo dispositivo e seleziona Wi-Fi . Non è possibile impostare un DNS personalizzato per le connessioni Internet mobili.",
+ "setup_devices_ios_list_2": "Tocca sul nome della rete attiva al momento.",
+ "setup_devices_ios_list_3": "Vai a DNS → Configura DNS → Manuale e aggiungi gli indirizzi del tuo server AdGuard Home.",
+ "setup_devices_ios_list_4": "Tocca Salva .",
+ "setup_devices_ios_title": "iOS",
+ "setup_devices_macos_list_1": "Fai clic sul menu Apple → Preferenze di Sistema → Rete .",
+ "setup_devices_macos_list_2": "Seleziona la connessione desiderata e fai clic su Avanzate .",
+ "setup_devices_macos_list_3": "Seleziona la scheda DNS e inserisci gli indirizzi del tuo server AdGuard Home.",
+ "setup_devices_macos_title": "macOS",
+ "setup_devices_router_desc_1": "Configura AdGuard Home sul router della rete domestica e proteggi tutti i dispositivi collegati ad esso. Non c'è bisogno di configurare manualmente ogni dispositivo.",
+ "setup_devices_router_desc_2": "Alcuni router non consentono di configurare un server DNS personalizzato. In questo caso, puoi provare a configurare AdGuard Home come server DHCP . Se ciò non funziona, consulta il manuale del tuo router per istruzioni su come personalizzare i server DNS per il tuo router specifico.",
+ "setup_devices_router_list_1": "Di solito puoi accedervi dal browser tramite un URL, come ad esempio http://192.168.0.1 oppure http://192.168.1.1 . Potresti essere invitato a inserire una password. Spesso puoi resettare la password premendo un pulsante direttamente sul router, ma tieni presente che potresti perdere tutta la configurazione del router. Se il tuo router deve essere configurato tramite un'app, installa l'app sul telefono o sul PC e usala per accedere alle impostazioni del router.",
+ "setup_devices_router_list_1_title": "Apri le impostazioni del tuo router.",
+ "setup_devices_router_list_2": "Per trovare le impostazioni, cerca la dicitura DNS accanto a un campo che consente l'inserimento di due o tre gruppi di numeri, ciascuno suddiviso in quattro sottogruppi da una a tre cifre.",
+ "setup_devices_router_list_2_title": "Nelle impostazioni DHCP/DNS, inserisci gli indirizzi del tuo server AdGuard Home.",
+ "setup_devices_router_title": "Router",
+ "setup_devices_windows_list_1": "Apri il Pannello di controllo dal menu Start o dalla Ricerca di Windows .",
+ "setup_devices_windows_list_2": "Fai clic su Rete e Internet → Centro connessioni di rete e condivisione → Modifica impostazioni scheda .",
+ "setup_devices_windows_list_3": "Fai clic destro sulla tua connessione attiva e seleziona Proprietà .",
+ "setup_devices_windows_list_4": "Nell'elenco, seleziona Protocollo Internet Versione 4 (TCP/IPv4) per IPv4 o Protocollo Internet Versione 6 (TCP/IPv6) per IPv6.",
+ "setup_devices_windows_list_5": "Fai clic su Proprietà → Utilizza i seguenti indirizzi server DNS .",
+ "setup_devices_windows_list_6": "Inserisci gli indirizzi del tuo server AdGuard Home.",
+ "setup_devices_windows_title": "Windows",
+ "setup_dns_desc": "Configura il tuo router o altri dispositivi in modo che utilizzino i seguenti indirizzi server DNS:",
+ "setup_dns_notice_new": "Per utilizzare DNS-over-HTTPS o DNS-over-TLS , è necessario configurare la crittografia nelle impostazioni di AdGuard Home.",
+ "setup_dns_privacy_ioc_mac": "configurazione iOS e macOS",
+ "setup_dns_privacy_other_title": "Altre implementazion",
+ "setup_dns_quote_desc": "Per funzionare correttamente, il server DNS di AdGuard Home richiede un indirizzo IP statico sulla rete locale. In caso contrario, il router potrebbe assegnargli un indirizzo IP diverso, compromettendo la protezione DNS",
+ "setup_dns_quote_title": "Assegna un indirizzo IP statico al server DNS di AdGuard Home sulla tua rete locale",
+ "setup_dns_title": "Imposta l'indirizzo del server DNS di AdGuard Home",
+ "setup_dns_title_banner": "Impostazioni del server DNS",
+ "setup_guide": "Guida di installazione",
+ "setup_guide_auth_privacy": "Accetto la Politica di riservatezza e i Termini e condizioni dei siti web di AdGuard",
+ "setup_guide_auth_subtitle": "Ciò è necessario per proteggere il pannello di amministrazione di AdGuard Home da accessi non autorizzati",
+ "setup_guide_auth_title": "Imposta la protezione con password",
+ "setup_guide_desc": "Configura i tuoi dispositivi per utilizzare AdGuard Home",
+ "setup_guide_device_type": "Seleziona il tipo di dispositivo",
+ "setup_guide_greeting_button": "Esegui procedura guidata",
+ "setup_guide_greeting_desc": "Questa procedura guidata ti aiuterà a configurare AdGuard Home:",
+ "setup_guide_greeting_list_1": "Crea le credenziali per accedere al pannello di amministrazione",
+ "setup_guide_greeting_list_2": "Imposta l'indirizzo dell'interfaccia utente web di AdGuard Home",
+ "setup_guide_greeting_list_3": "Imposta l'indirizzo del server DNS di AdGuard Home",
+ "setup_guide_greeting_list_4": "Scopri le istruzioni relative ai tuoi dispositivi specifici",
+ "setup_guide_greeting_list_5": "Accedi alla dashboard di AdGuard Home ",
+ "setup_guide_greeting_title": "Configura AdGuard Home",
+ "setup_guide_title": "Configura i tuoi dispositivi per utilizzare AdGuard Home",
+ "setup_ui_desc": "Utilizza questi indirizzi per accedere all'interfaccia utente Web di AdGuard Home. Se necessario, cambia la porta",
+ "setup_ui_title": "Imposta l'indirizzo dell'interfaccia utente web di AdGuard Home",
+ "setup_ui_title_banner": "Impostazioni dell'interfaccia utente web",
+ "show_blocked_responses": "Bloccato",
+ "show_more_count": "+ %count% di più",
+ "show_processed_responses": "Elaborato",
+ "sort_asc": "Ordina da A a Z",
+ "sort_desc": "Ordina da Z a A",
+ "source_label": "Fonte",
+ "source_tooltip": "Fonte",
+ "static_dhcp_leases_not_found": "Non sono stati trovati lease DHCP statici",
+ "stats_adult": "Siti per adulti bloccati",
+ "stats_query_domain": "Domini maggiormente richiesti",
+ "status_table_header": "Stato",
+ "sunday": "Domenica",
+ "system_host_files": "File host di sistema",
+ "system_theme": "Tema di sistema",
+ "table_client": "Client",
+ "tags_title": "Tag",
+ "test_upstreams": "Testa gli upstream",
+ "threats_blocked": "Minacce bloccate",
+ "threats_blocked_tooltip": "Numero di richieste DNS bloccate dal modulo sicurezza di navigazione di AdGuard",
+ "thursday": "Giovedì",
+ "time_table_header": "Ora",
+ "top_blocked_domains": "Domini maggiormente bloccati",
+ "top_clients": "Client più utilizzati",
+ "top_upstreams": "Top upstream",
+ "total": "Totale",
+ "try_again": "Riprova",
+ "tuesday": "Martedì",
+ "type_value": "Tipo: %value%",
+ "unavailable_dhcp": "DHCP non disponibile",
+ "unavailable_dhcp_desc": "AdGuard Home non può eseguire un server DHCP sul tuo sistema operativo",
+ "unblock": "Sblocca",
+ "unblock_client": "Sblocca client",
+ "unknown_filter": "Filtro sconosciuto %filterId%",
+ "update_button": "Aggiorna",
+ "update_filters_custom_hours": "Intervallo personalizzato, in ore",
+ "update_filters_disable": "Disattiva",
+ "update_filters_interval_custom": "Personalizzato",
+ "update_filters_interval_daily": "Ogni giorno",
+ "update_filters_interval_hourly": "Ogni ora",
+ "update_filters_interval_weekly": "Ogni settimana",
+ "update_filters_title": "Aggiorna automaticamente i filtri",
+ "updated_upstream_dns_toast": "Server upstream salvati",
+ "updates_checked": "Nuova versione di AdGuard Home è disponibile",
+ "updates_version_equal": "AdGuard Home è aggiornato",
+ "upstream": "Upstream",
+ "upstream_bootstrap_dns_faq": "Indirizzi IP dei server DNS utilizzati per risolvere gli indirizzi IP dei resolver upstream del DoH/DoT. I commenti non sono consentiti",
+ "upstream_bootstrap_dns_title": "Server DNS Bootstrap, uno per riga",
+ "upstream_dns": "Server DNS upstream",
+ "upstream_dns_addresses": "Indirizzi server, uno per riga",
+ "upstream_dns_addresses_faq": "Scopri di più sulla configurazione dei server DNS upstream. Ecco un elenco di provider DNS noti tra cui scegliere.",
+ "upstream_dns_configured_in_file": "Configurato in %path%",
+ "upstream_dns_fastest_addr": "Indirizzo IP più veloce",
+ "upstream_dns_parallel_requests": "Richieste parallele",
+ "upstream_dns_placeholder": "Indirizzi IP, sdns://, quic://, https://, h3:// o tls://",
+ "upstream_dns_servers_title": "Server DNS upstream",
+ "upstream_enable_reverse_lookup_desc": "Risolvi inversamente gli indirizzi IP dei client nei rispettivi nomi host inviando query PTR ai resolver appropriati: server DNS privati per i client locali oppure server upstream per i client con indirizzi IP pubblici",
+ "upstream_enable_reverse_lookup_title": "Abilita la risoluzione inversa degli indirizzi IP dei client",
+ "upstream_example_multiple_upstreams_reserved": "%value%: più upstream per domini specifici ",
+ "upstream_example_tcp": "%ipv4%,%ipv6%: DNS normale (over TCP)",
+ "upstream_example_tcp_hostname": "%value%: DNS normale (over TCP, nome host)",
+ "upstream_example_tcp_port": "%ipv4%,%ipv6%: DNS normale (over TCP, con porta)",
+ "upstream_example_udp": "%ipv4%,%ipv6%: DNS normale (over UDP)",
+ "upstream_example_udp_hostname": "%value%: DNS normale (over UDP, nome host)",
+ "upstream_example_udp_port": "%ipv4%,%ipv6%: DNS normale (over UDP, con porta)",
+ "upstream_example_upstream_comment": "%value%: un commento.",
+ "upstream_example_upstream_doh": "%value%: crittografato DNS-over-HTTPS ",
+ "upstream_example_upstream_doh3": "%value%: DNS-over-HTTPS crittografato con HTTP/3 forzato e nessun riserva a HTTP/2",
+ "upstream_example_upstream_doq": "%value%: crittografato DNS-over-QUIC ",
+ "upstream_example_upstream_dot": "%value%: crittografato DNS-over-TLS ",
+ "upstream_example_upstream_reserved": "%value%: un upstream per domini specifici ",
+ "upstream_example_upstream_sdns": "%value%: DNS Stamps per DNSCrypt o DNS-over-HTTPS resolver",
+ "upstream_examples_title": "Esempi",
+ "upstream_fallback_title": "Server DNS di riserva, uno per riga",
+ "upstream_fallback_title_faq": "Server DNS di riserva da utilizzare quando i server DNS a monte non sono disponibili. Utilizzare la stessa sintassi utilizzata per i server DNS upstream",
+ "upstream_private_ptr_resolvers_desc": "Risolvi le richieste PTR, SOA e NS per domini ARPA contenenti indirizzi IP privati tramite server upstream privati, DHCP, /etc/hosts, e cosi via. Se disabilitato, AdGuard Home risponderà a tutte queste richieste con NXDOMAIN",
+ "upstream_private_ptr_resolvers_title": "Utilizza dei resolver rDNS privati",
+ "upstream_ptr": "Server DNS inversi privati, uno per riga",
+ "upstream_ptr_faq_1": "AdGuard Home utilizza questi server DNS per le richieste private PTR, SOA e NS. Le richieste private provengono da client con un indirizzo IP privato e richiedono un dominio ARPA che contiene una sottorete da un intervallo di indirizzi IP privati, come %value%",
+ "upstream_ptr_faq_2": "Se questo campo è vuoto, verranno utilizzati i resolver DNS predefiniti del sistema operativo, ad eccezione degli indirizzi IP di AdGuard Home",
+ "upstream_ptr_faq_3": "Per impostazione predefinita, AdGuard Home utilizza i seguenti resolver DNS inversi: %value_1% e %value_2%",
+ "upstream_timeout": "Timeout upstream, in secondi",
+ "upstream_timeout_faq": "Tempo di attesa per ricevere una risposta dal server upstream",
+ "upstream_timeout_placeholder": "Inserisci la durata del timeout del server upstream",
+ "upstreams": "Upstream",
+ "url_label": "URL",
+ "use_saved_key": "Usa una chiave privata esistente",
+ "user_rules": "Regole utente",
+ "user_rules_add_to_allowlist": "Aggiungi alla lista bianca",
+ "user_rules_allow_service": "Consenti il servizio",
+ "user_rules_browsing_security_disabled": "Sicurezza di navigazione disabilitata",
+ "user_rules_check_button": "Controlla",
+ "user_rules_check_client_label": "Identificatore client (nome, ClientID o indirizzo IP)",
+ "user_rules_check_client_placeholder": "Inserisci identificatore client",
+ "user_rules_check_dns_record_type_label": "Tipo di record DNS",
+ "user_rules_check_hostname_label": "Nome host o nome di dominio",
+ "user_rules_check_hostname_placeholder": "Inserisci destinazione",
+ "user_rules_check_title": "Verifica il filtro dei domini a livello globale o per un singolo client",
+ "user_rules_cname": "CNAME: %cname%",
+ "user_rules_disable_browsing_security": "Disattiva la Sicurezza di navigazione",
+ "user_rules_disable_filter": "Disattiva filtro",
+ "user_rules_disable_parental_control": "Disattiva Controllo Parentale",
+ "user_rules_disable_safe_search": "Disattiva la Ricerca sicura",
+ "user_rules_dns_record_type_placeholder": "Seleziona il tipo di registrazione DNS",
+ "user_rules_dns_rewrite_removed": "Regola rimossa dalla riscrittura DNS",
+ "user_rules_domain": "Dominio: %value%",
+ "user_rules_domain_blocked": "Il dominio è bloccato",
+ "user_rules_domain_is_allowed": "Il dominio è consentito",
+ "user_rules_domain_is_processed": "Il dominio è trattato",
+ "user_rules_edit_dns_rewrite": "Modifica della riscrittura DNS",
+ "user_rules_example_block": "||%domain% Blocca l'accesso a %domain% e a tutti i suoi sottodomini",
+ "user_rules_example_comment": "! Questo è un commento Per commentare, inizia la riga con ! o #",
+ "user_rules_example_comment_2": "# Anche questo è un commento Per commentare, inizia la riga con ! o #",
+ "user_rules_example_regex": "/REGEX/ Blocca l'accesso ai domini che corrispondono all'espressione regolare specificata",
+ "user_rules_example_response": "127.0.0.1 %domain% Rispondi con 127.0.0.1 solo per %domain%, non per i suoi sottodomini",
+ "user_rules_example_unblock": "@@||%domain% Sblocca l'accesso a %domain% e a tutti i suoi sottodomini",
+ "user_rules_filter_was_disabled": "%value% è stato disabilitato",
+ "user_rules_ip": "Indirizzi IP: %ip%",
+ "user_rules_learn_more": "Scopri di più su come creare le tue liste di host.",
+ "user_rules_no_rules_matched": "Risultato: Nessuna regola corrisponde",
+ "user_rules_parental_control_disabled": "Controllo parentale disabilitato",
+ "user_rules_placeholder": "Questo è un segnaposto",
+ "user_rules_reason": "Motivo: %reason%",
+ "user_rules_reason_allowed_by": "Consentito da %source%",
+ "user_rules_reason_blocked_by": "Bloccato da %source%",
+ "user_rules_reason_filtered_by": "Filtrato da %source%",
+ "user_rules_redirected_to": "Reindirizzato a: %value%",
+ "user_rules_remove_dns_rewrite": "Rimuovi la riscrittura DNS",
+ "user_rules_rewrite_rule_is_applied": "Regola di riscrittura applicata",
+ "user_rules_rewritten_to": "Riscritto a: %value%",
+ "user_rules_rule": "Regola: %rule%",
+ "user_rules_rule_added_to_custom_filtering_rules": "Regola aggiunta alle regole di filtraggio personalizzate",
+ "user_rules_rule_removed": "Regola rimossa dalle regole di filtraggio personalizzate",
+ "user_rules_safe_search_disabled": "Ricerca sicura disabilitata",
+ "user_rules_service": "Servizio: %service%",
+ "user_rules_service_allowed": "Il servizio %value% è stato consentito",
+ "user_rules_source": "Fonte: %value%",
+ "user_rules_source_allowlist": "Lista consentiti: %value%",
+ "user_rules_source_blocklist": "Lista blocchi: %value%",
+ "user_rules_status": "Stato: %reason%",
+ "username_label": "Nome utente",
+ "username_placeholder": "Inserisci nome utente",
+ "validated_with_dnssec": "Verificato con DNSSEC",
+ "wednesday": "Mercoledì",
+ "whois": "WHOIS",
+ "yes": "Sì",
+ "yes_disable": "Sì, disabilita",
+ "yes_reset": "Sì, ripristina"
+}
diff --git a/client_v2/src/__locales/ja.json b/client_v2/src/__locales/ja.json
new file mode 100644
index 00000000000..041a10a60fc
--- /dev/null
+++ b/client_v2/src/__locales/ja.json
@@ -0,0 +1,485 @@
+{
+ "access_settings_title": "アクセス設定",
+ "actions_table_header": "操作",
+ "add": "追加する",
+ "add_allowlist": "許可リストに追加する",
+ "add_blocklist": "ブロックリストに追加する",
+ "add_persistent_client": "永続クライアントとして追加する",
+ "add_to_allowlist": "ホワイトリストに追加する",
+ "ads_blocked": "広告ブロック数",
+ "ads_blocked_card": "広告ブロック数",
+ "adult_websites_blocked_tooltip": "ブロックされたアダルトウェブサイトの数",
+ "all_lists_up_to_date_toast": "すべてのリストは既に最新です",
+ "allowed": "ホワイトリストに追加済",
+ "allowlist_add": "許可リストに追加する",
+ "allowlist_edit": "許可リストの編集",
+ "allowlists": "ホワイトリスト",
+ "allowlists_title": "ホワイトリスト",
+ "apply": "適用する",
+ "autofix_warning_list": "以下のタスクを実行します:
システムのDNSStubListenerを無効化
DNSサーバーのアドレスを127.0.0.1に設定
/etc/resolv.conf のシンボリックリンクの参照先を /run/systemd/resolve/resolv.conf に置き換え
DNSStubListener を停止(systemd-resolvedサービスを再読み込み)
",
+ "autofix_warning_result": "その結果、システムからのすべてのDNSリクエストは、デフォルトでAdGuard Homeによって処理されます。",
+ "autofix_warning_text": "「修正」をクリックすると、AdGuardHomeはAdGuardHome DNSサーバを使用するようにシステムを構成します。",
+ "average_time_processing": "平均処理時間",
+ "average_upstream_response_time": "アップストリームの平均応答時間",
+ "back": "戻る",
+ "block": "ブロックする",
+ "block_for_this_client_only": "このクライアントに対してのみブロックする",
+ "blocked": "ブロック済",
+ "blocked_adult_websites": "ペアレンタルコントロールによりブロック済み",
+ "blocked_by_cname_or_ip": "CNAMEもしくはIPアドレスによってブロック済み",
+ "blocked_service": "ブロックするサービス",
+ "blocked_services": "ブロックするサービス",
+ "blocked_threats": "ブロック済み脅威数",
+ "blocked_threats_chart": "ブロック済み脅威数",
+ "blocklist_add_manual": "手動で追加する",
+ "blocklist_edit": "ブロックリストの編集",
+ "blocklist_homepage": "ホームページ",
+ "blocklist_url_file_path": "URLまたはファイルパス",
+ "bootstrap_dns": "ブートストラップDNSサーバ",
+ "browse": "参照",
+ "cache_cleared": "DNSキャッシュのクリア完了です。",
+ "cache_config_clear": "キャッシュをクリアする",
+ "cache_config_optimistic": "Optimistic cashing (オプティミスティック・キャッシュ)",
+ "cache_config_ttl_validation": "最小キャッシュTTL上書き値は最大値以下にする必要があります",
+ "cache_enabled": "キャッシュを有効にする",
+ "cancel": "キャンセル ",
+ "cancel_btn": "キャンセル ",
+ "category_tooltip": "カテゴリ",
+ "changes_saved_success": "変更内容が保存されました。",
+ "check_dhcp_servers": "DHCPサーバをチェックする",
+ "check_not_found": "フィルタ一覧には見つかりません",
+ "check_updates_btn": "アップデートを確認する",
+ "clear_btn": "クリア",
+ "clear_query_log": "クエリログをクリア",
+ "client_add": "クライアントを追加する",
+ "client_details": "クライアントの詳細",
+ "client_id": "ClientID(クライアントID)",
+ "client_id_placeholder": "ClientIDを入力してください",
+ "client_identifier": "識別子",
+ "client_settings": "クライアント設定",
+ "client_table_header": "クライアント",
+ "clients_add_identifier": "識別子を追加する",
+ "clients_add_placeholder": "クライアント名を入力してください",
+ "clients_desc": "AdGuard Homeに接続済みデバイス用のルール",
+ "clients_dns_cache_size": "DNSキャッシュサイズ(バイト単位)",
+ "clients_dns_cache_size_placeholder": "数字を使用してください",
+ "clients_not_found": "クライアント情報はありません",
+ "clients_protection": "AdGuardによる保護",
+ "clients_tags": "タグ",
+ "clients_use_global_settings": "グローバル設定を使用する",
+ "close": "閉じる",
+ "confirm": "確認",
+ "confirm_static_ip": "AdGuard Homeは、%ip%を静的IPアドレスとして設定します。よろしいですか?",
+ "copied": "クリップボードにコピー済み",
+ "copy": "コピー",
+ "copy_to_clipboard": "クリップボードにコピー",
+ "custom_filtering_rules": "カスタム・フィルタリングルール",
+ "dark_theme": "ダークテーマ",
+ "dashboard": "ダッシュボード",
+ "delete_btn": "削除する",
+ "delete_table_action": "削除する",
+ "dhcp_add_static_lease": "静的割り当てを追加する",
+ "dhcp_config_saved": "DHCP構成が無事に保存されました。",
+ "dhcp_edit_static_lease": "静的リースを編集",
+ "dhcp_form_gateway_input": "ゲートウェイIP",
+ "dhcp_form_subnet_input": "サブネットマスク",
+ "dhcp_leases_not_found": "DHCP割当はありません",
+ "dhcp_leases_title": "DHCP割り当て",
+ "dhcp_new_static_lease": "新規静的割り当て",
+ "dhcp_reset_leases": "すべてのリースをリセットする",
+ "dhcp_reset_leases_success": "すべてのDHCPリース(割り当て)がリセット完了しました。",
+ "dhcp_settings": "DHCP設定",
+ "dhcp_static_leases_not_found": "DHCP静的割り当てはありません",
+ "dhcp_table_hostname": "ホスト名",
+ "dhcp_table_ip_address": "IPアドレス",
+ "disable": "無効にする",
+ "disable_protection_btn": "保護を無効にする",
+ "disabled_dhcp": "DHCPサーバを無効にしました",
+ "disallow_this_client": "このクライアントを拒否する",
+ "dns_access_settings_title": "アクセス設定",
+ "dns_allowed_clients": "許可されたクライアント",
+ "dns_blocking_mode": "ブロックモード",
+ "dns_blocking_mode_custom_ip": "カスタムIPアドレス",
+ "dns_blocking_mode_default": "デフォルト",
+ "dns_blocking_mode_ipv4_label": "ブロック中のIPv4",
+ "dns_blocking_mode_ipv6_label": "ブロック中のIPv6",
+ "dns_blocking_mode_nxdomain": "NXDOMAIN",
+ "dns_blocking_mode_refused": "REFUSED",
+ "dns_blocking_mode_title": "ブロックモード",
+ "dns_bootstrap_dns_placeholder": "IPアドレス",
+ "dns_bootstrap_dns_title": "ブートストラップDNSサーバ",
+ "dns_bootstrap_servers": "ブートストラップDNSサーバ",
+ "dns_cache_config": "DNSキャッシュ設定",
+ "dns_cache_size": "キャッシュサイズ",
+ "dns_cache_size_label": "DNSキャッシュサイズ(バイト単位)",
+ "dns_cache_size_title": "キャッシュサイズ",
+ "dns_clear_cache": "キャッシュをクリアする",
+ "dns_clear_cache_cancel": "キャンセル ",
+ "dns_clear_cache_confirm": "クリアする",
+ "dns_clear_cache_yes": "クリアする",
+ "dns_config": "DNSサーバ設定",
+ "dns_disallowed_clients": "拒否するクライアント",
+ "dns_disallowed_clients_title": "拒否するクライアント",
+ "dns_disallowed_domains": "拒否するドメイン",
+ "dns_disallowed_domains_title": "拒否するドメイン",
+ "dns_edns_custom_placeholder": "IPアドレスを入力してください",
+ "dns_fallback_dns_label": "フォールバックDNSサーバー(1行に1つ):",
+ "dns_fallback_dns_placeholder": "IPアドレス、sdns://、quic://、https://、h3://、または tls://",
+ "dns_fallback_dns_title": "フォールバックDNSサーバー",
+ "dns_fallback_servers": "フォールバックDNSサーバー",
+ "dns_optimistic_caching": "Optimistic cashing (オプティミスティック・キャッシュ)",
+ "dns_over_https": "DNS-over-HTTPS",
+ "dns_over_quic": "DNS-over-QUIC",
+ "dns_over_tls": "DNS-over-TLS",
+ "dns_override_max_ttl": "最大TTLの上書き(秒単位)",
+ "dns_override_max_ttl_title": "最大TTLの上書き(秒単位)",
+ "dns_override_min_ttl": "最小TTLの上書き(秒単位)",
+ "dns_override_min_ttl_title": "最小TTLの上書き(秒単位)",
+ "dns_privacy": "DNSプライバシー",
+ "dns_private_reverse_servers_placeholder": "IPアドレス、sdns://、quic://、https://、h3://、または tls://",
+ "dns_private_reverse_servers_title": "プライベートリバースDNSサーバー",
+ "dns_rate_limit": "頻度制限",
+ "dns_rate_limit_allowlist_placeholder": "IPアドレス",
+ "dns_rate_limit_placeholder": "頻度制限を入力してください",
+ "dns_rate_limit_title": "頻度制限",
+ "dns_rewrites": "DNS書き換え",
+ "dns_server_addresses": "サーバーアドレス",
+ "dns_server_addresses_label": "サーバーアドレス(1行に1つ):",
+ "dns_server_addresses_placeholder": "IPアドレス、sdns://、quic://、https://、h3://、または tls://",
+ "dns_server_addresses_title": "サーバーアドレス",
+ "dns_server_config_title": "DNSサーバ設定",
+ "dns_settings": "DNS",
+ "dns_test_upstreams": "アップストリームをテストする",
+ "dns_upstream_servers_title": "アップストリームDNSサーバー",
+ "dns_upstream_timeout": "Upstream timeout(アップストリームタイムアウト)",
+ "dns_upstream_timeout_title": "Upstream timeout(アップストリームタイムアウト)",
+ "dns_upstream_validation_invalid": "フォーマットが無効です",
+ "domain": "ドメイン",
+ "domain_or_client": "ドメインまたはクライアント",
+ "download_mobileconfig": "設定ファイルをダウンロードする",
+ "edit_table_action": "編集",
+ "enable": "有効にする",
+ "enabled_dhcp": "DHCPサーバを有効にしました",
+ "encryption_certificate_has_issues": "証明書に問題があります",
+ "encryption_certificate_path": "証明書のパスを入力してください",
+ "encryption_certificates": "証明書",
+ "encryption_certificates_desc": "暗号化を使用するには、ドメインに対して有効なSSL証明書チェーンを提供する必要があります。無料のSSL証明書はletsencrypt.org から取得するか、信頼できる認証局から購入できます。",
+ "encryption_certificates_input": "ここにPEM形式の証明書をコピー/ペーストしてください。",
+ "encryption_certificates_source_content": "証明書の内容をペーストする",
+ "encryption_certificates_source_path": "証明書のパスを設定する",
+ "encryption_chain_invalid": "証明書チェーンが無効です",
+ "encryption_chain_valid": "証明書チェーンは有効です。",
+ "encryption_config_saved": "暗号化構成が保存されました。",
+ "encryption_confirm_clear": "暗号化設定のリセット",
+ "encryption_confirm_clear_desc": "すべての暗号化設定がデフォルト値にリセットされます。",
+ "encryption_disable_plain_dns": "プレーンDNSを無効にしますか?",
+ "encryption_disable_plain_dns_desc": "暗号化DNSに対応していないデバイスは正常に動作しない可能性があります。",
+ "encryption_doq": "DNS-over-QUIC ポート",
+ "encryption_doq_tooltip": "このポートはDNS-over-QUICサーバーの稼働に使用されます。",
+ "encryption_dot": "DNS-over-TLS ポート",
+ "encryption_dot_tooltip": "このポートはDNS-over-TLSサーバーの稼働に使用されます。",
+ "encryption_encrypted_dns": "暗号化DNS",
+ "encryption_encrypted_dns_desc": "AdGuard Home の管理インターフェースは HTTPS を使用します。DNS サーバーは DNS-over-HTTPS、DNS-over-TLS、DNS-over-QUIC に対応しています。",
+ "encryption_expire": "有効期限: %value%",
+ "encryption_force_redirect": "HTTPからHTTPSへ自動的にリダイレクトする",
+ "encryption_hostnames": "ホスト名: %value%",
+ "encryption_https": "HTTPS ポート",
+ "encryption_https_tooltip": "このポートはAdGuard Homeの管理インターフェースへのHTTPSアクセスを提供します。また、'/dns-query' の場所でDNS-over-HTTPSも提供します。",
+ "encryption_issuer": "発行元: %value%",
+ "encryption_key": "秘密鍵",
+ "encryption_key_input": "ここに証明書のためのPEM形式の秘密鍵をコピー/ペーストしてください。",
+ "encryption_key_invalid": "秘密鍵が無効です",
+ "encryption_key_source_content": "秘密鍵の内容をペーストする",
+ "encryption_key_source_path": "秘密鍵ファイルを設定する",
+ "encryption_key_type": "暗号化アルゴリズム: %value%",
+ "encryption_key_valid": "秘密鍵は有効です",
+ "encryption_plain_dns": "通常のDNS",
+ "encryption_plain_dns_error": "プレーンDNSを無効にするには、暗号化DNSプロトコルを少なくとも 1 つ有効にしてください",
+ "encryption_private_key_path": "秘密鍵のパスを入力してください",
+ "encryption_server": "サーバー名",
+ "encryption_server_enter": "ドメイン名を入力してください",
+ "encryption_server_tooltip_1": "サーバー名が指定されていると、AdGuard Home は ClientID を検出し、DDRクエリに応答し、追加の接続検証を実行します。指定がない場合、これらの機能は無効になります。",
+ "encryption_server_tooltip_2": "※サーバー名は証明書内のDNS名と一致している必要があります。",
+ "encryption_subject": "Subject: %value%",
+ "encryption_title": "暗号化",
+ "enter_cache_size": "キャッシュサイズ(バイト単位)を入力してください",
+ "enter_ip_address_placeholder": "IPアドレスを入力してください",
+ "error": "エラー",
+ "fallback_dns": "フォールバックDNSサーバー",
+ "filter_added_successfully": "ブロックリストを追加しました: %value% ",
+ "filter_added_successfully_allowlist": "ホワイトリストを追加しました: %value% ",
+ "filter_added_successfully_more": "ブロックリストを追加しました: %value% と他%more%件",
+ "filter_category_general": "General",
+ "filter_category_general_desc": "ほとんどのデバイスにて追跡と広告をブロックするリストです。",
+ "filter_category_other": "その他",
+ "filter_category_other_desc": "その他のブロックリストです。",
+ "filter_category_regional": "地域別",
+ "filter_category_regional_desc": "それぞれの地域の広告と追跡サーバをターゲットするリストです。",
+ "filter_category_security": "マルウェアやフィッシング詐欺をブロック",
+ "filter_category_security_desc": "マルウェア、フィッシング、詐欺ドメインをブロックするために設計された専用リストです。",
+ "filter_removed_successfully": "ブロックリストを削除しました: %value% ",
+ "filter_removed_successfully_allowlist": "ホワイトリストを削除しました: %value% ",
+ "filters": "フィルタ",
+ "forgot_password": "パスワードをお忘れですか?",
+ "form_enter_blocked_response_ttl": "ブロック済み応答のTTL(秒単位)を入力してください",
+ "form_error_answer_format": "結果のフォーマットが無効です",
+ "form_error_client_id_format": "ClientIDには、数字、小文字、ハイフン以外は使用できません",
+ "form_error_domain_format": "ドメイン名のフォーマットが無効です",
+ "form_error_equal": "同じ値であってはなりません",
+ "form_error_format": "フォーマットが無効です",
+ "form_error_gateway_ip": "リースはゲートウェイのIPアドレスになっていることができません",
+ "form_error_ip4_format": "IPv4アドレスが無効です",
+ "form_error_ip6_format": "IPv6アドレスが無効です",
+ "form_error_ip_format": "IPアドレスが無効です",
+ "form_error_mac_format": "MACアドレスが無効です",
+ "form_error_password": "パスワードが一致しません",
+ "form_error_port": "有効なポート番号を入力してください",
+ "form_error_port_range": "80〜65535 の範囲内でポート番号を入力してください",
+ "form_error_port_unsafe": "これは不安全なポートです",
+ "form_error_server_name": "サーバー名が無効です",
+ "form_error_url_format": "有効なURLまたはファイルパスを入力してください",
+ "friday": "金曜日",
+ "gateway_or_subnet_invalid": "サブネットマスクが無効です",
+ "general_statistics": "全般的な統計",
+ "ignore_domains_desc_log": "これらのルールに一致するリクエストは「クエリログ」に表示されません。",
+ "ignore_domains_desc_stats": "これらのルールに一致したリクエストは統計に含まれません。",
+ "ignore_domains_title": "無視するドメイン",
+ "inactivity_schedule_add": "スケジュールを追加する",
+ "inactivity_schedule_all_day": "まる一日",
+ "inactivity_schedule_edit": "スケジュールを編集する",
+ "install_auth_confirm": "パスワードの確認",
+ "install_auth_password": "パスワード",
+ "install_auth_password_enter": "パスワードを入力してください",
+ "install_auth_username": "ユーザー名",
+ "install_auth_username_enter": "ユーザ名を入力してください",
+ "install_saved": "保存に成功しました",
+ "install_settings_all_interfaces": "すべてのインターフェイス",
+ "install_settings_port": "ポート",
+ "install_static_configure": "動的IPアドレス %ip%が使用されていることをAdGuard Homeが検出しました。このアドレスをあなたの静的アドレスとして設定しますか?",
+ "install_static_error": "AdGuard Homeは、このネットワークインターフェースを自動構成することはできません。手動で行う方法に関する取扱説明書を探してください。",
+ "install_static_ok": "良いニュースです! 既に静的IPアドレスで構成されています",
+ "install_step": "手順",
+ "install_submit_title": "おめでとうございます!",
+ "ip_address": "IPアドレス",
+ "known_tracker": "既知のトラッカー",
+ "language": "言語",
+ "light_theme": "ライトテーマ",
+ "list_updated": "%count%件のリストが更新されました | %count%件のリストが更新されました",
+ "login": "ログイン",
+ "logout": "ログアウト",
+ "make_static": "静的(static)にする",
+ "milliseconds_abbreviation": "ms",
+ "monday": "月曜日",
+ "name": "フィルタ名",
+ "name_label": "フィルタ名",
+ "name_table_header": "フィルタ名",
+ "name_tooltip": "フィルタ名",
+ "next": "次へ",
+ "no": "No",
+ "none_text": "なし",
+ "nothing_found": "何も見つかりません",
+ "notify_undo": "元に戻す",
+ "notify_updated": "最終更新",
+ "open_dashboard": "ダッシュボードを開く",
+ "parental_control": "ペアレンタルコントロール",
+ "parental_group_cdn": "CDN",
+ "parental_group_development": "ソフトウェア開発",
+ "parental_group_gaming": "ゲーム",
+ "parental_group_hosting": "ウェブホスティング",
+ "parental_group_privacy": "プライバシーツール",
+ "parental_group_shopping": "ショッピング",
+ "parental_group_social": "SNS",
+ "parental_group_social_network": "SNS",
+ "parental_group_software": "ソフトウェア開発",
+ "parental_group_streaming": "ストリーミングサービス対応",
+ "password_label": "パスワード",
+ "password_placeholder": "パスワードを入力してください",
+ "plain_dns": "通常のDNS",
+ "port_53_faq_link": "多くの場合、ポート53は \"DNSStubListener\" または \"systemd-resolved\" サービスによって利用されています。これを解決する方法については、この手順 をお読みください。",
+ "privacy_policy": "プライバシーポリシー",
+ "processing_time_ms": "%value% ms",
+ "protection": "AdGuardによる保護",
+ "protocol": "暗号化プロトコル",
+ "query_log_allowed": "ホワイトリストに追加済",
+ "query_log_blocked": "ブロック済",
+ "query_log_blocked_services": "ブロックするサービス",
+ "query_log_blocked_threats": "ブロック済み脅威数",
+ "query_log_custom_filtering_rules": "カスタム・フィルタリングルール",
+ "query_log_nothing_available": "何も見つかりません",
+ "query_log_processed": "処理済み",
+ "query_log_retention": "ログローテーション",
+ "query_log_rewritten": "書換",
+ "query_log_safe_search": "セーフサーチ",
+ "query_log_strict_search": "完全一致検索には二重引用符を使用します",
+ "rate_limit_subnet_len_ipv4_error": "IPv4 サブネットプレフィックス長は0〜32の範囲内である必要があります。",
+ "rate_limit_subnet_len_ipv6_error": "IPv6 サブネットのプレフィックス長は0〜128の範囲内である必要があります。",
+ "refresh_btn": "最新にする",
+ "refresh_statics": "統計データを最新にする",
+ "release_notes": "リリースノート",
+ "remove": "削除",
+ "report_an_issue": "問題を報告する",
+ "request_table_header": "リクエスト",
+ "requests_table_header": "リクエスト数",
+ "reset": "初期化",
+ "reset_settings": "設定をリセットする",
+ "reset_settings_confirm": "リセット",
+ "response_details": "応答の詳細",
+ "response_time": "応答時間",
+ "result": "結果",
+ "rewrite_add": "DNS書き換え情報を追加する",
+ "rewrite_edit": "DNS rewrite を編集する",
+ "rewrite_hosts_applied": "hostsファイルのルールによって書き換え済み",
+ "rewrite_settings_updated": "DNS rewrite設定の更新が正常に完了しました。",
+ "rewrite_updated": "DNS rewrite を更新完了しました。",
+ "rewritten": "書換",
+ "rules_label": "ルール",
+ "safe_browsing": "セーフブラウジング",
+ "safe_search": "セーフサーチ",
+ "saturday": "土曜日",
+ "save": "保存",
+ "save_btn": "保存",
+ "search_placeholder": "検索",
+ "selected": "%value% が選択されています",
+ "server_config_blocking_mode": "ブロックモード",
+ "server_config_blocking_mode_ipv4": "ブロック中のIPv4",
+ "server_config_blocking_mode_ipv6": "ブロック中のIPv6",
+ "server_config_custom_ip": "カスタムIPアドレス",
+ "server_config_default": "デフォルト",
+ "server_config_disable_ipv6": "IPv6アドレスの解決を無効にする",
+ "server_config_dnssec_enable": "DNSSECを有効にする",
+ "server_config_edns_enable": "EDNSクライアントサブネットを有効にする",
+ "server_config_nxdomain": "NXDOMAIN",
+ "server_config_rate_limit_placeholder": "頻度制限を入力してください",
+ "server_config_rate_limit_whitelist_faq": "rate limiting(レート制限)の対象から外すIPアドレスを指定できます。",
+ "server_config_refused": "REFUSED",
+ "server_config_subnet_len_ipv4": "IPv4 アドレスのサブネットプレフィックス長",
+ "server_config_subnet_len_ipv4_faq": "rate limiting(レート制限)に使用される IPv4 アドレスのサブネットプレフィックス長です。デフォルト値は 24 です。",
+ "server_config_subnet_len_ipv6": "IPv6 アドレスのサブネットプレフィックス長",
+ "server_config_subnet_len_ipv6_faq": "rate limiting(レート制限)に使用される IPv6 アドレスのサブネットプレフィックス長です。デフォルト値は 56 です。",
+ "set_static_ip": "静的IPアドレスを設定する",
+ "settings": "設定",
+ "settings_anonymize_client_ip": "クライアントIPアドレスを匿名化する",
+ "settings_anonymize_client_ip_desc": "IPアドレスの一部をゼロに置き換えます。",
+ "settings_browsing_security": "ブラウジング・セキュリティ",
+ "settings_browsing_security_desc": "悪意のあるドメインおよびフィッシングドメインのデータベースに含まれるドメインへのアクセスをブロックします。",
+ "settings_confirm_clear_query_log": "クエリログをクリアしますか?",
+ "settings_confirm_clear_query_log_desc": "これにより、クエリログ からすべてのリクエストが削除されます。統計情報は引き続き利用できます。",
+ "settings_confirm_clear_statistics": "統計をクリア(初期化)しますか?",
+ "settings_confirm_clear_statistics_desc": "これにより統計がすべて消去されます。クエリログ は引き続き利用できます。",
+ "settings_confirm_decrease_log_rotation_interval": "ログローテーション間隔を減らしますか?",
+ "settings_confirm_decrease_log_rotation_interval_desc": "%value%前以上のログはすべて削除されます。",
+ "settings_confirm_decrease_stats_rotation_interval": "統計の保持間隔を減らしますか?",
+ "settings_confirm_decrease_stats_rotation_interval_desc": "%value%前以上の統計はすべて削除されます。",
+ "settings_custom": "カスタム",
+ "settings_days": "%count%日間 | %count%日間",
+ "settings_domain_names": "ドメイン名(1行に1つ):",
+ "settings_filter_requests": "リクエストのフィルタリング",
+ "settings_filter_requests_desc": "フィルタリングルールの設定は、ブロックリスト 、ホワイトリスト 、およびユーザールール で行えます。",
+ "settings_filtering_and_security": "フィルタリングとセキュリティ",
+ "settings_general_short": "General",
+ "settings_global": "グローバル",
+ "settings_hours": "%count%時間 | %count%時間",
+ "settings_log_dns_requests": "DNSリクエストをログに記録する",
+ "settings_log_rotation_hours": "ログローテーション(時間単位)",
+ "settings_notify_changes_saved": "変更内容が保存されました。",
+ "settings_notify_query_log_cleared": "クエリログがクリアされました",
+ "settings_notify_statistics_cleared": "統計がクリアされました。",
+ "settings_parental_control": "ペアレンタルコントロール",
+ "settings_parental_control_desc": "子どもに不適切なコンテンツを含むウェブサイトをブロックします。",
+ "settings_rotation_placeholder": "数字を使用してください",
+ "settings_rule_syntax": "ルールの構文",
+ "settings_safe_search": "セーフサーチ",
+ "settings_safe_search_desc": "検索結果内の不適切なコンテンツをブロックします。",
+ "settings_statistics": "統計",
+ "settings_statistics_clear": "統計をクリア",
+ "settings_statistics_desc": "ダッシュボードにリクエストの統計およびクライアントの統計を表示します。",
+ "settings_statistics_retention": "統計保持",
+ "settings_statistics_retention_hours": "統計の保持期間(時間単位)",
+ "settings_tooltip_domain_names": "ドメイン名、ワイルドカード、またはフィルタリングルール",
+ "settings_tooltip_examples": "【例】",
+ "settings_yes_clear": "クリアする",
+ "settings_yes_decrease": "減らす",
+ "setup_devices_router_title": "ルータ",
+ "setup_dns_privacy_ioc_mac": "iOS と macOS での設定",
+ "setup_dns_privacy_other_title": "その他の機能",
+ "show_blocked_responses": "ブロック済",
+ "show_processed_responses": "処理済み",
+ "source_label": "ソース",
+ "source_tooltip": "ソース",
+ "stats_adult": "ブロックされたアダルトウェブサイト",
+ "stats_query_domain": "最も問合せされたドメイン",
+ "status_table_header": "ステータス",
+ "sunday": "日曜日",
+ "system_host_files": "システムのhostsファイル",
+ "system_theme": "システムテーマ",
+ "table_client": "クライアント",
+ "tags_title": "タグ",
+ "test_upstreams": "アップストリームをテストする",
+ "threats_blocked": "脅威ブロック数",
+ "thursday": "木曜日",
+ "time_table_header": "時刻",
+ "top_blocked_domains": "最もブロックされたドメイン",
+ "top_clients": "トップクライアント",
+ "top_upstreams": "上位のアップストリーム",
+ "total": "合計",
+ "try_again": "再試行する",
+ "tuesday": "火曜日",
+ "unavailable_dhcp": "DHCPは利用できません",
+ "unavailable_dhcp_desc": "AdGuard Homeはお使いのOS上でDHCPサーバを実行できません。",
+ "unblock": "ブロックを解除する",
+ "unknown_filter": "不明なフィルタ {%filterId%",
+ "update_button": "更新する",
+ "update_filters_disable": "無効にする",
+ "update_filters_interval_custom": "カスタム",
+ "update_filters_interval_daily": "毎日",
+ "update_filters_interval_hourly": "毎時",
+ "update_filters_title": "フィルタを自動更新する",
+ "updates_checked": "AdGuard Homeの新バージョンが利用可能です。",
+ "updates_version_equal": "AdGuard Homeは既に最新です",
+ "upstream": "アップストリーム",
+ "upstream_dns": "アップストリームDNSサーバー",
+ "upstream_dns_addresses": "サーバーアドレス(1行に1つ):",
+ "upstream_dns_addresses_faq": "でアップストリームDNSサーバーの設定についての詳細はこちら をご確認ください。既知のDNSプロバイダの一覧はこちら にありますので、そこから選択できます。",
+ "upstream_dns_fastest_addr": "最速のIPアドレス",
+ "upstream_dns_parallel_requests": "並列リクエスト",
+ "upstream_dns_placeholder": "IPアドレス、sdns://、quic://、https://、h3://、または tls://",
+ "upstream_dns_servers_title": "アップストリームDNSサーバー",
+ "upstream_example_multiple_upstreams_reserved": "%value%: 特定のドメイン向け の複数のアップストリーム",
+ "upstream_example_tcp": "%ipv4%,%ipv6%: 通常のDNS(over TCP)",
+ "upstream_example_tcp_hostname": "%value%: 通常のDNS(over TCP, ホスト名)",
+ "upstream_example_tcp_port": "%ipv4%,%ipv6%: 通常のDNS(over TCP, ポートあり)",
+ "upstream_example_udp": "%ipv4%,%ipv6%: 通常のDNS(over UDP)",
+ "upstream_example_udp_hostname": "%value%: 通常のDNS(over UDP, ホスト名)",
+ "upstream_example_udp_port": "%ipv4%,%ipv6%: 通常のDNS(over UDP, ポートあり)",
+ "upstream_example_upstream_comment": "%value%: コメント",
+ "upstream_example_upstream_doh": "%value%: 暗号化されたDNS-over-HTTPS ",
+ "upstream_example_upstream_doh3": "%value%: HTTP/2へのフォールバックなしで、HTTP/3 を強制した暗号化DNS-over-HTTPS",
+ "upstream_example_upstream_doq": "%value%: 暗号化されたDNS-over-QUIC ",
+ "upstream_example_upstream_dot": "%value%: 暗号化されたDNS-over-TLS ",
+ "upstream_example_upstream_reserved": "%value%: 特定のドメイン向け アップストリーム",
+ "upstream_example_upstream_sdns": "%value%: DNSスタンプ (DNSCrypt または DNS-over-HTTPS リゾルバー用)",
+ "upstream_examples_title": "例",
+ "upstream_fallback_title": "フォールバックDNSサーバー(1行に1つ):",
+ "upstream_private_ptr_resolvers_title": "プライベートリバースDNSリゾルバを使用",
+ "upstreams": "アップストリーム",
+ "url_label": "URL",
+ "use_saved_key": "既存の秘密鍵を使用する",
+ "user_rules": "ユーザーフィルタ",
+ "user_rules_add_to_allowlist": "ホワイトリストに追加する",
+ "user_rules_check_button": "チェックする",
+ "user_rules_check_client_placeholder": "クライアント識別子を入力してください",
+ "user_rules_check_hostname_label": "ホスト名またはドメイン名",
+ "user_rules_disable_filter": "フィルタを無効にする",
+ "user_rules_disable_parental_control": "ペアレンタルコントロールを無効にする",
+ "user_rules_dns_record_type_placeholder": "DNSレコードタイプ(DNS record type)を選択",
+ "user_rules_edit_dns_rewrite": "DNS rewrite を編集する",
+ "user_rules_rewrite_rule_is_applied": "書き換えルールを適用済み",
+ "username_label": "ユーザー名",
+ "username_placeholder": "ユーザ名を入力してください",
+ "validated_with_dnssec": "DNSSECにて検証済",
+ "wednesday": "水曜日",
+ "whois": "WHOIS",
+ "yes": "Yes",
+ "yes_disable": "無効にする",
+ "yes_reset": "リセット"
+}
diff --git a/client_v2/src/__locales/ko.json b/client_v2/src/__locales/ko.json
new file mode 100644
index 00000000000..4223ea15d48
--- /dev/null
+++ b/client_v2/src/__locales/ko.json
@@ -0,0 +1,390 @@
+{
+ "access_settings_title": "접근 설정",
+ "actions_table_header": "가능한 동작",
+ "add": "추가",
+ "add_allowlist": "허용 목록 추가",
+ "add_blocklist": "차단 목록 추가",
+ "add_persistent_client": "저장된 클라이언트에 추가",
+ "add_to_allowlist": "허용 목록에 추가",
+ "ads_blocked": "차단된 광고",
+ "ads_blocked_card": "차단된 광고",
+ "adult_websites_blocked_tooltip": "차단된 성인 웹 사이트의 수",
+ "all_lists_up_to_date_toast": "모든 리스트가 이미 최신입니다",
+ "allowed": "허용됨",
+ "allowlist_add": "허용 목록 추가",
+ "allowlist_edit": "허용 목록 수정",
+ "apply": "적용",
+ "autofix_warning_result": "결과적으로 시스템의 모든 DNS 요청은 기본적으로 AdGuard Home에 의해 처리됩니다.",
+ "autofix_warning_text": "'수정'을 클릭하면 AdGuard Home이 AdGuard Home DNS 서버를 사용하도록 시스템을 설정합니다.",
+ "average_time_processing": "평균처리 시간",
+ "average_upstream_response_time": "평균 업스트림 응답 시간",
+ "back": "뒤로가기",
+ "block": "차단",
+ "block_for_this_client_only": "이 클라이언트에 대해서만 차단",
+ "blocked": "차단됨",
+ "blocked_by_cname_or_ip": "CNAME 또는 IP에 의해 차단됨",
+ "blocked_service": "차단된 서비스",
+ "blocked_services": "차단된 서비스",
+ "blocked_threats": "차단된 위협",
+ "blocked_threats_chart": "차단된 위협",
+ "blocklist_add_manual": "수동으로 추가",
+ "blocklist_edit": "차단 목록 수정",
+ "blocklist_homepage": "홈페이지",
+ "blocklist_url_file_path": "URL 또는 파일 경로",
+ "bootstrap_dns": "부트스트랩 DNS 서버",
+ "browse": "파일 탐색",
+ "cache_cleared": "DNS 캐시를 성공적으로 지웠습니다",
+ "cache_config_clear": "캐시 지우기",
+ "cache_config_optimistic": "옵티미스틱 캐시",
+ "cache_config_ttl_validation": "최소 캐시 TTL 값은 최대 값보다 이하여야 합니다",
+ "cache_enabled": "캐시 활성화",
+ "cancel": "취소",
+ "cancel_btn": "취소",
+ "category_tooltip": "카테고리",
+ "changes_saved_success": "변경사항이 저장되었습니다.",
+ "check_dhcp_servers": "DHCP 서버 체크",
+ "check_not_found": "필터 목록에서 찾을 수 없음",
+ "check_updates_btn": "업데이트 확인",
+ "clear_btn": "비우기",
+ "client_add": "클라이언트 추가",
+ "client_details": "클라이언트 정보",
+ "client_id": "ClientID",
+ "client_id_placeholder": "ClientID 입력",
+ "client_identifier": "식별자",
+ "client_settings": "클라이언트 설정",
+ "client_table_header": "클라이언트",
+ "clients_add_identifier": "식별자 추가",
+ "clients_add_placeholder": "클라이언트 이름 입력",
+ "clients_dns_cache_size": "DNS 캐시 크기(바이트)",
+ "clients_not_found": "클라이언트가 없습니다",
+ "clients_protection": "보호",
+ "clients_tags": "태그",
+ "clients_use_global_settings": "글로벌 설정 사용",
+ "close": "닫기",
+ "confirm": "확인",
+ "copied": "클립보드에 복사되었습니다.",
+ "copy": "복사",
+ "copy_to_clipboard": "클립보드에 복사",
+ "custom_filtering_rules": "커스텀 필터링 규칙",
+ "dark_theme": "다크 테마",
+ "dashboard": "대시보드",
+ "delete_btn": "삭제",
+ "delete_table_action": "삭제",
+ "dhcp_add_static_lease": "고정 임대 추가",
+ "dhcp_config_saved": "DHCP 구성이 성공적으로 저장되었습니다",
+ "dhcp_edit_static_lease": "고정 임대 수정",
+ "dhcp_form_gateway_input": "게이트웨이 IP",
+ "dhcp_form_subnet_input": "서브넷 마스크",
+ "dhcp_leases_not_found": "DHCP 임대를 찾을 수 없음",
+ "dhcp_leases_title": "DHCP 임대",
+ "dhcp_new_static_lease": "새 고정 임대",
+ "dhcp_reset_leases": "모든 임대 초기화",
+ "dhcp_reset_leases_success": "DHCP 임대 성공적으로 초기화됨",
+ "dhcp_settings": "DHCP 설정",
+ "dhcp_static_leases_not_found": "DHCP 고정 임대를 찾을 수 없음",
+ "dhcp_table_hostname": "호스트 이름",
+ "dhcp_table_ip_address": "IP 주소",
+ "disable": "비활성화",
+ "disable_protection_btn": "보호 끄기",
+ "disabled_dhcp": "DHCP 서버 비활성화됨",
+ "disallow_this_client": "클라이언트 거부",
+ "dns_access_settings_title": "접근 설정",
+ "dns_allowed_clients": "허용된 클라이언트",
+ "dns_blocking_mode": "차단 모드",
+ "dns_blocking_mode_custom_ip": "사용자 지정 IP주소",
+ "dns_blocking_mode_default": "기본",
+ "dns_blocking_mode_ipv4_label": "IPv4 차단",
+ "dns_blocking_mode_ipv6_label": "IPv6 차단",
+ "dns_blocking_mode_nxdomain": "NXDOMAIN",
+ "dns_blocking_mode_refused": "REFUSED",
+ "dns_blocking_mode_title": "차단 모드",
+ "dns_bootstrap_dns_placeholder": "IP 주소",
+ "dns_bootstrap_dns_title": "부트스트랩 DNS 서버",
+ "dns_bootstrap_servers": "부트스트랩 DNS 서버",
+ "dns_cache_config": "DNS 캐시 구성",
+ "dns_cache_size": "캐시 크기",
+ "dns_cache_size_label": "DNS 캐시 크기(바이트)",
+ "dns_cache_size_title": "캐시 크기",
+ "dns_clear_cache": "캐시 지우기",
+ "dns_clear_cache_cancel": "취소",
+ "dns_clear_cache_confirm": "네, 지우기",
+ "dns_clear_cache_yes": "네, 지우기",
+ "dns_config": "DNS 서버 설정",
+ "dns_disallowed_clients": "차단된 클라이언트",
+ "dns_disallowed_clients_title": "차단된 클라이언트",
+ "dns_disallowed_domains": "차단된 도메인",
+ "dns_disallowed_domains_title": "차단된 도메인",
+ "dns_edns_custom_placeholder": "IP 주소 입력",
+ "dns_fallback_dns_title": "폴백 DNS 서버",
+ "dns_fallback_servers": "폴백 DNS 서버",
+ "dns_optimistic_caching": "옵티미스틱 캐시",
+ "dns_over_https": "DNS-over-HTTPS",
+ "dns_over_quic": "DNS-over-QUIC",
+ "dns_over_tls": "DNS-over-TLS",
+ "dns_override_max_ttl": "최대 TTL (초) 무시",
+ "dns_override_max_ttl_title": "최대 TTL (초) 무시",
+ "dns_override_min_ttl": "최소 TTL (초) 무시",
+ "dns_override_min_ttl_title": "최소 TTL (초) 무시",
+ "dns_privacy": "DNS 프라이버시",
+ "dns_private_reverse_servers_title": "프라이빗 역방향 DNS 서버",
+ "dns_rate_limit": "한도 제한",
+ "dns_rate_limit_allowlist_placeholder": "IP 주소",
+ "dns_rate_limit_placeholder": "한도 제한 입력하기",
+ "dns_rate_limit_title": "한도 제한",
+ "dns_rewrites": "DNS 변경",
+ "dns_server_addresses": "서버 주소",
+ "dns_server_addresses_label": "서버 주소, 한 줄에 하나씩 입력하세요",
+ "dns_server_addresses_title": "서버 주소",
+ "dns_server_config_title": "DNS 서버 설정",
+ "dns_settings": "DNS",
+ "dns_test_upstreams": "업스트림 테스트",
+ "dns_upstream_servers_title": "업스트림 DNS 서버",
+ "dns_upstream_timeout": "업스트림 제한 시간",
+ "dns_upstream_timeout_title": "업스트림 제한 시간",
+ "dns_upstream_validation_invalid": "잘못된 형식",
+ "domain": "도메인",
+ "domain_or_client": "도메인 또는 클라이언트",
+ "download_mobileconfig": "설정 파일 내려받기",
+ "edit_table_action": "편집",
+ "enable": "활성화",
+ "enabled_dhcp": "DHCP 서버 활성화됨",
+ "encryption_certificate_path": "인증서 경로",
+ "encryption_certificates": "인증서",
+ "encryption_certificates_input": "PEM으로 인코딩된 인증서 여기에 복사/붙여넣기하세요.",
+ "encryption_certificates_source_content": "인증서 내용 붙여넣기",
+ "encryption_certificates_source_path": "인증서 파일 경로 설정",
+ "encryption_chain_invalid": "인증서 체인이 유효하지 않습니다",
+ "encryption_chain_valid": "인증서 체인이 유효합니다",
+ "encryption_config_saved": "암호화 구성이 저장되었습니다",
+ "encryption_doq": "DNS-over-QUIC 포트",
+ "encryption_dot": "DNS-over-TLS 포트",
+ "encryption_https": "HTTP 포트",
+ "encryption_key": "개인 키",
+ "encryption_key_input": "PEM으로 인코딩된 개인 키를 여기에 복사/붙여넣기하세요.",
+ "encryption_key_source_content": "비밀키 내용 붙여넣기",
+ "encryption_plain_dns": "평문 DNS",
+ "encryption_plain_dns_error": "평문 DNS를 비활성화하려면, 암호화된 DNS 프로토콜을 하나 이상 활성화하세요",
+ "encryption_private_key_path": "비밀키 경로",
+ "encryption_server": "서버 이름",
+ "encryption_server_enter": "도메인 이름을 입력하세요.",
+ "encryption_title": "암호화",
+ "enter_cache_size": "캐시 크기를 입력하세요",
+ "enter_ip_address_placeholder": "IP 주소 입력",
+ "error": "오류",
+ "fallback_dns": "폴백 DNS 서버",
+ "filter_category_general": "General",
+ "filter_category_general_desc": "대부분의 기기에서 추적 및 광고를 차단하는 목록",
+ "filter_category_other": "기타",
+ "filter_category_other_desc": "기타 차단 목록",
+ "filter_category_regional": "지역 목록",
+ "filter_category_regional_desc": "지역 광고 및 추적 서버에 중점을 둔 목록",
+ "filter_category_security": "멀웨어, 피싱 차단",
+ "filter_category_security_desc": "악성 및 피싱 도메인을 차단하는 목록",
+ "filters": "필터 구독하기",
+ "forgot_password": "비밀번호를 잊어버렸나요?",
+ "form_enter_blocked_response_ttl": "차단된 응답 TTL(초)을 입력하세요.",
+ "form_error_client_id_format": "ClientID는 숫자, 소문자 및 붙임표(-)만 포함해야 합니다",
+ "form_error_domain_format": "도메인 형식이 잘못되었습니다",
+ "form_error_equal": "동일하지 않아야 합니다",
+ "form_error_format": "잘못된 형식",
+ "form_error_gateway_ip": "임대는 게이트웨이의 IP 주소를 가질 수 없습니다",
+ "form_error_ip4_format": "잘못된 IPv4 형식",
+ "form_error_ip6_format": "잘못된 IPv6 주소",
+ "form_error_ip_format": "유효하지 않은 IP주소",
+ "form_error_mac_format": "잘못된 MAC 주소",
+ "form_error_password": "비밀번호 불일치",
+ "form_error_port": "유효한 포트 번호를 입력하세요",
+ "form_error_port_range": "80-65535 범위의 포트 번호를 입력하세요",
+ "form_error_port_unsafe": "안전하지 않은 포트입니다",
+ "form_error_server_name": "유효하지 않은 서버 이름",
+ "form_error_url_format": "유효한 URL 또는 파일 경로를 입력하세요",
+ "friday": "금요일",
+ "gateway_or_subnet_invalid": "잘못된 서브넷 마스크",
+ "general_statistics": "일반 통계",
+ "ignore_domains_title": "무시된 도메인",
+ "inactivity_schedule_add": "일정 추가",
+ "inactivity_schedule_all_day": "하루 종일",
+ "inactivity_schedule_edit": "일정 수정",
+ "install_auth_confirm": "비밀번호 확인",
+ "install_auth_password": "비밀번호",
+ "install_auth_password_enter": "비밀번호 입력:",
+ "install_auth_username": "아이디",
+ "install_auth_username_enter": "사용자 이름 입력",
+ "install_saved": "성공적으로 저장되었습니다",
+ "install_settings_all_interfaces": "모든 인터페이스",
+ "install_settings_port": "포트",
+ "install_static_error": "AdGuard Home는 이 네트워크 인터페이스에서 자동 설정할 수 없습니다. 여기에서 어떻게 이걸 수동으로 할 수 있는지 확인해주세요.",
+ "install_static_ok": "좋은 소식입니다! 고정 IP 주소가 이미 설정되어있네요",
+ "install_step": "단계",
+ "install_submit_title": "축하합니다!",
+ "ip_address": "IP 주소",
+ "known_tracker": "알려진 추적기",
+ "language": "언어",
+ "light_theme": "라이트 테마",
+ "login": "로그인",
+ "logout": "로그아웃",
+ "make_static": "정적으로 만들기",
+ "milliseconds_abbreviation": "ms",
+ "monday": "월요일",
+ "name": "이름",
+ "name_label": "이름",
+ "name_table_header": "이름",
+ "name_tooltip": "이름",
+ "next": "다음",
+ "no": "아니오",
+ "none_text": "없음",
+ "nothing_found": "아무것도 찾을 수 없습니다",
+ "notify_undo": "되돌리기",
+ "notify_updated": "업데이트 됨",
+ "parental_control": "자녀 보호",
+ "parental_group_cdn": "CDN",
+ "parental_group_development": "소프트웨어 개발",
+ "parental_group_gaming": "게임",
+ "parental_group_hosting": "웹 호스팅",
+ "parental_group_privacy": "개인정보 보호 도구",
+ "parental_group_shopping": "쇼핑",
+ "parental_group_social": "소셜 네트워크",
+ "parental_group_social_network": "소셜 네트워크",
+ "parental_group_software": "소프트웨어 개발",
+ "parental_group_streaming": "스트리밍",
+ "password_label": "비밀번호",
+ "password_placeholder": "비밀번호 입력:",
+ "plain_dns": "평문 DNS",
+ "privacy_policy": "개인정보취급방침",
+ "protection": "보호",
+ "protocol": "프로토콜 선택",
+ "query_log_allowed": "허용됨",
+ "query_log_blocked": "차단됨",
+ "query_log_blocked_services": "차단된 서비스",
+ "query_log_blocked_threats": "차단된 위협",
+ "query_log_custom_filtering_rules": "커스텀 필터링 규칙",
+ "query_log_nothing_available": "아무것도 찾을 수 없습니다",
+ "query_log_processed": "처리됨",
+ "query_log_rewritten": "재작성됨",
+ "query_log_safe_search": "세이프서치 사용",
+ "query_log_strict_search": "검색을 제한하려면 쌍따옴표를 사용해주세요",
+ "rate_limit_subnet_len_ipv4_error": "IPv4 서브넷 접두사 길이는 0에서 32 사이여야 합니다.",
+ "rate_limit_subnet_len_ipv6_error": "IPv6 서브넷 접두사 길이는 0에서 128 사이여야 합니다.",
+ "refresh_btn": "새로고침",
+ "refresh_statics": "통계 새로 고침",
+ "release_notes": "릴리스 노트",
+ "remove": "제거",
+ "report_an_issue": "문제 신고",
+ "request_table_header": "요청",
+ "requests_table_header": "요청",
+ "reset": "초기화",
+ "reset_settings": "설정 초기화",
+ "reset_settings_confirm": "예, 제거",
+ "response_details": "응답 정보",
+ "response_time": "응답 시간",
+ "result": "결과",
+ "rewrite_add": "DNS 변환 정보를 추가합니다",
+ "rewrite_edit": "DNS 다시 쓰기 편집",
+ "rewrite_hosts_applied": "호스트 파일 규칙에 따라 재작성",
+ "rewrite_settings_updated": "DNS 재작성 설정이 성공적으로 업데이트되었습니다.",
+ "rewrite_updated": "DNS 다시 쓰기 업데이트 완료",
+ "rewritten": "재작성됨",
+ "rules_label": "필터 규칙",
+ "safe_browsing": "세이프 브라우징",
+ "safe_search": "세이프서치 사용",
+ "saturday": "토요일",
+ "save": "저장",
+ "save_btn": "저장",
+ "search_placeholder": "검색",
+ "server_config_blocking_mode": "차단 모드",
+ "server_config_blocking_mode_ipv4": "IPv4 차단",
+ "server_config_blocking_mode_ipv6": "IPv6 차단",
+ "server_config_custom_ip": "사용자 지정 IP주소",
+ "server_config_default": "기본",
+ "server_config_disable_ipv6": "IPv6 주소 확인 비활성화",
+ "server_config_dnssec_enable": "DNSSEC 활성화",
+ "server_config_edns_enable": "EDNS 클라이언트 서브넷 활성화",
+ "server_config_nxdomain": "NXDOMAIN",
+ "server_config_rate_limit_placeholder": "한도 제한 입력하기",
+ "server_config_rate_limit_whitelist_faq": "속도 제한에서 제외되는 IP 주소",
+ "server_config_refused": "REFUSED",
+ "server_config_subnet_len_ipv4": "IPv4 주소의 서브넷 접두사 길이",
+ "server_config_subnet_len_ipv4_faq": "속도 제한에 사용되는 IPv4 주소의 서브넷 접두사 길이입니다. 기본값은 24입니다.",
+ "server_config_subnet_len_ipv6": "IPv6 주소의 서브넷 접두사 길이",
+ "server_config_subnet_len_ipv6_faq": "속도 제한에 사용되는 IPv6 주소의 서브넷 접두사 길이입니다. 기본값은 56입니다.",
+ "set_static_ip": "고정 IP 주소 설정",
+ "settings": "설정",
+ "settings_browsing_security": "브라우징 보안",
+ "settings_confirm_clear_statistics": "통계를 삭제하시겠습니까?",
+ "settings_custom": "사용자 정의",
+ "settings_general_short": "General",
+ "settings_global": "글로벌",
+ "settings_notify_changes_saved": "변경사항이 저장되었습니다.",
+ "settings_notify_statistics_cleared": "통계 삭제됨",
+ "settings_parental_control": "자녀 보호",
+ "settings_rule_syntax": "규칙 구문",
+ "settings_safe_search": "세이프서치 사용",
+ "settings_statistics": "통계",
+ "settings_statistics_clear": "통계 비우기",
+ "settings_statistics_retention": "통계 저장 기간",
+ "settings_yes_clear": "네, 지우기",
+ "setup_devices_router_title": "라우터",
+ "setup_dns_privacy_ioc_mac": "iOS 및 macOS 설정",
+ "setup_dns_privacy_other_title": "기타 구현",
+ "show_blocked_responses": "차단됨",
+ "show_processed_responses": "처리됨",
+ "source_label": "소스",
+ "source_tooltip": "소스",
+ "stats_adult": "차단된 성인 웹사이트",
+ "stats_query_domain": "쿼리 도메인",
+ "status_table_header": "상태",
+ "sunday": "일요일",
+ "system_host_files": "시스템 호스트 파일",
+ "table_client": "클라이언트",
+ "tags_title": "태그",
+ "test_upstreams": "업스트림 테스트",
+ "threats_blocked": "차단된 위협",
+ "thursday": "목요일",
+ "time_table_header": "시간",
+ "top_blocked_domains": "차단된 도메인",
+ "top_clients": "클라이언트",
+ "top_upstreams": "상위 업스트림",
+ "total": "합계",
+ "try_again": "다시 시도해보세요",
+ "tuesday": "화요일",
+ "unavailable_dhcp": "DHCP가 사용 불가능합니다.",
+ "unavailable_dhcp_desc": "AdGuard Home은 이 OS에서 DHCP 서버를 구동할 수 없습니다.",
+ "unblock": "차단 해제",
+ "update_button": "업데이트",
+ "update_filters_disable": "비활성화",
+ "update_filters_interval_custom": "사용자 정의",
+ "update_filters_interval_daily": "매일",
+ "update_filters_interval_hourly": "한 시간 마다",
+ "update_filters_title": "필터를 자동으로 업데이트합니다.",
+ "updates_checked": "AdGuard Home의 새 버전을 사용할 수 있습니다",
+ "updates_version_equal": "AdGuard Home 최신 상태입니다.",
+ "upstream": "업스트림",
+ "upstream_dns": "업스트림 DNS 서버",
+ "upstream_dns_addresses": "서버 주소, 한 줄에 하나씩 입력하세요",
+ "upstream_dns_fastest_addr": "가장 빠른 IP 주소",
+ "upstream_dns_parallel_requests": "병렬 처리 요청",
+ "upstream_dns_servers_title": "업스트림 DNS 서버",
+ "upstream_examples_title": "예시",
+ "upstream_private_ptr_resolvers_title": "프라이빗 역방향 DNS 리졸버 사용",
+ "upstreams": "업스트림",
+ "url_label": "URL",
+ "user_rules": "사용자 규칙",
+ "user_rules_add_to_allowlist": "허용 목록에 추가",
+ "user_rules_check_button": "확인",
+ "user_rules_check_client_placeholder": "클라이언트 식별자 입력",
+ "user_rules_check_hostname_label": "호스트 이름 또는 도메인 이름",
+ "user_rules_disable_filter": "필터 비활성화",
+ "user_rules_disable_parental_control": "자녀 보호 비활성화",
+ "user_rules_dns_record_type_placeholder": "DNS 레코드 유형 선택",
+ "user_rules_domain_blocked": "차단된 도메인",
+ "user_rules_edit_dns_rewrite": "DNS 다시 쓰기 편집",
+ "user_rules_rewrite_rule_is_applied": "리디렉션 규칙이 적용됩니다",
+ "username_label": "아이디",
+ "username_placeholder": "사용자 이름 입력",
+ "validated_with_dnssec": "DNSSEC로 검증됨",
+ "wednesday": "수요일",
+ "whois": "WHOIS",
+ "yes": "네",
+ "yes_disable": "예, 비활성화",
+ "yes_reset": "예, 제거"
+}
diff --git a/client_v2/src/__locales/nl.json b/client_v2/src/__locales/nl.json
new file mode 100644
index 00000000000..5cb2a99c7a6
--- /dev/null
+++ b/client_v2/src/__locales/nl.json
@@ -0,0 +1,1021 @@
+{
+ "access_settings_allowed_faq": "Een lijst met CIDR's, IP-adressen of Client-ID's . Als dit veld vermeldingen bevat, accepteert AdGuard Home alleen verzoeken van deze clients",
+ "access_settings_allowed_placeholder": "Voer toegestane cliënten in",
+ "access_settings_allowed_title": "Toegestane clients, één per regel",
+ "access_settings_blocked_faq_1": "AdGuard Home zal DNS-verzoeken die overeenkomen met deze domeinen verwerpen. Dergelijke verzoeken verschijnen niet in het zoekopdrachtlogboek. Je kunt exacte domeinnamen, jokertekens of URL-filterregels invoeren, zoals example.org, *.example.org, of ||example.org^",
+ "access_settings_blocked_faq_2": "Niet te verwarren met filters",
+ "access_settings_blocked_placeholder": "Voer niet-toegestane domeinen in",
+ "access_settings_blocked_title": "Niet-toegestane domeinen, één per regel",
+ "access_settings_disallowed_faq": "Een lijst met CIDR's, IP-adressen of Client-ID's . Als dit veld items bevat, zal AdGuard Home verzoeken van deze clients verwerpen. Dit veld wordt genegeerd als er items zijn in Toegestane clients ",
+ "access_settings_disallowed_placeholder": "Voer niet-toegestane clients in",
+ "access_settings_disallowed_title": "Niet-toegestane clients, één per regel",
+ "access_settings_saved_toast": "Toegangsinstellingen opgeslagen",
+ "access_settings_title": "Toegangsinstellingen",
+ "actions_table_header": "Actie",
+ "add": "Toevoegen",
+ "add_allowlist": "Toelatingslijst toevoegen",
+ "add_blocklist": "Blokkeerlijst toevoegen",
+ "add_persistent_client": "Toevoegen als permanente client",
+ "add_tls_certificate": "TLS-certificaat toevoegen",
+ "add_tls_certificate_private_key": "TLS-certificaat privésleutel toevoegen",
+ "add_to_allowlist": "Toevoegen aan toelatingslijst",
+ "ads_blocked": "Geblokk. advertenties",
+ "ads_blocked_card": "Geblokk. advertenties",
+ "ads_blocked_tooltip": "Het aantal DNS-verzoeken dat wordt geblokkeerd door advertentieblokkeringsfilters en de hosts-blokkeerlijst",
+ "adult_websites_blocked": "Websites voor volwassenen geblokkeerd",
+ "adult_websites_blocked_tooltip": "Aantal geblokkeerde 18+ websites",
+ "all_domains": "Alle domeinen",
+ "all_lists_up_to_date_toast": "Alle lijsten zijn reeds actueel",
+ "all_rewrites_disabled": "Alle aangepaste DNS-antwoorden worden uitgeschakeld",
+ "all_rewrites_enabled": "Alle aangepaste DNS-antwoorden worden ingeschakeld",
+ "allow_service": "Service toestaan",
+ "allowed": "Toegestaan",
+ "allowlist_add": "Toelatingslijst toevoegen",
+ "allowlist_edit": "Toelatingslijst bewerken",
+ "allowlist_empty": "Nog geen toelatingslijsten — je eerste toevoegen ",
+ "allowlist_placeholder_example": "Mijn toelatingslijst",
+ "allowlist_remove": "Toelatingslijst verwijderen?",
+ "allowlist_remove_desc": "%value% zal worden verwijderd",
+ "allowlists": "Toelatingslijsten",
+ "allowlists_desc": "Domeinen op deze toelatingslijsten, worden toegestaan, zelfs als ze op een blokkeerlijst staan",
+ "allowlists_title": "Toelatingslijsten",
+ "apply": "Toepassen",
+ "aria_clear_input": "Invoer wissen",
+ "aria_next_page": "Volgende pagina",
+ "aria_previous_page": "Vorige pagina",
+ "auto_clients_desc": "Informatie over de IP-adressen van apparaten die AdGuard Home kunnen gebruiken",
+ "auto_clients_title": "Runtime",
+ "autofix_warning_list": "Het zal de volgende taken uitvoeren:
Systeem DNSStubListener deactiveren
DNS-serveradres instellen op 127.0.0.1
Het doel van de symbolische koppeling /etc/resolv.conf vervangen door /run/systemd/resolve/resolv.conf
DNSStubListener stoppen (herlaad systemd-resolved service)
",
+ "autofix_warning_result": "Als gevolg hiervan worden alle DNS-aanvragen van je systeem standaard door AdGuard Home verwerkt.",
+ "autofix_warning_text": "Als je op \"Repareren\" klikt, configureert AdGuard Home jouw systeem om de AdGuard Home DNS-server te gebruiken.",
+ "average_time_processing": "Gemiddelde procestijd",
+ "average_time_processing_tooltip": "Gemiddelde tijd in milliseconden voor het verwerken van een DNS-zoekopdracht",
+ "average_upstream_response_time": "Gemiddelde upstream responstijd",
+ "back": "Terug",
+ "block": "Blokkeren",
+ "block_client": "Client blokkeren",
+ "block_for_this_client_only": "Alleen voor deze cliënt blokkeren",
+ "blocked": "Geblokkeerd",
+ "blocked_adult_websites": "Geblokkeerd door Ouderlijk toezicht",
+ "blocked_by_cname_or_ip": "Geblokkeerd via CNAME of IP",
+ "blocked_queries": "Geblokkeerde zoekopdrachten",
+ "blocked_service": "Geblokkeerde service",
+ "blocked_services": "Geblokkeerde services",
+ "blocked_services_desc": "Selecteer populaire websites en diensten die moeten worden geblokkeerd",
+ "blocked_threats": "Geblokkeerde bedreigingen",
+ "blocked_threats_chart": "Geblokkeerde bedreigingen",
+ "blocked_total": "%value% totaal geblokkeerd",
+ "blocklist_add_from_list": "Toevoegen vanuit lijst",
+ "blocklist_add_manual": "Handmatig toevoegen",
+ "blocklist_contents": "Inhoud (.%value%)",
+ "blocklist_edit": "Blokkeerlijst beheren",
+ "blocklist_homepage": "Startpagina",
+ "blocklist_placeholder_example": "Mijn blokkeerlijst",
+ "blocklist_remove": "Blokkeerlijst verwijderen?",
+ "blocklist_remove_desc": "%value% zal worden verwijderd",
+ "blocklist_url_file_path": "URL of bestandspad",
+ "blocklists_add": "Blokkeerlijsten toevoegen",
+ "blocklists_add_desc": "Je kunt blokkeerlijsten uit de lijst selecteren of ze handmatig toevoegen",
+ "blocklists_desc": "Domeinen op deze lijsten worden geblokkeerd",
+ "blocklists_empty": "Nog geen blokkeerlijsten — je eerste toevoegen ",
+ "blocklists_title": "Blokkeerlijsten",
+ "bootstrap_dns": "Bootstrap DNS-servers",
+ "browse": "Bladeren",
+ "cache_cleared": "DNS-cache succesvol gewist",
+ "cache_config_clear": "Cache wissen",
+ "cache_config_max_ttl": "Maximum TTL overschrijven, in seconden",
+ "cache_config_max_ttl_faq": "De maximale time-to-live (TTL)-waarde voor vermeldingen in de DNS-cache",
+ "cache_config_max_ttl_placeholder": "Voer maximum TTL in",
+ "cache_config_min_ttl": "Minimum TTL overschrijven, in seconden",
+ "cache_config_min_ttl_faq": "De minimale time-to-live (TTL)-waarde voor vermeldingen in de DNS-cache",
+ "cache_config_min_ttl_placeholder": "Voer minimum TTL in",
+ "cache_config_optimistic": "Optimistisch cachen",
+ "cache_config_optimistic_desc": "AdGuard Home zal reageren vanuit de cache, zelfs als de vermeldingen zijn verlopen en zal proberen ze bij te werken",
+ "cache_config_size": "Cachegrootte, in bytes",
+ "cache_config_size_faq": "DNS-cachegrootte. Leeglaten om caching uit te schakelen",
+ "cache_config_size_validation": "De cachegrootte moet groter zijn dan nul wanneer deze is ingeschakeld",
+ "cache_config_ttl_validation": "Minimale waarde TTL-cache moet kleiner dan of gelijk zijn aan de maximale waarde",
+ "cache_confirm_clear_desc": "Dit zal alle in de cache opgeslagen DNS-antwoorden verwijderen. Daaropvolgende DNS-zoekopdrachten kunnen langzamer zijn totdat de cache opnieuw is opgebouwd.",
+ "cache_confirm_clear_title": "DNS-cache wissen",
+ "cache_enabled": "Cache inschakelen",
+ "cache_enabled_desc": "DNS-antwoorden lokaal opslaan",
+ "cancel": "Annuleren",
+ "cancel_btn": "Annuleren",
+ "category_tooltip": "Categorie",
+ "changes_saved_success": "Wijzigingen opgeslagen",
+ "check_dhcp_servers": "Controleren op DHCP-servers",
+ "check_not_found": "Niet in je lijst met filters gevonden",
+ "check_updates_btn": "Controleren op updates",
+ "clear_btn": "Wissen",
+ "clear_query_log": "Zoekopdrachtlogboek wissen",
+ "client_add": "Client toevoegen",
+ "client_added": "Client toegevoegd",
+ "client_already_blocked": "Client %ip% is al geblokkeerd",
+ "client_blocked_flash": "Client geblokkeerd",
+ "client_details": "Client details",
+ "client_id": "Client-ID",
+ "client_id_faq": "Clients kunnen worden geïdentificeerd aan de hand van hun ClientID. Ga voor meer informatie naar de AdGuard Home GitHub ",
+ "client_id_placeholder": "Client-ID invoeren",
+ "client_identifier": "Identificator",
+ "client_ip": "Client-IP",
+ "client_location": "Clientlocatie",
+ "client_name_already_exists": "Deze clientnaam bestaat al",
+ "client_removed": "Client verwijderd",
+ "client_settings": "Cliëntinstellingen",
+ "client_table_header": "Gebruiker",
+ "client_unblocked_flash": "Client gedeblokkeerd",
+ "clients": "Clienten",
+ "clients_add": "Client toevoegen",
+ "clients_add_identifier": "ID toevoegen",
+ "clients_add_name": "Clientnaam",
+ "clients_add_placeholder": "Vul clientnaam in",
+ "clients_desc": "Regels voor apparaten die verbonden zijn met AdGuard Home",
+ "clients_dns_cache_size": "DNS-cachegrootte, in bytes",
+ "clients_dns_cache_size_placeholder": "Gebruik cijfers",
+ "clients_dont_collect_stats": "Geen statistieken verzamelen voor deze client",
+ "clients_dont_log": "Deze client niet registreren",
+ "clients_edit": "Client bewerken",
+ "clients_identifier_already_used": "Deze identificator is al in gebruik",
+ "clients_identifier_format_error": "Gebruik cijfers, kleine Engelse letters of koppeltekens",
+ "clients_identifiers": "Identificatoren",
+ "clients_identifiers_desc": "Je kunt het IP-adres, CIDR, MAC-adres of ClientID van de client gebruiken (voor DoT, DoH en DoQ). Meer informatie ",
+ "clients_logs_and_statistics": "Logboeken en statistieken",
+ "clients_not_found": "Geen cliënten gevonden",
+ "clients_protection": "Bescherming",
+ "clients_protection_desc": "Beveiligingsfuncties, logregistratie en statistieken in- of uitschakelen",
+ "clients_remove_desc": "%value% wordt verwijderd uit persistente clients samen met de bijbehorende instellingen",
+ "clients_remove_title": "Client verwijderen?",
+ "clients_tags": "Labels",
+ "clients_tags_desc": "Je kunt labels toevoegen voor deze client en ze opnemen in filterregels. Meer informatie ",
+ "clients_tags_placeholder": "Voeg tags toe",
+ "clients_title": "Aanhoudend",
+ "clients_upstreams_desc": "Upstream DNS-servers, één per regel",
+ "clients_use_dns_cache": "DNS-cache gebruiken",
+ "clients_use_global_settings": "Gebruik globale instelling",
+ "clients_use_global_settings_desc": "Beschermingsinstellingen overnemen van je AdGuard Home-account",
+ "close": "Sluiten",
+ "close_result": "Resultaat sluiten",
+ "confirm": "Bevestigen",
+ "confirm_client_block_desc": "AdGuard Home zal alle DNS-verzoeken van de client %ip% blokkeren",
+ "confirm_client_block_title": "Client blokkeren?",
+ "confirm_client_unblock_desc": "DNS-verzoeken van de client %ip% zullen worden toegestaan",
+ "confirm_client_unblock_title": "Client deblokkeren?",
+ "confirm_static_ip": "AdGuard Home zal %ip% configureren als jouw statische IP-adres. Wil je doorgaan?",
+ "copied": "Gekopieerd naar klembord",
+ "copy": "Kopiëren",
+ "copy_to_clipboard": "Kopiëren naar klembord",
+ "custom_filtering_rules": "Aangepaste filterregels",
+ "custom_rules": "Aangepaste regels",
+ "dark_theme": "Donker thema",
+ "dashboard": "Dashboard",
+ "delete_btn": "Verwijderen",
+ "delete_confirm": "Lease verwijderen?",
+ "delete_confirm_desc": "Dit zal %ip% vrijgeven",
+ "delete_table_action": "Verwijderen",
+ "delete_table_action_confirm": "Ja, verwijderen",
+ "device_type": "Apparaattype",
+ "dhcp": "DHCP",
+ "dhcp_add_static_lease": "Statische lease toevoegen",
+ "dhcp_config_saved": "DHCP-configuratie succesvol opgeslagen",
+ "dhcp_dynamic_ip_found": "Je systeem gebruikt dynamische IP-adresconfiguratie voor de interface %interface_name%. Om een DHCP-server te gebruiken, moet een statisch IP-adres worden ingesteld. Je huidige IP-adres is %ip%. AdGuard Home zet dit IP-adres automatisch als statisch als je op de knop \"DHCP-server inschakelen\" drukt.",
+ "dhcp_edit_static_lease": "Statische lease bewerken",
+ "dhcp_enable": "AdGuard's eigen ingebouwde DHCP-server gebruiken",
+ "dhcp_error": "Kan de DHCP-server niet controleren. Wacht even voordat je het opnieuw probeert",
+ "dhcp_form_gateway_address": "Gateway-IP-adres",
+ "dhcp_form_gateway_address_value": "Gateway IP-adres: %value%",
+ "dhcp_form_gateway_input": "Gateway-IP",
+ "dhcp_form_lease_title": "DHCP-leasetijd, in seconden",
+ "dhcp_form_range_start": "Start IP-adres",
+ "dhcp_form_range_title": "IP-adresbereik",
+ "dhcp_form_subnet_input": "Subnetmasker",
+ "dhcp_found": "DHCP-server kan niet worden geactiveerd",
+ "dhcp_hardware_address_value": "Hardwareadres: %value%",
+ "dhcp_interface_select": "DHCP-interface",
+ "dhcp_ip_addresses_value": "IP-adressen: %value%",
+ "dhcp_ipv4_settings": "DHCP IPv4-instellingen",
+ "dhcp_ipv6_settings": "DHCP IPv6-instellingen",
+ "dhcp_lease_added": "Statische lease \"%key%\" succesvol toegevoegd",
+ "dhcp_lease_deleted": "Statische lease \"%key%\" succesvol verwijderd",
+ "dhcp_lease_updated": "Statische lease \"%key%\" succesvol bijgewerkt",
+ "dhcp_leases": "Dynamische leases",
+ "dhcp_leases_not_found": "Geen DHCP-lease gevonden",
+ "dhcp_leases_title": "DHCP-leases",
+ "dhcp_mac_address_already_added": "Dit MAC-adres is al toegevoegd",
+ "dhcp_new_static_lease": "Nieuwe statische lease",
+ "dhcp_not_found": "DHCP-server is actief",
+ "dhcp_reset": "Dit zal de DHCP-configuratie opnieuw instellen. Alle wijzigingen gaan verloren",
+ "dhcp_reset_leases": "Alle leases opnieuw instellen",
+ "dhcp_reset_leases_confirm": "Dit zal alle leases opnieuw instellen",
+ "dhcp_reset_leases_success": "DHCP-leases succesvol gereset",
+ "dhcp_settings": "DHCP-instellingen",
+ "dhcp_static_ip_error": "De DHCP-server vereist een statisch IP-adres. We konden niet bevestigen dat deze netwerkinterface er een heeft",
+ "dhcp_static_leases": "Statische leases",
+ "dhcp_static_leases_not_found": "Geen DHCP static lease gevonden",
+ "dhcp_table_hostname": "Hostnaam",
+ "dhcp_table_ip_address": "IP adres",
+ "dhcp_table_mac_address": "MAC-adres",
+ "dhcp_v6_unavailable": "Niet beschikbaar",
+ "dhcp_warning": "Om deze DHCP-server in te schakelen, moet je alle andere DHCP-servers op je netwerk uitschakelen. Anders kunnen apparaten hun internettoegang verliezen",
+ "disable": "Uitschakelen",
+ "disable_dns_rewrites": "DNS-herschrijvingen uitschakelen?",
+ "disable_protection_btn": "Bescherming uitschakelen",
+ "disabled_dhcp": "DHCP-server uitschakelen",
+ "disallow_client_confirm_allowlist_note": "Deze actie zal ook de regel %ip% uitsluiten van de lijst met toegestane clients.",
+ "disallow_client_confirm_text": "Hierdoor worden alle toekomstige DNS-verzoeken van de client genegeerd %ip% en de client uit de lijst met toegestane clients verwijderen",
+ "disallow_client_confirm_title": "Deze cliënt niet toestaan?",
+ "disallow_this_client": "Deze cliënt niet toelaten",
+ "dns_access_settings_title": "Toegangsinstellingen",
+ "dns_allowed_clients": "Toegestane cliënten",
+ "dns_allowed_clients_desc": "Accepteert alleen verzoeken van clients op deze lijst",
+ "dns_allowed_clients_desc_2": "Om een client toe te voegen, voer je het CIDR, IP-adres of ClientID in",
+ "dns_allowed_clients_label": "Toegestane clients, één per regel",
+ "dns_allowed_clients_placeholder": "Voer toegestane clients in",
+ "dns_blocking_mode": "Blokkeringsmodus",
+ "dns_blocking_mode_custom_ip": "Aangepast IP-adres",
+ "dns_blocking_mode_custom_ip_desc": "Reageert met een handmatig ingesteld IP-adres",
+ "dns_blocking_mode_default": "Standaard",
+ "dns_blocking_mode_default_desc": "Reageert met 0.0.0.0 voor A-records en :: voor AAAA-records als het wordt geblokkeerd door een Adblock-achtige regel. Reageert met het IP-adres dat in de regel is gespecificeerd als het wordt geblokkeerd door een hosts-achtige regel",
+ "dns_blocking_mode_desc": "Stelt het reactietype op geblokkeerde verzoeken in",
+ "dns_blocking_mode_ipv4_label": "Blokkeren IP4",
+ "dns_blocking_mode_ipv4_placeholder": "Voer IPv4-adres in",
+ "dns_blocking_mode_ipv6_label": "Blokkeren IPv6",
+ "dns_blocking_mode_ipv6_placeholder": "Voer IPv6-adres in",
+ "dns_blocking_mode_null_ip": "Null IP-adres",
+ "dns_blocking_mode_null_ip_desc": "Reageert met 0.0.0.0 voor A en :: voor AAAA",
+ "dns_blocking_mode_nxdomain": "NXDOMAIN",
+ "dns_blocking_mode_nxdomain_desc": "Reageert met code NXDOMAIN",
+ "dns_blocking_mode_refused": "REFUSED",
+ "dns_blocking_mode_refused_desc": "Reageert met code REFUSED",
+ "dns_blocking_mode_title": "Blokkeringsmodus",
+ "dns_blocking_mode_ttl_label": "Geblokkeerde respons TTL, in seconden",
+ "dns_blocking_mode_ttl_placeholder": "Voer TTL in",
+ "dns_bootstrap_dns_desc": "Stelt de DNS-servers in die worden gebruikt om de hostnamen van de upstream-resolvers voor DoH/DoT op te lossen",
+ "dns_bootstrap_dns_desc_2": "Voer alleen IP-adressen in. Opmerkingen zijn niet toegestaan",
+ "dns_bootstrap_dns_label": "Bootstrap DNS-servers, één per regel",
+ "dns_bootstrap_dns_placeholder": "IP-adressen",
+ "dns_bootstrap_dns_title": "Bootstrap DNS-servers",
+ "dns_bootstrap_servers": "Bootstrap DNS-servers",
+ "dns_cache_config": "DNS-cache configuratie",
+ "dns_cache_configuration_saved_toast": "DNS-cacheconfiguratie opgeslagen",
+ "dns_cache_desc": "Slaat DNS-antwoorden lokaal op",
+ "dns_cache_size": "Cachegrootte",
+ "dns_cache_size_desc": "Stelt de DNS-cachegrootte in",
+ "dns_cache_size_label": "DNS-cachegrootte, in bytes",
+ "dns_cache_size_title": "Cachegrootte",
+ "dns_cache_title": "DNS-cache",
+ "dns_clear_cache": "Cache wissen",
+ "dns_clear_cache_cancel": "Annuleren",
+ "dns_clear_cache_confirm": "Ja, wissen",
+ "dns_clear_cache_desc": "Hierdoor wordt de DNS-cache van je apparaat gewist en wordt het apparaat gevraagd nieuwe informatie op te vragen bij de DNS-server",
+ "dns_clear_cache_title": "Cache wissen?",
+ "dns_clear_cache_yes": "Ja, wissen",
+ "dns_config": "DNS-server configuratie",
+ "dns_confirm_clear_cache": "DNS-cache wissen?",
+ "dns_confirm_clear_cache_desc": "Weet je zeker dat je de DNS-cache wilt wissen?",
+ "dns_disallowed_clients": "Niet-toegestane clients",
+ "dns_disallowed_clients_desc": "Weigert verzoeken van clients op deze lijst. Deze instelling wordt genegeerd wanneer er vermeldingen zijn in Toegestane clients",
+ "dns_disallowed_clients_label": "Niet-toegestane clients, één per regel",
+ "dns_disallowed_clients_title": "Niet-toegestane clients",
+ "dns_disallowed_domains": "Niet-toegestane domeinen",
+ "dns_disallowed_domains_desc": "DNS-verzoeken die overeenkomen met deze domeinen worden genegeerd en verschijnen niet in het verzoeklogboek",
+ "dns_disallowed_domains_desc_2": "Voer exacte domeinnamen, jokertekens of URL-filterregels in, zoals example.org, *.example.org of ||example.org^",
+ "dns_disallowed_domains_label": "Niet-toegestane domeinen, één per regel",
+ "dns_disallowed_domains_title": "Niet-toegestane domeinen",
+ "dns_dnssec": "DNSSEC",
+ "dns_dnssec_desc": "Voegt de DNSSEC-vlag toe aan uitgaande DNS-verzoeken en controleert het resultaat. Hiervoor is een DNSSEC-compatibele resolver vereist",
+ "dns_edns_client_subnet": "EDNS-clientsubnet",
+ "dns_edns_client_subnet_desc": "Voegt EDNS Client Subnet (ECS)-gegevens toe aan upstream-verzoeken en registreert ECS-waarden in het verzoekenlogboek",
+ "dns_edns_custom_label": "Aangepast IP-adres voor EDNS",
+ "dns_edns_custom_placeholder": "Voer IP-adres in",
+ "dns_edns_desc": "Voeg EDNS Client Subnet (ECS)-gegevens toe aan upstream-verzoeken en registreer de ECS-waarden in het verzoekenlogboek. Je kunt het standaard client-subnet gebruiken of een aangepast IP-adres voor ECS opgeven",
+ "dns_edns_option_custom": "Aangepast IP-adres gebruiken",
+ "dns_edns_option_default": "Standaard EDNS-cliëntsubnet gebruiken",
+ "dns_edns_title": "EDNS-clientsubnet",
+ "dns_fallback_dns_desc": "Stelt alternatieve DNS-servers in die moeten worden gebruikt wanneer de bovenliggende DNS-servers niet beschikbaar zijn",
+ "dns_fallback_dns_desc_2": "Gebruik dezelfde syntaxis als voor de upstream DNS-servers ",
+ "dns_fallback_dns_label": "Backup-DNS-servers, één per regel",
+ "dns_fallback_dns_placeholder": "IP-adressen, sdns://, quic://, https://, h3://, of tls://",
+ "dns_fallback_dns_title": "Back-up DNS-servers",
+ "dns_fallback_servers": "Back-up DNS-servers",
+ "dns_ipv6_resolution": "IPv6-resolutie",
+ "dns_ipv6_resolution_desc": "Maakt het mogelijk om DNS-verzoeken voor IPv6-adressen (AAAA-records) op te lossen en voegt IPv6-hints toe aan HTTPS-antwoorden",
+ "dns_optimistic_caching": "Optimistisch cachen",
+ "dns_optimistic_caching_desc": "Reageert vanuit de cache, zelfs als de items zijn verlopen, en probeert ze bij te werken",
+ "dns_over_https": "DNS-over-HTTPS",
+ "dns_over_quic": "DNS-over-QUIC",
+ "dns_over_tls": "DNS-over-TLS",
+ "dns_override_max_ttl": "Maximale TTL overschrijven",
+ "dns_override_max_ttl_desc": "Stelt de maximale tijd in gedurende welke DNS-antwoorden in de cache worden bewaard. Hogere TTL-waarden worden teruggebracht tot deze waarde",
+ "dns_override_max_ttl_label": "Maximale levensduur (TTL), in seconden",
+ "dns_override_max_ttl_placeholder": "Levensduur (TTL)",
+ "dns_override_max_ttl_title": "Maximale TTL overschrijven",
+ "dns_override_min_ttl": "Minimale TTL overschrijven",
+ "dns_override_min_ttl_desc": "Stelt de minimale tijd in gedurende welke DNS-antwoorden in de cache worden bewaard. Lagere TTL-waarden worden verhoogd tot deze waarde",
+ "dns_override_min_ttl_label": "Minimale levensduur (TTL), in seconden",
+ "dns_override_min_ttl_placeholder": "Levensduur (TTL)",
+ "dns_override_min_ttl_title": "Minimale TTL overschrijven",
+ "dns_privacy": "DNS-privacy",
+ "dns_private_reverse_page_desc": "Verwerkt PTR-, SOA- en NS-verzoeken voor ARPA-domeinen met privé-IP-adressen via privé-upstreamservers, DHCP en /etc/hosts. Indien uitgeschakeld, wordt op al dergelijke verzoeken gereageerd met NXDOMAIN",
+ "dns_private_reverse_resolve_clients_desc": "Zet IP-adressen van clients om naar hun hostnamen met behulp van PTR-query’s. Maakt gebruik van eigen DNS-servers voor lokale clients en upstream-servers voor clients met openbare IP-adressen",
+ "dns_private_reverse_resolve_clients_title": "Omgekeerd opzoeken van IP-adressen van clients",
+ "dns_private_reverse_resolvers": "Privé reverse DNS-resolvers",
+ "dns_private_reverse_resolvers_desc": "Verwerkt PTR-, SOA- en NS-verzoeken voor ARPA-domeinen met privé-IP-adressen via privé-upstreamservers, DHCP en /etc/hosts",
+ "dns_private_reverse_resolvers_disabled_desc": "Als deze optie is uitgeschakeld, wordt op al dergelijke verzoeken met NXDOMAIN gereageerd",
+ "dns_private_reverse_servers_desc": "Stelt de DNS-servers in voor privé-PTR-, SOA- en NS-verzoeken. Deze verzoeken zijn afkomstig van clients met privé-IP-adressen en hebben betrekking op ARPA-domeinen die subnetten uit privé-IP-adresbereiken bevatten",
+ "dns_private_reverse_servers_desc_2": "Als dit veld leeg is, worden de standaard DNS-resolvers van je besturingssysteem gebruikt, behalve voor de IP-adressen van AdGuard Home. Standaard maakt AdGuard Home gebruik van de volgende reverse DNS-resolvers: %value_1% en %value_2%",
+ "dns_private_reverse_servers_label": "Privé reverse DNS-servers, één per regel",
+ "dns_private_reverse_servers_placeholder": "IP-adressen, sdns://, quic://, https://, h3://, of tls://",
+ "dns_private_reverse_servers_title": "Privé reverse DNS-servers",
+ "dns_protocols_title": "DNS-protocollen",
+ "dns_queries": "DNS-verzoeken",
+ "dns_queries_tooltip": "Het aantal DNS-verzoeken dat in de afgelopen 24 uur is verwerkt",
+ "dns_query": "DNS-verzoeken",
+ "dns_rate_limit": "Snelheidslimiet",
+ "dns_rate_limit_allowlist": "Toelatingslijst voor snelheidslimiet",
+ "dns_rate_limit_allowlist_desc": "Specificeert IP-adressen die zijn uitgesloten van snelheidsbeperking",
+ "dns_rate_limit_allowlist_label": "IP-adressen, één per regel",
+ "dns_rate_limit_allowlist_placeholder": "IP-adressen",
+ "dns_rate_limit_allowlist_title": "Toelatingslijst voor snelheidslimiet",
+ "dns_rate_limit_desc": "Stelt het aantal verzoeken per seconde in dat per client is toegestaan",
+ "dns_rate_limit_desc_2": "Een waarde van 0 betekent dat er geen limiet is",
+ "dns_rate_limit_no_limit": "Geen limiet",
+ "dns_rate_limit_placeholder": "Voer snelheidslimiet in",
+ "dns_rate_limit_title": "Snelheidslimiet",
+ "dns_rate_limit_value": "%value% verzoeken per seconde",
+ "dns_rewrite_exists": "Deze DNS-herschrijving bestaat al",
+ "dns_rewrite_removed": "DNS-herschrijving verwijderd",
+ "dns_rewrite_same": "Je kunt niet herschrijven naar hetzelfde domein of jokerteken",
+ "dns_rewrites": "DNS-herschrijvingen",
+ "dns_rewrites_desc": "Geeft een overzicht van je aangepaste DNS-antwoorden voor specifieke domeinnamen",
+ "dns_server_addresses": "Serveradressen",
+ "dns_server_addresses_configured_in_file": "Serveradressen worden geconfigureerd in het bestand %path% ",
+ "dns_server_addresses_desc": "Stelt DNS-serveradressen in waardoor AdGuard Home toegankelijk zal zijn",
+ "dns_server_addresses_desc_2": "Tips bekijken voor het configureren van upstream DNS-servers en onze lijst met vertrouwde DNS-providers ",
+ "dns_server_addresses_label": "Serveradressen, één per regel",
+ "dns_server_addresses_placeholder": "IP-adressen, sdns://, quic://, https://, h3://, of tls://",
+ "dns_server_addresses_title": "Serveradressen",
+ "dns_server_config_title": "DNS-server configuratie",
+ "dns_server_configuration_saved_toast": "DNS-serverconfiguratie opgeslagen",
+ "dns_settings": "DNS",
+ "dns_subnet_placeholder": "Voer de lengte van het voorvoegsel in",
+ "dns_subnet_prefix": "Lengte subnetprefix voor %value%-adressen",
+ "dns_subnet_prefix_desc": "Stelt de lengte van het subnetprefix in voor %value%-adressen die worden gebruikt voor snelheidsbeperking",
+ "dns_subnet_prefix_title": "Lengte subnetprefix voor %value%-adressen",
+ "dns_test_not_ok_toast": "Upstream %key% heeft de syntaxiscontrole doorstaan maar de test niet gehaald. Controleer de upstream-instellingen",
+ "dns_test_ok_toast": "Alle upstreams zijn operationeel",
+ "dns_test_parsing_error_toast": "Kan upstream-naam niet extraheren uit regel %number% in de sectie %section%. Controleer de regelindeling",
+ "dns_test_upstreams": "Test upstreams",
+ "dns_test_warning_toast": "Upstream %key% heeft niet gereageerd op het DNS-verzoek. Controleer de upstream-instellingen",
+ "dns_ttl_value": "%value% sec",
+ "dns_upstream_mode": "Upstream-modus",
+ "dns_upstream_mode_desc": "Bepaalt hoe DNS-verzoeken van je netwerk naar meerdere upstream-servers worden doorgestuurd",
+ "dns_upstream_mode_title": "Upstream-modus",
+ "dns_upstream_servers_title": "Upstream DNS-servers",
+ "dns_upstream_timeout": "Upstream time-out",
+ "dns_upstream_timeout_desc": "Hiermee wordt ingesteld hoe lang er moet worden gewacht op een reactie van de upstream-servers",
+ "dns_upstream_timeout_placeholder": "Voer de time-out in seconden in",
+ "dns_upstream_timeout_title": "Upstream time-out",
+ "dns_upstream_validation_invalid": "Ongeldige opmaak",
+ "dns_upstream_validation_invalid_lines": "Ongeldige opmaak op de regels {lines}",
+ "domain": "Domein",
+ "domain_or_client": "Domein of cliënt",
+ "download_mobileconfig": "Configuratiebestand downloaden",
+ "dynamic_dhcp_leases_not_found": "Dynamische DHCP-leases niet gevonden",
+ "edit_table_action": "Bewerken",
+ "enable": "Inschakelen",
+ "enable_dns_rewrites": "DNS-herschrijvingen inschakelen?",
+ "enabled_dhcp": "DHCP-server inschakelen",
+ "encrypted_dns_addresses": "Versleutelde DNS-serveradressen",
+ "encrypted_dns_settings": "Instellingen versleutelde DNS-servers",
+ "encryption_certificate_has_issues": "Certificaat heeft problemen",
+ "encryption_certificate_path": "Certificaat pad",
+ "encryption_certificates": "Certificaten",
+ "encryption_certificates_desc": "Om versleuteling te gebruiken, moet je een geldige SSL-certificaatketen voor je domein verstrekken. Je kunt een gratis SSL-certificaat krijgen van letsencrypt.org of er een kopen bij een vertrouwde certificeringsinstantie",
+ "encryption_certificates_input": "Kopieër en plak hier je PEM-gecodeerde certificaten.",
+ "encryption_certificates_source_content": "Inhoud certificaten plakken",
+ "encryption_certificates_source_path": "Certificaten bestandspad instellen",
+ "encryption_chain_invalid": "Certificaatketen is ongeldig",
+ "encryption_chain_valid": "Certificaatketen is geldig",
+ "encryption_config_saved": "Versleuteling configuratie opgeslagen",
+ "encryption_confirm_clear": "Versleutelingsinstellingen opnieuw instellen",
+ "encryption_confirm_clear_desc": "Alle versleutelingsinstellingen worden teruggezet naar hun standaardwaarden.",
+ "encryption_disable_plain_dns": "Niet-versleutelde DNS uitschakelen?",
+ "encryption_disable_plain_dns_desc": "Apparaten die geen versleutelde DNS ondersteunen, werken mogelijk niet goed",
+ "encryption_doq": "DNS-over-QUIC-poort",
+ "encryption_doq_summary": "DNS-over-QUIC-poort: %value%",
+ "encryption_doq_tooltip": "Deze poort wordt gebruikt om een DNS-over-QUIC-server te draaien",
+ "encryption_dot": "DNS-over-TLS-poort",
+ "encryption_dot_summary": "DNS-over-TLS-poort: %value%",
+ "encryption_dot_tooltip": "Deze poort wordt gebruikt om een DNS-over-TLS-server te draaien",
+ "encryption_encrypted_dns": "Versleutelde DNS",
+ "encryption_encrypted_dns_desc": "De AdGuard Home-beheerinterface gebruikt HTTPS. De DNS-server ondersteunt DNS-over-HTTPS, DNS-over-TLS en DNS-over-QUIC",
+ "encryption_expire": "Verloopt: %value%",
+ "encryption_force_redirect": "Automatisch omleiden van HTTP naar HTTPS",
+ "encryption_hostnames": "Hostnamen: %value%",
+ "encryption_https": "HTTPS poort",
+ "encryption_https_summary": "HTTPS-poort: %value%",
+ "encryption_https_tooltip": "Deze poort biedt HTTPS-toegang tot de AdGuard Home-beheerdersinterface. Het biedt ook DNS-over-HTTPS op de locatie '/dns-query'",
+ "encryption_invalid_data": "Ongeldige gegevens",
+ "encryption_issuer": "Uitgever: %value%",
+ "encryption_key": "Privesleutel",
+ "encryption_key_cert_mismatch": "Privésleutel komt niet overeen met certificaat",
+ "encryption_key_input": "Kopieër en plak hier je PEM-gecodeerde prive sleutel voor je certificaat.",
+ "encryption_key_invalid": "Privésleutel is ongeldig",
+ "encryption_key_source_content": "Inhoud privésleutel plakken",
+ "encryption_key_source_path": "Een privésleutelbestand instellen",
+ "encryption_key_type": "Versleutelingsalgoritme: %value%",
+ "encryption_key_valid": "Privésleutel is geldig",
+ "encryption_plain_dns": "Gewone DNS",
+ "encryption_plain_dns_desc": "Standaard is gewone DNS ingeschakeld. Door dit uit te schakelen, worden alle apparaten gedwongen versleutelde DNS te gebruiken. Hiervoor moet u ten minste één versleuteld DNS-protocol opgeven",
+ "encryption_plain_dns_error": "Als je gewone DNS wilt uitschakelen, schakel je ten minste één versleuteld DNS-protocol in",
+ "encryption_private_key_path": "Privésleutel pad",
+ "encryption_server": "Servernaam",
+ "encryption_server_enter": "Voer je domeinnaam in",
+ "encryption_server_summary": "Servernaam: %value%",
+ "encryption_server_tooltip_1": "Als dit is ingesteld, zal AdGuard Home ClientID's detecteren, DDR-zoekopdrachten beantwoorden en aanvullende verbindingsvalidaties uitvoeren. Als dit niet is ingesteld, zijn deze functies uitgeschakeld",
+ "encryption_server_tooltip_2": "Moet overeenkomen met een DNS-naam in het certificaat",
+ "encryption_subject": "Onderwerp: %value%",
+ "encryption_title": "Encryptie",
+ "encryption_unable_read_cert": "Kan certificaatbestand niet lezen",
+ "encryption_unable_read_key": "Kan privésleutelbestand niet lezen",
+ "enter_cache_size": "Cachegrootte invoeren (bytes)",
+ "enter_ip_address_placeholder": "IP-adres invoeren",
+ "error": "Fout",
+ "fallback_dns": "Back-up DNS-servers",
+ "filter_added_successfully": "Toegevoegde blokkeerlijst: %value% ",
+ "filter_added_successfully_allowlist": "Toegevoegde toelatingslijst: %value% ",
+ "filter_added_successfully_allowlist_noname": "Toelatingslijst succesvol toegevoegd",
+ "filter_added_successfully_more": "Toegevoegde blokkeerlijsten: %value% en %more% meer",
+ "filter_category_general": "Algemeen",
+ "filter_category_general_desc": "Lijsten die volgers en advertenties op de meeste apparaten blokkeert",
+ "filter_category_other": "Overige",
+ "filter_category_other_desc": "Overige blokkeerlijsten",
+ "filter_category_regional": "Regionaal",
+ "filter_category_regional_desc": "Lijsten die focussen op regionale ads en tracking servers",
+ "filter_category_security": "Beveiliging",
+ "filter_category_security_desc": "Lijsten gespecialiseerd in het blokkeren van malware, phising of scamdomeinen",
+ "filter_removed_successfully": "Verwijderde blokkeerlijst: %value% ",
+ "filter_removed_successfully_allowlist": "Verwijderde toelatingslijst: %value% ",
+ "filter_removed_successfully_allowlist_noname": "Toelatingslijst succesvol verwijderd",
+ "filters": "Filters",
+ "forgot_password": "Wachtwoord vergeten?",
+ "forgot_password_list_item_1": "Maak een nieuw wachtwoord aan met de htpasswd-tool of een andere online BCrypt-generator.",
+ "forgot_password_list_item_2": "Stop AdGuard Home.",
+ "forgot_password_list_item_3": "Open AdGuardHome.yaml, zoek het wachtwoordveld en vervang het door de nieuwe waarde.",
+ "forgot_password_list_item_4": "Start AdGuard Home.",
+ "forgot_password_list_item_5": "Meld je aan bij de webinterface met je nieuwe wachtwoord.",
+ "forgot_password_list_title": "Om je wachtwoord te wijzigen:",
+ "forgot_password_page_desc": "AdGuard Home slaat wachtwoorden op als BCrypt-versleutelde hashes",
+ "form_enter_blocked_response_ttl": "Voer geblokkeerd antwoord TTL in (seconden)",
+ "form_enter_hostname": "Voer een hostnaam in",
+ "form_enter_ip": "Voer een IP-adres in",
+ "form_enter_mac": "Voer een MAC-adres in",
+ "form_error_answer_format": "Ongeldig resultaatformaat",
+ "form_error_client_id_format": "Client-ID mag alleen cijfers, kleine letters en koppeltekens bevatten",
+ "form_error_domain_format": "Ongeldige opmaak domein",
+ "form_error_equal": "Mag niet gelijk zijn",
+ "form_error_format": "Ongeldige opmaak",
+ "form_error_format_line": "Ongeldige opmaak op regel %line%",
+ "form_error_format_lines": "Ongeldige opmaak op de regels %lines%",
+ "form_error_gateway_ip": "Lease kan niet het IP-adres van de gateway hebben",
+ "form_error_hostname_format": "Gebruik cijfers, kleine Engelse letters of koppeltekens",
+ "form_error_ip4_format": "Ongeldig IPv4-adres",
+ "form_error_ip6_format": "Ongeldig IPv6-adres",
+ "form_error_ip_already_added": "Dit IP-adres is al toegevoegd",
+ "form_error_ip_format": "Ongeldig IP-adres",
+ "form_error_mac_already_added": "Dit MAC-adres is al toegevoegd",
+ "form_error_mac_format": "Ongeldig MAC-adres",
+ "form_error_password": "Wachtwoord komt niet overeen",
+ "form_error_port": "Geldig poortnummer invoeren",
+ "form_error_port_range": "Poortnummer invoeren tussen 80 en 65535",
+ "form_error_port_unsafe": "Onveilige poort",
+ "form_error_range": "De waarde moet tussen %min% en %max% liggen",
+ "form_error_required": "Vul dit veld in",
+ "form_error_server_name": "Ongeldige servernaam",
+ "form_error_subnet": "Het IP-adres moet vallen binnen het subnet %cidr%",
+ "form_error_url_format": "Voer een geldige URL of bestandspad in",
+ "form_value_value_from_error": "Voer een waarde in tussen %min_value% en %max_value%",
+ "form_value_value_min_error": "Voer een waarde in van %min_value% of groter",
+ "found_in_known_domains": "Gevonden in de bekende domeinen",
+ "friday": "vrijdag",
+ "gateway_or_subnet_invalid": "Ongeldig subnetmasker",
+ "general_statistics": "Algemene statistieken",
+ "greater_range_start_error": "Het einde van het bereik moet groter zijn dan het begin",
+ "home_dns_addresses": "AdGuard Home DNS-adressen",
+ "home_dns_addresses_desc": "De AdGuard Home DNS-server luistert via de volgende interfaces/poorten:",
+ "ignore_domains_desc_log": "Verzoeken die overeenkomen met deze regels worden niet weergegeven in het zoekopdrachtenlogboek",
+ "ignore_domains_desc_stats": "Verzoeken die aan deze regels voldoen, worden niet opgenomen in de statistieken",
+ "ignore_domains_title": "Genegeerde domeinen",
+ "inactivity_schedule": "Inactiviteitsschema",
+ "inactivity_schedule_add": "Schema toevoegen",
+ "inactivity_schedule_all_day": "De hele dag",
+ "inactivity_schedule_delete": "Schema verwijderen",
+ "inactivity_schedule_delete_desc": "Serviceblokkering is de hele dag actief op %value% ",
+ "inactivity_schedule_desc": "Stel inactiviteitsperioden in voor het blokkeren van diensten",
+ "inactivity_schedule_edit": "Schema bewerken",
+ "inactivity_schedule_no_schedules": "Geen schema",
+ "inactivity_schedule_replace_desc": "Dit schema vervangt alle bestaande schema's voor dezelfde dag van de week. Elke dag kan slechts één inactiviteitsperiode hebben",
+ "inactivity_schedule_time_from": "Begintijd",
+ "inactivity_schedule_time_to": "Eindtijd",
+ "inactivity_schedule_timezone": "Tijdzone",
+ "install_auth_confirm": "Bevestig wachtwoord",
+ "install_auth_password": "Wachtwoord",
+ "install_auth_password_enter": "Voer wachtwoord in",
+ "install_auth_username": "Gebruikersnaam",
+ "install_auth_username_enter": "Voer gebruikersnaam in",
+ "install_saved": "Succesvol opgeslagen",
+ "install_settings_all_interfaces": "Alle interfaces",
+ "install_settings_port": "Poort",
+ "install_static_configure": "AdGuard Home heeft gedetecteerd dat het dynamische IP-adres %ip% wordt gebruikt. Wil je dat het als je statische adres wordt ingesteld?",
+ "install_static_error": "AdGuard Home kan dit niet automatisch configureren op deze netwerkinterface. Zoek een instructie om dit handmatig te doen.",
+ "install_static_ok": "Goed nieuws! Het statische IP-adres was al geconfigureerd",
+ "install_step": "Stap",
+ "install_submit_title": "Gefeliciteerd!",
+ "instructions": "Instructies",
+ "invalid": "Ongeldig",
+ "ip_address": "IP-adres",
+ "ip_addresses_placeholder": "Voer IP-adressen in, één per regel",
+ "known_tracker": "Bekende volger",
+ "language": "Taal",
+ "last_days": "| Afgelopen %count% dag | Afgelopen %count% dagen",
+ "last_hour": "Afgelopen uur",
+ "last_hours": "| Afgelopen %count% uur | Afgelopen %count% uur",
+ "last_updated_label": "Laatst bijgewerkt",
+ "light_theme": "Licht thema",
+ "list_updated": "| %count% lijst bijgewerkt | %count% lijsten bijgewerkt",
+ "login": "Aanmelden",
+ "logout": "Afmelden",
+ "make_static": "Statisch maken",
+ "make_static_desc": "Deze actie zet een dynamisch adres om in een statisch adres",
+ "milliseconds_abbreviation": "ms",
+ "monday": "maandag",
+ "name": "Naam",
+ "name_label": "Naam",
+ "name_table_header": "Naam",
+ "name_tooltip": "Naam",
+ "network_interface": "Netwerkinterface",
+ "next": "Volgende",
+ "no": "Nee",
+ "no_stats_yet": "Nog geen statistieken - blijf AdGuard Home gebruiken om ze te bekijken",
+ "none_text": "Geen",
+ "nothing_found": "Niets gevonden",
+ "notify_undo": "Ongedaan maken",
+ "notify_updated": "Bijgewerkt",
+ "open_dashboard": "Dashboard openen",
+ "out_of_range_error": "Het adres mag niet liggen binnen het DHCP-bereik %start%–%end%",
+ "parental_control": "Ouderlijk toezicht",
+ "parental_group_ai": "AI",
+ "parental_group_cdn": "CDN",
+ "parental_group_dating": "Dating",
+ "parental_group_development": "Softwareontwikkeling",
+ "parental_group_gambling": "Gokken en wedden",
+ "parental_group_gaming": "Gamen",
+ "parental_group_hosting": "Webhosting",
+ "parental_group_messaging": "Berichten",
+ "parental_group_messenger": "Berichten",
+ "parental_group_privacy": "Privacyhulpmiddelen",
+ "parental_group_shopping": "Winkelen",
+ "parental_group_social": "Sociale netwerken",
+ "parental_group_social_network": "Sociale netwerken",
+ "parental_group_software": "Softwareontwikkeling",
+ "parental_group_streaming": "Streamen",
+ "password_label": "Wachtwoord",
+ "password_login_error": "Je gebruikersnaam en wachtwoord komen niet overeen",
+ "password_placeholder": "Voer wachtwoord in",
+ "password_requirements": "Wachtwoordvereisten",
+ "password_requirements_characters": "Minimaal 8 tekens",
+ "password_requirements_lowercase": "Minimaal 1 kleine letter",
+ "password_requirements_match": "Wachtwoorden komen overeen",
+ "password_requirements_special": "Engelse letters, cijfers en speciale tekens",
+ "password_requirements_uppercase": "Minstens 1 hoofdletter",
+ "path_to_file_placeholder": "Pad naar bestand: lokaal of internetadres",
+ "pause_for_hour": "Pauzeren voor %count% uur",
+ "pause_for_minutes": "| Pauzeren voor %count% minuut | Pauzeren voor %count% minuten",
+ "pause_for_seconds": "| Pauzeren voor %count% seconde | Pauzeren voor %count% seconden",
+ "pause_until_tomorrow": "Pauzeren tot morgen, %time%",
+ "period_notify": "Om de weergegeven perioden te wijzigen, bewerk Statistieken behouden in Algemene instellingen ",
+ "plain_dns": "Gewone DNS",
+ "plain_dns_addresses": "Onversleutelde DNS-serveradressen",
+ "port_53_faq_link": "Poort 53 wordt vaak gebruikt door services als \"DNSStubListener\"- of \"systemd-resolved\". Lees deze instructie om dit op te lossen.",
+ "port_in_use": "Deze poort is al in gebruik",
+ "privacy_policy": "Privacybeleid",
+ "processing_time_ms": "%value% ms",
+ "protection": "Bescherming",
+ "protocol": "Protocol",
+ "protocols": "Protocollen",
+ "queries": "Zoekopdrachten",
+ "queries_total": "| %count% zoekopdracht totaal | %count% zoekopdrachten totaal",
+ "query_details": "Details van de zoekopdracht",
+ "query_log": "Zoekopdrachtlogboek",
+ "query_log_all_reasons": "Alle redenen",
+ "query_log_all_statuses": "Alle statussen",
+ "query_log_allowed": "Op toelatingslijst",
+ "query_log_blocked": "Geblokkeerd",
+ "query_log_blocked_by_filter": "Geblokkeerd door filter",
+ "query_log_blocked_by_parental_control": "Geblokkeerd door ouderlijk toezicht",
+ "query_log_blocked_services": "Geblokkeerde services",
+ "query_log_blocked_threats": "Geblokkeerde bedreigingen",
+ "query_log_custom_filtering_rules": "Aangepaste filterregels",
+ "query_log_detail_address": "Adres: %value% ",
+ "query_log_detail_category": "Categorie: %value% ",
+ "query_log_detail_country": "Land: %value% ",
+ "query_log_detail_date": "Datum: %value% ",
+ "query_log_detail_dns_server": "DNS-server: %value% ",
+ "query_log_detail_domain": "Domein: %value% ",
+ "query_log_detail_ecs": "ECS: %value% ",
+ "query_log_detail_elapsed": "Verstreken: %value% ",
+ "query_log_detail_name": "Naam: %value% ",
+ "query_log_detail_network": "Netwerk: %value% ",
+ "query_log_detail_original_response": "Oorspronkelijke reactie:",
+ "query_log_detail_protocol": "Protocol: %value% ",
+ "query_log_detail_reason": "Reden:",
+ "query_log_detail_response": "Antwoord:",
+ "query_log_detail_response_code": "Responscode: %value% ",
+ "query_log_detail_rules": "Regel(s):",
+ "query_log_detail_served_from_cache": "Uit cache weergegeven: %value% ",
+ "query_log_detail_service_name": "Servicenaam: %value% ",
+ "query_log_detail_source": "Bron: %value% ",
+ "query_log_detail_status": "Status: %value% ",
+ "query_log_detail_time": "Tijd: %value% ",
+ "query_log_detail_type": "Type: %value% ",
+ "query_log_nothing_available": "Niets gevonden",
+ "query_log_nothing_available_rotation": "Het verzoekenlogboek is uitgeschakeld in de algemene instellingen",
+ "query_log_processed": "Verwerkt",
+ "query_log_retention": "Logrotatie",
+ "query_log_rewritten": "Herschreven",
+ "query_log_safe_search": "Veilig zoeken",
+ "query_log_strict_search": "Gebruik dubbele aanhalingstekens voor strikt zoeken",
+ "rate_limit_subnet_len_ipv4_error": "De lengte van het IPv4-subnetvoorvoegsel moet tussen 0 en 32 liggen",
+ "rate_limit_subnet_len_ipv6_error": "De lengte van het IPv6-subnetvoorvoegsel moet tussen 0 en 128 liggen",
+ "reason_table_header": "Reden",
+ "refresh_btn": "Verversen",
+ "refresh_statics": "Statistieken vernieuwen",
+ "release_notes": "Release-opmerkingen",
+ "remove": "Verwijderen",
+ "remove_tag": "%value% verwijderen",
+ "report_an_issue": "Een probleem melden",
+ "request_table_header": "Verzoek",
+ "requests_table_header": "Verzoeken",
+ "reset": "Herstellen",
+ "reset_dhcp_settings": "DHCP-instellingen opnieuw instellen",
+ "reset_dns_confirm_text": "Alle instellingen van het DNS-protocol worden teruggezet naar de standaardwaarden",
+ "reset_dns_confirm_title": "DNS-protocollen opnieuw instellen?",
+ "reset_dns_protocols": "DNS-protocollen opnieuw instellen",
+ "reset_settings": "Instellingen herstellen",
+ "reset_settings_confirm": "Ja, opnieuw instellen",
+ "response_details": "Antwoorddetails",
+ "response_time": "Responsetijd",
+ "result": "Resultaat",
+ "resume_protection_timer": "Bescherming wordt hervat over %time%",
+ "rewrite_add": "DNS-herschrijving toevoegen",
+ "rewrite_domain": "Domeinnaam of jokerteken",
+ "rewrite_domain_input_placeholder": "Voer de domeinnaam of het jokerteken in",
+ "rewrite_edit": "DNS-herschrijven bewerken",
+ "rewrite_hosts_applied": "Herschreven door de hosts-bestandsregel",
+ "rewrite_settings_updated": "Instellingen voor DNS-herschrijven bijgewerkt",
+ "rewrite_updated": "DNS-herschrijven succesvol bijgewerkt",
+ "rewrites_answer_input_placeholder": "Voer IP-adres, CNAME-record, A of AAAA in",
+ "rewrites_empty": "Nog geen herschrijvingen — je eerste toevoegen ",
+ "rewrites_remove_desc": "De DNS-herschrijving voor %value% zal worden verwijderd",
+ "rewrites_remove_title": "DNS-herschrijving verwijderen?",
+ "rewrites_tooltip_examples_item1": "DNS-antwoorden alleen voor deze domeinnaam herschrijven",
+ "rewrites_tooltip_examples_item2": "DNS-antwoorden herschrijven voor alle subdomeinen van example.org",
+ "rewrites_tooltip_instructions_item1": "Om de domeinnaam om te zetten naar een IP-adres, voer je een IPv4-adres in voor A-records en een IPv6-adres voor AAAA-records",
+ "rewrites_tooltip_instructions_item2": "Om de domeinnaam te herleiden, voer je het CNAME-record van het doeldomein in",
+ "rewrites_tooltip_instructions_item3": "Om alleen A-records te behouden, enter",
+ "rewrites_tooltip_instructions_item4": "Om alleen AАAA-records te behouden, enter",
+ "rewritten": "Herschreven",
+ "rows_per_page": "Rijen per pagina %value%",
+ "rules_label": "Regels",
+ "safe_browsing": "Veilig browsen",
+ "safe_search": "Veilig zoeken",
+ "safe_search_used": "Veilig zoeken gebruikt",
+ "safe_search_used_tooltip": "Het aantal DNS-zoekopdrachten naar zoekmachines waarvoor Safe Search werd afgedwongen",
+ "saturday": "zaterdag",
+ "save": "Opslaan",
+ "save_btn": "Opslaan",
+ "search_placeholder": "Zoeken",
+ "selected": "%value% geselecteerd",
+ "server_config_blocking_mode": "Blokkeringsmodus",
+ "server_config_blocking_mode_custom_ip_desc": "Aangepast IP-adres: Reageren met een handmatig ingesteld IP-adres",
+ "server_config_blocking_mode_default_desc": "Standaard: Reageren met 0.0.0.0 voor A en :: voor AAAA als je wordt geblokkeerd door een Advertentieblokkeer-achtige regel. Reageren met het IP-adres dat in de regel is gespecificeerd als je wordt geblokkeerd door de /etc/hosts-stijl regel",
+ "server_config_blocking_mode_ipv4": "IPv4 blokkeren",
+ "server_config_blocking_mode_ipv4_faq": "IP-adres dat moet worden geretourneerd voor een geblokkeerd A-verzoek",
+ "server_config_blocking_mode_ipv4_placeholder": "Voer IPv4-adres in",
+ "server_config_blocking_mode_ipv6": "IPv6 blokkeren",
+ "server_config_blocking_mode_ipv6_faq": "IP-adres dat moet worden geretourneerd voor een geblokkeerd AAAA-verzoek",
+ "server_config_blocking_mode_ipv6_placeholder": "Voer IPv6-adres in",
+ "server_config_blocking_mode_null_ip_desc": "Null IP-adres: Reageren met 0.0.0.0 voor A en :: voor AAAA",
+ "server_config_blocking_mode_nxdomain_desc": "NXDOMAIN: Reageren met NXDOMAIN-code",
+ "server_config_blocking_mode_refused_desc": "REFUSED: Reageren met REFUSED-code",
+ "server_config_blocking_mode_ttl": "Geblokkeerde respons TTL, in seconden",
+ "server_config_blocking_mode_ttl_faq": "Geeft aan hoe lang clients een gefilterde respons in de cache moeten opslaan",
+ "server_config_custom_ip": "Aangepast IP-adres",
+ "server_config_default": "Standaard",
+ "server_config_disable_ipv6": "Oplossen IPv6-adressen uitschakelen",
+ "server_config_disable_ipv6_desc": "Alle DNS-verzoeken voor IPv6-adressen (type AAAA) negeren en IPv6-hints uit HTTPS-antwoorden verwijderen",
+ "server_config_dnssec_enable": "DNSSEC inschakelen",
+ "server_config_dnssec_enable_desc": "Stel de DNSSEC-vlag in bij uitgaande DNS-zoekopdrachten en controleer het resultaat. Vereist een DNSSEC-ondersteunende resolver",
+ "server_config_edns_cs_desc": "De optie EDNS Client Subnet (ECS) toevoegen aan upstream-verzoeken en de waarden die door de clients zijn verzonden registreren in het zoekopdrachtenlogboek",
+ "server_config_edns_enable": "EDNS client subnet inschakelen",
+ "server_config_edns_use_custom_ip": "Aangepast IP-adres gebruiken voor EDNS",
+ "server_config_edns_use_custom_ip_desc": "Vervang het client-IP-adres door het aangepaste IP-adres hieronder",
+ "server_config_null_ip": "Null IP-adres",
+ "server_config_nxdomain": "NXDOMAIN",
+ "server_config_rate_limit": "Snelheidslimiet, RPS",
+ "server_config_rate_limit_faq": "Het aantal verzoeken per seconde dat per client is toegestaan. Een waarde van 0 betekent geen limiet",
+ "server_config_rate_limit_placeholder": "Voer ratio limiet in",
+ "server_config_rate_limit_whitelist": "Toelatingslijst voor snelheidsbeperking",
+ "server_config_rate_limit_whitelist_faq": "IP-adressen uitgesloten van snelheidsbeperking",
+ "server_config_refused": "REFUSED",
+ "server_config_subnet_len_ipv4": "Lengte subnetvoorvoegsel voor IPv4-adressen",
+ "server_config_subnet_len_ipv4_faq": "Lengte subnetvoorvoegsel voor IPv4-adressen die worden gebruikt voor snelheidsbeperking. De standaardwaarde is 24",
+ "server_config_subnet_len_ipv6": "Lengte subnetvoorvoegsel voor IPv6-adressen",
+ "server_config_subnet_len_ipv6_faq": "Lengte subnetvoorvoegsel voor IPv6-adressen die worden gebruikt voor snelheidsbeperking. De standaardwaarde is 56",
+ "server_config_subnet_len_placeholder": "Voer de lengte van het voorvoegsel in",
+ "set_static_ip": "Stel een statisch IP-adres in",
+ "set_static_ip_manually": "Statisch IP-adres instellen",
+ "settings": "Instellingen",
+ "settings_anonymize_client_ip": "Client-IP-adressen anonimiseren",
+ "settings_anonymize_client_ip_desc": "Een deel van het IP-adres vervangen door nullen",
+ "settings_browsing_security": "Browserbeveiliging",
+ "settings_browsing_security_desc": "Toegang blokkeren tot domeinen die zijn gevonden in de database met kwaadaardige en phishingdomeinen",
+ "settings_confirm_clear_query_log": "Zoekopdrachtlogboek wissen?",
+ "settings_confirm_clear_query_log_desc": "Hiermee worden alle aanvragen uit het Zoekopdrachtlogboek verwijderd. Statistieken blijven beschikbaar",
+ "settings_confirm_clear_statistics": "Statistieken wissen?",
+ "settings_confirm_clear_statistics_desc": "Hiermee worden alle statistieken gewist. Zoekopdrachtlogboek zal nog steeds beschikbaar zijn",
+ "settings_confirm_decrease_log_rotation_interval": "Logrotatie-interval verlagen?",
+ "settings_confirm_decrease_log_rotation_interval_desc": "Hiermee worden alle logbestanden verwijderd die ouder zijn dan %value%",
+ "settings_confirm_decrease_stats_rotation_interval": "Bewaarinterval statistieken verlagen?",
+ "settings_confirm_decrease_stats_rotation_interval_desc": "Hiermee worden alle statistieken verwijderd die ouder zijn dan %value%",
+ "settings_custom": "Aangepast",
+ "settings_days": "| %count% dag | %count% dagen",
+ "settings_domain_names": "Domeinnamen, één per regel",
+ "settings_filter_requests": "Verzoeken filteren",
+ "settings_filter_requests_desc": "Je kunt Blokkeerlijsten , Toelatingslijsten en Gebruikersregels gebruiken om filterregels in te stellen",
+ "settings_filtering_and_security": "Filtering en beveiliging",
+ "settings_general_short": "Algemeen",
+ "settings_global": "Globaal",
+ "settings_hours": "| %count% uur | %count% uur",
+ "settings_log_dns_requests": "DNS-verzoeken registreren",
+ "settings_log_rotation_hours": "Logrotatie, in uren",
+ "settings_notify_changes_saved": "Wijzigingen opgeslagen",
+ "settings_notify_query_log_cleared": "Zoekopdrachtlogboek gewist",
+ "settings_notify_statistics_cleared": "Statistieken gewist",
+ "settings_parental_control": "Ouderlijk toezicht",
+ "settings_parental_control_desc": "Websites blokkeren met inhoud die ongepast is voor kinderen",
+ "settings_rotation_placeholder": "Gebruik cijfers",
+ "settings_rule_syntax": "Regel syntaxis",
+ "settings_safe_search": "Veilig zoeken",
+ "settings_safe_search_desc": "Ongepaste inhoud blokkeren in zoekresultaten",
+ "settings_statistics": "Statistieken",
+ "settings_statistics_clear": "Statistieken wissen",
+ "settings_statistics_desc": "Verzoek- en clientstatistieken weergeven op het dashboard",
+ "settings_statistics_retention": "Statistieken retentie",
+ "settings_statistics_retention_hours": "Bewaartermijn statistieken, in uren",
+ "settings_tooltip_domain_names": "Domeinnamen, jokertekens of filterregels",
+ "settings_tooltip_examples": "Voorbeelden:",
+ "settings_yes_clear": "Ja, wissen",
+ "settings_yes_decrease": "Ja, verlagen",
+ "setup_complete": "De installatie is voltooid en AdGuard Home is klaar voor gebruik",
+ "setup_devices_android_list_1": "Open de Instellingen -app op je apparaat en selecteer Wi‑Fi .",
+ "setup_devices_android_list_2": "Druk lang op je huidige netwerk en selecteer dan Netwerk wijzigen . Het is niet mogelijk om aangepaste DNS in te stellen voor mobiele internetverbindingen.",
+ "setup_devices_android_list_3": "Op sommige apparaten moet je mogelijk het selectievakje Geavanceerd aanvinken om meer instellingen te zien. Om je Android DNS-instellingen aan te passen, moet je de IP-instellingen wijzigen van DHCP naar Statisch .",
+ "setup_devices_android_list_4": "Wijzig de DNS 1 - en DNS 2 -waarden naar de adressen van jouw AdGuard Home-server.",
+ "setup_devices_android_title": "Android",
+ "setup_devices_dns_android_list_1": "Android 9 ondersteunt DNS-over-TLS standaard. Om dit te configureren, ga naar Instellingen → Netwerk en internet → Geavanceerd → Privé-DNS en voer je domeinnaam in.",
+ "setup_devices_dns_android_list_2": "AdGuard voor Android ondersteunt DNS-over-HTTPS , DNS-over-TLS en DNS-over-QUIC-configuratie .",
+ "setup_devices_dns_android_list_3": "Intra voegt DNS-over-HTTPS -ondersteuning toe aan Android.",
+ "setup_devices_dns_ios_list_1": "AdGuard for iOS ondersteunt de configuratie van DNS-over-HTTPS , DNS-over-TLS en DNS-over-QUIC .",
+ "setup_devices_dns_ios_list_2": "DNSCloak ondersteunt DNS-over-HTTPS , maar om het zo te configureren dat het je eigen server gebruikt, moet je er een DNS Stamp voor genereren.",
+ "setup_devices_dns_list_1": "Voor DNS-over-TLS , gebruik ",
+ "setup_devices_dns_list_2": "Voor DNS-over-HTTPS , gebruik ",
+ "setup_devices_dns_list_3": "Voor DNS-over-QUIC gebruikt ",
+ "setup_devices_dns_list_devices": "Hier is een lijst met software die je kunt gebruiken.",
+ "setup_devices_dns_macos_desc": "Op een iOS 14- of macOS Big Sur-apparaat kun je een speciaal mobileconfig-bestand downloaden dat DNS-over-HTTPS - of DNS-over-TLS -servers toevoegt aan de DNS-instellingen.",
+ "setup_devices_dns_other_list_1": "AdGuard Home zelf kan een veilige DNS-client zijn op elk platform.",
+ "setup_devices_dns_other_list_2": "dnsproxy ondersteunt alle bekende veilige DNS-protocollen.",
+ "setup_devices_dns_other_list_3": "dnscrypt-proxy ondersteunt DNS-over-HTTPS .",
+ "setup_devices_dns_other_list_4": "Mozilla Firefox ondersteunt DNS-over-HTTPS .",
+ "setup_devices_dns_other_list_5": "Voor meer implementaties, zie DNSCrypt en DNS-privacy .",
+ "setup_devices_ios_list_1": "Open de Instellingen -app van je apparaat en selecteer Wi-Fi . Het is niet mogelijk om aangepaste DNS in te stellen voor mobiele internetverbindingen.",
+ "setup_devices_ios_list_2": "Tik op de naam van het momenteel actieve netwerk.",
+ "setup_devices_ios_list_3": "Ga naar DNS → DNS configureren → Handmatig en voeg de adressen van jouw AdGuard Home-server toe.",
+ "setup_devices_ios_list_4": "Tik op Opslaan .",
+ "setup_devices_ios_title": "iOS",
+ "setup_devices_macos_list_1": "Klik op het Apple-menu → Systeemvoorkeuren → Netwerk .",
+ "setup_devices_macos_list_2": "Selecteer de vereiste verbinding en klik op Geavanceerd .",
+ "setup_devices_macos_list_3": "Selecteer het tabblad DNS en voer de adressen van jouw AdGuard Home-server in.",
+ "setup_devices_macos_title": "macOS",
+ "setup_devices_router_desc_1": "Stel AdGuard Home in op je thuisnetwerkrouter en bescherm alle apparaten die ermee verbonden zijn. Je hoeft niet elk apparaat handmatig te configureren.",
+ "setup_devices_router_desc_2": "Sommige routers laten je geen aangepaste DNS-server opzetten. In dat geval kun je proberen AdGuard Home op te zetten als een DHCP-server . Als dit niet werkt, raadpleeg dan je routerhandleiding voor instructies over hoe je DNS-servers voor jouw specifieke router kunt aanpassen.",
+ "setup_devices_router_list_1": "Je kunt ze meestal via je browser bereiken via een URL, zoals http://192.168.0.1 of http://192.168.1.1 . Je kunt gevraagd worden een wachtwoord in te voeren. Je kunt het wachtwoord vaak herstellen door op een knop op de router zelf te drukken, maar wees je ervan bewust dat je je hele routerconfiguratie kunt verliezen. Als je router via een app wordt ingesteld, installeer de app dan op je telefoon of pc en gebruik deze om toegang te krijgen tot de instellingen van de router.",
+ "setup_devices_router_list_1_title": "De instellingen van je router openen.",
+ "setup_devices_router_list_2": "Om de instellingen te vinden, zoek je naar DNS naast een veld dat twee of drie reeksen cijfers toestaat, elk verdeeld in vier groepen van één tot drie cijfers.",
+ "setup_devices_router_list_2_title": "Voer in de DHCP/DNS-instellingen de adressen van je AdGuard Home-server in.",
+ "setup_devices_router_title": "Router",
+ "setup_devices_windows_list_1": "Open Controlepaneel via het menu Start of Windows zoeken .",
+ "setup_devices_windows_list_2": "Klik op Netwerk en internet → Netwerkcentrum → Adapterinstellingen wijzigen .",
+ "setup_devices_windows_list_3": "Klik met de rechtermuisknop op je actieve verbinding en selecteer Eigenschappen .",
+ "setup_devices_windows_list_4": "Selecteer in de lijst Internet Protocol Version 4 (TCP/IPv4) voor IPv4 of Internet Protocol Version 6 (TCP/IPv6) voor IPv6.",
+ "setup_devices_windows_list_5": "Klik op Eigenschappen → De volgende DNS-serveradressen gebruiken .",
+ "setup_devices_windows_list_6": "Voer de adressen van je AdGuard Home-server in.",
+ "setup_devices_windows_title": "Windows",
+ "setup_dns_desc": "Configureer je router of andere apparaten om de volgende DNS-serveradressen te gebruiken:",
+ "setup_dns_notice_new": "Om DNS-over-HTTPS of DNS-over-TLS te gebruiken, moet je Encryptie configureren in de instellingen van AdGuard Home.",
+ "setup_dns_privacy_ioc_mac": "iOS en macOS configuratie",
+ "setup_dns_privacy_other_title": "Overig gebruik",
+ "setup_dns_quote_desc": "Om goed te functioneren vereist de AdGuard Home DNS-server een statisch IP-adres op je lokale netwerk. Anders kan je router hem uiteindelijk een ander IP-adres toewijzen en de DNS-bescherming verstoren",
+ "setup_dns_quote_title": "Wijs een statisch IP-adres toe aan de AdGuard Home DNS-server op je lokale netwerk",
+ "setup_dns_title": "Stel het AdGuard Home DNS-serveradres in",
+ "setup_dns_title_banner": "DNS-serverinstellingen",
+ "setup_guide": "Installatiehandleiding",
+ "setup_guide_auth_privacy": "Ik ga akkoord met het Privacybeleid en de Algemene voorwaarden van de AdGuard-websites",
+ "setup_guide_auth_subtitle": "Dit is vereist om het AdGuard Home-beheerderspaneel te beschermen tegen ongeautoriseerde toegang",
+ "setup_guide_auth_title": "Wachtwoordbeveiliging instellen",
+ "setup_guide_desc": "Je apparaten configureren om AdGuard Home te gebruiken",
+ "setup_guide_device_type": "Selecteer je apparaattype",
+ "setup_guide_greeting_button": "Wizard starten",
+ "setup_guide_greeting_desc": "Deze wizard helpt je AdGuard Home te configureren:",
+ "setup_guide_greeting_list_1": "Maak aanmeldgegevens aan om toegang te krijgen tot het beheerderspaneel",
+ "setup_guide_greeting_list_2": "Stel het webinterface-adres in voor AdGuard Home",
+ "setup_guide_greeting_list_3": "Stel het AdGuard Home DNS-serveradres in",
+ "setup_guide_greeting_list_4": "Krijg instructies voor je individuele apparaten",
+ "setup_guide_greeting_list_5": "Krijg toegang tot het AdGuard Home-dashboard ",
+ "setup_guide_greeting_title": "AdGuard Home instellen",
+ "setup_guide_title": "Je apparaten instellen om AdGuard Home te gebruiken",
+ "setup_ui_desc": "Gebruik deze adressen om toegang te krijgen tot de webgebruikersinterface van AdGuard Home. Wijzig indien nodig de poort",
+ "setup_ui_title": "Stel het webinterface-adres in voor AdGuard Home",
+ "setup_ui_title_banner": "Instellingen webinterface",
+ "show_blocked_responses": "Geblokkeerd",
+ "show_more_count": "+ %count% meer",
+ "show_processed_responses": "Verwerkt",
+ "sort_asc": "Sorteren van A tot Z",
+ "sort_desc": "Sorteren van Z naar A",
+ "source_label": "Bron",
+ "source_tooltip": "Bron",
+ "static_dhcp_leases_not_found": "Statische DHCP-leases niet gevonden",
+ "stats_adult": "Geblokkeerde 18+ websites",
+ "stats_query_domain": "Meest bezochte domeinen",
+ "status_table_header": "Status",
+ "subnet_error": "Bereikadressen moeten zich in hetzelfde subnet bevinden",
+ "sunday": "zondag",
+ "system_host_files": "Systeem host-bestanden",
+ "system_theme": "Systeemthema",
+ "table_client": "Gebruiker",
+ "tags_title": "Labels",
+ "test_upstreams": "Test upstream",
+ "threats_blocked": "Bedreigingen geblokkeerd",
+ "threats_blocked_tooltip": "Het aantal DNS-verzoeken dat door de AdGuard-browsebeveiligingsmodule is geblokkeerd",
+ "thursday": "donderdag",
+ "time_table_header": "Tijd",
+ "tls_cert_modal_description": "Om versleuteling te gebruiken, moet je een geldige TLS-certificaatketen voor je domein verstrekken. Je kunt een gratis TLS-certificaat krijgen van letsencrypt.org of er een kopen bij een vertrouwde certificeringsinstantie",
+ "tls_cert_path_label": "Volledig pad naar het certificaatbestand",
+ "tls_cert_path_option": "Voer het volledige pad naar het certificaatbestand in",
+ "tls_certificate": "TLS-certificaat",
+ "tls_certificate_expired": "Je TLS-certificaat is verlopen",
+ "tls_certificate_expiring": "Je TLS-certificaat verloopt bijna",
+ "tls_key_path_label": "Volledig pad naar het bestand met de privésleutel",
+ "tls_key_path_option": "Voer het volledige pad naar het bestand met de privésleutel in",
+ "top_blocked_domains": "Top geblokkeerde domeinen",
+ "top_clients": "Top gebruikers",
+ "top_upstreams": "Top upstreams",
+ "total": "Totaal",
+ "try_again": "Probeer opnieuw",
+ "tuesday": "dinsdag",
+ "type_value": "Type: %value%",
+ "unavailable_dhcp": "DHCP is niet beschikbaar",
+ "unavailable_dhcp_desc": "AdGuard Home kan geen DHCP-server draaien op uw OS",
+ "unblock": "Deblokkeren",
+ "unblock_client": "Client deblokkeren",
+ "unknown_filter": "Onbekend filter %filterId%",
+ "update_available": "Versie %version% is beschikbaar. Release-opmerkingen ",
+ "update_button": "Bijwerken",
+ "update_failed": "Automatisch bijwerken is mislukt. Volg deze stappen om handmatig bij te werken",
+ "update_filters_custom_hours": "Aangepast interval, in uren",
+ "update_filters_desc": "Stelt het interval voor automatisch bijwerken van filters in. Geldt voor zowel blokkeerlijsten als toelatingslijsten die via externe links zijn toegevoegd",
+ "update_filters_disable": "Uitschakelen",
+ "update_filters_interval_custom": "Aangepast",
+ "update_filters_interval_daily": "Elke dag",
+ "update_filters_interval_hourly": "Elk uur",
+ "update_filters_interval_weekly": "Elke week",
+ "update_filters_title": "Automatisch filters bijwerken",
+ "update_how_to": "Hoe bijwerken",
+ "updated_upstream_dns_toast": "Upstream-servers opgeslagen",
+ "updates_checked": "Een nieuwe versie van AdGuard Home is beschikbaar\n",
+ "updates_version_equal": "AdGuard Home is actueel",
+ "upstream": "Upstream",
+ "upstream_bootstrap_dns_faq": "IP-adressen van DNS-servers die worden gebruikt om de IP-adressen van DoH/DoT-upstream-resolvers om te zetten. Commentaar is niet toegestaan",
+ "upstream_bootstrap_dns_title": "Bootstrap DNS-servers, één per regel",
+ "upstream_dns": "Upstream DNS-servers",
+ "upstream_dns_addresses": "Serveradressen, één per regel",
+ "upstream_dns_addresses_faq": "Meer informatie over het configureren van upstream DNS-servers. Hier is een lijst met bekende DNS-providers om uit te kiezen.",
+ "upstream_dns_configured_in_file": "Geconfigureerd in %path%",
+ "upstream_dns_fastest_addr": "Snelste IP adres",
+ "upstream_dns_fastest_addr_desc": "Wacht op antwoorden van alle DNS-servers, meet de TCP-verbindingssnelheid voor elke server en retourneer het IP-adres van de server met de hoogste verbindingssnelheid",
+ "upstream_dns_fastest_addr_warning": "Deze modus kan DNS-zoekopdrachten aanzienlijk vertragen als één of meer upstream-servers niet reageren. Zorg ervoor dat je upstream-servers stabiel zijn en dat je upstream-timeout laag is",
+ "upstream_dns_load_balancing": "Load balancing",
+ "upstream_dns_load_balancing_desc": "Voer zoekopdrachten uit op één upstream-server tegelijk
AdGuard Home gebruikt een gewogen willekeurig algoritme om servers te selecteren met het laagste aantal mislukte zoekopdrachten en de laagste gemiddelde opzoektijd
",
+ "upstream_dns_parallel_requests": "Parallelle verzoeken",
+ "upstream_dns_parallel_requests_desc": "Parallelle zoekopdrachten gebruiken om het oplossen te versnellen door alle upstream-servers gelijktijdig te bevragen",
+ "upstream_dns_placeholder": "IP-adressen, sdns://, quic://, https://, h3://, of tls://",
+ "upstream_dns_servers_title": "Upstream DNS-servers",
+ "upstream_enable_reverse_lookup_desc": "Omgekeerde omzetting van IP-adressen van klanten naar hun hostnamen door PTR-zoekopdrachten naar de juiste resolvers te sturen: privé-DNS-servers voor lokale klanten of upstream servers voor klanten met openbare IP-adressen",
+ "upstream_enable_reverse_lookup_title": "Omgekeerd zoeken naar IP-adressen van clients inschakelen",
+ "upstream_example_multiple_upstreams_reserved": "%value%: meerdere upstreams voor specifieke domeinen ",
+ "upstream_example_tcp": "%ipv4%,%ipv6%: normale DNS (over TCP)",
+ "upstream_example_tcp_hostname": "%value%normale DNS (via TCP, hostnaam)",
+ "upstream_example_tcp_port": "%ipv4%,%ipv6%: normale DNS (over TCP, met poort)",
+ "upstream_example_udp": "%ipv4%,%ipv6%: normale DNS (over UDP)",
+ "upstream_example_udp_hostname": "%value%: normale DNS (over UDP, hostnaam)",
+ "upstream_example_udp_port": "%ipv4%,%ipv6%: normale DNS (over UDP, met poort)",
+ "upstream_example_upstream_comment": "%value%: een opmerking.",
+ "upstream_example_upstream_doh": "%value%: versleutelde DNS-over-HTTPS ",
+ "upstream_example_upstream_doh3": "%value%: versleutelde DNS-over-HTTPS met geforceerde HTTP/3 en geen terugval naar HTTP/2",
+ "upstream_example_upstream_doq": "%value%: versleutelde DNS-over-QUIC ",
+ "upstream_example_upstream_dot": "%value%: versleutelde DNS-over-TLS ",
+ "upstream_example_upstream_reserved": "%value%: een upstream voor specifieke domeinen ",
+ "upstream_example_upstream_sdns": "%value%: DNS Stamps voor DNSCrypt of DNS-over-HTTPS resolvers",
+ "upstream_examples_title": "Voorbeelden",
+ "upstream_fallback_title": "Terugval-DNS-servers, één per regel",
+ "upstream_fallback_title_faq": "Terugval-DNS-servers die gebruikt moeten worden wanneer de upstream-DNS-servers niet beschikbaar zijn. Gebruik dezelfde syntaxis als voor de upstream-DNS-servers",
+ "upstream_private_ptr_resolvers_desc": "PTR-, SOA- en NS-verzoeken voor ARPA-domeinen die privé-IP-adressen bevatten oplossen via privé-upstreamservers, DHCP, /etc/hosts. Indien uitgeschakeld, zal AdGuard Home op al dergelijke verzoeken reageren met NXDOMAIN",
+ "upstream_private_ptr_resolvers_title": "Private omgekeerde DNS-resolvers gebruiken",
+ "upstream_ptr": "Privé reverse DNS-servers, één per regel",
+ "upstream_ptr_faq_1": "AdGuard Home gebruikt deze DNS-servers voor privé PTR-, SOA- en NS-verzoeken. Privéverzoeken komen van clients met een privé-IP-adres en vragen om een ARPA-domein dat een subnet uit een privé-IP-adresbereik bevat, zoals %value%",
+ "upstream_ptr_faq_2": "Als dit veld leeg is, worden de standaard DNS-resolvers van je besturingssysteem gebruikt, met uitzondering van de IP-adressen van AdGuard Home",
+ "upstream_ptr_faq_3": "Standaard gebruikt AdGuard Home de volgende reverse DNS-resolvers: %value_1% en %value_2%",
+ "upstream_timeout": "Upstream time-out, in seconden",
+ "upstream_timeout_faq": "Tijd om te wachten op een reactie van de upstream-server",
+ "upstream_timeout_placeholder": "Voer de time-outduur van de upstreamserver in",
+ "upstreams": "Upstreams",
+ "url_label": "URL",
+ "use_saved_key": "Een bestaande privésleutel gebruiken",
+ "user_rules": "Gebruikersregels",
+ "user_rules_add_to_allowlist": "Toevoegen aan toelatingslijst",
+ "user_rules_allow_service": "Service toestaan",
+ "user_rules_browsing_security_disabled": "Browserbeveiliging uitgeschakeld",
+ "user_rules_check_button": "Controleren",
+ "user_rules_check_client_label": "Clientidentifier (naam, ClientID of IP-adres)",
+ "user_rules_check_client_placeholder": "Voer Client identificator in",
+ "user_rules_check_dns_record_type_label": "DNS-recordtype",
+ "user_rules_check_hostname_label": "Hostnaam of domeinnaam",
+ "user_rules_check_hostname_placeholder": "Voer doel in",
+ "user_rules_check_title": "Domeinfiltering globaal controleren of voor een cliënt",
+ "user_rules_cname": "CNAME: %cname%",
+ "user_rules_desc": "Voer één regel per regel in. Gebruik de syntaxis van Adblock-stijl of hosts",
+ "user_rules_disable_browsing_security": "Browsebeveiliging uitschakelen",
+ "user_rules_disable_filter": "Filter uitschakelen",
+ "user_rules_disable_parental_control": "Ouderlijk toezicht uitschakelen",
+ "user_rules_disable_safe_search": "Veilig zoeken uitschakelen",
+ "user_rules_dns_record_type_placeholder": "Selecteer type DNS-record",
+ "user_rules_dns_rewrite_removed": "Regel verwijderd uit DNS-herschrijving",
+ "user_rules_domain": "Domein: %value%",
+ "user_rules_domain_blocked": "Domein is geblokkeerd",
+ "user_rules_domain_is_allowed": "Domein is toegestaan",
+ "user_rules_domain_is_processed": "Domein is verwerkt",
+ "user_rules_edit_dns_rewrite": "DNS-herschrijven bewerken",
+ "user_rules_example_block": "||%domain% Toegang blokkeren tot %domain% en alle subdomeinen",
+ "user_rules_example_comment": "! Dit is een opmerking Om commentaar toe te voegen, begin de regel met ! of #",
+ "user_rules_example_comment_2": "# Dit is ook een opmerking Om commentaar toe te voegen, begin de regel met ! of #",
+ "user_rules_example_regex": "/REGEX/ Blokkeer toegang tot domeinen die overeenkomen met de opgegeven reguliere expressie",
+ "user_rules_example_response": "127.0.0.1 %domain% Geef 127.0.0.1 terug voor %domain% alleen, niet voor de subdomeinen ervan",
+ "user_rules_example_unblock": "@@||%domain% Toegang toestaan tot %domain% en alle subdomeinen",
+ "user_rules_filter_was_disabled": "%value% is uitgeschakeld",
+ "user_rules_ip": "IP-adressen: %ip%",
+ "user_rules_learn_more": "Meer informatie over het maken van je eigen hosts-lijsten.",
+ "user_rules_no_rules_matched": "Resultaat: Geen overeenkomende regels",
+ "user_rules_parental_control_disabled": "Ouderlijk toezicht uitgeschakeld",
+ "user_rules_placeholder": "Dit is een tijdelijke aanduiding",
+ "user_rules_reason": "Reden: %reason%",
+ "user_rules_reason_allowed_by": "Toegestaan door %source%",
+ "user_rules_reason_blocked_by": "Geblokkeerd door %source%",
+ "user_rules_reason_filtered_by": "Gefilterd door %source%",
+ "user_rules_redirected_to": "Omgeleid naar: %value%",
+ "user_rules_remove_dns_rewrite": "DNS-herschrijving verwijderen",
+ "user_rules_rewrite_rule_is_applied": "Herschrijven regel toegepast",
+ "user_rules_rewritten_to": "Herschreven naar: %value%",
+ "user_rules_rule": "Regel: %rule%",
+ "user_rules_rule_added": "Gebruikersregel toegevoegd: %rule% ",
+ "user_rules_rule_added_to_custom_filtering_rules": "Regel toegevoegd aan aangepaste filterregels",
+ "user_rules_rule_removed": "Regel verwijderd uit aangepaste filterregels",
+ "user_rules_safe_search_disabled": "Veilig zoeken uitgeschakeld",
+ "user_rules_service": "Service: %service%",
+ "user_rules_service_allowed": "De %value% -service was toegestaan",
+ "user_rules_source": "Bron: %value%",
+ "user_rules_source_allowlist": "Toelatingslijst: %value%",
+ "user_rules_source_blocklist": "Blokkeerlijst: %value%",
+ "user_rules_status": "Status: %reason%",
+ "user_rules_title": "Aangepaste regels",
+ "username_label": "Gebruikersnaam",
+ "username_placeholder": "Voer gebruikersnaam in",
+ "validated_with_dnssec": "Gevalideerd met DNSSEC",
+ "version_number": "Versie %value%",
+ "wednesday": "woensdag",
+ "whois": "Whois",
+ "yes": "Ja",
+ "yes_disable": "Ja, uitschakelen",
+ "yes_disallow": "Ja, niet toestaan",
+ "yes_remove": "Ja, verwijderen",
+ "yes_reset": "Ja, opnieuw instellen"
+}
diff --git a/client_v2/src/__locales/no.json b/client_v2/src/__locales/no.json
new file mode 100644
index 00000000000..4d7ef23278d
--- /dev/null
+++ b/client_v2/src/__locales/no.json
@@ -0,0 +1,388 @@
+{
+ "access_settings_title": "Tilgangsinnstillinger",
+ "actions_table_header": "Handlinger",
+ "add": "LEGG TIL",
+ "add_allowlist": "Legg til hviteliste",
+ "add_blocklist": "Legg til blokkeringsliste",
+ "add_persistent_client": "Legg til som vedvarende klient",
+ "add_to_allowlist": "LEGG TIL I HVITELISTEN",
+ "ads_blocked": "Reklamer blokkert",
+ "ads_blocked_card": "Reklamer blokkert",
+ "adult_websites_blocked_tooltip": "Antall voksennettsteder som ble blokkert",
+ "all_lists_up_to_date_toast": "Alle listene er allerede oppdatert",
+ "allowed": "Hvitelistet",
+ "allowlist_add": "Legg til hviteliste",
+ "allowlist_edit": "Rediger hviteliste",
+ "apply": "Benytt",
+ "autofix_warning_result": "Som følge av det vil alle DNS-forespørsler fra systemet ditt bli behandlet av AdGuard Home som standard.",
+ "autofix_warning_text": "Hvis du klikker på «Fiks», vil AdGuard Home sette opp systemet ditt til å bruke 'AdGuard Home'-DNS-tjeneren.",
+ "average_time_processing": "Gjennomsnittlig behandlingstid",
+ "average_upstream_response_time": "Gjennomsnittlig responstid fra oppstrømsserver",
+ "back": "Tilbake",
+ "block": "Blokker",
+ "block_for_this_client_only": "Blokker kun for denne klienten",
+ "blocked": "Blokkert",
+ "blocked_by_cname_or_ip": "Blokkert av CNAME eller IP",
+ "blocked_service": "Blokkert tjeneste",
+ "blocked_services": "Blokkerte tjenester",
+ "blocked_threats": "Blokkerte trusler",
+ "blocked_threats_chart": "Blokkerte trusler",
+ "blocklist_add_manual": "Legg til manuelt",
+ "blocklist_edit": "Rediger blokkeringsliste",
+ "blocklist_homepage": "Hjemmeside",
+ "blocklist_url_file_path": "URL eller filbane",
+ "bootstrap_dns": "Bootstrap-DNS-tjenere",
+ "browse": "Bla gjennom",
+ "cache_cleared": "DNS-bufferet ble vellykket tømt",
+ "cache_config_clear": "Tøm cache",
+ "cache_config_optimistic": "Optimistisk mellomlagring",
+ "cache_config_ttl_validation": "Minimums-mellomlagringslevetidsverdien må være mindre enn eller det samme som maksverdien",
+ "cache_enabled": "Aktiver cache",
+ "cancel": "Avbryt",
+ "cancel_btn": "Avbryt",
+ "category_tooltip": "Kategori",
+ "changes_saved_success": "Endringer lagret",
+ "check_dhcp_servers": "Se etter DHCP-tjenere",
+ "check_not_found": "Ikke funnet i filterlistene dine",
+ "check_updates_btn": "Se etter oppdateringer",
+ "clear_btn": "Tøm",
+ "client_add": "Legg til klient",
+ "client_details": "Klientdetaljer",
+ "client_id": "Klient-ID",
+ "client_id_placeholder": "Skriv inn klient-ID",
+ "client_identifier": "Identifikator",
+ "client_settings": "Klientinnstillinger",
+ "client_table_header": "Klient",
+ "clients_add_identifier": "Legg til identifikator",
+ "clients_add_placeholder": "Skriv inn klientnavnet",
+ "clients_dns_cache_size": "DNS-bufferstørrelse (i byte)",
+ "clients_not_found": "Ingen klienter ble funnet",
+ "clients_protection": "Beskyttelse",
+ "clients_tags": "Stempler",
+ "clients_use_global_settings": "Bruk de globale innstillingene",
+ "close": "Lukk",
+ "confirm": "BEKREFT",
+ "copied": "Kopiert til utklippstavlen",
+ "copy": "Kopier",
+ "copy_to_clipboard": "Kopier til utklippstavlen",
+ "custom_filtering_rules": "Selvvalgte filtreringsregler",
+ "dark_theme": "Mørkt tema",
+ "dashboard": "Kontrollsenter",
+ "delete_btn": "Slett",
+ "delete_table_action": "Slett",
+ "dhcp_add_static_lease": "Legg til statisk leieavtale",
+ "dhcp_config_saved": "Lagret DHCP-tjeneroppsettet",
+ "dhcp_edit_static_lease": "Rediger statisk leieavtale",
+ "dhcp_form_gateway_input": "Gateway-IP",
+ "dhcp_form_subnet_input": "Nettverksmaske",
+ "dhcp_leases_not_found": "Ingen DHCP-leieavtaler ble funnet",
+ "dhcp_leases_title": "DHCP-leieavtaler",
+ "dhcp_new_static_lease": "Ny statisk leieavtale",
+ "dhcp_reset_leases": "Tilbakestill alle leieavtaler",
+ "dhcp_reset_leases_success": "DHCP-leieavtaler tilbakestilt med suksess",
+ "dhcp_settings": "DHCP-innstillinger",
+ "dhcp_static_leases_not_found": "Ingen statiske DHCP-leieavtaler ble funnet",
+ "dhcp_table_hostname": "Vertsnavn",
+ "dhcp_table_ip_address": "IP-adresse",
+ "disable": "Deaktiver",
+ "disable_protection_btn": "Deaktiver beskyttelse",
+ "disabled_dhcp": "DHCP-tjeneren ble skrudd av",
+ "disallow_this_client": "Ikke tillat denne klienten",
+ "dns_access_settings_title": "Tilgangsinnstillinger",
+ "dns_allowed_clients": "Tillatte klienter",
+ "dns_blocking_mode": "Blokkeringsmodus",
+ "dns_blocking_mode_custom_ip": "Selvvalgt IP-adresse",
+ "dns_blocking_mode_default": "Standard",
+ "dns_blocking_mode_ipv4_label": "IPv4-blokkering",
+ "dns_blocking_mode_ipv6_label": "IPv6-blokkering",
+ "dns_blocking_mode_nxdomain": "NXDOMAIN",
+ "dns_blocking_mode_refused": "REFUSED",
+ "dns_blocking_mode_title": "Blokkeringsmodus",
+ "dns_bootstrap_dns_placeholder": "IP-adresser",
+ "dns_bootstrap_dns_title": "Bootstrap-DNS-tjenere",
+ "dns_bootstrap_servers": "Bootstrap-DNS-tjenere",
+ "dns_cache_config": "DNS-mellomlageroppsett",
+ "dns_cache_size": "Mellomlagerstørrelse",
+ "dns_cache_size_label": "DNS-bufferstørrelse (i byte)",
+ "dns_cache_size_title": "Mellomlagerstørrelse",
+ "dns_clear_cache": "Tøm cache",
+ "dns_clear_cache_cancel": "Avbryt",
+ "dns_clear_cache_confirm": "Ja, rens",
+ "dns_clear_cache_yes": "Ja, rens",
+ "dns_config": "DNS-tjeneroppsett",
+ "dns_disallowed_clients": "Klienter som skal avvises",
+ "dns_disallowed_clients_title": "Klienter som skal avvises",
+ "dns_disallowed_domains": "Blokkerte domener",
+ "dns_disallowed_domains_title": "Blokkerte domener",
+ "dns_edns_custom_placeholder": "Skriv inn IP-adresse",
+ "dns_fallback_dns_title": "Reserve DNS-servere",
+ "dns_fallback_servers": "Reserve DNS-servere",
+ "dns_optimistic_caching": "Optimistisk mellomlagring",
+ "dns_over_https": "DNS-over-HTTPS",
+ "dns_over_quic": "DNS-over-QUIC",
+ "dns_over_tls": "DNS-over-TLS",
+ "dns_override_max_ttl": "Overstyr maksimallevetiden",
+ "dns_override_max_ttl_title": "Overstyr maksimallevetiden",
+ "dns_override_min_ttl": "Overstyr minimumslevetiden",
+ "dns_override_min_ttl_title": "Overstyr minimumslevetiden",
+ "dns_privacy": "DNS-privatliv",
+ "dns_private_reverse_servers_title": "Private DNS-tjenere",
+ "dns_rate_limit": "Forespørselsfrekvensgrense",
+ "dns_rate_limit_allowlist_placeholder": "IP-adresser",
+ "dns_rate_limit_placeholder": "Skriv inn forespørselsfrekvensgrense",
+ "dns_rate_limit_title": "Forespørselsfrekvensgrense",
+ "dns_rewrites": "DNS-omdirigeringer",
+ "dns_server_addresses": "Serveradresser",
+ "dns_server_addresses_label": "Serveradresser, én per linje",
+ "dns_server_addresses_title": "Serveradresser",
+ "dns_server_config_title": "DNS-tjeneroppsett",
+ "dns_settings": "DNS",
+ "dns_test_upstreams": "Test oppstrømstilkoblinger",
+ "dns_upstream_servers_title": "Oppstrøms-DNS-tjenere",
+ "dns_upstream_timeout": "Oppstrøms tidsavbrudd",
+ "dns_upstream_timeout_title": "Oppstrøms tidsavbrudd",
+ "dns_upstream_validation_invalid": "Ugyldig format",
+ "domain": "Domene",
+ "domain_or_client": "Domene eller klient",
+ "download_mobileconfig": "Last ned oppsettsfil",
+ "edit_table_action": "Rediger",
+ "enable": "Aktiver",
+ "enabled_dhcp": "DHCP-tjeneren ble skrudd på",
+ "encryption_certificate_path": "Filbanen til sertifikatet",
+ "encryption_certificates": "Sertifikater",
+ "encryption_certificates_input": "Kopier / lim inn dine PEM-kodede sertifikater her.",
+ "encryption_certificates_source_content": "Lim inn innholdet til sertifikatet",
+ "encryption_certificates_source_path": "Bestem en filbane for sertifikater",
+ "encryption_chain_invalid": "Sertifikatskjeden er ugyldig",
+ "encryption_chain_valid": "Sertifikatskjeden er gyldig",
+ "encryption_config_saved": "Krypteringsoppsettet ble lagret",
+ "encryption_doq": "DNS-over-QUIC-port",
+ "encryption_dot": "'DNS-over-TLS'-port",
+ "encryption_https": "HTTPS-port",
+ "encryption_key": "Privat nøkkel",
+ "encryption_key_input": "Kopier / lim inn ditt sertifikats PEM-kodede private nøkkel her.",
+ "encryption_key_source_content": "Lim inn innholdet til den private nøkkelen",
+ "encryption_plain_dns": "Ordinær DNS",
+ "encryption_plain_dns_error": "For å deaktivere vanlig DNS, aktiver minst ett kryptert DNS-protokoll",
+ "encryption_private_key_path": "Filbanen til den private nøkkelen",
+ "encryption_server": "Tjenerens navn",
+ "encryption_server_enter": "Skriv inn domenenavnet ditt",
+ "encryption_title": "Kryptering",
+ "enter_cache_size": "Skriv inn mellomlagerstørrelse (i bytes)",
+ "enter_ip_address_placeholder": "Skriv inn IP-adresse",
+ "error": "Feil",
+ "fallback_dns": "Reserve DNS-servere",
+ "filter_category_general": "General",
+ "filter_category_general_desc": "Lister som blokkerer sporing og reklamer på de fleste enheter",
+ "filter_category_other": "Andre",
+ "filter_category_other_desc": "Andre blokkeringslister",
+ "filter_category_regional": "Regional",
+ "filter_category_regional_desc": "Lister som fokuserer på regionale reklamer og sporingstjenere",
+ "filter_category_security": "Sikkerhet",
+ "filter_category_security_desc": "Lister som spesialiserer seg på å blokkere skadevare-, phishing- eller svindeldomener",
+ "filters": "Filtre",
+ "forgot_password": "Har du glemt passordet?",
+ "form_enter_blocked_response_ttl": "Skriv inn TTL for blokkerte svar (sekunder)",
+ "form_error_client_id_format": "Ugyldig ID-klientformat",
+ "form_error_domain_format": "Ugyldig domeneformat",
+ "form_error_equal": "Burde ikke være de samme",
+ "form_error_format": "Ugyldig format",
+ "form_error_gateway_ip": "Leie kan ikke ha IP-adresse til gateway",
+ "form_error_ip4_format": "Ugyldig IPv4-format",
+ "form_error_ip6_format": "Ugyldig IPv6-format",
+ "form_error_ip_format": "Ugyldig IP-adresse",
+ "form_error_mac_format": "Ugyldig MAC-format",
+ "form_error_password": "Passordet samsvarer ikke",
+ "form_error_port": "Skriv inn en gyldig portverdi",
+ "form_error_port_range": "Skriv inn et portnummer i området 80-65535",
+ "form_error_port_unsafe": "Denne porten er ikke trygg",
+ "form_error_server_name": "Ugyldig tjenernavn",
+ "form_error_url_format": "Skriv inn en gyldig URL eller filbane",
+ "friday": "Fredag",
+ "gateway_or_subnet_invalid": "Ugyldig undernettmaske",
+ "general_statistics": "Generelle statistikker",
+ "ignore_domains_title": "Ignorerte domener",
+ "inactivity_schedule_add": "Legg til tidsplan",
+ "inactivity_schedule_all_day": "Hele dagen",
+ "inactivity_schedule_edit": "Endre tidsplan",
+ "install_auth_confirm": "Bekreft passord",
+ "install_auth_password": "Passord",
+ "install_auth_password_enter": "Angi passord",
+ "install_auth_username": "Brukernavn",
+ "install_auth_username_enter": "Skriv inn brukernavn",
+ "install_saved": "Lagringen var vellykket",
+ "install_settings_all_interfaces": "Alle grensesnitt",
+ "install_settings_port": "Port",
+ "install_static_error": "AdGuard Home kan ikke sette opp automatisk i dette nettverksgrensesnitt. Vennligst let opp anvisningen for hvordan man gjør det manuellt.",
+ "install_static_ok": "Gode nyheter! Den statiske IP-adressen er allerede satt opp",
+ "install_step": "Trinn",
+ "install_submit_title": "Gratulerer!",
+ "ip_address": "IP-adresse",
+ "known_tracker": "Kjent sporer",
+ "language": "Språk",
+ "light_theme": "Lyst tema",
+ "login": "LOGG INN",
+ "logout": "Logg av",
+ "make_static": "Gjør statisk",
+ "milliseconds_abbreviation": "ms",
+ "monday": "Mandag",
+ "name": "Navn",
+ "name_label": "Navn",
+ "name_table_header": "Navn",
+ "name_tooltip": "Navn",
+ "next": "Neste",
+ "no": "Nei",
+ "none_text": "Ingen",
+ "nothing_found": "Ingenting ble funnet",
+ "notify_undo": "Angre",
+ "notify_updated": "Oppdatert",
+ "parental_control": "Foreldrekontroll",
+ "parental_group_cdn": "CDN",
+ "parental_group_development": "Programvareutvikling",
+ "parental_group_gaming": "Spilling",
+ "parental_group_hosting": "Webhotell",
+ "parental_group_privacy": "Personvernverktøy",
+ "parental_group_shopping": "Handling",
+ "parental_group_social": "Sosiale nettverk",
+ "parental_group_social_network": "Sosiale nettverk",
+ "parental_group_software": "Programvareutvikling",
+ "password_label": "Passord",
+ "password_placeholder": "Angi passord",
+ "plain_dns": "Ordinær DNS",
+ "privacy_policy": "Personvernretningslinjer",
+ "protection": "Beskyttelse",
+ "protocol": "Velg protokoll",
+ "query_log_allowed": "Hvitelistet",
+ "query_log_blocked": "Blokkert",
+ "query_log_blocked_services": "Blokkerte tjenester",
+ "query_log_blocked_threats": "Blokkerte trusler",
+ "query_log_custom_filtering_rules": "Selvvalgte filtreringsregler",
+ "query_log_nothing_available": "Ingenting ble funnet",
+ "query_log_processed": "Behandlet",
+ "query_log_rewritten": "Omskrevet",
+ "query_log_safe_search": "Aktivert sikkert søk",
+ "query_log_strict_search": "Bruk anførselstegn for strenge søk",
+ "rate_limit_subnet_len_ipv4_error": "IPv4 subnet prefixlengde bør være mellom 0 og 32",
+ "rate_limit_subnet_len_ipv6_error": "IPv6 subnet prefixlengde bør være mellom 0 og 128",
+ "refresh_btn": "Oppfrisk",
+ "refresh_statics": "Oppfrisk statistikkene",
+ "release_notes": "Produktmerknader",
+ "remove": "Fjern",
+ "report_an_issue": "Rapporter et problem",
+ "request_table_header": "Forespørsel",
+ "requests_table_header": "Forespørsler",
+ "reset": "Tilbakestill",
+ "reset_settings": "Tilbakestill innstillinger",
+ "reset_settings_confirm": "Ja, tilbakestill",
+ "response_details": "Svardetaljer",
+ "response_time": "Responstid",
+ "result": "Resultat",
+ "rewrite_add": "Legg til DNS-omdirigering",
+ "rewrite_edit": "Rediger DNS-omskriving",
+ "rewrite_hosts_applied": "Omskrevet av 'hosts'-oppføringen",
+ "rewrite_settings_updated": "DNS-omskrivingsinnstillingene er oppdatert",
+ "rewrite_updated": "DNS-omskriving ble oppdatert",
+ "rewritten": "Omskrevet",
+ "rules_label": "Regler",
+ "safe_browsing": "Sikker surfing",
+ "safe_search": "Aktivert sikkert søk",
+ "saturday": "Lørdag",
+ "save": "Lagre",
+ "save_btn": "Lagre",
+ "search_placeholder": "Søk",
+ "server_config_blocking_mode": "Blokkeringsmodus",
+ "server_config_blocking_mode_ipv4": "IPv4-blokkering",
+ "server_config_blocking_mode_ipv6": "IPv6-blokkering",
+ "server_config_custom_ip": "Selvvalgt IP-adresse",
+ "server_config_default": "Standard",
+ "server_config_disable_ipv6": "Skru av IPv6",
+ "server_config_dnssec_enable": "Skru på DNSSEC",
+ "server_config_edns_enable": "Aktiver EDNS-klientundernett",
+ "server_config_nxdomain": "NXDOMAIN",
+ "server_config_rate_limit_placeholder": "Skriv inn forespørselsfrekvensgrense",
+ "server_config_rate_limit_whitelist_faq": "IP-adresser unntatt fra ratebegrensning",
+ "server_config_refused": "REFUSED",
+ "server_config_subnet_len_ipv4": "Lengde på subnettprefiks for IPv4-adresser",
+ "server_config_subnet_len_ipv4_faq": "Subnet prefixlengde for IPv4-adresser som brukes til ratebegrensning. Standard er 24",
+ "server_config_subnet_len_ipv6": "Subnet prefixlengde for IPv6-adresser",
+ "server_config_subnet_len_ipv6_faq": "Subnet prefixlengde for IPv6-adresser som brukes til ratebegrensning. Standard er 56",
+ "set_static_ip": "Velg en statisk IP-adresse",
+ "settings": "Innstillinger",
+ "settings_browsing_security": "Nettlesersikkerhet",
+ "settings_confirm_clear_statistics": "Vil du tømme statistikkene?",
+ "settings_custom": "Egendefinert",
+ "settings_general_short": "General",
+ "settings_global": "Global",
+ "settings_notify_changes_saved": "Endringer lagret",
+ "settings_notify_statistics_cleared": "Statistikker tømt",
+ "settings_parental_control": "Foreldrekontroll",
+ "settings_rule_syntax": "Regel syntaks",
+ "settings_safe_search": "Aktivert sikkert søk",
+ "settings_statistics": "Statistikk",
+ "settings_statistics_clear": "Tilbakestill statistikker",
+ "settings_statistics_retention": "Statistikkbeholding",
+ "settings_yes_clear": "Ja, rens",
+ "setup_devices_router_title": "Ruter",
+ "setup_dns_privacy_ioc_mac": "iOS- og macOS-oppsett",
+ "setup_dns_privacy_other_title": "Andre implementeringer",
+ "show_blocked_responses": "Blokkert",
+ "show_processed_responses": "Behandlet",
+ "source_label": "Kilde",
+ "source_tooltip": "Kilde",
+ "stats_adult": "Blokkerte voksennettsteder",
+ "stats_query_domain": "Mest forespurte domener",
+ "status_table_header": "Status",
+ "sunday": "Søndag",
+ "system_host_files": "System-'hosts'-filer",
+ "table_client": "Klient",
+ "tags_title": "Stempler",
+ "test_upstreams": "Test oppstrømstilkoblinger",
+ "threats_blocked": "Trusler blokkert",
+ "thursday": "Torsdag",
+ "time_table_header": "Tidspunkt",
+ "top_blocked_domains": "Mest blokkerte domener",
+ "top_clients": "Vanligste klienter",
+ "top_upstreams": "Topp oppstrøms servere",
+ "total": "Totalt",
+ "try_again": "Prøv på nytt",
+ "tuesday": "Tirsdag",
+ "unavailable_dhcp": "DHCP er utilgjengelig",
+ "unavailable_dhcp_desc": "AdGuard Home kan ikke kjøre en DHCP-tjener på ditt OS",
+ "unblock": "Fjern blokkering",
+ "update_button": "Oppdater",
+ "update_filters_disable": "Deaktiver",
+ "update_filters_interval_custom": "Egendefinert",
+ "update_filters_interval_daily": "Hver dag",
+ "update_filters_interval_hourly": "Hver time",
+ "update_filters_title": "Oppdater filtre automatisk",
+ "updates_checked": "En ny versjon av AdGuard Home er tilgjengelig",
+ "updates_version_equal": "AdGuard Home er fullt oppdatert",
+ "upstream": "Oppstrøms server",
+ "upstream_dns": "Oppstrøms-DNS-tjenere",
+ "upstream_dns_addresses": "Serveradresser, én per linje",
+ "upstream_dns_fastest_addr": "Raskeste IP-adresse",
+ "upstream_dns_parallel_requests": "Parallelle forespørsler",
+ "upstream_dns_servers_title": "Oppstrøms-DNS-tjenere",
+ "upstream_examples_title": "Eksempler",
+ "upstream_private_ptr_resolvers_title": "Bruk private omvendte DNS-løsere",
+ "upstreams": "Oppstrømstjenere",
+ "url_label": "URL",
+ "user_rules": "Brukerfilter",
+ "user_rules_add_to_allowlist": "LEGG TIL I HVITELISTEN",
+ "user_rules_check_button": "Sjekk",
+ "user_rules_check_client_placeholder": "Skriv inn klientidentifikator",
+ "user_rules_check_hostname_label": "Vertsnavn eller domenenavn",
+ "user_rules_disable_filter": "Deaktiver filter",
+ "user_rules_disable_parental_control": "Skru av foreldrekontroll",
+ "user_rules_dns_record_type_placeholder": "Velg type DNS-post",
+ "user_rules_edit_dns_rewrite": "Rediger DNS-omskriving",
+ "user_rules_rewrite_rule_is_applied": "Benyttet omdirigeringsregelen",
+ "username_label": "Brukernavn",
+ "username_placeholder": "Skriv inn brukernavn",
+ "validated_with_dnssec": "Validert med DNSSEC",
+ "wednesday": "Onsdag",
+ "whois": "Whois",
+ "yes": "Ja",
+ "yes_disable": "Ja, deaktiver",
+ "yes_reset": "Ja, tilbakestill"
+}
diff --git a/client_v2/src/__locales/pl.json b/client_v2/src/__locales/pl.json
new file mode 100644
index 00000000000..aac7b25cc08
--- /dev/null
+++ b/client_v2/src/__locales/pl.json
@@ -0,0 +1,390 @@
+{
+ "access_settings_title": "Ustawienia dostępu",
+ "actions_table_header": "Akcje",
+ "add": "Dodaj",
+ "add_allowlist": "Dodaj listę dozwolonych",
+ "add_blocklist": "Dodaj listę zablokowanych",
+ "add_persistent_client": "Dodaj do zapisanych klientów",
+ "add_to_allowlist": "Dodaj do listy dozwolonych",
+ "ads_blocked": "Zablokowanych reklam",
+ "ads_blocked_card": "Zablokowanych reklam",
+ "adult_websites_blocked_tooltip": "Liczba zablokowanych witryn dla dorosłych",
+ "all_lists_up_to_date_toast": "Wszystkie listy są już aktualne",
+ "allowed": "Na białej liście",
+ "allowlist_add": "Dodaj listę dozwolonych",
+ "allowlist_edit": "Edytuj listę dozwolonych",
+ "apply": "Zastosuj",
+ "autofix_warning_result": "W rezultacie wszystkie żądania DNS z Twojego systemu będą domyślnie przetwarzane przez AdGuardHome.",
+ "autofix_warning_text": "Jeśli klikniesz „Napraw”, AdGuardHome skonfiguruje system do korzystania z serwera DNS AdGuardHome.",
+ "average_time_processing": "Średni czas przetwarzania",
+ "average_upstream_response_time": "Średni czas odpowiedzi serwera nadrzędnego",
+ "back": "Wróć",
+ "block": "Blokuj",
+ "block_for_this_client_only": "Zablokuj tylko tego klienta",
+ "blocked": "Zablokowane",
+ "blocked_by_cname_or_ip": "Zablokowany przez rekord CNAME lub adres IP",
+ "blocked_service": "Zablokowana usługa",
+ "blocked_services": "Zablokowane usługi",
+ "blocked_threats": "Zablokowane zagrożenia",
+ "blocked_threats_chart": "Zablokowane zagrożenia",
+ "blocklist_add_manual": "Dodaj ręcznie",
+ "blocklist_edit": "Edytuj listę zablokowanych",
+ "blocklist_homepage": "Strona internetowa",
+ "blocklist_url_file_path": "Adres URL lub plik",
+ "bootstrap_dns": "Serwery DNS Bootstrap",
+ "browse": "Przeglądaj",
+ "cache_cleared": "Pamięć podręczna DNS została pomyślnie wyczyszczona",
+ "cache_config_clear": "Wyczyść pamięć podręczną",
+ "cache_config_optimistic": "Optymistyczne buforowanie",
+ "cache_config_ttl_validation": "Minimalne nadpisanie pamięci podręcznej TTL musi być mniejsze lub równe maksimum",
+ "cache_enabled": "Aktywuj pamięć podręczną",
+ "cancel": "Anuluj",
+ "cancel_btn": "Anuluj",
+ "category_tooltip": "Kategoria",
+ "changes_saved_success": "Zmiany zostały zapisane",
+ "check_dhcp_servers": "Sprawdź serwery DHCP",
+ "check_not_found": "Nie znaleziono na Twoich listach filtrów",
+ "check_updates_btn": "Sprawdź aktualizacje",
+ "clear_btn": "Wyczyść",
+ "client_add": "Dodaj klienta",
+ "client_details": "Szczegóły klienta",
+ "client_id": "ClientID",
+ "client_id_placeholder": "Wpisz ClientID",
+ "client_identifier": "Identyfikator",
+ "client_settings": "Ustawienia klienta",
+ "client_table_header": "Klient",
+ "clients_add_identifier": "Dodaj identyfikator",
+ "clients_add_placeholder": "Wpisz nazwę klienta",
+ "clients_dns_cache_size": "Rozmiar pamięci podręcznej DNS, w bajtach",
+ "clients_not_found": "Nie znaleziono klientów",
+ "clients_protection": "Ochrona",
+ "clients_tags": "Tagi",
+ "clients_use_global_settings": "Użyj ustawień globalnych",
+ "close": "Zamknij",
+ "confirm": "Potwierdź",
+ "copied": "Skopiowano do schowka",
+ "copy": "Kopiuj",
+ "copy_to_clipboard": "Kopiuj do schowka",
+ "custom_filtering_rules": "Niestandardowe reguły filtrowania",
+ "dark_theme": "Ciemny motyw",
+ "dashboard": "Panel kontrolny",
+ "delete_btn": "Usuń",
+ "delete_table_action": "Usuń",
+ "dhcp_add_static_lease": "Dodaj dzierżawę statyczną",
+ "dhcp_config_saved": "Konfiguracja DHCP została pomyślnie zapisana",
+ "dhcp_edit_static_lease": "Edytuj dzierżawę statyczną",
+ "dhcp_form_gateway_input": "Adres IP bramy",
+ "dhcp_form_subnet_input": "Maska podsieci",
+ "dhcp_leases_not_found": "Nie znaleziono dzierżaw DHCP",
+ "dhcp_leases_title": "Dzierżawa DHCP",
+ "dhcp_new_static_lease": "Nowa dzierżawa statyczna",
+ "dhcp_reset_leases": "Zresetuj wszystkie dzierżawy",
+ "dhcp_reset_leases_success": "Pomyślnie zresetowano dzierżawy DHCP",
+ "dhcp_settings": "Ustawienia DHCP",
+ "dhcp_static_leases_not_found": "Nie znaleziono statycznych dzierżaw DHCP",
+ "dhcp_table_hostname": "Nazwa hosta",
+ "dhcp_table_ip_address": "Adres IP",
+ "disable": "Wyłącz",
+ "disable_protection_btn": "Wyłącz ochronę",
+ "disabled_dhcp": "Serwer DHCP wyłączony",
+ "disallow_this_client": "Odrzuć tego klienta",
+ "dns_access_settings_title": "Ustawienia dostępu",
+ "dns_allowed_clients": "Dozwoleni klienci",
+ "dns_blocking_mode": "Tryb blokowania",
+ "dns_blocking_mode_custom_ip": "Niestandardowy adres IP",
+ "dns_blocking_mode_default": "Tryb domyślny",
+ "dns_blocking_mode_ipv4_label": "Blokowanie IPv4",
+ "dns_blocking_mode_ipv6_label": "Blokowanie IPv6",
+ "dns_blocking_mode_nxdomain": "NXDOMAIN",
+ "dns_blocking_mode_refused": "REFUSED",
+ "dns_blocking_mode_title": "Tryb blokowania",
+ "dns_bootstrap_dns_placeholder": "Adresy IP",
+ "dns_bootstrap_dns_title": "Serwery DNS Bootstrap",
+ "dns_bootstrap_servers": "Serwery DNS Bootstrap",
+ "dns_cache_config": "Konfiguracja pamięci podręcznej DNS",
+ "dns_cache_size": "Rozmiar pamięci podręcznej",
+ "dns_cache_size_label": "Rozmiar pamięci podręcznej DNS, w bajtach",
+ "dns_cache_size_title": "Rozmiar pamięci podręcznej",
+ "dns_clear_cache": "Wyczyść pamięć podręczną",
+ "dns_clear_cache_cancel": "Anuluj",
+ "dns_clear_cache_confirm": "Tak, wyczyść",
+ "dns_clear_cache_yes": "Tak, wyczyść",
+ "dns_config": "Konfiguracja serwera DNS",
+ "dns_disallowed_clients": "Niedozwoleni klienci",
+ "dns_disallowed_clients_title": "Niedozwoleni klienci",
+ "dns_disallowed_domains": "Niedozwolone domeny",
+ "dns_disallowed_domains_title": "Niedozwolone domeny",
+ "dns_edns_custom_placeholder": "Wprowadź adres IP",
+ "dns_fallback_dns_title": "Rezerwowe serwery DNS",
+ "dns_fallback_servers": "Rezerwowe serwery DNS",
+ "dns_optimistic_caching": "Optymistyczne buforowanie",
+ "dns_over_https": "DNS-przez-HTTPS",
+ "dns_over_quic": "DNS-over-QUIC",
+ "dns_over_tls": "DNS-przez-TLS",
+ "dns_override_max_ttl": "Nadpisz maksymalną wartość TTL",
+ "dns_override_max_ttl_title": "Nadpisz maksymalną wartość TTL",
+ "dns_override_min_ttl": "Nadpisz minimalną wartość TTL",
+ "dns_override_min_ttl_title": "Nadpisz minimalną wartość TTL",
+ "dns_privacy": "Prywatny DNS",
+ "dns_private_reverse_servers_title": "Prywatne odwrotne serwery DNS",
+ "dns_rate_limit": "Limit ilościowy",
+ "dns_rate_limit_allowlist_placeholder": "Adresy IP",
+ "dns_rate_limit_placeholder": "Wpisz limit ilościowy",
+ "dns_rate_limit_title": "Limit ilościowy",
+ "dns_rewrites": "Przepisywanie DNS",
+ "dns_server_addresses": "Adresy serwera",
+ "dns_server_addresses_label": "Adresy serwerów, po jednym w wierszu",
+ "dns_server_addresses_title": "Adresy serwera",
+ "dns_server_config_title": "Konfiguracja serwera DNS",
+ "dns_settings": "DNS",
+ "dns_test_upstreams": "Test głównych serwerów DNS",
+ "dns_upstream_servers_title": "Główne serwery DNS",
+ "dns_upstream_timeout": "Czas oczekiwania na odpowiedź",
+ "dns_upstream_timeout_title": "Czas oczekiwania na odpowiedź",
+ "dns_upstream_validation_invalid": "Niepoprawny format",
+ "domain": "Domena",
+ "domain_or_client": "Domena lub klient",
+ "download_mobileconfig": "Pobierz plik konfiguracyjny",
+ "edit_table_action": "Edytuj",
+ "enable": "Włącz",
+ "enabled_dhcp": "Serwer DHCP włączony",
+ "encryption_certificate_path": "Ścieżka certyfikatu",
+ "encryption_certificates": "Certyfikaty",
+ "encryption_certificates_input": "Kopiuj/wklej tutaj swoje zakodowane certyfikaty PEM.",
+ "encryption_certificates_source_content": "Wklej zawartość certyfikatów",
+ "encryption_certificates_source_path": "Ustaw ścieżkę do pliku certyfikatów",
+ "encryption_chain_invalid": "Łańcuch certyfikatu jest nieprawidłowy",
+ "encryption_chain_valid": "Łańcuch certyfikatów jest prawidłowy",
+ "encryption_config_saved": "Konfiguracja szyfrowania została zapisana",
+ "encryption_doq": "Port DNS-over-QUIC",
+ "encryption_dot": "Port DNS-over-TLS",
+ "encryption_https": "Port HTTPS",
+ "encryption_key": "Klucz prywatny",
+ "encryption_key_input": "Tutaj kopiuj/wklej klucze prywatne zakodowane w PEM do swojego certyfikatu.",
+ "encryption_key_source_content": "Wklej zawartość klucza prywatnego",
+ "encryption_plain_dns": "Zwykły DNS",
+ "encryption_plain_dns_error": "Aby wyłączyć zwykły DNS, włącz co najmniej jeden szyfrowany protokół DNS",
+ "encryption_private_key_path": "Ścieżka klucza prywatnego",
+ "encryption_server": "Nazwa Serwera",
+ "encryption_server_enter": "Wpisz swoją nazwę domeny",
+ "encryption_title": "Szyfrowanie",
+ "enter_cache_size": "Wpisz rozmiar pamięci podręcznej (w bajtach)",
+ "enter_ip_address_placeholder": "Wprowadź adres IP",
+ "error": "Błąd",
+ "fallback_dns": "Rezerwowe serwery DNS",
+ "filter_category_general": "General",
+ "filter_category_general_desc": "Listy, które blokują skrypty śledzące i reklamy na większości urządzeń",
+ "filter_category_other": "Inne",
+ "filter_category_other_desc": "Inne listy zablokowanych",
+ "filter_category_regional": "Regionalne",
+ "filter_category_regional_desc": "Listy, które koncentrują się na reklamach regionalnych i serwerach ze skryptami śledzącymi",
+ "filter_category_security": "Bezpieczeństwo",
+ "filter_category_security_desc": "Listy zaprojektowane specjalnie w celu blokowania złośliwych, phishingowych i oszukańczych domen",
+ "filters": "Filtry",
+ "forgot_password": "Zapomniano hasła?",
+ "form_enter_blocked_response_ttl": "Wprowadź TTL zablokowanej odpowiedzi (sekundy)",
+ "form_error_client_id_format": "ClientID musi zawierać tylko cyfry, małe litery i myślniki",
+ "form_error_domain_format": "Niepoprawny format domeny",
+ "form_error_equal": "Nie mogą być równe",
+ "form_error_format": "Niepoprawny format",
+ "form_error_gateway_ip": "Lease nie może mieć adresu IP bramy",
+ "form_error_ip4_format": "Nieprawidłowy adres IPv4",
+ "form_error_ip6_format": "Nieprawidłowy adres IPv6",
+ "form_error_ip_format": "Zły adres IP",
+ "form_error_mac_format": "Nieprawidłowy adres MAC",
+ "form_error_password": "Niezgodne hasło",
+ "form_error_port": "Wprowadź prawidłowy numer portu",
+ "form_error_port_range": "Wpisz numer portu z zakresu 80-65535",
+ "form_error_port_unsafe": "Niebezpieczny port",
+ "form_error_server_name": "Nieprawidłowa nazwa serwera",
+ "form_error_url_format": "Wprowadź prawidłowy adres URL lub ścieżkę do pliku",
+ "friday": "Piątek",
+ "gateway_or_subnet_invalid": "Nieprawidłowa maska podsieci",
+ "general_statistics": "Ogólne statystyki",
+ "ignore_domains_title": "Ignorowane domeny",
+ "inactivity_schedule_add": "Dodaj harmonogram",
+ "inactivity_schedule_all_day": "Cały dzień",
+ "inactivity_schedule_edit": "Edytuj harmonogram",
+ "install_auth_confirm": "Potwierdź hasło",
+ "install_auth_password": "Hasło",
+ "install_auth_password_enter": "Wpisz hasło",
+ "install_auth_username": "Nazwa użytkownika",
+ "install_auth_username_enter": "Wpisz nazwę użytkownika",
+ "install_saved": "Pomyślnie zapisany",
+ "install_settings_all_interfaces": "Wszystkie interfejsy",
+ "install_settings_port": "Port",
+ "install_static_error": "AdGuard Home nie może skonfigurować go automatycznie dla tego interfejsu sieciowego. Poszukaj instrukcji, jak to zrobić ręcznie.",
+ "install_static_ok": "Dobre wieści! Statyczny adres IP jest już skonfigurowany",
+ "install_step": "Krok",
+ "install_submit_title": "Gratulacje!",
+ "ip_address": "Adres IP",
+ "known_tracker": "Znany element śledzący",
+ "language": "Język",
+ "light_theme": "Jasny motyw",
+ "login": "Zaloguj się",
+ "logout": "Wyloguj się",
+ "make_static": "Ustaw adres statyczny",
+ "milliseconds_abbreviation": "ms",
+ "monday": "Poniedziałek",
+ "name": "Nazwa",
+ "name_label": "Nazwa",
+ "name_table_header": "Nazwa",
+ "name_tooltip": "Nazwa",
+ "next": "Dalej",
+ "no": "Nie",
+ "none_text": "Brak",
+ "nothing_found": "Nic nie znaleziono",
+ "notify_undo": "Cofnij",
+ "notify_updated": "Zaktualizowano",
+ "parental_control": "Kontrola rodzicielska",
+ "parental_group_cdn": "CDN",
+ "parental_group_development": "Rozwój oprogramowania",
+ "parental_group_gaming": "Gier",
+ "parental_group_hosting": "Hosting web",
+ "parental_group_privacy": "Narzędzia prywatności",
+ "parental_group_shopping": "Zakupy",
+ "parental_group_social": "Portale społecznościowe",
+ "parental_group_social_network": "Portale społecznościowe",
+ "parental_group_software": "Rozwój oprogramowania",
+ "parental_group_streaming": "Streaming",
+ "password_label": "Hasło",
+ "password_placeholder": "Wpisz hasło",
+ "plain_dns": "Zwykły DNS",
+ "privacy_policy": "Polityka prywatności",
+ "protection": "Ochrona",
+ "protocol": "Protokół",
+ "query_log_allowed": "Na białej liście",
+ "query_log_blocked": "Zablokowane",
+ "query_log_blocked_services": "Zablokowane usługi",
+ "query_log_blocked_threats": "Zablokowane zagrożenia",
+ "query_log_custom_filtering_rules": "Niestandardowe reguły filtrowania",
+ "query_log_nothing_available": "Nic nie znaleziono",
+ "query_log_processed": "Przetworzony",
+ "query_log_rewritten": "Przepisane",
+ "query_log_safe_search": "Bezpieczne wyszukiwanie",
+ "query_log_strict_search": "Używaj podwójnych cudzysłowów do ścisłego wyszukiwania",
+ "rate_limit_subnet_len_ipv4_error": "Długość maski podsieci IPv4 powinna wynosić od 0 do 32",
+ "rate_limit_subnet_len_ipv6_error": "Długość prefiksu podsieci IPv6 powinna wynosić od 0 do 128",
+ "refresh_btn": "Odśwież",
+ "refresh_statics": "Odśwież statystyki",
+ "release_notes": "Informacje o wydaniu",
+ "remove": "Usuń",
+ "report_an_issue": "Zgłoś problem",
+ "request_table_header": "Żądanie",
+ "requests_table_header": "Żądania",
+ "reset": "Zresetuj",
+ "reset_settings": "Resetowanie ustawień",
+ "reset_settings_confirm": "Tak, usuń",
+ "response_details": "Szczegóły odpowiedzi",
+ "response_time": "Czas odpowiedzi",
+ "result": "Wynik",
+ "rewrite_add": "Dodaj przepisywanie DNS",
+ "rewrite_edit": "Edytuj przepisywanie DNS",
+ "rewrite_hosts_applied": "Przepisana reguła w pliku hosts",
+ "rewrite_settings_updated": "Ustawienia przepisywania DNS zostały pomyślnie zaktualizowane",
+ "rewrite_updated": "Pomyślnie zaktualizowano przepisywanie DNS",
+ "rewritten": "Przepisane",
+ "rules_label": "Reguły",
+ "safe_browsing": "Bezpieczne przeglądanie",
+ "safe_search": "Bezpieczne wyszukiwanie",
+ "saturday": "Sobota",
+ "save": "Zapisz",
+ "save_btn": "Zapisz",
+ "search_placeholder": "Wyszukaj",
+ "server_config_blocking_mode": "Tryb blokowania",
+ "server_config_blocking_mode_ipv4": "Blokowanie IPv4",
+ "server_config_blocking_mode_ipv6": "Blokowanie IPv6",
+ "server_config_custom_ip": "Niestandardowy adres IP",
+ "server_config_default": "Tryb domyślny",
+ "server_config_disable_ipv6": "Wyłącz rozwiązywanie adresów IPv6",
+ "server_config_dnssec_enable": "Włącz DNSSEC",
+ "server_config_edns_enable": "Włącz podsieć klienta EDNS",
+ "server_config_nxdomain": "NXDOMAIN",
+ "server_config_rate_limit_placeholder": "Wpisz limit ilościowy",
+ "server_config_rate_limit_whitelist_faq": "Adresy IP wykluczone z ograniczania prędkości",
+ "server_config_refused": "REFUSED",
+ "server_config_subnet_len_ipv4": "Długość maski podsieci dla adresów IPv4",
+ "server_config_subnet_len_ipv4_faq": "Długość maski podsieci dla adresów IPv4 używanych do ograniczania prędkości. Domyślnie jest to 24",
+ "server_config_subnet_len_ipv6": "Długość prefiksu podsieci dla adresów IPv6",
+ "server_config_subnet_len_ipv6_faq": "Długość prefiksu podsieci dla adresów IPv6 używanych do ograniczania szybkości. Domyślnie jest to 56",
+ "set_static_ip": "Ustaw statyczny adres IP",
+ "settings": "Ustawienia",
+ "settings_browsing_security": "Bezpieczne przeglądanie",
+ "settings_confirm_clear_statistics": "Wyczyść statystyki?",
+ "settings_custom": "Inny",
+ "settings_general_short": "General",
+ "settings_global": "Globalny",
+ "settings_notify_changes_saved": "Zmiany zostały zapisane",
+ "settings_notify_statistics_cleared": "Statystyki zostały wyczyszczone",
+ "settings_parental_control": "Kontrola rodzicielska",
+ "settings_rule_syntax": "Składnia reguły",
+ "settings_safe_search": "Bezpieczne wyszukiwanie",
+ "settings_statistics": "Statystyki",
+ "settings_statistics_clear": "Wyczyść statystyki",
+ "settings_statistics_retention": "Przechowywanie statystyk",
+ "settings_yes_clear": "Tak, wyczyść",
+ "setup_devices_router_title": "Router",
+ "setup_dns_privacy_ioc_mac": "Konfiguracja iOS i macOS",
+ "setup_dns_privacy_other_title": "Inne implementacje",
+ "show_blocked_responses": "Zablokowane",
+ "show_processed_responses": "Przetworzony",
+ "source_label": "Źródło",
+ "source_tooltip": "Źródło",
+ "stats_adult": "Zablokowane witryny dla dorosłych",
+ "stats_query_domain": "Najczęściej wyszukiwane domeny",
+ "status_table_header": "Stan",
+ "sunday": "Niedziela",
+ "system_host_files": "Pliki hosts systemu",
+ "table_client": "Klient",
+ "tags_title": "Tagi",
+ "test_upstreams": "Test głównych serwerów DNS",
+ "threats_blocked": "Zablokowanych zagrożeń",
+ "thursday": "Czwartek",
+ "time_table_header": "Czas",
+ "top_blocked_domains": "Najpopularniejsze zablokowane domeny",
+ "top_clients": "Główni klienci",
+ "top_upstreams": "Często żądane serwery nadrzędne",
+ "total": "Całkowity",
+ "try_again": "Spróbuj ponownie",
+ "tuesday": "Wtorek",
+ "unavailable_dhcp": "Serwer DHCP jest niedostępny",
+ "unavailable_dhcp_desc": "AdGuard Home nie może uruchomić serwera DHCP na Twoim systemie operacyjnym",
+ "unblock": "Odblokuj",
+ "update_button": "Aktualizuj",
+ "update_filters_disable": "Wyłącz",
+ "update_filters_interval_custom": "Inny",
+ "update_filters_interval_daily": "Codziennie",
+ "update_filters_interval_hourly": "Co godzinę",
+ "update_filters_title": "Aktualizuj filtry automatycznie",
+ "updates_checked": "Dostępna jest nowa wersja programu AdGuard Home\n",
+ "updates_version_equal": "AdGuard Home jest aktualny",
+ "upstream": "Serwer nadrzędny",
+ "upstream_dns": "Główne serwery DNS",
+ "upstream_dns_addresses": "Adresy serwerów, po jednym w wierszu",
+ "upstream_dns_fastest_addr": "Najszybszy adres IP",
+ "upstream_dns_parallel_requests": "Równoległe żądania",
+ "upstream_dns_servers_title": "Główne serwery DNS",
+ "upstream_examples_title": "Przykłady",
+ "upstream_private_ptr_resolvers_title": "Użyj prywatnych odwrotnych resolwerów DNS",
+ "upstreams": "Główne serwery DNS",
+ "url_label": "URL",
+ "user_rules": "Filtr użytkownika",
+ "user_rules_add_to_allowlist": "Dodaj do listy dozwolonych",
+ "user_rules_check_button": "Sprawdź",
+ "user_rules_check_client_placeholder": "Wprowadź identyfikator klienta",
+ "user_rules_check_hostname_label": "Nazwa hosta lub nazwa domeny",
+ "user_rules_disable_filter": "Wyłącz filtr",
+ "user_rules_disable_parental_control": "Wyłącz kontrolę rodzicielską",
+ "user_rules_dns_record_type_placeholder": "Wybierz typ rekordu DNS",
+ "user_rules_domain_blocked": "Domena jest zablokowana",
+ "user_rules_edit_dns_rewrite": "Edytuj przepisywanie DNS",
+ "user_rules_rewrite_rule_is_applied": "Przepisano regułę",
+ "username_label": "Nazwa użytkownika",
+ "username_placeholder": "Wpisz nazwę użytkownika",
+ "validated_with_dnssec": "Zweryfikowany przez DNSSEC",
+ "wednesday": "Środa",
+ "whois": "WHOIS",
+ "yes": "Tak",
+ "yes_disable": "Tak, wyłącz",
+ "yes_reset": "Tak, usuń"
+}
diff --git a/client_v2/src/__locales/pt-br.json b/client_v2/src/__locales/pt-br.json
new file mode 100644
index 00000000000..c86c855e3e8
--- /dev/null
+++ b/client_v2/src/__locales/pt-br.json
@@ -0,0 +1,864 @@
+{
+ "access_settings_allowed_faq": "Uma lista de CIDRs, endereços IP ou IDs de cliente . Se esta lista tiver entradas, o AdGuard Home do aceitará solicitações apenas desses clientes",
+ "access_settings_allowed_placeholder": "Insira os clientes permitidos",
+ "access_settings_allowed_title": "Clientes permitidos, um por linha",
+ "access_settings_blocked_faq_1": "O AdGuard Home bloqueará consultas DNS que correspondam a esses domínios. Essas consultas não aparecem no registro de consultas. Você pode inserir nomes de domínio exatos, curingas ou regras de filtro de URL, como example.org, *.example.org ou ||example.org^",
+ "access_settings_blocked_faq_2": "Não confundir com filtros",
+ "access_settings_blocked_placeholder": "Insira os domínios não permitidos",
+ "access_settings_blocked_title": "Domínios não permitidos, um por linha",
+ "access_settings_disallowed_faq": "Uma lista de CIDRs, endereços IP ou ClientIDs . Se este campo contiver entradas, o AdGuard Home rejeitará as solicitações desses clientes. Este campo será ignorado se houver entradas no campo Clientes permitidos ",
+ "access_settings_disallowed_placeholder": "Insira os clientes não permitidos",
+ "access_settings_disallowed_title": "Clientes não permitidos, um por linha",
+ "access_settings_saved_toast": "Configurações de acesso salvas",
+ "access_settings_title": "Configurações de acessos",
+ "actions_table_header": "Ações",
+ "add": "Adicionar",
+ "add_allowlist": "Adicionar lista de permissões",
+ "add_blocklist": "Adicionar lista de bloqueio",
+ "add_persistent_client": "Adicionar como cliente persistente",
+ "add_to_allowlist": "Adicionar na lista de permissões",
+ "ads_blocked": "anúncios bloqueados",
+ "ads_blocked_card": "anúncios bloqueados",
+ "ads_blocked_tooltip": "O número de consultas DNS bloqueadas pelos filtros de bloqueio de anúncios e pela lista de bloqueio de hosts",
+ "adult_websites_blocked": "Sites adultos bloqueados",
+ "adult_websites_blocked_tooltip": "O número de sites adultos bloqueados",
+ "all_domains": "Todos os domínios",
+ "all_lists_up_to_date_toast": "Todas as listas já estão atualizadas",
+ "all_rewrites_disabled": "Todas as respostas DNS personalizadas serão desativadas",
+ "all_rewrites_enabled": "Todas as respostas DNS personalizadas serão habilitadas",
+ "allow_service": "Permitir serviço",
+ "allowed": "Na lista branca",
+ "allowlist_add": "Adicionar lista de permissões",
+ "allowlist_edit": "Editar lista de permissões",
+ "allowlist_empty": "Ainda não há listas de permissões, adicione a primeira ",
+ "allowlist_placeholder_example": "Minha lista de permissões",
+ "allowlist_remove": "Remover lista de permissões?",
+ "allowlist_remove_desc": "%value% será removido",
+ "allowlists": "Lista de permissões",
+ "allowlists_title": "Lista de permissões",
+ "apply": "Aplicar",
+ "aria_clear_input": "Limpar entrada",
+ "autofix_warning_list": "Ele irá realizar estas tarefas:
Desativar sistema DNSStubListener
Definir endereço do servidor DNS para 127.0.0.1
Substituir o alvo simbólico do link /etc/resolv.conf para /run/systemd/resolv.conf
Parar DNSStubListener (recarregar serviço resolvido pelo sistema)
",
+ "autofix_warning_result": "Como resultado, todos as solicitações DNS do seu sistema serão processadas pelo AdGuard Home por padrão.",
+ "autofix_warning_text": "Se clicar em \"Corrigir\", o AdGuardHome irá configurar o seu sistema para utilizar o servidor DNS do AdGuardHome.",
+ "average_time_processing": "Tempo médio de processamento",
+ "average_time_processing_tooltip": "Tempo médio em milissegundos no processamento de uma consulta DNS",
+ "average_upstream_response_time": "Tempo médio de resposta upstream",
+ "back": "Voltar",
+ "block": "Bloquear",
+ "block_client": "Bloquear cliente",
+ "block_for_this_client_only": "Bloquear apenas para este cliente",
+ "blocked": "Bloqueado",
+ "blocked_adult_websites": "Bloqueado pelo controle parental",
+ "blocked_by_cname_or_ip": "Bloqueado por CNAME ou IP",
+ "blocked_queries": "Consultas bloqueadas",
+ "blocked_service": "Serviço bloqueado",
+ "blocked_services": "Serviços bloqueados",
+ "blocked_services_desc": "Selecione sites e serviços populares que devem ser bloqueados",
+ "blocked_threats": "Ameaças bloqueadas",
+ "blocked_threats_chart": "Ameaças bloqueadas",
+ "blocked_total": "%value% bloqueado no total",
+ "blocklist_add_from_list": "Adicionar da lista",
+ "blocklist_add_manual": "Adicionar manualmente",
+ "blocklist_contents": "Conteúdos (.%value%)",
+ "blocklist_edit": "Editar lista de bloqueio",
+ "blocklist_homepage": "Página inicial",
+ "blocklist_placeholder_example": "Minha lista de bloqueio",
+ "blocklist_remove": "Remover lista de bloqueio?",
+ "blocklist_remove_desc": "%value% será removido",
+ "blocklist_url_file_path": "URL ou caminho do arquivo",
+ "blocklists_add": "Adicionar lista de bloqueio",
+ "blocklists_add_desc": "Você pode selecionar listas de bloqueio na lista ou adicioná-las manualmente",
+ "blocklists_desc": "Domínios dessas listas são bloqueados",
+ "blocklists_empty": "Ainda não há listas de bloqueio, adicione a primeira ",
+ "blocklists_title": "Listas de bloqueio",
+ "bootstrap_dns": "Servidores DNS de inicialização",
+ "browse": "Procurar...",
+ "cache_cleared": "Cache DNS limpo com sucesso",
+ "cache_config_clear": "Limpar cache",
+ "cache_config_max_ttl": "Substituir TTL máximo, em segundos",
+ "cache_config_max_ttl_faq": "O valor máximo de tempo de vida (TTL) para entradas no cache de DNS",
+ "cache_config_max_ttl_placeholder": "Digite o TTL máximo",
+ "cache_config_min_ttl": "Substituir TTL mínimo, em segundos",
+ "cache_config_min_ttl_faq": "O valor mínimo de tempo de vida (TTL) para entradas no cache DNS",
+ "cache_config_min_ttl_placeholder": "Digite o TTL mínimo",
+ "cache_config_optimistic": "Cache otimista",
+ "cache_config_optimistic_desc": "O AdGuard Home responderá a partir do cache mesmo que as entradas tenham expirado e tentará atualizá-las",
+ "cache_config_size": "Tamanho do cache, em bytes",
+ "cache_config_size_faq": "Tamanho do cache DNS. Para desativar o cache, deixe em branco",
+ "cache_config_size_validation": "O tamanho do cache deve ser maior que zero quando habilitado",
+ "cache_config_ttl_validation": "O substituto mínimo de cache TTL deve ser menor ou igual ao máximo",
+ "cache_confirm_clear_desc": "Isso removerá todas as respostas DNS em cache. Consultas DNS subsequentes podem ficar mais lentas até que o cache seja reconstruído",
+ "cache_confirm_clear_title": "Limpar cache DNS",
+ "cache_enabled": "Ativar cache",
+ "cache_enabled_desc": "Armazenar respostas DNS localmente",
+ "cancel": "Cancelar",
+ "cancel_btn": "Cancelar",
+ "category_tooltip": "Categoria",
+ "changes_saved_success": "Alterações salvas",
+ "check_dhcp_servers": "Verificar por servidores DHCP",
+ "check_not_found": "Não encontrado em suas listas de filtros",
+ "check_updates_btn": "Verificar atualizações",
+ "clear_btn": "Limpar",
+ "clear_query_log": "Limpar registro de consultas",
+ "client_add": "Adicionar cliente",
+ "client_added": "Cliente adicionado",
+ "client_already_blocked": "O cliente %ip% já está bloqueado",
+ "client_blocked_flash": "Cliente bloqueado",
+ "client_details": "Detalhes do cliente",
+ "client_id": "ID do cliente",
+ "client_id_faq": "Os clientes podem ser identificados pelo seu ClientID. Para saber mais, visite o AdGuard Home GitHub ",
+ "client_id_placeholder": "Digite o ID do cliente",
+ "client_identifier": "Identificador",
+ "client_ip": "IP do cliente",
+ "client_location": "Localização do cliente",
+ "client_removed": "Cliente removido",
+ "client_settings": "Configurações do cliente",
+ "client_table_header": "Cliente",
+ "client_unblocked_flash": "Cliente desbloqueado",
+ "clients": "Clientes",
+ "clients_add": "Adicionar cliente",
+ "clients_add_identifier": "Adicionar identificador",
+ "clients_add_name": "Nome do cliente",
+ "clients_add_placeholder": "Digite o nome do cliente",
+ "clients_desc": "Regras para dispositivos conectados ao AdGuard",
+ "clients_dns_cache_size": "Tamanho do cache do DNS, em bytes",
+ "clients_dns_cache_size_placeholder": "Use números",
+ "clients_dont_collect_stats": "Não coletar estatísticas para este cliente",
+ "clients_dont_log": "Não registrar este cliente",
+ "clients_edit": "Editar cliente",
+ "clients_identifier_already_used": "Este identificador já está em uso",
+ "clients_identifier_format_error": "Use números, letras minúsculas ou hífens",
+ "clients_identifiers": "Identificadores",
+ "clients_identifiers_desc": "Você pode usar o endereço IP do cliente, CIDR, endereço MAC ou ClientID (para DoT, DoH e DoQ). Saiba mais ",
+ "clients_logs_and_statistics": "Registros e estatísticas",
+ "clients_not_found": "Nenhuma cliente encontrado",
+ "clients_protection": "Proteção",
+ "clients_protection_desc": "Ativar ou desativar recursos de proteção, registros e estatísticas",
+ "clients_remove_desc": "%value% será removido dos clientes persistentes juntamente com suas configurações",
+ "clients_remove_title": "Remover cliente?",
+ "clients_tags": "Marcadores",
+ "clients_tags_desc": "Você pode adicionar etiquetas para este cliente e incluí-las nas regras de filtragem. Saiba mais ",
+ "clients_upstreams_desc": "Servidores DNS upstream, um por linha",
+ "clients_use_dns_cache": "Usar cache de DNS",
+ "clients_use_global_settings": "Usar configurações global",
+ "clients_use_global_settings_desc": "Herdar as configurações de proteção da sua conta do AdGuard Home",
+ "close": "Fechar",
+ "close_result": "Fechar resultado",
+ "confirm": "Confirmar",
+ "confirm_client_block_desc": "AdGuard Home irá descartar todas as requisições DNS do cliente %ip% ",
+ "confirm_client_block_title": "Bloquear cliente?",
+ "confirm_client_unblock_desc": "As solicitações DNS do cliente %ip% serão permitidas",
+ "confirm_client_unblock_title": "Desbloquear cliente?",
+ "confirm_static_ip": "O AdGuard Home irá configurar %ip% para ser seu endereço IP estático. Deseja continuar?",
+ "copied": "Copiado para a área de transferência",
+ "copy": "Copiar",
+ "copy_to_clipboard": "Copiar para área de transferência",
+ "custom_filtering_rules": "Regras de filtragem personalizadas",
+ "dark_theme": "Tema escuro",
+ "dashboard": "Painel",
+ "delete_btn": "Excluir",
+ "delete_confirm": "Excluir concessão?",
+ "delete_confirm_desc": "Isso liberará %ip% ",
+ "delete_table_action": "Excluir",
+ "delete_table_action_confirm": "Sim, excluir",
+ "device_type": "Tipo de dispositivo",
+ "dhcp_add_static_lease": "Adicionar nova concessão estática",
+ "dhcp_config_saved": "Configurações DHCP salvas com sucesso",
+ "dhcp_edit_static_lease": "Editar concessão estática",
+ "dhcp_error": "Falha ao verificar o servidor DHCP. Aguarde um pouco antes de tentar novamente",
+ "dhcp_form_gateway_address": "Endereço IP do gateway",
+ "dhcp_form_gateway_input": "IP do gateway",
+ "dhcp_form_range_start": "Endereço IP inicial",
+ "dhcp_form_range_title": "Intervalo de endereços IP",
+ "dhcp_form_subnet_input": "Máscara de sub-rede",
+ "dhcp_found": "O servidor DHCP não pode ser ativado",
+ "dhcp_ipv4_settings": "Configurações DHCP IPv4",
+ "dhcp_ipv6_settings": "Configurações DHCP IPv6",
+ "dhcp_lease_added": "Concessão estática \"%key%\" adicionada com sucesso",
+ "dhcp_lease_deleted": "Concessão estática \"%key%\" excluída com sucesso",
+ "dhcp_lease_updated": "Concessão estática \"%key%\" adicionada com sucesso",
+ "dhcp_leases_not_found": "Nenhuma concessão DHCP encontrada",
+ "dhcp_leases_title": "Concessões DHCP",
+ "dhcp_mac_address_already_added": "Este endereço MAC já foi adicionado",
+ "dhcp_new_static_lease": "Nova concessão estática",
+ "dhcp_not_found": "O servidor DHCP está ativo",
+ "dhcp_reset": "Isso redefinirá a configuração de DHCP. Todas as alterações serão perdidas",
+ "dhcp_reset_leases": "Redefinir todas as concessões",
+ "dhcp_reset_leases_confirm": "Isso redefinirá todas as concessões",
+ "dhcp_reset_leases_success": "Concessões de DHCP redefinidas com sucesso",
+ "dhcp_settings": "Configurações de DHCP",
+ "dhcp_static_ip_error": "O servidor DHCP requer um endereço IP estático. Não foi possível confirmar que esta interface de rede possui um endereços IP estático",
+ "dhcp_static_leases_not_found": "Nenhuma concessão DHCP estática foi encontrada",
+ "dhcp_table_hostname": "Nome do servidor",
+ "dhcp_table_ip_address": "Endereço de IP",
+ "dhcp_table_mac_address": "Endereço MAC",
+ "disable": "Desativar",
+ "disable_protection_btn": "Desativar proteção",
+ "disabled_dhcp": "Servidor DHCP desativado",
+ "disallow_client_confirm_allowlist_note": "Esta ação também excluirá a regra %ip% da lista de clientes permitidos.",
+ "disallow_client_confirm_title": "Retirar permissão deste cliente?",
+ "disallow_this_client": "Não permitir este cliente",
+ "dns_access_settings_title": "Configurações de acessos",
+ "dns_allowed_clients": "Clientes permitidos",
+ "dns_allowed_clients_label": "Clientes permitidos, um por linha",
+ "dns_allowed_clients_placeholder": "Insira os clientes permitidos",
+ "dns_blocking_mode": "Modo de bloqueio",
+ "dns_blocking_mode_custom_ip": "Endereço de IP personalizado",
+ "dns_blocking_mode_default": "Padrão",
+ "dns_blocking_mode_ipv4_label": "Bloqueando IPv4",
+ "dns_blocking_mode_ipv4_placeholder": "Digite o endereço de IPv4",
+ "dns_blocking_mode_ipv6_label": "Bloqueando IPv6",
+ "dns_blocking_mode_ipv6_placeholder": "Digite o endereço de IPv6",
+ "dns_blocking_mode_null_ip": "Endereço IP nulo",
+ "dns_blocking_mode_nxdomain": "NXDOMAIN",
+ "dns_blocking_mode_refused": "REFUSED",
+ "dns_blocking_mode_title": "Modo de bloqueio",
+ "dns_blocking_mode_ttl_label": "TTL da resposta bloqueada, em segundos",
+ "dns_bootstrap_dns_label": "Servidores DNS de bootstrap, um por linha",
+ "dns_bootstrap_dns_placeholder": "Endereço de IP",
+ "dns_bootstrap_dns_title": "Servidores DNS de inicialização",
+ "dns_bootstrap_servers": "Servidores DNS de inicialização",
+ "dns_cache_config": "Configuração de cache DNS",
+ "dns_cache_configuration_saved_toast": "Configuração do cache DNS salva",
+ "dns_cache_size": "Tamanho do cache",
+ "dns_cache_size_label": "Tamanho do cache do DNS, em bytes",
+ "dns_cache_size_title": "Tamanho do cache",
+ "dns_clear_cache": "Limpar cache",
+ "dns_clear_cache_cancel": "Cancelar",
+ "dns_clear_cache_confirm": "Sim, limpar",
+ "dns_clear_cache_yes": "Sim, limpar",
+ "dns_config": "Configuração do servidor DNS",
+ "dns_disallowed_clients": "Clientes não permitidos",
+ "dns_disallowed_clients_label": "Clientes não permitidos, um por linha",
+ "dns_disallowed_clients_title": "Clientes não permitidos",
+ "dns_disallowed_domains": "Domínios bloqueados",
+ "dns_disallowed_domains_label": "Domínios não permitidos, um por linha",
+ "dns_disallowed_domains_title": "Domínios bloqueados",
+ "dns_edns_custom_placeholder": "Digite o endereço de IP",
+ "dns_fallback_dns_label": "Servidores DNS de fallback, um por linha",
+ "dns_fallback_dns_placeholder": "Endereços IP, sdns://, quic://, https://, h3://, ou tls://",
+ "dns_fallback_dns_title": "Servidores DNS Fallback",
+ "dns_fallback_servers": "Servidores DNS Fallback",
+ "dns_optimistic_caching": "Cache otimista",
+ "dns_over_https": "DNS-sobre-HTTPS",
+ "dns_over_quic": "DNS-sobre-QUIC",
+ "dns_over_tls": "DNS-sobre-TLS",
+ "dns_override_max_ttl": "Sobrepor o TTL máximo",
+ "dns_override_max_ttl_title": "Sobrepor o TTL máximo",
+ "dns_override_min_ttl": "Sobrepor o TTL mínimo",
+ "dns_override_min_ttl_title": "Sobrepor o TTL mínimo",
+ "dns_privacy": "Privacidade de DNS",
+ "dns_private_reverse_servers_label": "Servidores de DNS reverso privados, um por linha",
+ "dns_private_reverse_servers_placeholder": "Endereços IP, sdns://, quic://, https://, h3://, ou tls://",
+ "dns_private_reverse_servers_title": "Servidores DNS reversos privados",
+ "dns_queries": "Consultas de DNS",
+ "dns_queries_tooltip": "O número de consultas DNS processadas nas últimas 24 horas",
+ "dns_query": "Consultas de DNS",
+ "dns_rate_limit": "Velocidade limite",
+ "dns_rate_limit_allowlist": "Lista de permissões de limitação de taxa",
+ "dns_rate_limit_allowlist_placeholder": "Endereço de IP",
+ "dns_rate_limit_allowlist_title": "Lista de permissões de limitação de taxa",
+ "dns_rate_limit_placeholder": "Insira a velocidade limite",
+ "dns_rate_limit_title": "Velocidade limite",
+ "dns_rewrites": "Reescritas de DNS",
+ "dns_server_addresses": "Endereços de servidor",
+ "dns_server_addresses_label": "Endereços de servidor, um por linha",
+ "dns_server_addresses_placeholder": "Endereços IP, sdns://, quic://, https://, h3://, ou tls://",
+ "dns_server_addresses_title": "Endereços de servidor",
+ "dns_server_config_title": "Configuração do servidor DNS",
+ "dns_server_configuration_saved_toast": "Configuração do servidor DNS salva",
+ "dns_settings": "DNS",
+ "dns_subnet_placeholder": "Insira o comprimento do prefixo",
+ "dns_test_upstreams": "Testar DNS primário",
+ "dns_upstream_servers_title": "Servidores DNS primário",
+ "dns_upstream_timeout": "Tempo limite de upstream",
+ "dns_upstream_timeout_title": "Tempo limite de upstream",
+ "dns_upstream_validation_invalid": "Formato inválido",
+ "domain": "Domínio",
+ "domain_or_client": "Domínio ou cliente",
+ "download_mobileconfig": "Baixar arquivo de configuração",
+ "dynamic_dhcp_leases_not_found": "Concessões DHCP dinâmicas não encontradas",
+ "edit_table_action": "Editar",
+ "enable": "Ativar",
+ "enabled_dhcp": "Servidor DHCP ativado",
+ "encrypted_dns_addresses": "Endereços de servidor DNS criptografados",
+ "encryption_certificate_has_issues": "O certificado apresenta problemas",
+ "encryption_certificate_path": "Caminho do certificado",
+ "encryption_certificates": "Certificados",
+ "encryption_certificates_desc": "Para usar a criptografia, você deve fornecer uma cadeia de certificados SSL válida para o seu domínio. Você pode obter um certificado SSL gratuito em letsencrypt.org ou comprar um de uma Autoridade Certificadora de confiança",
+ "encryption_certificates_input": "Copie/cole aqui seu certificado codificado em PEM.",
+ "encryption_certificates_source_content": "Colar o conteúdo dos certificados",
+ "encryption_certificates_source_path": "Definir o caminho do arquivo de certificados",
+ "encryption_chain_invalid": "A cadeia de certificado é inválida",
+ "encryption_chain_valid": "Cadeia de chave válida",
+ "encryption_config_saved": "Configuração de criptografia salva",
+ "encryption_confirm_clear": "Redefinir configurações de criptografia",
+ "encryption_confirm_clear_desc": "Todas as configurações de criptografia serão redefinidas para seus valores padrão",
+ "encryption_disable_plain_dns": "Desativar DNS simples?",
+ "encryption_disable_plain_dns_desc": "Dispositivos não compatíveis com DNS criptografado podem não funcionar corretamente",
+ "encryption_doq": "Porta DNS-sobre-QUIC",
+ "encryption_doq_tooltip": "Esta porta é usada para executar um servidor DNS-sobre-QUIC",
+ "encryption_dot": "Porta DNS-sobre-TLS",
+ "encryption_dot_tooltip": "Esta porta é usada para executar um servidor DNS-sobre-TLS",
+ "encryption_encrypted_dns": "DNS criptografado",
+ "encryption_encrypted_dns_desc": "A interface administrativa do AdGuard Home utiliza HTTPS. O servidor DNS é compatível com DNS-sobre-HTTPS, DNS-sobre-TLS e DNS-sobre-QUIC",
+ "encryption_expire": "Expirado em: %value%",
+ "encryption_force_redirect": "Redirecionar automaticamente de HTTP para HTTPS",
+ "encryption_hostnames": "Nomes de host: %value%",
+ "encryption_https": "Porta HTTPS",
+ "encryption_https_tooltip": "Esta porta fornece acesso HTTPS para a interface de administração do AdGuard Home. Ela também fornece DNS-sobre-HTTPS no local '/dns-query'",
+ "encryption_issuer": "Emissor: %value%",
+ "encryption_key": "Chave privada",
+ "encryption_key_input": "Copie/cole aqui a chave privada codificada em PEM para seu certificado.",
+ "encryption_key_invalid": "A chave privada é inválida",
+ "encryption_key_source_content": "Colar o conteúdo da chave privada",
+ "encryption_key_source_path": "Definir um arquivo de chave privada",
+ "encryption_key_type": "Algoritmo de criptografia: %value%",
+ "encryption_key_valid": "A chave privada é válida",
+ "encryption_plain_dns": "DNS simples",
+ "encryption_plain_dns_error": "Para desativar o DNS simples, ative pelo menos um protocolo DNS criptografado",
+ "encryption_private_key_path": "Caminho da chave privada",
+ "encryption_server": "Nome do servidor",
+ "encryption_server_enter": "Digite seu nome de domínio",
+ "encryption_server_tooltip_1": "Se definido, AdGuard Home detectará ClientIDs, responderá a inquéritos DDR e executará validações de conexão adicionais. Se não estiver definido, esses recursos estão desativados",
+ "encryption_server_tooltip_2": "Deve corresponder a um nome DNS no certificado",
+ "encryption_subject": "Assunto: %value%",
+ "encryption_title": "Criptografia",
+ "enter_cache_size": "Digite o tamanho do cache (bytes)",
+ "enter_ip_address_placeholder": "Digite o endereço de IP",
+ "error": "Erro",
+ "fallback_dns": "Servidores DNS Fallback",
+ "filter_added_successfully": "Lista de bloqueio adicionada: %value% ",
+ "filter_added_successfully_allowlist": "Adicionada à lista de permissões: %value% ",
+ "filter_added_successfully_more": "Listas de bloqueio adicionadas: %value% e mais %more%",
+ "filter_category_general": "Geral",
+ "filter_category_general_desc": "Listas que bloqueiam o rastreamento e a publicidade na maioria dos dispositivos",
+ "filter_category_other": "Outros",
+ "filter_category_other_desc": "Outras listas de bloqueio",
+ "filter_category_regional": "Regional",
+ "filter_category_regional_desc": "Listas focadas em anúncios regionais e servidores de rastreamento",
+ "filter_category_security": "Segurança",
+ "filter_category_security_desc": "Listas projetadas especificamente em bloquear domínios maliciosos, de phishing e fraude",
+ "filter_removed_successfully": "Lista de bloqueio removida: %value% ",
+ "filter_removed_successfully_allowlist": "Lista de permissões removida: %value% ",
+ "filters": "Filtros",
+ "forgot_password": "Esqueceu sua senha?",
+ "forgot_password_list_item_1": "Crie uma nova senha usando a ferramenta htpasswd ou qualquer gerador BCrypt online.",
+ "forgot_password_list_item_2": "Parar o AdGuard Home.",
+ "forgot_password_list_item_3": "Abra o arquivo AdGuardHome.yaml, encontre o campo de senha e substitua-o pelo novo valor.",
+ "forgot_password_list_item_4": "Inicie o AdGuard Home.",
+ "forgot_password_list_item_5": "Faça login na interface web com sua nova senha.",
+ "forgot_password_list_title": "Para alterar sua senha:",
+ "forgot_password_page_desc": "O AdGuard Home armazena senhas como hashes criptografados com BCrypt",
+ "form_enter_blocked_response_ttl": "Insira o TTL da resposta bloqueada (segundos)",
+ "form_enter_hostname": "Digite um nome de host",
+ "form_enter_ip": "Digite um endereço IP",
+ "form_enter_mac": "Digite um endereço MAC",
+ "form_error_answer_format": "Formato de resultado inválido",
+ "form_error_client_id_format": "O ID do cliente deve conter apenas números, letras minúsculas e hifens",
+ "form_error_domain_format": "Formato de domínio inválido",
+ "form_error_equal": "Não deve ser igual",
+ "form_error_format": "Formato inválido",
+ "form_error_gateway_ip": "A concessão não pode ter o endereço IP do gateway",
+ "form_error_hostname_format": "Use números, letras minúsculas ou hífens",
+ "form_error_ip4_format": "Endereço de IPv4 inválido",
+ "form_error_ip6_format": "Endereço de IPv6 inválido",
+ "form_error_ip_already_added": "Este endereço IP já foi adicionado",
+ "form_error_ip_format": "Endereço de IP inválido",
+ "form_error_mac_already_added": "Este endereço MAC já foi adicionado",
+ "form_error_mac_format": "Endereço de MAC inválido",
+ "form_error_password": "Senhas não coincidem",
+ "form_error_port": "Digite um numero de porta válida",
+ "form_error_port_range": "Digite um número de porta entre 80 e 65535",
+ "form_error_port_unsafe": "Porta não é segura",
+ "form_error_range": "O valor deve estar entre %min% e %max%",
+ "form_error_server_name": "Nome de servidor inválido",
+ "form_error_url_format": "Insira um URL ou caminho de arquivo válido",
+ "found_in_known_domains": "Encontrado domínios conhecidos",
+ "friday": "Sexta-feira",
+ "gateway_or_subnet_invalid": "Máscara de sub-rede inválida",
+ "general_statistics": "Estatísticas gerais",
+ "home_dns_addresses": "Endereços DNS do AdGuard Home",
+ "home_dns_addresses_desc": "O servidor DNS do AdGuard Home escuta nas seguintes interfaces/portas:",
+ "ignore_domains_desc_log": "As solicitações que correspondem a essas regras não são exibidas no registro de consultas",
+ "ignore_domains_desc_stats": "As solicitações que correspondem a essas regras não são incluídas nas estatísticas",
+ "ignore_domains_title": "Domínios ignorados",
+ "inactivity_schedule": "Agendamento de inatividade",
+ "inactivity_schedule_add": "Adicionar agendamento",
+ "inactivity_schedule_all_day": "O dia todo",
+ "inactivity_schedule_delete": "Excluir agendamento",
+ "inactivity_schedule_delete_desc": "O bloqueio de serviço estará ativo durante todo o dia em %value% ",
+ "inactivity_schedule_desc": "Definir períodos de inatividade para bloqueio de serviços",
+ "inactivity_schedule_edit": "Editar agendamento",
+ "inactivity_schedule_no_schedules": "Sem agendamento",
+ "inactivity_schedule_replace_desc": "Este agendamento substituirá quaisquer agendamentos existentes para o mesmo dia da semana. Cada dia pode ter apenas um período de inatividade",
+ "inactivity_schedule_time_from": "Hora de início",
+ "inactivity_schedule_time_to": "Hora do fim",
+ "inactivity_schedule_timezone": "Fuso horário",
+ "install_auth_confirm": "Confirmar senha",
+ "install_auth_password": "Senha",
+ "install_auth_password_enter": "Digite a senha",
+ "install_auth_username": "Nome de usuário",
+ "install_auth_username_enter": "Digite o nome de usuário",
+ "install_saved": "Salvo com sucesso",
+ "install_settings_all_interfaces": "Todas interfaces",
+ "install_settings_port": "Porta",
+ "install_static_configure": "O AdGuard Home detectou que o endereço IP dinâmico %ip% está sendo usado. Você deseja que seja definido como seu endereço estático?",
+ "install_static_error": "O AdGuard Home não pode configurar automaticamente para esta interface de rede. Por favor, procure uma instrução sobre como fazer isso manualmente.",
+ "install_static_ok": "Boas notícias! O endereço de IP estático já está configurado",
+ "install_step": "Passo",
+ "install_submit_title": "Parabéns!",
+ "instructions": "Instruções",
+ "invalid": "Inválido",
+ "ip_address": "Endereço de IP",
+ "ip_addresses_placeholder": "Insira endereços IP, um por linha",
+ "known_tracker": "Rastreador conhecido",
+ "language": "Idioma",
+ "last_days": "| Último %count% dia | Últimos %count% dias",
+ "last_hour": "Última hora",
+ "last_hours": "| Última %count% hora | Últimas %count% horas",
+ "last_updated_label": "Última atualização",
+ "light_theme": "Tema claro",
+ "list_updated": "| %count% lista atualizada | %count% listas atualizadas",
+ "login": "Entrar",
+ "logout": "Sair",
+ "make_static": "Tornar estático",
+ "make_static_desc": "Esta ação converte um endereço dinâmico em um endereço estático",
+ "milliseconds_abbreviation": "ms",
+ "monday": "Segunda-feira",
+ "name": "Nome",
+ "name_label": "Nome",
+ "name_table_header": "Nome",
+ "name_tooltip": "Nome",
+ "network_interface": "Interface de rede",
+ "next": "Próximo",
+ "no": "Não",
+ "no_stats_yet": "Ainda não há estatísticas — continue usando o AdGuard Home para visualizá-las",
+ "none_text": "Nenhum",
+ "nothing_found": "Nada encontrado",
+ "notify_undo": "Desfazer",
+ "notify_updated": "Atualizado",
+ "open_dashboard": "Abrir painel",
+ "parental_control": "Controle parental",
+ "parental_group_ai": "IA",
+ "parental_group_cdn": "CDN",
+ "parental_group_dating": "Encontros",
+ "parental_group_development": "Desenvolvimento de software",
+ "parental_group_gambling": "Jogos de azar, apostas",
+ "parental_group_gaming": "Jogos digitais",
+ "parental_group_hosting": "Hospedagem web",
+ "parental_group_messaging": "Mensagens",
+ "parental_group_messenger": "Mensagens",
+ "parental_group_privacy": "Ferramentas de privacidade",
+ "parental_group_shopping": "Compras",
+ "parental_group_social": "Redes sociais",
+ "parental_group_social_network": "Redes sociais",
+ "parental_group_software": "Desenvolvimento de software",
+ "parental_group_streaming": "Streaming",
+ "password_label": "Senha",
+ "password_placeholder": "Digite a senha",
+ "password_requirements": "Requisitos de senha",
+ "password_requirements_characters": "Mínimo de 8 caracteres",
+ "password_requirements_lowercase": "Pelo menos 1 letra minúscula",
+ "password_requirements_match": "As senhas coincidem",
+ "password_requirements_special": "Letras, números e caracteres especiais",
+ "password_requirements_uppercase": "Pelo menos 1 letra maiúscula",
+ "pause_for_hour": "Pausar por %count% hora",
+ "pause_for_minutes": "| Pausar por %count% minuto | Pausar por %count% minutos",
+ "pause_for_seconds": "| Pausar por %count% segundo | Pausar por %count% segundos",
+ "pause_until_tomorrow": "Pausar até amanhã, %time%",
+ "period_notify": "Para alterar os períodos exibidos, edite Retenção de estatísticas em Configurações gerais ",
+ "plain_dns": "DNS simples",
+ "plain_dns_addresses": "Endereços de servidor DNS simples",
+ "port_53_faq_link": "A porta 53 é frequentemente ocupada por serviços \"DNSStubListener\" ou \"systemd-resolved\". Por favor leia essa instrução para resolver isso.",
+ "port_in_use": "Essa porta já está em uso",
+ "privacy_policy": "Política de privacidade",
+ "processing_time_ms": "%value% ms",
+ "protection": "Proteção",
+ "protocol": "Protocolo",
+ "queries": "Consultas",
+ "queries_total": "| %count% consulta no total | %count% consultas no total",
+ "query_details": "Detalhes da consulta",
+ "query_log": "Registro de consultas",
+ "query_log_all_reasons": "Todos os motivos",
+ "query_log_all_statuses": "Todos os status",
+ "query_log_allowed": "Na lista branca",
+ "query_log_blocked": "Bloqueado",
+ "query_log_blocked_by_filter": "Bloqueado pelo filtro",
+ "query_log_blocked_by_parental_control": "Bloqueado pelo controle parental",
+ "query_log_blocked_services": "Serviços bloqueados",
+ "query_log_blocked_threats": "Ameaças bloqueadas",
+ "query_log_custom_filtering_rules": "Regras de filtragem personalizadas",
+ "query_log_detail_address": "Eendereço: %value% ",
+ "query_log_detail_category": "Categoria: %value% ",
+ "query_log_detail_country": "País: %value% ",
+ "query_log_detail_date": "Data: %value% ",
+ "query_log_detail_dns_server": "Servidor DNS: %value% ",
+ "query_log_detail_domain": "Domínio: %value% ",
+ "query_log_detail_ecs": "ECS: %value% ",
+ "query_log_detail_elapsed": "Tempo decorrido: %value% ",
+ "query_log_detail_name": "Nome: %value% ",
+ "query_log_detail_network": "Rede: %value% ",
+ "query_log_detail_original_response": "Resposta original:",
+ "query_log_detail_protocol": "Protocolo: %value% ",
+ "query_log_detail_reason": "Motivo:",
+ "query_log_detail_response": "Resposta:",
+ "query_log_detail_response_code": "Código de resposta: %value% ",
+ "query_log_detail_rules": "Regra(s):",
+ "query_log_detail_served_from_cache": "Servido do cache: %value% ",
+ "query_log_detail_service_name": "Nome do serviço: %value% ",
+ "query_log_detail_source": "Fonte: %value% ",
+ "query_log_detail_status": "Status: %value% ",
+ "query_log_detail_time": "Tempo: %value% ",
+ "query_log_detail_type": "Tipo: %value% ",
+ "query_log_nothing_available": "Nada encontrado",
+ "query_log_processed": "Processado",
+ "query_log_retention": "Rotação de registro",
+ "query_log_rewritten": "Reescrito",
+ "query_log_safe_search": "Pesquisa segura",
+ "query_log_strict_search": "Use aspas duplas para uma pesquisa mais criteriosa",
+ "rate_limit_subnet_len_ipv4_error": "O comprimento do prefixo da sub-rede IPv4 deve estar entre 0 e 32",
+ "rate_limit_subnet_len_ipv6_error": "O comprimento do prefixo da sub-rede IPv6 deve estar entre 0 e 128",
+ "reason_table_header": "Motivo",
+ "refresh_btn": "Atualizar",
+ "refresh_statics": "Atualizar estatísticas",
+ "release_notes": "Notas de lançamento",
+ "remove": "Remover",
+ "remove_tag": "Remover %value%",
+ "report_an_issue": "Reportar um problema",
+ "request_table_header": "Solicitação",
+ "requests_table_header": "Solicitações",
+ "reset": "Redefinir",
+ "reset_settings": "Redefinir configurações",
+ "reset_settings_confirm": "Sim, redefinir",
+ "response_details": "Detalhes da resposta",
+ "response_time": "Tempo de resposta",
+ "result": "Resultado",
+ "resume_protection_timer": "A proteção será retomada em %time%",
+ "rewrite_add": "Adicionar reescrita de DNS",
+ "rewrite_domain_input_placeholder": "Insira o nome de domínio ou wildcard",
+ "rewrite_edit": "Editar reconfiguração de DNS",
+ "rewrite_hosts_applied": "Reescrito pela regra do arquivo de hosts",
+ "rewrite_settings_updated": "Configurações de reescrita de DNS atualizadas com sucesso",
+ "rewrite_updated": "Reconfiguração de DNS atualizada com êxito",
+ "rewrites_answer_input_placeholder": "Insira o endereço IP, registro CNAME, A ou AAAA",
+ "rewrites_empty": "Ainda não há reescritas, adicione a primeira ",
+ "rewrites_remove_desc": "A reescrita de DNS para %value% será removida",
+ "rewrites_remove_title": "Remover reescrita de DNS?",
+ "rewrites_tooltip_examples_item1": "Reescreva respostas de DNS apenas para este nome de domínio",
+ "rewrites_tooltip_examples_item2": "Reescreva respostas de DNS para todos os subdomínios de example.org",
+ "rewrites_tooltip_instructions_item1": "Para resolver o nome de domínio para um endereço IP, insira um endereço IPv4 para registros A e um endereço IPv6 para registros AAAA",
+ "rewrites_tooltip_instructions_item2": "Para redirecionar o nome de domínio, insira o registro CNAME do domínio de destino",
+ "rewrites_tooltip_instructions_item3": "Para manter apenas registros A, insira",
+ "rewrites_tooltip_instructions_item4": "Para manter apenas registros AAAA, insira",
+ "rewritten": "Reescrito",
+ "rows_per_page": "Linhas por página: %value%",
+ "rules_label": "Regras",
+ "safe_browsing": "Navegação segura",
+ "safe_search": "Pesquisa segura",
+ "safe_search_used": "Pesquisa segura usada",
+ "safe_search_used_tooltip": "O número de consultas DNS a mecanismos de busca para as quais o Safe Search foi aplicado",
+ "saturday": "Sábado",
+ "save": "Salvar",
+ "save_btn": "Salvar",
+ "search_placeholder": "Procurar",
+ "selected": "%value% selecionado",
+ "server_config_blocking_mode": "Modo de bloqueio",
+ "server_config_blocking_mode_custom_ip_desc": "Endereço IP personalizado: Responder com um endereço IP definido manualmente",
+ "server_config_blocking_mode_default_desc": "Padrão: Responder com 0.0.0.0 para A e :: para AAAA se bloqueado por uma regra no estilo bloqueio de anúncios. Responder com o endereço IP especificado na regra se bloqueado por uma regra no estilo /etc/hosts",
+ "server_config_blocking_mode_ipv4": "Bloqueando IPv4",
+ "server_config_blocking_mode_ipv4_faq": "Endereço IP a ser retornado para uma solicitação A bloqueada",
+ "server_config_blocking_mode_ipv4_placeholder": "Digite o endereço de IPv4",
+ "server_config_blocking_mode_ipv6": "Bloqueando IPv6",
+ "server_config_blocking_mode_ipv6_faq": "Endereço IP a retornar para uma requisição AAAA bloqueada",
+ "server_config_blocking_mode_ipv6_placeholder": "Digite o endereço de IPv6",
+ "server_config_blocking_mode_null_ip_desc": "Endereço IP nulo: Responder com 0.0.0.0 para A e :: para AAAA",
+ "server_config_blocking_mode_nxdomain_desc": "NXDOMAIN: responder com NXDOMAIN",
+ "server_config_blocking_mode_refused_desc": "REFUSED: responder com o código REFUSED",
+ "server_config_blocking_mode_ttl": "TTL da resposta bloqueada, em segundos",
+ "server_config_blocking_mode_ttl_faq": "Especifica por quanto tempo os clientes devem armazenar em cache uma resposta filtrada",
+ "server_config_custom_ip": "Endereço de IP personalizado",
+ "server_config_default": "Padrão",
+ "server_config_disable_ipv6": "Desativar resolução de endereços IPv6",
+ "server_config_disable_ipv6_desc": "Descartar todas as consultas DNS para endereços IPv6 (tipo AAAA) e remover indicações de IPv6 das respostas HTTPS",
+ "server_config_dnssec_enable": "Ativar DNSSEC",
+ "server_config_dnssec_enable_desc": "Defina o sinalizador DNSSEC nas consultas DNS de saída e verifique o resultado. Requer um resolvedor com DNSSEC habilitado",
+ "server_config_edns_cs_desc": "Adicionar a opção EDNS Client Subnet (ECS) às requisições upstream e registrar os valores enviados pelos clientes no log de consultas",
+ "server_config_edns_enable": "Ativar a sub-rede do cliente EDNS",
+ "server_config_edns_use_custom_ip": "Usar endereço IP personalizado para EDNS",
+ "server_config_edns_use_custom_ip_desc": "Substituir o endereço IP do cliente pelo endereço IP personalizado abaixo",
+ "server_config_null_ip": "Endereço IP nulo",
+ "server_config_nxdomain": "NXDOMAIN",
+ "server_config_rate_limit": "Limite de taxa, RPS",
+ "server_config_rate_limit_faq": "Número de requisições por segundo permitidas por cliente. Um valor de 0 significa que não há limite",
+ "server_config_rate_limit_placeholder": "Insira a velocidade limite",
+ "server_config_rate_limit_whitelist": "Lista de permissões de limitação de taxa",
+ "server_config_rate_limit_whitelist_faq": "Endereços IP excluídos da limitação de velocidade",
+ "server_config_refused": "REFUSED",
+ "server_config_subnet_len_ipv4": "Comprimento do prefixo de sub-rede para endereços IPv4",
+ "server_config_subnet_len_ipv4_faq": "Comprimento do prefixo de sub-rede para endereços IPv4 usados para limitação de velocidade. O padrão é 24",
+ "server_config_subnet_len_ipv6": "Comprimento do prefixo de sub-rede para endereços IPv6",
+ "server_config_subnet_len_ipv6_faq": "Comprimento do prefixo de sub-rede para endereços IPv6 usados para limitação de velocidade. O padrão é 56",
+ "server_config_subnet_len_placeholder": "Insira o comprimento do prefixo",
+ "set_static_ip": "Definir um endereço de IP estático",
+ "settings": "Configurações",
+ "settings_anonymize_client_ip": "Anonimizar endereços IP de clientes",
+ "settings_anonymize_client_ip_desc": "Substituir parte do endereço IP por zeros",
+ "settings_browsing_security": "Segurança da navegação",
+ "settings_browsing_security_desc": "Bloquear o acesso a domínios encontrados no banco de dados de domínios maliciosos e de phishing",
+ "settings_confirm_clear_query_log": "Limpar registro de consultas?",
+ "settings_confirm_clear_query_log_desc": "Isso excluirá todas as solicitações do log de consultas . As estatísticas ainda estarão disponíveis",
+ "settings_confirm_clear_statistics": "Limpar estatísticas?",
+ "settings_confirm_clear_statistics_desc": "Isso limpará todas as estatísticas. O registro de consultas ainda estará disponível",
+ "settings_confirm_decrease_log_rotation_interval": "Diminuir o intervalo de rotação de logs?",
+ "settings_confirm_decrease_log_rotation_interval_desc": "Isso excluirá todos os registros mais antigos que %value%",
+ "settings_confirm_decrease_stats_rotation_interval": "Diminuir o intervalo de retenção de estatísticas?",
+ "settings_confirm_decrease_stats_rotation_interval_desc": "Isso excluirá todas as estatísticas mais antigas que %value%",
+ "settings_custom": "Personalizado",
+ "settings_days": "| %count% dia | %count% dias",
+ "settings_domain_names": "Nomes de domínio, um por linha",
+ "settings_filter_requests": "Solicitações de filtro",
+ "settings_filter_requests_desc": "Você pode usar Listas de bloqueio , Listas de permissão e Regras de usuário para configurar regras de filtragem",
+ "settings_filtering_and_security": "Filtragem e segurança",
+ "settings_general_short": "Geral",
+ "settings_global": "Global",
+ "settings_hours": "| %count% hora | %count% horas",
+ "settings_log_dns_requests": "Registros de solicitação de DNS",
+ "settings_log_rotation_hours": "Rotação do log, em horas",
+ "settings_notify_changes_saved": "Alterações salvas",
+ "settings_notify_query_log_cleared": "Registro de consultas apagado",
+ "settings_notify_statistics_cleared": "Estatísticas apagadas",
+ "settings_parental_control": "Controle parental",
+ "settings_parental_control_desc": "Bloquear sites com conteúdo impróprio para crianças",
+ "settings_rotation_placeholder": "Use números",
+ "settings_rule_syntax": "Sintaxe de regras",
+ "settings_safe_search": "Pesquisa segura",
+ "settings_safe_search_desc": "Bloquear conteúdo impróprio nos resultados da pesquisa",
+ "settings_statistics": "Estatísticas",
+ "settings_statistics_clear": "Limpar estatísticas",
+ "settings_statistics_desc": "Exibir estatísticas de solicitações e clientes no painel de controle",
+ "settings_statistics_retention": "Permanência das estatísticas",
+ "settings_statistics_retention_hours": "Retenção de estatísticas, em horas",
+ "settings_tooltip_domain_names": "Nomes de domínio, wildcards ou regras de filtragem",
+ "settings_tooltip_examples": "Exemplos:",
+ "settings_yes_clear": "Sim, limpar",
+ "settings_yes_decrease": "Sim, diminuir",
+ "setup_complete": "A configuração foi concluída e o AdGuard Home está pronto para uso",
+ "setup_devices_android_list_1": "Abra o aplicativo Configurações do seu dispositivo e selecione Wi-Fi .",
+ "setup_devices_android_list_2": "Mantenha pressionada sua rede atual e, em seguida, selecione Modificar rede . Não é possível definir DNS personalizado para conexões de Internet móvel.",
+ "setup_devices_android_list_3": "Em alguns dispositivos, pode ser necessário selecionar a caixa de seleção Avançado para ver mais configurações. Para personalizar as configurações de DNS do seu Android, você precisa alterar as configurações de IP de DHCP para Estático .",
+ "setup_devices_android_list_4": "Altere os valores de DNS 1 e DNS 2 para os endereços do seu servidor AdGuard Home.",
+ "setup_devices_android_title": "Android",
+ "setup_devices_dns_android_list_1": "O Android 9 oferece suporte nativo a DNS-over-TLS . Para configurá-lo, vá em Configurações → Rede e Internet → Avançado → DNS privado e insira o nome do seu domínio.",
+ "setup_devices_dns_android_list_2": "AdGuard para Android oferece suporte à configuração de DNS-over-HTTPS , DNS-over-TLS e DNS-over-QUIC .",
+ "setup_devices_dns_android_list_3": "Intra adiciona suporte a DNS-over-HTTPS ao Android.",
+ "setup_devices_dns_ios_list_1": "AdGuard para iOS oferece suporte à configuração de DNS-over-HTTPS , DNS-over-TLS e DNS-over-QUIC .",
+ "setup_devices_dns_ios_list_2": "DNSCloak é compatível com DNS-over-HTTPS , mas para configurá-lo para usar seu próprio servidor, você precisará gerar um DNS Stamp para ele.",
+ "setup_devices_dns_list_1": "Para DNS-sobre-TLS , use ",
+ "setup_devices_dns_list_2": "Para DNS-sobre-HTTPS , use ",
+ "setup_devices_dns_list_3": "Para DNS-sobre-QUIC , use ",
+ "setup_devices_dns_list_devices": "Aqui está uma lista de softwares que você pode usar.",
+ "setup_devices_dns_macos_desc": "Em um dispositivo com iOS 14 ou macOS Big Sur, você pode baixar um arquivo mobileconfig especial que adiciona servidores de DNS-over-HTTPS ou DNS-over-TLS às configurações de DNS.",
+ "setup_devices_dns_other_list_1": "O próprio AdGuard Home pode ser usado como um cliente DNS seguro em qualquer plataforma.",
+ "setup_devices_dns_other_list_2": "dnsproxy suporta todos os protocolos de DNS seguros conhecidos.",
+ "setup_devices_dns_other_list_3": "dnscrypt-proxy suporta DNS-sobre-HTTPS .",
+ "setup_devices_dns_other_list_4": "Mozilla Firefox suporta DNS-sobre-HTTPS .",
+ "setup_devices_dns_other_list_5": "Para mais implementações, veja DNSCrypt e privacidade de DNS .",
+ "setup_devices_ios_list_1": "Abra o aplicativo de Configurações do seu dispositivo e selecione Wi‑Fi . Não é possível definir DNS personalizado para conexões de Internet móvel.",
+ "setup_devices_ios_list_2": "Toque no nome da rede atualmente ativa.",
+ "setup_devices_ios_list_3": "Vá para DNS → Configurar DNS → Manual e adicione os endereços do seu servidor AdGuard Home.",
+ "setup_devices_ios_list_4": "Toque em Salvar .",
+ "setup_devices_ios_title": "iOS",
+ "setup_devices_macos_list_1": "Clique no menu Apple → Preferências do Sistema → Rede .",
+ "setup_devices_macos_list_2": "Selecione a conexão necessária e clique em Avançado .",
+ "setup_devices_macos_list_3": "Selecione a aba DNS e insira os endereços do seu servidor AdGuard Home.",
+ "setup_devices_macos_title": "macOS",
+ "setup_devices_router_desc_1": "Configure o AdGuard Home no roteador da sua rede doméstica e proteja todos os dispositivos conectados a ele. Não é necessário configurar manualmente cada dispositivo.",
+ "setup_devices_router_desc_2": "Alguns roteadores não permitem configurar um servidor DNS personalizado. Nesse caso, você pode tentar configurar o AdGuard Home como um servidor DHCP . Se isso não funcionar, consulte o manual do seu roteador para obter instruções sobre como personalizar servidores DNS para o seu roteador específico.",
+ "setup_devices_router_list_1": "Geralmente, você pode acessá-los pelo seu navegador por meio de um URL, como http://192.168.0.1 ou http://192.168.1.1 . Pode ser solicitado que você insira uma senha. Muitas vezes, é possível redefinir a senha pressionando um botão no próprio roteador, mas esteja ciente de que você pode perder toda a configuração do roteador. Se o seu roteador for configurado por meio de um aplicativo, instale o aplicativo no seu telefone ou PC e use-o para acessar as configurações do roteador.",
+ "setup_devices_router_list_1_title": "Abra as configurações do seu roteador.",
+ "setup_devices_router_list_2": "Para encontrar as configurações, procure por DNS ao lado de um campo que permite dois ou três conjuntos de números, cada um dividido em quatro grupos de um a três dígitos.",
+ "setup_devices_router_list_2_title": "Nas configurações de DHCP/DNS, insira os endereços do seu servidor AdGuard Home.",
+ "setup_devices_router_title": "Roteador",
+ "setup_devices_windows_list_1": "Abra o Painel de Controle a partir do menu Iniciar ou da Pesquisa do Windows .",
+ "setup_devices_windows_list_2": "Clique em Rede e Internet → Central de Rede e Compartilhamento → Alterar as configurações do adaptador .",
+ "setup_devices_windows_list_3": "Clique com o botão direito na sua conexão ativa e selecione Propriedades .",
+ "setup_devices_windows_list_4": "Na lista, selecione Protocolo de Internet versão 4 (TCP/IPv4) para IPv4 ou Protocolo de Internet versão 6 (TCP/IPv6) para IPv6.",
+ "setup_devices_windows_list_5": "Clique em Propriedades → Usar os seguintes endereços de servidor DNS .",
+ "setup_devices_windows_list_6": "Insira os endereços do seu servidor AdGuard Home.",
+ "setup_devices_windows_title": "Windows",
+ "setup_dns_desc": "Configure seu roteador ou outros dispositivos para usar os seguintes endereços de servidor DNS:",
+ "setup_dns_notice_new": "Para usar DNS-over-HTTPS ou DNS-over-TLS , você precisa configurar a criptografia nas configurações do AdGuard Home.",
+ "setup_dns_privacy_ioc_mac": "configuração para iOS e macOS",
+ "setup_dns_privacy_other_title": "Outras implementações",
+ "setup_dns_quote_desc": "Para funcionar corretamente, o servidor DNS do AdGuard Home requer um endereço IP estático na sua rede local. Caso contrário, seu roteador pode eventualmente atribuir a ele um endereço IP diferente e interromper a proteção de DNS",
+ "setup_dns_quote_title": "Atribua um endereço IP estático ao servidor DNS do AdGuard Home na sua rede local",
+ "setup_dns_title": "Defina o endereço do servidor DNS do AdGuard Home",
+ "setup_dns_title_banner": "Configurações do servidor DNS",
+ "setup_guide": "Guia de configuração",
+ "setup_guide_auth_privacy": "Eu aceito a Política de privacidade e os Termos e condições dos sites AdGuard",
+ "setup_guide_auth_subtitle": "Isso é necessário para proteger o painel de administração do AdGuard Home contra acesso não autorizado",
+ "setup_guide_auth_title": "Configurar proteção por senha",
+ "setup_guide_desc": "Configure seus dispositivos para usar o AdGuard Home",
+ "setup_guide_device_type": "Selecione o seu tipo de dispositivo",
+ "setup_guide_greeting_button": "Executar assistente",
+ "setup_guide_greeting_desc": "Este assistente ajudará você a configurar o AdGuard Home:",
+ "setup_guide_greeting_list_1": "Crie credenciais para acessar o painel de administração",
+ "setup_guide_greeting_list_2": "Defina o endereço da interface web do AdGuard Home",
+ "setup_guide_greeting_list_3": "Defina o endereço do servidor DNS do AdGuard Home",
+ "setup_guide_greeting_list_4": "Obtenha instruções para cada um dos seus dispositivos",
+ "setup_guide_greeting_list_5": "Acesse o painel do AdGuard Home ",
+ "setup_guide_greeting_title": "Configurar o AdGuard Home",
+ "setup_guide_title": "Configure seus dispositivos para usar o AdGuard Home",
+ "setup_ui_desc": "Use estes endereços para acessar a interface web do AdGuard Home. Se necessário, altere a porta",
+ "setup_ui_title": "Defina o endereço da interface web do AdGuard Home",
+ "setup_ui_title_banner": "Configurações da interface web",
+ "show_blocked_responses": "Bloqueado",
+ "show_more_count": "+ %count%",
+ "show_processed_responses": "Processado",
+ "sort_asc": "Ordenar de A a Z",
+ "sort_desc": "Ordenar de Z a A",
+ "source_label": "Fonte",
+ "source_tooltip": "Fonte",
+ "static_dhcp_leases_not_found": "Concessões DHCP estáticas não encontradas",
+ "stats_adult": "Bloqueado sites adultos",
+ "stats_query_domain": "Principais domínios consultados",
+ "status_table_header": "Status",
+ "sunday": "Domingo",
+ "system_host_files": "Arquivos hosts do sistema",
+ "system_theme": "Tema do sistema",
+ "table_client": "Cliente",
+ "tags_title": "Marcadores",
+ "test_upstreams": "Testar DNS primário",
+ "threats_blocked": "Ameaças bloqueadas",
+ "threats_blocked_tooltip": "O número de consultas DNS bloqueadas pelo módulo de segurança de navegação do AdGuard",
+ "thursday": "Quinta-feira",
+ "time_table_header": "Data",
+ "top_blocked_domains": "Principais domínios bloqueados",
+ "top_clients": "Principais clientes",
+ "top_upstreams": "Melhores servidores DNS primários",
+ "total": "Total",
+ "try_again": "Tente novamente",
+ "tuesday": "Terça-feira",
+ "type_value": "Tipo: %value%",
+ "unavailable_dhcp": "DHCP não está disponível",
+ "unavailable_dhcp_desc": "O AdGuard Home não pode executar um servidor DHCP em seu sistema operacional",
+ "unblock": "Desbloquear",
+ "unblock_client": "Desbloquear cliente",
+ "unknown_filter": "Filtro desconhecido %filterId%",
+ "update_button": "Atualizar",
+ "update_filters_custom_hours": "Intervalo personalizado, em horas",
+ "update_filters_disable": "Desativar",
+ "update_filters_interval_custom": "Personalizado",
+ "update_filters_interval_daily": "Diariamente",
+ "update_filters_interval_hourly": "A cada hora",
+ "update_filters_interval_weekly": "Toda semana",
+ "update_filters_title": "Atualiza os filtros automaticamente",
+ "updated_upstream_dns_toast": "Servidores upstream salvos",
+ "updates_checked": "Uma nova versão do AdGuard Home está disponível\n",
+ "updates_version_equal": "O AdGuard Home está atualizado.",
+ "upstream": "Servidor DNS primário",
+ "upstream_bootstrap_dns_faq": "Endereços IP dos servidores DNS usados para resolver os endereços IP dos resolvedores upstream DoH/DoT. Comentários não são permitidos",
+ "upstream_bootstrap_dns_title": "Servidores DNS de bootstrap, um por linha",
+ "upstream_dns": "Servidores DNS primário",
+ "upstream_dns_addresses": "Endereços de servidor, um por linha",
+ "upstream_dns_addresses_faq": "Saiba mais sobre a configuração de servidores DNS upstream. Aqui está uma lista de provedores de DNS conhecidos para escolher.",
+ "upstream_dns_configured_in_file": "Configurado em %path%",
+ "upstream_dns_fastest_addr": "Endereço de IP mais rápido",
+ "upstream_dns_parallel_requests": "Solicitações paralelas",
+ "upstream_dns_placeholder": "Endereços IP, sdns://, quic://, https://, h3://, ou tls://",
+ "upstream_dns_servers_title": "Servidores DNS primário",
+ "upstream_enable_reverse_lookup_desc": "Resolver reversamente os endereços IP dos clientes para seus nomes de host enviando consultas PTR aos resolvedores apropriados: servidores DNS privados para clientes locais ou servidores upstream para clientes com endereços IP públicos",
+ "upstream_enable_reverse_lookup_title": "Habilitar consulta reversa de endereços IP de clientes",
+ "upstream_example_multiple_upstreams_reserved": "%value%: múltiplos upstreams para domínios específicos ",
+ "upstream_example_tcp": "%ipv4%,%ipv6%: DNS padrão (via TCP)",
+ "upstream_example_tcp_hostname": "%value%: DNS padrão (via TCP, hostname)",
+ "upstream_example_tcp_port": "%ipv4%,%ipv6%: DNS padrão (via TCP, com porta)",
+ "upstream_example_udp": "%ipv4%,%ipv6%: DNS padrão (via UDP)",
+ "upstream_example_udp_hostname": "%value%: DNS padrão (via UDP, hostname)",
+ "upstream_example_udp_port": "%ipv4%,%ipv6%: DNS padrão (via UDP, com porta)",
+ "upstream_example_upstream_comment": "%value%: um comentário.",
+ "upstream_example_upstream_doh": "%value%: criptografado DNS-over-HTTPS ",
+ "upstream_example_upstream_doh3": "%value%: DNS-over-HTTPS criptografado com HTTP/3 forçado e sem fallback para HTTP/2",
+ "upstream_example_upstream_doq": "%value%: DNS-over-QUIC criptografado",
+ "upstream_example_upstream_dot": "%value%: DNS-over-TLS criptografado",
+ "upstream_example_upstream_reserved": "%value%: um upstream para domínios específicos ",
+ "upstream_example_upstream_sdns": "%value%: DNS Stamps para resolvedores DNSCrypt ou DNS-over-HTTPS ",
+ "upstream_examples_title": "Exemplos",
+ "upstream_fallback_title": "Servidores DNS de fallback, um por linha",
+ "upstream_fallback_title_faq": "Servidores DNS de fallback a serem usados quando os servidores DNS upstream estiverem indisponíveis. Use a mesma sintaxe dos servidores DNS upstream",
+ "upstream_private_ptr_resolvers_desc": "Resolver solicitações PTR, SOA e NS para domínios ARPA que contêm endereços IP privados por meio de servidores upstream privados, DHCP, /etc/hosts e assim por diante. Se desativado, o AdGuard Home responderá a todas essas solicitações com NXDOMAIN",
+ "upstream_private_ptr_resolvers_title": "Usar resolvedores DNS reversos privados",
+ "upstream_ptr": "Servidores de DNS reverso privados, um por linha",
+ "upstream_ptr_faq_1": "O AdGuard Home usa estes servidores DNS para solicitações privadas de PTR, SOA e NS. Solicitações privadas vêm de clientes com um endereço IP privado e solicitam um domínio ARPA que contém uma sub-rede de um intervalo de endereços IP privados, como %value%",
+ "upstream_ptr_faq_2": "Se este campo estiver vazio, os resolvedores DNS padrão do seu sistema operacional serão usados, exceto os endereços IP do AdGuard Home",
+ "upstream_ptr_faq_3": "Por padrão, o AdGuard Home usa os seguintes resolvedores de DNS reverso: %value_1% e %value_2%",
+ "upstream_timeout": "Tempo limite do upstream, em segundos",
+ "upstream_timeout_faq": "Tempo de espera por uma resposta do servidor upstream",
+ "upstream_timeout_placeholder": "Insira a duração do tempo limite do servidor upstream",
+ "upstreams": "DNS primário",
+ "url_label": "URL",
+ "use_saved_key": "Utilizar uma chave privada existente",
+ "user_rules": "Regras de usuário",
+ "user_rules_add_to_allowlist": "Adicionar na lista de permissões",
+ "user_rules_allow_service": "Permitir serviço",
+ "user_rules_browsing_security_disabled": "Segurança da navegação desativada",
+ "user_rules_check_button": "Verificar",
+ "user_rules_check_client_label": "Identificador do cliente (nome, ClientID ou endereço IP)",
+ "user_rules_check_client_placeholder": "Insira o identificador do cliente",
+ "user_rules_check_dns_record_type_label": "Tipo de registro DNS",
+ "user_rules_check_hostname_label": "Nome do anfitrião ou nome de domínio",
+ "user_rules_check_hostname_placeholder": "Digite o destino",
+ "user_rules_check_title": "Verificar a filtragem de domínio globalmente ou para um cliente",
+ "user_rules_cname": "CNAME: %cname%",
+ "user_rules_disable_browsing_security": "Desativar segurança da navegação",
+ "user_rules_disable_filter": "Desativar filtro",
+ "user_rules_disable_parental_control": "Desativar controle parental",
+ "user_rules_disable_safe_search": "Desativar pesquisa segura",
+ "user_rules_dns_record_type_placeholder": "Selecione o tipo de registro DNS",
+ "user_rules_dns_rewrite_removed": "Regra removida da reescrita de DNS",
+ "user_rules_domain": "Domínio: %value%",
+ "user_rules_domain_blocked": "O domínio está bloquado",
+ "user_rules_domain_is_allowed": "O domínio é permitido",
+ "user_rules_domain_is_processed": "O domínio está processado",
+ "user_rules_edit_dns_rewrite": "Editar reconfiguração de DNS",
+ "user_rules_example_block": "||%domain% Bloquear acesso a %domain% e todos os seus subdomínios",
+ "user_rules_example_comment": "! Isto é um comentário Para comentar, inicie a linha com ! ou #",
+ "user_rules_example_comment_2": "# Isto também é um comentário Para comentar, comece a linha com ! ou #",
+ "user_rules_example_regex": "/REGEX/ Bloquear o acesso a domínios que correspondem à expressão regular especificada",
+ "user_rules_example_response": "127.0.0.1 %domain% Responda com 127.0.0.1 para %domain% apenas, não para seus subdomínios",
+ "user_rules_example_unblock": "@@||%domain% Desbloquear o acesso a %domain% e todos os seus subdomínios",
+ "user_rules_filter_was_disabled": "%value% foi desativado",
+ "user_rules_ip": "Endereço de IP: %ip%",
+ "user_rules_learn_more": "Saiba mais sobre criar suas próprias listas de hosts.",
+ "user_rules_no_rules_matched": "Resultado: Nenhuma regra correspondeu",
+ "user_rules_parental_control_disabled": "Senha do controle parental desativado",
+ "user_rules_placeholder": "Este é um espaço reservado",
+ "user_rules_reason": "Motivo: %reason%",
+ "user_rules_reason_allowed_by": "Permitido por %source%",
+ "user_rules_reason_blocked_by": "Bloqueado por %source%",
+ "user_rules_reason_filtered_by": "Filtrado por: %source%",
+ "user_rules_redirected_to": "Redirecionado para: %value%",
+ "user_rules_remove_dns_rewrite": "Remover reescrita de DNS",
+ "user_rules_rewrite_rule_is_applied": "Regra de reescrita aplicada",
+ "user_rules_rewritten_to": "Reescrito para: %value%",
+ "user_rules_rule": "Regra: %rule%",
+ "user_rules_rule_added_to_custom_filtering_rules": "Regra adicionada às regras de filtragem personalizadas",
+ "user_rules_rule_removed": "Regra removida das regras de filtragem personalizadas",
+ "user_rules_safe_search_disabled": "Pesquisa segura desativada",
+ "user_rules_service": "Serviço: %service%",
+ "user_rules_service_allowed": "O serviço %value% foi permitido",
+ "user_rules_source": "Fonte: %value%",
+ "user_rules_source_allowlist": "Lista de permissões: %value%",
+ "user_rules_source_blocklist": "Listas de bloqueio: %value%",
+ "user_rules_status": "Status: %reason%",
+ "username_label": "Nome de usuário",
+ "username_placeholder": "Digite o nome de usuário",
+ "validated_with_dnssec": "Validado com DNSSEC",
+ "wednesday": "Quarta-feira",
+ "whois": "WHOIS",
+ "yes": "Sim",
+ "yes_disable": "Sim, desativar",
+ "yes_reset": "Sim, redefinir"
+}
diff --git a/client_v2/src/__locales/pt-pt.json b/client_v2/src/__locales/pt-pt.json
new file mode 100644
index 00000000000..c23274c65c9
--- /dev/null
+++ b/client_v2/src/__locales/pt-pt.json
@@ -0,0 +1,864 @@
+{
+ "access_settings_allowed_faq": "Uma lista de CIDRs, endereços IP ou ClientIDs . Se este campo tiver entradas, o AdGuard Home aceitará solicitações apenas desses clientes",
+ "access_settings_allowed_placeholder": "Introduzir clientes autorizados",
+ "access_settings_allowed_title": "Clientes permitidos, um por linha",
+ "access_settings_blocked_faq_1": "O AdGuard Home irá ignorar as consultas DNS que correspondam a estes domínios. Essas consultas não aparecem no registo de consultas. Pode introduzir nomes de domínio exatos, caracteres curinga ou regras de filtragem de URL, tais como example.org, *.example.org ou ||example.org^",
+ "access_settings_blocked_faq_2": "Não confundir com filtros",
+ "access_settings_blocked_placeholder": "Insira domínios não permitidos",
+ "access_settings_blocked_title": "Domínios não permitidos, um por linha",
+ "access_settings_disallowed_faq": "Uma lista de CIDRs, Endereços de IP ou IDs de cliente . Se este campo tiver entradas, o AdGuard Home soltará as solicitações desses clientes. Este campo é ignorado se houver entradas em clientes permitidos ",
+ "access_settings_disallowed_placeholder": "Introduzir clientes não autorizados",
+ "access_settings_disallowed_title": "Clientes não permitidos, um por linha",
+ "access_settings_saved_toast": "Definições de acesso quardadas",
+ "access_settings_title": "Definições de acesso",
+ "actions_table_header": "Acções",
+ "add": "Adicionar",
+ "add_allowlist": "Adicionar lista de permissões",
+ "add_blocklist": "Adicionar lista de bloqueio",
+ "add_persistent_client": "Adicionar como cliente persistente",
+ "add_to_allowlist": "Adicionar na lista de permissões",
+ "ads_blocked": "Anúncios bloqueados",
+ "ads_blocked_card": "Anúncios bloqueados",
+ "ads_blocked_tooltip": "O número de consultas DNS bloqueadas por filtros de bloqueio de anúncios e pela lista de bloqueio de hosts",
+ "adult_websites_blocked": "Sítios adultos bloqueados",
+ "adult_websites_blocked_tooltip": "O número de sítios adultos bloqueados",
+ "all_domains": "Todos os domínios",
+ "all_lists_up_to_date_toast": "Todas as listas já estão atualizadas",
+ "all_rewrites_disabled": "Todas as respostas DNS personalizadas serão desativadas",
+ "all_rewrites_enabled": "Todas as respostas DNS personalizadas serão ativadas",
+ "allow_service": "Permitir serviço",
+ "allowed": "Na lista branca",
+ "allowlist_add": "Adicionar lista de permissões",
+ "allowlist_edit": "Editar lista de permissões",
+ "allowlist_empty": "Ainda não há listas de permissões — adicione a sua primeira ",
+ "allowlist_placeholder_example": "Minha lista de permissões",
+ "allowlist_remove": "Remover lista de permissões?",
+ "allowlist_remove_desc": "%value% será removido",
+ "allowlists": "Lista de permissões",
+ "allowlists_title": "Lista de permissões",
+ "apply": "Aplicar",
+ "aria_clear_input": "Limpar entrada",
+ "autofix_warning_list": "Irá executar as seguintes tarefas:
Desativar o DNSStubListener do sistema
Definir o endereço do servidor DNS como 127.0.0.1
Substituir o destino do link simbólico de /etc/resolv.conf por /run/systemd/resolve/resolv.conf
Parar o DNSStubListener (recarregar o serviço systemd-resolved)
",
+ "autofix_warning_result": "Como resultado, todos as solicitações DNS do seu sistema serão processadas pelo AdGuard Home por predefinição.",
+ "autofix_warning_text": "Se clicar em \"Corrigir\", o AdGuardHome irá configurar o seu sistema para utilizar o servidor DNS do AdGuardHome.",
+ "average_time_processing": "Tempo médio de processamento",
+ "average_time_processing_tooltip": "Tempo médio, em milissegundos, de processamento de uma consulta DNS",
+ "average_upstream_response_time": "Tempo médio de resposta upstream",
+ "back": "Retroceder",
+ "block": "Bloquear",
+ "block_client": "Bloquear cliente",
+ "block_for_this_client_only": "Bloquear apenas para este cliente",
+ "blocked": "Bloqueado",
+ "blocked_adult_websites": "Bloqueado pelo controlo parental",
+ "blocked_by_cname_or_ip": "Bloqueado por CNAME ou IP",
+ "blocked_queries": "Consultas bloqueadas",
+ "blocked_service": "Serviço bloqueado",
+ "blocked_services": "Serviços bloqueados",
+ "blocked_services_desc": "Selecione sites e serviços populares que devem ser bloqueados",
+ "blocked_threats": "Ameaças bloqueadas",
+ "blocked_threats_chart": "Ameaças bloqueadas",
+ "blocked_total": "%value% total bloqueado",
+ "blocklist_add_from_list": "Adicionar da lista",
+ "blocklist_add_manual": "Adicionar manualmente",
+ "blocklist_contents": "Conteúdo (.%value%)",
+ "blocklist_edit": "Editar lista de bloqueio",
+ "blocklist_homepage": "Página inicial",
+ "blocklist_placeholder_example": "Minha lista de bloqueio",
+ "blocklist_remove": "Remover lista de bloqueio?",
+ "blocklist_remove_desc": "%value% será removido",
+ "blocklist_url_file_path": "URL ou caminho de ficheiro",
+ "blocklists_add": "Adicionar lista de bloqueio",
+ "blocklists_add_desc": "Você pode selecionar listas de bloqueio da lista ou adicioná-las manualmente",
+ "blocklists_desc": "Os domínios destas listas estão bloqueados",
+ "blocklists_empty": "Ainda não há listas de bloqueio — adicione a sua primeira ",
+ "blocklists_title": "Listas de bloqueio",
+ "bootstrap_dns": "Servidores DNS de arranque",
+ "browse": "Pesquisar",
+ "cache_cleared": "O cache DNS foi apagado com sucesso",
+ "cache_config_clear": "Limpar cache",
+ "cache_config_max_ttl": "Substituir TTL máximo, em segundos",
+ "cache_config_max_ttl_faq": "O valor máximo de tempo de vida (TTL) para entradas no cache de DNS",
+ "cache_config_max_ttl_placeholder": "Digite o TTL máximo",
+ "cache_config_min_ttl": "Substituir TTL mínimo, em segundos",
+ "cache_config_min_ttl_faq": "O valor mínimo de tempo de vida (TTL) para entradas no cache DNS",
+ "cache_config_min_ttl_placeholder": "Digite o TTL mínimo",
+ "cache_config_optimistic": "Cache otimista",
+ "cache_config_optimistic_desc": "O AdGuard Home responderá a partir do cache mesmo que as entradas tenham expirado e tentará atualizá-las",
+ "cache_config_size": "Tamanho do cache, em bytes",
+ "cache_config_size_faq": "Tamanho do cache DNS. Para desativar o cache, deixe em branco",
+ "cache_config_size_validation": "O tamanho do cache deve ser maior que zero quando habilitado",
+ "cache_config_ttl_validation": "O substituto mínimo de cache TTL deve ser menor ou igual ao máximo",
+ "cache_confirm_clear_desc": "Isto removerá todas as respostas DNS em cache. As consultas DNS subsequentes podem ser mais lentas até que o cache seja reconstruído",
+ "cache_confirm_clear_title": "Limpar cache DNS",
+ "cache_enabled": "Ativar cache",
+ "cache_enabled_desc": "Armazenar respostas DNS localmente",
+ "cancel": "Cancelar",
+ "cancel_btn": "Cancelar",
+ "category_tooltip": "Categoria",
+ "changes_saved_success": "Alterações guardadas",
+ "check_dhcp_servers": "Verificar por servidores DHCP",
+ "check_not_found": "Não encontrado nas tuas listas de filtros",
+ "check_updates_btn": "Verificar atualizações",
+ "clear_btn": "Limpar",
+ "clear_query_log": "Limpar o registo de consultas",
+ "client_add": "Adicionar cliente",
+ "client_added": "Cliente adicionado",
+ "client_already_blocked": "O cliente %ip% já está bloqueado",
+ "client_blocked_flash": "Cliente bloqueado",
+ "client_details": "Detalhes do cliente",
+ "client_id": "ID do cliente",
+ "client_id_faq": "Os clientes podem ser identificados através do seu ClientID. Para saber mais, visite o GitHub do AdGuard Home ",
+ "client_id_placeholder": "Insira o ID do cliente",
+ "client_identifier": "Identificador",
+ "client_ip": "IP do cliente",
+ "client_location": "Localização do cliente",
+ "client_removed": "Cliente removido",
+ "client_settings": "Definições do cliente",
+ "client_table_header": "Cliente",
+ "client_unblocked_flash": "Cliente desbloqueado",
+ "clients": "Clientes",
+ "clients_add": "Adicionar cliente",
+ "clients_add_identifier": "Adicionar identificador",
+ "clients_add_name": "Nome do cliente",
+ "clients_add_placeholder": "Insira o nome do cliente",
+ "clients_desc": "Regras para dispositivos ligados ao AdGuard",
+ "clients_dns_cache_size": "Tamanho da cache DNS, em bytes",
+ "clients_dns_cache_size_placeholder": "Utilize números",
+ "clients_dont_collect_stats": "Não coletar estatísticas para este cliente",
+ "clients_dont_log": "Não registrar este cliente",
+ "clients_edit": "Editar cliente",
+ "clients_identifier_already_used": "Este identificador já está em uso",
+ "clients_identifier_format_error": "Use números, letras minúsculas ou hífens",
+ "clients_identifiers": "Identificadores",
+ "clients_identifiers_desc": "Você pode usar o endereço IP do cliente, CIDR, endereço MAC ou ClientID (para DoT, DoH e DoQ). Saiba mais ",
+ "clients_logs_and_statistics": "Registros e estatísticas",
+ "clients_not_found": "Nenhum cliente foi encontrado",
+ "clients_protection": "Proteção",
+ "clients_protection_desc": "Ativar ou desativar recursos de proteção, registos e estatísticas",
+ "clients_remove_desc": "%value% será removido dos clientes persistentes juntamente com suas definições",
+ "clients_remove_title": "Remover cliente?",
+ "clients_tags": "Etiquetas",
+ "clients_tags_desc": "Você pode adicionar etiquetas para este cliente e incluí-las nas regras de filtragem. Saiba mais ",
+ "clients_upstreams_desc": "Servidores DNS upstream, um por linha",
+ "clients_use_dns_cache": "Utilizar cache de DNS",
+ "clients_use_global_settings": "Usar definições globais",
+ "clients_use_global_settings_desc": "Herdar as definições de proteção da sua conta do AdGuard Home",
+ "close": "Fechar",
+ "close_result": "Fechar resultado",
+ "confirm": "Confirmar",
+ "confirm_client_block_desc": "O AdGuard Home irá bloquear todos os pedidos de DNS do cliente %ip% ",
+ "confirm_client_block_title": "Bloquear cliente?",
+ "confirm_client_unblock_desc": "Os pedidos de DNS do cliente %ip% serão permitidos",
+ "confirm_client_unblock_title": "Desbloquear cliente?",
+ "confirm_static_ip": "O AdGuard Home irá configurar %ip% como o seu endereço IP estático. Deseja continuar?",
+ "copied": "Copiado para a área de transferência",
+ "copy": "Copiar",
+ "copy_to_clipboard": "Copiar para a área de transferência",
+ "custom_filtering_rules": "Regras de filtragem personalizadas",
+ "dark_theme": "Tema escuro",
+ "dashboard": "Painel",
+ "delete_btn": "Apagar",
+ "delete_confirm": "Excluir contrato de locação?",
+ "delete_confirm_desc": "Isso irá liberar %ip% ",
+ "delete_table_action": "Apagar",
+ "delete_table_action_confirm": "Sim, apagar",
+ "device_type": "Tipo de dispositivo",
+ "dhcp_add_static_lease": "Adicionar nova concessão estática",
+ "dhcp_config_saved": "Definições DHCP guardadas com sucesso",
+ "dhcp_edit_static_lease": "Editar concessão estática",
+ "dhcp_error": "Falha ao verificar o servidor DHCP. Aguarde um pouco e tente novamente",
+ "dhcp_form_gateway_address": "Endereço IP do gateway",
+ "dhcp_form_gateway_input": "IP do gateway",
+ "dhcp_form_range_start": "Endereço IP inicial",
+ "dhcp_form_range_title": "Intervalo de endereços IP",
+ "dhcp_form_subnet_input": "Máscara de sub-rede",
+ "dhcp_found": "O servidor DHCP não pode ser ativado",
+ "dhcp_ipv4_settings": "Definições DHCP IPv4",
+ "dhcp_ipv6_settings": "Definições DHCP IPv6",
+ "dhcp_lease_added": "Concessão estática \"%key%\" adicionada com sucesso",
+ "dhcp_lease_deleted": "Concessão estática \"%key%\" apagada com sucesso",
+ "dhcp_lease_updated": "Concessão estática \"%key%\" atualizada com sucesso",
+ "dhcp_leases_not_found": "Nenhuma concessão DHCP encontrada",
+ "dhcp_leases_title": "Concessões DHCP",
+ "dhcp_mac_address_already_added": "Este endereço MAC já foi adicionado",
+ "dhcp_new_static_lease": "Nova concessão estática",
+ "dhcp_not_found": "O servidor DHCP está ativo",
+ "dhcp_reset": "Isto irá reiniciar a configuração do DHCP. Todas as alterações serão perdidas",
+ "dhcp_reset_leases": "Repor todas as concessões",
+ "dhcp_reset_leases_confirm": "Isto irá reiniciar todos os períodos de concessão",
+ "dhcp_reset_leases_success": "Concessões de DHCP repostas com sucesso",
+ "dhcp_settings": "Definições de DHCP",
+ "dhcp_static_ip_error": "O servidor DHCP requer um endereço IP estático. Não foi possível confirmar que esta interface de rede o possua",
+ "dhcp_static_leases_not_found": "Nenhuma concessão DHCP estática foi encontrada",
+ "dhcp_table_hostname": "Nome do servidor",
+ "dhcp_table_ip_address": "Endereço de IP",
+ "dhcp_table_mac_address": "Endereço MAC",
+ "disable": "Desativar",
+ "disable_protection_btn": "Desativar proteção",
+ "disabled_dhcp": "Servidor DHCP desativado",
+ "disallow_client_confirm_allowlist_note": "Esta ação irá também excluir a regra %ip% da lista de clientes autorizados.",
+ "disallow_client_confirm_title": "Bloquear este cliente?",
+ "disallow_this_client": "Não permitir este cliente",
+ "dns_access_settings_title": "Definições de acesso",
+ "dns_allowed_clients": "Clientes permitidos",
+ "dns_allowed_clients_label": "Clientes permitidos, um por linha",
+ "dns_allowed_clients_placeholder": "Introduzir clientes autorizados",
+ "dns_blocking_mode": "Modo de bloqueio",
+ "dns_blocking_mode_custom_ip": "Endereço IP personalizado",
+ "dns_blocking_mode_default": "Predefinido",
+ "dns_blocking_mode_ipv4_label": "A bloquear IPv4",
+ "dns_blocking_mode_ipv4_placeholder": "Digite o endereço de IPv4",
+ "dns_blocking_mode_ipv6_label": "A bloquear IPv6",
+ "dns_blocking_mode_ipv6_placeholder": "Digite o endereço de IPv6",
+ "dns_blocking_mode_null_ip": "Endereço IP nulo",
+ "dns_blocking_mode_nxdomain": "NXDOMAIN",
+ "dns_blocking_mode_refused": "REFUSED",
+ "dns_blocking_mode_title": "Modo de bloqueio",
+ "dns_blocking_mode_ttl_label": "TTL da resposta bloqueada, em segundos",
+ "dns_bootstrap_dns_label": "Servidores DNS de arranque, um por linha",
+ "dns_bootstrap_dns_placeholder": "Endereços de IP",
+ "dns_bootstrap_dns_title": "Servidores DNS de arranque",
+ "dns_bootstrap_servers": "Servidores DNS de arranque",
+ "dns_cache_config": "Definição de cache DNS",
+ "dns_cache_configuration_saved_toast": "Definição do cache DNS guardada",
+ "dns_cache_size": "Tamanho do cache",
+ "dns_cache_size_label": "Tamanho da cache DNS, em bytes",
+ "dns_cache_size_title": "Tamanho do cache",
+ "dns_clear_cache": "Limpar cache",
+ "dns_clear_cache_cancel": "Cancelar",
+ "dns_clear_cache_confirm": "Sim, limpar",
+ "dns_clear_cache_yes": "Sim, limpar",
+ "dns_config": "Definição do servidor DNS",
+ "dns_disallowed_clients": "Clientes não permitidos",
+ "dns_disallowed_clients_label": "Clientes não permitidos, um por linha",
+ "dns_disallowed_clients_title": "Clientes não permitidos",
+ "dns_disallowed_domains": "Domínios bloqueados",
+ "dns_disallowed_domains_label": "Domínios não permitidos, um por linha",
+ "dns_disallowed_domains_title": "Domínios bloqueados",
+ "dns_edns_custom_placeholder": "Digite o endereço de IP",
+ "dns_fallback_dns_label": "Servidores DNS de fallback, um por linha",
+ "dns_fallback_dns_placeholder": "Endereços IP, sdns://, quic://, https://, h3:// ou tls://",
+ "dns_fallback_dns_title": "Servidores DNS de fallback",
+ "dns_fallback_servers": "Servidores DNS de fallback",
+ "dns_optimistic_caching": "Cache otimista",
+ "dns_over_https": "DNS-sobre-HTTPS",
+ "dns_over_quic": "DNS-sobre-QUIC",
+ "dns_over_tls": "DNS-sobre-TLS",
+ "dns_override_max_ttl": "Sobrepor o TTL máximo",
+ "dns_override_max_ttl_title": "Sobrepor o TTL máximo",
+ "dns_override_min_ttl": "Sobrepor o TTL mínimo",
+ "dns_override_min_ttl_title": "Sobrepor o TTL mínimo",
+ "dns_privacy": "Privacidade de DNS",
+ "dns_private_reverse_servers_label": "Servidores DNS reversos privados, um por linha",
+ "dns_private_reverse_servers_placeholder": "Endereços IP, sdns://, quic://, https://, h3:// ou tls://",
+ "dns_private_reverse_servers_title": "Servidores DNS reversos privados",
+ "dns_queries": "Consultas de DNS",
+ "dns_queries_tooltip": "O número de consultas DNS processadas nas últimas 24 horas",
+ "dns_query": "Consultas de DNS",
+ "dns_rate_limit": "Limite de velocidade",
+ "dns_rate_limit_allowlist": "Lista de permissões de limitação de taxa",
+ "dns_rate_limit_allowlist_placeholder": "Endereços de IP",
+ "dns_rate_limit_allowlist_title": "Lista de permissões de limitação de taxa",
+ "dns_rate_limit_placeholder": "Insira o limite de velocidade",
+ "dns_rate_limit_title": "Limite de velocidade",
+ "dns_rewrites": "Reescritas de DNS",
+ "dns_server_addresses": "Endereços de servidor",
+ "dns_server_addresses_label": "Endereços de servidor, um por linha",
+ "dns_server_addresses_placeholder": "Endereços IP, sdns://, quic://, https://, h3:// ou tls://",
+ "dns_server_addresses_title": "Endereços de servidor",
+ "dns_server_config_title": "Definição do servidor DNS",
+ "dns_server_configuration_saved_toast": "Definição do servidor DNS guardada",
+ "dns_settings": "DNS",
+ "dns_subnet_placeholder": "Introduza o comprimento do prefixo",
+ "dns_test_upstreams": "Testar DNS primário",
+ "dns_upstream_servers_title": "Servidores DNS primário",
+ "dns_upstream_timeout": "Tempo esgotado de upstream",
+ "dns_upstream_timeout_title": "Tempo esgotado de upstream",
+ "dns_upstream_validation_invalid": "Formato inválido",
+ "domain": "Domínio",
+ "domain_or_client": "Domínio ou cliente",
+ "download_mobileconfig": "Transferir ficheiro de configuração",
+ "dynamic_dhcp_leases_not_found": "Concessões DHCP dinâmicas não encontradas",
+ "edit_table_action": "Editar",
+ "enable": "Ativar",
+ "enabled_dhcp": "Servidor DHCP ativado",
+ "encrypted_dns_addresses": "Endereços de servidor DNS criptografados",
+ "encryption_certificate_has_issues": "O certificado apresenta problemas",
+ "encryption_certificate_path": "Caminho do certificado",
+ "encryption_certificates": "Certificados",
+ "encryption_certificates_desc": "Para usar a encriptação, você deve fornecer uma cadeia de certificados SSL válida para o seu domínio. Você pode obter um certificado SSL gratuito em letsencrypt.org ou comprar um de uma Autoridade Certificadora de confiança",
+ "encryption_certificates_input": "Copie/cole aqui o seu certificado codificado em PEM.",
+ "encryption_certificates_source_content": "Colar o conteúdo dos certificados",
+ "encryption_certificates_source_path": "Definir um caminho do ficheiro de certificados",
+ "encryption_chain_invalid": "A cadeia de certificado é inválida",
+ "encryption_chain_valid": "Cadeia de certificado válida",
+ "encryption_config_saved": "Definição de criptografia guardada",
+ "encryption_confirm_clear": "Redefinir definições de encriptação",
+ "encryption_confirm_clear_desc": "Todas as definições de encriptação serão repostas para os seus valores predefinidos",
+ "encryption_disable_plain_dns": "Desativar DNS simples?",
+ "encryption_disable_plain_dns_desc": "Dispositivos não compatíveis com DNS encriptação podem não funcionar corretamente",
+ "encryption_doq": "Porta DNS-sobre-QUIC",
+ "encryption_doq_tooltip": "Esta porta é usada para executar um servidor DNS-sobre-QUIC",
+ "encryption_dot": "Porta DNS-sobre-TLS",
+ "encryption_dot_tooltip": "Esta porta é usada para executar um servidor DNS-sobre-TLS",
+ "encryption_encrypted_dns": "DNS encriptado",
+ "encryption_encrypted_dns_desc": "A interface administrativa do AdGuard Home utiliza HTTPS. O servidor DNS é compatível com DNS-sobre-HTTPS, DNS-sobre-TLS e DNS-sobre-QUIC",
+ "encryption_expire": "Expirado em: %value%",
+ "encryption_force_redirect": "Redirecionar automaticamente de HTTP para HTTPS",
+ "encryption_hostnames": "Nomes de anfitrião: %value%",
+ "encryption_https": "Porta HTTPS",
+ "encryption_https_tooltip": "Esta porta fornece acesso HTTPS para a interface de administração do AdGuard Home. Ela também fornece DNS-sobre-HTTPS no local '/dns-query'",
+ "encryption_issuer": "Emissor: %value%",
+ "encryption_key": "Chave privada",
+ "encryption_key_input": "Copie/cole aqui a chave privada codificada em PEM para o seu certificado.",
+ "encryption_key_invalid": "A chave privada não é válida",
+ "encryption_key_source_content": "Colar o conteúdo da chave privada",
+ "encryption_key_source_path": "Definir um ficheiro de chave privada",
+ "encryption_key_type": "Algoritmo de encriptação: %value%",
+ "encryption_key_valid": "A chave privada é válida",
+ "encryption_plain_dns": "DNS simples",
+ "encryption_plain_dns_error": "Para desabilitar o DNS simples, habilite pelo menos um protocolo DNS criptografado",
+ "encryption_private_key_path": "Caminho da chave privada",
+ "encryption_server": "Nome do servidor",
+ "encryption_server_enter": "Insira o seu nome de domínio",
+ "encryption_server_tooltip_1": "Se definido, AdGuard Home detetará ClientIDs, responderá a inquéritos DDR e executará validações de conexão adicionais. Se não estiver definido, esses recursos estão desativados",
+ "encryption_server_tooltip_2": "Deve corresponder a um nome DNS no certificado",
+ "encryption_subject": "Assunto: %value%",
+ "encryption_title": "Encriptação",
+ "enter_cache_size": "Digite o tamanho do cache (bytes)",
+ "enter_ip_address_placeholder": "Digite o endereço de IP",
+ "error": "Erro",
+ "fallback_dns": "Servidores DNS de fallback",
+ "filter_added_successfully": "Adicionado à lista de bloqueados: %value% ",
+ "filter_added_successfully_allowlist": "Lista de permissões adicionada: %value% ",
+ "filter_added_successfully_more": "Listas de bloqueio adicionadas: %value% e %more% entre outras",
+ "filter_category_general": "Geral",
+ "filter_category_general_desc": "Listas que bloqueiam o monitorização e a publicidade na maioria dos dispositivos",
+ "filter_category_other": "Noutro",
+ "filter_category_other_desc": "Outras listas de bloqueio",
+ "filter_category_regional": "Regional",
+ "filter_category_regional_desc": "Listas focadas em anúncios regionais e servidores de monitorização",
+ "filter_category_security": "Segurança",
+ "filter_category_security_desc": "Listas projetadas especificamente em bloquear domínios maliciosos, de phishing e fraude",
+ "filter_removed_successfully": "Lista de bloqueios removida: %value% ",
+ "filter_removed_successfully_allowlist": "Lista de permissões removida: %value% ",
+ "filters": "Filtros",
+ "forgot_password": "Não se lembra da palavra-passe?",
+ "forgot_password_list_item_1": "Crie uma nova senha usando a ferramenta htpasswd ou qualquer gerador BCrypt online.",
+ "forgot_password_list_item_2": "Parar o AdGuard Home.",
+ "forgot_password_list_item_3": "Abra o ficheiro AdGuardHome.yaml, localize o campo da palavra-passe e substitua-o pelo novo valor.",
+ "forgot_password_list_item_4": "Inicie o AdGuard Home.",
+ "forgot_password_list_item_5": "Inicie sessão na interface web com a sua nova palavra-passe.",
+ "forgot_password_list_title": "Para alterar a sua palavra-passe:",
+ "forgot_password_page_desc": "O AdGuard Home armazena as palavras-passe como hashes encriptados com BCrypt",
+ "form_enter_blocked_response_ttl": "Insira o TTL da resposta bloqueada (segundos)",
+ "form_enter_hostname": "Introduza um nome de anfitrião",
+ "form_enter_ip": "Digite um endereço IP",
+ "form_enter_mac": "Digite um endereço MAC",
+ "form_error_answer_format": "Formato de resultado inválido",
+ "form_error_client_id_format": "O ID do cliente deve conter apenas números, letras minúsculas e hifens",
+ "form_error_domain_format": "Formato de domínio inválido",
+ "form_error_equal": "Não deve ser igual",
+ "form_error_format": "Formato inválido",
+ "form_error_gateway_ip": "A concessão não pode ter o endereço IP do gateway",
+ "form_error_hostname_format": "Utilize números, letras minúsculas do alfabeto ou hífens",
+ "form_error_ip4_format": "Endereço de IPv4 inválido",
+ "form_error_ip6_format": "Endereço de IPv6 inválido",
+ "form_error_ip_already_added": "Este endereço IP já foi adicionado",
+ "form_error_ip_format": "Endereço de email inválido",
+ "form_error_mac_already_added": "Este endereço MAC já foi adicionado",
+ "form_error_mac_format": "Endereço de MAC inválido",
+ "form_error_password": "As palavras-passe não coincidem",
+ "form_error_port": "Insira um número de porta válida",
+ "form_error_port_range": "Digite um numero de porta entre 80 e 65535",
+ "form_error_port_unsafe": "Porta não é segura",
+ "form_error_range": "O valor deve estar entre %min% e %max%",
+ "form_error_server_name": "Nome de servidor inválido",
+ "form_error_url_format": "Insira um URL ou caminho de ficheiro válido",
+ "found_in_known_domains": "Encontrado nos domínios conhecidos",
+ "friday": "Sexta-feira",
+ "gateway_or_subnet_invalid": "Máscara de sub-rede inválida",
+ "general_statistics": "Estatísticas gerais",
+ "home_dns_addresses": "Endereços DNS do AdGuard Home",
+ "home_dns_addresses_desc": "O servidor DNS do AdGuard Home está a escutar nas seguintes interfaces/portas:",
+ "ignore_domains_desc_log": "Os pedidos que correspondem a estas regras não são apresentados no registo de consultas",
+ "ignore_domains_desc_stats": "Os pedidos que correspondem a estas regras não são incluídos nas estatísticas",
+ "ignore_domains_title": "Domínios ignorados",
+ "inactivity_schedule": "Calendário de inatividade",
+ "inactivity_schedule_add": "Adicionar agendamento",
+ "inactivity_schedule_all_day": "O dia todo",
+ "inactivity_schedule_delete": "Eliminar a programação",
+ "inactivity_schedule_delete_desc": "O bloqueio do serviço estará ativo durante todo o dia em %value% ",
+ "inactivity_schedule_desc": "Defina períodos de inatividade para bloqueio de serviços",
+ "inactivity_schedule_edit": "Editar agendamento",
+ "inactivity_schedule_no_schedules": "Sem agendamento",
+ "inactivity_schedule_replace_desc": "Esta programação substituirá qualquer programação existente para o mesmo dia da semana. Cada dia pode ter apenas um período de inatividade",
+ "inactivity_schedule_time_from": "Horário de início",
+ "inactivity_schedule_time_to": "Horário do fim",
+ "inactivity_schedule_timezone": "Fuso horário",
+ "install_auth_confirm": "Confirmar palavra-passe",
+ "install_auth_password": "Palavra-passe",
+ "install_auth_password_enter": "Insira palavra-passe",
+ "install_auth_username": "Nome do utilizador",
+ "install_auth_username_enter": "Insira o nome de utilizador",
+ "install_saved": "Guardado com sucesso",
+ "install_settings_all_interfaces": "Todas as interfaces",
+ "install_settings_port": "Porta",
+ "install_static_configure": "O AdGuard Home detetou que está a ser utilizado o endereço IP dinâmico %ip%. Pretende defini-lo como o seu endereço estático?",
+ "install_static_error": "O AdGuard Home não pode configurar automaticamente para esta interface de rede. Por favor, procure uma instrução sobre como fazer isso manualmente.",
+ "install_static_ok": "Boas notícias! O endereço de IP estático já está configurado",
+ "install_step": "Passo",
+ "install_submit_title": "Parabéns!",
+ "instructions": "Instruções",
+ "invalid": "Inválido",
+ "ip_address": "Endereço de IP",
+ "ip_addresses_placeholder": "Introduza os endereços IP, um por linha",
+ "known_tracker": "Rastreador conhecido",
+ "language": "Língua",
+ "last_days": "| Último %count% dia | Últimos %count% dias",
+ "last_hour": "Última hora",
+ "last_hours": "| Última %count% hora | Últimas %count% horas",
+ "last_updated_label": "Ultima atualização",
+ "light_theme": "Tema claro",
+ "list_updated": "| %count% lista atualizada | %count% listas atualizadas",
+ "login": "Iniciar sessão",
+ "logout": "Encerrar sessão",
+ "make_static": "Tornar estático",
+ "make_static_desc": "Esta ação converte um endereço dinâmico num endereço estático",
+ "milliseconds_abbreviation": "ms",
+ "monday": "Segunda-feira",
+ "name": "Nome",
+ "name_label": "Nome",
+ "name_table_header": "Nome",
+ "name_tooltip": "Nome",
+ "network_interface": "Interface de rede",
+ "next": "Seguinte",
+ "no": "Não",
+ "no_stats_yet": "Ainda não há estatísticas — continue a usar o AdGuard Home para visualizá-las",
+ "none_text": "Nenhuma",
+ "nothing_found": "Nada encontrado",
+ "notify_undo": "Anular",
+ "notify_updated": "Atualizado",
+ "open_dashboard": "Abrir painel",
+ "parental_control": "Controlo parental",
+ "parental_group_ai": "IA",
+ "parental_group_cdn": "CDN",
+ "parental_group_dating": "Namoro",
+ "parental_group_development": "Desenvolvimento de software",
+ "parental_group_gambling": "Jogos de azar, apostas",
+ "parental_group_gaming": "Jogos",
+ "parental_group_hosting": "Alojamento web",
+ "parental_group_messaging": "Mensagens",
+ "parental_group_messenger": "Mensagens",
+ "parental_group_privacy": "Ferramentas de privacidade",
+ "parental_group_shopping": "Compras",
+ "parental_group_social": "Redes sociais",
+ "parental_group_social_network": "Redes sociais",
+ "parental_group_software": "Desenvolvimento de software",
+ "parental_group_streaming": "Streaming",
+ "password_label": "Palavra-passe",
+ "password_placeholder": "Insira palavra-passe",
+ "password_requirements": "Requisitos de palavra-passe",
+ "password_requirements_characters": "Mínimo de 8 caracteres",
+ "password_requirements_lowercase": "Pelo menos 1 letra minúscula",
+ "password_requirements_match": "As palavras-passe coincidem",
+ "password_requirements_special": "Letras inglesas, números e caracteres especiais",
+ "password_requirements_uppercase": "Pelo menos 1 letra maiúscula",
+ "pause_for_hour": "Pausar por %count% hora",
+ "pause_for_minutes": "| Pausa de %count% minuto | Pausa de %count% minutos",
+ "pause_for_seconds": "| Pausa durante %count% segundo | Pausa durante %count% segundos",
+ "pause_until_tomorrow": "Pausar até amanhã, %time%",
+ "period_notify": "Para alterar os períodos exibidos, edite Retenção de estatísticas em Configurações gerais ",
+ "plain_dns": "DNS simples",
+ "plain_dns_addresses": "Endereços de servidor DNS simples",
+ "port_53_faq_link": "A porta 53 é frequentemente ocupada por serviços \"DNSStubListener\" ou \"systemd-resolved\". Por favor leia essa instrução para resolver isso.",
+ "port_in_use": "Essa porta já está em uso",
+ "privacy_policy": "Política de privacidade",
+ "processing_time_ms": "%value% ms",
+ "protection": "Proteção",
+ "protocol": "Protocolo",
+ "queries": "Consultas",
+ "queries_total": "| %count% consulta total | %count% consultas totais",
+ "query_details": "Detalhes da consulta",
+ "query_log": "Registo de consultas",
+ "query_log_all_reasons": "Todos os motivos",
+ "query_log_all_statuses": "Todos os estados",
+ "query_log_allowed": "Na lista branca",
+ "query_log_blocked": "Bloqueado",
+ "query_log_blocked_by_filter": "Bloqueado pelo filtro",
+ "query_log_blocked_by_parental_control": "Bloqueado pelo controlo parental",
+ "query_log_blocked_services": "Serviços bloqueados",
+ "query_log_blocked_threats": "Ameaças bloqueadas",
+ "query_log_custom_filtering_rules": "Regras de filtragem personalizadas",
+ "query_log_detail_address": "Eendereço: %value% ",
+ "query_log_detail_category": "Categoria: %value% ",
+ "query_log_detail_country": "País: %value% ",
+ "query_log_detail_date": "Data: %value% ",
+ "query_log_detail_dns_server": "Servidor DNS: %value% ",
+ "query_log_detail_domain": "Domínio: %value% ",
+ "query_log_detail_ecs": "ECS: %value% ",
+ "query_log_detail_elapsed": "Tempo decorrido: %value% ",
+ "query_log_detail_name": "Nome: %value% ",
+ "query_log_detail_network": "Rede: %value% ",
+ "query_log_detail_original_response": "Resposta original:",
+ "query_log_detail_protocol": "Protocolo: %value% ",
+ "query_log_detail_reason": "Motivo:",
+ "query_log_detail_response": "Resposta:",
+ "query_log_detail_response_code": "Código de resposta: %value% ",
+ "query_log_detail_rules": "Regra(s):",
+ "query_log_detail_served_from_cache": "Servido do cache: %value% ",
+ "query_log_detail_service_name": "Nome do serviço: %value% ",
+ "query_log_detail_source": "Fonte: %value% ",
+ "query_log_detail_status": "Estado: %value% ",
+ "query_log_detail_time": "Tempo: %value% ",
+ "query_log_detail_type": "Tipo: %value% ",
+ "query_log_nothing_available": "Nada encontrado",
+ "query_log_processed": "Processado",
+ "query_log_retention": "Rotação de registo",
+ "query_log_rewritten": "Reescrito",
+ "query_log_safe_search": "Pesquisa segura",
+ "query_log_strict_search": "Usar aspas duplas para uma pesquisa rigorosa",
+ "rate_limit_subnet_len_ipv4_error": "O comprimento do prefixo da sub-rede IPv4 deve estar entre 0 e 32",
+ "rate_limit_subnet_len_ipv6_error": "O comprimento do prefixo da sub-rede IPv6 deve situar-se entre 0 e 128",
+ "reason_table_header": "Motivo",
+ "refresh_btn": "Atualizar",
+ "refresh_statics": "Atualizar estatísticas",
+ "release_notes": "Notas de lançamento",
+ "remove": "Remover",
+ "remove_tag": "Remover %value%",
+ "report_an_issue": "Informar um problema",
+ "request_table_header": "Solicitação",
+ "requests_table_header": "Solicitações",
+ "reset": "Repor",
+ "reset_settings": "Repor definições",
+ "reset_settings_confirm": "Sim, repor",
+ "response_details": "Detalhes da resposta",
+ "response_time": "Tempo de resposta",
+ "result": "Resultado",
+ "resume_protection_timer": "A proteção será retomada em %time%",
+ "rewrite_add": "Adicionar reescrita de DNS",
+ "rewrite_domain_input_placeholder": "Insira o nome de domínio ou wildcard",
+ "rewrite_edit": "Editar reedição de DNS",
+ "rewrite_hosts_applied": "Reescrito pela regra do ficheiro de hosts",
+ "rewrite_settings_updated": "Definições de reescrita de DNS actualizadas com sucesso",
+ "rewrite_updated": "Reedição de DNS atualizada com sucesso",
+ "rewrites_answer_input_placeholder": "Digite o endereço IP, registro CNAME, A ou AAAA",
+ "rewrites_empty": "Ainda não há reescritas, adicione a primeira ",
+ "rewrites_remove_desc": "A reescrita de DNS para %value% será removida",
+ "rewrites_remove_title": "Remover reescrita de DNS?",
+ "rewrites_tooltip_examples_item1": "Reescrever respostas DNS apenas para este nome de domínio",
+ "rewrites_tooltip_examples_item2": "Reescrever respostas DNS para todos os subdomínios de example.org",
+ "rewrites_tooltip_instructions_item1": "Para resolver o nome de domínio num endereço IP, introduza um endereço IPv4 para os registos A e um endereço IPv6 para os registos AAAA",
+ "rewrites_tooltip_instructions_item2": "Para redirecionar o nome de domínio, introduza o registo CNAME do domínio de destino",
+ "rewrites_tooltip_instructions_item3": "Para manter apenas os registos A, introduza",
+ "rewrites_tooltip_instructions_item4": "Para manter apenas os registos AАAA, introduza",
+ "rewritten": "Reescrito",
+ "rows_per_page": "Linhas por página %value%",
+ "rules_label": "Regras",
+ "safe_browsing": "Navegação segura",
+ "safe_search": "Pesquisa segura",
+ "safe_search_used": "Pesquisa segura utilizada",
+ "safe_search_used_tooltip": "O número de consultas DNS a motores de busca em que a Pesquisa Segura foi aplicada",
+ "saturday": "Sábado",
+ "save": "Guardar",
+ "save_btn": "Guardar",
+ "search_placeholder": "Pesquisar",
+ "selected": "%value% selecionado",
+ "server_config_blocking_mode": "Modo de bloqueio",
+ "server_config_blocking_mode_custom_ip_desc": "Endereço IP personalizado: Responder com um endereço IP configurado manualmente",
+ "server_config_blocking_mode_default_desc": "Predefinição: Responder com 0.0.0.0 para A e :: para AAAA se bloqueado por uma regra do tipo bloqueio de anúncios. Responder com o endereço IP especificado na regra se bloqueado por uma regra do tipo /etc/hosts",
+ "server_config_blocking_mode_ipv4": "A bloquear IPv4",
+ "server_config_blocking_mode_ipv4_faq": "Endereço IP a devolver para um pedido A bloqueado",
+ "server_config_blocking_mode_ipv4_placeholder": "Digite o endereço de IPv4",
+ "server_config_blocking_mode_ipv6": "A bloquear IPv6",
+ "server_config_blocking_mode_ipv6_faq": "Endereço IP a devolver em caso de um pedido AAAA bloqueado",
+ "server_config_blocking_mode_ipv6_placeholder": "Digite o endereço de IPv6",
+ "server_config_blocking_mode_null_ip_desc": "Endereço IP nulo: Responder com 0.0.0.0 para A e :: para AAAA",
+ "server_config_blocking_mode_nxdomain_desc": "NXDOMAIN: responder com o código NXDOMAIN",
+ "server_config_blocking_mode_refused_desc": "REFUSED: responder com o código REFUSED",
+ "server_config_blocking_mode_ttl": "TTL da resposta bloqueada, em segundos",
+ "server_config_blocking_mode_ttl_faq": "Especifica durante quanto tempo os clientes devem manter em cache uma resposta filtrada",
+ "server_config_custom_ip": "Endereço IP personalizado",
+ "server_config_default": "Predefinido",
+ "server_config_disable_ipv6": "Desativar resolução de endereços IPv6",
+ "server_config_disable_ipv6_desc": "Ignorar todas as consultas DNS para endereços IPv6 (tipo AAAA) e remover as sugestões IPv6 das respostas HTTPS",
+ "server_config_dnssec_enable": "Ativar DNSSEC",
+ "server_config_dnssec_enable_desc": "Defina o sinalizador DNSSEC nas consultas DNS de saída e verifique o resultado. Requer um resolvedor compatível com DNSSEC",
+ "server_config_edns_cs_desc": "Adicionar a opção EDNS Client Subnet (ECS) aos pedidos de upstream e registar os valores enviados pelos clientes no registo de consultas",
+ "server_config_edns_enable": "Ativar a sub-rede do cliente EDNS",
+ "server_config_edns_use_custom_ip": "Utilizar um endereço IP personalizado para o EDNS",
+ "server_config_edns_use_custom_ip_desc": "Substitua o endereço IP do cliente pelo endereço IP personalizado abaixo",
+ "server_config_null_ip": "Endereço IP nulo",
+ "server_config_nxdomain": "NXDOMAIN",
+ "server_config_rate_limit": "Limite de taxa, RPS",
+ "server_config_rate_limit_faq": "O número de pedidos por segundo permitido por cliente. Um valor de 0 significa que não há limite",
+ "server_config_rate_limit_placeholder": "Insira o limite de velocidade",
+ "server_config_rate_limit_whitelist": "Lista de permissões de limitação de taxa",
+ "server_config_rate_limit_whitelist_faq": "Endereços IP excluídos da limitação de velocidade",
+ "server_config_refused": "REFUSED",
+ "server_config_subnet_len_ipv4": "Comprimento do prefixo de sub-rede para endereços IPv4",
+ "server_config_subnet_len_ipv4_faq": "Comprimento do prefixo de sub-rede para endereços IPv4 usados para limitação de velocidade. O padrão é 24",
+ "server_config_subnet_len_ipv6": "Comprimento do prefixo de sub-rede para endereços IPv6",
+ "server_config_subnet_len_ipv6_faq": "Comprimento do prefixo de sub-rede para endereços IPv6 usados para limitação de velocidade. O padrão é 56",
+ "server_config_subnet_len_placeholder": "Introduza o comprimento do prefixo",
+ "set_static_ip": "Definir um endereço de IP estático",
+ "settings": "Definições",
+ "settings_anonymize_client_ip": "Anonimizar os endereços IP dos clientes",
+ "settings_anonymize_client_ip_desc": "Substituir parte do endereço IP por zeros",
+ "settings_browsing_security": "Segurança da Navegação",
+ "settings_browsing_security_desc": "Bloquear o acesso aos domínios encontrados na base de dados de domínios maliciosos e de phishing",
+ "settings_confirm_clear_query_log": "Limpar o registo de consultas?",
+ "settings_confirm_clear_query_log_desc": "Isto irá eliminar todos os pedidos do registo de consultas . As estatísticas continuarão disponíveis",
+ "settings_confirm_clear_statistics": "Limpar estatísticas?",
+ "settings_confirm_clear_statistics_desc": "Isto irá apagar todas as estatísticas. O registo de consultas continuará disponível",
+ "settings_confirm_decrease_log_rotation_interval": "Reduzir o intervalo de rotação das toras?",
+ "settings_confirm_decrease_log_rotation_interval_desc": "Isto irá eliminar todos os registos com data anterior a %value%",
+ "settings_confirm_decrease_stats_rotation_interval": "Reduzir o intervalo de retenção das estatísticas?",
+ "settings_confirm_decrease_stats_rotation_interval_desc": "Isto irá eliminar todas as estatísticas anteriores a %value%",
+ "settings_custom": "Personalizado",
+ "settings_days": "| %count% dia | %count% dias",
+ "settings_domain_names": "Nomes de domínio, um por linha",
+ "settings_filter_requests": "Solicitações de filtro",
+ "settings_filter_requests_desc": "Pode utilizar listas de bloqueio , listas de autorização e regras de utilizador para configurar regras de filtragem",
+ "settings_filtering_and_security": "Filtragem e segurança",
+ "settings_general_short": "Geral",
+ "settings_global": "Global",
+ "settings_hours": "| %count% hora | %count% horas",
+ "settings_log_dns_requests": "Registros de solicitação de DNS",
+ "settings_log_rotation_hours": "Rotação do registo, em horas",
+ "settings_notify_changes_saved": "Alterações guardadas",
+ "settings_notify_query_log_cleared": "Registo de consultas apagado",
+ "settings_notify_statistics_cleared": "Estatísticas apagadas",
+ "settings_parental_control": "Controlo parental",
+ "settings_parental_control_desc": "Bloquear sites com conteúdos inadequados para crianças",
+ "settings_rotation_placeholder": "Utilize números",
+ "settings_rule_syntax": "Sintaxe de regras",
+ "settings_safe_search": "Pesquisa segura",
+ "settings_safe_search_desc": "Bloquear conteúdos inadequados nos resultados de pesquisa",
+ "settings_statistics": "Estatísticas",
+ "settings_statistics_clear": "Limpar estatísticas",
+ "settings_statistics_desc": "Apresentar as estatísticas de pedidos e de clientes no painel de controlo",
+ "settings_statistics_retention": "Retenção de estatísticas",
+ "settings_statistics_retention_hours": "Período de retenção das estatísticas, em horas",
+ "settings_tooltip_domain_names": "Nomes de domínio, caracteres wildcard ou regras de filtragem",
+ "settings_tooltip_examples": "Exemplos:",
+ "settings_yes_clear": "Sim, limpar",
+ "settings_yes_decrease": "Sim, diminuir",
+ "setup_complete": "A configuração foi concluída e o AdGuard Home está pronto para uso",
+ "setup_devices_android_list_1": "Abra a aplicação «Definições » do seu dispositivo e selecione «Wi-Fi ».",
+ "setup_devices_android_list_2": "Mantenha premido o nome da sua rede atual e, em seguida, selecione «Modificar rede ». Não é possível definir um DNS personalizado para ligações à Internet móvel.",
+ "setup_devices_android_list_3": "Em alguns dispositivos, pode ser necessário selecionar a caixa de seleção Avançado para ver mais configurações. Para personalizar as configurações de DNS do Android, é necessário alterar as configurações de IP de DHCP para Estático .",
+ "setup_devices_android_list_4": "Altere os valores de DNS 1 e DNS 2 para os endereços do seu servidor AdGuard Home.",
+ "setup_devices_android_title": "Android",
+ "setup_devices_dns_android_list_1": "O Android 9 suporta nativamente o DNS-over-TLS . Para o configurar, aceda a Definições → Rede e Internet → Avançado → DNS privado e introduza o seu nome de domínio.",
+ "setup_devices_dns_android_list_2": "O AdGuard para Android é compatível com DNS sobre HTTPS , DNS sobre TLS e configuração de DNS-over-QUIC .",
+ "setup_devices_dns_android_list_3": "Intra adiciona o suporte DNS-sobre-HTTPS para o Android.",
+ "setup_devices_dns_ios_list_1": "O AdGuard para iOS suporta a configuração de DNS sobre HTTPS , DNS sobre TLS e DNS sobre QUIC .",
+ "setup_devices_dns_ios_list_2": "O DNSCloak suporta DNS-over-HTTPS , mas para o configurar para utilizar o seu próprio servidor, terá de gerar um DNS Stamp para o mesmo.",
+ "setup_devices_dns_list_1": "Para DNS-sobre-TLS , use ",
+ "setup_devices_dns_list_2": "Para DNS-sobre-HTTPS , use ",
+ "setup_devices_dns_list_3": "Para DNS-sobre-QUIC , use ",
+ "setup_devices_dns_list_devices": "Aqui está uma lista de softwares que você pode usar.",
+ "setup_devices_dns_macos_desc": "Num dispositivo com iOS 14 ou macOS Big Sur, pode descarregar um ficheiro mobileconfig especial que adiciona servidores DNS-over-HTTPS ou DNS-over-TLS às definições de DNS.",
+ "setup_devices_dns_other_list_1": "O próprio AdGuard Home pode ser usado como um cliente DNS seguro em qualquer plataforma.",
+ "setup_devices_dns_other_list_2": "dnsproxy suporta todos os protocolos de DNS seguros conhecidos.",
+ "setup_devices_dns_other_list_3": "dnscrypt-proxy suporta DNS-sobre-HTTPS .",
+ "setup_devices_dns_other_list_4": "Mozilla Firefox suporta DNS-sobre-HTTPS .",
+ "setup_devices_dns_other_list_5": "Para mais implementações, consulte DNSCrypt e DNS privacy .",
+ "setup_devices_ios_list_1": "Abra o app Definições do seu dispositivo e selecione Wi-Fi . Não é possível definir DNS personalizado para conexões de Internet móvel.",
+ "setup_devices_ios_list_2": "Toque no nome da rede atualmente ativa.",
+ "setup_devices_ios_list_3": "Vá para DNS → Configurar DNS → Manual e adicione os endereços do seu servidor AdGuard Home.",
+ "setup_devices_ios_list_4": "Toque em Guardar .",
+ "setup_devices_ios_title": "iOS",
+ "setup_devices_macos_list_1": "Clique no menu Apple → Preferências do Sistema → Rede .",
+ "setup_devices_macos_list_2": "Selecione a ligação pretendida e clique em «Avançado ».",
+ "setup_devices_macos_list_3": "Selecione a aba DNS e insira os endereços do seu servidor AdGuard Home.",
+ "setup_devices_macos_title": "macOS",
+ "setup_devices_router_desc_1": "Configure o AdGuard Home no router da sua rede doméstica e proteja todos os dispositivos ligados ao mesmo. Não é necessário configurar manualmente cada dispositivo.",
+ "setup_devices_router_desc_2": "Alguns roteadores não permitem configurar um servidor DNS personalizado. Nesse caso, você pode tentar configurar o AdGuard Home como um servidor DHCP . Se isso não funcionar, consulte o manual do seu roteador para obter instruções sobre como personalizar os servidores DNS para o seu roteador específico.",
+ "setup_devices_router_list_1": "Normalmente, pode aceder-lhes a partir do seu navegador através de um URL, como http://192.168.0.1 ou http://192.168.1.1 . Poderá ser-lhe pedido que introduza uma palavra-passe. Muitas vezes, é possível redefinir a palavra-passe premindo um botão no próprio router, mas tenha em atenção que poderá perder toda a configuração do router. Se o seu router tiver de ser configurado através de uma aplicação, instale a aplicação no seu telemóvel ou computador e utilize-a para aceder às definições do router.",
+ "setup_devices_router_list_1_title": "Abra as definições do seu router.",
+ "setup_devices_router_list_2": "Para encontrar as configurações, procure por DNS ao lado de um campo que permite dois ou três conjuntos de números, cada um dividido em quatro grupos de um a três dígitos.",
+ "setup_devices_router_list_2_title": "Nas configurações de DHCP/DNS, insira os endereços do seu servidor AdGuard Home.",
+ "setup_devices_router_title": "Router",
+ "setup_devices_windows_list_1": "Abra o Painel de Controlo no menu Iniciar ou na Pesquisa do Windows .",
+ "setup_devices_windows_list_2": "Clique em Rede e Internet → Central de Rede e Compartilhamento → Alterar configurações do adaptador .",
+ "setup_devices_windows_list_3": "Clique com o botão direito na sua conexão ativa e selecione Propriedades .",
+ "setup_devices_windows_list_4": "Na lista, selecione «Protocolo de Internet Versão 4 (TCP/IPv4) » para IPv4 ou «Protocolo de Internet Versão 6 (TCP/IPv6) » para IPv6.",
+ "setup_devices_windows_list_5": "Clique em «Propriedades » → Selecione Utilizar os seguintes endereços de servidor DNS ».",
+ "setup_devices_windows_list_6": "Introduza os endereços do seu servidor AdGuard Home.",
+ "setup_devices_windows_title": "Windows",
+ "setup_dns_desc": "Configure o seu router ou outros dispositivos para utilizarem os seguintes endereços de servidores DNS:",
+ "setup_dns_notice_new": "Para utilizar o DNS-over-HTTPS ou o DNS-over-TLS , é necessário configurar a encriptação nas definições do AdGuard Home.",
+ "setup_dns_privacy_ioc_mac": "configuração para iOS e macOS",
+ "setup_dns_privacy_other_title": "Outras implementações",
+ "setup_dns_quote_desc": "Para funcionar corretamente, o servidor DNS AdGuard Home requer um endereço IP estático na sua rede local. Caso contrário, o seu roteador pode eventualmente atribuir-lhe um endereço IP diferente e interromper a proteção DNS",
+ "setup_dns_quote_title": "Atribua um endereço IP estático ao servidor DNS do AdGuard Home na sua rede local",
+ "setup_dns_title": "Definir o endereço do servidor DNS do AdGuard Home",
+ "setup_dns_title_banner": "Configurações do servidor DNS",
+ "setup_guide": "Guia de instalação",
+ "setup_guide_auth_privacy": "Aceito a Política de Privacidade e os Termos e Condições dos sites da AdGuard",
+ "setup_guide_auth_subtitle": "Isto é necessário para proteger o painel de administração do AdGuard Home contra acesso não autorizado",
+ "setup_guide_auth_title": "Configurar proteção por palavra-passe",
+ "setup_guide_desc": "Configure os seus dispositivos para utilizarem o AdGuard Home",
+ "setup_guide_device_type": "Selecione o seu tipo de dispositivo",
+ "setup_guide_greeting_button": "Executar assistente",
+ "setup_guide_greeting_desc": "Este assistente irá ajudá-lo a configurar o AdGuard Home:",
+ "setup_guide_greeting_list_1": "Crie credenciais para aceder ao painel de administração",
+ "setup_guide_greeting_list_2": "Defina o endereço da interface web para o AdGuard Home",
+ "setup_guide_greeting_list_3": "Defina o endereço do servidor DNS do AdGuard Home",
+ "setup_guide_greeting_list_4": "Obtenha instruções para cada um dos seus dispositivos",
+ "setup_guide_greeting_list_5": "Obter acesso ao painel do AdGuard Home ",
+ "setup_guide_greeting_title": "Configurar o AdGuard Home",
+ "setup_guide_title": "Configure os seus dispositivos para utilizarem o AdGuard Home",
+ "setup_ui_desc": "Use estes endereços para acessar a interface web do AdGuard Home. Se necessário, altere a porta",
+ "setup_ui_title": "Definir o endereço da interface web do AdGuard Home",
+ "setup_ui_title_banner": "Definições da interface web",
+ "show_blocked_responses": "Bloqueado",
+ "show_more_count": "+ %count% a mais",
+ "show_processed_responses": "Processado",
+ "sort_asc": "Ordenar de A a Z",
+ "sort_desc": "Ordenar de Z a A",
+ "source_label": "Fonte",
+ "source_tooltip": "Fonte",
+ "static_dhcp_leases_not_found": "Não foram encontradas concessões DHCP estáticas",
+ "stats_adult": "Sítios adultos bloqueados",
+ "stats_query_domain": "Principais domínios consultados",
+ "status_table_header": "Estado",
+ "sunday": "Domingo",
+ "system_host_files": "Arquivos hosts do sistema",
+ "system_theme": "Tema do sistema",
+ "table_client": "Cliente",
+ "tags_title": "Etiquetas",
+ "test_upstreams": "Testar DNS primário",
+ "threats_blocked": "Ameaças bloqueadas",
+ "threats_blocked_tooltip": "O número de consultas DNS bloqueadas pelo módulo de segurança de navegação do AdGuard",
+ "thursday": "Quinta-feira",
+ "time_table_header": "Data",
+ "top_blocked_domains": "Principais domínios bloqueados",
+ "top_clients": "Principais clientes",
+ "top_upstreams": "Melhores servidores DNS primários",
+ "total": "Total",
+ "try_again": "Tente novamente",
+ "tuesday": "Terça-feira",
+ "type_value": "Tipo: %value%",
+ "unavailable_dhcp": "DHCP não está disponível",
+ "unavailable_dhcp_desc": "O AdGuard Home não pode executar um servidor DHCP em seu sistema operacional",
+ "unblock": "Desbloquear",
+ "unblock_client": "Desbloquear cliente",
+ "unknown_filter": "Filtro desconhecido %filterId%",
+ "update_button": "Atualizar",
+ "update_filters_custom_hours": "Intervalo personalizado, em horas",
+ "update_filters_disable": "Desativar",
+ "update_filters_interval_custom": "Personalizado",
+ "update_filters_interval_daily": "Diariamente",
+ "update_filters_interval_hourly": "Cada %s hora",
+ "update_filters_interval_weekly": "Toda semana",
+ "update_filters_title": "Atualizar automaticamente os filtros",
+ "updated_upstream_dns_toast": "Servidores upstream guardados",
+ "updates_checked": "Uma nova versão do AdGuard Home está disponível\n",
+ "updates_version_equal": "O AdGuard Home está atualizado",
+ "upstream": "Servidor DNS primário",
+ "upstream_bootstrap_dns_faq": "Endereços IP dos servidores DNS utilizados para resolver os endereços IP dos resolvedores a montante DoH/DoT. Não são permitidos comentários",
+ "upstream_bootstrap_dns_title": "Servidores DNS de arranque, um por linha",
+ "upstream_dns": "Servidores DNS primário",
+ "upstream_dns_addresses": "Endereços de servidor, um por linha",
+ "upstream_dns_addresses_faq": "Saiba mais sobre como configurar servidores DNS a montante. Aqui está umalista de fornecedores de DNS conhecidos à sua escolha.",
+ "upstream_dns_configured_in_file": "Configurado em %path%",
+ "upstream_dns_fastest_addr": "Endereço de IP mais rápido",
+ "upstream_dns_parallel_requests": "Solicitações paralelas",
+ "upstream_dns_placeholder": "Endereços IP, sdns://, quic://, https://, h3:// ou tls://",
+ "upstream_dns_servers_title": "Servidores DNS primário",
+ "upstream_enable_reverse_lookup_desc": "Efetuar a resolução inversa dos endereços IP dos clientes para os respetivos nomes de anfitrião, enviando consultas PTR aos resolvedores adequados: servidores DNS privados para clientes locais ou servidores a montante para clientes com endereços IP públicos",
+ "upstream_enable_reverse_lookup_title": "Ativar a pesquisa inversa de endereços IP de clientes",
+ "upstream_example_multiple_upstreams_reserved": "%value%: múltiplos upstreams para domínios específicos ",
+ "upstream_example_tcp": "%ipv4%,%ipv6%: DNS padrão (sobre TCP)",
+ "upstream_example_tcp_hostname": "%value%: DNS padrão (sobre TCP, nome do host)",
+ "upstream_example_tcp_port": "%ipv4%,%ipv6%: DNS padrão (via TCP, com porta)",
+ "upstream_example_udp": "%ipv4%,%ipv6%: DNS comum (sobre UDP)",
+ "upstream_example_udp_hostname": "%value%: DNS comum (via UDP, nome do host)",
+ "upstream_example_udp_port": "%ipv4%,%ipv6%: DNS comum (via UDP, com porta)",
+ "upstream_example_upstream_comment": "%value%: um comentário.",
+ "upstream_example_upstream_doh": "%value%: DNS-over-HTTPS criptografado",
+ "upstream_example_upstream_doh3": "%value%: DNS sobre HTTPS criptografado com HTTP/3 forçado e sem fallback para HTTP/2",
+ "upstream_example_upstream_doq": "%value%: DNS-over-QUIC criptografado",
+ "upstream_example_upstream_dot": "%value%: DNS-over-TLS criptografado",
+ "upstream_example_upstream_reserved": "%value%: um servidor upstream para domínios específicos ",
+ "upstream_example_upstream_sdns": "%value%: DNS Stamps para resolvedores DNSCrypt ou DNS-over-HTTPS ",
+ "upstream_examples_title": "Exemplos",
+ "upstream_fallback_title": "Servidores DNS de fallback, um por linha",
+ "upstream_fallback_title_faq": "Servidores DNS de reserva a utilizar quando os servidores DNS principais não estiverem disponíveis. Utilize a mesma sintaxe que para os servidores DNS principais",
+ "upstream_private_ptr_resolvers_desc": "Resolva pedidos PTR, SOA e NS para domínios ARPA que contenham endereços IP privados através de servidores upstream privados, DHCP, /etc/hosts, etc. Se esta opção estiver desativada, o AdGuard Home responderá a todos esses pedidos com NXDOMAIN",
+ "upstream_private_ptr_resolvers_title": "Usar resolvedores DNS reversos privados",
+ "upstream_ptr": "Servidores DNS reversos privados, um por linha",
+ "upstream_ptr_faq_1": "O AdGuard Home utiliza estes servidores DNS para pedidos PTR, SOA e NS privados. Os pedidos privados provêm de clientes com um endereço IP privado e solicitam um domínio ARPA que contenha uma sub-rede de um intervalo de endereços IP privados, tal como %value%",
+ "upstream_ptr_faq_2": "Se este campo estiver vazio, serão utilizados os resolvedores DNS predefinidos do seu sistema operativo, com exceção dos endereços IP do AdGuard Home",
+ "upstream_ptr_faq_3": "Por predefinição, o AdGuard Home utiliza os seguintes resolvedores de DNS inverso: %value_1% e %value_2%",
+ "upstream_timeout": "Tempo limite de upstream, em segundos",
+ "upstream_timeout_faq": "Tempo de espera pela resposta do servidor upstream",
+ "upstream_timeout_placeholder": "Introduza a duração do tempo limite do servidor upstream",
+ "upstreams": "DNS primário",
+ "url_label": "URL",
+ "use_saved_key": "Utilizar uma chave privada existente",
+ "user_rules": "Regras do utilizador",
+ "user_rules_add_to_allowlist": "Adicionar na lista de permissões",
+ "user_rules_allow_service": "Permitir serviço",
+ "user_rules_browsing_security_disabled": "Segurança da navegação desativada",
+ "user_rules_check_button": "Verificar",
+ "user_rules_check_client_label": "Identificador do cliente (nome, ClientID ou endereço IP)",
+ "user_rules_check_client_placeholder": "Insira o identificador do cliente",
+ "user_rules_check_dns_record_type_label": "Tipo de registro DNS",
+ "user_rules_check_hostname_label": "Nome do hospedeiro ou nome de domínio",
+ "user_rules_check_hostname_placeholder": "Insira o destino",
+ "user_rules_check_title": "Verificar a filtragem de domínio globalmente ou para um cliente",
+ "user_rules_cname": "CNAME: %cname%",
+ "user_rules_disable_browsing_security": "Desativar segurança da navegação",
+ "user_rules_disable_filter": "Desativar filtro",
+ "user_rules_disable_parental_control": "Desativar controlo parental",
+ "user_rules_disable_safe_search": "Desativar pesquisa segura",
+ "user_rules_dns_record_type_placeholder": "Selecione o tipo de registro DNS",
+ "user_rules_dns_rewrite_removed": "Regra removida da reescrita de DNS",
+ "user_rules_domain": "Domínio: %value%",
+ "user_rules_domain_blocked": "O domínio está bloquado",
+ "user_rules_domain_is_allowed": "O domínio é permitido",
+ "user_rules_domain_is_processed": "O domínio está processado",
+ "user_rules_edit_dns_rewrite": "Editar reedição de DNS",
+ "user_rules_example_block": "||%domain% Bloquear o acesso a %domain% e a todos os seus subdomínios",
+ "user_rules_example_comment": "! Isto é um comentário Para comentar, inicie a linha com ! ou #",
+ "user_rules_example_comment_2": "# Isto também é um comentário Para comentar, comece a linha com ! ou #",
+ "user_rules_example_regex": "/REGEX/ Bloquear o acesso a domínios que correspondem à expressão regular especificada",
+ "user_rules_example_response": "127.0.0.1 %domain% Responda com 127.0.0.1 para %domain% apenas, não para os seus subdomínios",
+ "user_rules_example_unblock": "@@||%domain% Desbloquear o acesso a %domain% e todos os seus subdomínios",
+ "user_rules_filter_was_disabled": "%value% foi desativado",
+ "user_rules_ip": "Endereço de IP: %ip%",
+ "user_rules_learn_more": "Saiba mais sobre criar suas próprias listas de hosts.",
+ "user_rules_no_rules_matched": "Resultado: Não foram encontradas regras correspondentes",
+ "user_rules_parental_control_disabled": "Senha do controlo parental desativado",
+ "user_rules_placeholder": "Isto é um placeholder",
+ "user_rules_reason": "Motivo: %reason%",
+ "user_rules_reason_allowed_by": "Autorizado por %source%",
+ "user_rules_reason_blocked_by": "Bloqueado por %source%",
+ "user_rules_reason_filtered_by": "Filtrado por: %source%",
+ "user_rules_redirected_to": "Redirecionado para: %value%",
+ "user_rules_remove_dns_rewrite": "Remover reescrita de DNS",
+ "user_rules_rewrite_rule_is_applied": "Regra de reescrita aplicada",
+ "user_rules_rewritten_to": "Reescrito para: %value%",
+ "user_rules_rule": "Regra: %rule%",
+ "user_rules_rule_added_to_custom_filtering_rules": "Regra adicionada às regras de filtragem personalizadas",
+ "user_rules_rule_removed": "Regra removida das regras de filtragem personalizadas",
+ "user_rules_safe_search_disabled": "Pesquisa segura desativada",
+ "user_rules_service": "Serviço: %service%",
+ "user_rules_service_allowed": "O serviço %value% foi permitido",
+ "user_rules_source": "Fonte: %value%",
+ "user_rules_source_allowlist": "Lista de permissões: %value%",
+ "user_rules_source_blocklist": "Listas de bloqueio: %value%",
+ "user_rules_status": "Estado: %reason%",
+ "username_label": "Nome do utilizador",
+ "username_placeholder": "Insira o nome de utilizador",
+ "validated_with_dnssec": "Validado com DNSSEC",
+ "wednesday": "Quarta-feira",
+ "whois": "WHOIS",
+ "yes": "Sim",
+ "yes_disable": "Sim, desativar",
+ "yes_reset": "Sim, repor"
+}
diff --git a/client_v2/src/__locales/ro.json b/client_v2/src/__locales/ro.json
new file mode 100644
index 00000000000..8f7cb4a637b
--- /dev/null
+++ b/client_v2/src/__locales/ro.json
@@ -0,0 +1,390 @@
+{
+ "access_settings_title": "Setări de acces",
+ "actions_table_header": "Acțiuni",
+ "add": "Adaugă",
+ "add_allowlist": "Adăugați autorizare",
+ "add_blocklist": "Adăugați blocaj",
+ "add_persistent_client": "Adăugați ca client persistent",
+ "add_to_allowlist": "Adăugare la lista de autorizări",
+ "ads_blocked": "Reclame blocate",
+ "ads_blocked_card": "Reclame blocate",
+ "adult_websites_blocked_tooltip": "Numărul de site-uri pentru adulți blocate",
+ "all_lists_up_to_date_toast": "Toate listele sunt deja la zi",
+ "allowed": "Autorizate",
+ "allowlist_add": "Adăugați autorizare",
+ "allowlist_edit": "Editare autorizare",
+ "apply": "Aplică",
+ "autofix_warning_result": "Ca urmare, toate cererile DNS ale sistemul dvs. vor fi procesate în mod implicit de AdGuardHome.",
+ "autofix_warning_text": "Dacă clicați pe \"Fix\", AdGuardHome va configura sistemul dvs. pentru a utiliza serverul DNS AdGuardHome.",
+ "average_time_processing": "Timpul mediu de procesare",
+ "average_upstream_response_time": "Timpul mediu de răspuns al serverului în amonte",
+ "back": "Înapoi",
+ "block": "Blocați",
+ "block_for_this_client_only": "Blocați numai pentru acest client",
+ "blocked": "Blocat",
+ "blocked_by_cname_or_ip": "Blocat de CNAME sau IP",
+ "blocked_service": "Serviciu blocat",
+ "blocked_services": "Servicii blocate",
+ "blocked_threats": "Amenințări blocate",
+ "blocked_threats_chart": "Amenințări blocate",
+ "blocklist_add_manual": "Adăugați manual",
+ "blocklist_edit": "Editare blocare",
+ "blocklist_homepage": "Pagina de start",
+ "blocklist_url_file_path": "URL sau locația fișierului",
+ "bootstrap_dns": "Serverele DNS Bootstrap",
+ "browse": "Navigați",
+ "cache_cleared": "Cache-ul DNS a fost golit cu succes",
+ "cache_config_clear": "Goliți memoria cache",
+ "cache_config_optimistic": "Caching optimistic",
+ "cache_config_ttl_validation": "Valoarea TTL cache minimă trebuie să fie mai mică sau egală cu valoarea maximă",
+ "cache_enabled": "Activați memoria cache",
+ "cancel": "Anulare",
+ "cancel_btn": "Anulare",
+ "category_tooltip": "Categorie",
+ "changes_saved_success": "Modificările au fost salvate",
+ "check_dhcp_servers": "Căutați servere DHCP",
+ "check_not_found": "Nu se găsește în listele de filtre",
+ "check_updates_btn": "Caută actualizări",
+ "clear_btn": "Șterge",
+ "client_add": "Adăugați client",
+ "client_details": "Detalii client",
+ "client_id": "ClientID",
+ "client_id_placeholder": "Introduceți un ClientID",
+ "client_identifier": "Identificator",
+ "client_settings": "Setări client",
+ "client_table_header": "Client",
+ "clients_add_identifier": "Adăugați identificator",
+ "clients_add_placeholder": "Introduceți nume client",
+ "clients_dns_cache_size": "Dimensiunea cache-ului DNS, în octeți",
+ "clients_not_found": "Nu au fost găsiți clienți",
+ "clients_protection": "Protecție",
+ "clients_tags": "Etichete",
+ "clients_use_global_settings": "Folosiți setări globale",
+ "close": "Închideți",
+ "confirm": "Confirmați",
+ "copied": "Copiat în clipboard",
+ "copy": "Copiați",
+ "copy_to_clipboard": "Copiază în clipboard",
+ "custom_filtering_rules": "Reguli de filtrare personalizate",
+ "dark_theme": "Temă întunecată",
+ "dashboard": "Tablou de bord",
+ "delete_btn": "Ștergere",
+ "delete_table_action": "Ștergere",
+ "dhcp_add_static_lease": "Adăugați închiriere statică",
+ "dhcp_config_saved": "Configurare DHCP salvată cu succes",
+ "dhcp_edit_static_lease": "Editați închiriere statică",
+ "dhcp_form_gateway_input": "IP Gateway",
+ "dhcp_form_subnet_input": "Mască subnet",
+ "dhcp_leases_not_found": "Nu s-au găsit DHCP închiriate",
+ "dhcp_leases_title": "DHCP închiriate",
+ "dhcp_new_static_lease": "Închiriere statică nouă",
+ "dhcp_reset_leases": "Resetați toate închirierile",
+ "dhcp_reset_leases_success": "Închirierile DHCP au fost resetate cu succes",
+ "dhcp_settings": "Setări DHCP",
+ "dhcp_static_leases_not_found": "Nu s-au găsit închirieri statice DHCP",
+ "dhcp_table_hostname": "Hostname",
+ "dhcp_table_ip_address": "Adresa IP",
+ "disable": "Dezactivare",
+ "disable_protection_btn": "Dezactivare protecție",
+ "disabled_dhcp": "Server DHCP dezactivat",
+ "disallow_this_client": "Nu permiteți acest client",
+ "dns_access_settings_title": "Setări de acces",
+ "dns_allowed_clients": "Clienți autorizați",
+ "dns_blocking_mode": "Modul de blocare",
+ "dns_blocking_mode_custom_ip": "Adresa IP personalizată",
+ "dns_blocking_mode_default": "Implicit",
+ "dns_blocking_mode_ipv4_label": "Blocarea IPv4",
+ "dns_blocking_mode_ipv6_label": "Blocarea IPv6",
+ "dns_blocking_mode_nxdomain": "NXDOMAIN",
+ "dns_blocking_mode_refused": "REFUZAT",
+ "dns_blocking_mode_title": "Modul de blocare",
+ "dns_bootstrap_dns_placeholder": "Adrese IP",
+ "dns_bootstrap_dns_title": "Serverele DNS Bootstrap",
+ "dns_bootstrap_servers": "Serverele DNS Bootstrap",
+ "dns_cache_config": "Configurare cache DNS",
+ "dns_cache_size": "Mărime cache",
+ "dns_cache_size_label": "Dimensiunea cache-ului DNS, în octeți",
+ "dns_cache_size_title": "Mărime cache",
+ "dns_clear_cache": "Goliți memoria cache",
+ "dns_clear_cache_cancel": "Anulare",
+ "dns_clear_cache_confirm": "Da, resetați",
+ "dns_clear_cache_yes": "Da, resetați",
+ "dns_config": "Configurația serverului DNS",
+ "dns_disallowed_clients": "Clienți neautorizați",
+ "dns_disallowed_clients_title": "Clienți neautorizați",
+ "dns_disallowed_domains": "Domenii blocate",
+ "dns_disallowed_domains_title": "Domenii blocate",
+ "dns_edns_custom_placeholder": "Introduceți adresa IP",
+ "dns_fallback_dns_title": "Servere DNS de rezervă",
+ "dns_fallback_servers": "Servere DNS de rezervă",
+ "dns_optimistic_caching": "Caching optimistic",
+ "dns_over_https": "DNS-over-HTTPS",
+ "dns_over_quic": "DNS-over-QUIC",
+ "dns_over_tls": "DNS-over-TLS",
+ "dns_override_max_ttl": "Suprascrieți maximum TTL",
+ "dns_override_max_ttl_title": "Suprascrieți maximum TTL",
+ "dns_override_min_ttl": "Suprascrieți minimum TTL",
+ "dns_override_min_ttl_title": "Suprascrieți minimum TTL",
+ "dns_privacy": "Confidențialitate DNS",
+ "dns_private_reverse_servers_title": "Servere DNS inverse private",
+ "dns_rate_limit": "Limita ratei",
+ "dns_rate_limit_allowlist_placeholder": "Adrese IP",
+ "dns_rate_limit_placeholder": "Introduceți limita ratei",
+ "dns_rate_limit_title": "Limita ratei",
+ "dns_rewrites": "Rescrieri DNS",
+ "dns_server_addresses": "Adresele serverului",
+ "dns_server_addresses_label": "Adrese de server, una pe linie",
+ "dns_server_addresses_title": "Adresele serverului",
+ "dns_server_config_title": "Configurația serverului DNS",
+ "dns_settings": "DNS",
+ "dns_test_upstreams": "Testați upstreams",
+ "dns_upstream_servers_title": "Servere DNS în amonte",
+ "dns_upstream_timeout": "Durata de așteptare a răspunsurilor de la serverele upstream",
+ "dns_upstream_timeout_title": "Durata de așteptare a răspunsurilor de la serverele upstream",
+ "dns_upstream_validation_invalid": "Format nevalid",
+ "domain": "Domeniu",
+ "domain_or_client": "Domeniu sau client",
+ "download_mobileconfig": "Descărcați fișierul de configurare",
+ "edit_table_action": "Editare",
+ "enable": "Activare",
+ "enabled_dhcp": "Server DHCP activat",
+ "encryption_certificate_path": "Locația certificatului",
+ "encryption_certificates": "Certificate",
+ "encryption_certificates_input": "Copiați/lipiți certificatele dvs. PEM-codate aici.",
+ "encryption_certificates_source_content": "Lipiți conținutul certificatelor",
+ "encryption_certificates_source_path": "Precizați locația certificatelor",
+ "encryption_chain_invalid": "Lanț de certificate invalid",
+ "encryption_chain_valid": "Lanț de certificate valid",
+ "encryption_config_saved": "Configurația de criptare salvată",
+ "encryption_doq": "Portul DNS-over-QUIC",
+ "encryption_dot": "Port DNS-over-TLS",
+ "encryption_https": "Port HTTPS",
+ "encryption_key": "Cheie privată",
+ "encryption_key_input": "Copiați/lipiți cheia dvs. privată PEM-codată pentru certificatul dvs. aici.",
+ "encryption_key_source_content": "Lipiți conținutul cheii private",
+ "encryption_plain_dns": "DNS simplu",
+ "encryption_plain_dns_error": "Pentru a dezactiva DNS simplu, activați cel puțin un protocol DNS criptat",
+ "encryption_private_key_path": "Locația cheii private",
+ "encryption_server": "Nume de server",
+ "encryption_server_enter": "Introduceți numele domeniului",
+ "encryption_title": "Criptare",
+ "enter_cache_size": "Introduceți mărimea cache-ului (bytes)",
+ "enter_ip_address_placeholder": "Introduceți adresa IP",
+ "error": "Eroare",
+ "fallback_dns": "Servere DNS de rezervă",
+ "filter_category_general": "General",
+ "filter_category_general_desc": "Liste care blochează urmărirea și publicitatea pe majoritatea aparatelor",
+ "filter_category_other": "Altele",
+ "filter_category_other_desc": "Alte liste de blocări",
+ "filter_category_regional": "Regional",
+ "filter_category_regional_desc": "Liste focalizate pe reclame regionale și servere de urmărire",
+ "filter_category_security": "Securitate",
+ "filter_category_security_desc": "Listele concepute special pentru a bloca domenii rău intenționate, phishing și înșelătorie",
+ "filters": "Filtre",
+ "forgot_password": "Ați uitat parola?",
+ "form_enter_blocked_response_ttl": "Introduceți răspunsul blocat TTL (secunde)",
+ "form_error_client_id_format": "ClientID-ul trebuie să conțină numai numere, litere minuscule și cratime",
+ "form_error_domain_format": "Format de domeniu invalid",
+ "form_error_equal": "Nu trebuie să fie egale",
+ "form_error_format": "Format nevalid",
+ "form_error_gateway_ip": "Locația nu poate avea adresa IP a gateway-ului",
+ "form_error_ip4_format": "Adresă IPv4 nevalidă",
+ "form_error_ip6_format": "Adresa IPv6 nevalidă",
+ "form_error_ip_format": "Adresă IP greșită",
+ "form_error_mac_format": "Adresă MAC nevalidă",
+ "form_error_password": "Parolele nu corespund",
+ "form_error_port": "Introduceți un număr de port valid",
+ "form_error_port_range": "Introduceți valoarea portului între 80-65535",
+ "form_error_port_unsafe": "Port nesigur",
+ "form_error_server_name": "Nume de server nevalid",
+ "form_error_url_format": "Introduceți o adresă URL sau o cale de fișier validă",
+ "friday": "Vineri",
+ "gateway_or_subnet_invalid": "Mască de subrețea nevalidă",
+ "general_statistics": "Statistici generale",
+ "ignore_domains_title": "Domenii ignorate",
+ "inactivity_schedule_add": "Adăugați program",
+ "inactivity_schedule_all_day": "Toată ziua",
+ "inactivity_schedule_edit": "Editare program",
+ "install_auth_confirm": "Confirmați parola",
+ "install_auth_password": "Parola",
+ "install_auth_password_enter": "Introduceți parola",
+ "install_auth_username": "Nume utilizator",
+ "install_auth_username_enter": "Introduceți nume utilizator",
+ "install_saved": "Salvat cu succes",
+ "install_settings_all_interfaces": "Toate interfețele",
+ "install_settings_port": "Portul",
+ "install_static_error": "AdGuard Home nu o poate configura automat pentru această interfață de rețea. Vă rugăm să căutați instrucțiuni despre cum să faceți acest lucru manual.",
+ "install_static_ok": "Vești bune! Adresa IP statică este deja configurată",
+ "install_step": "Etapa",
+ "install_submit_title": "Felicitări!",
+ "ip_address": "Adresa IP",
+ "known_tracker": "Tracker cunoscut",
+ "language": "Limbă",
+ "light_theme": "Temă luminoasă",
+ "login": "Conectare",
+ "logout": "Deconectați-vă",
+ "make_static": "Faceți static",
+ "milliseconds_abbreviation": "ms",
+ "monday": "Luni",
+ "name": "Nume",
+ "name_label": "Nume",
+ "name_table_header": "Nume",
+ "name_tooltip": "Nume",
+ "next": "Următor",
+ "no": "Nu",
+ "none_text": "Nici una",
+ "nothing_found": "Nimic găsit",
+ "notify_undo": "Anulare",
+ "notify_updated": "Actualizat",
+ "parental_control": "Control Parental",
+ "parental_group_cdn": "CDN",
+ "parental_group_development": "Dezvoltare de program",
+ "parental_group_gaming": "Jocuri",
+ "parental_group_hosting": "Găzduire web",
+ "parental_group_privacy": "Instrumente de confidențialitate",
+ "parental_group_shopping": "Cumpărături",
+ "parental_group_social": "Rețele sociale",
+ "parental_group_social_network": "Rețele sociale",
+ "parental_group_software": "Dezvoltare de program",
+ "parental_group_streaming": "Streaming",
+ "password_label": "Parola",
+ "password_placeholder": "Introduceți parola",
+ "plain_dns": "DNS simplu",
+ "privacy_policy": "Politică Intimitate",
+ "protection": "Protecție",
+ "protocol": "Protocol",
+ "query_log_allowed": "Autorizate",
+ "query_log_blocked": "Blocat",
+ "query_log_blocked_services": "Servicii blocate",
+ "query_log_blocked_threats": "Amenințări blocate",
+ "query_log_custom_filtering_rules": "Reguli de filtrare personalizate",
+ "query_log_nothing_available": "Nimic găsit",
+ "query_log_processed": "Procesat",
+ "query_log_rewritten": "Rescrise",
+ "query_log_safe_search": "Căutarea sigură",
+ "query_log_strict_search": "Utilizați ghilimele duble pentru căutare strictă",
+ "rate_limit_subnet_len_ipv4_error": "Lungimea prefixului de subrețea IPv4 ar trebui să fie între 0 și 32",
+ "rate_limit_subnet_len_ipv6_error": "Lungimea prefixului de subrețea IPv6 ar trebui să fie între 0 și 128",
+ "refresh_btn": "Actualizare",
+ "refresh_statics": "Actualizare statistici",
+ "release_notes": "Note de lansare",
+ "remove": "Elimină",
+ "report_an_issue": "Semnalați o problemă",
+ "request_table_header": "Solicitare",
+ "requests_table_header": "Solicitări",
+ "reset": "Resetare",
+ "reset_settings": "Resetare setări",
+ "reset_settings_confirm": "Da",
+ "response_details": "Detalii răspuns",
+ "response_time": "Timp de răspuns",
+ "result": "Rezultat",
+ "rewrite_add": "Adăugați rescriere DNS",
+ "rewrite_edit": "Editați rescrierea DNS",
+ "rewrite_hosts_applied": "Rescrisă de regula fișierului hosts",
+ "rewrite_settings_updated": "Setările de rescriere DNS au fost actualizate cu succes",
+ "rewrite_updated": "DNS rescrie actualizat cu succes",
+ "rewritten": "Rescrise",
+ "rules_label": "Reguli",
+ "safe_browsing": "Navigare în siguranță",
+ "safe_search": "Căutarea sigură",
+ "saturday": "Sâmbătă",
+ "save": "Salvați",
+ "save_btn": "Salvați",
+ "search_placeholder": "Caută",
+ "server_config_blocking_mode": "Modul de blocare",
+ "server_config_blocking_mode_ipv4": "Blocarea IPv4",
+ "server_config_blocking_mode_ipv6": "Blocarea IPv6",
+ "server_config_custom_ip": "Adresa IP personalizată",
+ "server_config_default": "Implicit",
+ "server_config_disable_ipv6": "Dezactivați rezolvarea adreselor IPv6",
+ "server_config_dnssec_enable": "Activați DNSSEC",
+ "server_config_edns_enable": "Activați subrețeaua de clienți EDNS",
+ "server_config_nxdomain": "NXDOMAIN",
+ "server_config_rate_limit_placeholder": "Introduceți limita ratei",
+ "server_config_rate_limit_whitelist_faq": "Adresele IP excluse de la limitarea ratei",
+ "server_config_refused": "REFUZAT",
+ "server_config_subnet_len_ipv4": "Lungimea prefixului de subrețea pentru adrese IPv4",
+ "server_config_subnet_len_ipv4_faq": "Lungimea prefixului de subrețea pentru adresele IPv4 utilizate pentru limitarea ratei. Valoarea implicită este 24",
+ "server_config_subnet_len_ipv6": "Lungimea prefixului de subrețea pentru adrese IPv6",
+ "server_config_subnet_len_ipv6_faq": "Lungimea prefixului de subrețea pentru adresele IPv6 utilizate pentru limitarea ratei. Valoarea implicită este 56",
+ "set_static_ip": "Setați o adresă IP statică",
+ "settings": "Setări",
+ "settings_browsing_security": "Securitate la navigare",
+ "settings_confirm_clear_statistics": "Ștergeți statisticile?",
+ "settings_custom": "Personalizat",
+ "settings_general_short": "General",
+ "settings_global": "General",
+ "settings_notify_changes_saved": "Modificările au fost salvate",
+ "settings_notify_statistics_cleared": "Statistici șterse",
+ "settings_parental_control": "Control Parental",
+ "settings_rule_syntax": "Sintaxa regulilor",
+ "settings_safe_search": "Căutarea sigură",
+ "settings_statistics": "Statistici",
+ "settings_statistics_clear": "Ștergere statistici",
+ "settings_statistics_retention": "Păstrarea statisticilor",
+ "settings_yes_clear": "Da, resetați",
+ "setup_devices_router_title": "Router",
+ "setup_dns_privacy_ioc_mac": "Configurarea iOS și macOS",
+ "setup_dns_privacy_other_title": "Alte implementări",
+ "show_blocked_responses": "Blocat",
+ "show_processed_responses": "Procesat",
+ "source_label": "Sursă",
+ "source_tooltip": "Sursă",
+ "stats_adult": "Site-uri cu conținut adult blocate",
+ "stats_query_domain": "Domeniile cele mai căutate",
+ "status_table_header": "Statut",
+ "sunday": "Duminică",
+ "system_host_files": "Fișiere de sistem hosts",
+ "table_client": "Client",
+ "tags_title": "Etichete",
+ "test_upstreams": "Testați upstreams",
+ "threats_blocked": "Amenințări blocate",
+ "thursday": "Joi",
+ "time_table_header": "Ora",
+ "top_blocked_domains": "Domeniile blocate cel mai des",
+ "top_clients": "Clienți de top",
+ "top_upstreams": "Top servere în amonte",
+ "total": "Total",
+ "try_again": "Încercați din nou",
+ "tuesday": "Marți",
+ "unavailable_dhcp": "DHCP este indisponibil",
+ "unavailable_dhcp_desc": "AdGuard Home nu poate rula un server DHCP pe OS-ul dvs.",
+ "unblock": "Deblocați",
+ "update_button": "Actualizează",
+ "update_filters_disable": "Dezactivare",
+ "update_filters_interval_custom": "Personalizat",
+ "update_filters_interval_daily": "În fiecare zi",
+ "update_filters_interval_hourly": "La fiecare oră",
+ "update_filters_title": "Actualizează filtrele în mod automat",
+ "updates_checked": "Este disponibilă o nouă versiune de AdGuard Home\n",
+ "updates_version_equal": "AdGuard Home este la zi",
+ "upstream": "Server în amonte",
+ "upstream_dns": "Servere DNS în amonte",
+ "upstream_dns_addresses": "Adrese de server, una pe linie",
+ "upstream_dns_fastest_addr": "Cea mai rapidă adresă IP",
+ "upstream_dns_parallel_requests": "Solicitări paralele",
+ "upstream_dns_servers_title": "Servere DNS în amonte",
+ "upstream_examples_title": "Exemple",
+ "upstream_private_ptr_resolvers_title": "Utilizați rezolvatori DNS inverși privați",
+ "upstreams": "Upstreams",
+ "url_label": "URL",
+ "user_rules": "Reguli utilizator",
+ "user_rules_add_to_allowlist": "Adăugare la lista de autorizări",
+ "user_rules_check_button": "Verificați",
+ "user_rules_check_client_placeholder": "Introduceți identificatorul clientului",
+ "user_rules_check_hostname_label": "Nume gazdă sau nume de domeniu",
+ "user_rules_disable_filter": "Dezactivează filtrul",
+ "user_rules_disable_parental_control": "Dezactivați Controlul Parental",
+ "user_rules_dns_record_type_placeholder": "Selectați tipul de înregistrare DNS",
+ "user_rules_domain_blocked": "Domeniul este blocat",
+ "user_rules_edit_dns_rewrite": "Editați rescrierea DNS",
+ "user_rules_rewrite_rule_is_applied": "Regula de rescriere s-a aplicat",
+ "username_label": "Nume utilizator",
+ "username_placeholder": "Introduceți nume utilizator",
+ "validated_with_dnssec": "Validat cu DNSSEC",
+ "wednesday": "Miercuri",
+ "whois": "WHOIS",
+ "yes": "Da",
+ "yes_disable": "Da",
+ "yes_reset": "Da"
+}
diff --git a/client_v2/src/__locales/ru.json b/client_v2/src/__locales/ru.json
new file mode 100644
index 00000000000..0ba6b1a9a37
--- /dev/null
+++ b/client_v2/src/__locales/ru.json
@@ -0,0 +1,391 @@
+{
+ "access_settings_title": "Настройки доступа",
+ "actions_table_header": "Действия",
+ "add": "Добавить",
+ "add_allowlist": "Добавить белый список",
+ "add_blocklist": "Добавить чёрный список",
+ "add_persistent_client": "Добавить в сохранённые клиенты",
+ "add_to_allowlist": "Добавить в белый список",
+ "ads_blocked": "Заблокировано баннеров",
+ "ads_blocked_card": "Заблокировано баннеров",
+ "adult_websites_blocked_tooltip": "Количество заблокированных «сайтов для взрослых»",
+ "all_lists_up_to_date_toast": "Все списки уже обновлены",
+ "allowed": "Разрешённые",
+ "allowlist_add": "Добавить белый список",
+ "allowlist_edit": "Редактировать белый список",
+ "apply": "Применить",
+ "autofix_warning_result": "В результате все DNS-запросы от вашей системы будут по умолчанию обрабатываться AdGuard Home.\n",
+ "autofix_warning_text": "При нажатии «Исправить» AdGuard Home настроит вашу систему на использование DNS-сервера AdGuard Home.",
+ "average_time_processing": "Среднее время обработки запроса",
+ "average_upstream_response_time": "Среднее время ответа upstream-сервера",
+ "back": "Назад",
+ "block": "Заблокировать",
+ "block_for_this_client_only": "Заблокировать только для этого клиента",
+ "blocked": "Заблокировано",
+ "blocked_by_cname_or_ip": "Заблокировано с помощью CNAME или IP",
+ "blocked_service": "Заблокированный сервис",
+ "blocked_services": "Заблокированные сервисы",
+ "blocked_threats": "Заблокировано угроз",
+ "blocked_threats_chart": "Заблокировано угроз",
+ "blocklist_add_manual": "Добавить",
+ "blocklist_edit": "Редактировать чёрный список",
+ "blocklist_homepage": "Сайт",
+ "blocklist_url_file_path": "URL или путь к файлу",
+ "bootstrap_dns": "Bootstrap DNS-серверы",
+ "browse": "Загрузить из файла",
+ "cache_cleared": "Кеш DNS успешно очищен",
+ "cache_config_clear": "Очистить кеш",
+ "cache_config_optimistic": "Оптимистическое кеширование",
+ "cache_config_ttl_validation": "Значение для переопределения минимального TTL должно быть меньше или равно значению для переопределения максимального",
+ "cache_enabled": "Включить кеш",
+ "cancel": "Отмена",
+ "cancel_btn": "Отмена",
+ "category_tooltip": "Категория",
+ "changes_saved_success": "Изменения сохранены",
+ "check_dhcp_servers": "Проверить DHCP-серверы",
+ "check_not_found": "Не найдено в вашем списке фильтров",
+ "check_updates_btn": "Проверить обновления",
+ "clear_btn": "Очистить",
+ "client_add": "Добавить клиента",
+ "client_details": "Информация о клиенте",
+ "client_id": "ClientID",
+ "client_id_placeholder": "Введите ClientID",
+ "client_identifier": "Идентификатор",
+ "client_settings": "Настройки клиентов",
+ "client_table_header": "Клиент",
+ "clients_add_identifier": "Добавить идентификатор",
+ "clients_add_placeholder": "Введите имя клиента",
+ "clients_dns_cache_size": "Размер DNS-кеша в байтах",
+ "clients_not_found": "Клиентов не найдено",
+ "clients_protection": "Защита",
+ "clients_tags": "Теги",
+ "clients_use_global_settings": "Использовать глобальные настройки",
+ "close": "Закрыть",
+ "confirm": "Подтвердить",
+ "copied": "Скопировано",
+ "copy": "Копировать",
+ "copy_to_clipboard": "Копировать",
+ "custom_filtering_rules": "Пользовательские правила фильтрации",
+ "dark_theme": "Тёмная тема",
+ "dashboard": "Панель управления",
+ "delete_btn": "Удалить",
+ "delete_table_action": "Удалить",
+ "dhcp_add_static_lease": "Добавить статическую аренду",
+ "dhcp_config_saved": "Конфигурация DHCP-сервера успешно сохранена",
+ "dhcp_edit_static_lease": "Редактирование статической аренды",
+ "dhcp_form_gateway_input": "IP-адрес шлюза",
+ "dhcp_form_subnet_input": "Маска подсети",
+ "dhcp_leases_not_found": "Аренда DHCP не обнаружена",
+ "dhcp_leases_title": "Аренда DHCP",
+ "dhcp_new_static_lease": "Новая статическая аренда",
+ "dhcp_reset_leases": "Сбросить все аренды",
+ "dhcp_reset_leases_success": "Аренды DHCP успешно удалены",
+ "dhcp_settings": "Настройки DHCP",
+ "dhcp_static_leases_not_found": "Не найдено статических аренд DHCP",
+ "dhcp_table_hostname": "Имя хоста",
+ "dhcp_table_ip_address": "IP-адрес",
+ "disable": "Отключить",
+ "disable_protection_btn": "Отключить защиту",
+ "disabled_dhcp": "DHCP-сервер отключён",
+ "disallow_this_client": "Запретить доступ клиенту",
+ "dns_access_settings_title": "Настройки доступа",
+ "dns_allowed_clients": "Разрешённые клиенты",
+ "dns_blocking_mode": "Режим блокировки",
+ "dns_blocking_mode_custom_ip": "Пользовательский IP-адрес",
+ "dns_blocking_mode_default": "Стандартный",
+ "dns_blocking_mode_ipv4_label": "Блокировка IPv4",
+ "dns_blocking_mode_ipv6_label": "Блокировка IPv6",
+ "dns_blocking_mode_nxdomain": "NXDOMAIN",
+ "dns_blocking_mode_refused": "REFUSED",
+ "dns_blocking_mode_title": "Режим блокировки",
+ "dns_bootstrap_dns_placeholder": "IP-адреса",
+ "dns_bootstrap_dns_title": "Bootstrap DNS-серверы",
+ "dns_bootstrap_servers": "Bootstrap DNS-серверы",
+ "dns_cache_config": "Настройка кеша DNS",
+ "dns_cache_size": "Размер кеша",
+ "dns_cache_size_label": "Размер DNS-кеша в байтах",
+ "dns_cache_size_title": "Размер кеша",
+ "dns_clear_cache": "Очистить кеш",
+ "dns_clear_cache_cancel": "Отмена",
+ "dns_clear_cache_confirm": "Да, очистить",
+ "dns_clear_cache_yes": "Да, очистить",
+ "dns_config": "Настройки DNS-сервера",
+ "dns_disallowed_clients": "Запрещённые клиенты",
+ "dns_disallowed_clients_title": "Запрещённые клиенты",
+ "dns_disallowed_domains": "Неразрешённые домены",
+ "dns_disallowed_domains_title": "Неразрешённые домены",
+ "dns_edns_custom_placeholder": "Введите IP-адрес",
+ "dns_fallback_dns_title": "Резервные DNS-серверы",
+ "dns_fallback_servers": "Резервные DNS-серверы",
+ "dns_optimistic_caching": "Оптимистическое кеширование",
+ "dns_over_https": "DNS-over-HTTPS",
+ "dns_over_quic": "DNS-over-QUIC",
+ "dns_over_tls": "DNS-over-TLS",
+ "dns_override_max_ttl": "Переопределить максимальный TTL",
+ "dns_override_max_ttl_title": "Переопределить максимальный TTL",
+ "dns_override_min_ttl": "Переопределить минимальный TTL",
+ "dns_override_min_ttl_title": "Переопределить минимальный TTL",
+ "dns_privacy": "Зашифрованный DNS",
+ "dns_private_reverse_servers_title": "Приватные серверы для обратного DNS",
+ "dns_rate_limit": "Rate limit",
+ "dns_rate_limit_allowlist_placeholder": "IP-адреса",
+ "dns_rate_limit_placeholder": "Введите rate limit",
+ "dns_rate_limit_title": "Rate limit",
+ "dns_rewrites": "Перезапись DNS-запросов",
+ "dns_server_addresses": "Адреса серверов",
+ "dns_server_addresses_label": "Адреса сервера, по одному в строке",
+ "dns_server_addresses_title": "Адреса серверов",
+ "dns_server_config_title": "Настройки DNS-сервера",
+ "dns_settings": "DNS",
+ "dns_test_upstreams": "Тест upstream серверов",
+ "dns_upstream_servers_title": "Upstream DNS-серверы",
+ "dns_upstream_timeout": "Время ожидания ответов от upstream-серверов",
+ "dns_upstream_timeout_title": "Время ожидания ответов от upstream-серверов",
+ "dns_upstream_validation_invalid": "Неверный формат",
+ "domain": "Домен",
+ "domain_or_client": "Домен или клиент",
+ "download_mobileconfig": "Загрузить файл конфигурации",
+ "edit_table_action": "Редактировать",
+ "enable": "Включить",
+ "enabled_dhcp": "DHCP-сервер включён",
+ "encryption_certificate_path": "Путь к сертификату",
+ "encryption_certificates": "Сертификаты",
+ "encryption_certificates_input": "Скопируйте сюда сертификаты в PEM-кодировке.",
+ "encryption_certificates_source_content": "Вставить содержимое сертификатов",
+ "encryption_certificates_source_path": "Указать путь к файлу сертификатов",
+ "encryption_chain_invalid": "Цепочка сертификатов не прошла проверку",
+ "encryption_chain_valid": "Цепочка сертификатов прошла проверку",
+ "encryption_config_saved": "Настройки шифрования сохранены",
+ "encryption_doq": "Порт DNS-over-QUIC",
+ "encryption_dot": "Порт DNS-over-TLS",
+ "encryption_https": "Порт HTTPS",
+ "encryption_key": "Приватный ключ",
+ "encryption_key_input": "Скопируйте сюда приватный ключ в PEM-кодировке.",
+ "encryption_key_source_content": "Вставить содержимое закрытого ключа",
+ "encryption_plain_dns": "Нешифрованный DNS",
+ "encryption_plain_dns_error": "Чтобы отключить незашифрованный DNS, включите хотя бы один зашифрованный протокол DNS",
+ "encryption_private_key_path": "Путь к закрытому ключу",
+ "encryption_server": "Имя сервера",
+ "encryption_server_enter": "Введите ваше доменное имя",
+ "encryption_title": "Шифрование",
+ "enter_cache_size": "Введите размер кеша (в байтах)",
+ "enter_ip_address_placeholder": "Введите IP-адрес",
+ "error": "Ошибка",
+ "fallback_dns": "Резервные DNS-серверы",
+ "filter_category_general": "General",
+ "filter_category_general_desc": "Списки, которые блокируют отслеживание и рекламу на большинстве устройств",
+ "filter_category_other": "Прочее",
+ "filter_category_other_desc": "Другие списки блокировки",
+ "filter_category_regional": "Региональные",
+ "filter_category_regional_desc": "Списки, которые фокусируются на региональной рекламе и серверах отслеживания",
+ "filter_category_security": "Безопасность",
+ "filter_category_security_desc": "Списки, созданные специально для блокировки вредоносных, фишинговых и мошеннических доменов",
+ "filters": "Фильтры",
+ "forgot_password": "Забыли пароль?",
+ "form_enter_blocked_response_ttl": "Введите TTL заблокированного ответа (в секундах)",
+ "form_error_client_id_format": "ClientID может содержать только цифры, строчные латинские буквы и дефисы",
+ "form_error_domain_format": "Некорректный домен",
+ "form_error_equal": "Не должны быть равны",
+ "form_error_format": "Неверный формат",
+ "form_error_gateway_ip": "Аренда не может иметь IP-адрес шлюза",
+ "form_error_ip4_format": "Некорректный IPv4-адрес",
+ "form_error_ip6_format": "Некорректный IPv6-адрес",
+ "form_error_ip_format": "Некорректный IP-адрес",
+ "form_error_mac_format": "Некорректный MAC-адрес",
+ "form_error_password": "Пароли не совпадают",
+ "form_error_port": "Введите корректный порт",
+ "form_error_port_range": "Введите номер порта из интервала 80-65535",
+ "form_error_port_unsafe": "Небезопасный порт",
+ "form_error_server_name": "Некорректное имя сервера",
+ "form_error_url_format": "Введите корректный URL или путь к файлу",
+ "friday": "Пятница",
+ "gateway_or_subnet_invalid": "Некорректная маска подсети",
+ "general_statistics": "Общая статистика",
+ "ignore_domains_title": "Игнорируемые домены",
+ "inactivity_schedule_add": "Добавить расписание",
+ "inactivity_schedule_all_day": "Весь день",
+ "inactivity_schedule_edit": "Изменить расписание",
+ "install_auth_confirm": "Подтвердить пароль",
+ "install_auth_password": "Пароль",
+ "install_auth_password_enter": "Введите пароль",
+ "install_auth_username": "Имя пользователя",
+ "install_auth_username_enter": "Введите имя пользователя",
+ "install_saved": "Успешно сохранено",
+ "install_settings_all_interfaces": "Все интерфейсы",
+ "install_settings_port": "Порт",
+ "install_static_error": "AdGuard Home не может автоматически настроить его для этого сетевого интерфейса. Пожалуйста, посмотрите инструкцию о том, как это сделать вручную.",
+ "install_static_ok": "Хорошие новости! Ваш статический IP-адрес уже настроен",
+ "install_step": "Шаг",
+ "install_submit_title": "Поздравляем!",
+ "ip_address": "IP-адрес",
+ "known_tracker": "Известный трекер",
+ "language": "Язык",
+ "light_theme": "Светлая тема",
+ "login": "Войти",
+ "logout": "Выйти",
+ "make_static": "Сделать статической",
+ "milliseconds_abbreviation": "мс",
+ "monday": "Понедельник",
+ "name": "Название",
+ "name_label": "Название",
+ "name_table_header": "Название",
+ "name_tooltip": "Название",
+ "next": "Далее",
+ "no": "Нет",
+ "none_text": "Отсутствует",
+ "nothing_found": "Ничего не найдено",
+ "notify_undo": "Отменить",
+ "notify_updated": "Обновлено",
+ "parental_control": "Родительский контроль",
+ "parental_group_cdn": "CDN",
+ "parental_group_development": "Разработка программного обеспечения",
+ "parental_group_gaming": "Игры",
+ "parental_group_hosting": "Веб-хостинг",
+ "parental_group_privacy": "Инструменты конфиденциальности",
+ "parental_group_shopping": "Шопинг",
+ "parental_group_social": "Социальные сети",
+ "parental_group_social_network": "Социальные сети",
+ "parental_group_software": "Разработка программного обеспечения",
+ "parental_group_streaming": "Доступен стриминг",
+ "password_label": "Пароль",
+ "password_placeholder": "Введите пароль",
+ "plain_dns": "Нешифрованный DNS",
+ "privacy_policy": "Политика конфиденциальности",
+ "protection": "Защита",
+ "protocol": "Протокол",
+ "query_log_allowed": "Разрешённые",
+ "query_log_blocked": "Заблокировано",
+ "query_log_blocked_services": "Заблокированные сервисы",
+ "query_log_blocked_threats": "Заблокировано угроз",
+ "query_log_custom_filtering_rules": "Пользовательские правила фильтрации",
+ "query_log_nothing_available": "Ничего не найдено",
+ "query_log_processed": "Обработан",
+ "query_log_rewritten": "Перезаписан",
+ "query_log_safe_search": "Безопасный поиск",
+ "query_log_strict_search": "Используйте двойные кавычки для строгого поиска",
+ "rate_limit_subnet_len_ipv4_error": "Длина префикса IPv4-подсетей должна составлять от 0 до 32",
+ "rate_limit_subnet_len_ipv6_error": "Длина префикса IPv6-подсетей должна составлять от 0 до 128",
+ "refresh_btn": "Обновить",
+ "refresh_statics": "Обновить статистику",
+ "release_notes": "Список изменений",
+ "remove": "Удалить",
+ "report_an_issue": "Сообщить о проблеме",
+ "request_table_header": "Запрос",
+ "requests_table_header": "Запросы",
+ "reset": "Сбросить",
+ "reset_settings": "Сбросить настройки",
+ "reset_settings_confirm": "Да, сбросить",
+ "response_details": "Детали ответа",
+ "response_time": "Время ответа",
+ "result": "Результат",
+ "rewrite_add": "Добавить правило перезаписи DNS-запросов",
+ "rewrite_edit": "Редактировать правило перезаписи DNS-запросов",
+ "rewrite_hosts_applied": "Переписано по правилу файла hosts",
+ "rewrite_settings_updated": "Настройки перезаписи DNS-запросов успешно обновлены",
+ "rewrite_updated": "Правило перезаписи DNS-запросов успешно обновлено",
+ "rewritten": "Перезаписан",
+ "rules_label": "Правила",
+ "safe_browsing": "Безопасный интернет",
+ "safe_search": "Безопасный поиск",
+ "saturday": "Суббота",
+ "save": "Сохранить",
+ "save_btn": "Сохранить",
+ "search_placeholder": "Поиск",
+ "server_config_blocking_mode": "Режим блокировки",
+ "server_config_blocking_mode_ipv4": "Блокировка IPv4",
+ "server_config_blocking_mode_ipv6": "Блокировка IPv6",
+ "server_config_custom_ip": "Пользовательский IP-адрес",
+ "server_config_default": "Стандартный",
+ "server_config_disable_ipv6": "Отключить обработку IPv6-адресов",
+ "server_config_dnssec_enable": "Включить DNSSEC",
+ "server_config_edns_enable": "Включить отправку EDNS Client Subnet",
+ "server_config_nxdomain": "NXDOMAIN",
+ "server_config_rate_limit": "Ограничение частоты запросов, RPS",
+ "server_config_rate_limit_placeholder": "Введите rate limit",
+ "server_config_rate_limit_whitelist_faq": "IP-адреса, на которые не распространяется ограничение скорости",
+ "server_config_refused": "REFUSED",
+ "server_config_subnet_len_ipv4": "Длина префикса подсети для IPv4-адресов",
+ "server_config_subnet_len_ipv4_faq": "Длина префикса подсети для IPv4-адресов, используемых для ограничения скорости. По умолчанию 24",
+ "server_config_subnet_len_ipv6": "Длина префикса подсети для IPv6-адресов",
+ "server_config_subnet_len_ipv6_faq": "Длина префикса подсети для IPv6-адресов, используемых для ограничения скорости. По умолчанию 56",
+ "set_static_ip": "Установить статический IP-адрес",
+ "settings": "Настройки",
+ "settings_browsing_security": "Антифишинг",
+ "settings_confirm_clear_statistics": "Очистить статистику?",
+ "settings_custom": "Другая",
+ "settings_general_short": "General",
+ "settings_global": "Глобальные",
+ "settings_notify_changes_saved": "Изменения сохранены",
+ "settings_notify_statistics_cleared": "Статистика очищена",
+ "settings_parental_control": "Родительский контроль",
+ "settings_rule_syntax": "Синтаксис",
+ "settings_safe_search": "Безопасный поиск",
+ "settings_statistics": "Статистика",
+ "settings_statistics_clear": "Сбросить статистику",
+ "settings_statistics_retention": "Сохранение статистики",
+ "settings_yes_clear": "Да, очистить",
+ "setup_devices_router_title": "Роутер",
+ "setup_dns_privacy_ioc_mac": "Конфигурация для iOS и macOS",
+ "setup_dns_privacy_other_title": "Другие решения",
+ "show_blocked_responses": "Заблокировано",
+ "show_processed_responses": "Обработан",
+ "source_label": "Источник",
+ "source_tooltip": "Источник",
+ "stats_adult": "Заблокированные «взрослые» сайты",
+ "stats_query_domain": "Часто запрашиваемые домены",
+ "status_table_header": "Статус",
+ "sunday": "Воскресенье",
+ "system_host_files": "Системные hosts-файлы",
+ "table_client": "Клиент",
+ "tags_title": "Теги",
+ "test_upstreams": "Тест upstream серверов",
+ "threats_blocked": "Заблокировано угроз",
+ "thursday": "Четверг",
+ "time_table_header": "Время",
+ "top_blocked_domains": "Часто блокируемые домены",
+ "top_clients": "Частые клиенты",
+ "top_upstreams": "Часто запрашиваемые upstream-серверы",
+ "total": "Всего",
+ "try_again": "Попробовать снова",
+ "tuesday": "Вторник",
+ "unavailable_dhcp": "DHCP недоступен",
+ "unavailable_dhcp_desc": "AdGuard Home не может запустить DHCP-сервер на вашей ОС",
+ "unblock": "Разблокировать",
+ "update_button": "Обновить",
+ "update_filters_disable": "Отключить",
+ "update_filters_interval_custom": "Другая",
+ "update_filters_interval_daily": "Каждый день",
+ "update_filters_interval_hourly": "Каждый час",
+ "update_filters_title": "Обновлять фильтры автоматически",
+ "updates_checked": "Доступна новая версия AdGuard Home",
+ "updates_version_equal": "Версия AdGuard Home актуальна",
+ "upstream": "Upstream-сервер",
+ "upstream_dns": "Upstream DNS-серверы",
+ "upstream_dns_addresses": "Адреса сервера, по одному в строке",
+ "upstream_dns_fastest_addr": "Самый быстрый IP-адрес",
+ "upstream_dns_parallel_requests": "Параллельные запросы",
+ "upstream_dns_servers_title": "Upstream DNS-серверы",
+ "upstream_examples_title": "Примеры",
+ "upstream_private_ptr_resolvers_title": "Использовать приватные обратные DNS-резолверы",
+ "upstreams": "Upstreams",
+ "url_label": "URL",
+ "user_rules": "Пользовательские правила",
+ "user_rules_add_to_allowlist": "Добавить в белый список",
+ "user_rules_check_button": "Проверить",
+ "user_rules_check_client_placeholder": "Введите идентификатор клиента",
+ "user_rules_check_hostname_label": "Имя хоста или домена",
+ "user_rules_disable_filter": "Отключить фильтр",
+ "user_rules_disable_parental_control": "Отключить Родительский контроль",
+ "user_rules_dns_record_type_placeholder": "Выберите тип DNS-записи",
+ "user_rules_domain_blocked": "Домен заблокирован",
+ "user_rules_edit_dns_rewrite": "Редактировать правило перезаписи DNS-запросов",
+ "user_rules_rewrite_rule_is_applied": "Применено правило перезаписи",
+ "username_label": "Имя пользователя",
+ "username_placeholder": "Введите имя пользователя",
+ "validated_with_dnssec": "Подтверждено с помощью DNSSEC",
+ "wednesday": "Среда",
+ "whois": "WHOIS",
+ "yes": "Да",
+ "yes_disable": "Да, отключить",
+ "yes_reset": "Да, сбросить"
+}
diff --git a/client_v2/src/__locales/si-lk.json b/client_v2/src/__locales/si-lk.json
new file mode 100644
index 00000000000..aac0743b2b3
--- /dev/null
+++ b/client_v2/src/__locales/si-lk.json
@@ -0,0 +1,279 @@
+{
+ "access_settings_title": "ප්රවේශ සැකසුම්",
+ "actions_table_header": "ක්රියාමාර්ග",
+ "add_allowlist": "ඉඩ දීමේ ලැයිස්තුවක් එකතු කරන්න",
+ "add_blocklist": "අවහිර කිරීමේ ලැයිස්තුවක් එකතු කරන්න",
+ "add_persistent_client": "නිබඳිත පාරිභෝගිකයා ලෙස එකතු කරන්න",
+ "adult_websites_blocked_tooltip": "අවහිර කළ වැඩිහිටි වියමන අඩවි ගණන",
+ "all_lists_up_to_date_toast": "සියළුම ලැයිස්තු දැනටමත් යාවත්කාලීනයි",
+ "allowlist_add": "ඉඩ දීමේ ලැයිස්තුවක් එකතු කරන්න",
+ "allowlist_edit": "ඉඩ දීමේ ලැයිස්තුව සංස්කරණය",
+ "apply": "යොදන්න",
+ "autofix_warning_result": "ප්රතිඵලයක් ලෙස ඔබගේ පද්ධතියෙන් ලැබෙන සියළුම ව.නා.ප. ඉල්ලීම් මූලිකවම ඇඩ්ගාර්ඩ් හෝම් විසින් සකසනු ඇත.",
+ "autofix_warning_text": "ඔබ \"නිරාකරණය\" යන්න එබුවහොත්, ඔබගේ පද්ධතිය ඇඩ්ගාර්ඩ් හෝම් ව.නා.ප. සේවාදායකය භාවිතයට වින්යාසගත කෙරෙනු ඇත.",
+ "average_time_processing": "සාමාන්ය සැකසුම් කාලය",
+ "average_upstream_response_time": "උඩුගත කරන සේවාදායකයේ සාමාන්ය පිළිතුරු කාලය",
+ "block": "අවහිර",
+ "block_for_this_client_only": "මෙම අනුග්රාහකයට අවහිර කරන්න",
+ "blocked_by_cname_or_ip": "අන්. නාමයක් (CNAME) හෝ අ.ජා.කෙ. මගින් අවහිර කර ඇත",
+ "blocked_service": "අවහිර කළ සේවාව",
+ "blocked_services": "අවහිර කළ සේවා",
+ "blocklist_edit": "අවහිර කිරීමේ ලැයිස්තුව සංස්කරණය",
+ "blocklist_homepage": "මුල් පිටුව",
+ "bootstrap_dns": "Bootstrap DNS සේවාදායකය",
+ "cache_cleared": "ව.නා.ප. නිහිතය හිස් කෙරිණි",
+ "cache_config_clear": "නිහිතය මකන්න",
+ "cache_config_optimistic": "සර්වශුභවාදී නිහිතගතය",
+ "cache_config_ttl_validation": "නිහිතයෙහි පාගාගෙන යන අවම පව. කා. (TTL) උපරිමයට වඩා අඩු හෝ සමාන විය යුතුය",
+ "cache_enabled": "නිහිතය සබල කරන්න",
+ "cancel": "අවලංගු",
+ "cancel_btn": "අවලංගු",
+ "category_tooltip": "ප්රවර්ගය",
+ "check_dhcp_servers": "ග.ධා.වි.කෙ. සේවාදායක පරීක්ෂා කරන්න",
+ "check_not_found": "ඔබගේ පෙරහන් ලැයිස්තු තුළ නැත",
+ "check_updates_btn": "යාවත්කාල පරීක්ෂා කරන්න",
+ "client_add": "අනුග්රාහකයක් එකතු කරන්න",
+ "client_details": "අනුග්රාහකයේ විස්තර",
+ "client_id": "අනුග්රාහකයේ හැඳු.",
+ "client_id_placeholder": "අනුග්රාහකයක හැඳු. යොදන්න",
+ "client_identifier": "හඳුන්වනය",
+ "client_settings": "අනුග්රාහකයේ සැකසුම්",
+ "client_table_header": "අනුග්රාහකය",
+ "clients_add_identifier": "හඳුන්වනයක් එකතු කරන්න",
+ "clients_add_placeholder": "අනුග්රාහකයේ නම ඇතුල් කරන්න",
+ "clients_dns_cache_size": "ව.නා.ප. නිහිතයේ ප්රමාණය (බයිට වලින්)",
+ "clients_not_found": "අනුග්රාහක හමු නොවිණි",
+ "clients_tags": "අනන්යන",
+ "clients_use_global_settings": "ගෝලීය සැකසුම් භාවිතා කරන්න",
+ "close": "වසන්න",
+ "custom_filtering_rules": "අභිරුචි පෙරීමේ නීති",
+ "dark_theme": "අඳුරු තේමාව",
+ "dashboard": "උපකරණ පුවරුව",
+ "dhcp_add_static_lease": "ස්ථිර කල්පැවරීමක් යොදන්න",
+ "dhcp_config_saved": "ග.ධා.වි.කෙ. වින්යාසය සාර්ථකව සුරකින ලදි",
+ "dhcp_edit_static_lease": "ස්ථිර කල්පැවරීම සංස්කරණය කරන්න",
+ "dhcp_form_gateway_input": "සේවා ප්රවර්ධක දොරටු IP අංකය",
+ "dhcp_form_subnet_input": "උපජාල කාණ්ඩය",
+ "dhcp_leases_not_found": "ග.ධා.වි.කෙ. කල්පැවරීම් නැත",
+ "dhcp_leases_title": "ග.ධා.වි.කෙ. කල්පැවරීම",
+ "dhcp_new_static_lease": "නව ස්ථිර කල්පැවරීම",
+ "dhcp_reset_leases": "කල්පැවරීම් යළි සකසන්න",
+ "dhcp_reset_leases_success": "ග.ධා.වි.කෙ. කල්පැවරීම් යළි සැකසිණි",
+ "dhcp_settings": "ග.ධා.වි.කෙ. සැකසුම්",
+ "dhcp_static_leases_not_found": "ග.ධා.වි.කෙ. ස්ථිර කල්පැවරීම් නැත",
+ "dhcp_table_hostname": "සත්කාරක නාමය",
+ "dhcp_table_ip_address": "අ.ජා.කෙ. ලිපිනය",
+ "disable": "අබල කරන්න",
+ "disabled_dhcp": "ග.ධා.වි.කෙ. සේවාදායකය අබල කෙරිණි",
+ "disallow_this_client": "මෙම අනුග්රාහකයට ඉඩ නොදෙන්න",
+ "dns_access_settings_title": "ප්රවේශ සැකසුම්",
+ "dns_allowed_clients": "ඉඩ දෙන අනුග්රාහක",
+ "dns_blocking_mode": "අවහිර කරන ආකාරය",
+ "dns_blocking_mode_ipv4_label": "IPv4 අවහිර කිරීම",
+ "dns_blocking_mode_ipv6_label": "IPv6 අවහිර කිරීම",
+ "dns_blocking_mode_nxdomain": "නොපවතින වසම",
+ "dns_blocking_mode_refused": "REFUSED",
+ "dns_blocking_mode_title": "අවහිර කරන ආකාරය",
+ "dns_bootstrap_dns_placeholder": "අ.ජා.කෙ. (IP) ලිපින",
+ "dns_bootstrap_dns_title": "Bootstrap DNS සේවාදායකය",
+ "dns_bootstrap_servers": "Bootstrap DNS සේවාදායකය",
+ "dns_cache_config": "ව.නා.ප. නිහිත වින්යාසය",
+ "dns_cache_size": "නිහිතයෙහි ප්රමාණය",
+ "dns_cache_size_label": "ව.නා.ප. නිහිතයේ ප්රමාණය (බයිට වලින්)",
+ "dns_cache_size_title": "නිහිතයෙහි ප්රමාණය",
+ "dns_clear_cache": "නිහිතය මකන්න",
+ "dns_clear_cache_cancel": "අවලංගු",
+ "dns_config": "ව.නා.ප. සේවාදායක වින්යාසය",
+ "dns_disallowed_clients": "ඉඩ නොදෙන අනුග්රාහක",
+ "dns_disallowed_clients_title": "ඉඩ නොදෙන අනුග්රාහක",
+ "dns_disallowed_domains": "ඉඩ නොදෙන වසම්",
+ "dns_disallowed_domains_title": "ඉඩ නොදෙන වසම්",
+ "dns_fallback_dns_title": "සමාන්ය DNS සේවාදායක",
+ "dns_fallback_servers": "සමාන්ය DNS සේවාදායක",
+ "dns_optimistic_caching": "සර්වශුභවාදී නිහිතගතය",
+ "dns_override_max_ttl": "උපරිම පව. කා. අභිබවන්න",
+ "dns_override_max_ttl_title": "උපරිම පව. කා. අභිබවන්න",
+ "dns_override_min_ttl": "අවම පව. කා. අභිබවන්න",
+ "dns_override_min_ttl_title": "අවම පව. කා. අභිබවන්න",
+ "dns_privacy": "ව.නා.ප. රහස්යතා",
+ "dns_private_reverse_servers_title": "පෞද්ගලික ප්රතිවර්ත ව.නා.ප. සේවාදායක",
+ "dns_rate_limit": "අනුපාත සීමාව",
+ "dns_rate_limit_allowlist_placeholder": "අ.ජා.කෙ. (IP) ලිපින",
+ "dns_rate_limit_placeholder": "අනුපාත සීමාව ඇතුල් කරන්න",
+ "dns_rate_limit_title": "අනුපාත සීමාව",
+ "dns_rewrites": "ව.නා.ප. නැවත ලිවීම්",
+ "dns_server_config_title": "ව.නා.ප. සේවාදායක වින්යාසය",
+ "dns_test_upstreams": "අත්හදා බලන්න",
+ "dns_upstream_servers_title": "Upstream ව.නා.ප. සේවාදායක",
+ "dns_upstream_timeout": "උඩුගත කරන සේවාදායකයේ කාල සීමාව",
+ "dns_upstream_timeout_title": "උඩුගත කරන සේවාදායකයේ කාල සීමාව",
+ "domain": "වසම",
+ "domain_or_client": "වසම හෝ අනුග්රාහකය",
+ "download_mobileconfig": "වින්යාසගත ගොනුව බාගන්න",
+ "edit_table_action": "සංස්කරණය",
+ "enabled_dhcp": "ග.ධා.වි.කෙ. සේවාදායකය සබල කෙරිණි",
+ "encryption_certificate_path": "සහතිකයේ මාර්ගය",
+ "encryption_certificates": "සහතික",
+ "encryption_certificates_input": "ඔබගේ PEM-කේතනය කළ සහතික පිටපත් කර මෙහි අලවන්න.",
+ "encryption_certificates_source_content": "සහතිකවල අන්තර්ගත අලවන්න",
+ "encryption_certificates_source_path": "සහතික ගොනුවක මාර්ගය සකසන්න",
+ "encryption_chain_invalid": "සහතික දාමය වලංගු නොවේ",
+ "encryption_chain_valid": "සහතික දාමය වලංගු ය",
+ "encryption_config_saved": "සංකේතන වින්යාසය සුරකින ලදි",
+ "encryption_doq": "QUIC-මගින්-ව.නා.ප. තොට",
+ "encryption_dot": "TLS-මගින්-ව.නා.ප. තොට",
+ "encryption_https": "HTTPS තොට",
+ "encryption_key": "පුද්ගලික යතුර",
+ "encryption_key_input": "ඔබගේ සහතිකය සඳහා PEM-කේතනය කළ පුද්ගලික යතුර පිටපත් කර මෙහි අලවන්න.",
+ "encryption_key_source_content": "පුද්ගලික යතුරෙහි අන්තර්ගත අලවන්න",
+ "encryption_plain_dns": "සරල ව.නා.ප.",
+ "encryption_plain_dns_error": "ප්ලේන් DNS අබල කිරීමට, අනිවාර්ය වශයෙන් කුමක් හෝ සංකේතාත්මක DNS කෙටුම්පතක් සක්රීය කරන්න.",
+ "encryption_private_key_path": "පුද්ගලික යතුරෙහි මාර්ගය",
+ "encryption_server_enter": "වසමේ නම ඇතුල් කරන්න",
+ "encryption_title": "සංකේතනය",
+ "enter_cache_size": "ව.නා.ප. නිහිතයෙහි ප්රමාණය යොදන්න (බයිට)",
+ "fallback_dns": "සමාන්ය DNS සේවාදායක",
+ "filter_category_general_desc": "බොහෝ උපාංගවල ලුහුබැඳීම් සහ දැන්වීම් අවහිර කරන ලැයිස්තු",
+ "filter_category_other_desc": "වෙනත් අවහිර කිරීමේ ලැයිස්තු",
+ "filter_category_regional": "ප්රාදේශීය",
+ "filter_category_regional_desc": "ප්රාදේශීය දැන්වීම් සහ ලුහුබැඳීමේ සේවාදායක කෙරෙහි අවධානය යොමු කරන ලැයිස්තු",
+ "filter_category_security_desc": "ද්වේෂසහගත, තතුබෑම් සහ වංචනික වසම් අවහිර කිරීමට නිර්මාණය කළ ලැයිස්තු",
+ "filters": "පෙරහන්",
+ "forgot_password": "මුරපදය අමතක වුණා ද?",
+ "form_enter_blocked_response_ttl": "අසැක කරන මගුලකෙන් කාලය ඇතුලත් කරන්න (තත්පර)",
+ "form_error_client_id_format": "අනුග්රාහකයේ හැඳු. වලංගු නොවේ",
+ "form_error_domain_format": "වලංගු නොවන වසම් ආකෘතියකි",
+ "form_error_equal": "සමාන නොවිය යුතුය",
+ "form_error_gateway_ip": "කලාවලට ගේට්වේ IP ලිපිනයක් නොඑන්න",
+ "form_error_ip4_format": "IPv4 ලිපිනය වලංගු නොවේ",
+ "form_error_ip6_format": "වලංගු නොවන IPv6 ලිපිනයකි",
+ "form_error_mac_format": "මා.ප්ර.පා. ලිපිනය වලංගු නොවේ",
+ "form_error_password": "මුරපදය නොගැළපේ",
+ "form_error_port": "වලංගු තොටක අගයක් යොදන්න",
+ "form_error_port_range": "80-65535 පරාසය හි තොටක අගයක් ඇතුල් කරන්න",
+ "form_error_port_unsafe": "මෙය අනාරක්ෂිත තොටකි",
+ "form_error_server_name": "සේවාදායකයේ නම වලංගු නොවේ",
+ "friday": "සිකුරාදා",
+ "gateway_or_subnet_invalid": "උපජාල කාණ්ඩය වලංගු නොවේ",
+ "general_statistics": "පොදු සංඛ්යාලේඛන",
+ "ignore_domains_title": "නොසලකන වසම්",
+ "inactivity_schedule_add": "කාලසටහන එක් කරන්න",
+ "inactivity_schedule_all_day": "දවස පුරාම",
+ "inactivity_schedule_edit": "කාලසටහන සංස්කරණය කරන්න",
+ "install_auth_password": "මුරපදය",
+ "install_auth_username": "පරිශීලක නාමය",
+ "install_auth_username_enter": "පරිශ්රීලක නාමය යොදන්න",
+ "install_saved": "සාර්ථකව සුරකින ලදි",
+ "install_settings_all_interfaces": "සියළුම අතුරුමුහුණත්",
+ "install_settings_port": "කෙවෙනිය",
+ "install_static_error": "මෙම ජාල අතුරුමුහුණත සඳහා ඇඩ්ගාර්ඩ් හෝම් හට එය ස්වයංක්රීයව වින්යාසගත කිරීමට නොහැකිය. මෙය අතින් කරන්නේ කෙසේද යන්න පිළිබඳ උපදෙස් සොයා ගන්න.",
+ "install_static_ok": "සුභ තොරතුරක්! ස්ථිතික අන්තර්ජාල කෙටුම්පත් (IP) ලිපිනය දැනටමත් වින්යාසගත කර ඇත",
+ "install_step": "පියවර",
+ "install_submit_title": "සුභ පැතුම්!",
+ "ip_address": "අ.ජා.කෙ. ලිපිනය",
+ "known_tracker": "දැනුවත් ලුහුබැඳීමකි",
+ "language": "භාෂාව",
+ "light_theme": "දීප්ත තේමාව",
+ "make_static": "ස්ථිතික කරන්න",
+ "milliseconds_abbreviation": "මිලි තත්.",
+ "monday": "සඳුදා",
+ "nothing_found": "කිසිවක් හමු නොවිණි",
+ "parental_control": "දෙමාපිය පාලනය",
+ "parental_group_development": "මෘදුකාංග සංවර්ධනය",
+ "parental_group_gaming": "ක්රීඩා කිරීම",
+ "parental_group_hosting": "වෙබ් සත්කාරකත්වය",
+ "parental_group_privacy": "රහස්යතා මෙවලම්",
+ "parental_group_software": "මෘදුකාංග සංවර්ධනය",
+ "password_label": "මුරපදය",
+ "plain_dns": "සරල ව.නා.ප.",
+ "query_log_blocked_services": "අවහිර කළ සේවා",
+ "query_log_custom_filtering_rules": "අභිරුචි පෙරීමේ නීති",
+ "query_log_nothing_available": "කිසිවක් හමු නොවිණි",
+ "query_log_rewritten": "නැවත ලියන ලද",
+ "query_log_strict_search": "ඉතා නිවැරදිව සෙවීමට ද්විත්ව උද්ධෘතය භාවිතා කරන්න",
+ "rate_limit_subnet_len_ipv4_error": "IPv4 අඩවියේ ප්රතිමා අකුණු දිග 0 සහ 32 අතර විය යුතුය",
+ "rate_limit_subnet_len_ipv6_error": "IPv6 අඩවියේ ප්රතිමා අකුණු දිග 0 සහ 128 අතර විය යුතුය",
+ "refresh_btn": "නැවුම් කරන්න",
+ "refresh_statics": "සංඛ්යාලේඛන නැවුම් කරන්න",
+ "remove": "ඉවත් කරන්න",
+ "request_table_header": "ඉල්ලීම",
+ "reset_settings": "සැකසුම් යළි පිහිටුවන්න",
+ "response_details": "ප්රතිචාරයෙහි විස්තර",
+ "response_time": "ප්රතිචාර කාලය",
+ "rewrite_add": "ව.නා.ප. නැවත ලිවීමක් යොදන්න",
+ "rewrite_edit": "ව.නා.ප. නැවත ලිවීම සංස්කරණය",
+ "rewrite_hosts_applied": "සත්කාරක ගොනුවක නීතියකින් නැවත ලියා ඇත",
+ "rewrite_updated": "ව.නා.ප. නැවත ලිවීම සාර්ථකව යාවත්කාලීන කෙරිණි",
+ "rewritten": "නැවත ලියන ලද",
+ "safe_browsing": "ආරක්ෂිත පිරික්සුම",
+ "saturday": "සෙනසුරාදා",
+ "save": "සුරකින්න",
+ "save_btn": "සුරකින්න",
+ "search_placeholder": "සොයන්න",
+ "server_config_blocking_mode": "අවහිර කරන ආකාරය",
+ "server_config_blocking_mode_ipv4": "IPv4 අවහිර කිරීම",
+ "server_config_blocking_mode_ipv6": "IPv6 අවහිර කිරීම",
+ "server_config_disable_ipv6": "IPv6 ලිපින විසඳීම අබල කරන්න",
+ "server_config_dnssec_enable": "DNSSEC සබල කරන්න",
+ "server_config_edns_enable": "EDNS අනුග්රාහක අනුජාලය සබල කරන්න",
+ "server_config_nxdomain": "නොපවතින වසම",
+ "server_config_rate_limit_placeholder": "අනුපාත සීමාව ඇතුල් කරන්න",
+ "server_config_rate_limit_whitelist_faq": "අ.ජා.කෙ. ලිපින (IP) අනුපාත සීමාවෙන් බැහැර කරයි.",
+ "server_config_refused": "REFUSED",
+ "server_config_subnet_len_ipv4": "IPv4 ලිපින සදහා උපසර්න පෙරහන් දිග",
+ "server_config_subnet_len_ipv4_faq": "සමත් කරණය සීමා කරයින් සදහන් කිරීම සඳහා, IPv4 ලිපින සඳහා උපසර්න පෙරහන් දිග. පෙරහනේ පදනම 24 වේ",
+ "server_config_subnet_len_ipv6": "IPv6 ලිපින සඳහා අඩවිය අකුණු දිග",
+ "server_config_subnet_len_ipv6_faq": "අඩවිය අකුණු දිගක් සමඟ අවම සීමා කිරීමට භාවිතා කරන IPv6 ලිපින සඳහා. ප්රතිමන් 56 වනු ඇත",
+ "set_static_ip": "ස්ථිතික අ.ජා.කෙ. (IP) ලිපිනයක් සකසන්න",
+ "settings": "සැකසුම්",
+ "settings_custom": "අභිරුචි",
+ "settings_global": "ගෝලීය",
+ "settings_parental_control": "දෙමාපිය පාලකය",
+ "settings_statistics_retention": "සංඛ්යාලේඛන රඳවා තබා ගැනීම",
+ "setup_devices_router_title": "මාර්ගකාරකය",
+ "setup_dns_privacy_ioc_mac": "අයිඕඑස් සහ මැක්ඕඑස් වින්යාසය",
+ "setup_dns_privacy_other_title": "වෙනත් ක්රියාවට නැංවූ දෑ",
+ "stats_adult": "අවහිර කළ වැඩිහිටි වියමන අඩවි",
+ "stats_query_domain": "ප්රචලිත විමසන ලද වසම්",
+ "status_table_header": "තත්වය",
+ "sunday": "ඉරිදා",
+ "system_host_files": "පද්ධතියේ සත්කාරක ගොනු",
+ "table_client": "අනුග්රාහකය",
+ "tags_title": "අනන්යන",
+ "test_upstreams": "අත්හදා බලන්න",
+ "thursday": "බ්රහස්පතින්දා",
+ "time_table_header": "වේලාව",
+ "top_blocked_domains": "ප්රචලිත අවහිර කළ වසම්",
+ "top_clients": "ප්රචලිත අනුග්රාහක",
+ "top_upstreams": "නිෂ්පාදන සම්පුර්ණ",
+ "total": "එකතුව",
+ "try_again": "යළි උත්සාහය",
+ "tuesday": "අඟහරුවාදා",
+ "unavailable_dhcp": "ග.ධා.වි.කෙ. නැත",
+ "unavailable_dhcp_desc": "ඇඩ්ගාර්ඩ් හෝම් හට ඔබගේ මෙහෙයුම් පද්ධතියේ ග.ධා.වි.කෙ. සේවාදායකයක් ධාවනය කිරීමට නොහැකිය",
+ "unblock": "අනවහිර",
+ "update_filters_disable": "අබල කරන්න",
+ "update_filters_interval_custom": "අභිරුචි",
+ "updates_checked": "ඇඩ්ගාර්ඩ් හෝම් හි නව අනුවාදයක් තිබේ",
+ "updates_version_equal": "ඇඩ්ගාර්ඩ් හෝම් යාවත්කාලීනයි",
+ "upstream": "උඩුගත",
+ "upstream_dns": "Upstream ව.නා.ප. සේවාදායක",
+ "upstream_dns_fastest_addr": "වේගවත්ම අන්තර්ජාල කෙටුම්පතක (IP) ලිපිනය",
+ "upstream_dns_parallel_requests": "සමාන්තර ඉල්ලීම්",
+ "upstream_dns_servers_title": "Upstream ව.නා.ප. සේවාදායක",
+ "upstream_examples_title": "උදාහරණ",
+ "upstream_private_ptr_resolvers_title": "පෞද්. ප්රතිවර්ත ව.නා.ප. පිළිවිසඳු භාවිතය",
+ "upstreams": "උඩුගත ධාරාව",
+ "user_rules_check_button": "පරීක්ෂාව",
+ "user_rules_check_client_placeholder": "අනුග්රාහකයේ හඳුන්වනය ලියන්න",
+ "user_rules_check_hostname_label": "සත්කාරක හෝ වසම් නාමය",
+ "user_rules_disable_filter": "පෙරීම අබල කර ඇත",
+ "user_rules_dns_record_type_placeholder": "ව.නා.ප. වාර්තා වර්ගය තෝරන්න",
+ "user_rules_edit_dns_rewrite": "ව.නා.ප. නැවත ලිවීම සංස්කරණය",
+ "user_rules_rewrite_rule_is_applied": "නැවත ලිවීමේ නීතිය යොදා ඇත",
+ "username_label": "පරිශීලක නාමය",
+ "username_placeholder": "පරිශ්රීලක නාමය යොදන්න",
+ "validated_with_dnssec": "DNSSEC සමඟ වලංගු කෙරිණි",
+ "wednesday": "බදාදා",
+ "whois": "Whois"
+}
diff --git a/client_v2/src/__locales/sk.json b/client_v2/src/__locales/sk.json
new file mode 100644
index 00000000000..314c61a501d
--- /dev/null
+++ b/client_v2/src/__locales/sk.json
@@ -0,0 +1,864 @@
+{
+ "access_settings_allowed_faq": "Zoznam CIDR, IP adries alebo ClientID . Ak toto pole obsahuje záznamy, AdGuard Home bude akceptovať dopyty iba od týchto klientov",
+ "access_settings_allowed_placeholder": "Zadajte povolených klientov",
+ "access_settings_allowed_title": "Povolení klienti, jeden na riadok",
+ "access_settings_blocked_faq_1": "AdGuard Home bude ignorovať DNS dopyty, ktoré zodpovedajú týmto doménam. Takéto dotazy sa nezobrazujú v protokole dotazov. Môžete zadať presné názvy domén, zástupné znaky alebo pravidlá filtrovania URL, napríklad example.org, *.example.org alebo ||example.org^",
+ "access_settings_blocked_faq_2": "Nesmie sa zamieňať s filtrami",
+ "access_settings_blocked_placeholder": "Zadajte nepovolené domény",
+ "access_settings_blocked_title": "Zakázané domény, jedna na riadok",
+ "access_settings_disallowed_faq": "Zoznam CIDR, IP adries alebo ClientID . Ak toto pole obsahuje položky, AdGuard Home zamietne požiadavky od týchto klientov. Toto pole sa ignoruje, ak sú v poli Povolení klienti položky",
+ "access_settings_disallowed_placeholder": "Zadajte nepovolených klientov",
+ "access_settings_disallowed_title": "Zakázaní klienti, jeden na riadok",
+ "access_settings_saved_toast": "Nastavenia prístupu boli uložené",
+ "access_settings_title": "Nastavenia prístupu",
+ "actions_table_header": "Akcie",
+ "add": "Pridať",
+ "add_allowlist": "Pridať zoznam povolených DNS",
+ "add_blocklist": "Pridať zoznam blokovaných DNS",
+ "add_persistent_client": "Pridať ako trvalého klienta",
+ "add_to_allowlist": "Pridať do zoznamu povolení",
+ "ads_blocked": "Zablokovaných reklám",
+ "ads_blocked_card": "Zablokovaných reklám",
+ "ads_blocked_tooltip": "Počet DNS dopytov blokovaných filtrami reklám a zoznamom hostov",
+ "adult_websites_blocked": "Blokované webové stránky pre dospelých",
+ "adult_websites_blocked_tooltip": "Počet zablokovaných stránok pre dospelých",
+ "all_domains": "Všetky domény",
+ "all_lists_up_to_date_toast": "Všetky zoznamy sú už aktuálne",
+ "all_rewrites_disabled": "Všetky vlastné odpovede DNS budú zakázané",
+ "all_rewrites_enabled": "Všetky vlastné odpovede DNS budú povolené",
+ "allow_service": "Povoliť službu",
+ "allowed": "Obsiahnuté v bielej listine",
+ "allowlist_add": "Pridať zoznam povolených DNS",
+ "allowlist_edit": "Upraviť zoznam povolených DNS",
+ "allowlist_empty": "Zatiaľ tu nie sú žiadne zoznamy povolených položiek — pridajte svoj prvý ",
+ "allowlist_placeholder_example": "Môj zoznam povolených položiek",
+ "allowlist_remove": "Odstrániť zoznam povolených položiek?",
+ "allowlist_remove_desc": "%value% bude odstránené",
+ "allowlists": "Zoznamy povolených položiek",
+ "allowlists_title": "Zoznamy povolených položiek",
+ "apply": "Použiť",
+ "aria_clear_input": "Vymazať vstup",
+ "autofix_warning_list": "Vykoná tieto úlohy:
Deaktivuje systémový DNSStubListener
Nastaví adresu DNS servera na 127.0.0.1
Nahradí cieľ symbolického odkazu /etc/resolv.conf súborom /run/systemd/resolve/resolv.conf
Zastaví DNSStubListener (znovu načíta službu systemd-resolved)
",
+ "autofix_warning_result": "Výsledkom bude, že všetky DNS dopyty z Vášho systému budú štandardne spracované službou AdGuard Home.",
+ "autofix_warning_text": "Ak kliknete na „Opraviť“, AdGuardHome nakonfiguruje Váš systém tak, aby používal DNS server AdGuardHome.",
+ "average_time_processing": "Priemerný čas spracovania",
+ "average_time_processing_tooltip": "Priemerný čas spracovania DNS dopytu v milisekundách",
+ "average_upstream_response_time": "Priemerný čas odozvy upstreamu",
+ "back": "Naspäť",
+ "block": "Blokovať",
+ "block_client": "Zablokovať klienta",
+ "block_for_this_client_only": "Blokovať len pre tohto klienta",
+ "blocked": "Zablokované",
+ "blocked_adult_websites": "Zablokované rodičovskou kontrolou",
+ "blocked_by_cname_or_ip": "Blokované pomocou CNAME alebo IP",
+ "blocked_queries": "Blokované dopyty",
+ "blocked_service": "Blokované služby",
+ "blocked_services": "Blokované služby",
+ "blocked_services_desc": "Vyberte si populárne webové stránky a služby, ktoré by mali byť zablokované",
+ "blocked_threats": "Zablokované hrozby",
+ "blocked_threats_chart": "Zablokované hrozby",
+ "blocked_total": "Celkovo zablokované: %value%",
+ "blocklist_add_from_list": "Pridať zo zoznamu",
+ "blocklist_add_manual": "Pridať manuálne",
+ "blocklist_contents": "Obsah (.%value%)",
+ "blocklist_edit": "Upraviť zoznam blokovaných DNS",
+ "blocklist_homepage": "Domovská stránka",
+ "blocklist_placeholder_example": "Môj zoznam blokovaných položiek",
+ "blocklist_remove": "Odstrániť zoznam blokovaných položiek?",
+ "blocklist_remove_desc": "%value% bude odstránené",
+ "blocklist_url_file_path": "URL adresa alebo cesta k súboru",
+ "blocklists_add": "Pridať zoznamy blokovaných položiek",
+ "blocklists_add_desc": "Zoznam blokovaných položiek si môžete vybrať zo zoznamu alebo ich pridať manuálne",
+ "blocklists_desc": "Domény z týchto zoznamov sú blokované",
+ "blocklists_empty": "Zatiaľ tu nie sú žiadne zoznamy blokovaných položiek — pridajte svoj prvý ",
+ "blocklists_title": "Zoznamy blokovaných položiek",
+ "bootstrap_dns": "Bootstrap DNS servery",
+ "browse": "Vyhľadať",
+ "cache_cleared": "Vyrovnávacia pamäť DNS bola úspešne vymazaná",
+ "cache_config_clear": "Vymazať vyrovnávaciu pamäť",
+ "cache_config_max_ttl": "Prepísanie maximálnej hodnoty TTL v sekundách",
+ "cache_config_max_ttl_faq": "Maximálna hodnota doby životnosti (TTL) pre záznamy vo vyrovnávacej pamäti DNS",
+ "cache_config_max_ttl_placeholder": "Zadajte maximálnu hodnotu TTL",
+ "cache_config_min_ttl": "Prepísanie minimálnej hodnoty TTL v sekundách",
+ "cache_config_min_ttl_faq": "Minimálna hodnota doby životnosti (TTL) pre záznamy vo vyrovnávacej pamäti DNS",
+ "cache_config_min_ttl_placeholder": "Zadajte minimálnu hodnotu TTL",
+ "cache_config_optimistic": "Optimistické nastavenie",
+ "cache_config_optimistic_desc": "AdGuard Home bude odpovedať z vyrovnávacej pamäte, aj keď platnosť záznamov vypršala, a pokúsi sa ich aktualizovať",
+ "cache_config_size": "Veľkosť vyrovnávacej pamäte v bajtoch",
+ "cache_config_size_faq": "Veľkosť vyrovnávacej pamäte DNS. Ak chcete vypnúť ukladanie do vyrovnávacej pamäte, nechajte pole prázdne",
+ "cache_config_size_validation": "Veľkosť vyrovnávacej pamäte musí byť po povolení väčšia ako nula",
+ "cache_config_ttl_validation": "Minimálna hodnota TTL cache musí byť menšia alebo rovná maximálnej hodnote",
+ "cache_confirm_clear_desc": "Týmto sa odstránia všetky odpovede DNS z vyrovnávacej pamäte. Následné dotazy DNS môžu byť pomalšie, kým sa vyrovnávacia pamäť znovu nevytvorí.",
+ "cache_confirm_clear_title": "Vymazať vyrovnávaciu pamäť",
+ "cache_enabled": "Povoliť vyrovnávaciu pamäť",
+ "cache_enabled_desc": "Ukladať DNS odpovede lokálne",
+ "cancel": "Zrušiť",
+ "cancel_btn": "Zrušiť",
+ "category_tooltip": "Kategória",
+ "changes_saved_success": "Zmeny uložené",
+ "check_dhcp_servers": "Skontrolovať DHCP servery",
+ "check_not_found": "Nenašlo sa vo Vašom zozname filtrov",
+ "check_updates_btn": "Skontrolovať aktualizácie",
+ "clear_btn": "Vymazať",
+ "clear_query_log": "Vymazať denník dopytov",
+ "client_add": "Pridať klienta",
+ "client_added": "Klient bol pridaný",
+ "client_already_blocked": "Klient %ip% je už zablokovaný",
+ "client_blocked_flash": "Klient bol zablokovaný",
+ "client_details": "Podrobnosti klienta",
+ "client_id": "ID klienta",
+ "client_id_faq": "Klienti môžu byť identifikovaní podľa svojho ClientID. Ak sa chcete dozvedieť viac, navštívte AdGuard Home GitHub ",
+ "client_id_placeholder": "Zadať ID klienta",
+ "client_identifier": "Identifikátor",
+ "client_ip": "ID klienta",
+ "client_location": "Lokalita klienta",
+ "client_removed": "Klient bol odstránený",
+ "client_settings": "Nastavenie klienta",
+ "client_table_header": "Klient",
+ "client_unblocked_flash": "Klient bol odblokovaný",
+ "clients": "Klienti",
+ "clients_add": "Pridať klienta",
+ "clients_add_identifier": "Pridajte identifikátor",
+ "clients_add_name": "Meno klienta",
+ "clients_add_placeholder": "Zadajte meno klienta",
+ "clients_desc": "Pravidlá pre zariadenia pripojené k AdGuard Home",
+ "clients_dns_cache_size": "Veľkosť cache pamäte DNS v bajtoch",
+ "clients_dns_cache_size_placeholder": "Použiť čísla",
+ "clients_dont_collect_stats": "Nezhromažďovať štatistiky pre tohto klienta",
+ "clients_dont_log": "Nezaznamenávať tohto klienta",
+ "clients_edit": "Upraviť klienta",
+ "clients_identifier_already_used": "Tento identifikátor sa už používa",
+ "clients_identifier_format_error": "Používajte čísla, malé anglické písmená alebo pomlčky",
+ "clients_identifiers": "Identifikátory",
+ "clients_identifiers_desc": "Môžete použiť IP adresu klienta, CIDR, MAC adresu alebo ClientID (pre DoT, DoH a DoQ). Zistite viac ",
+ "clients_logs_and_statistics": "Denníky a štatistiky",
+ "clients_not_found": "Neboli nájdení žiadni klienti",
+ "clients_protection": "Ochrana",
+ "clients_protection_desc": "Povoliť alebo zakázať funkcie ochrany, protokolovanie a štatistiky",
+ "clients_remove_desc": "%value% bude odstránený z trvalých klientov spolu s jeho nastaveniami",
+ "clients_remove_title": "Odstrániť klienta?",
+ "clients_tags": "Tagy",
+ "clients_tags_desc": "Pre tohto klienta môžete pridať značky a zahrnúť ich do pravidiel filtrovania. Zistite viac ",
+ "clients_upstreams_desc": "Nadradené DNS servery, jeden na riadok",
+ "clients_use_dns_cache": "Použiť vyrovnávaciu pamäť DNS",
+ "clients_use_global_settings": "Použiť globálne nastavenia",
+ "clients_use_global_settings_desc": "Zdediť nastavenia ochrany z Vášho účtu AdGuard Home",
+ "close": "Zavrieť",
+ "close_result": "Tesný výsledok",
+ "confirm": "Potvrdiť",
+ "confirm_client_block_desc": "AdGuard Home bude ignorovať všetky požiadavky na DNS od klienta %ip% ",
+ "confirm_client_block_title": "Zablokovať klienta?",
+ "confirm_client_unblock_desc": "DNS požiadavky od klienta %ip% budú povolené",
+ "confirm_client_unblock_title": "Odblokovať klienta?",
+ "confirm_static_ip": "AdGuard Home nakonfiguruje %ip% ako Vašu statickú IP adresu. Chcete pokračovať?",
+ "copied": "Skopírované do pamäťovej schránky",
+ "copy": "Kopírovať",
+ "copy_to_clipboard": "Skopírovať do pamäťovej schránky",
+ "custom_filtering_rules": "Vlastné filtračné pravidlá",
+ "dark_theme": "Tmavá téma",
+ "dashboard": "Riadiaci panel",
+ "delete_btn": "Vymazať",
+ "delete_confirm": "Odstrániť nájom?",
+ "delete_confirm_desc": "Týmto sa uvoľní %ip% ",
+ "delete_table_action": "Vymazať",
+ "delete_table_action_confirm": "Áno, odstrániť",
+ "device_type": "Typ zariadenia",
+ "dhcp_add_static_lease": "Pridať statický prenájom",
+ "dhcp_config_saved": "Konfigurácia DHCP servera bola úspešne uložená",
+ "dhcp_edit_static_lease": "Upraviť statický prenájom",
+ "dhcp_error": "Nepodarilo sa skontrolovať DHCP server. Pred opätovným pokusom chvíľu počkajte",
+ "dhcp_form_gateway_address": "IP adresa brány",
+ "dhcp_form_gateway_input": "IP brána",
+ "dhcp_form_range_start": "Statická IP adresa",
+ "dhcp_form_range_title": "Rozsah IP adries",
+ "dhcp_form_subnet_input": "Maska podsiete",
+ "dhcp_found": "DHCP server sa nedá aktivovať",
+ "dhcp_ipv4_settings": "Nastavenia DHCP IPv4",
+ "dhcp_ipv6_settings": "Nastavenia DHCP IPv6",
+ "dhcp_lease_added": "Statický prenájom \"%key%\" bol úspešne pridaný",
+ "dhcp_lease_deleted": "Statický prenájom \"%key%\" bol úspešne vymazaný",
+ "dhcp_lease_updated": "Statický prenájom \"%key%\" bol úspešne aktualizovaný",
+ "dhcp_leases_not_found": "Neboli nájdené žiadne DHCP prenájmy",
+ "dhcp_leases_title": "DHCP prenájom",
+ "dhcp_mac_address_already_added": "Táto MAC adresa je už pridaná",
+ "dhcp_new_static_lease": "Nový statický prenájom",
+ "dhcp_not_found": "DHCP server je aktívny",
+ "dhcp_reset": "Týmto sa vynuluje konfigurácia DHCP. Všetky zmeny sa stratia",
+ "dhcp_reset_leases": "Resetovať všetky prenájmy",
+ "dhcp_reset_leases_confirm": "Týmto sa vynulujú všetky prenájmy",
+ "dhcp_reset_leases_success": "DHCP prenájmy boli úspešne resetované",
+ "dhcp_settings": "Nastavenia DHCP",
+ "dhcp_static_ip_error": "DHCP server vyžaduje statickú IP adresu. Nepodarilo sa nám potvrdiť, že ju toto sieťové rozhranie má",
+ "dhcp_static_leases_not_found": "Nebol nájdený žiadny statický DHCP prenájom",
+ "dhcp_table_hostname": "Meno hostiteľa",
+ "dhcp_table_ip_address": "IP adresa",
+ "dhcp_table_mac_address": "MAC adresa",
+ "disable": "Vypnúť",
+ "disable_protection_btn": "Vypnúť ochranu",
+ "disabled_dhcp": "DHCP server vypnutý",
+ "disallow_client_confirm_allowlist_note": "Táto akcia tiež vylúči pravidlo %ip% zo zoznamu povolených klientov.",
+ "disallow_client_confirm_title": "Zakázať tohto klienta?",
+ "disallow_this_client": "Zablokovať tohto klienta",
+ "dns_access_settings_title": "Nastavenia prístupu",
+ "dns_allowed_clients": "Povolení klienti",
+ "dns_allowed_clients_label": "Povolení klienti, jeden na riadok",
+ "dns_allowed_clients_placeholder": "Zadajte povolených klientov",
+ "dns_blocking_mode": "Spôsob blokovania",
+ "dns_blocking_mode_custom_ip": "Vlastná IP adresa",
+ "dns_blocking_mode_default": "Predvolené",
+ "dns_blocking_mode_ipv4_label": "Blokovanie IPv4",
+ "dns_blocking_mode_ipv4_placeholder": "Zadajte IPv4 adresu",
+ "dns_blocking_mode_ipv6_label": "Blokovanie IPv6",
+ "dns_blocking_mode_ipv6_placeholder": "Zadajte IPv6 adresu",
+ "dns_blocking_mode_null_ip": "Nulová IP adresa",
+ "dns_blocking_mode_nxdomain": "NXDOMAIN",
+ "dns_blocking_mode_refused": "REFUSED",
+ "dns_blocking_mode_title": "Spôsob blokovania",
+ "dns_blocking_mode_ttl_label": "Blokovaná odozva TTL v sekundách",
+ "dns_bootstrap_dns_label": "Bootstrap DNS servery, jeden na riadok",
+ "dns_bootstrap_dns_placeholder": "IP adresy",
+ "dns_bootstrap_dns_title": "Bootstrap DNS servery",
+ "dns_bootstrap_servers": "Bootstrap DNS servery",
+ "dns_cache_config": "Konfigurácia DNS cache",
+ "dns_cache_configuration_saved_toast": "Konfigurácia vyrovnávacej pamäte DNS bola uložená",
+ "dns_cache_size": "Veľkosť cache",
+ "dns_cache_size_label": "Veľkosť cache pamäte DNS v bajtoch",
+ "dns_cache_size_title": "Veľkosť cache",
+ "dns_clear_cache": "Vymazať vyrovnávaciu pamäť",
+ "dns_clear_cache_cancel": "Zrušiť",
+ "dns_clear_cache_confirm": "Áno",
+ "dns_clear_cache_yes": "Áno",
+ "dns_config": "Konfigurácia DNS servera",
+ "dns_disallowed_clients": "Nepovolení klienti",
+ "dns_disallowed_clients_label": "Zakázaní klienti, jeden na riadok",
+ "dns_disallowed_clients_title": "Nepovolení klienti",
+ "dns_disallowed_domains": "Nepovolené domény",
+ "dns_disallowed_domains_label": "Zakázané domény, jedna na riadok",
+ "dns_disallowed_domains_title": "Nepovolené domény",
+ "dns_edns_custom_placeholder": "Zadajte IP adresu",
+ "dns_fallback_dns_label": "Záložné DNS servery, jeden na riadok",
+ "dns_fallback_dns_placeholder": "IP adresy, sdns://, quic://, https://, h3:// alebo tls://",
+ "dns_fallback_dns_title": "Záložné servery DNS",
+ "dns_fallback_servers": "Záložné servery DNS",
+ "dns_optimistic_caching": "Optimistické nastavenie",
+ "dns_over_https": "DNS-over-HTTPS",
+ "dns_over_quic": "DNS-over-Quic",
+ "dns_over_tls": "DNS-over-TLS",
+ "dns_override_max_ttl": "Prepísať maximálne TTL",
+ "dns_override_max_ttl_title": "Prepísať maximálne TTL",
+ "dns_override_min_ttl": "Prepísať minimálne TTL",
+ "dns_override_min_ttl_title": "Prepísať minimálne TTL",
+ "dns_privacy": "DNS súkromie",
+ "dns_private_reverse_servers_label": "Súkromné reverzné DNS servery, jeden na riadok",
+ "dns_private_reverse_servers_placeholder": "IP adresy, sdns://, quic://, https://, h3:// alebo tls://",
+ "dns_private_reverse_servers_title": "Súkromné reverzné DNS servery",
+ "dns_queries": "DNS dopyty",
+ "dns_queries_tooltip": "Počet DNS dopytov spracovaných za posledných 24 hodín",
+ "dns_query": "DNS dopyty",
+ "dns_rate_limit": "Rýchlostný limit",
+ "dns_rate_limit_allowlist": "Obmedzenie rýchlosti zoznamu povolených položiek",
+ "dns_rate_limit_allowlist_placeholder": "IP adresy",
+ "dns_rate_limit_allowlist_title": "Obmedzenie rýchlosti zoznamu povolených položiek",
+ "dns_rate_limit_placeholder": "Zadajte rýchlostný limit",
+ "dns_rate_limit_title": "Rýchlostný limit",
+ "dns_rewrites": "DNS prepisovanie",
+ "dns_server_addresses": "Adresy serverov",
+ "dns_server_addresses_label": "Adresy servera, jedna na riadok",
+ "dns_server_addresses_placeholder": "IP adresy, sdns://, quic://, https://, h3:// alebo tls://",
+ "dns_server_addresses_title": "Adresy serverov",
+ "dns_server_config_title": "Konfigurácia DNS servera",
+ "dns_server_configuration_saved_toast": "Konfigurácia DNS servera bola uložená",
+ "dns_settings": "DNS",
+ "dns_subnet_placeholder": "Zadajte dĺžku predpony",
+ "dns_test_upstreams": "Test upstreamov",
+ "dns_upstream_servers_title": "Upstream DNS servery",
+ "dns_upstream_timeout": "Časový limit pre upstream",
+ "dns_upstream_timeout_title": "Časový limit pre upstream",
+ "dns_upstream_validation_invalid": "Neplatný formát",
+ "domain": "Doména",
+ "domain_or_client": "Doména alebo klient",
+ "download_mobileconfig": "Stiahnuť konfiguračný súbor",
+ "dynamic_dhcp_leases_not_found": "Dynamické prenájmy DHCP sa nenašli",
+ "edit_table_action": "Upraviť",
+ "enable": "Zapnúť",
+ "enabled_dhcp": "DHCP server zapnutý",
+ "encrypted_dns_addresses": "Adresy šifrovaných DNS serverov",
+ "encryption_certificate_has_issues": "Certifikát má problémy",
+ "encryption_certificate_path": "Cesta k certifikátu",
+ "encryption_certificates": "Certifikáty",
+ "encryption_certificates_desc": "Ak chcete používať šifrovanie, musíte pre svoju doménu poskytnúť platný reťazec SSL certifikátov. Bezplatný SSL certifikát môžete získať z letsencrypt.org alebo si ho môžete kúpiť od dôveryhodnej certifikačnej autority",
+ "encryption_certificates_input": "Skopírujte alebo prilepte sem certifikáty vo formáte PEM.",
+ "encryption_certificates_source_content": "Vložte obsah certifikátu",
+ "encryption_certificates_source_path": "Nastavte cestu k súboru s certifikátom",
+ "encryption_chain_invalid": "Certifikačný reťazec je neplatný",
+ "encryption_chain_valid": "Certifikačný reťazec je platný",
+ "encryption_config_saved": "Konfigurácia šifrovania uložená",
+ "encryption_confirm_clear": "Obnoviť nastavenia šifrovania",
+ "encryption_confirm_clear_desc": "Všetky nastavenia šifrovania budú obnovené na predvolené hodnoty",
+ "encryption_disable_plain_dns": "Vypnúť obyčajné DNS?",
+ "encryption_disable_plain_dns_desc": "Zariadenia, ktoré nepodporujú šifrované DNS, nemusia fungovať správne",
+ "encryption_doq": "Port DNS-cez-QUIC",
+ "encryption_doq_tooltip": "Tento port slúži na spustenie servera DNS-over-QUIC",
+ "encryption_dot": "Port DNS-cez-TLS",
+ "encryption_dot_tooltip": "Tento port sa používa na spustenie servera DNS-over-TLS",
+ "encryption_encrypted_dns": "Šifrované DNS",
+ "encryption_encrypted_dns_desc": "Administrátorské rozhranie AdGuard Home používa HTTPS. DNS server podporuje DNS-over-HTTPS, DNS-over-TLS a DNS-over-QUIC",
+ "encryption_expire": "Vyprší: %value%",
+ "encryption_force_redirect": "Automaticky presmerovať z HTTP na HTTPS",
+ "encryption_hostnames": "Názvy hostiteľov: %value%",
+ "encryption_https": "HTTPS port",
+ "encryption_https_tooltip": "Tento port poskytuje prístup HTTPS pre administrátorské rozhranie AdGuard Home. Taktiež poskytuje DNS-over-HTTPS v lokalite „/dns-query“",
+ "encryption_issuer": "Vydavateľ: %value%",
+ "encryption_key": "Súkromný kľúč",
+ "encryption_key_input": "Skopírujte a prilepte sem svoj súkromný kľúč vo formáte PEM pre Váš certifikát.",
+ "encryption_key_invalid": "Súkromný kľúč je neplatný",
+ "encryption_key_source_content": "Vložte obsah privátneho kľúča",
+ "encryption_key_source_path": "Nastavenie súboru súkromného kľúča",
+ "encryption_key_type": "Šifrovací algoritmus: %value%",
+ "encryption_key_valid": "Súkromný kľúč je platný",
+ "encryption_plain_dns": "Obyčajné DNS",
+ "encryption_plain_dns_error": "Ak chcete vypnúť jednoduchý DNS protokol, zapnite aspoň jeden šifrovaný DNS protokol",
+ "encryption_private_key_path": "Cesta k súkromného kľúču",
+ "encryption_server": "Meno servera",
+ "encryption_server_enter": "Zadajte meno Vašej domény",
+ "encryption_server_tooltip_1": "Ak je nastavené, AdGuard Home bude detegovať ClientID, odpovedať na DDR dotazy a vykonávať ďalšie overenia pripojenia. Ak nie je nastavené, tieto funkcie sú vypnuté",
+ "encryption_server_tooltip_2": "Musí sa zhodovať s názvom DNS v certifikáte",
+ "encryption_subject": "Predmet: %value%",
+ "encryption_title": "Šifrovanie",
+ "enter_cache_size": "Zadať veľkosť cache (v bajtoch)",
+ "enter_ip_address_placeholder": "Zadajte IP adresu",
+ "error": "Chyba",
+ "fallback_dns": "Záložné servery DNS",
+ "filter_added_successfully": "Pridaný zoznam blokovaných položiek: %value% ",
+ "filter_added_successfully_allowlist": "Pridaný zoznam povolených položiek: %value% ",
+ "filter_added_successfully_more": "Pridané blokovacie zoznamy: %value% a ďalších %more%",
+ "filter_category_general": "General",
+ "filter_category_general_desc": "Zoznamy, ktoré blokujú sledovanie a reklamu na väčšine zariadení",
+ "filter_category_other": "Iné",
+ "filter_category_other_desc": "Iné blokovacie zoznamy",
+ "filter_category_regional": "Regionálne",
+ "filter_category_regional_desc": "Zoznamy zamerané na regionálne reklamy a sledovacie servery",
+ "filter_category_security": "Bezpečnosť",
+ "filter_category_security_desc": "Zoznamy určené špeciálne na blokovanie škodlivých, phishingových a podvodníckych domén",
+ "filter_removed_successfully": "Odstránený zoznam blokovaných položiek: %value% ",
+ "filter_removed_successfully_allowlist": "Odstránený zoznam povolených položiek: %value% ",
+ "filters": "Filtre",
+ "forgot_password": "Zabudnuté heslo?",
+ "forgot_password_list_item_1": "Vytvorte nové heslo pomocou nástroja htpasswd alebo akéhokoľvek online generátora BCrypt.",
+ "forgot_password_list_item_2": "Zastavte AdGuard Home.",
+ "forgot_password_list_item_3": "Otvorte AdGuardHome.yaml, nájdite pole hesla a nahraďte ho novou hodnotou.",
+ "forgot_password_list_item_4": "Spustite AdGuard Home.",
+ "forgot_password_list_item_5": "Prihláste sa do webového rozhrania pomocou svojho nového hesla.",
+ "forgot_password_list_title": "Ak chcete zmeniť svoje heslo:",
+ "forgot_password_page_desc": "AdGuard Home ukladá heslá vo forme hashov šifrovaných pomocou BCrypt",
+ "form_enter_blocked_response_ttl": "Zadajte TTL blokovanej odozve (sekundy)",
+ "form_enter_hostname": "Zadajte názov hostiteľa",
+ "form_enter_ip": "Zadajte IP adresu",
+ "form_enter_mac": "Zadajte MAC adresu",
+ "form_error_answer_format": "Neplatný formát výsledku",
+ "form_error_client_id_format": "ID klienta musí obsahovať iba čísla, malé písmená a spojovníky",
+ "form_error_domain_format": "Neplatný formát domény",
+ "form_error_equal": "Nesmie byť rovnaká",
+ "form_error_format": "Neplatný formát",
+ "form_error_gateway_ip": "Prenájom nemôže mať IP adresu brány",
+ "form_error_hostname_format": "Používajte čísla, malé anglické písmená alebo pomlčky",
+ "form_error_ip4_format": "Neplatná IPv4 adresa",
+ "form_error_ip6_format": "Neplatná IPv6 adresa",
+ "form_error_ip_already_added": "Táto IP adresa je už pridaná",
+ "form_error_ip_format": "Neplatná IP adresa",
+ "form_error_mac_already_added": "Táto MAC adresa je už pridaná",
+ "form_error_mac_format": "Neplatná MAC adresa",
+ "form_error_password": "Heslo sa nezhoduje",
+ "form_error_port": "Zadajte platné číslo portu",
+ "form_error_port_range": "Zadajte číslo portu v rozsahu 80-65535",
+ "form_error_port_unsafe": "Nezabezpečený port",
+ "form_error_range": "Hodnota musí byť medzi %min% a %max%",
+ "form_error_server_name": "Neplatné meno servera",
+ "form_error_url_format": "Zadajte platnú adresu URL alebo cestu k súboru",
+ "found_in_known_domains": "Nájdené v databáze známych domén",
+ "friday": "Piatok",
+ "gateway_or_subnet_invalid": "Maska podsiete je neplatná",
+ "general_statistics": "Všeobecná štatistika",
+ "home_dns_addresses": "Adresy DNS AdGuard Home",
+ "home_dns_addresses_desc": "DNS server AdGuard Home počúva na nasledujúcich rozhraniach/portoch:",
+ "ignore_domains_desc_log": "Požiadavky, ktoré zodpovedajú týmto pravidlám, sa nezobrazujú v denníku dopytov",
+ "ignore_domains_desc_stats": "Dopyty, ktoré spĺňajú tieto pravidlá, nie sú zahrnuté v štatistikách",
+ "ignore_domains_title": "Ignorované domény",
+ "inactivity_schedule": "Časový plán nečinnosti",
+ "inactivity_schedule_add": "Pridať časový plán",
+ "inactivity_schedule_all_day": "Celý deň",
+ "inactivity_schedule_delete": "Vymazať časový plán",
+ "inactivity_schedule_delete_desc": "Blokovanie služby bude aktívne celý deň v %value% ",
+ "inactivity_schedule_desc": "Nastavenie doby nečinnosti pre blokované služby",
+ "inactivity_schedule_edit": "Upraviť časový plán",
+ "inactivity_schedule_no_schedules": "Žiadny časový plán",
+ "inactivity_schedule_replace_desc": "Tento časový plán nahradí všetky existujúce plány pre ten istý deň v týždni. Každý deň môže mať iba jedno obdobie nečinnosti",
+ "inactivity_schedule_time_from": "Čas od",
+ "inactivity_schedule_time_to": "Čas do",
+ "inactivity_schedule_timezone": "Časové pásmo",
+ "install_auth_confirm": "Potvrdiť heslo",
+ "install_auth_password": "Heslo",
+ "install_auth_password_enter": "Zadajte heslo",
+ "install_auth_username": "Meno používateľa",
+ "install_auth_username_enter": "Zadajte meno používateľa",
+ "install_saved": "Úspešne uložené",
+ "install_settings_all_interfaces": "Všetky rozhrania",
+ "install_settings_port": "Port",
+ "install_static_configure": "AdGuard Home zistil, že sa používa dynamická IP adresa %ip%. Chcete ju nastaviť ako statickú adresu?",
+ "install_static_error": "AdGuard Home ho nemôže automaticky nakonfigurovať pre toto sieťové rozhranie. Vyhľadajte návod, ako to urobiť manuálne.",
+ "install_static_ok": "Dobré správy! Statická IP adresa je už nakonfigurovaná",
+ "install_step": "Krok",
+ "install_submit_title": "Gratulujeme!",
+ "instructions": "Inštrukcie",
+ "invalid": "Neplatný",
+ "ip_address": "IP adresa",
+ "ip_addresses_placeholder": "Zadajte IP adresy, jednu na riadok",
+ "known_tracker": "Známy sledovač",
+ "language": "Jazyk",
+ "last_days": "Posledný %count% deň | Posledné %count% dni | Posledných %count% dní",
+ "last_hour": "Posledná hodina",
+ "last_hours": "| Posledná %count% hodina | Posledné %count% hodiny | Posledných %count% hodín",
+ "last_updated_label": "Posledná aktualizácia",
+ "light_theme": "Svetlá téma",
+ "list_updated": "| %count% zoznam aktualizovaný | %count% zoznamy aktualizované | %count% zoznamov aktualizovaných",
+ "login": "Prihlásenie",
+ "logout": "Odhlásenie",
+ "make_static": "Vytvárať štatistiku",
+ "make_static_desc": "Táto akcia transformuje dynamickú adresu na statickú",
+ "milliseconds_abbreviation": "ms",
+ "monday": "Pondelok",
+ "name": "Meno",
+ "name_label": "Meno",
+ "name_table_header": "Meno",
+ "name_tooltip": "Meno",
+ "network_interface": "Sieťové rozhranie",
+ "next": "Ďalej",
+ "no": "Nie",
+ "no_stats_yet": "Zatiaľ žiadne štatistiky – na ich zobrazenie používajte AdGuard Home",
+ "none_text": "Žiadne",
+ "nothing_found": "Nič sa nenašlo",
+ "notify_undo": "Vrátiť",
+ "notify_updated": "Aktualizované",
+ "open_dashboard": "Otvoriť ovládací panel",
+ "parental_control": "Rodičovská kontrola",
+ "parental_group_ai": "Umelá inteligencia",
+ "parental_group_cdn": "CDN",
+ "parental_group_dating": "Zoznamka",
+ "parental_group_development": "Vývoj softvéru",
+ "parental_group_gambling": "Hazardné hry, stávkovanie",
+ "parental_group_gaming": "Hranie hier",
+ "parental_group_hosting": "Webhosting",
+ "parental_group_messaging": "Správy",
+ "parental_group_messenger": "Správy",
+ "parental_group_privacy": "Nástroje na ochranu súkromia",
+ "parental_group_shopping": "Nakupovanie",
+ "parental_group_social": "Sociálne siete",
+ "parental_group_social_network": "Sociálne siete",
+ "parental_group_software": "Vývoj softvéru",
+ "parental_group_streaming": "Streamovanie",
+ "password_label": "Heslo",
+ "password_placeholder": "Zadajte heslo",
+ "password_requirements": "Požiadavky na heslo",
+ "password_requirements_characters": "Minimálne 8 znakov",
+ "password_requirements_lowercase": "Aspoň 1 malé písmeno",
+ "password_requirements_match": "Heslá sa zhodujú",
+ "password_requirements_special": "Anglické písmená, čísla a špeciálne znaky",
+ "password_requirements_uppercase": "Aspoň 1 veľké písmeno",
+ "pause_for_hour": "Pauza na %count% hodinu",
+ "pause_for_minutes": "| Pozastaviť na %count% minútu | Pozastaviť na %count% minúty | Pozastaviť na %count% minút",
+ "pause_for_seconds": "| Pozastaviť na %count% sekundu | Pozastaviť na %count% sekundy | Pozastaviť na %count% sekúnd",
+ "pause_until_tomorrow": "Pozastaviť do zajtra, %time%",
+ "period_notify": "Ak chcete zmeniť zobrazované obdobia, upravte Uchovávanie štatistík vo Všeobecných nastaveniach ",
+ "plain_dns": "Obyčajné DNS",
+ "plain_dns_addresses": "Adresy nešifrovaných DNS serverov",
+ "port_53_faq_link": "Port 53 je často obsadený službami „DNSStubListener“ alebo „systemd-resolved“. Prečítajte si, prosím, tento návod , ako to vyriešiť.",
+ "port_in_use": "Tento port sa už používa",
+ "privacy_policy": "Pravidlá ochrany súkromia",
+ "processing_time_ms": "%value% ms",
+ "protection": "Ochrana",
+ "protocol": "Protokol",
+ "queries": "Dopyty",
+ "queries_total": "| %count% celkový počet dopytov | %count% celkový počet dopytov",
+ "query_details": "Podrobnosti dopytu",
+ "query_log": "Denník dopytov",
+ "query_log_all_reasons": "Všetky dôvody",
+ "query_log_all_statuses": "Všetky stavy",
+ "query_log_allowed": "Obsiahnuté v bielej listine",
+ "query_log_blocked": "Zablokované",
+ "query_log_blocked_by_filter": "Zablokované filtrom",
+ "query_log_blocked_by_parental_control": "Zablokované rodičovskou kontrolou",
+ "query_log_blocked_services": "Blokované služby",
+ "query_log_blocked_threats": "Zablokované hrozby",
+ "query_log_custom_filtering_rules": "Vlastné filtračné pravidlá",
+ "query_log_detail_address": "Adresa: %value% ",
+ "query_log_detail_category": "Kategória: %value% ",
+ "query_log_detail_country": "Krajina: %value% ",
+ "query_log_detail_date": "Dátum: %value% ",
+ "query_log_detail_dns_server": "DNS server: %value% ",
+ "query_log_detail_domain": "Doména: %value% ",
+ "query_log_detail_ecs": "ECS: %value% ",
+ "query_log_detail_elapsed": "Uplynulo: %value% ",
+ "query_log_detail_name": "Meno: %value% ",
+ "query_log_detail_network": "Sieť: %value% ",
+ "query_log_detail_original_response": "Pôvodná odpoveď:",
+ "query_log_detail_protocol": "Protokol: %value% ",
+ "query_log_detail_reason": "Dôvod:",
+ "query_log_detail_response": "Odpoveď:",
+ "query_log_detail_response_code": "Kód odpovede: %value% ",
+ "query_log_detail_rules": "Pravidlo(-á):",
+ "query_log_detail_served_from_cache": "Zobrazené z vyrovnávacej pamäte: %value% ",
+ "query_log_detail_service_name": "Názov služby: %value% ",
+ "query_log_detail_source": "Zdroj: %value% ",
+ "query_log_detail_status": "Stav: %value% ",
+ "query_log_detail_time": "Čas: %value% ",
+ "query_log_detail_type": "Typ: %value% ",
+ "query_log_nothing_available": "Nič sa nenašlo",
+ "query_log_processed": "Spracované",
+ "query_log_retention": "Rotácia denníkov",
+ "query_log_rewritten": "Prepísané",
+ "query_log_safe_search": "Bezpečné vyhľadávanie",
+ "query_log_strict_search": "Na prísne vyhľadávanie použite dvojité úvodzovky",
+ "rate_limit_subnet_len_ipv4_error": "Dĺžka prefixu podsiete IPv4 musí byť od 0 do 32",
+ "rate_limit_subnet_len_ipv6_error": "Dĺžka prefixu podsiete IPv6 musí byť od 0 do 128",
+ "reason_table_header": "Dôvod",
+ "refresh_btn": "Obnoviť",
+ "refresh_statics": "Obnoviť štatistiku",
+ "release_notes": "Poznámky k vydaniu",
+ "remove": "Odstrániť",
+ "remove_tag": "Odstrániť %value%",
+ "report_an_issue": "Nahlásiť problém",
+ "request_table_header": "Dopyt",
+ "requests_table_header": "Dopyty",
+ "reset": "Odhlásiť",
+ "reset_settings": "Obnoviť nastavenia",
+ "reset_settings_confirm": "Áno, resetovať",
+ "response_details": "Podrobnosti odpovede",
+ "response_time": "Čas odozvy",
+ "result": "Výsledok",
+ "resume_protection_timer": "Ochrana sa obnoví o %time%",
+ "rewrite_add": "Pridať DNS prepísanie",
+ "rewrite_domain_input_placeholder": "Zadajte názov domény alebo zástupný znak",
+ "rewrite_edit": "Upraviť prepísanie DNS",
+ "rewrite_hosts_applied": "Prepísané pravidlom súboru hostiteľov",
+ "rewrite_settings_updated": "Úspešná aktualizácia nastavení prepisovania DNS",
+ "rewrite_updated": "Prepísanie DNS bolo úspešne aktualizované",
+ "rewrites_answer_input_placeholder": "Zadajte IP adresu, záznam CNAME, A alebo AAAA",
+ "rewrites_empty": "Zatiaľ tu nie sú žiadne prepísania — pridajte svoje prvé ",
+ "rewrites_remove_desc": "Prepisovanie DNS pre %value% bude odstránené",
+ "rewrites_remove_title": "Odstrániť prepisovanie DNS?",
+ "rewrites_tooltip_examples_item1": "Prepísať DNS odpovede iba pre tento názov domény",
+ "rewrites_tooltip_examples_item2": "Prepísať odpovede DNS pre všetky subdomény example.org",
+ "rewrites_tooltip_instructions_item1": "Ak chcete priradiť doménové meno k IP adrese, zadajte adresu IPv4 pre záznamy typu A a adresu IPv6 pre záznamy typu AAAA",
+ "rewrites_tooltip_instructions_item2": "Ak chcete presmerovať názov domény, zadajte záznam CNAME cieľovej domény",
+ "rewrites_tooltip_instructions_item3": "Ak chcete uchovávať iba záznamy A, zadajte",
+ "rewrites_tooltip_instructions_item4": "Ak chcete uchovávať iba záznamy AAAAA, zadajte",
+ "rewritten": "Prepísané",
+ "rows_per_page": "Počet riadkov na stránku %value%",
+ "rules_label": "Pravidlá",
+ "safe_browsing": "Bezpečné prehliadanie",
+ "safe_search": "Bezpečné vyhľadávanie",
+ "safe_search_used": "Použité bezpečné vyhľadávanie",
+ "safe_search_used_tooltip": "Počet DNS dopytov do vyhľadávačov, pre ktoré bolo vynútené Bezpečné vyhľadávanie",
+ "saturday": "Sobota",
+ "save": "Uložiť",
+ "save_btn": "Uložiť",
+ "search_placeholder": "Hľadať",
+ "selected": "%value% vybraných",
+ "server_config_blocking_mode": "Spôsob blokovania",
+ "server_config_blocking_mode_custom_ip_desc": "Vlastná IP adresa: Odpovedať manuálne nastavenou IP adresou",
+ "server_config_blocking_mode_default_desc": "Predvolené: Odpovedať s 0.0.0.0 pre A a :: pre AAAA, ak je blokované pravidlom v štýle Adblock. Odpovedať s IP adresou uvedenou v pravidle, ak je blokované pravidlom v štýle /etc/hosts",
+ "server_config_blocking_mode_ipv4": "Blokovanie IPv4",
+ "server_config_blocking_mode_ipv4_faq": "IP adresa, ktorá sa má vrátiť pre zablokovanú požiadavku A",
+ "server_config_blocking_mode_ipv4_placeholder": "Zadajte IPv4 adresu",
+ "server_config_blocking_mode_ipv6": "Blokovanie IPv6",
+ "server_config_blocking_mode_ipv6_faq": "IP adresa, ktorá sa má vrátiť pre blokovanú požiadavku AAAA",
+ "server_config_blocking_mode_ipv6_placeholder": "Zadajte IPv6 adresu",
+ "server_config_blocking_mode_null_ip_desc": "Nulová IP adresa: Odpovedať s 0.0.0.0 pre A a :: pre AAAA",
+ "server_config_blocking_mode_nxdomain_desc": "NXDOMAIN: Odpovedať kódom NXDOMAIN",
+ "server_config_blocking_mode_refused_desc": "REFUSED: Odpovedať s kódom REFUSED",
+ "server_config_blocking_mode_ttl": "Blokovaná odozva TTL v sekundách",
+ "server_config_blocking_mode_ttl_faq": "Určuje, ako dlho majú klienti ukladať filtrovanú odpoveď do vyrovnávacej pamäte",
+ "server_config_custom_ip": "Vlastná IP adresa",
+ "server_config_default": "Predvolené",
+ "server_config_disable_ipv6": "Vypnúť rozlišovanie IPv6 adries",
+ "server_config_disable_ipv6_desc": "Zrušiť všetky DNS dopyty pre IPv6 adresy (typ AAAA) a odstrániť IPv6 tipy z HTTPS odpovedí",
+ "server_config_dnssec_enable": "Zapnúť DNSSEC",
+ "server_config_dnssec_enable_desc": "Nastavte príznak DNSSEC v odchádzajúcich DNS dopytoch a skontrolujte výsledok. Vyžaduje sa resolver s povoleným DNSSEC",
+ "server_config_edns_cs_desc": "Pridajte možnosť podsiete klienta EDNS (ECS) do dopytov odosielaných z nadradených zdrojov a zaznamenávajte hodnoty odoslané klientmi do protokolu dopytov",
+ "server_config_edns_enable": "Povoliť klientsku podsiete EDNS",
+ "server_config_edns_use_custom_ip": "Použiť vlastnú IP adresu pre EDNS",
+ "server_config_edns_use_custom_ip_desc": "Nahraďte IP adresu klienta vlastnou IP adresou uvedenou nižšie",
+ "server_config_null_ip": "Nulová IP adresa",
+ "server_config_nxdomain": "NXDOMAIN",
+ "server_config_rate_limit": "Limit rýchlosti, RPS",
+ "server_config_rate_limit_faq": "Počet dopytov za sekundu povolený na klienta. Hodnota 0 znamená žiadny limit",
+ "server_config_rate_limit_placeholder": "Zadajte rýchlostný limit",
+ "server_config_rate_limit_whitelist": "Obmedzenie rýchlosti zoznamu povolených položiek",
+ "server_config_rate_limit_whitelist_faq": "IP adresy vylúčené z obmedzenia rýchlosti",
+ "server_config_refused": "REFUSED",
+ "server_config_subnet_len_ipv4": "Dĺžka prefixu podsiete pre adresy IPv4",
+ "server_config_subnet_len_ipv4_faq": "Dĺžka prefixu podsiete pre adresy IPv4 používané na obmedzenie rýchlosti. Predvolená hodnota je 24",
+ "server_config_subnet_len_ipv6": "Dĺžka prefixu podsiete pre adresy IPv6",
+ "server_config_subnet_len_ipv6_faq": "Dĺžka prefixu podsiete pre adresy IPv6 používané na obmedzenie rýchlosti. Predvolená hodnota je 56",
+ "server_config_subnet_len_placeholder": "Zadajte dĺžku predpony",
+ "set_static_ip": "Nastaviť statickú IP adresu",
+ "settings": "Nastavenia",
+ "settings_anonymize_client_ip": "Anonymizovať IP adresy klientov",
+ "settings_anonymize_client_ip_desc": "Nahradiť časť IP adresy nulami",
+ "settings_browsing_security": "Bezpečnosť prehliadania",
+ "settings_browsing_security_desc": "Blokovať prístup k doménam nájdeným v databáze škodlivých a phishingových domén",
+ "settings_confirm_clear_query_log": "Vymazať denníky dopytov?",
+ "settings_confirm_clear_query_log_desc": "Týmto sa vymažú všetky dopyty z denníka dopytov . Štatistiky budú stále k dispozícii",
+ "settings_confirm_clear_statistics": "Vynulovať štatistiku?",
+ "settings_confirm_clear_statistics_desc": "Týmto sa vymažú všetky štatistiky. Záznam dopytov bude stále k dispozícii",
+ "settings_confirm_decrease_log_rotation_interval": "Znížiť interval rotácie denníkov?",
+ "settings_confirm_decrease_log_rotation_interval_desc": "Týmto sa odstránia všetky záznamy staršie ako %value%",
+ "settings_confirm_decrease_stats_rotation_interval": "Skrátiť interval uchovávania štatistík?",
+ "settings_confirm_decrease_stats_rotation_interval_desc": "Týmto sa odstránia všetky záznamy staršie ako %value%",
+ "settings_custom": "Iný problém",
+ "settings_days": "| %count% deň | %count% dni | %count% dní",
+ "settings_domain_names": "Názvy domén, jedna na riadok",
+ "settings_filter_requests": "Filtrovať dopyty",
+ "settings_filter_requests_desc": "Na nastavenie filtračných pravidiel môžete použiť zoznamy blokovaných položiek , zoznamy povolených položiek a používateľské pravidlá ",
+ "settings_filtering_and_security": "Filtrácia a bezpečnosť",
+ "settings_general_short": "General",
+ "settings_global": "Globálne",
+ "settings_hours": "| %count% hodina | %count% hodiny | %count% hodín",
+ "settings_log_dns_requests": "Denník DNS dopytov",
+ "settings_log_rotation_hours": "Rotácia denníka, v hodinách",
+ "settings_notify_changes_saved": "Zmeny uložené",
+ "settings_notify_query_log_cleared": "Denník dopytov bol vymazaný",
+ "settings_notify_statistics_cleared": "Štatistiky boli vymazané",
+ "settings_parental_control": "Rodičovská kontrola",
+ "settings_parental_control_desc": "Blokovať webové stránky s obsahom nevhodným pre deti",
+ "settings_rotation_placeholder": "Použiť čísla",
+ "settings_rule_syntax": "Syntax pravidiel",
+ "settings_safe_search": "Bezpečné vyhľadávanie",
+ "settings_safe_search_desc": "Blokovať nevhodný obsah vo výsledkoch vyhľadávania",
+ "settings_statistics": "Štatistika",
+ "settings_statistics_clear": "Vynulovať štatistiku",
+ "settings_statistics_desc": "Zobraziť štatistiky dopytov a klientov na paneli",
+ "settings_statistics_retention": "Štatistika za obdobie",
+ "settings_statistics_retention_hours": "Uchovávanie štatistík, v hodinách",
+ "settings_tooltip_domain_names": "Názvy domén, zástupné znaky alebo pravidlá filtrovania",
+ "settings_tooltip_examples": "Príklady:",
+ "settings_yes_clear": "Áno",
+ "settings_yes_decrease": "Áno, znížiť",
+ "setup_complete": "Nastavenie je dokončené a AdGuard Home je pripravený na použitie",
+ "setup_devices_android_list_1": "Otvorte aplikáciu Nastavenia na Vašom zariadení a vyberte Wi-Fi .",
+ "setup_devices_android_list_2": "Dlho stlačte svoju aktuálnu sieť a potom vyberte Upraviť sieť . Pre mobilné internetové pripojenia nie je možné nastaviť vlastný DNS.",
+ "setup_devices_android_list_3": "Na niektorých zariadeniach možno budete musieť vybrať zaškrtávacie políčko Advanced , aby ste zobrazili viac nastavení. Ak chcete prispôsobiť nastavenia DNS v systéme Android, musíte zmeniť nastavenia IP z DHCP na Static .",
+ "setup_devices_android_list_4": "Zmeňte hodnoty DNS 1 a DNS 2 na adresy vášho servera AdGuard Home.",
+ "setup_devices_android_title": "Android",
+ "setup_devices_dns_android_list_1": "Android 9 natívne podporuje DNS-over-TLS . Ak ho chcete nakonfigurovať, prejdite na Nastavenia → Sieť a internet → Rozšírené → Súkromné DNS a zadajte názov svojej domény.",
+ "setup_devices_dns_android_list_2": "AdGuard pre Android podporuje DNS-over-HTTPS , DNS-over-TLS a nastavenie DNS-over-QUIC .",
+ "setup_devices_dns_android_list_3": "Intra pridáva podporu DNS-over-HTTPS pre Android.",
+ "setup_devices_dns_ios_list_1": "AdGuard pre iOS podporuje nastavenie DNS cez HTTPS , DNS cez TLS a DNS cez QUIC .",
+ "setup_devices_dns_ios_list_2": "DNSCloak podporuje DNS-over-HTTPS , ale ak ho chcete nakonfigurovať na používanie vlastného servera, budete musieť preň vygenerovať DNS pečiatku .",
+ "setup_devices_dns_list_1": "Pre DNS cez TLS použite ",
+ "setup_devices_dns_list_2": "Pre DNS-over-HTTPS použite ",
+ "setup_devices_dns_list_3": "Pre DNS-over-QUIC použite ",
+ "setup_devices_dns_list_devices": "Tu je zoznam softvéru, ktorý môžete použiť.",
+ "setup_devices_dns_macos_desc": "Na zariadeniach s iOS 14 alebo macOS Big Sur si môžete stiahnuť špeciálny súbor mobileconfig, ktorý pridá servery DNS-over-HTTPS alebo DNS-over-TLS do nastavení DNS.",
+ "setup_devices_dns_other_list_1": "AdGuard Home môže byť sám o sebe bezpečný DNS klient na akejkoľvek platforme.",
+ "setup_devices_dns_other_list_2": "dnsproxy podporuje všetky známe zabezpečené DNS protokoly.",
+ "setup_devices_dns_other_list_3": "dnscrypt-proxy podporuje DNS-over-HTTPS .",
+ "setup_devices_dns_other_list_4": "Mozilla Firefox podporuje DNS-over-HTTPS .",
+ "setup_devices_dns_other_list_5": "Pre viac implementácií si pozrite DNSCrypt a súkromie DNS .",
+ "setup_devices_ios_list_1": "Otvorte aplikáciu Nastavenia vo svojom zariadení a vyberte Wi‑Fi . Nie je možné nastaviť vlastné DNS pre mobilné internetové pripojenia.",
+ "setup_devices_ios_list_2": "Klepnite na názov aktuálne aktívnej siete.",
+ "setup_devices_ios_list_3": "Prejdite na DNS → Konfigurovať DNS → Manuálne a pridajte adresy Vášho servera AdGuard Home.",
+ "setup_devices_ios_list_4": "Klepnite na Uložiť .",
+ "setup_devices_ios_title": "iOS",
+ "setup_devices_macos_list_1": "Kliknite na menu Apple → Systémové nastavenia → Sieť .",
+ "setup_devices_macos_list_2": "Vyberte požadované pripojenie a kliknite na Advanced .",
+ "setup_devices_macos_list_3": "Vyberte kartu DNS a zadajte adresy Vášho servera AdGuard Home.",
+ "setup_devices_macos_title": "macOS",
+ "setup_devices_router_desc_1": "Nastavte AdGuard Home na smerovači Vašej domácej siete a chráňte všetky zariadenia k nemu pripojené. Nie je potrebné manuálne konfigurovať každé zariadenie.",
+ "setup_devices_router_desc_2": "Niektoré smerovače neumožňujú nastaviť vlastný DNS server. V takom prípade môžete skúsiť nastaviť AdGuard Home ako DHCP server . Ak to nefunguje, pozrite si návod na obsluhu smerovača, kde nájdete pokyny na prispôsobenie DNS serverov pre Váš konkrétny smerovač.",
+ "setup_devices_router_list_1": "Zvyčajne k nim máte prístup z prehliadača prostredníctvom adresy URL, napríklad http://192.168.0.1 alebo http://192.168.1.1 . Môže sa zobraziť výzva na zadanie hesla. Heslo môžete často obnoviť stlačením tlačidla na samotnom smerovači, ale majte na pamäti, že by ste mohli stratiť celú konfiguráciu smerovača. Ak sa smerovač nastavuje pomocou aplikácie, nainštalujte si aplikáciu do telefónu alebo počítača a použite ju na prístup k nastaveniam smerovača.",
+ "setup_devices_router_list_1_title": "Aktualizujte nastavenia Vášho smerovača.",
+ "setup_devices_router_list_2": "Ak chcete nájsť nastavenia, vyhľadajte DNS vedľa poľa, ktoré umožňuje dve alebo tri sady čísel, pričom každá je rozdelená do štyroch skupín s jednou až tromi číslicami.",
+ "setup_devices_router_list_2_title": "V nastaveniach DHCP/DNS zadajte adresy Vášho servera AdGuard Home.",
+ "setup_devices_router_title": "Smerovač",
+ "setup_devices_windows_list_1": "Otvorte Ovládací panel z ponuky Štart alebo Vyhľadávanie vo Windowse .",
+ "setup_devices_windows_list_2": "Kliknite na Sieť a internet → Centrum sietí a zdieľania → Zmeniť nastavenie adaptéra .",
+ "setup_devices_windows_list_3": "Kliknite pravým tlačidlom myši na Vaše aktívne pripojenie a vyberte Vlastnosti .",
+ "setup_devices_windows_list_4": "V zozname vyberte Internet Protocol Version 4 (TCP/IPv4) pre IPv4 alebo Internet Protocol Version 6 (TCP/IPv6) pre IPv6.",
+ "setup_devices_windows_list_5": "Kliknite na Vlastnosti → Použiť nasledujúce adresy serverov DNS .",
+ "setup_devices_windows_list_6": "Zadajte adresy Vášho servera AdGuard Home.",
+ "setup_devices_windows_title": "Windows",
+ "setup_dns_desc": "Nakonfigurujte svoj smerovač alebo iné zariadenia tak, aby používali nasledujúce adresy DNS serverov:",
+ "setup_dns_notice_new": "Ak chcete používať DNS-over-HTTPS alebo DNS-over-TLS , musíte nakonfigurovať šifrovanie v nastaveniach AdGuard Home.",
+ "setup_dns_privacy_ioc_mac": "Konfigurácia iOS a macOS",
+ "setup_dns_privacy_other_title": "Ostatné implementácie",
+ "setup_dns_quote_desc": "Pre správne fungovanie vyžaduje DNS server AdGuard Home statickú IP adresu vo Vašej lokálnej sieti. V opačnom prípade mu môže Váš smerovač priradiť inú IP adresu a narušiť ochranu DNS",
+ "setup_dns_quote_title": "Priraďte statickú IP adresu DNS serveru AdGuard Home vo Vašej lokálnej sieti",
+ "setup_dns_title": "Nastavte adresu DNS servera AdGuard Home",
+ "setup_dns_title_banner": "Nastavenia DNS servera",
+ "setup_guide": "Sprievodca nastavením",
+ "setup_guide_auth_privacy": "Súhlasím so Zásadami ochrany osobných údajov a Podmienkami používania webových stránok AdGuard",
+ "setup_guide_auth_subtitle": "Toto je potrebné na ochranu administrátorského panela AdGuard Home pred neoprávneným prístupom",
+ "setup_guide_auth_title": "Nastaviť ochranu heslom",
+ "setup_guide_desc": "Nakonfigurujte si zariadenia na používanie AdGuard Home",
+ "setup_guide_device_type": "Vyberte typ zariadenia",
+ "setup_guide_greeting_button": "Spustiť sprievodcu",
+ "setup_guide_greeting_desc": "Tento sprievodca Vám pomôže nakonfigurovať AdGuard Home:",
+ "setup_guide_greeting_list_1": "Vytvorte si prihlasovacie údaje pre prístup k administrátorskému panelu",
+ "setup_guide_greeting_list_2": "Nastavenie adresy webového rozhrania pre AdGuard Home",
+ "setup_guide_greeting_list_3": "Nastavte adresu DNS servera AdGuard Home",
+ "setup_guide_greeting_list_4": "Získajte pokyny pre Vaše konkrétne zariadenia",
+ "setup_guide_greeting_list_5": "Získajte prístup k ovládaciemu panelu AdGuard Home ",
+ "setup_guide_greeting_title": "Nastaviť AdGuard Home",
+ "setup_guide_title": "Nastavte svoje zariadenia na používanie AdGuard Home",
+ "setup_ui_desc": "Použite tieto adresy na prístup k webovému používateľskému rozhraniu AdGuard Home. V prípade potreby zmeňte port",
+ "setup_ui_title": "Nastavenie adresy webového rozhrania pre AdGuard Home",
+ "setup_ui_title_banner": "Nastavenia webového používateľského rozhrania",
+ "show_blocked_responses": "Zablokované",
+ "show_more_count": "+ %count% viac",
+ "show_processed_responses": "Spracované",
+ "sort_asc": "Zoradiť od A po Z",
+ "sort_desc": "Zoradiť od Z po A",
+ "source_label": "Zdroj",
+ "source_tooltip": "Zdroj",
+ "static_dhcp_leases_not_found": "Statické prenájmy DHCP sa nenašli",
+ "stats_adult": "Blokovaná stránka pre dospelých",
+ "stats_query_domain": "Najčastejšie dopytované domény",
+ "status_table_header": "Stav",
+ "sunday": "Nedeľa",
+ "system_host_files": "Systémové súbory hosts",
+ "system_theme": "Systémová téma",
+ "table_client": "Klient",
+ "tags_title": "Tagy",
+ "test_upstreams": "Test upstreamov",
+ "threats_blocked": "Zablok. hrozieb",
+ "threats_blocked_tooltip": "Počet DNS dopytpv blokovaných bezpečnostným modulom AdGuard pre prehliadanie",
+ "thursday": "Štvrtok",
+ "time_table_header": "Čas",
+ "top_blocked_domains": "Najčastejšie zablokované domény",
+ "top_clients": "Najčastejší klienti",
+ "top_upstreams": "Často požadované upstream servery",
+ "total": "Spolu",
+ "try_again": "Skúste znova",
+ "tuesday": "Utorok",
+ "type_value": "Typ: %value%",
+ "unavailable_dhcp": "DHCP nie je dostupné",
+ "unavailable_dhcp_desc": "AdGuard Home nemôže vo Vašom OS prevádzkovať DHCP server",
+ "unblock": "Odblokovať",
+ "unblock_client": "Odblokovať klienta",
+ "unknown_filter": "Neznámy filter %filterId%",
+ "update_button": "Aktualizovať",
+ "update_filters_custom_hours": "Vlastný interval v hodinách",
+ "update_filters_disable": "Vypnúť",
+ "update_filters_interval_custom": "Iný problém",
+ "update_filters_interval_daily": "Každý deň",
+ "update_filters_interval_hourly": "Každú hodinu",
+ "update_filters_interval_weekly": "Každý týždeň",
+ "update_filters_title": "Aktualizovať filtre automaticky",
+ "updated_upstream_dns_toast": "Nadradené servery boli uložené",
+ "updates_checked": "K dispozícii je nová verzia aplikácie AdGuard Home\n",
+ "updates_version_equal": "AdGuard Home je aktuálny",
+ "upstream": "Upstream server",
+ "upstream_bootstrap_dns_faq": "IP adresy DNS serverov používaných na preklad IP adries upstream resolverov DoH/DoT. Komentáre nie sú povolené",
+ "upstream_bootstrap_dns_title": "Bootstrap DNS servery, jeden na riadok",
+ "upstream_dns": "Upstream DNS servery",
+ "upstream_dns_addresses": "Adresy servera, jedna na riadok",
+ "upstream_dns_addresses_faq": "Získajte viac informácií o konfigurácii nadradených DNS serverov. Tu jezoznam známych poskytovateľov DNS , z ktorých si môžete vybrať.",
+ "upstream_dns_configured_in_file": "Nakonfigurované v %path%",
+ "upstream_dns_fastest_addr": "Najrýchlejšia IP adresa",
+ "upstream_dns_parallel_requests": "Paralelné dopyty",
+ "upstream_dns_placeholder": "IP adresy, sdns://, quic://, https://, h3:// alebo tls://",
+ "upstream_dns_servers_title": "Upstream DNS servery",
+ "upstream_enable_reverse_lookup_desc": "Spätne preložiť IP adresy klientov na ich názvy hostiteľov odoslaním PTR dotazov príslušným prekladačom: súkromným DNS serverom pre lokálnych klientov alebo nadradeným serverom pre klientov s verejnými IP adresami",
+ "upstream_enable_reverse_lookup_title": "Povoliť spätné vyhľadávanie IP adries klientov",
+ "upstream_example_multiple_upstreams_reserved": "%value%: viacero upstreamov pre konkrétne domény ",
+ "upstream_example_tcp": "%ipv4%,%ipv6%: bežné DNS (cez TCP)",
+ "upstream_example_tcp_hostname": "%value%: bežné DNS (cez TCP, názov hostiteľa)",
+ "upstream_example_tcp_port": "%ipv4%,%ipv6%: bežné DNS (cez TCP, s portom)",
+ "upstream_example_udp": "%ipv4%,%ipv6%: bežné DNS (cez UDP)",
+ "upstream_example_udp_hostname": "%value%: bežné DNS (cez UDP, názov hostiteľa)",
+ "upstream_example_udp_port": "%ipv4%,%ipv6%: bežné DNS (cez UDP, s portom)",
+ "upstream_example_upstream_comment": "%value%: komentár.",
+ "upstream_example_upstream_doh": "%value%: šifrované DNS-over-HTTPS ",
+ "upstream_example_upstream_doh3": "%value%: šifrované DNS-over-HTTPS s vynúteným HTTP/3 a bez návratu na HTTP/2",
+ "upstream_example_upstream_doq": "%value%: šifrované DNS cez QUIC ",
+ "upstream_example_upstream_dot": "%value%: šifrované DNS-over-TLS ",
+ "upstream_example_upstream_reserved": "%value%: upstream pre konkrétne domény ",
+ "upstream_example_upstream_sdns": "%value%: DNS pečiatky pre DNSCrypt alebo DNS-over-HTTPS resolvery",
+ "upstream_examples_title": "Príklady",
+ "upstream_fallback_title": "Záložné DNS servery, jeden na riadok",
+ "upstream_fallback_title_faq": "Záložné DNS servery, ktoré sa majú použiť, keď nie sú k dispozícii nadradené DNS servery. Použite rovnakú syntax ako pre nadradené DNS servery",
+ "upstream_private_ptr_resolvers_desc": "Riešenie požiadaviek PTR, SOA a NS pre domény ARPA obsahujúce súkromné IP adresy prostredníctvom súkromných upstreamových serverov, DHCP, /etc/hosts atď. Ak je táto možnosť vypnutá, AdGuard Home bude na všetky takéto požiadavky odpovedať kódom NXDOMAIN",
+ "upstream_private_ptr_resolvers_title": "Použiť súkromné reverzné DNS resolvery",
+ "upstream_ptr": "Súkromné reverzné DNS servery, jeden na riadok",
+ "upstream_ptr_faq_1": "AdGuard Home používa tieto DNS servery pre súkromné dopyty PTR, SOA a NS. Súkromnédopyty prichádzajú od klientov so súkromnou IP adresou a žiadajú o doménu ARPA, ktorá obsahuje podsieť zo súkromného rozsahu IP adries, ako napríklad %value%",
+ "upstream_ptr_faq_2": "Ak je toto pole prázdne, použijú sa predvolené DNS prekladače Vášho operačného systému, s výnimkou IP adries AdGuard Home",
+ "upstream_ptr_faq_3": "AdGuard Home štandardne používa nasledujúce reverzné DNS resolvery: %value_1% a %value_2%",
+ "upstream_timeout": "Časový limit pre upstream, v sekundách",
+ "upstream_timeout_faq": "Čas čakania na odpoveď od nadradeného servera",
+ "upstream_timeout_placeholder": "Zadajte časový limit nadradeného servera",
+ "upstreams": "Upstreams",
+ "url_label": "URL adresa",
+ "use_saved_key": "Použite existujúci súkromný kľúč",
+ "user_rules": "Používateľský filter",
+ "user_rules_add_to_allowlist": "Pridať do zoznamu povolení",
+ "user_rules_allow_service": "Povoliť službu",
+ "user_rules_browsing_security_disabled": "Zabezpečenie prehliadania je vypnuté",
+ "user_rules_check_button": "Kontrola",
+ "user_rules_check_client_label": "Identifikátor klienta (názov, ClientID alebo IP adresa)",
+ "user_rules_check_client_placeholder": "Zadajte identifikátor klienta",
+ "user_rules_check_dns_record_type_label": "Typ DNS záznamu",
+ "user_rules_check_hostname_label": "Názov hostiteľa alebo názov domény",
+ "user_rules_check_hostname_placeholder": "Zadajte cieľ",
+ "user_rules_check_title": "Skontrolujte filtráciu domén globálne alebo pre klienta",
+ "user_rules_cname": "CNAME: %cname%",
+ "user_rules_disable_browsing_security": "Vypnúť zabezpečenie prehliadania",
+ "user_rules_disable_filter": "Vypnúť filter",
+ "user_rules_disable_parental_control": "Vypnúť Rodičovskú kontrolu",
+ "user_rules_disable_safe_search": "Vypnúť bezpečné vyhľadávanie",
+ "user_rules_dns_record_type_placeholder": "Vyberte typ DNS záznamu",
+ "user_rules_dns_rewrite_removed": "Pravidlo bolo odstránené z prepisovania DNS",
+ "user_rules_domain": "Doména: %value%",
+ "user_rules_domain_blocked": "Doména je zablokovaná",
+ "user_rules_domain_is_allowed": "Doména je povolená",
+ "user_rules_domain_is_processed": "Doména je spracovaná",
+ "user_rules_edit_dns_rewrite": "Upraviť prepísanie DNS",
+ "user_rules_example_block": "||%domain% Blokovať prístup k %domain% a všetkým jeho subdoménam",
+ "user_rules_example_comment": "! Toto je komentár Ak chcete komentovať, začnite riadok znakom ! alebo #",
+ "user_rules_example_comment_2": "# Toto je tiež komentár Ak chcete komentovať, začnite riadok znakom ! alebo #",
+ "user_rules_example_regex": "/REGEX/ Blokovať prístup k doménam, ktoré zodpovedajú zadanému regulárnemu výrazu",
+ "user_rules_example_response": "127.0.0.1 %domain% Odpovedať s 127.0.0.1 iba pre %domain%, nie pre jej subdomény",
+ "user_rules_example_unblock": "@@||%domain% Odblokovať prístup k %domain% a všetkým jeho subdoménam",
+ "user_rules_filter_was_disabled": "%value% bol vypnutý",
+ "user_rules_ip": "IP adresy: %ip%",
+ "user_rules_learn_more": "Získajte viac informácií o vytváraní vlastných zoznamov hostiteľov.",
+ "user_rules_no_rules_matched": "Výsledok: Nezodpovedajú žiadne pravidlá",
+ "user_rules_parental_control_disabled": "Rodičovská kontrola je vypnutá",
+ "user_rules_placeholder": "Toto je zástupný symbol",
+ "user_rules_reason": "Dôvod: %reason%",
+ "user_rules_reason_allowed_by": "Povolené podľa %source%",
+ "user_rules_reason_blocked_by": "Zablokované podľa %source%",
+ "user_rules_reason_filtered_by": "Filtrované podľa %source%",
+ "user_rules_redirected_to": "Presmerované na: %value%",
+ "user_rules_remove_dns_rewrite": "Odstrániť prepísanie DNS",
+ "user_rules_rewrite_rule_is_applied": "Použilo sa pravidlo prepisovania",
+ "user_rules_rewritten_to": "Prepísané na: %value%",
+ "user_rules_rule": "Pravidlo: %rule%",
+ "user_rules_rule_added_to_custom_filtering_rules": "Pravidlo pridané do vlastných filtrovacích pravidiel",
+ "user_rules_rule_removed": "Pravidlo bolo odstránené z vlastných filtračných pravidiel",
+ "user_rules_safe_search_disabled": "Bezpečné vyhľadávanie bolo vypnuté",
+ "user_rules_service": "Služba: %service%",
+ "user_rules_service_allowed": "Služba %value% bola povolená",
+ "user_rules_source": "Zdroj: %value%",
+ "user_rules_source_allowlist": "Zoznam povolených položiek: %value%",
+ "user_rules_source_blocklist": "Zoznam blokovaných položiek: %value%",
+ "user_rules_status": "Stav: %reason%",
+ "username_label": "Meno používateľa",
+ "username_placeholder": "Zadajte meno používateľa",
+ "validated_with_dnssec": "Overené pomocou DNSSEC",
+ "wednesday": "Streda",
+ "whois": "WHOIS",
+ "yes": "Áno",
+ "yes_disable": "Áno, vypnúť",
+ "yes_reset": "Áno, resetovať"
+}
diff --git a/client_v2/src/__locales/sl.json b/client_v2/src/__locales/sl.json
new file mode 100644
index 00000000000..e41a617a832
--- /dev/null
+++ b/client_v2/src/__locales/sl.json
@@ -0,0 +1,390 @@
+{
+ "access_settings_title": "Nastavitve dostopa",
+ "actions_table_header": "Akcij",
+ "add": "Dodaj",
+ "add_allowlist": "Dodaj seznam dovoljenih",
+ "add_blocklist": "Dodaj seznam nedovoljenih",
+ "add_persistent_client": "Dodaj kot vztrajnega odjemalca",
+ "add_to_allowlist": "Dodaj na seznam dovoljenih",
+ "ads_blocked": "Onemogočeno oglasov",
+ "ads_blocked_card": "Onemogočeno oglasov",
+ "adult_websites_blocked_tooltip": "Število onemogočenih spletnih strani za odrasle",
+ "all_lists_up_to_date_toast": "Vsi seznami so že posodobljeni",
+ "allowed": "Na seznamu dovoljenih",
+ "allowlist_add": "Dodaj seznam dovoljenih",
+ "allowlist_edit": "Uredi seznam dovoljenih",
+ "apply": "Uporabi",
+ "autofix_warning_result": "Kot rezultat, bo vse zahteve DNS iz vašega sistema privzeto obdelal AdGuard Home.",
+ "autofix_warning_text": "Če kliknete 'Popravi', bo AdGuardHome konfiguriral vaš sistem za uporabo strežnika AdGuardHome DNS.",
+ "average_time_processing": "Povprečni čas obdelave",
+ "average_upstream_response_time": "Povprečni gorvodni odzivni čas",
+ "back": "Nazaj",
+ "block": "Onemogoči",
+ "block_for_this_client_only": "Onemogoči samo za tega odjemalca",
+ "blocked": "Onemogočen",
+ "blocked_by_cname_or_ip": "Onemogočeno s CNAME ali IP naslovom",
+ "blocked_service": "Onemogočena storitev",
+ "blocked_services": "Onemogočene storitve",
+ "blocked_threats": "Onemogočeno groženj",
+ "blocked_threats_chart": "Onemogočeno groženj",
+ "blocklist_add_manual": "Ročno dodaj",
+ "blocklist_edit": "Uredi seznam nedovoljenih",
+ "blocklist_homepage": "Domača stran",
+ "blocklist_url_file_path": "URL naslov ali datoteka pot",
+ "bootstrap_dns": "Zagonski DNS strežniki",
+ "browse": "Prebrskaj",
+ "cache_cleared": "Predpomnilnik DNS je bil uspešno počiščen",
+ "cache_config_clear": "Počisti predpomnilnik",
+ "cache_config_optimistic": "Optimistično predpomnjenje",
+ "cache_config_ttl_validation": "Najmanjša preglasitev TTL predpomnilnika mora biti manjša ali enaka najvišji",
+ "cache_enabled": "Omogoči predpomnilnik",
+ "cancel": "Prekliči",
+ "cancel_btn": "Prekliči",
+ "category_tooltip": "Kategorija",
+ "changes_saved_success": "Spremembe shranjene",
+ "check_dhcp_servers": "Preveri strežnike DHCP",
+ "check_not_found": "Ni najdeno na vašem seznamu filtrov",
+ "check_updates_btn": "Preveri obstoj posodobitev",
+ "clear_btn": "Počisti",
+ "client_add": "Dodaj odjemalca",
+ "client_details": "Podatki o odjemalcu",
+ "client_id": "ID odjemalca",
+ "client_id_placeholder": "Vnesite ID odjemalca",
+ "client_identifier": "Identifikator",
+ "client_settings": "Nastavitve odjemalca",
+ "client_table_header": "Odjemalec",
+ "clients_add_identifier": "Dodaj identifikatorja",
+ "clients_add_placeholder": "Vnesite ime odjemalca",
+ "clients_dns_cache_size": "Velikost predpomnilnika DNS, v bajtih",
+ "clients_not_found": "Ni najdenih odjemalcev",
+ "clients_protection": "Zaščita",
+ "clients_tags": "Oznake",
+ "clients_use_global_settings": "Uporabi splošne nastavitve",
+ "close": "Zapri",
+ "confirm": "Potrdi",
+ "copied": "Kopirano v odložišče",
+ "copy": "Kopiraj",
+ "copy_to_clipboard": "Kopiraj v odložišče",
+ "custom_filtering_rules": "Pravila filtriranja po meri",
+ "dark_theme": "Temna tema",
+ "dashboard": "Nadzorna plošča",
+ "delete_btn": "Izbriši",
+ "delete_table_action": "Izbriši",
+ "dhcp_add_static_lease": "Dodaj statičen najem",
+ "dhcp_config_saved": "Nastavitve DHCP so bile uspešno shranjena",
+ "dhcp_edit_static_lease": "Uredi statični najem",
+ "dhcp_form_gateway_input": "IP prehoda",
+ "dhcp_form_subnet_input": "Maska podomrežja",
+ "dhcp_leases_not_found": "Ni najdenih najemov DHCP",
+ "dhcp_leases_title": "Najemi DHCP",
+ "dhcp_new_static_lease": "Nov statični najem",
+ "dhcp_reset_leases": "Ponastavi vse najeme",
+ "dhcp_reset_leases_success": "Najemi DHCP so bili uspešno ponastavljeni",
+ "dhcp_settings": "Nastavitve DHCP",
+ "dhcp_static_leases_not_found": "Ni najdenih statičnih najemov DHCP",
+ "dhcp_table_hostname": "Ime gostitelja",
+ "dhcp_table_ip_address": "IP naslov",
+ "disable": "Onemogoči",
+ "disable_protection_btn": "Onemogoči zaščito",
+ "disabled_dhcp": "Strežnik DHCP je onemogočen",
+ "disallow_this_client": "Onemogoči tega odjemalca",
+ "dns_access_settings_title": "Nastavitve dostopa",
+ "dns_allowed_clients": "Dovoljeni odjemalci",
+ "dns_blocking_mode": "Način zaviranja",
+ "dns_blocking_mode_custom_ip": "IP naslov po meri",
+ "dns_blocking_mode_default": "Privzeto",
+ "dns_blocking_mode_ipv4_label": "Onemogočanje IPv4",
+ "dns_blocking_mode_ipv6_label": "Onemogočanje IPv6",
+ "dns_blocking_mode_nxdomain": "NXDOMAIN",
+ "dns_blocking_mode_refused": "REFUSED",
+ "dns_blocking_mode_title": "Način zaviranja",
+ "dns_bootstrap_dns_placeholder": "IP naslovi",
+ "dns_bootstrap_dns_title": "Zagonski DNS strežniki",
+ "dns_bootstrap_servers": "Zagonski DNS strežniki",
+ "dns_cache_config": "Konfiguracija strežnika DNS",
+ "dns_cache_size": "Velikost predpomnilnika",
+ "dns_cache_size_label": "Velikost predpomnilnika DNS, v bajtih",
+ "dns_cache_size_title": "Velikost predpomnilnika",
+ "dns_clear_cache": "Počisti predpomnilnik",
+ "dns_clear_cache_cancel": "Prekliči",
+ "dns_clear_cache_confirm": "Da, počisti",
+ "dns_clear_cache_yes": "Da, počisti",
+ "dns_config": "Konfiguracija strežnika DNS",
+ "dns_disallowed_clients": "Zavrnjeni odjemalci",
+ "dns_disallowed_clients_title": "Zavrnjeni odjemalci",
+ "dns_disallowed_domains": "Prepovedane domene",
+ "dns_disallowed_domains_title": "Prepovedane domene",
+ "dns_edns_custom_placeholder": "Vnesite elektronski naslov",
+ "dns_fallback_dns_title": "Rezervni strežniki DNS",
+ "dns_fallback_servers": "Rezervni strežniki DNS",
+ "dns_optimistic_caching": "Optimistično predpomnjenje",
+ "dns_over_https": "DNS-prek-HTTPS",
+ "dns_over_quic": "DNS-over-QUIC",
+ "dns_over_tls": "DNS-prek-TLS",
+ "dns_override_max_ttl": "Preglasi največji TTL",
+ "dns_override_max_ttl_title": "Preglasi največji TTL",
+ "dns_override_min_ttl": "Preglasi najmanjši TTL",
+ "dns_override_min_ttl_title": "Preglasi najmanjši TTL",
+ "dns_privacy": "Zasebnost DNS",
+ "dns_private_reverse_servers_title": "Zasebni povratni strežniki DNS",
+ "dns_rate_limit": "Omejitev hitrosti",
+ "dns_rate_limit_allowlist_placeholder": "IP naslovi",
+ "dns_rate_limit_placeholder": "Vnesite omejitev hitrosti",
+ "dns_rate_limit_title": "Omejitev hitrosti",
+ "dns_rewrites": "Prepisovanja NDS",
+ "dns_server_addresses": "Naslovi strežnika",
+ "dns_server_addresses_label": "Naslovi strežnikov, po eden na vrstico",
+ "dns_server_addresses_title": "Naslovi strežnika",
+ "dns_server_config_title": "Konfiguracija strežnika DNS",
+ "dns_settings": "DNS",
+ "dns_test_upstreams": "Preizkusi upstreame",
+ "dns_upstream_servers_title": "Zagonski DNS strežniki",
+ "dns_upstream_timeout": "Čas čakanja na odzive strežnikov upstream",
+ "dns_upstream_timeout_title": "Čas čakanja na odzive strežnikov upstream",
+ "dns_upstream_validation_invalid": "Neveljaven format",
+ "domain": "Domena",
+ "domain_or_client": "Domena ali odjemalec",
+ "download_mobileconfig": "Prenesi nastavitveno datoteko",
+ "edit_table_action": "Uredi",
+ "enable": "Omogoči",
+ "enabled_dhcp": "Strežnik DHCP je omogočen",
+ "encryption_certificate_path": "Pot digitalnega potrdila",
+ "encryption_certificates": "Digitalna potrdila",
+ "encryption_certificates_input": "Tukaj kopirajte/prilepite PEM šifrirana digitalna potrdila.",
+ "encryption_certificates_source_content": "Prilepi vsebino digitalnih potrdil",
+ "encryption_certificates_source_path": "Nastavi pot datoteke digitalnih potrdil",
+ "encryption_chain_invalid": "Veriga digitalih potrdil ni veljavna",
+ "encryption_chain_valid": "Veriga digitalih potrdil je veljavna",
+ "encryption_config_saved": "Nastavitve šifriranja so shranjene",
+ "encryption_doq": "DNS-prek-vrat QUIC",
+ "encryption_dot": "Vrata DNS-prek-TLS",
+ "encryption_https": "Vrata HTTPS",
+ "encryption_key": "Zasebni ključ",
+ "encryption_key_input": "Tukaj kopirajte/prilepite PEM-kodiran zasebni ključ za vaše digitalno potrdilo.",
+ "encryption_key_source_content": "Prilepi vsebino zasebnega ključa",
+ "encryption_plain_dns": "Navadni DNS",
+ "encryption_plain_dns_error": "Da onemogočite navaden DNS, omogočite vsaj en šifriran protokol DNS",
+ "encryption_private_key_path": "Pot zasebnega ključa",
+ "encryption_server": "Ime strežnika",
+ "encryption_server_enter": "Vnesite ime vaše domene",
+ "encryption_title": "Šifriranje",
+ "enter_cache_size": "Vnesite velikost predpomnilnika (v bajtih)",
+ "enter_ip_address_placeholder": "Vnesite elektronski naslov",
+ "error": "Napaka",
+ "fallback_dns": "Rezervni strežniki DNS",
+ "filter_category_general": "General",
+ "filter_category_general_desc": "Seznami, ki zavirajo sledenje in oglaševanje na večini naprav",
+ "filter_category_other": "Ostalo",
+ "filter_category_other_desc": "Drugi seznami za zaviranje",
+ "filter_category_regional": "Področno",
+ "filter_category_regional_desc": "Seznami, ki so osredotočeni na področne oglase in strežnike za sledenje",
+ "filter_category_security": "Varnost",
+ "filter_category_security_desc": "Seznami, posebej zasnovani za onemogočanje zlonamernih domen, domen z lažnim predstavljanjem in prevarami",
+ "filters": "Filtri",
+ "forgot_password": "Izgubljeno geslo?",
+ "form_enter_blocked_response_ttl": "Vnesite TTL zaviranega odgovora (sekunde)",
+ "form_error_client_id_format": "ID odjemalca mora vsebovati samo številke, male črke in vezaje",
+ "form_error_domain_format": "Neveljavna oblika domene",
+ "form_error_equal": "Ne sme biti enako",
+ "form_error_format": "Neveljaven format",
+ "form_error_gateway_ip": "Najem ne more imeti naslova IP prehoda",
+ "form_error_ip4_format": "Neveljaven naslov IPv4.",
+ "form_error_ip6_format": "Neveljaven naslov IPv6",
+ "form_error_ip_format": "Neveljaven IP naslov",
+ "form_error_mac_format": "Neveljaven naslov MAC",
+ "form_error_password": "Geslo se ne ujema",
+ "form_error_port": "Vnesite veljavno številko vrat",
+ "form_error_port_range": "Vnesite številko vrat v razponu med 80-65535",
+ "form_error_port_unsafe": "Nevarna vrata",
+ "form_error_server_name": "Neveljavno ime strežnika",
+ "form_error_url_format": "Vnesite veljaven URL ali pot do datoteke",
+ "friday": "Petek",
+ "gateway_or_subnet_invalid": "Maska podomrežja ni veljavna",
+ "general_statistics": "Splošna statistika",
+ "ignore_domains_title": "Prezrte domene",
+ "inactivity_schedule_add": "Dodaj rokovnik",
+ "inactivity_schedule_all_day": "Ves dan",
+ "inactivity_schedule_edit": "Uredi rokovnik",
+ "install_auth_confirm": "Potrdi geslo",
+ "install_auth_password": "Geslo",
+ "install_auth_password_enter": "Vnesite geslo",
+ "install_auth_username": "Uporabniško ime",
+ "install_auth_username_enter": "Vnesite uporabniško ime",
+ "install_saved": "Shranjeno uspešno",
+ "install_settings_all_interfaces": "Vsi vmesniki",
+ "install_settings_port": "Vrata",
+ "install_static_error": "AdGuard Home tega omrežnega vmesnika ne more samodejno konfigurirati. Poiščite navodila, kako to storiti ročno.",
+ "install_static_ok": "Dobra novica! Statičen IP naslov je že konfiguriran",
+ "install_step": "Korak",
+ "install_submit_title": "Čestitamo!",
+ "ip_address": "IP naslov",
+ "known_tracker": "Znan sledilec",
+ "language": "Jezik",
+ "light_theme": "Svetla tema",
+ "login": "Vpis",
+ "logout": "Odjava",
+ "make_static": "Naredi statično",
+ "milliseconds_abbreviation": "ms",
+ "monday": "Ponedeljek",
+ "name": "Ime",
+ "name_label": "Ime",
+ "name_table_header": "Ime",
+ "name_tooltip": "Ime",
+ "next": "Naprej",
+ "no": "Ne",
+ "none_text": "Brez",
+ "nothing_found": "Nič ni bilo najdeno",
+ "notify_undo": "Razveljavi",
+ "notify_updated": "Posodobljen",
+ "parental_control": "Starševski nadzor",
+ "parental_group_cdn": "CDN",
+ "parental_group_development": "Razvoj programske opreme",
+ "parental_group_gaming": "Igre",
+ "parental_group_hosting": "Spletno gostovanje",
+ "parental_group_privacy": "Orodja za zasebnost",
+ "parental_group_shopping": "Nakupi",
+ "parental_group_social": "Družbeno omrežje",
+ "parental_group_social_network": "Družbeno omrežje",
+ "parental_group_software": "Razvoj programske opreme",
+ "parental_group_streaming": "Pretakanje",
+ "password_label": "Geslo",
+ "password_placeholder": "Vnesite geslo",
+ "plain_dns": "Navadni DNS",
+ "privacy_policy": "Politika zasebnosti",
+ "protection": "Zaščita",
+ "protocol": "Protokol",
+ "query_log_allowed": "Na seznamu dovoljenih",
+ "query_log_blocked": "Onemogočen",
+ "query_log_blocked_services": "Onemogočene storitve",
+ "query_log_blocked_threats": "Onemogočeno groženj",
+ "query_log_custom_filtering_rules": "Pravila filtriranja po meri",
+ "query_log_nothing_available": "Nič ni bilo najdeno",
+ "query_log_processed": "Obdelano",
+ "query_log_rewritten": "Znova napisano",
+ "query_log_safe_search": "Varno iskanje",
+ "query_log_strict_search": "Za strogo iskanje uporabite dvojne narekovaje",
+ "rate_limit_subnet_len_ipv4_error": "Dolžina predpone podomrežja IPv4 mora biti med 0 in 32",
+ "rate_limit_subnet_len_ipv6_error": "Dolžina podomrežne predpone IPv6 mora biti med 0 in 128",
+ "refresh_btn": "Osveži",
+ "refresh_statics": "Osveži statistiko",
+ "release_notes": "Opombe ob izdaji",
+ "remove": "Odstrani",
+ "report_an_issue": "Prijavite težavo",
+ "request_table_header": "Zahteva",
+ "requests_table_header": "Zahteve",
+ "reset": "Ponastavi",
+ "reset_settings": "Ponastavi nastavitve",
+ "reset_settings_confirm": "Da, ponastavi",
+ "response_details": "Podrobnosti o odzivu",
+ "response_time": "Odzivni čas",
+ "result": "Rezultat",
+ "rewrite_add": "Dodaj prepisovanje DNS",
+ "rewrite_edit": "Urejanje prepisa DNS",
+ "rewrite_hosts_applied": "Prepisano s pravilom gostiteljske datoteke",
+ "rewrite_settings_updated": "Nastavitve prepisovanja DNS so bile uspešno posodobljene",
+ "rewrite_updated": "DNS prepisovanje uspešno posodobljen",
+ "rewritten": "Znova napisano",
+ "rules_label": "Pravila",
+ "safe_browsing": "Varno brskanje",
+ "safe_search": "Varno iskanje",
+ "saturday": "Sobota",
+ "save": "Shrani",
+ "save_btn": "Shrani",
+ "search_placeholder": "Poišči",
+ "server_config_blocking_mode": "Način zaviranja",
+ "server_config_blocking_mode_ipv4": "Onemogočanje IPv4",
+ "server_config_blocking_mode_ipv6": "Onemogočanje IPv6",
+ "server_config_custom_ip": "IP naslov po meri",
+ "server_config_default": "Privzeto",
+ "server_config_disable_ipv6": "Onemogoči reševanje naslovov IPv6",
+ "server_config_dnssec_enable": "Omogoči DNSSEC",
+ "server_config_edns_enable": "Omogoči odjemalsko podomrežje EDNS",
+ "server_config_nxdomain": "NXDOMAIN",
+ "server_config_rate_limit_placeholder": "Vnesite omejitev hitrosti",
+ "server_config_rate_limit_whitelist_faq": "Naslovi IP so izključeni iz omejitve hitrosti",
+ "server_config_refused": "REFUSED",
+ "server_config_subnet_len_ipv4": "Dolžina predpone podomrežja za naslove IPv4",
+ "server_config_subnet_len_ipv4_faq": "Dolžina predpone podomrežja za naslove IPv4, ki se uporabljajo za omejevanje hitrosti. Privzeto je 24",
+ "server_config_subnet_len_ipv6": "Dolžina predpone podomrežja za naslove IPv4",
+ "server_config_subnet_len_ipv6_faq": "Dolžina predpone podomrežja za naslove IPv6, ki se uporabljajo za omejevanje hitrosti. Privzeta vrednost je 56",
+ "set_static_ip": "Nastavi statičen IP naslov",
+ "settings": "Nastavitve",
+ "settings_browsing_security": "Varnost brskanja",
+ "settings_confirm_clear_statistics": "Počistim statistiko?",
+ "settings_custom": "Po meri",
+ "settings_general_short": "General",
+ "settings_global": "Splošno",
+ "settings_notify_changes_saved": "Spremembe shranjene",
+ "settings_notify_statistics_cleared": "Statistika je bila ponastavljena",
+ "settings_parental_control": "Starševski nadzor",
+ "settings_rule_syntax": "Sintaksa pravila",
+ "settings_safe_search": "Varno iskanje",
+ "settings_statistics": "Statistika",
+ "settings_statistics_clear": "Počisti statistiko",
+ "settings_statistics_retention": "Statistika zadrževanja",
+ "settings_yes_clear": "Da, počisti",
+ "setup_devices_router_title": "Usmerjevalnik",
+ "setup_dns_privacy_ioc_mac": "Nastavitve iOS in macOS",
+ "setup_dns_privacy_other_title": "Druge izvedbe",
+ "show_blocked_responses": "Onemogočen",
+ "show_processed_responses": "Obdelano",
+ "source_label": "Vir",
+ "source_tooltip": "Vir",
+ "stats_adult": "Onemogočeno spletnih strani za odrasle",
+ "stats_query_domain": "Najbolj poizvedovane domene",
+ "status_table_header": "Stanje",
+ "sunday": "Nedelja",
+ "system_host_files": "Sistemske gostiteljske datooteke",
+ "table_client": "Odjemalec",
+ "tags_title": "Oznake",
+ "test_upstreams": "Preizkusi upstreame",
+ "threats_blocked": "Onemogočeno groženj",
+ "thursday": "Četrtek",
+ "time_table_header": "Čas",
+ "top_blocked_domains": "Najbolj zavirane domene",
+ "top_clients": "Najpogostejši odjemalci",
+ "top_upstreams": "Pogosto zahtevani gorvodni strežniki",
+ "total": "Skupaj",
+ "try_again": "Poskusi znova",
+ "tuesday": "Torek",
+ "unavailable_dhcp": "DHCP ni na voljo",
+ "unavailable_dhcp_desc": "AdGuard Home ne more zagnati strežnika DHCP v vašem operacijskem sistemu",
+ "unblock": "Omogoči",
+ "update_button": "Posodobi",
+ "update_filters_disable": "Onemogoči",
+ "update_filters_interval_custom": "Po meri",
+ "update_filters_interval_daily": "Vsak dan",
+ "update_filters_interval_hourly": "Vsako uro",
+ "update_filters_title": "Samodejno posodobi filtre",
+ "updates_checked": "Na voljo je nova različica programa AdGuard Home\n",
+ "updates_version_equal": "AdGuard Home je posodobljen",
+ "upstream": "Gorvodni strežnik",
+ "upstream_dns": "Zagonski DNS strežniki",
+ "upstream_dns_addresses": "Naslovi strežnikov, po eden na vrstico",
+ "upstream_dns_fastest_addr": "Najhitrejši IP naslov",
+ "upstream_dns_parallel_requests": "Vzporedne zahteve",
+ "upstream_dns_servers_title": "Zagonski DNS strežniki",
+ "upstream_examples_title": "Primeri",
+ "upstream_private_ptr_resolvers_title": "Uporabi zasebne povratne razreševalnike rDNS",
+ "upstreams": "Tokovi navzgor",
+ "url_label": "URL naslov",
+ "user_rules": "Uporabniška pravila",
+ "user_rules_add_to_allowlist": "Dodaj na seznam dovoljenih",
+ "user_rules_check_button": "Preveri",
+ "user_rules_check_client_placeholder": "Vnesite identifikator odjemalca",
+ "user_rules_check_hostname_label": "Ime gostitelja ali ime domene",
+ "user_rules_disable_filter": "Onemogoči filter",
+ "user_rules_disable_parental_control": "Onemogoči Starševski nadzor",
+ "user_rules_dns_record_type_placeholder": "Izberite vrsto DNS zapisa",
+ "user_rules_domain_blocked": "Domena je onemogočena",
+ "user_rules_edit_dns_rewrite": "Urejanje prepisa DNS",
+ "user_rules_rewrite_rule_is_applied": "Uporabljeno Pravilo za prepisovanje",
+ "username_label": "Uporabniško ime",
+ "username_placeholder": "Vnesite uporabniško ime",
+ "validated_with_dnssec": "Potrjen z DNSSEC",
+ "wednesday": "Sreda",
+ "whois": "WHOIS",
+ "yes": "Da",
+ "yes_disable": "Da, onemogoči",
+ "yes_reset": "Da, ponastavi"
+}
diff --git a/client_v2/src/__locales/sr-cs.json b/client_v2/src/__locales/sr-cs.json
new file mode 100644
index 00000000000..414ffbc3d98
--- /dev/null
+++ b/client_v2/src/__locales/sr-cs.json
@@ -0,0 +1,390 @@
+{
+ "access_settings_title": "Postavke pristupa",
+ "actions_table_header": "Radnje",
+ "add": "Dodaj",
+ "add_allowlist": "Dodaj listu dozvoljenih",
+ "add_blocklist": "Dodaj blok listu",
+ "add_persistent_client": "Dodati u sačuvane klijente",
+ "add_to_allowlist": "Dodaj na belu listu",
+ "ads_blocked": "Blokirano reklama",
+ "ads_blocked_card": "Blokirano reklama",
+ "adult_websites_blocked_tooltip": "Broj blokiranih sajtova za odrasle",
+ "all_lists_up_to_date_toast": "Sve liste su već ažurirane",
+ "allowed": "Stavljeno na belu listu.",
+ "allowlist_add": "Dodaj listu dozvoljenih",
+ "allowlist_edit": "Uredi listu dozvoljenih",
+ "apply": "Primeni",
+ "autofix_warning_result": "Kao rezultat, svi DNS zahtevi sa vašeg sistema će biti obrađeni od AdGuardHome.",
+ "autofix_warning_text": "Ako kliknete \"Popravi\", AdGuardHome će konfigurisati vaš sistem da koristi AdGuardHome DNS server.",
+ "average_time_processing": "Prosečno vreme obrade",
+ "average_upstream_response_time": "Prosečno vreme odziva upstream-servera",
+ "back": "Nazad",
+ "block": "Blokiraj",
+ "block_for_this_client_only": "Blokiraj samo za ovaj klijent",
+ "blocked": "Blokirano",
+ "blocked_by_cname_or_ip": "Blokirano od CNAME ili IP",
+ "blocked_service": "Blokirana usluga",
+ "blocked_services": "Blokiraj usluge",
+ "blocked_threats": "Blokiranih pretnji",
+ "blocked_threats_chart": "Blokiranih pretnji",
+ "blocklist_add_manual": "Dodaj ručno",
+ "blocklist_edit": "Uredi blok listu",
+ "blocklist_homepage": "Početna stranica",
+ "blocklist_url_file_path": "Unesi URL ili putanju",
+ "bootstrap_dns": "Bootstrap DNS serveri",
+ "browse": "Pregledaj",
+ "cache_cleared": "DNS keš je uspešno očišćen",
+ "cache_config_clear": "Obriši keš memoriju",
+ "cache_config_optimistic": "Optimistično keširanje",
+ "cache_config_ttl_validation": "Minimalna TTL vrednost mora biti manja ili jednaka najvišij vrednosti",
+ "cache_enabled": "Omogući keširanje",
+ "cancel": "Otkaži",
+ "cancel_btn": "Otkaži",
+ "category_tooltip": "Kategorija",
+ "changes_saved_success": "Promene su sačuvane",
+ "check_dhcp_servers": "Proveri DHCP servere",
+ "check_not_found": "Nije pronađeno na vašoj listi filtera",
+ "check_updates_btn": "Proveri ažuriranja",
+ "clear_btn": "Očisti",
+ "client_add": "Dodaj klijent",
+ "client_details": "Pojedinosti klijenta",
+ "client_id": "ClientID",
+ "client_id_placeholder": "Unesite ClientID",
+ "client_identifier": "Identifikator",
+ "client_settings": "Postavke klijenta",
+ "client_table_header": "Klijent",
+ "clients_add_identifier": "Dodaj identifikator",
+ "clients_add_placeholder": "Unesite ime klijenta",
+ "clients_dns_cache_size": "Veličina DNS keša, u bajtovima",
+ "clients_not_found": "Nema pronađenih klijenata",
+ "clients_protection": "Zaštita",
+ "clients_tags": "Oznake",
+ "clients_use_global_settings": "Koristi globalne postavke",
+ "close": "Zatvori",
+ "confirm": "Potvrdi",
+ "copied": "Kopirano u ostavu",
+ "copy": "Kopiraj",
+ "copy_to_clipboard": "Kopiraj u ostavu",
+ "custom_filtering_rules": "Prilagođena pravila filtriranja",
+ "dark_theme": "Tamna tema",
+ "dashboard": "Kontrolna tabla",
+ "delete_btn": "Izbriši",
+ "delete_table_action": "Izbriši",
+ "dhcp_add_static_lease": "Dodaj statičko iznajmljivanje",
+ "dhcp_config_saved": "Sačuvaj DHCP konfiguraciju servera",
+ "dhcp_edit_static_lease": "Uređivanje statičkog iznajmljivanja",
+ "dhcp_form_gateway_input": "IP mrežnog prolaza",
+ "dhcp_form_subnet_input": "Subnet mask",
+ "dhcp_leases_not_found": "DHCP pozajmljivanja nisu pronađena",
+ "dhcp_leases_title": "DHCP pozajmljivanja",
+ "dhcp_new_static_lease": "Novo statičko iznajmljivanje",
+ "dhcp_reset_leases": "Resetuj sva unajmljivanja",
+ "dhcp_reset_leases_success": "DHCP unajmljivanja uspešno resetovana",
+ "dhcp_settings": "DHCP postavke",
+ "dhcp_static_leases_not_found": "Nisu pronađena statička DHCP iznajmljivanja",
+ "dhcp_table_hostname": "Ime hosta",
+ "dhcp_table_ip_address": "IP adresa",
+ "disable": "Isključi",
+ "disable_protection_btn": "Isključi zaštitu",
+ "disabled_dhcp": "DHCP server isključen",
+ "disallow_this_client": "Zabrani ovaj klijent",
+ "dns_access_settings_title": "Postavke pristupa",
+ "dns_allowed_clients": "Dozvoljeni klijenti",
+ "dns_blocking_mode": "Način blokiranja",
+ "dns_blocking_mode_custom_ip": "Prilagođena IP adresa",
+ "dns_blocking_mode_default": "Podrazumevano",
+ "dns_blocking_mode_ipv4_label": "Blokiranje IPv4",
+ "dns_blocking_mode_ipv6_label": "Blokiranje IPv6",
+ "dns_blocking_mode_nxdomain": "NXDOMAIN",
+ "dns_blocking_mode_refused": "Odbijeno",
+ "dns_blocking_mode_title": "Način blokiranja",
+ "dns_bootstrap_dns_placeholder": "IP adrese",
+ "dns_bootstrap_dns_title": "Bootstrap DNS serveri",
+ "dns_bootstrap_servers": "Bootstrap DNS serveri",
+ "dns_cache_config": "Konfigurisanje DNS predmemorije",
+ "dns_cache_size": "Veličina predmemorije",
+ "dns_cache_size_label": "Veličina DNS keša, u bajtovima",
+ "dns_cache_size_title": "Veličina predmemorije",
+ "dns_clear_cache": "Obriši keš memoriju",
+ "dns_clear_cache_cancel": "Otkaži",
+ "dns_clear_cache_confirm": "Da",
+ "dns_clear_cache_yes": "Da",
+ "dns_config": "Konfiguracija DNS servera",
+ "dns_disallowed_clients": "Zabranjeni klijenti",
+ "dns_disallowed_clients_title": "Zabranjeni klijenti",
+ "dns_disallowed_domains": "Blokirani domeni",
+ "dns_disallowed_domains_title": "Blokirani domeni",
+ "dns_edns_custom_placeholder": "Unesite IP adresu",
+ "dns_fallback_dns_title": "Odstupajući DNS serveri",
+ "dns_fallback_servers": "Odstupajući DNS serveri",
+ "dns_optimistic_caching": "Optimistično keširanje",
+ "dns_over_https": "DNS-over-HTTPS",
+ "dns_over_quic": "DNS-over-QUIC",
+ "dns_over_tls": "DNS-over-TLS",
+ "dns_override_max_ttl": "Prepiši najveći TTL",
+ "dns_override_max_ttl_title": "Prepiši najveći TTL",
+ "dns_override_min_ttl": "Prepiši najmanji TTL",
+ "dns_override_min_ttl_title": "Prepiši najmanji TTL",
+ "dns_privacy": "DNS privatnost",
+ "dns_private_reverse_servers_title": "Private reverse DNS serveri",
+ "dns_rate_limit": "Ograničenje brzine",
+ "dns_rate_limit_allowlist_placeholder": "IP adrese",
+ "dns_rate_limit_placeholder": "Unesite ograničenje brzine",
+ "dns_rate_limit_title": "Ograničenje brzine",
+ "dns_rewrites": "DNS prepisivanja",
+ "dns_server_addresses": "Adrese servera",
+ "dns_server_addresses_label": "Adrese servera, jedna po redu",
+ "dns_server_addresses_title": "Adrese servera",
+ "dns_server_config_title": "Konfiguracija DNS servera",
+ "dns_settings": "DNS",
+ "dns_test_upstreams": "Testiraj upstreams",
+ "dns_upstream_servers_title": "Upstream DNS serveri",
+ "dns_upstream_timeout": "Upravljački timeout",
+ "dns_upstream_timeout_title": "Upravljački timeout",
+ "dns_upstream_validation_invalid": "Nevažeći format",
+ "domain": "Domen",
+ "domain_or_client": "Domen ili klijent",
+ "download_mobileconfig": "Preuzmi konfiguracionu datoteku",
+ "edit_table_action": "Izmeni",
+ "enable": "Uključi",
+ "enabled_dhcp": "DHCP server uključen",
+ "encryption_certificate_path": "Putanja sertifikata",
+ "encryption_certificates": "Sertifikati",
+ "encryption_certificates_input": "Kopirajte/nalepite vaše PEM-kodirane sertifikate ovde.",
+ "encryption_certificates_source_content": "Nalepite sadržaj sertifikata",
+ "encryption_certificates_source_path": "Postavi putanju do datoteke sertifikata",
+ "encryption_chain_invalid": "Lanac sertifikata je nevažeći",
+ "encryption_chain_valid": "Lanac sertifikata je važeći",
+ "encryption_config_saved": "Konfiguracija šifrovanja je sačuvana",
+ "encryption_doq": "DNS-over-QUIC port",
+ "encryption_dot": "DNS-over-TLS port",
+ "encryption_https": "HTTPS port",
+ "encryption_key": "Privatni ključ",
+ "encryption_key_input": "Kopirajte/nalepite vaš PEM-kodirani privatni ključ za vaš sertifikat ovde.",
+ "encryption_key_source_content": "Nalepi sadržaj privatnog ključa",
+ "encryption_plain_dns": "Plain DNS",
+ "encryption_plain_dns_error": "Da biste onemogućili običan DNS, omogućite najmanje jedan šifrovani DNS protokol",
+ "encryption_private_key_path": "Putanja privatnog ključa",
+ "encryption_server": "Ime servera",
+ "encryption_server_enter": "Unesite vaše ime domena",
+ "encryption_title": "Šifrovanje",
+ "enter_cache_size": "Unesite veličinu predmemorije",
+ "enter_ip_address_placeholder": "Unesite IP adresu",
+ "error": "Greške ",
+ "fallback_dns": "Odstupajući DNS serveri",
+ "filter_category_general": "General",
+ "filter_category_general_desc": "Lista koja blokira praćenja i reklame na većini uređaja",
+ "filter_category_other": "Ostalo",
+ "filter_category_other_desc": "Ostale liste blokiranja",
+ "filter_category_regional": "Region",
+ "filter_category_regional_desc": "Lista koja se usredsređuje na regionalne reklame i servere praćenja",
+ "filter_category_security": "Bezbednost",
+ "filter_category_security_desc": "Lista specijalizovana za blokiranje štetnog softvera, štetnih i fišing domena",
+ "filters": "Filteri",
+ "forgot_password": "Zaboravili ste lozinku?",
+ "form_enter_blocked_response_ttl": "Unesite TTL blokiranog odgovora (sekunde)",
+ "form_error_client_id_format": "ClientID mora da sadrži samo brojeve, malim slovima i crticama",
+ "form_error_domain_format": "Nevažeći format domena",
+ "form_error_equal": "Ne smije biti jednako",
+ "form_error_format": "Nevažeći format",
+ "form_error_gateway_ip": "Zakup ne može imati IP adresu mrežnog prolaza",
+ "form_error_ip4_format": "Nevažeća IPv4 adresa",
+ "form_error_ip6_format": "Nevažeća IPv6 adresa",
+ "form_error_ip_format": "Pogrešna IP adresa",
+ "form_error_mac_format": "Nevažeća MAC adresa",
+ "form_error_password": "Lozinke se ne podudaraju",
+ "form_error_port": "Unesite važeći broj porta",
+ "form_error_port_range": "Unesite vrednost porta u opsegu od 80-65535",
+ "form_error_port_unsafe": "Nije siguran port",
+ "form_error_server_name": "Nevažeće ime servera",
+ "form_error_url_format": "Unesite važeću URL adresu ili putanju datoteke",
+ "friday": "Petak",
+ "gateway_or_subnet_invalid": "Subnet mask nevažeća",
+ "general_statistics": "Opšte statistike",
+ "ignore_domains_title": "Zanemareni domeni",
+ "inactivity_schedule_add": "Dodaj raspored",
+ "inactivity_schedule_all_day": "Ceo dan",
+ "inactivity_schedule_edit": "Uredi raspored",
+ "install_auth_confirm": "Potvrdi lozinku",
+ "install_auth_password": "Lozinka",
+ "install_auth_password_enter": "Unesite lozinku",
+ "install_auth_username": "Korisničko ime",
+ "install_auth_username_enter": "Unesite korisničko ime",
+ "install_saved": "Uspešno sačuvano",
+ "install_settings_all_interfaces": "Sva okruženja",
+ "install_settings_port": "Port",
+ "install_static_error": "AdGuard Home se ne može automatski konfigurisati za ovo mrežno okruženje. Pogledajte uputstvo kako da to ručno uradite.",
+ "install_static_ok": "Dobre vesti! Statička IP adresa je već konfigurisana",
+ "install_step": "Korak",
+ "install_submit_title": "Čestitamo!",
+ "ip_address": "IP adresa",
+ "known_tracker": "Poznato praćenje",
+ "language": "Jezik",
+ "light_theme": "Svetla tema",
+ "login": "Prijavi se",
+ "logout": "Odjavi se",
+ "make_static": "Učini statičnim",
+ "milliseconds_abbreviation": "ms",
+ "monday": "Ponedeljak",
+ "name": "Ime",
+ "name_label": "Ime",
+ "name_table_header": "Ime",
+ "name_tooltip": "Ime",
+ "next": "Dalje",
+ "no": "Ne",
+ "none_text": "Ništa",
+ "nothing_found": "Ništa nije pronađeno",
+ "notify_undo": "Opozovi",
+ "notify_updated": "Ažurirano",
+ "parental_control": "Roditeljska kontrola",
+ "parental_group_cdn": "CDN",
+ "parental_group_development": "Razvoj softvera",
+ "parental_group_gaming": "Igre",
+ "parental_group_hosting": "Veb hosting",
+ "parental_group_privacy": "Alati za privatnost",
+ "parental_group_shopping": "Kupovina",
+ "parental_group_social": "Društvene mreže",
+ "parental_group_social_network": "Društvene mreže",
+ "parental_group_software": "Razvoj softvera",
+ "parental_group_streaming": "Striming",
+ "password_label": "Lozinka",
+ "password_placeholder": "Unesite lozinku",
+ "plain_dns": "Plain DNS",
+ "privacy_policy": "Politika privatnosti",
+ "protection": "Zaštita",
+ "protocol": "Izaberite protokol",
+ "query_log_allowed": "Stavljeno na belu listu.",
+ "query_log_blocked": "Blokirano",
+ "query_log_blocked_services": "Blokiraj usluge",
+ "query_log_blocked_threats": "Blokiranih pretnji",
+ "query_log_custom_filtering_rules": "Prilagođena pravila filtriranja",
+ "query_log_nothing_available": "Ništa nije pronađeno",
+ "query_log_processed": "Obrađeno",
+ "query_log_rewritten": "Prepisano",
+ "query_log_safe_search": "uključi sigurno pretraživanje",
+ "query_log_strict_search": "Koristi duple navodnike za striktnu pretragu",
+ "rate_limit_subnet_len_ipv4_error": "Dužina prefixa IPv4 podmreže treba da bude između 0 i 32",
+ "rate_limit_subnet_len_ipv6_error": "Dužina prefixa IPv6 podmreže treba da bude između 0 i 128",
+ "refresh_btn": "Osveži",
+ "refresh_statics": "Osveži statistike",
+ "release_notes": "Napomene uz izdanje",
+ "remove": "Ukloni",
+ "report_an_issue": "Prijavi problem",
+ "request_table_header": "Zahtev",
+ "requests_table_header": "Zahtevi",
+ "reset": "Resetuj",
+ "reset_settings": "Vrati postavke na podrazumevano",
+ "reset_settings_confirm": "Da",
+ "response_details": "Pojedinosti odgovora",
+ "response_time": "Vreme odziva",
+ "result": "Rezultat",
+ "rewrite_add": "Dodaj DNS prepisivanje",
+ "rewrite_edit": "Uređivanje DNS prepravke",
+ "rewrite_hosts_applied": "Prepisano od pravila hosts datoteke",
+ "rewrite_settings_updated": "Podešavanja DNS prepisivanja uspešno ažurirana",
+ "rewrite_updated": "DNS ponovo napisao uspešno ažuriran",
+ "rewritten": "Prepisano",
+ "rules_label": "Pravila",
+ "safe_browsing": "Sigurno pregledanje",
+ "safe_search": "uključi sigurno pretraživanje",
+ "saturday": "Subota",
+ "save": "Sačuvaj",
+ "save_btn": "Sačuvaj",
+ "search_placeholder": "Traži",
+ "server_config_blocking_mode": "Način blokiranja",
+ "server_config_blocking_mode_ipv4": "Blokiranje IPv4",
+ "server_config_blocking_mode_ipv6": "Blokiranje IPv6",
+ "server_config_custom_ip": "Prilagođena IP adresa",
+ "server_config_default": "Podrazumevano",
+ "server_config_disable_ipv6": "Onemogući rešavanje IPv6 adresa",
+ "server_config_dnssec_enable": "Uključi DNSSEC",
+ "server_config_edns_enable": "Uključi EDNS Client Subnet",
+ "server_config_nxdomain": "NXDOMAIN",
+ "server_config_rate_limit_placeholder": "Unesite ograničenje brzine",
+ "server_config_rate_limit_whitelist_faq": "IP adrese koje nisu obuhvaćene ograničenjem brzine",
+ "server_config_refused": "Odbijeno",
+ "server_config_subnet_len_ipv4": "Dužina prefixa podmreže za IPv4 adrese",
+ "server_config_subnet_len_ipv4_faq": "Dužina prefixa podmreže za IPv4 adrese koje se koriste za ograničavanje brzine. Podrazumevano je 24",
+ "server_config_subnet_len_ipv6": "Dužina prefixa podmreže za IPv6 adrese",
+ "server_config_subnet_len_ipv6_faq": "Dužina prefixa podmreže za IPv6 adrese koje se koriste za ograničavanje brzine. Podrazumevano je 56",
+ "set_static_ip": "Postavite statičku IP adresu",
+ "settings": "Postavke",
+ "settings_browsing_security": "Bezbednosno pregledanje",
+ "settings_confirm_clear_statistics": "Očisti statistiku?",
+ "settings_custom": "Prilagođeno",
+ "settings_general_short": "General",
+ "settings_global": "Globalno",
+ "settings_notify_changes_saved": "Promene su sačuvane",
+ "settings_notify_statistics_cleared": "Statistike su očišćene",
+ "settings_parental_control": "Roditeljska kontrola",
+ "settings_rule_syntax": "Sintaksa pravila",
+ "settings_safe_search": "uključi sigurno pretraživanje",
+ "settings_statistics": "Statistike",
+ "settings_statistics_clear": "Očisti statistiku",
+ "settings_statistics_retention": "Zadržavanje statistike",
+ "settings_yes_clear": "Da",
+ "setup_devices_router_title": "Ruter",
+ "setup_dns_privacy_ioc_mac": "iOS i macOS konfiguracija",
+ "setup_dns_privacy_other_title": "Druge implementacije",
+ "show_blocked_responses": "Blokirano",
+ "show_processed_responses": "Obrađeno",
+ "source_label": "Izvor",
+ "source_tooltip": "Izvor",
+ "stats_adult": "Blokiraj sajtove za odrasle",
+ "stats_query_domain": "Najčešće unošeni domeni",
+ "status_table_header": "Stanje",
+ "sunday": "Nedelja",
+ "system_host_files": "System hosts datoteke",
+ "table_client": "Klijent",
+ "tags_title": "Oznake",
+ "test_upstreams": "Testiraj upstreams",
+ "threats_blocked": "Blokirano pretnji",
+ "thursday": "Četvrtak",
+ "time_table_header": "Vreme",
+ "top_blocked_domains": "Najčešće blokirani domeni",
+ "top_clients": "Najčešći klijenti",
+ "top_upstreams": "Često traženi upstream serveri",
+ "total": "Ukupno",
+ "try_again": "Pokušaj ponovo",
+ "tuesday": "Utorak",
+ "unavailable_dhcp": "DHCP nije dostupan",
+ "unavailable_dhcp_desc": "AdGuard Home Ne može da pokrene DHCP na vašem OS",
+ "unblock": "Odblokiraj",
+ "update_button": "Ažuriraj",
+ "update_filters_disable": "Isključi",
+ "update_filters_interval_custom": "Prilagođeno",
+ "update_filters_interval_daily": "Svaki dan",
+ "update_filters_interval_hourly": "Svaki sat",
+ "update_filters_title": "Automatski ažuriraj filtere",
+ "updates_checked": "Dostupna je nova verzija AdGuard Home-a",
+ "updates_version_equal": "AdGuard Home je ažuriran na najnoviju verziju",
+ "upstream": "Upstream-server",
+ "upstream_dns": "Upstream DNS serveri",
+ "upstream_dns_addresses": "Adrese servera, jedna po redu",
+ "upstream_dns_fastest_addr": "Najbrža IP adresa",
+ "upstream_dns_parallel_requests": "Paralelni zahtevi",
+ "upstream_dns_servers_title": "Upstream DNS serveri",
+ "upstream_examples_title": "Primeri",
+ "upstream_private_ptr_resolvers_title": "Koristi privatne obrnute razrešivače",
+ "upstreams": "Upstreams",
+ "url_label": "URL",
+ "user_rules": "Korisnički filter",
+ "user_rules_add_to_allowlist": "Dodaj na belu listu",
+ "user_rules_check_button": "Proveri",
+ "user_rules_check_client_placeholder": "Unesite identifikator klijenta",
+ "user_rules_check_hostname_label": "Ime domaćina ili ime domena",
+ "user_rules_disable_filter": "Isključi filter",
+ "user_rules_disable_parental_control": "Onemogući roditeljsku kontrolu",
+ "user_rules_dns_record_type_placeholder": "Izaberite tip DNS zapisa",
+ "user_rules_domain_blocked": "Domen je blokiran",
+ "user_rules_edit_dns_rewrite": "Uređivanje DNS prepravke",
+ "user_rules_rewrite_rule_is_applied": "Primenjeno pravilo prepisivanja",
+ "username_label": "Korisničko ime",
+ "username_placeholder": "Unesite korisničko ime",
+ "validated_with_dnssec": "Potvrđeno sa DNSSEC",
+ "wednesday": "Sreda",
+ "whois": "WHOIS",
+ "yes": "Da",
+ "yes_disable": "Da",
+ "yes_reset": "Da"
+}
diff --git a/client_v2/src/__locales/sv.json b/client_v2/src/__locales/sv.json
new file mode 100644
index 00000000000..6750e7ab3a5
--- /dev/null
+++ b/client_v2/src/__locales/sv.json
@@ -0,0 +1,387 @@
+{
+ "access_settings_title": "Åtkomstinställningar",
+ "actions_table_header": "Åtgärder",
+ "add": "Lägg till",
+ "add_allowlist": "Lägg till frilista",
+ "add_blocklist": "Lägg till blockeringslista",
+ "add_persistent_client": "Lägg till som beständig klient",
+ "ads_blocked": "Annonser blockerade",
+ "ads_blocked_card": "Annonser blockerade",
+ "adult_websites_blocked_tooltip": "Antalet vuxensajter som blockerats",
+ "all_lists_up_to_date_toast": "Alla listor är redan uppdaterade",
+ "allowed": "Vitlistade",
+ "allowlist_add": "Lägg till frilista",
+ "allowlist_edit": "Redigera frilista",
+ "apply": "Tillämpa",
+ "autofix_warning_result": "Som ett resultat kommer alla DNS-förfrågningar från ditt system att behandlas av AdGuard Home som standard.",
+ "autofix_warning_text": "Om du klickar på \"Fix\" kommer AdGuard Home att konfigurera ditt system för att använda AdGuard Home DNS server.",
+ "average_time_processing": "Genomsnittlig processtid",
+ "average_upstream_response_time": "Genomsnittlig svarstid uppströmsserver",
+ "back": "Tillbaka",
+ "block": "Blockera",
+ "block_for_this_client_only": "Blockera endast för denna klient",
+ "blocked": "Blockerade",
+ "blocked_by_cname_or_ip": "Blockerad av CNAME eller IP",
+ "blocked_service": "Blockerad tjänst",
+ "blocked_services": "Blockerade tjänster",
+ "blocked_threats": "Blockerade hot",
+ "blocked_threats_chart": "Blockerade hot",
+ "blocklist_add_manual": "Lägg till manuellt",
+ "blocklist_edit": "Redigera blockeringslista",
+ "blocklist_homepage": "Hemsida",
+ "blocklist_url_file_path": "URL eller sökväg",
+ "bootstrap_dns": "Bootstrap-DNS-servrar",
+ "browse": "Bläddra",
+ "cache_cleared": "DNS-cacheminnet har rensats",
+ "cache_config_clear": "Rensa cache",
+ "cache_config_optimistic": "Optimistisk cachning",
+ "cache_config_ttl_validation": "Minsta cache TTL-värde måste vara mindre än eller lika med maxvärdet",
+ "cache_enabled": "Aktivera cache",
+ "cancel": "Avbryt",
+ "cancel_btn": "Avbryt",
+ "category_tooltip": "Kategori",
+ "changes_saved_success": "Ändringar sparade",
+ "check_dhcp_servers": "Letar efter DHCP-servrar",
+ "check_not_found": "Hittades inte i dina filterlistor",
+ "check_updates_btn": "Sök efter uppdateringar",
+ "clear_btn": "Rensa",
+ "client_add": "Lägg till klient",
+ "client_details": "Klient information",
+ "client_id": "Klient ID",
+ "client_id_placeholder": "Ange klient ID",
+ "client_identifier": "Identifikator",
+ "client_settings": "Klientinställningar",
+ "client_table_header": "Klient",
+ "clients_add_identifier": "Lägg till identifierare",
+ "clients_add_placeholder": "Ange klientnamn",
+ "clients_dns_cache_size": "DNS-cachestorlek, i byte",
+ "clients_not_found": "Inga klienter hittade",
+ "clients_protection": "Skydd",
+ "clients_tags": "Taggar",
+ "clients_use_global_settings": "Använda globala inställningar",
+ "close": "Stäng",
+ "confirm": "Bekräfta",
+ "copied": "Kopierad till urklipp",
+ "copy": "Kopiera",
+ "copy_to_clipboard": "Kopiera till urklipp",
+ "custom_filtering_rules": "Egna filterregler",
+ "dark_theme": "Mörkt tema",
+ "dashboard": "Kontrollpanel",
+ "delete_btn": "Radera",
+ "delete_table_action": "Radera",
+ "dhcp_add_static_lease": "Lägg till statisk lease",
+ "dhcp_config_saved": "DHCP-konfigurationen har sparats",
+ "dhcp_edit_static_lease": "Redigera statiskt lease",
+ "dhcp_form_gateway_input": "Gateway-IP",
+ "dhcp_form_subnet_input": "Subnetmask",
+ "dhcp_leases_not_found": "Ingen DHCP-lease hittad",
+ "dhcp_leases_title": "DHCP-lease",
+ "dhcp_new_static_lease": "Ny statisk lease",
+ "dhcp_reset_leases": "Återställ alla leasingavtal",
+ "dhcp_reset_leases_success": "DHCP-leasing har återställts",
+ "dhcp_settings": "DHCP-inställningar",
+ "dhcp_static_leases_not_found": "Inga statiska DHCP-leases hittade",
+ "dhcp_table_hostname": "Värdnamn",
+ "dhcp_table_ip_address": "IP-adress",
+ "disable": "Inaktivera",
+ "disable_protection_btn": "Inaktivera skydd",
+ "disabled_dhcp": "Dhcp-server avaktiverad",
+ "disallow_this_client": "Tillåt inte den här klienten",
+ "dns_access_settings_title": "Åtkomstinställningar",
+ "dns_allowed_clients": "Tillåtna klienter",
+ "dns_blocking_mode": "Blockeringsläge",
+ "dns_blocking_mode_custom_ip": "Anpassad IP-adress",
+ "dns_blocking_mode_default": "Standard",
+ "dns_blocking_mode_ipv4_label": "Blockera IPv4",
+ "dns_blocking_mode_ipv6_label": "Blockera IPv6",
+ "dns_blocking_mode_nxdomain": "NXDOMÄN",
+ "dns_blocking_mode_refused": "REFUSED",
+ "dns_blocking_mode_title": "Blockeringsläge",
+ "dns_bootstrap_dns_placeholder": "IP-adresser",
+ "dns_bootstrap_dns_title": "Bootstrap-DNS-servrar",
+ "dns_bootstrap_servers": "Bootstrap-DNS-servrar",
+ "dns_cache_config": "DNS cache konfiguration",
+ "dns_cache_size": "Cachestorlek",
+ "dns_cache_size_label": "DNS-cachestorlek, i byte",
+ "dns_cache_size_title": "Cachestorlek",
+ "dns_clear_cache": "Rensa cache",
+ "dns_clear_cache_cancel": "Avbryt",
+ "dns_clear_cache_confirm": "Ja, rensa",
+ "dns_clear_cache_yes": "Ja, rensa",
+ "dns_config": "DNS server konfiguration",
+ "dns_disallowed_clients": "Otillåtna klienter",
+ "dns_disallowed_clients_title": "Otillåtna klienter",
+ "dns_disallowed_domains": "Blockerade domäner",
+ "dns_disallowed_domains_title": "Blockerade domäner",
+ "dns_edns_custom_placeholder": "Skriv in IP-adress",
+ "dns_fallback_dns_title": "Reserv DNS-servrar",
+ "dns_fallback_servers": "Reserv DNS-servrar",
+ "dns_optimistic_caching": "Optimistisk cachning",
+ "dns_over_https": "DNS-over-HTTPS",
+ "dns_over_quic": "DNS-over-QUIC",
+ "dns_over_tls": "DNS-over-TLS",
+ "dns_override_max_ttl": "Åsidosätt maximal TTL",
+ "dns_override_max_ttl_title": "Åsidosätt maximal TTL",
+ "dns_override_min_ttl": "Åsidosätt minsta TTL",
+ "dns_override_min_ttl_title": "Åsidosätt minsta TTL",
+ "dns_privacy": "DNS-Integritet",
+ "dns_private_reverse_servers_title": "Privata omvända DNS-servrar",
+ "dns_rate_limit": "Förfrågnings gräns",
+ "dns_rate_limit_allowlist_placeholder": "IP-adresser",
+ "dns_rate_limit_placeholder": "Ange förfrågnings gräns",
+ "dns_rate_limit_title": "Förfrågnings gräns",
+ "dns_rewrites": "DNS omskrivningar",
+ "dns_server_addresses": "Serveradresser",
+ "dns_server_addresses_label": "Serveradresser, en per rad",
+ "dns_server_addresses_title": "Serveradresser",
+ "dns_server_config_title": "DNS server konfiguration",
+ "dns_settings": "DNS",
+ "dns_test_upstreams": "Testa uppströmmar",
+ "dns_upstream_servers_title": "Upstream DNS-servrar",
+ "dns_upstream_timeout": "Uppströms timeout",
+ "dns_upstream_timeout_title": "Uppströms timeout",
+ "dns_upstream_validation_invalid": "Ogiltigt format",
+ "domain": "Domän",
+ "domain_or_client": "Domän eller klient",
+ "download_mobileconfig": "Ladda ner konfigurationsfil",
+ "edit_table_action": "Redigera",
+ "enable": "Aktivera",
+ "enabled_dhcp": "DHCP-server aktiverad",
+ "encryption_certificate_path": "Certifikatsökväg",
+ "encryption_certificates": "Certifikat",
+ "encryption_certificates_input": "Kopiera/klistra in dina PEM-kodade certifikat här.",
+ "encryption_certificates_source_content": "Klistra in certifikatets innehåll",
+ "encryption_certificates_source_path": "Ange sökväg för certifikatfilen",
+ "encryption_chain_invalid": "Certifikatkedjan är ogiltig",
+ "encryption_chain_valid": "Certifikatkedjan är giltig",
+ "encryption_config_saved": "Krypteringsinställningar sparade",
+ "encryption_doq": "DNS-over-QUIC port",
+ "encryption_dot": "DNS-över-TLS port",
+ "encryption_https": "HTTPS-port",
+ "encryption_key": "Privat nyckel",
+ "encryption_key_input": "Kopiera/klistra in din PEM-kodade privata nyckel för ditt certifikat här.",
+ "encryption_key_source_content": "Klistra in den privata nyckelns innehåll",
+ "encryption_plain_dns": "Vanlig DNS",
+ "encryption_plain_dns_error": "För att inaktivera vanlig DNS, aktivera minst ett krypterat DNS-protokoll",
+ "encryption_private_key_path": "Privat nyckel sökväg",
+ "encryption_server": "Servernamn",
+ "encryption_server_enter": "Skriv in ditt domännamn",
+ "encryption_title": "Kryptering",
+ "enter_cache_size": "Ange cachestorlek (byte)",
+ "enter_ip_address_placeholder": "Skriv in IP-adress",
+ "error": "Fel",
+ "fallback_dns": "Reserv DNS-servrar",
+ "filter_category_general": "Allmänt",
+ "filter_category_general_desc": "Listor som blockerar spårning och reklam på de flesta enheterna",
+ "filter_category_other": "Övrigt",
+ "filter_category_other_desc": "Andra blockeringslistor",
+ "filter_category_regional": "Regional",
+ "filter_category_regional_desc": "Listor som fokuserar på regionala annonser och spårningsservrar",
+ "filter_category_security": "Säkerhet",
+ "filter_category_security_desc": "Listor utformade specifikt för att blockera skadliga domäner, nätfiske och bluffdomäner",
+ "filters": "Filter",
+ "forgot_password": "Glömt lösenord?",
+ "form_enter_blocked_response_ttl": "Ange TTL för blockerat svar (sekunder)",
+ "form_error_client_id_format": "Ogiltigt klient-ID",
+ "form_error_domain_format": "Ogiltigt domänformat",
+ "form_error_equal": "Får inte vara samma",
+ "form_error_format": "Ogiltigt format",
+ "form_error_gateway_ip": "Lease kan inte ha IP-adressen för gatewayen",
+ "form_error_ip4_format": "Ogiltig IPv4-adress",
+ "form_error_ip6_format": "Ogiltig IPv6-adress",
+ "form_error_ip_format": "Ogiltig IP-adress",
+ "form_error_mac_format": "Ogiltig MAC-adress",
+ "form_error_password": "Lösenorden överensstämmer inte",
+ "form_error_port": "Skriv in ett giltigt portnummer",
+ "form_error_port_range": "Ange ett portnummer inom värdena 80-65535",
+ "form_error_port_unsafe": "Osäker port",
+ "form_error_server_name": "Ogiltigt servernamn",
+ "form_error_url_format": "Ange en giltig URL eller filsökväg",
+ "friday": "Fredag",
+ "gateway_or_subnet_invalid": "Subnätmask ogiltig",
+ "general_statistics": "Allmän statistik",
+ "ignore_domains_title": "Ignorerade domäner",
+ "inactivity_schedule_add": "Lägg till schema",
+ "inactivity_schedule_all_day": "Hela dagen",
+ "inactivity_schedule_edit": "Redigera schema",
+ "install_auth_confirm": "Bekräfta lösenord",
+ "install_auth_password": "Lösenord",
+ "install_auth_password_enter": "Ange lösenord",
+ "install_auth_username": "Användarnamn",
+ "install_auth_username_enter": "Ange användarnamn",
+ "install_saved": "Sparat utan fel",
+ "install_settings_all_interfaces": "Alla gränssnitt",
+ "install_settings_port": "Port",
+ "install_static_error": "AdGuard Home kan inte konfigurera det automatiskt för detta nätverksgränssnitt. Vänligen leta efter en instruktion om hur du gör detta manuellt.",
+ "install_static_ok": "Goda nyheter! Den statiska IP adressen är redan konfigurerad",
+ "install_step": "Steg",
+ "install_submit_title": "Grattis!",
+ "ip_address": "IP-adress",
+ "known_tracker": "Känd spårare",
+ "language": "Språk",
+ "light_theme": "Ljust tema",
+ "login": "Logga in",
+ "logout": "Logga ut",
+ "make_static": "Gör statisk",
+ "milliseconds_abbreviation": "ms",
+ "monday": "Måndag",
+ "name": "Namn",
+ "name_label": "Namn",
+ "name_table_header": "Namn",
+ "name_tooltip": "Namn",
+ "next": "Nästa",
+ "no": "Nej",
+ "none_text": "Ingen",
+ "nothing_found": "Ingenting hittades",
+ "notify_undo": "Ångra",
+ "notify_updated": "Updaterad",
+ "parental_control": "Föräldrakontroll",
+ "parental_group_cdn": "CDN",
+ "parental_group_development": "Programvaruutveckling",
+ "parental_group_gaming": "Gaming",
+ "parental_group_hosting": "Webbhotell",
+ "parental_group_privacy": "Integritetsverktyg",
+ "parental_group_shopping": "Shopping",
+ "parental_group_social": "Sociala nätverk",
+ "parental_group_social_network": "Sociala nätverk",
+ "parental_group_software": "Programvaruutveckling",
+ "parental_group_streaming": "Streaming",
+ "password_label": "Lösenord",
+ "password_placeholder": "Ange lösenord",
+ "plain_dns": "Vanlig DNS",
+ "privacy_policy": "Integritetspolicy",
+ "protection": "Skydd",
+ "protocol": "Protokoll",
+ "query_log_allowed": "Vitlistade",
+ "query_log_blocked": "Blockerade",
+ "query_log_blocked_services": "Blockerade tjänster",
+ "query_log_blocked_threats": "Blockerade hot",
+ "query_log_custom_filtering_rules": "Egna filterregler",
+ "query_log_nothing_available": "Ingenting hittades",
+ "query_log_processed": "Utförda",
+ "query_log_rewritten": "Omskriven",
+ "query_log_safe_search": "Säker sökning",
+ "query_log_strict_search": "Använd dubbla citattecken för strikt sökning",
+ "rate_limit_subnet_len_ipv4_error": "IPv4-subnätets prefixlängd ska vara mellan 0 och 32",
+ "rate_limit_subnet_len_ipv6_error": "IPv6-subnätets prefixlängd ska vara mellan 0 och 128",
+ "refresh_btn": "Läs in igen",
+ "refresh_statics": "Uppdatera statistik",
+ "release_notes": "Utgåvenoteringar",
+ "remove": "Ta bort",
+ "report_an_issue": "Rapportera ett problem",
+ "request_table_header": "Förfrågning",
+ "requests_table_header": "Förfrågningar",
+ "reset": "Återställ",
+ "reset_settings": "Återställ inställningar",
+ "reset_settings_confirm": "Ja, återställ",
+ "response_details": "Svarsdetaljer",
+ "response_time": "Svarstid",
+ "result": "Resultat",
+ "rewrite_add": "Lägg till DNS omskrivning",
+ "rewrite_edit": "Redigera DNS-omskrivning",
+ "rewrite_hosts_applied": "Omskriven av värd fil regel",
+ "rewrite_settings_updated": "Inställningarna för DNS-omskrivning har uppdaterats",
+ "rewrite_updated": "DNS-omskrivning har uppdaterats",
+ "rewritten": "Omskriven",
+ "rules_label": "Filtreringsregler:",
+ "safe_browsing": "Säker surfning",
+ "safe_search": "Säker sökning",
+ "saturday": "Lördag",
+ "save": "Spara",
+ "save_btn": "Spara",
+ "search_placeholder": "Sök",
+ "server_config_blocking_mode": "Blockeringsläge",
+ "server_config_blocking_mode_ipv4": "Blockera IPv4",
+ "server_config_blocking_mode_ipv6": "Blockera IPv6",
+ "server_config_custom_ip": "Anpassad IP-adress",
+ "server_config_default": "Standard",
+ "server_config_disable_ipv6": "Inaktivera upplösning av IPv6 adresser",
+ "server_config_dnssec_enable": "Aktivera DNSSEC",
+ "server_config_edns_enable": "Aktivera EDNS-klient subnät",
+ "server_config_nxdomain": "NXDOMÄN",
+ "server_config_rate_limit_placeholder": "Ange förfrågnings gräns",
+ "server_config_rate_limit_whitelist_faq": "IP-adresser uteslutna från hastighetsbegränsning",
+ "server_config_refused": "REFUSED",
+ "server_config_subnet_len_ipv4": "Prefixlängd för subnät för IPv4-adresser",
+ "server_config_subnet_len_ipv4_faq": "Subnätprefixlängd för IPv4-adresser som används för hastighetsbegränsning. Standard är 24",
+ "server_config_subnet_len_ipv6": "Prefixlängd för subnät för IPv6-adresser",
+ "server_config_subnet_len_ipv6_faq": "Subnätprefixlängd för IPv6-adresser som används för hastighetsbegränsning. Standard är 56",
+ "set_static_ip": "Ställ in en statisk IP adress",
+ "settings": "Inställningar",
+ "settings_browsing_security": "Surfsäkerhet",
+ "settings_confirm_clear_statistics": "Rensa statistik?",
+ "settings_custom": "Anpassad",
+ "settings_general_short": "Allmänt",
+ "settings_global": "Global",
+ "settings_notify_changes_saved": "Ändringar sparade",
+ "settings_notify_statistics_cleared": "Statistik raderad",
+ "settings_parental_control": "Föräldrakontroll",
+ "settings_rule_syntax": "Syntax",
+ "settings_safe_search": "Säker sökning",
+ "settings_statistics": "Statistik",
+ "settings_statistics_clear": "Rensa statistik",
+ "settings_statistics_retention": "Bevarande av statistik",
+ "settings_yes_clear": "Ja, rensa",
+ "setup_devices_router_title": "Router",
+ "setup_dns_privacy_ioc_mac": "iOS och macOS konfiguration",
+ "setup_dns_privacy_other_title": "Andra implementeringar",
+ "show_blocked_responses": "Blockerade",
+ "show_processed_responses": "Utförda",
+ "source_label": "Webbkälla",
+ "source_tooltip": "Webbkälla",
+ "stats_adult": "Blockerade vuxensajter",
+ "stats_query_domain": "Mest eftersökta domäner",
+ "status_table_header": "Status",
+ "sunday": "Söndag",
+ "system_host_files": "Systemfiler",
+ "table_client": "Klient",
+ "tags_title": "Taggar",
+ "test_upstreams": "Testa uppströmmar",
+ "threats_blocked": "Hot blockerade",
+ "thursday": "Torsdag",
+ "time_table_header": "Tid",
+ "top_blocked_domains": "Flest blockerade domäner",
+ "top_clients": "Toppklienter",
+ "top_upstreams": "Topp uppströmsservrar",
+ "total": "Totalt",
+ "try_again": "Försök igen",
+ "tuesday": "Tisdag",
+ "unavailable_dhcp": "DHCP är inte tillgängligt",
+ "unavailable_dhcp_desc": "AdGuard Home kan inte köra en DHCP-server på ditt operativsystem",
+ "unblock": "Avblockera",
+ "update_button": "Uppdatera",
+ "update_filters_disable": "Inaktivera",
+ "update_filters_interval_custom": "Anpassad",
+ "update_filters_interval_daily": "Varje dag",
+ "update_filters_interval_hourly": "Varje timme",
+ "update_filters_title": "Uppdatera filtren autmatiskt",
+ "updates_checked": "En ny version av AdGuard Home är tillgänglig\n",
+ "updates_version_equal": "AdGuard Home är uppdaterat",
+ "upstream": "Uppströms server",
+ "upstream_dns": "Upstream DNS-servrar",
+ "upstream_dns_addresses": "Serveradresser, en per rad",
+ "upstream_dns_fastest_addr": "Snabbaste IP adressen",
+ "upstream_dns_parallel_requests": "Parallella förfrågningar",
+ "upstream_dns_servers_title": "Upstream DNS-servrar",
+ "upstream_examples_title": "Exempel",
+ "upstream_private_ptr_resolvers_title": "Använd privata omvända DNS upplösare",
+ "upstreams": "Uppströms",
+ "url_label": "URL",
+ "user_rules": "Användarregler",
+ "user_rules_check_button": "Kontrollera",
+ "user_rules_check_client_placeholder": "Ange klientidentifierare",
+ "user_rules_check_hostname_label": "Värdnamn eller domännamn",
+ "user_rules_disable_filter": "Inaktivera filter",
+ "user_rules_disable_parental_control": "Inaktivera Föräldrakontroll",
+ "user_rules_dns_record_type_placeholder": "Välj typ av DNS-post",
+ "user_rules_edit_dns_rewrite": "Redigera DNS-omskrivning",
+ "user_rules_rewrite_rule_is_applied": "Omskrivningsregeln tillämpas",
+ "username_label": "Användarnamn",
+ "username_placeholder": "Ange användarnamn",
+ "validated_with_dnssec": "Validerad med DNSSEC",
+ "wednesday": "Onsdag",
+ "whois": "WHOIS",
+ "yes": "Ja",
+ "yes_disable": "Stoppa",
+ "yes_reset": "Ja, återställ"
+}
diff --git a/client_v2/src/__locales/th.json b/client_v2/src/__locales/th.json
new file mode 100644
index 00000000000..a58c813d404
--- /dev/null
+++ b/client_v2/src/__locales/th.json
@@ -0,0 +1,359 @@
+{
+ "access_settings_title": "เข้าถึงการตั้งค่า",
+ "actions_table_header": "การกระทำ",
+ "add": "เพิ่ม",
+ "add_allowlist": "ไม่มีรายการอนุญาต",
+ "add_blocklist": "เพิ่มรายการบล็อก",
+ "add_persistent_client": "เพิ่มเป็นไคลเอนต์ถาวร",
+ "ads_blocked": "ปิดกั้นโฆษณาแล้ว",
+ "ads_blocked_card": "ปิดกั้นโฆษณาแล้ว",
+ "adult_websites_blocked_tooltip": "มีการปิดกั้นเว็บไซต์สำหรับผู้ใหญ่จำนวนหนึ่ง",
+ "all_lists_up_to_date_toast": "รายการทั้งหมดเป็นข้อมูลล่าสุดอยู่แล้ว",
+ "allowed": "รายการที่อนุญาต",
+ "allowlist_add": "ไม่มีรายการอนุญาต",
+ "allowlist_edit": "แก้ไขรายการอนุญาต",
+ "apply": "นำไปใช้",
+ "autofix_warning_result": "ดังนั้น AdGuardHome จะประมวลผลคำขอ DNS ทั้งหมดจากระบบของคุณตามค่าเริ่มต้น",
+ "autofix_warning_text": "หากคุณคลิก \"แก้ไข\" AdGuardHome จะกำหนดค่าระบบของคุณเพื่อใช้เซิร์ฟเวอร์ AdGuardHome",
+ "average_time_processing": "เวลาประมวลผลโดยเฉลี่ย",
+ "average_upstream_response_time": "เวลาเฉลี่ยในการตอบสนองของพอร์ต",
+ "back": "กลับ",
+ "block": "ปิดกั้น",
+ "block_for_this_client_only": "ปิดกั้นสำหรับไคลเอ็นต์นี้เท่านั้น",
+ "blocked": "ปิดกั้นแล้ว",
+ "blocked_by_cname_or_ip": "บล็อกโดย CNAME หรือ IP",
+ "blocked_service": "ปิดกั้นบริการ",
+ "blocked_services": "ปิดกั้นบริการ",
+ "blocklist_add_manual": "เพิ่มด้วยตนเอง",
+ "blocklist_edit": "แก้ไขรายการบล็อก",
+ "blocklist_homepage": "หน้าหลัก",
+ "blocklist_url_file_path": "URL หรือเส้นทางไฟล์",
+ "bootstrap_dns": "Bootstrap เซิร์ฟเวอร์ DNS",
+ "browse": "เรียกดู",
+ "cache_cleared": "ล้างแคช DNS สำเร็จแล้ว",
+ "cache_config_clear": "ล้างแคช",
+ "cache_config_optimistic": "การเก็บข้อมูลแบบมีความหวัง",
+ "cache_config_ttl_validation": "ค่าการเขียนทับ TTL ของแคชขั้นต่ำจะต้องน้อยกว่าหรือเท่ากับค่าสูงสุด",
+ "cache_enabled": "เปิดใช้งานแคช",
+ "cancel": "ยกเลิก",
+ "cancel_btn": "ยกเลิก",
+ "category_tooltip": "ประเภท",
+ "changes_saved_success": "บันทึกการเปลี่ยนแปลงแล้ว",
+ "check_dhcp_servers": "ตรวจสอบ DHCP servers",
+ "check_not_found": "ไม่พบในรายการตัวกรองของคุณ",
+ "check_updates_btn": "ตรวจสอบการปรับปรุง",
+ "clear_btn": "ล้าง",
+ "client_add": "เพิ่มเครื่องลูกข่าย",
+ "client_details": "รายละเอียดลูกข่าย",
+ "client_id": "รหัสลูกค้า",
+ "client_id_placeholder": "ป้อนรหัสลูกค้า",
+ "client_identifier": "ตรวจสอบโดย",
+ "client_settings": "การตั้งค่าไคลเอนต์",
+ "client_table_header": "เครื่องลูกข่าย",
+ "clients_add_identifier": "เพิ่มตัวระบุ",
+ "clients_add_placeholder": "กรอกชื่อเครื่องลูกข่าย",
+ "clients_dns_cache_size": "ขนาดแคช DNS, เป็นไบต์",
+ "clients_not_found": "ไม่มีเครื่องลูกข่าย",
+ "clients_protection": "การป้องกัน",
+ "clients_tags": "แท็ก",
+ "clients_use_global_settings": "ใช้การตั้งค่าทั่วโลก",
+ "close": "ปิด",
+ "copy_to_clipboard": "คัดลอกไปที่คลิปบอร์ด",
+ "custom_filtering_rules": "กฎการกรองที่กำหนดเอง",
+ "dark_theme": "ธีมสีเข้ม",
+ "dashboard": "แผงควบคุม",
+ "delete_btn": "ลบ",
+ "delete_table_action": "ลบ",
+ "dhcp_add_static_lease": "เพิ่มสัญญาเช่าคงที่",
+ "dhcp_config_saved": "บันทึกการกำหนดค่า DHCP สำเร็จแล้ว",
+ "dhcp_edit_static_lease": "แก้ไขสัญญาเช่าคงที่",
+ "dhcp_form_gateway_input": "IP ของเกตเวย์",
+ "dhcp_form_subnet_input": "ซับเน็ตมาสก์",
+ "dhcp_leases_not_found": "ไม่พบสัญญาเช่า DHCP",
+ "dhcp_leases_title": "สัญญาเช่า DHCP",
+ "dhcp_new_static_lease": "เช่าใหม่คงที่",
+ "dhcp_reset_leases": "รีเซ็ตสัญญาเช่าทั้งหมด",
+ "dhcp_reset_leases_success": "รีเซ็ตสัญญาเช่า DHCP สำเร็จแล้ว",
+ "dhcp_settings": "การตั้งค่า DHCP",
+ "dhcp_static_leases_not_found": "ไม่พบสัญญาเช่า DHCP แบบคงที่",
+ "dhcp_table_hostname": "ชื่อโฮสต์",
+ "dhcp_table_ip_address": "IP addresses",
+ "disable": "ปิดใช้งาน",
+ "disabled_dhcp": "ปิดการใช้งาน DHCP server แล้ว",
+ "disallow_this_client": "ไม่อนุญาตไคลเอ็นต์นี้",
+ "dns_access_settings_title": "เข้าถึงการตั้งค่า",
+ "dns_allowed_clients": "ลูกค้าที่ได้รับอนุญาต",
+ "dns_blocking_mode": "โหมดการปิดกั้น",
+ "dns_blocking_mode_custom_ip": "ที่อยู่ IP ที่กำหนดเอง",
+ "dns_blocking_mode_default": "ค่าเริ่มต้น",
+ "dns_blocking_mode_ipv4_label": "ปิดกั้น IPv4",
+ "dns_blocking_mode_ipv6_label": "ปิดกั้น IPv6",
+ "dns_blocking_mode_nxdomain": "NXDOMAIN",
+ "dns_blocking_mode_refused": "ปฏิเสธ",
+ "dns_blocking_mode_title": "โหมดการปิดกั้น",
+ "dns_bootstrap_dns_placeholder": "ที่อยู่ IP",
+ "dns_bootstrap_dns_title": "Bootstrap เซิร์ฟเวอร์ DNS",
+ "dns_bootstrap_servers": "Bootstrap เซิร์ฟเวอร์ DNS",
+ "dns_cache_config": "การกำหนดค่าแคช DNS",
+ "dns_cache_size": "ขนาดแคช",
+ "dns_cache_size_label": "ขนาดแคช DNS, เป็นไบต์",
+ "dns_cache_size_title": "ขนาดแคช",
+ "dns_clear_cache": "ล้างแคช",
+ "dns_clear_cache_cancel": "ยกเลิก",
+ "dns_clear_cache_confirm": "ใช่, ล้าง",
+ "dns_clear_cache_yes": "ใช่, ล้าง",
+ "dns_config": "การกำหนดค่าเซิร์ฟเวอร์ DNS",
+ "dns_disallowed_clients": "ลูกค้าไม่ได้รับอนุญาต",
+ "dns_disallowed_clients_title": "ลูกค้าไม่ได้รับอนุญาต",
+ "dns_disallowed_domains": "โดเมนที่ถูกปิดกั้น",
+ "dns_disallowed_domains_title": "โดเมนที่ถูกปิดกั้น",
+ "dns_edns_custom_placeholder": "ป้อนที่อยู่ IP",
+ "dns_fallback_dns_title": "เซิร์ฟเวอร์ DNS สำรอง",
+ "dns_fallback_servers": "เซิร์ฟเวอร์ DNS สำรอง",
+ "dns_optimistic_caching": "การเก็บข้อมูลแบบมีความหวัง",
+ "dns_over_https": "DNS-over-HTTPS",
+ "dns_over_quic": "DNS-over-QUIC",
+ "dns_over_tls": "DNS-over-TLS",
+ "dns_override_max_ttl": "ไม่มีการกำหนดค่า TTL สูงสุด",
+ "dns_override_max_ttl_title": "ไม่มีการกำหนดค่า TTL สูงสุด",
+ "dns_override_min_ttl": "ไม่มีการกำหนดค่า TTL ขั้นต่ำ",
+ "dns_override_min_ttl_title": "ไม่มีการกำหนดค่า TTL ขั้นต่ำ",
+ "dns_privacy": "ความเป็นส่วนตัวของ DNS",
+ "dns_private_reverse_servers_title": "เซิร์ฟเวอร์ DNS ส่วนตัว",
+ "dns_rate_limit": "จำกัดอัตรา",
+ "dns_rate_limit_allowlist_placeholder": "ที่อยู่ IP",
+ "dns_rate_limit_placeholder": "ป้อนขีดจำกัดอัตรา",
+ "dns_rate_limit_title": "จำกัดอัตรา",
+ "dns_rewrites": "การเขียน DNS ใหม่",
+ "dns_server_addresses_label": "ที่อยู่เซิร์ฟเวอร์ หนึ่งต่อบรรทัด",
+ "dns_server_config_title": "การกำหนดค่าเซิร์ฟเวอร์ DNS",
+ "dns_settings": "DNS",
+ "dns_test_upstreams": "ทดสอบต้นทาง",
+ "dns_upstream_servers_title": "เซิร์ฟเวอร์ DNS ต้นทาง",
+ "dns_upstream_timeout": "ระยะเวลาหมดอายุของต้นทาง",
+ "dns_upstream_timeout_title": "ระยะเวลาหมดอายุของต้นทาง",
+ "dns_upstream_validation_invalid": "รูปแบบไม่ถูกต้อง",
+ "domain": "โดเมน",
+ "domain_or_client": "โดเมนหรือไคลเอนต์",
+ "download_mobileconfig": "ดาวน์โหลดไฟล์กำหนดค่า",
+ "edit_table_action": "แก้ไข",
+ "enable": "เปิดใช้งาน",
+ "enabled_dhcp": "เปิดการใช้งาน DHCP server แล้ว",
+ "encryption_certificate_path": "เส้นทางใบรับรอง",
+ "encryption_certificates": "ใบรับรอง",
+ "encryption_certificates_input": "คัดลอก/วางใบรับรองที่เข้ารหัส PEM ของคุณที่นี่",
+ "encryption_certificates_source_content": "วางเนื้อหา certificates ",
+ "encryption_certificates_source_path": "ตั้งค่าเส้นทาง certificates ",
+ "encryption_chain_invalid": "ใบรับรองไม่มีความน่าเชื่อถือ",
+ "encryption_chain_valid": "ใบรับรองมีความน่าเชื่อถือ",
+ "encryption_config_saved": "บันทึกการตั้งค่าเข้ารหัสเรียบร้อยแล้ว",
+ "encryption_doq": "พอร์ต DNS-over-QUIC",
+ "encryption_dot": "พอร์ต DNS-over-TLS",
+ "encryption_https": "พอร์ท HTTPS",
+ "encryption_key": "รหัสส่วนตัว (Private key)",
+ "encryption_key_input": "คัดลอก/วาง PEM-encoded private key ของคุณตรงนี้",
+ "encryption_key_source_content": "วางเนื้อหาคีย์ส่วนตัว",
+ "encryption_plain_dns": "DNS ธรรมดา",
+ "encryption_plain_dns_error": "หากต้องการปิดใช้งาน DNS ธรรมดา ให้เปิดใช้งานโปรโตคอล DNS ที่เข้ารหัสอย่างน้อยหนึ่งรายการ",
+ "encryption_private_key_path": "เส้นทางกุญแจส่วนตัว",
+ "encryption_server": "ชื่อเซิร์ฟเวอร์",
+ "encryption_server_enter": "ป้อนชื่อโดเมน",
+ "encryption_title": "การเข้ารหัส",
+ "enter_cache_size": "ป้อนขนาดแคช (ไบต์)",
+ "enter_ip_address_placeholder": "ป้อนที่อยู่ IP",
+ "fallback_dns": "เซิร์ฟเวอร์ DNS สำรอง",
+ "filter_category_general_desc": "รายการที่ปิดกั้นการติดตามและโฆษณาบนอุปกรณ์ส่วนใหญ่",
+ "filter_category_other": "อื่น ๆ",
+ "filter_category_other_desc": "รายการปิดกั้นอื่น ๆ",
+ "filter_category_regional": "ภูมิภาค",
+ "filter_category_regional_desc": "รายการที่เน้นโฆษณาระดับภูมิภาคและเซิร์ฟเวอร์ตัวติดตาม",
+ "filter_category_security_desc": "รายการที่ออกแบบมาเพื่อปิดกั้นโดเมนที่เป็นอันตราย ฟิชชิ่ง และโดเมนหลอกลวง",
+ "filters": "ตัวกรอง",
+ "forgot_password": "ลืมรหัสผ่าน?",
+ "form_enter_blocked_response_ttl": "ป้อนการตอบกลับที่ถูกบล็อค TTL (วินาที)",
+ "form_error_client_id_format": "รูปแบบ ID ลูกค้าไม่ถูกต้อง",
+ "form_error_domain_format": "รูปแบบ Domain ไม่ถูกต้อง",
+ "form_error_equal": "ไม่ควรตรงกัน",
+ "form_error_format": "รูปแบบไม่ถูกต้อง",
+ "form_error_gateway_ip": "สัญญาเช่าไม่สามารถมีที่อยู่ IP ของเกตเวย์ได้",
+ "form_error_ip4_format": "รูปแบบ IPv4 ไม่ถูกต้อง",
+ "form_error_ip6_format": "รูปแบบ IPv6 ไม่ถูกต้อง",
+ "form_error_mac_format": "รูปแบบ MAC ไม่ถูกต้อง",
+ "form_error_password": "รหัสผ่านไม่ตรงกัน",
+ "form_error_port": "ป้อนค่าพอร์ตที่ถูกต้อง",
+ "form_error_port_range": "ป้อนค่าพอร์ตในช่วง 80-65535",
+ "form_error_port_unsafe": "เป็นพอร์ทที่ไม่ปลอดภัย",
+ "form_error_server_name": "ชื่อเซิร์ฟเวอร์ไม่ถูกต้อง",
+ "form_error_url_format": "ใส่ URL หรือเส้นทางไฟล์ที่ถูกต้อง",
+ "friday": "ศุกร์",
+ "gateway_or_subnet_invalid": "หน้ากากซับเน็ตไม่ถูกต้อง",
+ "general_statistics": "สถิติทั่วไป",
+ "ignore_domains_title": "โดเมนที่ถูกละเว้น",
+ "inactivity_schedule_add": "เพิ่มตารางเวลา",
+ "inactivity_schedule_all_day": "ตลอดทั้งวัน",
+ "inactivity_schedule_edit": "แก้ไขตารางเวลา",
+ "install_auth_password": "รหัสผ่าน",
+ "install_auth_password_enter": "ใส่รหัสผ่าน",
+ "install_auth_username": "ยูเซอร์เนม",
+ "install_auth_username_enter": "กรอกชื่อผู้ใช้",
+ "install_saved": "บันทึกเรียบร้อยแล้ว",
+ "install_settings_all_interfaces": "อินเทอร์เฟซทั้งหมด",
+ "install_settings_port": "พอร์ต",
+ "install_static_error": "AdGuard Home ไม่สามารถกำหนดค่าโดยอัตโนมัติสำหรับอินเตอร์เฟซเครือข่ายนี้ได้ กรุณาหาเอกสารในการทำขั้นตอนนี้ด้วยตนเอง.",
+ "install_static_ok": "ข่าวดี! ที่อยู่ IP คงที่ได้ถูกกำหนดค่าแล้ว",
+ "install_step": "ขั้นตอน",
+ "install_submit_title": "ยินดีด้วย!",
+ "ip_address": "IP addresses",
+ "known_tracker": "ตัวติดตามที่รู้จัก",
+ "language": "ภาษา",
+ "light_theme": "ธีมสีอ่อน",
+ "login": "เข้าสู่ระบบ",
+ "logout": "ออกจากระบบ",
+ "make_static": "ทำให้คงที่",
+ "milliseconds_abbreviation": "ms",
+ "monday": "วันจันทร์",
+ "next": "ถัดไป",
+ "no": "ไม่",
+ "none_text": "ไม่มี",
+ "nothing_found": "ไม่พบอะไร",
+ "notify_undo": "เลิกทำ",
+ "notify_updated": "อัปเดต",
+ "parental_control": "ควบคุมโดยผู้ปกครอง",
+ "parental_group_cdn": "CDN",
+ "parental_group_development": "การพัฒนาซอฟต์แวร์",
+ "parental_group_gaming": "การเล่นเกม",
+ "parental_group_hosting": "เว็บโฮสติ้ง",
+ "parental_group_privacy": "เครื่องมือความเป็นส่วนตัว",
+ "parental_group_shopping": "ช้อปปิ้ง",
+ "parental_group_social": "สังคมออนไลน์",
+ "parental_group_social_network": "สังคมออนไลน์",
+ "parental_group_software": "การพัฒนาซอฟต์แวร์",
+ "password_label": "รหัสผ่าน",
+ "password_placeholder": "ใส่รหัสผ่าน",
+ "plain_dns": "DNS ธรรมดา",
+ "privacy_policy": "นโยบายความเป็นส่วนตัว",
+ "protection": "การป้องกัน",
+ "protocol": "เลือกโปรโตคอล",
+ "query_log_allowed": "รายการที่อนุญาต",
+ "query_log_blocked": "ปิดกั้นแล้ว",
+ "query_log_blocked_services": "ปิดกั้นบริการ",
+ "query_log_custom_filtering_rules": "กฎการกรองที่กำหนดเอง",
+ "query_log_nothing_available": "ไม่พบอะไร",
+ "query_log_processed": "ประมวลผลแล้ว",
+ "query_log_rewritten": "เขียนใหม่",
+ "query_log_strict_search": "ใช้เครื่องหมายคำพูดคู่เพื่อการค้นหาที่จำกัด",
+ "rate_limit_subnet_len_ipv4_error": "ความยาวพรีฟิกซ์ซับเน็ต IPv4 ควรอยู่ระหว่าง 0 ถึง 32",
+ "rate_limit_subnet_len_ipv6_error": "ความยาวพรีฟิกซ์ซับเน็ต IPv6 ควรอยู่ระหว่าง 0 ถึง 128",
+ "refresh_btn": "รีเฟรช",
+ "refresh_statics": "รีเฟรชสถิติ",
+ "release_notes": "บันทึกการปรับปรุง",
+ "remove": "เอาออก",
+ "report_an_issue": "รายงานปัญหา",
+ "request_table_header": "คำขอ",
+ "reset": "รีเซ็ต",
+ "reset_settings": "รีเซ็ตการตั้งค่า",
+ "reset_settings_confirm": "ใช่ รีเซ็ต",
+ "response_details": "รายละเอียดการตอบกลับ",
+ "response_time": "เวลาในการตอบสนอง",
+ "rewrite_add": "เพิ่ม DNS rewrite",
+ "rewrite_edit": "แก้ไขการเขียน DNS ใหม่",
+ "rewrite_hosts_applied": "เขียนใหม่โดยกฎไฟล์โฮสต์",
+ "rewrite_settings_updated": "การตั้งค่าเขียนทับ DNS อัปเดตเรียบร้อยแล้ว",
+ "rewrite_updated": "อัปเดตการเขียน DNS ใหม่สำเร็จแล้ว",
+ "rewritten": "เขียนใหม่",
+ "rules_label": "กฎ",
+ "safe_browsing": "ท่องเว็บอย่างปลอดภัย",
+ "saturday": "เสาร์",
+ "save": "บันทึก",
+ "save_btn": "บันทึก",
+ "search_placeholder": "ค้นหา",
+ "server_config_blocking_mode": "โหมดการปิดกั้น",
+ "server_config_blocking_mode_ipv4": "ปิดกั้น IPv4",
+ "server_config_blocking_mode_ipv6": "ปิดกั้น IPv6",
+ "server_config_custom_ip": "ที่อยู่ IP ที่กำหนดเอง",
+ "server_config_default": "ค่าเริ่มต้น",
+ "server_config_disable_ipv6": "ปิดใช้งาน IPv6",
+ "server_config_dnssec_enable": "เปิดใช้ DNSSEC",
+ "server_config_edns_enable": "เปิดใช้งานซับเน็ตไคลเอ็นต์ EDNS",
+ "server_config_nxdomain": "NXDOMAIN",
+ "server_config_rate_limit_placeholder": "ป้อนขีดจำกัดอัตรา",
+ "server_config_rate_limit_whitelist_faq": "ที่อยู่ IP ที่ถูกยกเว้นจากการจำกัดอัตรา",
+ "server_config_refused": "ปฏิเสธ",
+ "server_config_subnet_len_ipv4": "ความยาวของคำนำหน้าซับเน็ตสำหรับที่อยู่ IPv4",
+ "server_config_subnet_len_ipv4_faq": "ความยาวพรีฟิกซ์ซับเน็ตสำหรับที่อยู่ IPv4 ที่ใช้สำหรับการจำกัดอัตรา ค่าเริ่มต้นคือ 24",
+ "server_config_subnet_len_ipv6": "ความยาวของคำนำหน้าซับเน็ตสำหรับที่อยู่ IPv6",
+ "server_config_subnet_len_ipv6_faq": "ความยาวพรีฟิกซ์ซับเน็ตสำหรับที่อยู่ IPv6 ที่ใช้สำหรับการจำกัดอัตรา ค่าเริ่มต้นคือ 56",
+ "set_static_ip": "ตั้งค่าที่อยู่ IP แบบคงที่",
+ "settings": "การตั้งค่า",
+ "settings_browsing_security": "การรักษาความปลอดภัยการท่องเว็บ",
+ "settings_confirm_clear_statistics": "ล้างสถิติ?",
+ "settings_custom": "กำหนดเอง",
+ "settings_global": "ทั่วโลก",
+ "settings_notify_changes_saved": "บันทึกการเปลี่ยนแปลงแล้ว",
+ "settings_parental_control": "ควบคุมโดยผู้ปกครอง",
+ "settings_rule_syntax": "ไวยากรณ์กฎ",
+ "settings_statistics": "สถิติ",
+ "settings_statistics_clear": "ล้างสถิติ",
+ "settings_statistics_retention": "การเก็บรักษาสถิติ",
+ "settings_yes_clear": "ใช่, ล้าง",
+ "setup_devices_router_title": "เราเตอร์",
+ "setup_dns_privacy_ioc_mac": "การกำหนดค่า iOS และ macOS",
+ "setup_dns_privacy_other_title": "การใช้งานอื่น ๆ",
+ "show_blocked_responses": "ปิดกั้นแล้ว",
+ "show_processed_responses": "ประมวลผลแล้ว",
+ "source_label": "ที่มา",
+ "source_tooltip": "ที่มา",
+ "stats_adult": "ปิดกั้นเว็บไซต์สำหรับผู้ใหญ่แล้ว",
+ "stats_query_domain": "โดเมนที่เข้าบ่อยสุด",
+ "status_table_header": "สถานะ",
+ "sunday": "วันอาทิตย์",
+ "system_host_files": "ไฟล์โฮสต์ระบบ",
+ "table_client": "เครื่องลูกข่าย",
+ "tags_title": "แท็ก",
+ "test_upstreams": "ทดสอบต้นทาง",
+ "threats_blocked": "ปิดกั้นภัยคุกคามแล้ว",
+ "thursday": "พฤหัสบดี",
+ "time_table_header": "เวลา",
+ "top_blocked_domains": "โดเมนที่ถูกปิดกั้นมากที่สุด",
+ "top_clients": "ลูกข่ายที่ใช้งานบ่อยสุด",
+ "top_upstreams": "เซิร์ฟเวอร์ต้นทางที่ดีที่สุด",
+ "total": "รวม",
+ "try_again": "ลองอีกครั้ง",
+ "tuesday": "อังคาร",
+ "unavailable_dhcp": "DHCP ไม่พร้อมใช้งาน",
+ "unavailable_dhcp_desc": "AdGuard Home ไม่สามารถทำงาน DHCP server บนระบบปฎิบัติการของ Server คุณ",
+ "unblock": "เลิกปิดกั้น",
+ "update_button": "อัปเดต",
+ "update_filters_disable": "ปิดใช้งาน",
+ "update_filters_interval_custom": "กำหนดเอง",
+ "update_filters_interval_daily": "ทุกวัน",
+ "update_filters_interval_hourly": "ทุก ๆ ชั่วโมง",
+ "update_filters_title": "อัปเดตตัวกรองโดยอัตโนมัติ",
+ "updates_checked": "มีรุ่นใหม่ของ AdGuard Home พร้อมใช้งาน",
+ "updates_version_equal": "AdGuard Home เป็นตัวล่าสุดแล้ว",
+ "upstream": "เซิร์ฟเวอร์ต้นทาง",
+ "upstream_dns": "เซิร์ฟเวอร์ DNS ต้นทาง",
+ "upstream_dns_addresses": "ที่อยู่เซิร์ฟเวอร์ หนึ่งต่อบรรทัด",
+ "upstream_dns_fastest_addr": "ที่อยู่ IP ที่เร็วที่สุด",
+ "upstream_dns_parallel_requests": "คำขอแบบคู่ขนาน",
+ "upstream_dns_servers_title": "เซิร์ฟเวอร์ DNS ต้นทาง",
+ "upstream_examples_title": "ตัวอย่าง",
+ "upstream_private_ptr_resolvers_title": "ใช้ DNS resolver ส่วนตัวสำหรับ DNS ตรงข้าม",
+ "upstreams": "ต้นทาง",
+ "url_label": "URL",
+ "user_rules": "ตัวกรองผู้ใช้",
+ "user_rules_check_button": "ตรวจสอบ",
+ "user_rules_check_client_placeholder": "ป้อนตัวระบุคล้าย",
+ "user_rules_check_hostname_label": "ชื่อโฮสต์หรือชื่อโดเมน",
+ "user_rules_dns_record_type_placeholder": "เลือกประเภทระเบียน DNS",
+ "user_rules_edit_dns_rewrite": "แก้ไขการเขียน DNS ใหม่",
+ "user_rules_rewrite_rule_is_applied": "กฎการเขียนใหม่ถูกนำมาใช้",
+ "username_label": "ยูเซอร์เนม",
+ "username_placeholder": "กรอกชื่อผู้ใช้",
+ "validated_with_dnssec": "ตรวจสอบกับ DNSSEC",
+ "wednesday": "พุธ",
+ "whois": "Whois",
+ "yes": "ใช่",
+ "yes_disable": "ใช่, ปิดการใช้งาน",
+ "yes_reset": "ใช่ รีเซ็ต"
+}
diff --git a/client_v2/src/__locales/tr.json b/client_v2/src/__locales/tr.json
new file mode 100644
index 00000000000..a16d8b4c4d1
--- /dev/null
+++ b/client_v2/src/__locales/tr.json
@@ -0,0 +1,404 @@
+{
+ "access_settings_allowed_placeholder": "İzin verilen istemcileri girin",
+ "access_settings_blocked_placeholder": "İzin verilmeyen alan adlarını girin",
+ "access_settings_disallowed_placeholder": "İzin verilmeyen istemcileri girin",
+ "access_settings_saved_toast": "Erişim ayarları kaydedildi",
+ "access_settings_title": "Erişim ayarları",
+ "actions_table_header": "Eylemler",
+ "add": "Ekle",
+ "add_allowlist": "İzin listesi ekle",
+ "add_blocklist": "Engel listesi ekle",
+ "add_persistent_client": "Kalıcı istemci olarak ekle",
+ "add_to_allowlist": "İzin listesine ekle",
+ "ads_blocked": "Engellenen reklam",
+ "ads_blocked_card": "Engellenen reklam",
+ "adult_websites_blocked_tooltip": "Engellenen yetişkin içerikli sitelerin sayısı",
+ "all_lists_up_to_date_toast": "Tüm listeler güncel",
+ "allowed": "İzin verilen",
+ "allowlist_add": "İzin listesi ekle",
+ "allowlist_edit": "İzin listesini düzenle",
+ "apply": "Uygula",
+ "autofix_warning_result": "Sonuç olarak, sisteminizden gelen tüm DNS istekleri varsayılan olarak AdGuard Home tarafından işlenecektir.",
+ "autofix_warning_text": "\"Düzelt\" öğesine tıklarsanız, AdGuard Home, sisteminizi AdGuard Home DNS sunucusunu kullanacak şekilde yapılandıracaktır.",
+ "average_time_processing": "Ortalama işlem süresi",
+ "average_upstream_response_time": "Ortalama üst kaynak yanıt süresi",
+ "back": "Geri",
+ "block": "Engelle",
+ "block_for_this_client_only": "Yalnızca bu istemci için engelle",
+ "blocked": "Engellendi",
+ "blocked_by_cname_or_ip": "CNAME veya IP tarafından engellendi",
+ "blocked_service": "Hizmet engellendi",
+ "blocked_services": "Engellenen hizmetler",
+ "blocked_threats": "Tehdit engellendi",
+ "blocked_threats_chart": "Tehdit engellendi",
+ "blocklist_add_from_list": "Listeden ekle",
+ "blocklist_add_manual": "El ile ekle",
+ "blocklist_edit": "Engel listesini düzenle",
+ "blocklist_homepage": "Ana sayfa",
+ "blocklist_url_file_path": "URL veya dosya yolu",
+ "bootstrap_dns": "Ön yükleme DNS sunucuları",
+ "browse": "Gözat",
+ "cache_cleared": "DNS önbelleği başarıyla temizlendi",
+ "cache_config_clear": "Önbelleği temizle",
+ "cache_config_optimistic": "Kesintisiz önbellek",
+ "cache_config_ttl_validation": "Önbellek kullanım değeri belirleme alt sınırı, üst sınıra eşit veya ondan küçük olmalıdır",
+ "cache_enabled": "Önbelleği etkinleştir",
+ "cancel": "İptal",
+ "cancel_btn": "İptal",
+ "category_tooltip": "Kategori",
+ "changes_saved_success": "Değişiklikler kaydedildi",
+ "check_dhcp_servers": "DHCP sunucularını denetle",
+ "check_not_found": "Filtre listelerinizde bulunamadı",
+ "check_updates_btn": "Güncellemeleri denetle",
+ "clear_btn": "Temizle",
+ "client_add": "İstemci ekle",
+ "client_added": "İstemci eklendi",
+ "client_details": "İstemci ayrıntıları",
+ "client_id": "ClientID",
+ "client_id_placeholder": "ClientID girin",
+ "client_identifier": "Tanımlayıcı",
+ "client_removed": "İstemci kaldırıldı",
+ "client_settings": "İstemci ayarları",
+ "client_table_header": "İstemci",
+ "clients_add_identifier": "Tanımlayıcı ekle",
+ "clients_add_placeholder": "İstemci adını girin",
+ "clients_dns_cache_size": "DNS önbellek boyutu, bayt cinsinden",
+ "clients_not_found": "İstemci bulunamadı",
+ "clients_protection": "Koruma",
+ "clients_tags": "Etiketler",
+ "clients_use_global_settings": "Genel ayarları kullan",
+ "close": "Kapat",
+ "confirm": "Doğrula",
+ "copied": "Panoya kopyalandı",
+ "copy": "Kopyala",
+ "copy_to_clipboard": "Panoya kopyala",
+ "custom_filtering_rules": "Özel filtreleme kuralları",
+ "dark_theme": "Koyu tema",
+ "dashboard": "Pano",
+ "delete_btn": "Sil",
+ "delete_table_action": "Sil",
+ "dhcp_add_static_lease": "Sabit atama ekle",
+ "dhcp_config_saved": "DHCP yapılandırması başarıyla kaydedildi",
+ "dhcp_edit_static_lease": "Sabit atamayı düzenle",
+ "dhcp_form_gateway_input": "Ağ geçidi IP",
+ "dhcp_form_subnet_input": "Alt ağ maskesi",
+ "dhcp_leases_not_found": "DHCP ataması bulunamadı",
+ "dhcp_leases_title": "DHCP atamaları",
+ "dhcp_new_static_lease": "Yeni sabit atama",
+ "dhcp_reset_leases": "Tüm atamaları sıfırla",
+ "dhcp_reset_leases_success": "DHCP atamaları başarıyla sıfırlandı",
+ "dhcp_settings": "DHCP ayarları",
+ "dhcp_static_leases_not_found": "Sabit DHCP ataması bulunamadı",
+ "dhcp_table_hostname": "Ana makine Adı",
+ "dhcp_table_ip_address": "IP adresi",
+ "disable": "Devre dışı bırak",
+ "disable_protection_btn": "Korumayı devre dışı bırak",
+ "disabled_dhcp": "DHCP sunucusu devre dışı bırakıldı",
+ "disallow_this_client": "Bu istemciye izin verme",
+ "dns_access_settings_title": "Erişim ayarları",
+ "dns_allowed_clients": "İzin verilen istemciler",
+ "dns_allowed_clients_placeholder": "İzin verilen istemcileri girin",
+ "dns_blocking_mode": "Engelleme modu",
+ "dns_blocking_mode_custom_ip": "Özel IP adresi",
+ "dns_blocking_mode_default": "Varsayılan",
+ "dns_blocking_mode_ipv4_label": "IPv4 engelleme",
+ "dns_blocking_mode_ipv6_label": "IPv6 engelleme",
+ "dns_blocking_mode_nxdomain": "NXDOMAIN",
+ "dns_blocking_mode_refused": "REFUSED",
+ "dns_blocking_mode_title": "Engelleme modu",
+ "dns_bootstrap_dns_placeholder": "IP adresleri",
+ "dns_bootstrap_dns_title": "Ön yükleme DNS sunucuları",
+ "dns_bootstrap_servers": "Ön yükleme DNS sunucuları",
+ "dns_cache_config": "DNS önbellek yapılandırması",
+ "dns_cache_size": "Önbellek boyutu",
+ "dns_cache_size_label": "DNS önbellek boyutu, bayt cinsinden",
+ "dns_cache_size_title": "Önbellek boyutu",
+ "dns_clear_cache": "Önbelleği temizle",
+ "dns_clear_cache_cancel": "İptal",
+ "dns_clear_cache_confirm": "Evet, temizle",
+ "dns_clear_cache_yes": "Evet, temizle",
+ "dns_config": "DNS sunucu yapılandırması",
+ "dns_disallowed_clients": "İzin verilmeyen istemciler",
+ "dns_disallowed_clients_title": "İzin verilmeyen istemciler",
+ "dns_disallowed_domains": "İzin verilmeyen alan adları",
+ "dns_disallowed_domains_title": "İzin verilmeyen alan adları",
+ "dns_edns_custom_placeholder": "IP adresi girin",
+ "dns_fallback_dns_title": "Yedek DNS sunucuları",
+ "dns_fallback_servers": "Yedek DNS sunucuları",
+ "dns_optimistic_caching": "Kesintisiz önbellek",
+ "dns_over_https": "DNS-over-HTTPS",
+ "dns_over_quic": "DNS-over-QUIC",
+ "dns_over_tls": "DNS-over-TLS",
+ "dns_override_max_ttl": "Kullanım değeri üst sınırını belirle",
+ "dns_override_max_ttl_title": "Kullanım değeri üst sınırını belirle",
+ "dns_override_min_ttl": "Kullanım değeri alt sınırını belirle",
+ "dns_override_min_ttl_title": "Kullanım değeri alt sınırını belirle",
+ "dns_privacy": "DNS Gizliliği",
+ "dns_private_reverse_servers_title": "Özel ters DNS sunucuları",
+ "dns_queries": "DNS sorguları",
+ "dns_query": "DNS sorguları",
+ "dns_rate_limit": "Hız sınırı",
+ "dns_rate_limit_allowlist_placeholder": "IP adresleri",
+ "dns_rate_limit_placeholder": "Hız sınırı girin",
+ "dns_rate_limit_title": "Hız sınırı",
+ "dns_rewrites": "DNS yeniden yazımları",
+ "dns_server_addresses": "Sunucu adresleri",
+ "dns_server_addresses_label": "Sunucu adresleri, her satıra bir tane",
+ "dns_server_addresses_title": "Sunucu adresleri",
+ "dns_server_config_title": "DNS sunucu yapılandırması",
+ "dns_settings": "DNS",
+ "dns_test_upstreams": "Üst kaynakları test et",
+ "dns_upstream_servers_title": "Üst kaynak DNS sunucusu",
+ "dns_upstream_timeout": "Üst kaynak zaman aşımı",
+ "dns_upstream_timeout_title": "Üst kaynak zaman aşımı",
+ "dns_upstream_validation_invalid": "Geçersiz biçim",
+ "domain": "Alan adı",
+ "domain_or_client": "Alan adı veya istemci",
+ "download_mobileconfig": "Yapılandırma dosyasını indir",
+ "edit_table_action": "Düzenle",
+ "enable": "Etkinleştir",
+ "enabled_dhcp": "DHCP sunucusu etkinleştirildi",
+ "encryption_certificate_path": "Sertifika dosya yolu",
+ "encryption_certificates": "Sertifikalar",
+ "encryption_certificates_input": "PEM biçimindeki sertifikalarınızı kopyalayıp buraya yapıştırın.",
+ "encryption_certificates_source_content": "Sertifika içeriğini yapıştır",
+ "encryption_certificates_source_path": "Bir sertifika dosyası yolu ayarlayın",
+ "encryption_chain_invalid": "Sertifika zinciri geçersiz",
+ "encryption_chain_valid": "Sertifika zinciri geçerli",
+ "encryption_config_saved": "Şifreleme yapılandırması kaydedildi",
+ "encryption_doq": "DNS-over-QUIC bağlantı noktası",
+ "encryption_dot": "DNS-over-TLS bağlantı noktası",
+ "encryption_https": "HTTPS bağlantı noktası",
+ "encryption_key": "Özel anahtar",
+ "encryption_key_input": "Sertifikanızın PEM biçimli özel anahtarını kopyalayıp buraya yapıştırın.",
+ "encryption_key_source_content": "Özel anahtar içeriğini yapıştır",
+ "encryption_plain_dns": "Düz DNS",
+ "encryption_plain_dns_error": "Düz DNS'i devre dışı bırakmak için en az bir şifrelenmiş DNS protokolünü etkinleştirin",
+ "encryption_private_key_path": "Özel anahtar dosya yolu",
+ "encryption_server": "Sunucu adı",
+ "encryption_server_enter": "Alan adınızı girin",
+ "encryption_title": "Şifreleme",
+ "enter_cache_size": "Önbellek boyutunu bayt türünden girin",
+ "enter_ip_address_placeholder": "IP adresi girin",
+ "error": "Hata",
+ "fallback_dns": "Yedek DNS sunucuları",
+ "filter_category_general": "Genel",
+ "filter_category_general_desc": "Çoğu cihazda izlemeyi ve reklamları engelleyen listeler",
+ "filter_category_other": "Diğer",
+ "filter_category_other_desc": "Diğer engel listeleri",
+ "filter_category_regional": "Bölgesel",
+ "filter_category_regional_desc": "Bölgesel reklamlara ve izleme sunucularına odaklanan listeler",
+ "filter_category_security": "Güvenlik",
+ "filter_category_security_desc": "Kötü amaçlı, kimlik avı ve dolandırıcılık alan adlarını engellemek için özel olarak tasarlanmış listeler",
+ "filters": "Filtreler",
+ "forgot_password": "Parolanızı mı unuttunuz?",
+ "form_enter_blocked_response_ttl": "Engellenen yanıtın kullanım değerini girin (saniye)",
+ "form_error_answer_format": "Sonuç biçimi geçersiz",
+ "form_error_client_id_format": "ClientID yalnızca sayılar, küçük harfler ve kısa çizgiler içermelidir",
+ "form_error_domain_format": "Geçersiz alan adı biçimi",
+ "form_error_equal": "Aynı olmamalıdır",
+ "form_error_format": "Geçersiz biçim",
+ "form_error_gateway_ip": "Kiralama, ağ geçidinin IP adresiyle aynı olamaz",
+ "form_error_ip4_format": "IPv4 adresi geçersiz",
+ "form_error_ip6_format": "IPv6 adresi geçersiz",
+ "form_error_ip_format": "IP adresi geçersiz",
+ "form_error_mac_format": "MAC adresi geçersiz",
+ "form_error_password": "Parolalar uyuşmuyor",
+ "form_error_port": "Geçerli bir bağlantı noktası değeri girin",
+ "form_error_port_range": "80-65535 aralığında geçerli bir bağlantı noktası değeri girin",
+ "form_error_port_unsafe": "Güvenli olmayan bağlantı noktası",
+ "form_error_server_name": "Sunucu adı geçersiz",
+ "form_error_url_format": "Geçerli bir URL veya dosya yolu girin",
+ "friday": "Cuma",
+ "gateway_or_subnet_invalid": "Geçersiz alt ağ maskesi",
+ "general_statistics": "Genel istatistikler",
+ "ignore_domains_title": "Alan adlarını yok say",
+ "inactivity_schedule_add": "Plan ekle",
+ "inactivity_schedule_all_day": "Tüm gün",
+ "inactivity_schedule_edit": "Planı düzenle",
+ "install_auth_confirm": "Parolayı onayla",
+ "install_auth_password": "Parola",
+ "install_auth_password_enter": "Parolayı girin",
+ "install_auth_username": "Kullanıcı adı",
+ "install_auth_username_enter": "Kullanıcı adı girin",
+ "install_saved": "Başarıyla kaydedildi",
+ "install_settings_all_interfaces": "Tüm arayüzler",
+ "install_settings_port": "Bağlantı noktası",
+ "install_static_error": "AdGuard Home, bu ağ arayüzü için otomatik olarak yapılandırılamıyor. Lütfen bunu el ile nasıl yapacağınızla ilgili talimatlara bakın.",
+ "install_static_ok": "İyi haber! Sabit IP adresi zaten yapılandırılmış",
+ "install_step": "Adım",
+ "install_submit_title": "Tebrikler!",
+ "ip_address": "IP adresi",
+ "known_tracker": "Bilinen izleyici",
+ "language": "Dil",
+ "light_theme": "Açık tema",
+ "login": "Giriş yap",
+ "logout": "Çıkış yap",
+ "make_static": "Sabit yap",
+ "milliseconds_abbreviation": "ms",
+ "monday": "Pazartesi",
+ "name": "Adı",
+ "name_label": "Adı",
+ "name_table_header": "Adı",
+ "name_tooltip": "Adı",
+ "next": "Sonraki",
+ "no": "Hayır",
+ "none_text": "Hiçbiri",
+ "nothing_found": "Hiçbir şey bulunamadı",
+ "notify_undo": "Geri al",
+ "notify_updated": "Güncellendi",
+ "open_dashboard": "Panoyu aç",
+ "parental_control": "Ebeveyn Denetimi",
+ "parental_group_cdn": "CDN",
+ "parental_group_development": "Yazılım geliştirme",
+ "parental_group_gaming": "Oyun",
+ "parental_group_hosting": "Web barındırma",
+ "parental_group_privacy": "Gizlilik araçları",
+ "parental_group_shopping": "Alışveriş",
+ "parental_group_social": "Sosyal ağlar",
+ "parental_group_social_network": "Sosyal ağlar",
+ "parental_group_software": "Yazılım geliştirme",
+ "parental_group_streaming": "Canlı yayın akışı",
+ "password_label": "Parola",
+ "password_placeholder": "Parolayı girin",
+ "plain_dns": "Düz DNS",
+ "privacy_policy": "Gizlilik politikası",
+ "processing_time_ms": "%value% ms",
+ "protection": "Koruma",
+ "protocol": "Protokol",
+ "query_log_allowed": "İzin verilen",
+ "query_log_blocked": "Engellendi",
+ "query_log_blocked_services": "Engellenen hizmetler",
+ "query_log_blocked_threats": "Tehdit engellendi",
+ "query_log_custom_filtering_rules": "Özel filtreleme kuralları",
+ "query_log_nothing_available": "Hiçbir şey bulunamadı",
+ "query_log_processed": "İşlendi",
+ "query_log_rewritten": "Yeniden yazıldı",
+ "query_log_safe_search": "Güvenli arama",
+ "query_log_strict_search": "Tam arama için çift tırnak işareti kullanın",
+ "rate_limit_subnet_len_ipv4_error": "IPv4 alt ağ önek uzunluğu 0 ile 32 arasında olmalıdır",
+ "rate_limit_subnet_len_ipv6_error": "IPv6 alt ağ önek uzunluğu 0 ile 128 arasında olmalıdır",
+ "refresh_btn": "Yenile",
+ "refresh_statics": "İstatistikleri yenile",
+ "release_notes": "Sürüm notları",
+ "remove": "Kaldır",
+ "report_an_issue": "Bir sorun bildir",
+ "request_table_header": "İstek",
+ "requests_table_header": "İstekler",
+ "reset": "Sıfırla",
+ "reset_settings": "Ayarları sıfırla",
+ "reset_settings_confirm": "Evet, sıfırla",
+ "response_details": "Yanıt ayrıntıları",
+ "response_time": "Yanıt süresi",
+ "result": "Sonuç",
+ "rewrite_add": "DNS yeniden yazımı ekle",
+ "rewrite_edit": "DNS yeniden yazımı düzenle",
+ "rewrite_hosts_applied": "Hosts dosyası kuralı tarafından yeniden yazıldı",
+ "rewrite_settings_updated": "DNS yeniden yazım ayarları başarıyla güncellendi",
+ "rewrite_updated": "DNS yeniden yazımı başarıyla güncellendi",
+ "rewritten": "Yeniden yazıldı",
+ "rules_label": "Kurallar",
+ "safe_browsing": "Güvenli Gezinti",
+ "safe_search": "Güvenli arama",
+ "saturday": "Cumartesi",
+ "save": "Kaydet",
+ "save_btn": "Kaydet",
+ "search_placeholder": "Ara",
+ "server_config_blocking_mode": "Engelleme modu",
+ "server_config_blocking_mode_ipv4": "IPv4 engelleme",
+ "server_config_blocking_mode_ipv6": "IPv6 engelleme",
+ "server_config_custom_ip": "Özel IP adresi",
+ "server_config_default": "Varsayılan",
+ "server_config_disable_ipv6": "IPv6 adreslerinin çözümlenmesini devre dışı bırak",
+ "server_config_dnssec_enable": "DNSSEC'i etkinleştir",
+ "server_config_edns_enable": "EDNS istemci alt ağını etkinleştir",
+ "server_config_nxdomain": "NXDOMAIN",
+ "server_config_rate_limit_placeholder": "Hız sınırı girin",
+ "server_config_rate_limit_whitelist_faq": "Hız sınırlamasından hariç tutulan IP adresleri",
+ "server_config_refused": "REFUSED",
+ "server_config_subnet_len_ipv4": "IPv4 adresleri için alt ağ önek uzunluğu",
+ "server_config_subnet_len_ipv4_faq": "Hız sınırlaması için kullanılan IPv4 adreslerinin alt ağ önek uzunluğu. Varsayılan 24 değeridir",
+ "server_config_subnet_len_ipv6": "IPv6 adresleri için alt ağ önek uzunluğu",
+ "server_config_subnet_len_ipv6_faq": "Hız sınırlaması için kullanılan IPv6 adreslerinin alt ağ önek uzunluğu. Varsayılan 56 değeridir",
+ "set_static_ip": "Sabit IP adresi olarak ayarla",
+ "settings": "Ayarlar",
+ "settings_browsing_security": "Gezinti koruması",
+ "settings_confirm_clear_statistics": "İstatistikler temizlensin mi?",
+ "settings_custom": "Özel",
+ "settings_general_short": "Genel",
+ "settings_global": "Genel",
+ "settings_notify_changes_saved": "Değişiklikler kaydedildi",
+ "settings_notify_statistics_cleared": "İstatistikler temizlendi",
+ "settings_parental_control": "Ebeveyn denetimi",
+ "settings_rule_syntax": "Kural söz dizimi",
+ "settings_safe_search": "Güvenli arama",
+ "settings_statistics": "İstatistikler",
+ "settings_statistics_clear": "İstatistikleri temizle",
+ "settings_statistics_retention": "İstatistikleri sakla",
+ "settings_yes_clear": "Evet, temizle",
+ "setup_devices_router_title": "Yönlendirici",
+ "setup_dns_privacy_ioc_mac": "iOS ve macOS yapılandırması",
+ "setup_dns_privacy_other_title": "Diğer kullanım alanları",
+ "show_blocked_responses": "Engellendi",
+ "show_processed_responses": "İşlendi",
+ "source_label": "Kaynak",
+ "source_tooltip": "Kaynak",
+ "stats_adult": "Engellenen yetişkin içerikli siteler",
+ "stats_query_domain": "Başlıca sorgulanan alan adları",
+ "status_table_header": "Durum",
+ "sunday": "Pazar",
+ "system_host_files": "Sistem hosts dosyaları",
+ "table_client": "İstemci",
+ "tags_title": "Etiketler",
+ "test_upstreams": "Üst kaynakları test et",
+ "threats_blocked": "Engellenen tehdit",
+ "thursday": "Perşembe",
+ "time_table_header": "Zaman",
+ "top_blocked_domains": "Başlıca engellenen alan adları",
+ "top_clients": "Başlıca istemciler",
+ "top_upstreams": "Başlıca üst kaynaklar",
+ "total": "Toplam",
+ "try_again": "Tekrar dene",
+ "tuesday": "Salı",
+ "unavailable_dhcp": "DHCP kullanılamıyor",
+ "unavailable_dhcp_desc": "AdGuard Home, işletim sisteminizde DHCP sunucusu çalıştıramıyor",
+ "unblock": "Engeli kaldır",
+ "unknown_filter": "Bilinmeyen filtre %filterId%",
+ "update_button": "Güncelle",
+ "update_filters_disable": "Devre dışı bırak",
+ "update_filters_interval_custom": "Özel",
+ "update_filters_interval_daily": "Her gün",
+ "update_filters_interval_hourly": "Her saat",
+ "update_filters_title": "Filtreleri otomatik günceller",
+ "updates_checked": "AdGuard Home'un yeni bir sürümü mevcut",
+ "updates_version_equal": "AdGuard Home güncel",
+ "upstream": "Üst kaynak",
+ "upstream_dns": "Üst kaynak DNS sunucusu",
+ "upstream_dns_addresses": "Sunucu adresleri, her satıra bir tane",
+ "upstream_dns_fastest_addr": "En hızlı IP adresi",
+ "upstream_dns_servers_title": "Üst kaynak DNS sunucusu",
+ "upstream_examples_title": "Örnekler",
+ "upstream_private_ptr_resolvers_title": "Özel ters DNS çözümleyicileri kullan",
+ "upstreams": "Üst kaynak",
+ "url_label": "URL",
+ "use_saved_key": "Mevcut bir özel anahtarı kullan",
+ "user_rules": "Kullanıcı kuralları",
+ "user_rules_add_to_allowlist": "İzin listesine ekle",
+ "user_rules_check_button": "Denetle",
+ "user_rules_check_client_placeholder": "İstemci tanımlayıcısını girin",
+ "user_rules_check_hostname_label": "Ana makine adı veya alan adı",
+ "user_rules_disable_filter": "Filtreyi devre dışı bırak",
+ "user_rules_disable_parental_control": "Ebeveyn Denetimini devre dışı bırak",
+ "user_rules_dns_record_type_placeholder": "DNS kayıt türünü seçin",
+ "user_rules_domain_blocked": "Alan adı engellendi",
+ "user_rules_edit_dns_rewrite": "DNS yeniden yazımı düzenle",
+ "user_rules_rewrite_rule_is_applied": "Yeniden yazım kuralı uygulandı",
+ "username_label": "Kullanıcı adı",
+ "username_placeholder": "Kullanıcı adı girin",
+ "validated_with_dnssec": "DNSSEC ile doğrulandı",
+ "wednesday": "Çarşamba",
+ "whois": "WHOIS",
+ "yes": "Evet",
+ "yes_disable": "Evet, devre dışı bırak",
+ "yes_reset": "Evet, sıfırla"
+}
diff --git a/client_v2/src/__locales/uk.json b/client_v2/src/__locales/uk.json
new file mode 100644
index 00000000000..ab29162732c
--- /dev/null
+++ b/client_v2/src/__locales/uk.json
@@ -0,0 +1,390 @@
+{
+ "access_settings_title": "Налаштування доступу",
+ "actions_table_header": "Дії",
+ "add": "Додати",
+ "add_allowlist": "Додати список дозволів",
+ "add_blocklist": "Додати список блокування",
+ "add_persistent_client": "Додати в збережені клієнти",
+ "add_to_allowlist": "Додати до Списку дозволів",
+ "ads_blocked": "Заблоковано реклами",
+ "ads_blocked_card": "Заблоковано реклами",
+ "adult_websites_blocked_tooltip": "Кількість заблокованих вебсайтів для дорослих",
+ "all_lists_up_to_date_toast": "Всі списки вже оновлені",
+ "allowed": "Дозволені",
+ "allowlist_add": "Додати список дозволів",
+ "allowlist_edit": "Змінити список дозволів",
+ "apply": "Застосувати",
+ "autofix_warning_result": "В результаті буде усталено, що усі DNS-запити вашої системи будуть опрацьовані AdGuard Home.",
+ "autofix_warning_text": "Якщо ви натиснете «Виправити», AdGuard Home налаштує вашу систему на використання DNS-сервера AdGuard Home.",
+ "average_time_processing": "Середній час обробки",
+ "average_upstream_response_time": "Середній час відгуку upstream-сервера",
+ "back": "Назад",
+ "block": "Заблокувати",
+ "block_for_this_client_only": "Заборонити тільки цей клієнт",
+ "blocked": "Заблоковано",
+ "blocked_by_cname_or_ip": "Заблоковано по CNAME або IP",
+ "blocked_service": "Заблокований сервіс",
+ "blocked_services": "Заблоковані сервіси",
+ "blocked_threats": "Заблоковано загроз",
+ "blocked_threats_chart": "Заблоковано загроз",
+ "blocklist_add_manual": "Додати вручну",
+ "blocklist_edit": "Змінити список блокування",
+ "blocklist_homepage": "Домашня сторінка",
+ "blocklist_url_file_path": "URL або шлях до файлу",
+ "bootstrap_dns": "Bootstrap DNS-сервери",
+ "browse": "Відкрити...",
+ "cache_cleared": "Кеш DNS успішно очищено",
+ "cache_config_clear": "Очистити кеш",
+ "cache_config_optimistic": "Оптимістичне кешування",
+ "cache_config_ttl_validation": "Мінімальне TTL-значення має бути меншим або рівним максимальному значенню",
+ "cache_enabled": "Увімкнути кеш",
+ "cancel": "Скасувати",
+ "cancel_btn": "Скасувати",
+ "category_tooltip": "Категорія",
+ "changes_saved_success": "Зміни збережено",
+ "check_dhcp_servers": "Перевірити DHCP-сервери",
+ "check_not_found": "Не знайдено у ваших списках фільтрів",
+ "check_updates_btn": "Перевірити оновлення",
+ "clear_btn": "Очистити",
+ "client_add": "Додати Клієнта",
+ "client_details": "Подробиці про клієнта",
+ "client_id": "ClientID",
+ "client_id_placeholder": "Введіть ClientID",
+ "client_identifier": "Ідентифікатор",
+ "client_settings": "Налаштування клієнта",
+ "client_table_header": "Клієнт",
+ "clients_add_identifier": "Додати ідентифікатор",
+ "clients_add_placeholder": "Введіть ім'я клієнта",
+ "clients_dns_cache_size": "Розмір кешу DNS, у байтах",
+ "clients_not_found": "Клієнтів не знайдено",
+ "clients_protection": "Захист",
+ "clients_tags": "Теги",
+ "clients_use_global_settings": "Використати загальні налаштування",
+ "close": "Закрити",
+ "confirm": "Підтвердити",
+ "copied": "Скопійовано в буфер обміну",
+ "copy": "Скопіювати",
+ "copy_to_clipboard": "Копіювати в буфер обміну",
+ "custom_filtering_rules": "Власні правила фільтрування",
+ "dark_theme": "Темна тема",
+ "dashboard": "Панель керування",
+ "delete_btn": "Видалити",
+ "delete_table_action": "Видалити",
+ "dhcp_add_static_lease": "Додати статичну оренду",
+ "dhcp_config_saved": "Конфігурацію DHCP-сервера успішно збережено",
+ "dhcp_edit_static_lease": "Редагувати статичну оренду",
+ "dhcp_form_gateway_input": "IP-адреса шлюзу",
+ "dhcp_form_subnet_input": "Маска підмережі",
+ "dhcp_leases_not_found": "Оренду DHCP не знайдено",
+ "dhcp_leases_title": "Оренда DHCP",
+ "dhcp_new_static_lease": "Нова статична оренда",
+ "dhcp_reset_leases": "Скинути всі аренди",
+ "dhcp_reset_leases_success": "Оренду DHCP успішно скинуто",
+ "dhcp_settings": "Налаштування DHCP",
+ "dhcp_static_leases_not_found": "Не знайдено статичних оренд DHCP",
+ "dhcp_table_hostname": "Назва вузла",
+ "dhcp_table_ip_address": "IP-адреса",
+ "disable": "Вимкнути",
+ "disable_protection_btn": "Вимкнути захист",
+ "disabled_dhcp": "DHCP-сервер вимкнено",
+ "disallow_this_client": "Заборонити цього клієнта",
+ "dns_access_settings_title": "Налаштування доступу",
+ "dns_allowed_clients": "Дозволені клієнти",
+ "dns_blocking_mode": "Режим блокування",
+ "dns_blocking_mode_custom_ip": "Власна IP-адреса",
+ "dns_blocking_mode_default": "Усталений",
+ "dns_blocking_mode_ipv4_label": "Блокування IPv4",
+ "dns_blocking_mode_ipv6_label": "Блокування IPv6",
+ "dns_blocking_mode_nxdomain": "NXDOMAIN",
+ "dns_blocking_mode_refused": "REFUSED",
+ "dns_blocking_mode_title": "Режим блокування",
+ "dns_bootstrap_dns_placeholder": "IP-адреси",
+ "dns_bootstrap_dns_title": "Bootstrap DNS-сервери",
+ "dns_bootstrap_servers": "Bootstrap DNS-сервери",
+ "dns_cache_config": "Конфігурація кешу DNS",
+ "dns_cache_size": "Розмір кешу",
+ "dns_cache_size_label": "Розмір кешу DNS, у байтах",
+ "dns_cache_size_title": "Розмір кешу",
+ "dns_clear_cache": "Очистити кеш",
+ "dns_clear_cache_cancel": "Скасувати",
+ "dns_clear_cache_confirm": "Так, очистити",
+ "dns_clear_cache_yes": "Так, очистити",
+ "dns_config": "Конфігурація DNS-сервера",
+ "dns_disallowed_clients": "Заборонені клієнти",
+ "dns_disallowed_clients_title": "Заборонені клієнти",
+ "dns_disallowed_domains": "Заборонені домени",
+ "dns_disallowed_domains_title": "Заборонені домени",
+ "dns_edns_custom_placeholder": "Введіть IP-адресу",
+ "dns_fallback_dns_title": "Резервні DNS-сервери",
+ "dns_fallback_servers": "Резервні DNS-сервери",
+ "dns_optimistic_caching": "Оптимістичне кешування",
+ "dns_over_https": "DNS-over-HTTPS",
+ "dns_over_quic": "DNS-over-QUIC",
+ "dns_over_tls": "DNS-over-TLS",
+ "dns_override_max_ttl": "Змінити максимальний TTL",
+ "dns_override_max_ttl_title": "Змінити максимальний TTL",
+ "dns_override_min_ttl": "Змінити мінімальний TTL",
+ "dns_override_min_ttl_title": "Змінити мінімальний TTL",
+ "dns_privacy": "Конфіденційність DNS",
+ "dns_private_reverse_servers_title": "Приватні сервери для зворотного DNS",
+ "dns_rate_limit": "Обмеження швидкості",
+ "dns_rate_limit_allowlist_placeholder": "IP-адреси",
+ "dns_rate_limit_placeholder": "Уведіть обмеження швидкості",
+ "dns_rate_limit_title": "Обмеження швидкості",
+ "dns_rewrites": "DNS перезаписи",
+ "dns_server_addresses": "Адреси серверів",
+ "dns_server_addresses_label": "Адреси серверів, по одній на рядок",
+ "dns_server_addresses_title": "Адреси серверів",
+ "dns_server_config_title": "Конфігурація DNS-сервера",
+ "dns_settings": "DNS",
+ "dns_test_upstreams": "Перевірити сервери",
+ "dns_upstream_servers_title": "Upstream DNS-сервери",
+ "dns_upstream_timeout": "Час вийшов для upstream",
+ "dns_upstream_timeout_title": "Час вийшов для upstream",
+ "dns_upstream_validation_invalid": "Неприпустимий формат",
+ "domain": "Домен",
+ "domain_or_client": "Домен чи клієнт",
+ "download_mobileconfig": "Завантажити файл конфігурації",
+ "edit_table_action": "Редагувати",
+ "enable": "Увімкнути",
+ "enabled_dhcp": "DHCP-сервер увімкнено",
+ "encryption_certificate_path": "Шлях до сертифіката",
+ "encryption_certificates": "Сертифікати",
+ "encryption_certificates_input": "Скопіюйте/вставте сюди свої кодовані PEM сертифікати.",
+ "encryption_certificates_source_content": "Вставити вміст сертифікату",
+ "encryption_certificates_source_path": "Вказати шлях до сертифікату",
+ "encryption_chain_invalid": "Ланцюжок довіри сертифікатів не дійсний",
+ "encryption_chain_valid": "Ланцюжок довіри сертифікатів дійсний",
+ "encryption_config_saved": "Конфігурацію шифрування збережено",
+ "encryption_doq": "Порт DNS-over-QUIC",
+ "encryption_dot": "Порт DNS-over-TLS",
+ "encryption_https": "Порт HTTPS",
+ "encryption_key": "Приватний ключ",
+ "encryption_key_input": "Скопіюйте/вставте сюди свій приватний ключ кодований PEM для вашого сертифіката.",
+ "encryption_key_source_content": "Вставити вміст приватного ключа",
+ "encryption_plain_dns": "Звичайний DNS",
+ "encryption_plain_dns_error": "Щоб вимкнути звичайний DNS, увімкніть принаймні один зашифрований протокол DNS",
+ "encryption_private_key_path": "Шлях до приватного ключа",
+ "encryption_server": "Назва сервера",
+ "encryption_server_enter": "Введіть ваше доменне ім'я",
+ "encryption_title": "Шифрування",
+ "enter_cache_size": "Введіть розмір кешу (байт)",
+ "enter_ip_address_placeholder": "Введіть IP-адресу",
+ "error": "Помилка",
+ "fallback_dns": "Резервні DNS-сервери",
+ "filter_category_general": "General",
+ "filter_category_general_desc": "Списки, які блокують відстеження та рекламу на більшості пристроїв",
+ "filter_category_other": "Інші",
+ "filter_category_other_desc": "Інші списки блокувань",
+ "filter_category_regional": "Регіональні",
+ "filter_category_regional_desc": "Списки, орієнтовані на регіональні оголошення та сервери відстеження",
+ "filter_category_security": "Безпека",
+ "filter_category_security_desc": "Фільтри, які спеціалізуються на блокуванні зловмисних програм, фішингу та шахрайських доменів",
+ "filters": "Фільтри",
+ "forgot_password": "Забули пароль?",
+ "form_enter_blocked_response_ttl": "Введіть TTL заблокованої відповіді (секунди)",
+ "form_error_client_id_format": "ID клієнта має містити лише цифри, малі букви та дефіси",
+ "form_error_domain_format": "Неправильний формат домену",
+ "form_error_equal": "Мають бути різні значення",
+ "form_error_format": "Неприпустимий формат",
+ "form_error_gateway_ip": "Оренда не може мати IP-адресу шлюзу",
+ "form_error_ip4_format": "Неправильна IPv4-адреса",
+ "form_error_ip6_format": "Неправильна IPv6-адреса",
+ "form_error_ip_format": "Недійсна IP-адреса",
+ "form_error_mac_format": "Неправильна MAC-адреса",
+ "form_error_password": "Паролі не збігаються",
+ "form_error_port": "Уведіть правильне значення порту",
+ "form_error_port_range": "Введіть значення порту в діапазоні 80−65535",
+ "form_error_port_unsafe": "Небезпечний порт",
+ "form_error_server_name": "Неправильна назва сервера",
+ "form_error_url_format": "Введіть дійсну URL-адресу або шлях до файлу",
+ "friday": "П'ятниця",
+ "gateway_or_subnet_invalid": "Неправильна маска підмережі",
+ "general_statistics": "Загальна статистика",
+ "ignore_domains_title": "Ігноровані домени",
+ "inactivity_schedule_add": "Додати розклад",
+ "inactivity_schedule_all_day": "Увесь день",
+ "inactivity_schedule_edit": "Редагувати розклад",
+ "install_auth_confirm": "Повторіть новий пароль",
+ "install_auth_password": "Пароль",
+ "install_auth_password_enter": "Уведіть пароль",
+ "install_auth_username": "Ім'я користувача",
+ "install_auth_username_enter": "Уведіть ім'я користувача",
+ "install_saved": "Збережено успішно",
+ "install_settings_all_interfaces": "Усі інтерфейси",
+ "install_settings_port": "Порт",
+ "install_static_error": "AdGuard Home не може налаштувати його автоматично для цього мережевого інтерфейсу. Будь ласка, шукайте інструкції як це зробити вручну.",
+ "install_static_ok": "Гарні новини! Статична IP-адреса вже налаштована",
+ "install_step": "Крок",
+ "install_submit_title": "Вітаємо!",
+ "ip_address": "IP-адреса",
+ "known_tracker": "Відомі трекери",
+ "language": "Мова",
+ "light_theme": "Світла тема",
+ "login": "Увійти",
+ "logout": "Вийти",
+ "make_static": "Зробити статичним",
+ "milliseconds_abbreviation": "мс",
+ "monday": "Понеділок",
+ "name": "Назва",
+ "name_label": "Назва",
+ "name_table_header": "Назва",
+ "name_tooltip": "Назва",
+ "next": "Далі",
+ "no": "Ні",
+ "none_text": "Немає",
+ "nothing_found": "Нічого не знайдено...",
+ "notify_undo": "Скасувати",
+ "notify_updated": "Оновлено",
+ "parental_control": "Батьківський контроль",
+ "parental_group_cdn": "CDN",
+ "parental_group_development": "Розробка програмного забезпечення",
+ "parental_group_gaming": "Геймінг",
+ "parental_group_hosting": "Вебхостинг",
+ "parental_group_privacy": "Інструменти конфіденційності",
+ "parental_group_shopping": "Магазини",
+ "parental_group_social": "Соціальні мережі",
+ "parental_group_social_network": "Соціальні мережі",
+ "parental_group_software": "Розробка програмного забезпечення",
+ "parental_group_streaming": "Стрімінг",
+ "password_label": "Пароль",
+ "password_placeholder": "Уведіть пароль",
+ "plain_dns": "Звичайний DNS",
+ "privacy_policy": "Політика конфіденційності",
+ "protection": "Захист",
+ "protocol": "Протокол",
+ "query_log_allowed": "Дозволені",
+ "query_log_blocked": "Заблоковано",
+ "query_log_blocked_services": "Заблоковані сервіси",
+ "query_log_blocked_threats": "Заблоковано загроз",
+ "query_log_custom_filtering_rules": "Власні правила фільтрування",
+ "query_log_nothing_available": "Нічого не знайдено...",
+ "query_log_processed": "Оброблено",
+ "query_log_rewritten": "Перезаписано",
+ "query_log_safe_search": "Безпечний пошук",
+ "query_log_strict_search": "Використовуйте подвійні лапки для точного пошуку",
+ "rate_limit_subnet_len_ipv4_error": "Довжина префікса підмережі IPv4 має бути від 0 до 32",
+ "rate_limit_subnet_len_ipv6_error": "Довжина префікса підмережі IPv6 має бути від 0 до 128",
+ "refresh_btn": "Оновити",
+ "refresh_statics": "Оновити статистику",
+ "release_notes": "Примітки до випуску",
+ "remove": "Видалити",
+ "report_an_issue": "Повідомити про проблему",
+ "request_table_header": "Запит",
+ "requests_table_header": "Запити",
+ "reset": "Скинути",
+ "reset_settings": "Скинути налаштування",
+ "reset_settings_confirm": "Так, скинути",
+ "response_details": "Деталі відповіді",
+ "response_time": "Час відгуку",
+ "result": "Результат",
+ "rewrite_add": "Додати перезапис DNS",
+ "rewrite_edit": "Редагувати перезапис DNS",
+ "rewrite_hosts_applied": "Перезаписано правилом hosts-файлу",
+ "rewrite_settings_updated": "Налаштування перезапису DNS успішно оновлено",
+ "rewrite_updated": "Перезапис DNS успішно оновлено",
+ "rewritten": "Перезаписано",
+ "rules_label": "Правила",
+ "safe_browsing": "Безпечний перегляд",
+ "safe_search": "Безпечний пошук",
+ "saturday": "Субота",
+ "save": "Зберегти",
+ "save_btn": "Зберегти",
+ "search_placeholder": "Пошук",
+ "server_config_blocking_mode": "Режим блокування",
+ "server_config_blocking_mode_ipv4": "Блокування IPv4",
+ "server_config_blocking_mode_ipv6": "Блокування IPv6",
+ "server_config_custom_ip": "Власна IP-адреса",
+ "server_config_default": "Усталений",
+ "server_config_disable_ipv6": "Вимкнути вирішення IPv6-адрес",
+ "server_config_dnssec_enable": "Увімкнути DNSSEC",
+ "server_config_edns_enable": "Увімкнути відправку EDNS Client Subnet",
+ "server_config_nxdomain": "NXDOMAIN",
+ "server_config_rate_limit_placeholder": "Уведіть обмеження швидкості",
+ "server_config_rate_limit_whitelist_faq": "IP-адреси, на які не поширюється обмеження швидкості",
+ "server_config_refused": "REFUSED",
+ "server_config_subnet_len_ipv4": "Довжина префікса підмережі для адрес IPv4",
+ "server_config_subnet_len_ipv4_faq": "Довжина префікса підмережі для адрес IPv4, які використовуються для обмеження швидкості. Типовим значенням є 24",
+ "server_config_subnet_len_ipv6": "Довжина префікса підмережі для адрес IPv6",
+ "server_config_subnet_len_ipv6_faq": "Довжина префікса підмережі для адрес IPv6, які використовуються для обмеження швидкості. Типовим значенням є 56",
+ "set_static_ip": "Встановити статичну IP-адресу",
+ "settings": "Налаштування",
+ "settings_browsing_security": "Безпека перегляду",
+ "settings_confirm_clear_statistics": "Очистити статистику?",
+ "settings_custom": "Інше",
+ "settings_general_short": "General",
+ "settings_global": "Загальні",
+ "settings_notify_changes_saved": "Зміни збережено",
+ "settings_notify_statistics_cleared": "Статистика очищена",
+ "settings_parental_control": "Батьківський контроль",
+ "settings_rule_syntax": "Синтаксис",
+ "settings_safe_search": "Безпечний пошук",
+ "settings_statistics": "Статистика",
+ "settings_statistics_clear": "Очистити статистику",
+ "settings_statistics_retention": "Збереження статистики",
+ "settings_yes_clear": "Так, очистити",
+ "setup_devices_router_title": "Роутер",
+ "setup_dns_privacy_ioc_mac": "Конфігурація для iOS та macOS",
+ "setup_dns_privacy_other_title": "Інші реалізації",
+ "show_blocked_responses": "Заблоковано",
+ "show_processed_responses": "Оброблено",
+ "source_label": "Джерело",
+ "source_tooltip": "Джерело",
+ "stats_adult": "Заблоковано вебсайтів для дорослих",
+ "stats_query_domain": "Найчастіші запити доменів",
+ "status_table_header": "Статус",
+ "sunday": "Неділя",
+ "system_host_files": "Системні hosts-файли",
+ "table_client": "Клієнт",
+ "tags_title": "Теги",
+ "test_upstreams": "Перевірити сервери",
+ "threats_blocked": "Заблоковано загроз",
+ "thursday": "Четвер",
+ "time_table_header": "Час",
+ "top_blocked_domains": "Найчастіше блоковані домени",
+ "top_clients": "Найактивніші клієнти",
+ "top_upstreams": "Часто запитувані upstream-сервери",
+ "total": "Усе",
+ "try_again": "Спробувати знову",
+ "tuesday": "Вівторок",
+ "unavailable_dhcp": "DHCP недоступний",
+ "unavailable_dhcp_desc": "AdGuard Home не може запустити DHCP-сервер у вашій ОС",
+ "unblock": "Розблокувати",
+ "update_button": "Встановити",
+ "update_filters_disable": "Вимкнути",
+ "update_filters_interval_custom": "Інше",
+ "update_filters_interval_daily": "Щодня",
+ "update_filters_interval_hourly": "Щогодини",
+ "update_filters_title": "Оновлювати фільтри автоматично",
+ "updates_checked": "Доступна нова версія AdGuard Home",
+ "updates_version_equal": "AdGuard Home останньої версії",
+ "upstream": "Upstream-сервер",
+ "upstream_dns": "Upstream DNS-сервери",
+ "upstream_dns_addresses": "Адреси серверів, по одній на рядок",
+ "upstream_dns_fastest_addr": "Найшвидша IP-адреса",
+ "upstream_dns_parallel_requests": "Паралельні запити",
+ "upstream_dns_servers_title": "Upstream DNS-сервери",
+ "upstream_examples_title": "Зразки",
+ "upstream_private_ptr_resolvers_title": "Використовувати приватні зворотні DNS-резолвери",
+ "upstreams": "Upstreams",
+ "url_label": "URL",
+ "user_rules": "Правила користувача",
+ "user_rules_add_to_allowlist": "Додати до Списку дозволів",
+ "user_rules_check_button": "Перевірити",
+ "user_rules_check_client_placeholder": "Введіть ідентифікатор клієнта",
+ "user_rules_check_hostname_label": "Ім'я хоста або доменне ім'я",
+ "user_rules_disable_filter": "Вимкнути фільтр",
+ "user_rules_disable_parental_control": "Вимкнути «Батьківський контроль»",
+ "user_rules_dns_record_type_placeholder": "Виберіть тип DNS запису",
+ "user_rules_domain_blocked": "Домен заблоковано",
+ "user_rules_edit_dns_rewrite": "Редагувати перезапис DNS",
+ "user_rules_rewrite_rule_is_applied": "Застосовано правило перезапису",
+ "username_label": "Ім'я користувача",
+ "username_placeholder": "Уведіть ім'я користувача",
+ "validated_with_dnssec": "Засвідчено DNSSEC",
+ "wednesday": "Середа",
+ "whois": "WHOIS",
+ "yes": "Так",
+ "yes_disable": "Так, вимкнути",
+ "yes_reset": "Так, скинути"
+}
diff --git a/client_v2/src/__locales/vi.json b/client_v2/src/__locales/vi.json
new file mode 100644
index 00000000000..bfa68cd743d
--- /dev/null
+++ b/client_v2/src/__locales/vi.json
@@ -0,0 +1,390 @@
+{
+ "access_settings_title": "Cài đặt truy cập",
+ "actions_table_header": "Thao tác",
+ "add": "Thêm",
+ "add_allowlist": "Thêm danh sách",
+ "add_blocklist": "Thêm danh sách",
+ "add_persistent_client": "Thêm làm ứng dụng khách liên tục",
+ "add_to_allowlist": "Thêm vào danh sách cho phép",
+ "ads_blocked": "Quảng cáo bị chặn",
+ "ads_blocked_card": "Quảng cáo bị chặn",
+ "adult_websites_blocked_tooltip": "Số trang web người lớn đã chặn",
+ "all_lists_up_to_date_toast": "Tất cả danh sách đã ở phiên bản mới nhất",
+ "allowed": "Đã thêm vào danh sách cho phép",
+ "allowlist_add": "Thêm danh sách",
+ "allowlist_edit": "Chỉnh sửa danh sách cho phép",
+ "apply": "Áp dụng",
+ "autofix_warning_result": "Do đó, tất cả các yêu cầu DNS từ hệ thống của bạn sẽ được AdGuard Home xử lý theo mặc định.",
+ "autofix_warning_text": "Nếu bạn nhấp vào \"Khắc phục\", AdGuard Home sẽ định cấu hình hệ thống của bạn để sử dụng máy chủ DNS của AdGuard Home.",
+ "average_time_processing": "Thời gian xử lý trung bình",
+ "average_upstream_response_time": "Thời gian phản hồi trung bình từ máy chủ thượng nguồn",
+ "back": "Back",
+ "block": "Chặn",
+ "block_for_this_client_only": "Chỉ chặn ứng dụng khách này",
+ "blocked": "Bị chặn",
+ "blocked_by_cname_or_ip": "Đã bị chặn bởi CNAME hoặc IP",
+ "blocked_service": "Dịch vụ bị chặn",
+ "blocked_services": "Dịch vụ bị chặn",
+ "blocked_threats": "Mối nguy hiểm đã chặn",
+ "blocked_threats_chart": "Mối nguy hiểm đã chặn",
+ "blocklist_add_manual": "Thêm thủ công",
+ "blocklist_edit": "Chỉnh sửa danh sách chặn",
+ "blocklist_homepage": "Trang chủ",
+ "blocklist_url_file_path": "URL hoặc đường dẫn tệp",
+ "bootstrap_dns": "Máy chủ DNS Bootstrap",
+ "browse": "Duyệt",
+ "cache_cleared": "Đã xóa thành công bộ đệm DNS",
+ "cache_config_clear": "Xóa bộ nhớ cache",
+ "cache_config_optimistic": "Bộ nhớ đệm lạc quan",
+ "cache_config_ttl_validation": "Giá trị TTL trong bộ nhớ cache tối thiểu phải nhỏ hơn hoặc bằng giá trị lớn nhất",
+ "cache_enabled": "Bật bộ nhớ đệm",
+ "cancel": "Huỷ",
+ "cancel_btn": "Huỷ",
+ "category_tooltip": "Thể loại",
+ "changes_saved_success": "Các thay đổi đã được cứu",
+ "check_dhcp_servers": "Kiểm tra máy chủ DHCP",
+ "check_not_found": "Không tìm thấy trong danh sách bộ lọc của bạn",
+ "check_updates_btn": "Kiểm tra cập nhật",
+ "clear_btn": "Xoá",
+ "client_add": "Thêm Máy Khách",
+ "client_details": "Thông tin máy khách",
+ "client_id": "ClientID",
+ "client_id_placeholder": "Nhập một ClientID",
+ "client_identifier": "Định danh",
+ "client_settings": "Cài đặt thiết bị",
+ "client_table_header": "Người dùng",
+ "clients_add_identifier": "Thêm định danh",
+ "clients_add_placeholder": "Nhập tên máy khách",
+ "clients_dns_cache_size": "Kích thước bộ nhớ cache DNS, tính bằng byte",
+ "clients_not_found": "Không có người dùng",
+ "clients_protection": "Sự bảo vệ",
+ "clients_tags": "Thẻ",
+ "clients_use_global_settings": "Sử dụng cài đặt toàn cầu",
+ "close": "Đóng",
+ "confirm": "Xác nhận",
+ "copied": "Sao chép vào clipboard",
+ "copy": "Chép",
+ "copy_to_clipboard": "Chép vào clipboard",
+ "custom_filtering_rules": "Quy tắc lọc tuỳ chỉnh",
+ "dark_theme": "Chủ đề tối",
+ "dashboard": "Tổng quan",
+ "delete_btn": "Xoá",
+ "delete_table_action": "Xoá",
+ "dhcp_add_static_lease": "Thêm thuê tĩnh",
+ "dhcp_config_saved": "Đã lưu cấu hình máy chủ DHCP",
+ "dhcp_edit_static_lease": "Chỉnh sửa hợp đồng thuê tĩnh",
+ "dhcp_form_gateway_input": "Cổng IP",
+ "dhcp_form_subnet_input": "Mặt nạ mạng con",
+ "dhcp_leases_not_found": "Không tìm thấy DHCP cho thuê",
+ "dhcp_leases_title": "Thuê DHCP",
+ "dhcp_new_static_lease": "Cho thuê tĩnh mới",
+ "dhcp_reset_leases": "Đặt lại tất cả các hợp đồng thuê",
+ "dhcp_reset_leases_success": "DHCP cho thuê đã đặt lại thành công",
+ "dhcp_settings": "Cài đặt DHCP",
+ "dhcp_static_leases_not_found": "Không tìm thấy DHCP cho thuê tĩnh",
+ "dhcp_table_hostname": "Tên máy chủ",
+ "dhcp_table_ip_address": "Địa chỉ IP",
+ "disable": "Vô hiệu",
+ "disable_protection_btn": "Vô hiệu bảo vệ",
+ "disabled_dhcp": "Máy chủ DHCP đã tắt",
+ "disallow_this_client": "Không cho phép client này",
+ "dns_access_settings_title": "Cài đặt truy cập",
+ "dns_allowed_clients": "Máy chủ được phép",
+ "dns_blocking_mode": "Chế độ chặn",
+ "dns_blocking_mode_custom_ip": "Địa chỉ IP tùy chỉnh",
+ "dns_blocking_mode_default": "Mặc định",
+ "dns_blocking_mode_ipv4_label": "Chặn IPv4",
+ "dns_blocking_mode_ipv6_label": "Chặn IPv6",
+ "dns_blocking_mode_nxdomain": "NXDOMAIN",
+ "dns_blocking_mode_refused": "REFUSED",
+ "dns_blocking_mode_title": "Chế độ chặn",
+ "dns_bootstrap_dns_placeholder": "Các địa chỉ IP",
+ "dns_bootstrap_dns_title": "Máy chủ DNS Bootstrap",
+ "dns_bootstrap_servers": "Máy chủ DNS Bootstrap",
+ "dns_cache_config": "Cấu hình cache DNS",
+ "dns_cache_size": "Kích thước cache",
+ "dns_cache_size_label": "Kích thước bộ nhớ cache DNS, tính bằng byte",
+ "dns_cache_size_title": "Kích thước cache",
+ "dns_clear_cache": "Xóa bộ nhớ cache",
+ "dns_clear_cache_cancel": "Huỷ",
+ "dns_clear_cache_confirm": "Vâng, rõ ràng",
+ "dns_clear_cache_yes": "Vâng, rõ ràng",
+ "dns_config": "Thiết lập máy chủ DNS",
+ "dns_disallowed_clients": "Máy chủ không được phép",
+ "dns_disallowed_clients_title": "Máy chủ không được phép",
+ "dns_disallowed_domains": "Tên miền bị chặn",
+ "dns_disallowed_domains_title": "Tên miền bị chặn",
+ "dns_edns_custom_placeholder": "Nhập địa chỉ IP",
+ "dns_fallback_dns_title": "Máy chủ DNS dự phòng",
+ "dns_fallback_servers": "Máy chủ DNS dự phòng",
+ "dns_optimistic_caching": "Bộ nhớ đệm lạc quan",
+ "dns_over_https": "DNS-over-HTTPS",
+ "dns_over_quic": "DNS-over-QUIC",
+ "dns_over_tls": "DNS-over-TLS",
+ "dns_override_max_ttl": "Ghi đè TTL tối đa",
+ "dns_override_max_ttl_title": "Ghi đè TTL tối đa",
+ "dns_override_min_ttl": "Ghi đè TTL tối thiểu",
+ "dns_override_min_ttl_title": "Ghi đè TTL tối thiểu",
+ "dns_privacy": "DNS Riêng Tư",
+ "dns_private_reverse_servers_title": "Máy chủ DNS riêng tư",
+ "dns_rate_limit": "Giới hạn yêu cầu",
+ "dns_rate_limit_allowlist_placeholder": "Các địa chỉ IP",
+ "dns_rate_limit_placeholder": "Nhập giới hạn yêu cầu",
+ "dns_rate_limit_title": "Giới hạn yêu cầu",
+ "dns_rewrites": "DNS viết lại",
+ "dns_server_addresses": "Địa chỉ máy chủ",
+ "dns_server_addresses_label": "Địa chỉ máy chủ, mỗi địa chỉ một dòng",
+ "dns_server_addresses_title": "Địa chỉ máy chủ",
+ "dns_server_config_title": "Thiết lập máy chủ DNS",
+ "dns_settings": "DNS",
+ "dns_test_upstreams": "Kiểm tra",
+ "dns_upstream_servers_title": "Máy chủ DNS tìm kiếm",
+ "dns_upstream_timeout": "Thời gian chờ ngược dòng",
+ "dns_upstream_timeout_title": "Thời gian chờ ngược dòng",
+ "dns_upstream_validation_invalid": "Định dạng không hợp lệ",
+ "domain": "Tên miền",
+ "domain_or_client": "Tên miền hoặc khách hàng",
+ "download_mobileconfig": "Tải xuống tệp cấu hình",
+ "edit_table_action": "Chỉnh sửa",
+ "enable": "Kích hoạt",
+ "enabled_dhcp": "Máy chủ DHCP đã kích hoạt",
+ "encryption_certificate_path": "Đường dẫn chứng chỉ",
+ "encryption_certificates": "Chứng chỉ",
+ "encryption_certificates_input": "Sao chép/dán chứng chỉ được mã hóa PEM của bạn tại đây.",
+ "encryption_certificates_source_content": "Dán nội dung chứng chỉ",
+ "encryption_certificates_source_path": "Đặt đường dẫn tệp chứng chỉ",
+ "encryption_chain_invalid": "Chứng chỉ không hợp lệ",
+ "encryption_chain_valid": "Chứng chỉ hợp lệ",
+ "encryption_config_saved": "Đã lưu cấu hình mã hóa",
+ "encryption_doq": "Cổng DNS-over-QUIC",
+ "encryption_dot": "Cổng DNS-over-TLS",
+ "encryption_https": "Cổng HTTPS",
+ "encryption_key": "Khóa riêng",
+ "encryption_key_input": "Sao chép/dán khóa riêng được mã hóa PEM cho chứng chỉ của bạn tại đây.",
+ "encryption_key_source_content": "Dán nội dung khóa riêng",
+ "encryption_plain_dns": "DNS thuần",
+ "encryption_plain_dns_error": "Để tắt DNS đơn giản, hãy bật ít nhất một giao thức DNS được mã hóa",
+ "encryption_private_key_path": "Đường dẫn khóa riêng",
+ "encryption_server": "Tên máy chủ",
+ "encryption_server_enter": "Nhập tên miền của bạn",
+ "encryption_title": "Mã hóa",
+ "enter_cache_size": "Nhập kích thước bộ nhớ cache (byte)",
+ "enter_ip_address_placeholder": "Nhập địa chỉ IP",
+ "error": "Lỗi",
+ "fallback_dns": "Máy chủ DNS dự phòng",
+ "filter_category_general": "General",
+ "filter_category_general_desc": "Bộ lọc chặn quảng cáo và theo dõi cho hầu hết các thiết bị",
+ "filter_category_other": "Khác",
+ "filter_category_other_desc": "Bộ lọc chặn khác",
+ "filter_category_regional": "Khu vực",
+ "filter_category_regional_desc": "Bộ lọc tập trung vào từng khu vực",
+ "filter_category_security": "Chặn phần mềm độc hại và lừa đảo",
+ "filter_category_security_desc": "Bộ lọc chuyên biệt chặn tên miền chứa mã độc và lừa đảo",
+ "filters": "Bộ lọc",
+ "forgot_password": "Quên mật khẩu?",
+ "form_enter_blocked_response_ttl": "Nhập phản hồi bị chặn TTL (giây)",
+ "form_error_client_id_format": "ClientID chỉ được chứa số, chữ thường và dấu gạch nối",
+ "form_error_domain_format": "Định dạng tên miền không hợp lệ",
+ "form_error_equal": "Không nên bằng nhau",
+ "form_error_format": "Định dạng không hợp lệ",
+ "form_error_gateway_ip": "Cho thuê không thể có địa chỉ IP của cổng",
+ "form_error_ip4_format": "Địa chỉ IPv4 không hợp lệ",
+ "form_error_ip6_format": "Địa chỉ IPv6 không hợp lệ",
+ "form_error_ip_format": "Địa chỉ IP không hợp lệ",
+ "form_error_mac_format": "Địa chỉ MAC không hợp lệ",
+ "form_error_password": "Mật khẩu không khớp",
+ "form_error_port": "Nhập số cổng hợp lệ",
+ "form_error_port_range": "Nhập giá trị cổng trong phạm vi 80-65535",
+ "form_error_port_unsafe": "Đây là một cổng không an toàn",
+ "form_error_server_name": "Tên máy chủ không hợp lệ",
+ "form_error_url_format": "Nhập URL hoặc đường dẫn tệp hợp lệ",
+ "friday": "Thứ sáu",
+ "gateway_or_subnet_invalid": "Mặt nạ mạng con không hợp lệ",
+ "general_statistics": "Thống kê chung",
+ "ignore_domains_title": "Các miền bị bỏ qua",
+ "inactivity_schedule_add": "Thêm lịch trình",
+ "inactivity_schedule_all_day": "Cả ngày",
+ "inactivity_schedule_edit": "Chỉnh sửa lịch trình",
+ "install_auth_confirm": "Xác nhận mật khẩu",
+ "install_auth_password": "Mật khẩu",
+ "install_auth_password_enter": "Nhập mật khẩu",
+ "install_auth_username": "Tên người dùng",
+ "install_auth_username_enter": "Nhập tên đăng nhập",
+ "install_saved": "Lưu thành công",
+ "install_settings_all_interfaces": "Tất cả các giao diện",
+ "install_settings_port": "Cổng",
+ "install_static_error": "AdGuard Home không thể cấu hình tự động cho giao diện mạng này. Vui lòng tìm hướng dẫn về cách thực hiện việc này theo cách thủ công.",
+ "install_static_ok": "Địa chỉ IP tĩnh đã được thiết lập",
+ "install_step": "Bước",
+ "install_submit_title": "Xin chúc mừng!",
+ "ip_address": "Địa chỉ IP",
+ "known_tracker": "Theo dõi đã biết",
+ "language": "Ngôn ngữ",
+ "light_theme": "Chủ đề sáng",
+ "login": "Đăng nhập",
+ "logout": "Đăng xuất",
+ "make_static": "Chuyển sang tĩnh",
+ "milliseconds_abbreviation": "ms",
+ "monday": "Thứ hai",
+ "name": "Tên",
+ "name_label": "Tên",
+ "name_table_header": "Tên",
+ "name_tooltip": "Tên",
+ "next": "Tiếp theo",
+ "no": "Không",
+ "none_text": "Không có",
+ "nothing_found": "Không tìm thấy",
+ "notify_undo": "Hoàn tác",
+ "notify_updated": "Đã cập nhật",
+ "parental_control": "Quản lý của phụ huynh",
+ "parental_group_cdn": "CDN",
+ "parental_group_development": "Phát triển phần mềm",
+ "parental_group_gaming": "Chơi game",
+ "parental_group_hosting": "Lưu trữ web",
+ "parental_group_privacy": "Công cụ bảo mật",
+ "parental_group_shopping": "Mua sắm",
+ "parental_group_social": "Mạng xã hội",
+ "parental_group_social_network": "Mạng xã hội",
+ "parental_group_software": "Phát triển phần mềm",
+ "parental_group_streaming": "Phát trực tuyến",
+ "password_label": "Mật khẩu",
+ "password_placeholder": "Nhập mật khẩu",
+ "plain_dns": "DNS thuần",
+ "privacy_policy": "Chính sách bảo mật",
+ "protection": "Sự bảo vệ",
+ "protocol": "Giao thức",
+ "query_log_allowed": "Đã thêm vào danh sách cho phép",
+ "query_log_blocked": "Bị chặn",
+ "query_log_blocked_services": "Dịch vụ bị chặn",
+ "query_log_blocked_threats": "Mối nguy hiểm đã chặn",
+ "query_log_custom_filtering_rules": "Quy tắc lọc tuỳ chỉnh",
+ "query_log_nothing_available": "Không tìm thấy",
+ "query_log_processed": "Đã xử lý",
+ "query_log_rewritten": "Đã viết lại",
+ "query_log_safe_search": "Kích hoạt Tìm kiếm An toàn",
+ "query_log_strict_search": "Sử dụng dấu ngoặc kép để tìm kiếm nghiêm ngặt",
+ "rate_limit_subnet_len_ipv4_error": "Độ dài tiền tố mạng con IPv4 phải nằm trong khoảng từ 0 đến 32",
+ "rate_limit_subnet_len_ipv6_error": "Độ dài tiền tố mạng con IPv6 phải nằm trong khoảng từ 0 đến 128",
+ "refresh_btn": "Làm mới",
+ "refresh_statics": "Làm mới thống kê",
+ "release_notes": "Ghi chú phát hành",
+ "remove": "Xoá",
+ "report_an_issue": "Báo cáo sự cố",
+ "request_table_header": "Yêu cầu",
+ "requests_table_header": "Yêu cầu",
+ "reset": "Đặt lại",
+ "reset_settings": "Đặt lại cài đặt",
+ "reset_settings_confirm": "Có, đặt lại",
+ "response_details": "Chi tiết về phản hồi",
+ "response_time": "Thời gian đáp ứng",
+ "result": "Kết quả",
+ "rewrite_add": "Thêm DNS viết lại",
+ "rewrite_edit": "Chỉnh sửa viết lại DNS",
+ "rewrite_hosts_applied": "Viết lại bởi quy tắc tệp máy chủ",
+ "rewrite_settings_updated": "Cài đặt viết lại DNS được cập nhật thành công",
+ "rewrite_updated": "Viết lại DNS được cập nhật thành công",
+ "rewritten": "Đã viết lại",
+ "rules_label": "Quy tắc",
+ "safe_browsing": "Duyệt web an toàn",
+ "safe_search": "Kích hoạt Tìm kiếm An toàn",
+ "saturday": "Thứ bảy",
+ "save": "Lưu",
+ "save_btn": "Lưu",
+ "search_placeholder": "Tìm kiếm",
+ "server_config_blocking_mode": "Chế độ chặn",
+ "server_config_blocking_mode_ipv4": "Chặn IPv4",
+ "server_config_blocking_mode_ipv6": "Chặn IPv6",
+ "server_config_custom_ip": "Địa chỉ IP tùy chỉnh",
+ "server_config_default": "Mặc định",
+ "server_config_disable_ipv6": "Tắt IPv6",
+ "server_config_dnssec_enable": "Bật DNSSEC",
+ "server_config_edns_enable": "Bật mạng con EDNS Client",
+ "server_config_nxdomain": "NXDOMAIN",
+ "server_config_rate_limit_placeholder": "Nhập giới hạn yêu cầu",
+ "server_config_rate_limit_whitelist_faq": "Địa chỉ IP bị loại trừ khỏi giới hạn tốc độ",
+ "server_config_refused": "REFUSED",
+ "server_config_subnet_len_ipv4": "Độ dài tiền tố mạng con cho địa chỉ IPv4",
+ "server_config_subnet_len_ipv4_faq": "Độ dài tiền tố mạng con cho các địa chỉ IPv4 được sử dụng để giới hạn tốc độ. Mặc định là 24",
+ "server_config_subnet_len_ipv6": "Độ dài tiền tố mạng con cho địa chỉ IPv6",
+ "server_config_subnet_len_ipv6_faq": "Độ dài tiền tố mạng con cho các địa chỉ IPv6 được sử dụng để giới hạn tốc độ. Mặc định là 56",
+ "set_static_ip": "Thiết lập địa chỉ IP tĩnh",
+ "settings": "Cài đặt",
+ "settings_browsing_security": "Bảo vệ duyệt web",
+ "settings_confirm_clear_statistics": "Xoá các thống kê?",
+ "settings_custom": "Tuỳ chọn",
+ "settings_general_short": "General",
+ "settings_global": "Toàn cầu",
+ "settings_notify_changes_saved": "Các thay đổi đã được cứu",
+ "settings_notify_statistics_cleared": "Đã xoá thống kê",
+ "settings_parental_control": "Quản lý của phụ huynh",
+ "settings_rule_syntax": "Cú pháp quy tắc",
+ "settings_safe_search": "Kích hoạt Tìm kiếm An toàn",
+ "settings_statistics": "Thống kê",
+ "settings_statistics_clear": "Xoá dữ liệu thống kê",
+ "settings_statistics_retention": "Duy trì thống kê",
+ "settings_yes_clear": "Vâng, rõ ràng",
+ "setup_devices_router_title": "Bộ định tuyến",
+ "setup_dns_privacy_ioc_mac": "Cấu hình iOS và macOS",
+ "setup_dns_privacy_other_title": "Triển khai khác",
+ "show_blocked_responses": "Bị chặn",
+ "show_processed_responses": "Đã xử lý",
+ "source_label": "Nguồn",
+ "source_tooltip": "Nguồn",
+ "stats_adult": "Website người lớn đã chặn",
+ "stats_query_domain": "Tên miền truy vấn nhiều",
+ "status_table_header": "Trạng thái",
+ "sunday": "Chủ nhật",
+ "system_host_files": "Hệ thống lưu trữ tệp",
+ "table_client": "Người dùng",
+ "tags_title": "Thẻ",
+ "test_upstreams": "Kiểm tra",
+ "threats_blocked": "Các mối đe doạ bị chặn",
+ "thursday": "Thứ năm",
+ "time_table_header": "Thời gian",
+ "top_blocked_domains": "Tên miền chặn nhiều",
+ "top_clients": "Người dùng hàng đầu",
+ "top_upstreams": "Máy chủ thượng nguồn hàng đầu",
+ "total": "Tổng cộng",
+ "try_again": "Hãy thử lại",
+ "tuesday": "Thứ ba",
+ "unavailable_dhcp": "DHCP không khả dụng",
+ "unavailable_dhcp_desc": "AdGuard Home không thể chạy máy chủ DHCP trên hệ điều hành của bạn",
+ "unblock": "Bỏ chặn",
+ "update_button": "Cập nhật",
+ "update_filters_disable": "Vô hiệu",
+ "update_filters_interval_custom": "Tuỳ chọn",
+ "update_filters_interval_daily": "Hằng ngày",
+ "update_filters_interval_hourly": "Mỗi giờ",
+ "update_filters_title": "Cập nhật bộ lọc tự động",
+ "updates_checked": "Phiên bản mới của AdGuard Home có sẵn",
+ "updates_version_equal": "AdGuard Home đã được cập nhật",
+ "upstream": "Máy chủ thượng nguồn",
+ "upstream_dns": "Máy chủ DNS tìm kiếm",
+ "upstream_dns_addresses": "Địa chỉ máy chủ, mỗi địa chỉ một dòng",
+ "upstream_dns_fastest_addr": "Địa chỉ IP nhanh nhất",
+ "upstream_dns_parallel_requests": "Yêu cầu song song",
+ "upstream_dns_servers_title": "Máy chủ DNS tìm kiếm",
+ "upstream_examples_title": "Ví dụ",
+ "upstream_private_ptr_resolvers_title": "Sử dụng trình rDNS riêng tư",
+ "upstreams": "Nguồn",
+ "url_label": "URL",
+ "user_rules": "Bộ lọc của Người dùng",
+ "user_rules_add_to_allowlist": "Thêm vào danh sách cho phép",
+ "user_rules_check_button": "Kiểm tra",
+ "user_rules_check_client_placeholder": "Nhập định danh khách hàng",
+ "user_rules_check_hostname_label": "Tên máy chủ hoặc tên miền",
+ "user_rules_disable_filter": "Vô hiệu bộ lọc",
+ "user_rules_disable_parental_control": "Disable ParentalControl",
+ "user_rules_dns_record_type_placeholder": "Chọn loại bản ghi DNS",
+ "user_rules_domain_blocked": "tên miền bị chặn",
+ "user_rules_edit_dns_rewrite": "Chỉnh sửa viết lại DNS",
+ "user_rules_rewrite_rule_is_applied": "Đã áp dụng quy tắc Viết lại",
+ "username_label": "Tên người dùng",
+ "username_placeholder": "Nhập tên đăng nhập",
+ "validated_with_dnssec": "Xác thực bỏi DNSSEC",
+ "wednesday": "Thứ Tư",
+ "whois": "WHOIS",
+ "yes": "Đồng ý",
+ "yes_disable": "Có, tắt",
+ "yes_reset": "Có, đặt lại"
+}
diff --git a/client_v2/src/__locales/zh-cn.json b/client_v2/src/__locales/zh-cn.json
new file mode 100644
index 00000000000..bd12d3c78bc
--- /dev/null
+++ b/client_v2/src/__locales/zh-cn.json
@@ -0,0 +1,864 @@
+{
+ "access_settings_allowed_faq": "CIDR、IP 地址或 ClientID 列表。如果此字段不为空,AdGuard Home 将仅接受来自这些客户端的请求。",
+ "access_settings_allowed_placeholder": "输入允许的客户端",
+ "access_settings_allowed_title": "允许的客户端,每行一个",
+ "access_settings_blocked_faq_1": "AdGuard Home 将丢弃匹配这些域名的 DNS 查询。此类查询不会出现在查询日志中。您可以输入精确域名、通配符或 URL 过滤规则,例如 example.org、*.example.org 或 ||example.org^。",
+ "access_settings_blocked_faq_2": "请勿与过滤器混淆",
+ "access_settings_blocked_placeholder": "输入禁止的域名",
+ "access_settings_blocked_title": "禁止的域名,每行一个",
+ "access_settings_disallowed_faq": "CIDR、IP 地址或 ClientID 列表。如果此字段不为空,AdGuard Home 将丢弃来自这些客户端的请求。如果允许的客户端 不为空,则忽略此字段。",
+ "access_settings_disallowed_placeholder": "输入禁止的客户端",
+ "access_settings_disallowed_title": "禁止的客户端,每行一个",
+ "access_settings_saved_toast": "访问设置已保存",
+ "access_settings_title": "访问设置",
+ "actions_table_header": "操作",
+ "add": "添加",
+ "add_allowlist": "添加白名单",
+ "add_blocklist": "添加黑名单",
+ "add_persistent_client": "添加为持久客户端",
+ "add_to_allowlist": "添加到白名单",
+ "ads_blocked": "广告已拦截",
+ "ads_blocked_card": "广告已拦截",
+ "ads_blocked_tooltip": "被广告拦截过滤器和 hosts 拦截列表拦截的 DNS 查询数量",
+ "adult_websites_blocked": "成人网站已拦截",
+ "adult_websites_blocked_tooltip": "被阻止的成人网站总数",
+ "all_domains": "所有域名",
+ "all_lists_up_to_date_toast": "所有列表都是最新的",
+ "all_rewrites_disabled": "所有自定义 DNS 响应将被禁用",
+ "all_rewrites_enabled": "所有自定义 DNS 响应将被启用。",
+ "allow_service": "允许服务",
+ "allowed": "白名单",
+ "allowlist_add": "添加白名单",
+ "allowlist_edit": "编辑白名单",
+ "allowlist_empty": "暂无允许列表:添加第一个 ",
+ "allowlist_placeholder_example": "我的允许列表",
+ "allowlist_remove": "移除允许列表?",
+ "allowlist_remove_desc": "将移除 %value% ",
+ "allowlists": "允许列表",
+ "allowlists_title": "允许列表",
+ "apply": "应用",
+ "aria_clear_input": "清除输入",
+ "autofix_warning_list": "它将执行以下任务:
停用系统 DNSStubListener
将 DNS 服务器地址设为 127.0.0.1
将 /etc/resolv.conf 的符号链接目标替换为 /run/systemd/resolve/resolv.conf
停止 DNSStubListener(重新加载 systemd-resolved 服务)
",
+ "autofix_warning_result": "因此,默认情况下所有来自系统的 DNS 请求都将由 AdGuard Home 处理。",
+ "autofix_warning_text": "若您单击「修复」,AdGuard Home 将会配置您的系统以使用 AdGuard Home 的 DNS 服务器。",
+ "average_time_processing": "平均处理时间",
+ "average_time_processing_tooltip": "DNS 查询平均处理时间(毫秒)",
+ "average_upstream_response_time": "上游服务器的平均响应时间",
+ "back": "返回",
+ "block": "拦截",
+ "block_client": "拦截客户端",
+ "block_for_this_client_only": "仅对此客户端拦截",
+ "blocked": "已拦截",
+ "blocked_adult_websites": "被家长控制拦截",
+ "blocked_by_cname_or_ip": "按 CNAME 或 IP 拦截",
+ "blocked_queries": "已拦截的查询",
+ "blocked_service": "已阻止的服务",
+ "blocked_services": "已阻止的服务",
+ "blocked_services_desc": "选择需要拦截的热门网站和服务。",
+ "blocked_threats": "已拦截的威胁",
+ "blocked_threats_chart": "已拦截的威胁",
+ "blocked_total": "共拦截 %value%",
+ "blocklist_add_from_list": "从列表添加",
+ "blocklist_add_manual": "手动添加",
+ "blocklist_contents": "内容(.%value%)",
+ "blocklist_edit": "编辑黑名单",
+ "blocklist_homepage": "主页",
+ "blocklist_placeholder_example": "我的拦截列表",
+ "blocklist_remove": "移除拦截列表?",
+ "blocklist_remove_desc": "将移除 %value% ",
+ "blocklist_url_file_path": "URL 或文件路径",
+ "blocklists_add": "添加拦截列表",
+ "blocklists_add_desc": "您可以从列表中选择拦截列表,或手动添加。",
+ "blocklists_desc": "这些列表中的域名将被拦截",
+ "blocklists_empty": "暂无拦截列表:添加第一个 ",
+ "blocklists_title": "拦截列表",
+ "bootstrap_dns": "Bootstrap DNS 服务器",
+ "browse": "浏览",
+ "cache_cleared": "已成功清除 DNS 缓存",
+ "cache_config_clear": "清除缓存",
+ "cache_config_max_ttl": "覆盖最大 TTL(秒)",
+ "cache_config_max_ttl_faq": "DNS 缓存条目的最大生存时间(TTL)值",
+ "cache_config_max_ttl_placeholder": "输入最大 TTL",
+ "cache_config_min_ttl": "覆盖最小 TTL(秒)",
+ "cache_config_min_ttl_faq": "DNS 缓存条目的最小生存时间(TTL)值",
+ "cache_config_min_ttl_placeholder": "输入最小 TTL",
+ "cache_config_optimistic": "乐观缓存",
+ "cache_config_optimistic_desc": "即使缓存条目已过期,AdGuard Home 也会从缓存中响应,并尝试更新它们。",
+ "cache_config_size": "缓存大小(字节)",
+ "cache_config_size_faq": "DNS 缓存大小。要禁用缓存,请留空。",
+ "cache_config_size_validation": "启用时,缓存大小必须大于零。",
+ "cache_config_ttl_validation": "最小缓存 TTL 值必须小于或等于最大值",
+ "cache_confirm_clear_desc": "这将删除所有缓存的 DNS 响应。后续 DNS 查询可能会变慢,直到缓存重建完成。",
+ "cache_confirm_clear_title": "清除 DNS 缓存",
+ "cache_enabled": "启用缓存",
+ "cache_enabled_desc": "在本地存储 DNS 响应",
+ "cancel": "取消",
+ "cancel_btn": "取消",
+ "category_tooltip": "类别",
+ "changes_saved_success": "更改已保存",
+ "check_dhcp_servers": "检查 DHCP 服务器",
+ "check_not_found": "未在您的筛选列表中找到",
+ "check_updates_btn": "检查更新",
+ "clear_btn": "清除",
+ "clear_query_log": "清除查询日志",
+ "client_add": "添加客户端",
+ "client_added": "客户端已添加",
+ "client_already_blocked": "客户端 %ip% 已被拦截",
+ "client_blocked_flash": "客户端已拦截",
+ "client_details": "客户端详情",
+ "client_id": "客户端 ID",
+ "client_id_faq": "客户端可以通过其 ClientID 进行识别。要了解更多信息,请访问 AdGuard Home GitHub 。",
+ "client_id_placeholder": "输入客户端 ID",
+ "client_identifier": "标识符",
+ "client_ip": "客户端 IP",
+ "client_location": "客户端位置",
+ "client_removed": "客户端已移除",
+ "client_settings": "客户端设置",
+ "client_table_header": "客户端",
+ "client_unblocked_flash": "客户端已解除拦截",
+ "clients": "客户端",
+ "clients_add": "添加客户端",
+ "clients_add_identifier": "添加标识符",
+ "clients_add_name": "客户端名称",
+ "clients_add_placeholder": "输入客户端名称",
+ "clients_desc": "以下规则用于连接到 AdGuard Home 的设备",
+ "clients_dns_cache_size": "DNS 缓存大小,单位:字节",
+ "clients_dns_cache_size_placeholder": "请输入数字",
+ "clients_dont_collect_stats": "不收集此客户端的统计数据",
+ "clients_dont_log": "不记录此客户端",
+ "clients_edit": "编辑客户端",
+ "clients_identifier_already_used": "此标识符已被使用",
+ "clients_identifier_format_error": "请使用数字、小写英文字母或连字符。",
+ "clients_identifiers": "标识符",
+ "clients_identifiers_desc": "您可以使用客户端的 IP 地址、CIDR、MAC 地址或 ClientID(适用于 DoT、DoH 和 DoQ)。了解更多 ",
+ "clients_logs_and_statistics": "日志与统计",
+ "clients_not_found": "未找到客户端",
+ "clients_protection": "防护",
+ "clients_protection_desc": "启用或禁用保护功能、日志记录和统计。",
+ "clients_remove_desc": "%value% 及其设置将从永久客户端中移除。",
+ "clients_remove_title": "删除客户端?",
+ "clients_tags": "标签",
+ "clients_tags_desc": "您可以为此客户端添加标签,并将其包含在过滤规则中。了解更多 ",
+ "clients_upstreams_desc": "上游 DNS 服务器,每行一个。",
+ "clients_use_dns_cache": "使用 DNS 缓存",
+ "clients_use_global_settings": "使用全局设置",
+ "clients_use_global_settings_desc": "继承 AdGuard Home 账号的保护设置",
+ "close": "关闭",
+ "close_result": "关闭结果",
+ "confirm": "确认",
+ "confirm_client_block_desc": "AdGuard Home 将丢弃来自客户端 %ip% 的所有 DNS 请求。",
+ "confirm_client_block_title": "拦截客户端?",
+ "confirm_client_unblock_desc": "来自客户端 %ip% 的 DNS 请求将被允许",
+ "confirm_client_unblock_title": "解除拦截客户端?",
+ "confirm_static_ip": "AdGuard Home 将配置 %ip% 为您的静态 IP 地址。是否继续?",
+ "copied": "已复制到剪贴板",
+ "copy": "复制",
+ "copy_to_clipboard": "复制到剪贴板",
+ "custom_filtering_rules": "自定义过滤器规则",
+ "dark_theme": "暗黑主题",
+ "dashboard": "仪表盘",
+ "delete_btn": "删除",
+ "delete_confirm": "删除租约?",
+ "delete_confirm_desc": "这将释放 %ip% ",
+ "delete_table_action": "删除",
+ "delete_table_action_confirm": "确认删除",
+ "device_type": "设备类型",
+ "dhcp_add_static_lease": "添加静态租约",
+ "dhcp_config_saved": "已成功保存 DHCP 服务器配置",
+ "dhcp_edit_static_lease": "编辑静态租约",
+ "dhcp_error": "检查 DHCP 服务器失败。请稍后重试。",
+ "dhcp_form_gateway_address": "网关 IP 地址",
+ "dhcp_form_gateway_input": "网关 IP",
+ "dhcp_form_range_start": "起始 IP 地址",
+ "dhcp_form_range_title": "IP 地址范围",
+ "dhcp_form_subnet_input": "子网掩码",
+ "dhcp_found": "无法启用 DHCP 服务器",
+ "dhcp_ipv4_settings": "DHCP IPv4 设置",
+ "dhcp_ipv6_settings": "DHCP IPv6 设置",
+ "dhcp_lease_added": "静态租约「%key%」已成功添加",
+ "dhcp_lease_deleted": "静态租约「%key%」已成功删除",
+ "dhcp_lease_updated": "静态租约「%key%」已成功更新",
+ "dhcp_leases_not_found": "未找到 DHCP 租约",
+ "dhcp_leases_title": "DHCP 租约",
+ "dhcp_mac_address_already_added": "此 MAC 地址已添加",
+ "dhcp_new_static_lease": "新建静态租约",
+ "dhcp_not_found": "DHCP 服务器已启用",
+ "dhcp_reset": "这将重置 DHCP 配置。所有更改将丢失。",
+ "dhcp_reset_leases": "重置所有租约",
+ "dhcp_reset_leases_confirm": "这将重置所有租约",
+ "dhcp_reset_leases_success": "成功重置了 DHCP 租约",
+ "dhcp_settings": "DHCP 设置",
+ "dhcp_static_ip_error": "DHCP 服务器需要静态 IP 地址。我们无法确认此网络接口已配置静态 IP。",
+ "dhcp_static_leases_not_found": "未找到 DHCP 静态租约",
+ "dhcp_table_hostname": "主机名",
+ "dhcp_table_ip_address": "IP 地址",
+ "dhcp_table_mac_address": "MAC 地址",
+ "disable": "禁用",
+ "disable_protection_btn": "禁用保护",
+ "disabled_dhcp": "DHCP 服务器已禁用",
+ "disallow_client_confirm_allowlist_note": "此操作还将从允许的客户端列表中移除规则 %ip%。",
+ "disallow_client_confirm_title": "禁止此客户端?",
+ "disallow_this_client": "不允许这个客户端",
+ "dns_access_settings_title": "访问设置",
+ "dns_allowed_clients": "允许的客户端",
+ "dns_allowed_clients_label": "允许的客户端,每行一个",
+ "dns_allowed_clients_placeholder": "输入允许的客户端",
+ "dns_blocking_mode": "拦截模式",
+ "dns_blocking_mode_custom_ip": "自定义 IP 地址",
+ "dns_blocking_mode_default": "默认",
+ "dns_blocking_mode_ipv4_label": "拦截 IPv4",
+ "dns_blocking_mode_ipv4_placeholder": "输入 IPv4 地址",
+ "dns_blocking_mode_ipv6_label": "拦截 IPv6",
+ "dns_blocking_mode_ipv6_placeholder": "输入 IPv6 地址",
+ "dns_blocking_mode_null_ip": "空 IP 地址",
+ "dns_blocking_mode_nxdomain": "NXDOMAIN",
+ "dns_blocking_mode_refused": "REFUSED",
+ "dns_blocking_mode_title": "拦截模式",
+ "dns_blocking_mode_ttl_label": "拦截响应 TTL(秒)",
+ "dns_bootstrap_dns_label": "Bootstrap DNS 服务器,每行一个",
+ "dns_bootstrap_dns_placeholder": "IP 地址",
+ "dns_bootstrap_dns_title": "Bootstrap DNS 服务器",
+ "dns_bootstrap_servers": "Bootstrap DNS 服务器",
+ "dns_cache_config": "DNS 缓存配置",
+ "dns_cache_configuration_saved_toast": "DNS 缓存配置已保存",
+ "dns_cache_size": "缓存大小",
+ "dns_cache_size_label": "DNS 缓存大小,单位:字节",
+ "dns_cache_size_title": "缓存大小",
+ "dns_clear_cache": "清除缓存",
+ "dns_clear_cache_cancel": "取消",
+ "dns_clear_cache_confirm": "是的,清除",
+ "dns_clear_cache_yes": "是的,清除",
+ "dns_config": "DNS 服务配置",
+ "dns_disallowed_clients": "不允许的客户端",
+ "dns_disallowed_clients_label": "禁止的客户端,每行一个",
+ "dns_disallowed_clients_title": "不允许的客户端",
+ "dns_disallowed_domains": "不允许的域名",
+ "dns_disallowed_domains_label": "禁止的域名,每行一个",
+ "dns_disallowed_domains_title": "不允许的域名",
+ "dns_edns_custom_placeholder": "输入 IP 地址",
+ "dns_fallback_dns_label": "备用 DNS 服务器,每行一个",
+ "dns_fallback_dns_placeholder": "IP 地址、sdns://、quic://、https://、h3:// 或 tls://",
+ "dns_fallback_dns_title": "后备 DNS 服务器",
+ "dns_fallback_servers": "后备 DNS 服务器",
+ "dns_optimistic_caching": "乐观缓存",
+ "dns_over_https": "DNS-over-HTTPS",
+ "dns_over_quic": "DNS-over-QUIC",
+ "dns_over_tls": "DNS-over-TLS",
+ "dns_override_max_ttl": "覆盖最大 TTL 值",
+ "dns_override_max_ttl_title": "覆盖最大 TTL 值",
+ "dns_override_min_ttl": "覆盖最小 TTL 值",
+ "dns_override_min_ttl_title": "覆盖最小 TTL 值",
+ "dns_privacy": "DNS 隐私",
+ "dns_private_reverse_servers_label": "私有反向 DNS 服务器,每行一个",
+ "dns_private_reverse_servers_placeholder": "IP 地址、sdns://、quic://、https://、h3:// 或 tls://",
+ "dns_private_reverse_servers_title": "私人反向 DNS 服务器",
+ "dns_queries": "DNS 查询",
+ "dns_queries_tooltip": "过去 24 小时内处理的 DNS 查询数量",
+ "dns_query": "DNS 查询",
+ "dns_rate_limit": "速度限制",
+ "dns_rate_limit_allowlist": "速率限制白名单",
+ "dns_rate_limit_allowlist_placeholder": "IP 地址",
+ "dns_rate_limit_allowlist_title": "速率限制白名单",
+ "dns_rate_limit_placeholder": "输入限制速率",
+ "dns_rate_limit_title": "速度限制",
+ "dns_rewrites": "DNS 重写",
+ "dns_server_addresses": "服务器地址",
+ "dns_server_addresses_label": "服务器地址(每行一个)",
+ "dns_server_addresses_placeholder": "IP 地址、sdns://、quic://、https://、h3:// 或 tls://",
+ "dns_server_addresses_title": "服务器地址",
+ "dns_server_config_title": "DNS 服务配置",
+ "dns_server_configuration_saved_toast": "DNS 服务器配置已保存",
+ "dns_settings": "DNS",
+ "dns_subnet_placeholder": "输入前缀长度",
+ "dns_test_upstreams": "测试上游",
+ "dns_upstream_servers_title": "上游 DNS 服务器",
+ "dns_upstream_timeout": "上游超时",
+ "dns_upstream_timeout_title": "上游超时",
+ "dns_upstream_validation_invalid": "格式无效",
+ "domain": "域名",
+ "domain_or_client": "域名或客户端",
+ "download_mobileconfig": "下载配置文件",
+ "dynamic_dhcp_leases_not_found": "未找到动态 DHCP 租约",
+ "edit_table_action": "编辑",
+ "enable": "启用",
+ "enabled_dhcp": "DHCP 服务器已启用",
+ "encrypted_dns_addresses": "加密 DNS 服务器地址",
+ "encryption_certificate_has_issues": "证书存在问题",
+ "encryption_certificate_path": "证书路径",
+ "encryption_certificates": "证书",
+ "encryption_certificates_desc": "要启用加密,必须为您的域名提供有效的 SSL 证书链。您可以从 letsencrypt.org 获取免费 SSL 证书,或从受信任的证书颁发机构购买。",
+ "encryption_certificates_input": "将您以 PEM 格式编码的证书复制粘贴到此处。",
+ "encryption_certificates_source_content": "粘贴证书内容",
+ "encryption_certificates_source_path": "设置证书路径",
+ "encryption_chain_invalid": "证书链无效",
+ "encryption_chain_valid": "证书链有效",
+ "encryption_config_saved": "加密配置已保存",
+ "encryption_confirm_clear": "重置加密设置",
+ "encryption_confirm_clear_desc": "所有加密设置将重置为默认值",
+ "encryption_disable_plain_dns": "禁用纯 DNS?",
+ "encryption_disable_plain_dns_desc": "不支持加密 DNS 的设备可能无法正常工作",
+ "encryption_doq": "DNS-over-QUIC 端口",
+ "encryption_doq_tooltip": "此端口用于运行 DNS-over-QUIC 服务器",
+ "encryption_dot": "DNS-over-TLS 端口",
+ "encryption_dot_tooltip": "此端口用于运行 DNS-over-TLS 服务器",
+ "encryption_encrypted_dns": "加密 DNS",
+ "encryption_encrypted_dns_desc": "AdGuard Home 管理界面使用 HTTPS。DNS 服务器支持 DNS-over-HTTPS、DNS-over-TLS 和 DNS-over-QUIC。",
+ "encryption_expire": "过期时间:%value%",
+ "encryption_force_redirect": "自动从 HTTP 重定向到 HTTPS",
+ "encryption_hostnames": "主机名:%value%",
+ "encryption_https": "HTTPS 端口",
+ "encryption_https_tooltip": "此端口提供 AdGuard Home 管理界面的 HTTPS 访问。同时在 '/dns-query' 路径上提供 DNS-over-HTTPS。",
+ "encryption_issuer": "颁发者:%value%",
+ "encryption_key": "私钥",
+ "encryption_key_input": "将您以 PEM 格式编码的证书私钥复制粘贴到此处。",
+ "encryption_key_invalid": "私钥无效",
+ "encryption_key_source_content": "粘贴私钥内容",
+ "encryption_key_source_path": "设置私钥文件",
+ "encryption_key_type": "加密算法:%value%",
+ "encryption_key_valid": "私钥有效",
+ "encryption_plain_dns": "无加密 DNS",
+ "encryption_plain_dns_error": "要禁用无加密 DNS,请至少启用一个加密 DNS 协议",
+ "encryption_private_key_path": "私钥路径",
+ "encryption_server": "服务器名称",
+ "encryption_server_enter": "输入您的域名",
+ "encryption_server_tooltip_1": "如果已设置,AdGuard Home 将检测 ClientID、响应 DDR 查询并执行额外的连接验证。如果未设置,这些功能将被禁用。",
+ "encryption_server_tooltip_2": "必须与证书中的 DNS 名称匹配",
+ "encryption_subject": "主题:%value%",
+ "encryption_title": "加密",
+ "enter_cache_size": "输入缓存大小(字节)",
+ "enter_ip_address_placeholder": "输入 IP 地址",
+ "error": "错误",
+ "fallback_dns": "后备 DNS 服务器",
+ "filter_added_successfully": "已添加拦截列表:%value% ",
+ "filter_added_successfully_allowlist": "已添加允许列表:%value% ",
+ "filter_added_successfully_more": "已添加拦截列表:%value% 及其他 %more% 个",
+ "filter_category_general": "General",
+ "filter_category_general_desc": "在大多数设备上阻止跟踪和广告的列表",
+ "filter_category_other": "其它",
+ "filter_category_other_desc": "其他黑名单",
+ "filter_category_regional": "区域",
+ "filter_category_regional_desc": "专注于区域广告和跟踪服务器的列表",
+ "filter_category_security": "安全",
+ "filter_category_security_desc": "专用于拦截恶意软件、钓鱼或欺诈域名的列表",
+ "filter_removed_successfully": "已移除拦截列表:%value% ",
+ "filter_removed_successfully_allowlist": "已移除允许列表:%value% ",
+ "filters": "过滤器",
+ "forgot_password": "忘记密码?",
+ "forgot_password_list_item_1": "使用 htpasswd 工具或任何在线 BCrypt 生成器创建新密码。",
+ "forgot_password_list_item_2": "停止 AdGuard Home。",
+ "forgot_password_list_item_3": "打开 AdGuardHome.yaml,找到 password 字段,将其替换为新值。",
+ "forgot_password_list_item_4": "启动 AdGuard Home。",
+ "forgot_password_list_item_5": "使用新密码登录 Web 界面。",
+ "forgot_password_list_title": "要更改您的密码:",
+ "forgot_password_page_desc": "AdGuard Home 将密码存储为 BCrypt 加密哈希值",
+ "form_enter_blocked_response_ttl": "输入拦截的 TTL 应答(秒)",
+ "form_enter_hostname": "输入主机名",
+ "form_enter_ip": "输入 IP 地址",
+ "form_enter_mac": "输入 MAC 地址",
+ "form_error_answer_format": "结果格式无效",
+ "form_error_client_id_format": "客户端 ID 必须只包含数字、小写字母和连字符",
+ "form_error_domain_format": "无效的域格式",
+ "form_error_equal": "不可相同",
+ "form_error_format": "格式无效",
+ "form_error_gateway_ip": "租约期限不能有网关的 IP 地址",
+ "form_error_hostname_format": "请使用数字、小写英文字母或连字符。",
+ "form_error_ip4_format": "无效的 IPv4 地址",
+ "form_error_ip6_format": "无效的 IPv6 地址",
+ "form_error_ip_already_added": "此 IP 地址已添加",
+ "form_error_ip_format": "无效 IP 地址",
+ "form_error_mac_already_added": "此 MAC 地址已添加",
+ "form_error_mac_format": "无效的 MAC 地址",
+ "form_error_password": "密码不匹配",
+ "form_error_port": "输入有效的端口值",
+ "form_error_port_range": "输入 80 - 65535 范围内的端口值",
+ "form_error_port_unsafe": "这是一个不安全的端口",
+ "form_error_range": "数值必须介于 %min% 和 %max% 之间",
+ "form_error_server_name": "无效的服务器名",
+ "form_error_url_format": "输入一个有效的 URL 或文件路径",
+ "found_in_known_domains": "在已知域名中找到",
+ "friday": "星期五",
+ "gateway_or_subnet_invalid": "子网掩码无效",
+ "general_statistics": "概况统计",
+ "home_dns_addresses": "AdGuard Home DNS 地址",
+ "home_dns_addresses_desc": "AdGuard Home DNS 服务器监听以下接口/端口:",
+ "ignore_domains_desc_log": "匹配这些规则的请求不会显示在「查询日志」中",
+ "ignore_domains_desc_stats": "匹配这些规则的请求不会纳入统计",
+ "ignore_domains_title": "被忽略的域名",
+ "inactivity_schedule": "非活跃时段",
+ "inactivity_schedule_add": "添加时间表",
+ "inactivity_schedule_all_day": "全天",
+ "inactivity_schedule_delete": "删除时段",
+ "inactivity_schedule_delete_desc": "服务拦截将在 %value% 全天生效",
+ "inactivity_schedule_desc": "设置服务拦截的非活跃时段。",
+ "inactivity_schedule_edit": "编辑时间表",
+ "inactivity_schedule_no_schedules": "无时段设置",
+ "inactivity_schedule_replace_desc": "此时段将替换同一天现有的所有时段。每天只能有一个非活跃时段。",
+ "inactivity_schedule_time_from": "开始时间",
+ "inactivity_schedule_time_to": "结束时间",
+ "inactivity_schedule_timezone": "时区",
+ "install_auth_confirm": "确认密码",
+ "install_auth_password": "密码",
+ "install_auth_password_enter": "输入密码",
+ "install_auth_username": "用户名",
+ "install_auth_username_enter": "输入用户名",
+ "install_saved": "保存成功",
+ "install_settings_all_interfaces": "所有接口",
+ "install_settings_port": "端口",
+ "install_static_configure": "AdGuard Home 检测到使用了动态 IP 地址 %ip%。是否将其设为静态地址?",
+ "install_static_error": "AdGuard Home 无法为这个网络接口自动配置它。请参阅如何手动完成此操作的说明。",
+ "install_static_ok": "好消息!静态 IP 地址已经配置",
+ "install_step": "步骤",
+ "install_submit_title": "恭喜您!",
+ "instructions": "指示说明",
+ "invalid": "无效",
+ "ip_address": "IP 地址",
+ "ip_addresses_placeholder": "输入 IP 地址,每行一个",
+ "known_tracker": "已知跟踪器",
+ "language": "语言",
+ "last_days": "最近 %count% 天 | 最近 %count% 天",
+ "last_hour": "最近 1 小时",
+ "last_hours": "最近 %count% 小时 | 最近 %count% 小时",
+ "last_updated_label": "上次更新",
+ "light_theme": "浅色主题",
+ "list_updated": "%count% 个列表已更新 | %count% 个列表已更新",
+ "login": "登录",
+ "logout": "退出",
+ "make_static": "静态化",
+ "make_static_desc": "此操作将动态地址转换为静态地址",
+ "milliseconds_abbreviation": "毫秒",
+ "monday": "星期一",
+ "name": "名称",
+ "name_label": "名称",
+ "name_table_header": "名称",
+ "name_tooltip": "名称",
+ "network_interface": "网络接口",
+ "next": "下一步",
+ "no": "否",
+ "no_stats_yet": "暂无统计数据:请继续使用 AdGuard Home 以查看",
+ "none_text": "无",
+ "nothing_found": "没找到",
+ "notify_undo": "撤销",
+ "notify_updated": "已更新",
+ "open_dashboard": "打开仪表盘",
+ "parental_control": "家长控制",
+ "parental_group_ai": "人工智能",
+ "parental_group_cdn": "内容分发网络",
+ "parental_group_dating": "交友",
+ "parental_group_development": "软件开发",
+ "parental_group_gambling": "赌博、投注",
+ "parental_group_gaming": "游戏",
+ "parental_group_hosting": "网站托管",
+ "parental_group_messaging": " 即时通讯",
+ "parental_group_messenger": " 即时通讯",
+ "parental_group_privacy": "隐私工具",
+ "parental_group_shopping": "购物",
+ "parental_group_social": "社交网络",
+ "parental_group_social_network": "社交网络",
+ "parental_group_software": "软件开发",
+ "parental_group_streaming": "串流",
+ "password_label": "密码",
+ "password_placeholder": "输入密码",
+ "password_requirements": "密码要求",
+ "password_requirements_characters": "至少 8 个字符",
+ "password_requirements_lowercase": "至少 1 个小写字母",
+ "password_requirements_match": "密码匹配",
+ "password_requirements_special": "英文字母、数字和特殊字符",
+ "password_requirements_uppercase": "至少 1 个大写字母",
+ "pause_for_hour": "暂停 %count% 小时",
+ "pause_for_minutes": "暂停 %count% 分钟 | 暂停 %count% 分钟",
+ "pause_for_seconds": "暂停 %count% 秒 | 暂停 %count% 秒",
+ "pause_until_tomorrow": "暂停至明天 %time%",
+ "period_notify": "要更改显示的时间范围,请在常规设置 中编辑「统计数据保留时间」。",
+ "plain_dns": "无加密 DNS",
+ "plain_dns_addresses": "普通 DNS 服务器地址",
+ "port_53_faq_link": "端口 53 常被「DNSStubListener」或「systemd-resolved」服务占用。请阅读此说明 了解如何解决。",
+ "port_in_use": "此端口已被占用",
+ "privacy_policy": "隐私政策",
+ "processing_time_ms": "%value% ms",
+ "protection": "防护",
+ "protocol": "协议",
+ "queries": "查询",
+ "queries_total": "共 %count% 条查询 | 共 %count% 条查询",
+ "query_details": "查询详情",
+ "query_log": "查询日志",
+ "query_log_all_reasons": "全部原因",
+ "query_log_all_statuses": "全部状态",
+ "query_log_allowed": "白名单",
+ "query_log_blocked": "已拦截",
+ "query_log_blocked_by_filter": "被过滤器拦截",
+ "query_log_blocked_by_parental_control": "被家长控制拦截",
+ "query_log_blocked_services": "已阻止的服务",
+ "query_log_blocked_threats": "已拦截的威胁",
+ "query_log_custom_filtering_rules": "自定义过滤器规则",
+ "query_log_detail_address": "地址:%value% ",
+ "query_log_detail_category": "类别:%value% ",
+ "query_log_detail_country": "国家/地区:%value% ",
+ "query_log_detail_date": "日期:%value% ",
+ "query_log_detail_dns_server": "DNS 服务器:%value% ",
+ "query_log_detail_domain": "域名:%value% ",
+ "query_log_detail_ecs": "ECS:%value% ",
+ "query_log_detail_elapsed": "耗时:%value% ",
+ "query_log_detail_name": "名称:%value% ",
+ "query_log_detail_network": "网络:%value% ",
+ "query_log_detail_original_response": "原始响应:",
+ "query_log_detail_protocol": "协议:%value% ",
+ "query_log_detail_reason": "原因:",
+ "query_log_detail_response": "响应:",
+ "query_log_detail_response_code": "响应码:%value% ",
+ "query_log_detail_rules": "规则:",
+ "query_log_detail_served_from_cache": "来自缓存:%value% ",
+ "query_log_detail_service_name": "服务名称:%value% ",
+ "query_log_detail_source": "来源:%value% ",
+ "query_log_detail_status": "状态:%value% ",
+ "query_log_detail_time": "时间:%value% ",
+ "query_log_detail_type": "类型:%value% ",
+ "query_log_nothing_available": "没找到",
+ "query_log_processed": "已处理",
+ "query_log_retention": "日志轮替",
+ "query_log_rewritten": "重写项",
+ "query_log_safe_search": "安全搜索",
+ "query_log_strict_search": "使用双引号进行精确搜索",
+ "rate_limit_subnet_len_ipv4_error": "IPv4 子网前缀长度应介于 0 到 32 之间",
+ "rate_limit_subnet_len_ipv6_error": "IPv6 子网前缀长度应介于 0 到 128 之间",
+ "reason_table_header": "原因",
+ "refresh_btn": "刷新",
+ "refresh_statics": "刷新统计数据",
+ "release_notes": "更新日志",
+ "remove": "移除",
+ "remove_tag": "移除 %value%",
+ "report_an_issue": "报告错误",
+ "request_table_header": "请求",
+ "requests_table_header": "请求",
+ "reset": "重置",
+ "reset_settings": "重置设置",
+ "reset_settings_confirm": "重置",
+ "response_details": "响应细节",
+ "response_time": "响应时间",
+ "result": "结果",
+ "resume_protection_timer": "防护将在 %time% 恢复",
+ "rewrite_add": "添加 DNS 重写",
+ "rewrite_domain_input_placeholder": "输入域名或通配符",
+ "rewrite_edit": "编辑 DNS 重写",
+ "rewrite_hosts_applied": "根据 hosts 文件规则已被重写",
+ "rewrite_settings_updated": "DNS 重写设置更新成功",
+ "rewrite_updated": "DNS 重写已成功更新",
+ "rewrites_answer_input_placeholder": "输入 IP 地址、CNAME 记录、A 记录或 AAAA 记录",
+ "rewrites_empty": "暂无重写规则:添加第一条 ",
+ "rewrites_remove_desc": "%value% 的 DNS 重写将被移除",
+ "rewrites_remove_title": "移除 DNS 重写?",
+ "rewrites_tooltip_examples_item1": "仅重写此域名的 DNS 响应",
+ "rewrites_tooltip_examples_item2": "重写 example.org 所有子域名的 DNS 响应",
+ "rewrites_tooltip_instructions_item1": "要将域名解析为 IP 地址,请输入 A 记录的 IPv4 地址或 AAAA 记录的 IPv6 地址。",
+ "rewrites_tooltip_instructions_item2": "要重定向域名,请输入目标域名的 CNAME 记录。",
+ "rewrites_tooltip_instructions_item3": "仅保留 A 记录,请输入",
+ "rewrites_tooltip_instructions_item4": "仅保留 AAAA 记录,请输入",
+ "rewritten": "重写项",
+ "rows_per_page": "每页行数 %value%",
+ "rules_label": "规则",
+ "safe_browsing": "安全浏览",
+ "safe_search": "安全搜索",
+ "safe_search_used": "已使用安全搜索",
+ "safe_search_used_tooltip": "对搜索引擎发起的、已强制启用安全搜索的 DNS 查询数量。",
+ "saturday": "星期六",
+ "save": "保存",
+ "save_btn": "保存",
+ "search_placeholder": "搜索",
+ "selected": "已选择 %value%",
+ "server_config_blocking_mode": "拦截模式",
+ "server_config_blocking_mode_custom_ip_desc": "自定义 IP 地址 :响应手动设置的 IP 地址",
+ "server_config_blocking_mode_default_desc": "默认 :如果被 Adblock 风格规则拦截,对 A 请求响应 0.0.0.0,对 AAAA 请求响应 ::。如果被 /etc/hosts 风格规则拦截,则响应规则中指定的 IP 地址。",
+ "server_config_blocking_mode_ipv4": "拦截 IPv4",
+ "server_config_blocking_mode_ipv4_faq": "被拦截的 A 请求返回的 IP 地址",
+ "server_config_blocking_mode_ipv4_placeholder": "输入 IPv4 地址",
+ "server_config_blocking_mode_ipv6": "拦截 IPv6",
+ "server_config_blocking_mode_ipv6_faq": "被拦截的 AAAA 请求返回的 IP 地址",
+ "server_config_blocking_mode_ipv6_placeholder": "输入 IPv6 地址",
+ "server_config_blocking_mode_null_ip_desc": "空 IP 地址 :对 A 请求响应 0.0.0.0,对 AAAA 请求响应 ::",
+ "server_config_blocking_mode_nxdomain_desc": "NXDOMAIN :响应 NXDOMAIN 码",
+ "server_config_blocking_mode_refused_desc": "REFUSED :响应 REFUSED 码",
+ "server_config_blocking_mode_ttl": "拦截响应 TTL(秒)",
+ "server_config_blocking_mode_ttl_faq": "指定客户端应缓存已过滤响应的时长",
+ "server_config_custom_ip": "自定义 IP 地址",
+ "server_config_default": "默认",
+ "server_config_disable_ipv6": "禁用 IPv6 地址的解析",
+ "server_config_disable_ipv6_desc": "丢弃所有针对 IPv6 地址的 DNS 查询(AAAA 类型),并从 HTTPS 响应中移除 IPv6 提示。",
+ "server_config_dnssec_enable": "启用 DNSSEC",
+ "server_config_dnssec_enable_desc": "在出站 DNS 查询中设置 DNSSEC 标志并检查结果。需要支持 DNSSEC 的解析器。",
+ "server_config_edns_cs_desc": "向上游请求添加 EDNS 客户端子网(ECS)选项,并在查询日志中记录客户端发送的值。",
+ "server_config_edns_enable": "启用 EDNS 客户端子网",
+ "server_config_edns_use_custom_ip": "为 EDNS 使用自定义 IP 地址",
+ "server_config_edns_use_custom_ip_desc": "将客户端 IP 地址替换为下方的自定义 IP 地址",
+ "server_config_null_ip": "空 IP 地址",
+ "server_config_nxdomain": "NXDOMAIN",
+ "server_config_rate_limit": "速率限制(每秒请求数)",
+ "server_config_rate_limit_faq": "每个客户端每秒允许的请求数。值为 0 表示无限制。",
+ "server_config_rate_limit_placeholder": "输入限制速率",
+ "server_config_rate_limit_whitelist": "速率限制白名单",
+ "server_config_rate_limit_whitelist_faq": "排除在速率限制之外的 IP 地址",
+ "server_config_refused": "REFUSED",
+ "server_config_subnet_len_ipv4": "IPv4 地址子网前缀长度",
+ "server_config_subnet_len_ipv4_faq": "用于速率限制的 IPv4 地址子网前缀长度。默认为 24",
+ "server_config_subnet_len_ipv6": "IPv6 地址子网前缀长度",
+ "server_config_subnet_len_ipv6_faq": "用于速率限制的 IPv6 地址子网前缀长度。默认为 56",
+ "server_config_subnet_len_placeholder": "输入前缀长度",
+ "set_static_ip": "设置一个静态 IP",
+ "settings": "设置",
+ "settings_anonymize_client_ip": "对客户端 IP 地址进行匿名化处理",
+ "settings_anonymize_client_ip_desc": "将 IP 地址的一部分替换为零",
+ "settings_browsing_security": "安全浏览",
+ "settings_browsing_security_desc": "拦截恶意和钓鱼域名数据库中的域名访问。",
+ "settings_confirm_clear_query_log": "清除查询日志?",
+ "settings_confirm_clear_query_log_desc": "这将从「查询日志 」中删除所有请求。统计数据仍将保留。",
+ "settings_confirm_clear_statistics": "清除统计数据?",
+ "settings_confirm_clear_statistics_desc": "这将清除所有统计数据。查询日志 仍将保留。",
+ "settings_confirm_decrease_log_rotation_interval": "缩短日志轮替间隔?",
+ "settings_confirm_decrease_log_rotation_interval_desc": "这将删除所有早于 %value% 的日志",
+ "settings_confirm_decrease_stats_rotation_interval": "缩短统计数据保留间隔?",
+ "settings_confirm_decrease_stats_rotation_interval_desc": "这将删除所有早于 %value% 的统计数据",
+ "settings_custom": "用户",
+ "settings_days": "%count% 天 | %count% 天",
+ "settings_domain_names": "域名,每行一个",
+ "settings_filter_requests": "过滤请求",
+ "settings_filter_requests_desc": "您可以使用拦截列表 、允许列表 和用户规则 来设置过滤规则。",
+ "settings_filtering_and_security": "过滤与安全",
+ "settings_general_short": "General",
+ "settings_global": "全局",
+ "settings_hours": "%count% 个小时 | %count% 个小时",
+ "settings_log_dns_requests": "记录 DNS 请求",
+ "settings_log_rotation_hours": "日志轮替时间(小时)",
+ "settings_notify_changes_saved": "更改已保存",
+ "settings_notify_query_log_cleared": "查询日志已清除",
+ "settings_notify_statistics_cleared": "统计数据已清除",
+ "settings_parental_control": "家长控制",
+ "settings_parental_control_desc": "封锁含有儿童不宜内容的网站。",
+ "settings_rotation_placeholder": "请输入数字",
+ "settings_rule_syntax": "规则语法",
+ "settings_safe_search": "安全搜索",
+ "settings_safe_search_desc": "在搜索结果中屏蔽不当内容。",
+ "settings_statistics": "统计",
+ "settings_statistics_clear": "清除统计数据",
+ "settings_statistics_desc": "在仪表盘上显示请求和客户端统计信息",
+ "settings_statistics_retention": "统计保留",
+ "settings_statistics_retention_hours": "统计数据保留时间(小时)",
+ "settings_tooltip_domain_names": "域名、通配符或过滤规则",
+ "settings_tooltip_examples": "示例:",
+ "settings_yes_clear": "是的,清除",
+ "settings_yes_decrease": "确认缩短",
+ "setup_complete": "设置已完成,AdGuard Home 已准备就绪。",
+ "setup_devices_android_list_1": "打开设备的「设置」应用,选择「Wi-Fi」。",
+ "setup_devices_android_list_2": "长按当前网络,然后选择「修改网络」。无法为移动网络连接设置自定义 DNS。",
+ "setup_devices_android_list_3": "在某些设备上,您可能需要勾选「高级」以查看更多设置。要自定义 Android DNS 设置,您需要将 IP 设置从「DHCP」改为「静态」。",
+ "setup_devices_android_list_4": "将「DNS 1」和「DNS 2」的值改为 AdGuard Home 服务器的地址。",
+ "setup_devices_android_title": "Android",
+ "setup_devices_dns_android_list_1": "Android 9 原生支持 DNS-over-TLS 。要配置它,请前往「设置」→「网络和互联网」→「高级」→「私人 DNS」,然后输入您的域名。",
+ "setup_devices_dns_android_list_2": "AdGuard Android 版 支持 DNS-over-HTTPS 、DNS-over-TLS 和 DNS-over-QUIC 设置。",
+ "setup_devices_dns_android_list_3": "Intra 为 Android 增加 DNS-over-HTTPS 支持。",
+ "setup_devices_dns_ios_list_1": "AdGuard iOS 版 支持 DNS-over-HTTPS 、DNS-over-TLS 和 DNS-over-QUIC 设置。",
+ "setup_devices_dns_ios_list_2": "DNSCloak 支持 DNS-over-HTTPS ,但要配置它使用您自己的服务器,需要为其生成一个 DNS Stamp 。",
+ "setup_devices_dns_list_1": "对于 DNS-over-TLS ,使用 ",
+ "setup_devices_dns_list_2": "对于 DNS-over-HTTPS ,使用 ",
+ "setup_devices_dns_list_3": "对于 DNS-over-QUIC ,使用 ",
+ "setup_devices_dns_list_devices": "以下是可以使用的软件列表。",
+ "setup_devices_dns_macos_desc": "在 iOS 14 或 macOS Big Sur 设备上,您可以下载一个特殊的 mobileconfig 文件,在 DNS 设置中添加 DNS-over-HTTPS 或 DNS-over-TLS 服务器。",
+ "setup_devices_dns_other_list_1": "AdGuard Home 本身可以在任何平台上作为安全的 DNS 客户端使用。",
+ "setup_devices_dns_other_list_2": "dnsproxy 支持所有已知的安全 DNS 协议。",
+ "setup_devices_dns_other_list_3": "dnscrypt-proxy 支持 DNS-over-HTTPS 。",
+ "setup_devices_dns_other_list_4": "Mozilla Firefox 支持 DNS-over-HTTPS 。",
+ "setup_devices_dns_other_list_5": "更多实现,请参阅 DNSCrypt 和 DNS 隐私 。",
+ "setup_devices_ios_list_1": "打开设备的「设置」应用,选择「Wi-Fi」。无法为移动网络连接设置自定义 DNS。",
+ "setup_devices_ios_list_2": "点击当前活动网络的名称。",
+ "setup_devices_ios_list_3": "进入「DNS」→「配置 DNS」→「手动」,然后添加 AdGuard Home 服务器的地址。",
+ "setup_devices_ios_list_4": "点击「保存」。",
+ "setup_devices_ios_title": "iOS",
+ "setup_devices_macos_list_1": "点击 Apple 菜单 →「系统偏好设置」→「网络」。",
+ "setup_devices_macos_list_2": "选择所需的连接,然后点击「高级」。",
+ "setup_devices_macos_list_3": "选择「DNS」标签,然后输入 AdGuard Home 服务器的地址。",
+ "setup_devices_macos_title": "macOS",
+ "setup_devices_router_desc_1": "在家庭网络路由器上设置 AdGuard Home,保护所有连接到该路由器的设备。无需手动配置每台设备。",
+ "setup_devices_router_desc_2": "某些路由器不允许设置自定义 DNS 服务器。在这种情况下,您可以尝试将 AdGuard Home 设置为 DHCP 服务器 。如果仍然无效,请查阅您的路由器手册,了解如何为特定路由器自定义 DNS 服务器。",
+ "setup_devices_router_list_1": "通常可以通过浏览器访问类似 http://192.168.0.1 或 http://192.168.1.1 的 URL 进入。可能会要求输入密码。通常可以通过按下路由器上的按钮重置密码,但请注意,这可能会清除整个路由器的配置。如果路由器需要通过应用进行设置,请在手机或电脑上安装该应用,然后使用它访问路由器的设置。",
+ "setup_devices_router_list_1_title": "打开路由器的设置。",
+ "setup_devices_router_list_2": "要查找设置,请寻找 DNS 字段,该字段通常允许输入两组或三组数字,每组分为四个由一到三位数字组成的部分。",
+ "setup_devices_router_list_2_title": "在 DHCP/DNS 设置中,输入 AdGuard Home 服务器的地址。",
+ "setup_devices_router_title": "路由器",
+ "setup_devices_windows_list_1": "从「开始菜单」或「Windows 搜索」打开「控制面板」。",
+ "setup_devices_windows_list_2": "点击「网络和 Internet」→「网络和共享中心」→「更改适配器设置」。",
+ "setup_devices_windows_list_3": "右键点击活动连接,然后选择「属性」。",
+ "setup_devices_windows_list_4": "在列表中,对于 IPv4 选择「Internet 协议版本 4 (TCP/IPv4)」,对于 IPv6 选择「Internet 协议版本 6 (TCP/IPv6)」。",
+ "setup_devices_windows_list_5": "点击「属性」→「使用以下 DNS 服务器地址」。",
+ "setup_devices_windows_list_6": "输入 AdGuard Home 服务器的地址。",
+ "setup_devices_windows_title": "Windows",
+ "setup_dns_desc": "配置路由器或其他设备使用以下 DNS 服务器地址:",
+ "setup_dns_notice_new": "要使用 DNS-over-HTTPS 或 DNS-over-TLS ,您需要在 AdGuard Home 设置中配置加密 。",
+ "setup_dns_privacy_ioc_mac": "iOS 和 macOS 配置",
+ "setup_dns_privacy_other_title": "其他实施方案",
+ "setup_dns_quote_desc": "要正常运行,AdGuard Home DNS 服务器需要在本地网络上拥有静态 IP 地址。否则,路由器可能会将其分配为不同的 IP 地址,从而导致 DNS 保护中断。",
+ "setup_dns_quote_title": "在本地网络上为 AdGuard Home DNS 服务器分配静态 IP 地址",
+ "setup_dns_title": "设置 AdGuard Home DNS 服务器地址",
+ "setup_dns_title_banner": "DNS 服务器设置",
+ "setup_guide": "设置指南",
+ "setup_guide_auth_privacy": "我接受 AdGuard 网站的隐私政策 和条款与条件 ",
+ "setup_guide_auth_subtitle": "这是保护 AdGuard Home 管理面板免遭未经授权访问所必需的。",
+ "setup_guide_auth_title": "设置密码保护",
+ "setup_guide_desc": "配置您的设备使用 AdGuard Home",
+ "setup_guide_device_type": "选择设备类型",
+ "setup_guide_greeting_button": "运行向导",
+ "setup_guide_greeting_desc": "此向导将帮助您配置 AdGuard Home:",
+ "setup_guide_greeting_list_1": "创建用于访问管理面板的凭证",
+ "setup_guide_greeting_list_2": "设置 AdGuard Home 的 Web UI 地址",
+ "setup_guide_greeting_list_3": "设置 AdGuard Home DNS 服务器地址",
+ "setup_guide_greeting_list_4": "获取针对您设备的配置说明",
+ "setup_guide_greeting_list_5": "获取 AdGuard Home 仪表盘的访问权限",
+ "setup_guide_greeting_title": "设置 AdGuard Home",
+ "setup_guide_title": "配置您的设备使用 AdGuard Home",
+ "setup_ui_desc": "使用这些地址访问 AdGuard Home 的 Web 用户界面。如有需要,可更改端口。",
+ "setup_ui_title": "设置 AdGuard Home 的 Web UI 地址",
+ "setup_ui_title_banner": "Web UI 设置",
+ "show_blocked_responses": "已拦截",
+ "show_more_count": "另外 %count% 个",
+ "show_processed_responses": "已处理",
+ "sort_asc": "按 A-Z 排序",
+ "sort_desc": "按 Z-A 排序",
+ "source_label": "来源",
+ "source_tooltip": "来源",
+ "static_dhcp_leases_not_found": "未找到静态 DHCP 租约",
+ "stats_adult": "被拦截的成人网站",
+ "stats_query_domain": "请求域名排行",
+ "status_table_header": "状态",
+ "sunday": "星期日",
+ "system_host_files": "系统主机文件",
+ "system_theme": "系统主题",
+ "table_client": "客户端",
+ "tags_title": "标签",
+ "test_upstreams": "测试上游",
+ "threats_blocked": "危险已拦截",
+ "threats_blocked_tooltip": "被 AdGuard 浏览安全模块拦截的 DNS 查询数量",
+ "thursday": "星期四",
+ "time_table_header": "时间",
+ "top_blocked_domains": "被拦截域名排行",
+ "top_clients": "客户端排行",
+ "top_upstreams": "经常请求的上游服务器",
+ "total": "合计",
+ "try_again": "重试",
+ "tuesday": "星期二",
+ "type_value": "类型:%value%",
+ "unavailable_dhcp": "DHCP 无法使用",
+ "unavailable_dhcp_desc": "AdGuard Home 无法在您的操作系统上运行 DHCP 服务器",
+ "unblock": "取消拦截",
+ "unblock_client": "解除拦截客户端",
+ "unknown_filter": "未知过滤器 %filterId%",
+ "update_button": "更新",
+ "update_filters_custom_hours": "自定义间隔(小时)",
+ "update_filters_disable": "禁用",
+ "update_filters_interval_custom": "用户",
+ "update_filters_interval_daily": "每天",
+ "update_filters_interval_hourly": "每小时",
+ "update_filters_interval_weekly": "每周",
+ "update_filters_title": "自动更新过滤器",
+ "updated_upstream_dns_toast": "上游服务器已保存",
+ "updates_checked": "AdGuard Home 的新版本现在可用",
+ "updates_version_equal": "AdGuard Home已经是最新版本",
+ "upstream": "上游服务器",
+ "upstream_bootstrap_dns_faq": "用于解析 DoH/DoT 上游解析器 IP 地址的 DNS 服务器地址。不允许注释。",
+ "upstream_bootstrap_dns_title": "Bootstrap DNS 服务器,每行一个",
+ "upstream_dns": "上游 DNS 服务器",
+ "upstream_dns_addresses": "服务器地址(每行一个)",
+ "upstream_dns_addresses_faq": "了解更多 关于上游 DNS 服务器配置的信息。以下为已知 DNS 提供商列表 供您选择。",
+ "upstream_dns_configured_in_file": "配置于 %path%",
+ "upstream_dns_fastest_addr": "最快的 IP 地址",
+ "upstream_dns_parallel_requests": "并行请求",
+ "upstream_dns_placeholder": "IP 地址、sdns://、quic://、https://、h3:// 或 tls://",
+ "upstream_dns_servers_title": "上游 DNS 服务器",
+ "upstream_enable_reverse_lookup_desc": "通过向适当的解析器发送 PTR 查询,将客户端 IP 地址反向解析为主机名:本地客户端使用私有 DNS 服务器,公网 IP 地址客户端使用上游服务器。",
+ "upstream_enable_reverse_lookup_title": "启用客户端 IP 地址反向查询",
+ "upstream_example_multiple_upstreams_reserved": "%value%:针对特定域名 的多个上游服务器",
+ "upstream_example_tcp": "%ipv4%、%ipv6%:常规 DNS(基于 TCP)",
+ "upstream_example_tcp_hostname": "%value%:常规 DNS(基于 TCP,主机名)",
+ "upstream_example_tcp_port": "%ipv4%、%ipv6%:常规 DNS(基于 TCP,指定端口)",
+ "upstream_example_udp": "%ipv4%、%ipv6%:常规 DNS(基于 UDP)",
+ "upstream_example_udp_hostname": "%value%:常规 DNS(基于 UDP,主机名)",
+ "upstream_example_udp_port": "%ipv4%、%ipv6%:常规 DNS(基于 UDP,指定端口)",
+ "upstream_example_upstream_comment": "%value%:注释",
+ "upstream_example_upstream_doh": "%value%:加密的 DNS-over-HTTPS ",
+ "upstream_example_upstream_doh3": "%value%:强制使用 HTTP/3 且不降级到 HTTP/2 的加密 DNS-over-HTTPS",
+ "upstream_example_upstream_doq": "%value%:加密的 DNS-over-QUIC ",
+ "upstream_example_upstream_dot": "%value%:加密的 DNS-over-TLS ",
+ "upstream_example_upstream_reserved": "%value%:针对特定域名 的上游服务器",
+ "upstream_example_upstream_sdns": "%value%:适用于 DNSCrypt 或 DNS-over-HTTPS 解析器的 DNS Stamps ",
+ "upstream_examples_title": "示例",
+ "upstream_fallback_title": "备用 DNS 服务器,每行一个",
+ "upstream_fallback_title_faq": "当上游 DNS 服务器不可用时使用的备用 DNS 服务器。语法与上游 DNS 服务器相同。",
+ "upstream_private_ptr_resolvers_desc": "通过私有上游服务器、DHCP、/etc/hosts 等方式解析包含私有 IP 地址的 ARPA 域名的 PTR、SOA 和 NS 请求。如果禁用,AdGuard Home 将对所有此类请求响应 NXDOMAIN。",
+ "upstream_private_ptr_resolvers_title": "使用私人反向 DNS 解析器",
+ "upstream_ptr": "私有反向 DNS 服务器,每行一个",
+ "upstream_ptr_faq_1": "AdGuard Home 使用这些 DNS 服务器处理私有 PTR、SOA 和 NS 请求。私有请求来自具有私有 IP 地址的客户端,查询的 ARPA 域名包含私有 IP 地址范围内的子网,例如 %value%",
+ "upstream_ptr_faq_2": "如果留空,将使用操作系统的默认 DNS 解析器(AdGuard Home IP 地址除外)",
+ "upstream_ptr_faq_3": "默认情况下,AdGuard Home 使用以下反向 DNS 解析器:%value_1% 和 %value_2%",
+ "upstream_timeout": "上游超时时间(秒)",
+ "upstream_timeout_faq": "等待上游服务器响应的时间",
+ "upstream_timeout_placeholder": "输入上游服务器超时时间",
+ "upstreams": "上游服务器",
+ "url_label": "URL",
+ "use_saved_key": "使用现有私钥",
+ "user_rules": "用户过滤器",
+ "user_rules_add_to_allowlist": "添加到白名单",
+ "user_rules_allow_service": "允许服务",
+ "user_rules_browsing_security_disabled": "浏览安全已禁用",
+ "user_rules_check_button": "检查",
+ "user_rules_check_client_label": "客户端标识符(名称、ClientID 或 IP 地址)",
+ "user_rules_check_client_placeholder": "输入客户端标识符",
+ "user_rules_check_dns_record_type_label": "DNS 记录类型",
+ "user_rules_check_hostname_label": "主机名或域名",
+ "user_rules_check_hostname_placeholder": "输入目标",
+ "user_rules_check_title": "检查全局或针对特定客户端的域名过滤",
+ "user_rules_cname": "CNAME :%cname%",
+ "user_rules_disable_browsing_security": "禁用浏览安全",
+ "user_rules_disable_filter": "禁用过滤器",
+ "user_rules_disable_parental_control": "停用家长控制",
+ "user_rules_disable_safe_search": "禁用安全搜索",
+ "user_rules_dns_record_type_placeholder": "选择 DNS 记录类型",
+ "user_rules_dns_rewrite_removed": "规则已从 DNS 重写中移除",
+ "user_rules_domain": "域名 :%value%",
+ "user_rules_domain_blocked": "域名已被拦截",
+ "user_rules_domain_is_allowed": "域名已被允许",
+ "user_rules_domain_is_processed": "域名正在处理中",
+ "user_rules_edit_dns_rewrite": "编辑 DNS 重写",
+ "user_rules_example_block": "||%domain% 拦截对 %domain% 及其所有子域名的访问",
+ "user_rules_example_comment": "! 这是一条注释 要以注释开头,请使用 ! 或 #",
+ "user_rules_example_comment_2": "# 这也是一条注释 要以注释开头,请使用 ! 或 #",
+ "user_rules_example_regex": "/REGEX/ 拦截与指定正则表达式匹配的域名",
+ "user_rules_example_response": "127.0.0.1 %domain% 仅对 %domain% 响应 127.0.0.1(不适用于其子域名)",
+ "user_rules_example_unblock": "@@||%domain% 放行对 %domain% 及其所有子域名的访问",
+ "user_rules_filter_was_disabled": "已禁用 %value% ",
+ "user_rules_ip": "IP 地址 :%ip%",
+ "user_rules_learn_more": "了解更多 关于创建自定义 hosts 列表的信息。",
+ "user_rules_no_rules_matched": "结果 :未匹配到规则",
+ "user_rules_parental_control_disabled": "家长控制已禁用",
+ "user_rules_placeholder": "这是一个占位符",
+ "user_rules_reason": "原因 :%reason%",
+ "user_rules_reason_allowed_by": "被 %source% 允许",
+ "user_rules_reason_blocked_by": "被 %source% 拦截",
+ "user_rules_reason_filtered_by": "被 %source% 过滤",
+ "user_rules_redirected_to": "已重定向到 :%value%",
+ "user_rules_remove_dns_rewrite": "移除 DNS 重写",
+ "user_rules_rewrite_rule_is_applied": " 重定向规则已应用",
+ "user_rules_rewritten_to": "已重写为 :%value%",
+ "user_rules_rule": "规则 :%rule%",
+ "user_rules_rule_added_to_custom_filtering_rules": "规则已添加到自定义过滤规则",
+ "user_rules_rule_removed": "规则已从自定义过滤规则中移除",
+ "user_rules_safe_search_disabled": "安全搜索已禁用",
+ "user_rules_service": "服务 :%service%",
+ "user_rules_service_allowed": "已允许 %value% 服务",
+ "user_rules_source": "来源 :%value%",
+ "user_rules_source_allowlist": "允许列表 :%value%",
+ "user_rules_source_blocklist": "拦截列表 :%value%",
+ "user_rules_status": "状态 :%reason%",
+ "username_label": "用户名",
+ "username_placeholder": "输入用户名",
+ "validated_with_dnssec": "通过 DNSSEC 验证",
+ "wednesday": "星期三",
+ "whois": "WHOIS",
+ "yes": "是",
+ "yes_disable": "禁用",
+ "yes_reset": "重置"
+}
diff --git a/client_v2/src/__locales/zh-hk.json b/client_v2/src/__locales/zh-hk.json
new file mode 100644
index 00000000000..4c261c82492
--- /dev/null
+++ b/client_v2/src/__locales/zh-hk.json
@@ -0,0 +1,315 @@
+{
+ "access_settings_title": "存取設定",
+ "actions_table_header": "動作",
+ "add_allowlist": "新增白名單",
+ "add_blocklist": "新增黑名單",
+ "add_persistent_client": "加入到用戶端",
+ "adult_websites_blocked_tooltip": "已封鎖成人網站總數",
+ "all_lists_up_to_date_toast": "所有清單已更新至最新",
+ "allowlist_add": "新增白名單",
+ "allowlist_edit": "編輯白名單",
+ "apply": "套用",
+ "autofix_warning_result": "就結論來說 DNS 請求預設由本機的 AdGuard Home 處理。",
+ "autofix_warning_text": "如果您點擊「修復」,AdGuard Home 將更改您的系統 DNS 設定更改為 AdGuard Home DNS 伺服器",
+ "average_time_processing": "平均的處理時間",
+ "average_upstream_response_time": "平均上游伺服器回應時間",
+ "back": "返回",
+ "block": "封鎖",
+ "block_for_this_client_only": "僅封鎖此用戶端",
+ "blocked_by_cname_or_ip": "使用 CNAME 或 IP 封鎖",
+ "blocked_service": "封鎖服務",
+ "blocked_services": "已封鎖服務",
+ "blocklist_add_manual": "手動增加",
+ "blocklist_edit": "編輯黑名單",
+ "bootstrap_dns": "引導(Boostrap) DNS 伺服器",
+ "cache_cleared": "DNS 快取成功清除",
+ "cache_config_clear": "清除快取",
+ "cache_config_optimistic": "優化的",
+ "cache_config_ttl_validation": "最小快取 TTL 值必須小於或等於最大值",
+ "cache_enabled": "啟用快取",
+ "cancel": "取消",
+ "cancel_btn": "取消",
+ "category_tooltip": "類別",
+ "check_dhcp_servers": "檢查 DHCP 伺服器",
+ "check_not_found": "未在您的過濾清單中找到",
+ "check_updates_btn": "檢查更新",
+ "clear_btn": "清除",
+ "client_add": "新增用戶端",
+ "client_details": "用戶端詳細資料",
+ "client_id": "用戶端 ID",
+ "client_id_placeholder": "輸入用戶端 ID",
+ "client_identifier": "識別碼",
+ "client_settings": "用戶端設定",
+ "client_table_header": "用戶端",
+ "clients_add_identifier": "新增識別碼",
+ "clients_add_placeholder": "輸入用戶端名稱",
+ "clients_dns_cache_size": "DNS 快取大小(bytes)",
+ "clients_not_found": "找不到用戶端",
+ "clients_protection": "保護",
+ "clients_tags": "標籤",
+ "clients_use_global_settings": "使用全域設定",
+ "close": "關閉",
+ "custom_filtering_rules": "自訂過濾規則",
+ "dark_theme": "深色主題",
+ "dashboard": "儀表板",
+ "dhcp_add_static_lease": "新增靜態租用",
+ "dhcp_config_saved": "DHCP 設定已儲存",
+ "dhcp_edit_static_lease": "編輯靜態租約",
+ "dhcp_form_gateway_input": "閘道 IP 位址",
+ "dhcp_form_subnet_input": "子網路遮罩",
+ "dhcp_leases_not_found": "找不到 DHCP 租約",
+ "dhcp_leases_title": "DHCP 租用",
+ "dhcp_new_static_lease": "新增靜態租用",
+ "dhcp_reset_leases": "重置所有 DHCP 租約",
+ "dhcp_reset_leases_success": "重置 DHCP 租約成功",
+ "dhcp_settings": "DHCP 設定",
+ "dhcp_static_leases_not_found": "找不到 DHCP 靜態租用",
+ "dhcp_table_hostname": "主機名稱",
+ "dhcp_table_ip_address": "IP 位址",
+ "disable": "停用",
+ "disabled_dhcp": "DHCP 伺服器已關閉",
+ "disallow_this_client": "不允許此用戶端",
+ "dns_access_settings_title": "存取設定",
+ "dns_allowed_clients": "用戶端白名單",
+ "dns_blocking_mode": "封鎖模式",
+ "dns_blocking_mode_custom_ip": "自訂 IP 位址",
+ "dns_blocking_mode_default": "預設",
+ "dns_blocking_mode_ipv4_label": "封鎖 IPv4",
+ "dns_blocking_mode_ipv6_label": "封鎖 IPv6",
+ "dns_blocking_mode_nxdomain": "NXDOMAIN",
+ "dns_blocking_mode_refused": "REFUSED",
+ "dns_blocking_mode_title": "封鎖模式",
+ "dns_bootstrap_dns_placeholder": "IP 位址",
+ "dns_bootstrap_dns_title": "引導(Boostrap) DNS 伺服器",
+ "dns_bootstrap_servers": "引導(Boostrap) DNS 伺服器",
+ "dns_cache_config": "DNS 快取設定",
+ "dns_cache_size": "快取大小",
+ "dns_cache_size_label": "DNS 快取大小(bytes)",
+ "dns_cache_size_title": "快取大小",
+ "dns_clear_cache": "清除快取",
+ "dns_clear_cache_cancel": "取消",
+ "dns_clear_cache_confirm": "確定清除",
+ "dns_clear_cache_yes": "確定清除",
+ "dns_config": "DNS 伺服器設定",
+ "dns_disallowed_clients": "用戶端黑名單",
+ "dns_disallowed_clients_title": "用戶端黑名單",
+ "dns_disallowed_domains": "網域黑名單",
+ "dns_disallowed_domains_title": "網域黑名單",
+ "dns_fallback_dns_title": "備用 DNS 伺服器",
+ "dns_fallback_servers": "備用 DNS 伺服器",
+ "dns_optimistic_caching": "優化的",
+ "dns_over_https": "DNS-over-HTTPS",
+ "dns_over_tls": "DNS-over-TLS",
+ "dns_override_max_ttl": "覆寫最大 TTL 值",
+ "dns_override_max_ttl_title": "覆寫最大 TTL 值",
+ "dns_override_min_ttl": "覆寫最小 TTL 值",
+ "dns_override_min_ttl_title": "覆寫最小 TTL 值",
+ "dns_privacy": "DNS 隱私",
+ "dns_private_reverse_servers_title": "私人 DNS 伺服器",
+ "dns_rate_limit": "速率限制",
+ "dns_rate_limit_allowlist_placeholder": "IP 位址",
+ "dns_rate_limit_placeholder": "輸入速率限制",
+ "dns_rate_limit_title": "速率限制",
+ "dns_rewrites": "DNS 覆寫",
+ "dns_server_addresses_label": "DNS 伺服器位置,每行一個",
+ "dns_server_config_title": "DNS 伺服器設定",
+ "dns_test_upstreams": "測試上游 DNS",
+ "dns_upstream_servers_title": "上游 DNS 伺服器",
+ "dns_upstream_timeout": "上游超時",
+ "dns_upstream_timeout_title": "上游超時",
+ "domain": "網域",
+ "domain_or_client": "網域或用戶端",
+ "download_mobileconfig": "下載描述檔",
+ "edit_table_action": "編輯",
+ "enable": "啟用",
+ "enabled_dhcp": "DHCP 伺服器已啟動",
+ "encryption_certificate_path": "憑證路徑",
+ "encryption_certificates": "憑證",
+ "encryption_certificates_input": "在這裡複製/貼上您的 PEM 憑證。",
+ "encryption_certificates_source_content": "貼上憑證內容",
+ "encryption_certificates_source_path": "設定憑證檔案路徑",
+ "encryption_chain_invalid": "憑證連結無效",
+ "encryption_chain_valid": "憑證鏈結有效",
+ "encryption_config_saved": "加密設定已儲存",
+ "encryption_doq": "DNS-over-QUIC 連接埠",
+ "encryption_dot": "DNS-over-TLS 連接埠",
+ "encryption_https": "HTTPS 連接埠",
+ "encryption_key": "私密金鑰",
+ "encryption_key_input": "在這裡複製/貼上您的 PEM 憑證。",
+ "encryption_key_source_content": "貼上私鑰內容",
+ "encryption_plain_dns": "一般未加密 DNS",
+ "encryption_plain_dns_error": "若要停用一般 DNS,請啟用至少一個加密的 DNS 協定",
+ "encryption_private_key_path": "私鑰路徑",
+ "encryption_server_enter": "輸入您的網域名稱",
+ "encryption_title": "加密",
+ "enter_cache_size": "輸入快取大小(bytes)",
+ "error": "錯誤",
+ "fallback_dns": "備用 DNS 伺服器",
+ "filter_category_general_desc": "封鎖大多數裝置的廣告與追蹤器清單",
+ "filter_category_other": "其它",
+ "filter_category_other_desc": "其他封鎖清單",
+ "filter_category_regional": "區域性",
+ "filter_category_regional_desc": "針對地區性廣告與追蹤器伺服器的封鎖清單",
+ "filter_category_security_desc": "針對惡意軟體、網路釣魚或詐騙網域的封鎖清單",
+ "forgot_password": "忘記密碼?",
+ "form_enter_blocked_response_ttl": "輸入已封鎖的回應 TTL(秒)",
+ "form_error_client_id_format": "無效的「客戶端 ID」格式",
+ "form_error_domain_format": "網域格式無效",
+ "form_error_equal": "不可相同",
+ "form_error_gateway_ip": "租約不能使用閘道器的 IP 位址",
+ "form_error_ip4_format": "無效的 IPv4 格式",
+ "form_error_ip6_format": "無效的 IPv6 格式",
+ "form_error_ip_format": "無效的 IP 位址",
+ "form_error_mac_format": "無效的 「MAC 位址」格式",
+ "form_error_password": "密碼不相符",
+ "form_error_port": "輸入有效的連接埠",
+ "form_error_port_range": "輸入範圍 80-65535 中的值",
+ "form_error_port_unsafe": "這個連接埠不安全",
+ "form_error_server_name": "無效伺服器名稱",
+ "form_error_url_format": "請輸入正確的網址或檔案路徑",
+ "friday": "星期五",
+ "gateway_or_subnet_invalid": "無效子網路",
+ "general_statistics": "一般統計資料",
+ "ignore_domains_title": "已忽略網域",
+ "inactivity_schedule_add": "新增排程",
+ "inactivity_schedule_all_day": "全天",
+ "inactivity_schedule_edit": "編輯排程",
+ "install_auth_username_enter": "輸入用戶名",
+ "install_saved": "成功儲存",
+ "install_settings_all_interfaces": "所有介面",
+ "install_static_error": "AdGuard Home 無法在這個網路介面上執行自動設定。請尋找有關如何手動更改設定的說明。",
+ "install_static_ok": "好消息!靜態 IP 位址設定完成了",
+ "install_step": "步驟",
+ "install_submit_title": "恭喜!",
+ "ip_address": "IP 位址",
+ "known_tracker": "已知追蹤器",
+ "light_theme": "淺色主題",
+ "logout": "登出",
+ "make_static": "新增為靜態",
+ "milliseconds_abbreviation": "ms",
+ "monday": "星期一",
+ "nothing_found": "沒有結果",
+ "notify_updated": "已更新",
+ "parental_control": "家長監護",
+ "parental_group_development": "軟體開發",
+ "parental_group_gaming": "遊戲",
+ "parental_group_hosting": "網頁寄存",
+ "parental_group_privacy": "隱私權工具",
+ "parental_group_shopping": "購物",
+ "parental_group_social": "社交媒體",
+ "parental_group_social_network": "社交媒體",
+ "parental_group_software": "軟體開發",
+ "parental_group_streaming": "線上串流",
+ "plain_dns": "一般未加密 DNS",
+ "privacy_policy": "隱私政策",
+ "protection": "保護",
+ "protocol": "選擇協定",
+ "query_log_blocked_services": "已封鎖服務",
+ "query_log_custom_filtering_rules": "自訂過濾規則",
+ "query_log_nothing_available": "沒有結果",
+ "query_log_processed": "已處理",
+ "query_log_rewritten": "已覆寫",
+ "query_log_safe_search": "安全搜尋",
+ "query_log_strict_search": "使用雙引號來強調搜尋結果",
+ "rate_limit_subnet_len_ipv4_error": "IPv4 子網路前綴長度應介於 0 到 32 之間",
+ "rate_limit_subnet_len_ipv6_error": "IPv6 子網路前綴長度應介於 0 到 128 之間",
+ "refresh_btn": "重新整理",
+ "refresh_statics": "重新整理統計資料",
+ "release_notes": "發行說明",
+ "remove": "移除",
+ "request_table_header": "請求",
+ "reset": "重設",
+ "reset_settings": "重設設定",
+ "reset_settings_confirm": "是,重設",
+ "response_details": "回應詳細資料",
+ "response_time": "回應時間",
+ "rewrite_add": "新增 DNS 覆寫",
+ "rewrite_edit": "編輯 DNS 覆寫",
+ "rewrite_hosts_applied": "由「hosts 檔案」覆寫",
+ "rewrite_updated": "已更新 DNS 覆寫",
+ "rewritten": "已覆寫",
+ "rules_label": "規則",
+ "safe_browsing": "安全瀏覽",
+ "safe_search": "安全搜尋",
+ "saturday": "星期六",
+ "save": "儲存",
+ "save_btn": "儲存",
+ "server_config_blocking_mode": "封鎖模式",
+ "server_config_blocking_mode_ipv4": "封鎖 IPv4",
+ "server_config_blocking_mode_ipv6": "封鎖 IPv6",
+ "server_config_custom_ip": "自訂 IP 位址",
+ "server_config_default": "預設",
+ "server_config_disable_ipv6": "停止解析 IPv6 位址",
+ "server_config_dnssec_enable": "啟用 DNSSEC",
+ "server_config_edns_enable": "啟用 EDNS Client Subnet",
+ "server_config_nxdomain": "NXDOMAIN",
+ "server_config_rate_limit_placeholder": "輸入速率限制",
+ "server_config_rate_limit_whitelist_faq": "排除在速率限制之外的 IP 位址",
+ "server_config_refused": "REFUSED",
+ "server_config_subnet_len_ipv4": "IPv4 位址的子網路前綴長度",
+ "server_config_subnet_len_ipv4_faq": "用於速率限制的 IPv4 位址的子網路前綴長度。 預設為 24",
+ "server_config_subnet_len_ipv6": "IPv6 位址的子網路前綴長度",
+ "server_config_subnet_len_ipv6_faq": "用於速率限制的 IPv6 位址的子網路前綴長度。 預設為 56",
+ "set_static_ip": "設定一組靜態 IP 位址",
+ "settings": "設定",
+ "settings_browsing_security": "瀏覽安全",
+ "settings_custom": "自訂",
+ "settings_global": "全域",
+ "settings_rule_syntax": "規則語法",
+ "settings_safe_search": "安全搜尋",
+ "settings_statistics": "統計資料",
+ "settings_statistics_retention": "統計資料保留時間",
+ "settings_yes_clear": "確定清除",
+ "setup_devices_router_title": "路由器",
+ "setup_dns_privacy_ioc_mac": "iOS 與 macOS 描述檔",
+ "setup_dns_privacy_other_title": "其他實作方式",
+ "show_processed_responses": "已處理",
+ "stats_adult": "已封鎖成人網站",
+ "stats_query_domain": "熱門查詢網域排行",
+ "status_table_header": "狀態",
+ "sunday": "星期日",
+ "system_host_files": "系統 hosts 檔案",
+ "table_client": "用戶端",
+ "tags_title": "標籤",
+ "test_upstreams": "測試上游 DNS",
+ "threats_blocked": "已封鎖的威脅",
+ "thursday": "星期四",
+ "time_table_header": "時間",
+ "top_blocked_domains": "熱門封鎖網域排行",
+ "top_clients": "熱門用戶端排行",
+ "top_upstreams": "熱門上游伺服器",
+ "total": "總計",
+ "try_again": "再試一次",
+ "tuesday": "星期二",
+ "unavailable_dhcp": "DHCP 無法使用",
+ "unavailable_dhcp_desc": "AdGuard Home 無法在您的作業系統上運行 DHCP 伺服器",
+ "unblock": "解除封鎖",
+ "update_filters_disable": "停用",
+ "update_filters_interval_custom": "自訂",
+ "update_filters_interval_daily": "每天",
+ "update_filters_title": "自動地更新過濾器",
+ "updates_checked": "檢查更新成功",
+ "updates_version_equal": "AdGuard Home 是最新的版本",
+ "upstream": "上游伺服器",
+ "upstream_dns": "上游 DNS 伺服器",
+ "upstream_dns_addresses": "DNS 伺服器位置,每行一個",
+ "upstream_dns_fastest_addr": "Fastest IP 位址",
+ "upstream_dns_parallel_requests": "平行處理",
+ "upstream_dns_servers_title": "上游 DNS 伺服器",
+ "upstream_examples_title": "範例",
+ "upstream_private_ptr_resolvers_title": "使用私人 DNS 反解器",
+ "upstreams": "上游",
+ "user_rules_check_button": "檢查",
+ "user_rules_check_client_placeholder": "輸入用戶識別碼",
+ "user_rules_check_hostname_label": "主機名稱或域名",
+ "user_rules_dns_record_type_placeholder": "選擇 DNS 記錄類型",
+ "user_rules_domain_blocked": "已封鎖網域",
+ "user_rules_edit_dns_rewrite": "編輯 DNS 覆寫",
+ "user_rules_rewrite_rule_is_applied": "已套用 DNS 覆寫規則",
+ "username_placeholder": "輸入用戶名",
+ "validated_with_dnssec": "DNSSEC 驗證有效",
+ "wednesday": "星期三",
+ "whois": "Whois",
+ "yes": "是",
+ "yes_reset": "是,重設"
+}
diff --git a/client_v2/src/__locales/zh-tw.json b/client_v2/src/__locales/zh-tw.json
new file mode 100644
index 00000000000..c06fe10f1ef
--- /dev/null
+++ b/client_v2/src/__locales/zh-tw.json
@@ -0,0 +1,864 @@
+{
+ "access_settings_allowed_faq": "CIDR、IP 位址或 ClientID 清單。如果此欄位不為空,AdGuard Home 將僅接受來自這些用戶端的請求。",
+ "access_settings_allowed_placeholder": "輸入允許的用戶端",
+ "access_settings_allowed_title": "允許的用戶端,每行一個",
+ "access_settings_blocked_faq_1": "AdGuard Home 將丟棄符合這些網域的 DNS 查詢。此類查詢不會出現在查詢紀錄中。您可以輸入精確網域、萬用字元或 URL 過濾規則,例如 `example.org`、`*.example.org` 或 `||example.org^`。",
+ "access_settings_blocked_faq_2": "請勿與過濾器混淆",
+ "access_settings_blocked_placeholder": "輸入禁止的網域",
+ "access_settings_blocked_title": "禁止的網域,每行一個",
+ "access_settings_disallowed_faq": "CIDR、IP 位址或 ClientID 清單。如果此欄位不為空,AdGuard Home 將丟棄來自這些用戶端的請求。如果允許的用戶端 不為空,則忽略此欄位。",
+ "access_settings_disallowed_placeholder": "輸入禁止的用戶端",
+ "access_settings_disallowed_title": "禁止的用戶端,每行一個",
+ "access_settings_saved_toast": "存取設定已儲存",
+ "access_settings_title": "存取設定",
+ "actions_table_header": "動作",
+ "add": "新增",
+ "add_allowlist": "新增允許清單",
+ "add_blocklist": "新增封鎖清單",
+ "add_persistent_client": "新增為永久性客戶端",
+ "add_to_allowlist": "加至允許清單",
+ "ads_blocked": "已封鎖的廣告",
+ "ads_blocked_card": "已封鎖的廣告",
+ "ads_blocked_tooltip": "被廣告封鎖過濾器和 hosts 封鎖清單封鎖的 DNS 查詢數量",
+ "adult_websites_blocked": "成人網站已封鎖",
+ "adult_websites_blocked_tooltip": "已封鎖的成人網站之數量",
+ "all_domains": "所有網域",
+ "all_lists_up_to_date_toast": "所有的清單已是最新的",
+ "all_rewrites_disabled": "所有自訂 DNS 回應將被停用",
+ "all_rewrites_enabled": "所有自訂 DNS 回應將被啟用",
+ "allow_service": "允許服務",
+ "allowed": "被允許的",
+ "allowlist_add": "新增允許清單",
+ "allowlist_edit": "編輯允許清單",
+ "allowlist_empty": "暫無允許清單:新增第一個 ",
+ "allowlist_placeholder_example": "我的允許清單",
+ "allowlist_remove": "移除允許清單?",
+ "allowlist_remove_desc": "將移除 %value% ",
+ "allowlists": "允許清單",
+ "allowlists_title": "允許清單",
+ "apply": "套用",
+ "aria_clear_input": "清除輸入",
+ "autofix_warning_list": "它將執行以下任務:
停用系統 DNSStubListener
將 DNS 伺服器位址設為 127.0.0.1
將 /etc/resolv.conf 的符號連結目標替換為 /run/systemd/resolve/resolv.conf
停止 DNSStubListener(重新載入 systemd-resolved 服務)
",
+ "autofix_warning_result": "因此,預設下,來自您的系統之所有的 DNS 請求將被 AdGuard Home 處理。",
+ "autofix_warning_text": "如果您點擊\"修復\",AdGuard Home 將配置您的系統使用 AdGuard Home DNS 伺服器。",
+ "average_time_processing": "平均的處理時間",
+ "average_time_processing_tooltip": "DNS 查詢平均處理時間(毫秒)",
+ "average_upstream_response_time": "平均的上游回應時間",
+ "back": "返回",
+ "block": "封鎖",
+ "block_client": "封鎖用戶端",
+ "block_for_this_client_only": "僅對此用戶端封鎖",
+ "blocked": "已封鎖的",
+ "blocked_adult_websites": "被家長控制封鎖",
+ "blocked_by_cname_or_ip": "被正規名稱(CNAME)或 IP 封鎖",
+ "blocked_queries": "已封鎖的查詢",
+ "blocked_service": "已封鎖的服務",
+ "blocked_services": "已封鎖的服務",
+ "blocked_services_desc": "選擇需要封鎖的熱門網站和服務。",
+ "blocked_threats": "已封鎖的威脅",
+ "blocked_threats_chart": "已封鎖的威脅",
+ "blocked_total": "共封鎖 %value%",
+ "blocklist_add_from_list": "從清單新增",
+ "blocklist_add_manual": "手動地新增",
+ "blocklist_contents": "內容(.%value%)",
+ "blocklist_edit": "編輯封鎖清單",
+ "blocklist_homepage": "首頁",
+ "blocklist_placeholder_example": "我的封鎖清單",
+ "blocklist_remove": "移除封鎖清單?",
+ "blocklist_remove_desc": "將移除 %value% ",
+ "blocklist_url_file_path": "網址或檔案安裝路徑",
+ "blocklists_add": "新增封鎖清單",
+ "blocklists_add_desc": "您可以從清單中選擇封鎖清單,或手動新增。",
+ "blocklists_desc": "這些清單中的網域將被封鎖",
+ "blocklists_empty": "暫無封鎖清單:新增第一個 ",
+ "blocklists_title": "封鎖清單",
+ "bootstrap_dns": "自我啟動(Bootstrap)DNS 伺服器",
+ "browse": "瀏覽",
+ "cache_cleared": "DNS 快取被成功地清除",
+ "cache_config_clear": "清除快取",
+ "cache_config_max_ttl": "覆蓋最大 TTL(秒)",
+ "cache_config_max_ttl_faq": "DNS 快取條目的最大存活時間(TTL)值",
+ "cache_config_max_ttl_placeholder": "輸入最大 TTL",
+ "cache_config_min_ttl": "覆蓋最小 TTL(秒)",
+ "cache_config_min_ttl_faq": "DNS 快取條目的最小存活時間(TTL)值",
+ "cache_config_min_ttl_placeholder": "輸入最小 TTL",
+ "cache_config_optimistic": "樂觀快取",
+ "cache_config_optimistic_desc": "即使快取條目已過期,AdGuard Home 也會從快取中回應,並嘗試更新它們。",
+ "cache_config_size": "快取大小(位元組)",
+ "cache_config_size_faq": "DNS 快取大小。要停用快取,請留空。",
+ "cache_config_size_validation": "啟用時,快取大小必須大於零。",
+ "cache_config_ttl_validation": "最小的快取存活時間(TTL)覆寫必須小於或等於最大的",
+ "cache_confirm_clear_desc": "這將刪除所有快取的 DNS 回應。後續 DNS 查詢可能會變慢,直到快取重建完成。",
+ "cache_confirm_clear_title": "清除 DNS 快取",
+ "cache_enabled": "啟用快取",
+ "cache_enabled_desc": "在本機儲存 DNS 回應",
+ "cancel": "取消",
+ "cancel_btn": "取消",
+ "category_tooltip": "類別",
+ "changes_saved_success": "更改已儲存",
+ "check_dhcp_servers": "檢查動態主機設定協定(DHCP)伺服器",
+ "check_not_found": "未在您的過濾器清單中被找到",
+ "check_updates_btn": "檢查更新",
+ "clear_btn": "清除",
+ "clear_query_log": "清除查詢記錄",
+ "client_add": "新增用戶端",
+ "client_added": "用戶端已新增",
+ "client_already_blocked": "用戶端 %ip% 已被封鎖",
+ "client_blocked_flash": "用戶端已封鎖",
+ "client_details": "用戶端細節",
+ "client_id": "用戶端 ID",
+ "client_id_faq": "用戶端可以透過其 ClientID 進行識別。要了解更多資訊,請造訪 AdGuard Home GitHub 。",
+ "client_id_placeholder": "輸入用戶端 ID",
+ "client_identifier": "識別碼",
+ "client_ip": "用戶端 IP",
+ "client_location": "用戶端位置",
+ "client_removed": "用戶端已移除",
+ "client_settings": "用戶端設定",
+ "client_table_header": "用戶端",
+ "client_unblocked_flash": "用戶端已解除封鎖",
+ "clients": "用戶端",
+ "clients_add": "新增用戶端",
+ "clients_add_identifier": "新增識別碼",
+ "clients_add_name": "用戶端名稱",
+ "clients_add_placeholder": "輸入用戶端名稱",
+ "clients_desc": "以下規則用於連線到 AdGuard Home 的裝置",
+ "clients_dns_cache_size": "DNS 快取大小,單位:位元",
+ "clients_dns_cache_size_placeholder": "請輸入數字",
+ "clients_dont_collect_stats": "不收集此用戶端的統計資料",
+ "clients_dont_log": "不記錄此用戶端",
+ "clients_edit": "編輯用戶端",
+ "clients_identifier_already_used": "此識別碼已被使用",
+ "clients_identifier_format_error": "請使用數字、小寫英文字母或連字號。",
+ "clients_identifiers": "識別碼",
+ "clients_identifiers_desc": "您可以使用用戶端的 IP 位址、CIDR、MAC 位址或 ClientID(適用於 DoT、DoH 和 DoQ)。了解更多 ",
+ "clients_logs_and_statistics": "紀錄與統計",
+ "clients_not_found": "無已發現之用戶端",
+ "clients_protection": "防護",
+ "clients_protection_desc": "啟用或停用保護功能、日誌記錄和統計。",
+ "clients_remove_desc": "%value% 及其設定將從永久用戶端中移除。",
+ "clients_remove_title": "移除用戶端?",
+ "clients_tags": "標記",
+ "clients_tags_desc": "您可以為此用戶端新增標籤,並將其包含在過濾規則中。了解更多 ",
+ "clients_upstreams_desc": "上游 DNS 伺服器,每行一個",
+ "clients_use_dns_cache": "使用 DNS 快取",
+ "clients_use_global_settings": "使用全域的設定",
+ "clients_use_global_settings_desc": "繼承 AdGuard Home 帳戶的保護設定",
+ "close": "關閉",
+ "close_result": "關閉結果",
+ "confirm": "確認",
+ "confirm_client_block_desc": "AdGuard Home 將丟棄來自用戶端 %ip% 的所有 DNS 請求。",
+ "confirm_client_block_title": "封鎖用戶端?",
+ "confirm_client_unblock_desc": "來自用戶端 %ip% 的 DNS 請求將被允許。",
+ "confirm_client_unblock_title": "解除封鎖用戶端?",
+ "confirm_static_ip": "AdGuard Home 將設定 %ip% 為您的靜態 IP 位址。是否繼續?",
+ "copied": "已複製到剪貼簿",
+ "copy": "複製",
+ "copy_to_clipboard": "複製到剪貼簿",
+ "custom_filtering_rules": "自訂的過濾規則",
+ "dark_theme": "深色主題",
+ "dashboard": "儀表板",
+ "delete_btn": "刪除",
+ "delete_confirm": "刪除租約?",
+ "delete_confirm_desc": "這將釋放 %ip% ",
+ "delete_table_action": "刪除",
+ "delete_table_action_confirm": "確認刪除",
+ "device_type": "裝置類型",
+ "dhcp_add_static_lease": "新增靜態租約",
+ "dhcp_config_saved": "動態主機設定協定(DHCP)配置被成功地儲存",
+ "dhcp_edit_static_lease": "編輯靜態租約",
+ "dhcp_error": "檢查 DHCP 伺服器失敗。請稍後重試。",
+ "dhcp_form_gateway_address": "閘道 IP 位址",
+ "dhcp_form_gateway_input": "閘道 IP",
+ "dhcp_form_range_start": "起始 IP 位址",
+ "dhcp_form_range_title": "IP 位址範圍",
+ "dhcp_form_subnet_input": "子網路遮罩",
+ "dhcp_found": "無法啟用 DHCP 伺服器",
+ "dhcp_ipv4_settings": "DHCP IPv4 設定",
+ "dhcp_ipv6_settings": "DHCP IPv6 設定",
+ "dhcp_lease_added": "靜態租約「%key%」已成功新增",
+ "dhcp_lease_deleted": "靜態租約「%key%」已成功刪除",
+ "dhcp_lease_updated": "靜態租約『%key%」已成功更新",
+ "dhcp_leases_not_found": "無已發現之動態主機設定協定(DHCP)租約",
+ "dhcp_leases_title": "動態主機設定協定(DHCP)租約",
+ "dhcp_mac_address_already_added": "此 MAC 位址已新增",
+ "dhcp_new_static_lease": "新的靜態租約",
+ "dhcp_not_found": "DHCP 伺服器已啟用",
+ "dhcp_reset": "這將重設 DHCP 設定。所有變更將遺失。",
+ "dhcp_reset_leases": "重置所有的租約",
+ "dhcp_reset_leases_confirm": "這將重設所有租約",
+ "dhcp_reset_leases_success": "動態主機設定協定(DHCP)租約被成功地重置",
+ "dhcp_settings": "動態主機設定協定(DHCP)設定",
+ "dhcp_static_ip_error": "DHCP 伺服器需要靜態 IP 位址。我們無法確認此網路介面已設定靜態 IP。",
+ "dhcp_static_leases_not_found": "無已發現之動態主機設定協定(DHCP)靜態租約",
+ "dhcp_table_hostname": "主機名稱",
+ "dhcp_table_ip_address": "IP 位址",
+ "dhcp_table_mac_address": "MAC 位址",
+ "disable": "禁用",
+ "disable_protection_btn": "禁用防護",
+ "disabled_dhcp": "DHCP 伺服器已停用",
+ "disallow_client_confirm_allowlist_note": "此操作還將從允許的用戶端清單中移除規則 %ip%。",
+ "disallow_client_confirm_title": "禁止此用戶端?",
+ "disallow_this_client": "不允許此用戶端",
+ "dns_access_settings_title": "存取設定",
+ "dns_allowed_clients": "被允許的用戶端",
+ "dns_allowed_clients_label": "允許的用戶端,每行一個",
+ "dns_allowed_clients_placeholder": "輸入允許的用戶端",
+ "dns_blocking_mode": "封鎖模式",
+ "dns_blocking_mode_custom_ip": "自訂的 IP 位址",
+ "dns_blocking_mode_default": "預設",
+ "dns_blocking_mode_ipv4_label": "封鎖 IPv4",
+ "dns_blocking_mode_ipv4_placeholder": "輸入 IPv4 位址",
+ "dns_blocking_mode_ipv6_label": "封鎖 IPv6",
+ "dns_blocking_mode_ipv6_placeholder": "輸入 IPv6 位址",
+ "dns_blocking_mode_null_ip": "空 IP 位址",
+ "dns_blocking_mode_nxdomain": "不存在的網域(NXDOMAIN)",
+ "dns_blocking_mode_refused": "已拒絕(REFUSED)",
+ "dns_blocking_mode_title": "封鎖模式",
+ "dns_blocking_mode_ttl_label": "封鎖回應 TTL(秒)",
+ "dns_bootstrap_dns_label": "Bootstrap DNS 伺服器,每行一個",
+ "dns_bootstrap_dns_placeholder": "IP 位址",
+ "dns_bootstrap_dns_title": "自我啟動(Bootstrap)DNS 伺服器",
+ "dns_bootstrap_servers": "自我啟動(Bootstrap)DNS 伺服器",
+ "dns_cache_config": "DNS 快取配置",
+ "dns_cache_configuration_saved_toast": "DNS 快取設定已儲存",
+ "dns_cache_size": "快取大小",
+ "dns_cache_size_label": "DNS 快取大小,單位:位元",
+ "dns_cache_size_title": "快取大小",
+ "dns_clear_cache": "清除快取",
+ "dns_clear_cache_cancel": "取消",
+ "dns_clear_cache_confirm": "是的,清除",
+ "dns_clear_cache_yes": "是的,清除",
+ "dns_config": "DNS 伺服器配置",
+ "dns_disallowed_clients": "未被允許的用戶端",
+ "dns_disallowed_clients_label": "禁止的用戶端,每行一個",
+ "dns_disallowed_clients_title": "未被允許的用戶端",
+ "dns_disallowed_domains": "未被允許的網域",
+ "dns_disallowed_domains_label": "禁止的網域,每行一個",
+ "dns_disallowed_domains_title": "未被允許的網域",
+ "dns_edns_custom_placeholder": "輸入 IP 位址",
+ "dns_fallback_dns_label": "備用 DNS 伺服器,每行一個",
+ "dns_fallback_dns_placeholder": "IP 位址、sdns://、quic://、https://、h3:// 或 tls://",
+ "dns_fallback_dns_title": "應變 DNS 伺服器",
+ "dns_fallback_servers": "應變 DNS 伺服器",
+ "dns_optimistic_caching": "樂觀快取",
+ "dns_over_https": "DNS-over-HTTPS",
+ "dns_over_quic": "DNS-over-QUIC",
+ "dns_over_tls": "DNS-over-TLS",
+ "dns_override_max_ttl": "覆寫最大的存活時間(TTL)",
+ "dns_override_max_ttl_title": "覆寫最大的存活時間(TTL)",
+ "dns_override_min_ttl": "覆寫最小的存活時間(TTL)",
+ "dns_override_min_ttl_title": "覆寫最小的存活時間(TTL)",
+ "dns_privacy": "DNS 隱私",
+ "dns_private_reverse_servers_label": "私有反向 DNS 伺服器,每行一個",
+ "dns_private_reverse_servers_placeholder": "IP 位址、sdns://、quic://、https://、h3:// 或 tls://",
+ "dns_private_reverse_servers_title": "私人反向的 DNS 伺服器",
+ "dns_queries": "DNS 查詢",
+ "dns_queries_tooltip": "過去 24 小時內處理的 DNS 查詢數量",
+ "dns_query": "DNS 查詢",
+ "dns_rate_limit": "速率限制",
+ "dns_rate_limit_allowlist": "速率限制允許清單",
+ "dns_rate_limit_allowlist_placeholder": "IP 位址",
+ "dns_rate_limit_allowlist_title": "速率限制允許清單",
+ "dns_rate_limit_placeholder": "輸入速率限制",
+ "dns_rate_limit_title": "速率限制",
+ "dns_rewrites": "DNS 改寫",
+ "dns_server_addresses": "伺服器地址",
+ "dns_server_addresses_label": "伺服器位址,每行一個",
+ "dns_server_addresses_placeholder": "IP 位址、sdns://、quic://、https://、h3:// 或 tls://",
+ "dns_server_addresses_title": "伺服器地址",
+ "dns_server_config_title": "DNS 伺服器配置",
+ "dns_server_configuration_saved_toast": "DNS 伺服器設定已儲存",
+ "dns_settings": "DNS",
+ "dns_subnet_placeholder": "輸入前綴長度",
+ "dns_test_upstreams": "測試上行資料流",
+ "dns_upstream_servers_title": "上游的 DNS 伺服器",
+ "dns_upstream_timeout": "上游超時",
+ "dns_upstream_timeout_title": "上游超時",
+ "dns_upstream_validation_invalid": "格式無效",
+ "domain": "網域",
+ "domain_or_client": "網域或用戶端",
+ "download_mobileconfig": "下載配置檔案",
+ "dynamic_dhcp_leases_not_found": "未找到動態 DHCP 租約",
+ "edit_table_action": "編輯",
+ "enable": "啟用",
+ "enabled_dhcp": "動態主機設定協定(DHCP)伺服器被啟用",
+ "encrypted_dns_addresses": "加密 DNS 伺服器位址",
+ "encryption_certificate_has_issues": "憑證存在問題",
+ "encryption_certificate_path": "憑證路徑",
+ "encryption_certificates": "憑證",
+ "encryption_certificates_desc": "要啟用加密,您必須為您的網域提供有效的 SSL 憑證鏈。您可以從 letsencrypt.org 獲取免費 SSL 憑證,或從受信任的憑證授權機構購買。",
+ "encryption_certificates_input": "於此複製/貼上您的隱私增強郵件編碼之(PEM-encoded)憑證。",
+ "encryption_certificates_source_content": "貼上該憑證內容",
+ "encryption_certificates_source_path": "設定一個憑證檔案路徑",
+ "encryption_chain_invalid": "憑證鏈結為無效的",
+ "encryption_chain_valid": "憑證鏈結為有效的",
+ "encryption_config_saved": "加密配置被儲存",
+ "encryption_confirm_clear": "重設加密設定",
+ "encryption_confirm_clear_desc": "所有加密設定將重設為預設值。",
+ "encryption_disable_plain_dns": "停用純 DNS?",
+ "encryption_disable_plain_dns_desc": "不支援加密 DNS 的裝置可能無法正常運作",
+ "encryption_doq": "DNS-over-QUIC 連接埠",
+ "encryption_doq_tooltip": "此連接埠用於執行 DNS-over-QUIC 伺服器",
+ "encryption_dot": "DNS-over-TLS 連接埠",
+ "encryption_dot_tooltip": "此連接埠用於執行 DNS-over-TLS 伺服器",
+ "encryption_encrypted_dns": "加密 DNS",
+ "encryption_encrypted_dns_desc": "AdGuard Home 管理介面使用 HTTPS。DNS 伺服器支援 DNS-over-HTTPS、DNS-over-TLS 和 DNS-over-QUIC。",
+ "encryption_expire": "過期時間:%value%",
+ "encryption_force_redirect": "自動從 HTTP 重新導向到 HTTPS",
+ "encryption_hostnames": "主機名稱:%value%",
+ "encryption_https": "HTTPS 連接埠",
+ "encryption_https_tooltip": "此連接埠提供 AdGuard Home 管理介面的 HTTPS 存取。同時在 '/dns-query' 路徑上提供 DNS-over-HTTPS。",
+ "encryption_issuer": "發行者:%value%",
+ "encryption_key": "私密金鑰",
+ "encryption_key_input": "於此複製/貼上您的隱私增強郵件編碼之(PEM-encoded)私密金鑰供您的憑證。",
+ "encryption_key_invalid": "私鑰無效",
+ "encryption_key_source_content": "貼上該私密金鑰內容",
+ "encryption_key_source_path": "設定私鑰檔案",
+ "encryption_key_type": "加密演算法:%value%",
+ "encryption_key_valid": "私鑰有效",
+ "encryption_plain_dns": "一般的 DNS",
+ "encryption_plain_dns_error": "若要停用一般 DNS,請啟用至少一個加密 DNS 通訊協定",
+ "encryption_private_key_path": "私密金鑰路徑",
+ "encryption_server": "伺服器名稱",
+ "encryption_server_enter": "輸入您的域名",
+ "encryption_server_tooltip_1": "如果已設定,AdGuard Home 將偵測 ClientID、回應 DDR 查詢並執行額外的連線驗證。如果未設定,這些功能將被停用。",
+ "encryption_server_tooltip_2": "必須與憑證中的 DNS 名稱匹配",
+ "encryption_subject": "主旨:%value%",
+ "encryption_title": "加密",
+ "enter_cache_size": "輸入快取大小(位元組)",
+ "enter_ip_address_placeholder": "輸入 IP 位址",
+ "error": "錯誤",
+ "fallback_dns": "應變 DNS 伺服器",
+ "filter_added_successfully": "已新增封鎖清單:%value% ",
+ "filter_added_successfully_allowlist": "已新增允許清單:%value% ",
+ "filter_added_successfully_more": "已新增封鎖清單:%value% 及其他 %more% 個",
+ "filter_category_general": "General",
+ "filter_category_general_desc": "封鎖大多數朝向裝置的追蹤和廣告之清單",
+ "filter_category_other": "其它的",
+ "filter_category_other_desc": "其它的封鎖清單",
+ "filter_category_regional": "區域性的",
+ "filter_category_regional_desc": "專注於區域性的廣告和追蹤伺服器之清單",
+ "filter_category_security": "安全性",
+ "filter_category_security_desc": "專門地旨在封鎖惡意、網路釣魚和詐騙的網域之清單",
+ "filter_removed_successfully": "已移除封鎖清單:%value% ",
+ "filter_removed_successfully_allowlist": "已移除允許清單:%value% ",
+ "filters": "過濾器",
+ "forgot_password": "忘了密碼嗎?",
+ "forgot_password_list_item_1": "使用 htpasswd 工具或任何線上 BCrypt 產生器建立新密碼。",
+ "forgot_password_list_item_2": "停止 AdGuard Home。",
+ "forgot_password_list_item_3": "開啟 AdGuardHome.yaml,找到 password 欄位,將其替換為新值。",
+ "forgot_password_list_item_4": "啟動 AdGuard Home。",
+ "forgot_password_list_item_5": "使用新密碼登入 Web 介面。",
+ "forgot_password_list_title": "要變更密碼:",
+ "forgot_password_page_desc": "AdGuard Home 將密碼儲存為 BCrypt 加密雜湊值",
+ "form_enter_blocked_response_ttl": "請輸入已封鎖回應的存活時間(秒)",
+ "form_enter_hostname": "輸入主機名稱",
+ "form_enter_ip": "輸入 IP 位址",
+ "form_enter_mac": "輸入 MAC 位址",
+ "form_error_answer_format": "結果格式無效",
+ "form_error_client_id_format": "用戶端 ID 必須只包含數字、小寫字母和連字號",
+ "form_error_domain_format": "無效的網域格式",
+ "form_error_equal": "必須為不相等的",
+ "form_error_format": "格式無效",
+ "form_error_gateway_ip": "租約不能有閘道的 IP 位址",
+ "form_error_hostname_format": "請使用數字、小寫英文字母或連字號。",
+ "form_error_ip4_format": "無效的 IPv4 位址",
+ "form_error_ip6_format": "無效的 IPv6 位址",
+ "form_error_ip_already_added": "此 IP 位址已新增",
+ "form_error_ip_format": "無效的 IP 位址",
+ "form_error_mac_already_added": "此 MAC 位址已新增",
+ "form_error_mac_format": "無效的媒體存取控制(MAC)位址",
+ "form_error_password": "不相符的密碼",
+ "form_error_port": "輸入有效的連接埠號碼",
+ "form_error_port_range": "輸入在 80-65535 之範圍內的連接埠號碼",
+ "form_error_port_unsafe": "不安全的連接埠",
+ "form_error_range": "數值必須介於 %min% 和 %max%",
+ "form_error_server_name": "無效的伺服器名稱",
+ "form_error_url_format": "輸入有效的網址或檔案路徑",
+ "found_in_known_domains": "在已知網域中找到",
+ "friday": "星期五",
+ "gateway_or_subnet_invalid": "無效的子網路遮罩",
+ "general_statistics": "一般的統計資料",
+ "home_dns_addresses": "AdGuard Home DNS 位址",
+ "home_dns_addresses_desc": "AdGuard Home DNS 伺服器監聽以下介面/連接埠:",
+ "ignore_domains_desc_log": "符合這些規則的請求不會顯示在「查詢紀錄」中",
+ "ignore_domains_desc_stats": "符合這些規則的請求不會納入統計",
+ "ignore_domains_title": "被忽略的網域",
+ "inactivity_schedule": "非活躍時段",
+ "inactivity_schedule_add": "新增排程",
+ "inactivity_schedule_all_day": "全天",
+ "inactivity_schedule_delete": "刪除時段",
+ "inactivity_schedule_delete_desc": "服務封鎖將在 %value% 全天生效",
+ "inactivity_schedule_desc": "設定服務封鎖的非活躍時段。",
+ "inactivity_schedule_edit": "編輯排程",
+ "inactivity_schedule_no_schedules": "無時段設定",
+ "inactivity_schedule_replace_desc": "此時段將取代同一天現有的所有時段。每天只能有一個非活躍時段。",
+ "inactivity_schedule_time_from": "開始時間",
+ "inactivity_schedule_time_to": "結束時間",
+ "inactivity_schedule_timezone": "時區",
+ "install_auth_confirm": "確認密碼",
+ "install_auth_password": "密碼",
+ "install_auth_password_enter": "輸入密碼",
+ "install_auth_username": "使用者名稱",
+ "install_auth_username_enter": "輸入使用者名稱",
+ "install_saved": "被成功地儲存",
+ "install_settings_all_interfaces": "所有的介面",
+ "install_settings_port": "連接埠",
+ "install_static_configure": "AdGuard Home 偵測到使用了動態 IP 位址 %ip%。是否將其設為您的靜態位址?",
+ "install_static_error": "AdGuard Home 無法自動地配置它供此網路介面。請尋找有關如何手動地完成這個的用法說明。",
+ "install_static_ok": "好消息!該靜態 IP 位址已被配置",
+ "install_step": "步驟",
+ "install_submit_title": "恭喜!",
+ "instructions": "說明",
+ "invalid": "無效",
+ "ip_address": "IP 位址",
+ "ip_addresses_placeholder": "輸入 IP 位址,每行一個",
+ "known_tracker": "已知的追蹤器",
+ "language": "語言",
+ "last_days": "最近 %count% 天 | 最近 %count% 天",
+ "last_hour": "最近 1 小時",
+ "last_hours": "最近 %count% 小時 | 最近 %count% 小時",
+ "last_updated_label": "上次更新",
+ "light_theme": "淺色主題",
+ "list_updated": "%count% 個清單已更新 | %count% 個清單已更新",
+ "login": "登入",
+ "logout": "登出",
+ "make_static": "靜態化",
+ "make_static_desc": "此操作將動態位址轉換為靜態位址",
+ "milliseconds_abbreviation": "ms",
+ "monday": "星期一",
+ "name": "名稱",
+ "name_label": "名稱",
+ "name_table_header": "名稱",
+ "name_tooltip": "名稱",
+ "network_interface": "網路介面",
+ "next": "下一步",
+ "no": "否",
+ "no_stats_yet": "暫無統計資料:請繼續使用 AdGuard Home 以查看",
+ "none_text": "無",
+ "nothing_found": "沒找到什麼",
+ "notify_undo": "復原",
+ "notify_updated": "已更新於",
+ "open_dashboard": "開啟儀表板",
+ "parental_control": "家長控制",
+ "parental_group_ai": "AI",
+ "parental_group_cdn": "內容傳遞網路(CDN)",
+ "parental_group_dating": "交友",
+ "parental_group_development": "軟體開發",
+ "parental_group_gambling": "賭博、投注",
+ "parental_group_gaming": "遊戲",
+ "parental_group_hosting": "Web 主機服務",
+ "parental_group_messaging": "即時通訊",
+ "parental_group_messenger": "即時通訊",
+ "parental_group_privacy": "隱私工具",
+ "parental_group_shopping": "購物",
+ "parental_group_social": "社群網路",
+ "parental_group_social_network": "社群網路",
+ "parental_group_software": "軟體開發",
+ "parental_group_streaming": "串流",
+ "password_label": "密碼",
+ "password_placeholder": "輸入密碼",
+ "password_requirements": "密碼要求",
+ "password_requirements_characters": "至少 8 個字元",
+ "password_requirements_lowercase": "至少 1 個小寫字母",
+ "password_requirements_match": "密碼匹配",
+ "password_requirements_special": "英文字母、數字和特殊字元",
+ "password_requirements_uppercase": "至少 1 個大寫字母",
+ "pause_for_hour": "暫停 %count% 小時",
+ "pause_for_minutes": "暫停 %count% 分鐘 | 暫停 %count% 分鐘",
+ "pause_for_seconds": "暫停 %count% 秒 | 暫停 %count% 秒",
+ "pause_until_tomorrow": "暫停至明天 %time%",
+ "period_notify": "要變更顯示的時間範圍,請在 一般設定 中編輯「統計資料保留時間」。",
+ "plain_dns": "一般的 DNS",
+ "plain_dns_addresses": "普通 DNS 伺服器位址",
+ "port_53_faq_link": "連接埠 53 常被「DNSStubListener」或「systemd-resolved」服務佔用。請閱讀此說明 了解如何解決。",
+ "port_in_use": "此連接埠已被佔用",
+ "privacy_policy": "隱私政策",
+ "processing_time_ms": "%value% ms",
+ "protection": "防護",
+ "protocol": "協定",
+ "queries": "查詢",
+ "queries_total": "共 %count% 條查詢 | 共 %count% 條查詢",
+ "query_details": "查詢詳細資料",
+ "query_log": "查詢紀錄",
+ "query_log_all_reasons": "全部原因",
+ "query_log_all_statuses": "全部狀態",
+ "query_log_allowed": "被允許的",
+ "query_log_blocked": "已封鎖的",
+ "query_log_blocked_by_filter": "被過濾器封鎖",
+ "query_log_blocked_by_parental_control": "被家長控制封鎖",
+ "query_log_blocked_services": "已封鎖的服務",
+ "query_log_blocked_threats": "已封鎖的威脅",
+ "query_log_custom_filtering_rules": "自訂的過濾規則",
+ "query_log_detail_address": "位址:%value% ",
+ "query_log_detail_category": "類別:%value% ",
+ "query_log_detail_country": "國家/地區:%value% ",
+ "query_log_detail_date": "日期:%value% ",
+ "query_log_detail_dns_server": "DNS 伺服器:%value% ",
+ "query_log_detail_domain": "網域:%value% ",
+ "query_log_detail_ecs": "ECS:%value% ",
+ "query_log_detail_elapsed": "耗時:%value% ",
+ "query_log_detail_name": "名稱:%value% ",
+ "query_log_detail_network": "網路:%value% ",
+ "query_log_detail_original_response": "原始回應:",
+ "query_log_detail_protocol": "協定:%value% ",
+ "query_log_detail_reason": "原因:",
+ "query_log_detail_response": "回應:",
+ "query_log_detail_response_code": "回應碼:%value% ",
+ "query_log_detail_rules": "規則:",
+ "query_log_detail_served_from_cache": "來自快取:%value% ",
+ "query_log_detail_service_name": "服務名稱:%value% ",
+ "query_log_detail_source": "來源:%value% ",
+ "query_log_detail_status": "狀態:%value% ",
+ "query_log_detail_time": "時間:%value% ",
+ "query_log_detail_type": "類型:%value% ",
+ "query_log_nothing_available": "沒找到什麼",
+ "query_log_processed": "已處理的",
+ "query_log_retention": "紀錄輪替",
+ "query_log_rewritten": "已改寫的",
+ "query_log_safe_search": "安全搜尋",
+ "query_log_strict_search": "使用雙引號於嚴謹的搜尋",
+ "rate_limit_subnet_len_ipv4_error": "IPv4 子網路前綴長度應在 0 至 32 之間",
+ "rate_limit_subnet_len_ipv6_error": "IPv6 子網路前綴長度應在 0 至 128 之間",
+ "reason_table_header": "原因",
+ "refresh_btn": "重新整理",
+ "refresh_statics": "重新整理統計資料",
+ "release_notes": "更新紀錄",
+ "remove": "移除",
+ "remove_tag": "移除 %value%",
+ "report_an_issue": "報告程式錯誤",
+ "request_table_header": "請求",
+ "requests_table_header": "請求",
+ "reset": "重置",
+ "reset_settings": "重置設定",
+ "reset_settings_confirm": "重置",
+ "response_details": "回應細節",
+ "response_time": "回應時間",
+ "result": "結果",
+ "resume_protection_timer": "防護將在 %time% 恢復",
+ "rewrite_add": "新增 DNS 改寫",
+ "rewrite_domain_input_placeholder": "輸入網域或萬用字元",
+ "rewrite_edit": "編輯 DNS 重寫",
+ "rewrite_hosts_applied": "被該主機檔案規則改寫",
+ "rewrite_settings_updated": "DNS 重寫設定更新成功",
+ "rewrite_updated": "DNS 重寫已成功更新",
+ "rewrites_answer_input_placeholder": "輸入 IP 位址、CNAME 記錄、A 記錄或 AAAA 記錄",
+ "rewrites_empty": "暫無重寫規則:新增第一條 ",
+ "rewrites_remove_desc": "%value% 的 DNS 重寫將被移除",
+ "rewrites_remove_title": "移除 DNS 重寫?",
+ "rewrites_tooltip_examples_item1": "僅重寫此網域的 DNS 回應",
+ "rewrites_tooltip_examples_item2": "重寫 example.org 所有子網域的 DNS 回應",
+ "rewrites_tooltip_instructions_item1": "要將網域解析為 IP 位址,請輸入 A 記錄的 IPv4 位址或 AAAA 記錄的 IPv6 位址。",
+ "rewrites_tooltip_instructions_item2": "要重新導向網域,請輸入目標網域的 CNAME 記錄。",
+ "rewrites_tooltip_instructions_item3": "僅保留 A 記錄,請輸入",
+ "rewrites_tooltip_instructions_item4": "僅保留 AAAA 記錄,請輸入",
+ "rewritten": "已改寫的",
+ "rows_per_page": "每頁行數 %value%",
+ "rules_label": "規則",
+ "safe_browsing": "安全瀏覽",
+ "safe_search": "安全搜尋",
+ "safe_search_used": "已使用安全搜尋",
+ "safe_search_used_tooltip": "對搜尋引擎發起的、已強制啟用安全搜尋的 DNS 查詢數量。",
+ "saturday": "星期六",
+ "save": "儲存",
+ "save_btn": "儲存",
+ "search_placeholder": "搜尋",
+ "selected": "已選擇 %value%",
+ "server_config_blocking_mode": "封鎖模式",
+ "server_config_blocking_mode_custom_ip_desc": "自訂 IP 位址 :回應手動設定的 IP 位址",
+ "server_config_blocking_mode_default_desc": "預設 :如果被 Adblock 風格規則封鎖,對 A 請求回應 `0.0.0.0`,對 AAAA 請求回應 `::`。如果被 /etc/hosts 風格規則封鎖,則回應規則中指定的 IP 位址。",
+ "server_config_blocking_mode_ipv4": "封鎖 IPv4",
+ "server_config_blocking_mode_ipv4_faq": "被封鎖的 A 請求返回的 IP 位址",
+ "server_config_blocking_mode_ipv4_placeholder": "輸入 IPv4 位址",
+ "server_config_blocking_mode_ipv6": "封鎖 IPv6",
+ "server_config_blocking_mode_ipv6_faq": "被封鎖的 AAAA 請求返回的 IP 位址",
+ "server_config_blocking_mode_ipv6_placeholder": "輸入 IPv6 位址",
+ "server_config_blocking_mode_null_ip_desc": "空 IP 位址 :對 A 請求回應 0.0.0.0,對 AAAA 請求回應 ::",
+ "server_config_blocking_mode_nxdomain_desc": "NXDOMAIN :回應 NXDOMAIN 碼",
+ "server_config_blocking_mode_refused_desc": "REFUSED :回應 REFUSED 碼",
+ "server_config_blocking_mode_ttl": "封鎖回應 TTL(秒)",
+ "server_config_blocking_mode_ttl_faq": "指定用戶端應快取已過濾回應的時長",
+ "server_config_custom_ip": "自訂的 IP 位址",
+ "server_config_default": "預設",
+ "server_config_disable_ipv6": "停用 IPv6 位址解析",
+ "server_config_disable_ipv6_desc": "丟棄所有針對 IPv6 位址的 DNS 查詢(AAAA 類型),並從 HTTPS 回應中移除 IPv6 提示。",
+ "server_config_dnssec_enable": "啟用網域名稱系統安全性擴充功能(DNSSEC)",
+ "server_config_dnssec_enable_desc": "在對外 DNS 查詢中設定 DNSSEC 標誌並檢查結果。需要支援 DNSSEC 的解析器。",
+ "server_config_edns_cs_desc": "向上游請求新增 EDNS 用戶端子網路(ECS)選項,並在查詢紀錄中記錄用戶端發送的值。",
+ "server_config_edns_enable": "啟用對於 DNS 的擴充機制(EDNS)用戶端子網路",
+ "server_config_edns_use_custom_ip": "為 EDNS 使用自訂 IP 位址",
+ "server_config_edns_use_custom_ip_desc": "將用戶端 IP 位址替換為下方的自訂 IP 位址",
+ "server_config_null_ip": "空 IP 位址",
+ "server_config_nxdomain": "不存在的網域(NXDOMAIN)",
+ "server_config_rate_limit": "速率限制(RPS)",
+ "server_config_rate_limit_faq": "每個用戶端每秒允許的請求數。值為 0 表示無限制。",
+ "server_config_rate_limit_placeholder": "輸入速率限制",
+ "server_config_rate_limit_whitelist": "速率限制允許清單",
+ "server_config_rate_limit_whitelist_faq": "從速率限制被排除的 IP 位址",
+ "server_config_refused": "已拒絕(REFUSED)",
+ "server_config_subnet_len_ipv4": "用於 IPv4 位址的子網路前綴長度",
+ "server_config_subnet_len_ipv4_faq": "用於速率限制的 IPv4 位址的子網路前綴長度。預設值為 24",
+ "server_config_subnet_len_ipv6": "用於 IPv6 位址的子網路前綴長度",
+ "server_config_subnet_len_ipv6_faq": "用於速率限制的 IPv6 位址的子網路前綴長度。預設值為 56",
+ "server_config_subnet_len_placeholder": "輸入前綴長度",
+ "set_static_ip": "設定一組靜態 IP 位址",
+ "settings": "設定",
+ "settings_anonymize_client_ip": "對用戶端 IP 位址進行匿名化處理",
+ "settings_anonymize_client_ip_desc": "將 IP 位址的一部分替換為零",
+ "settings_browsing_security": "瀏覽安全",
+ "settings_browsing_security_desc": "封鎖惡意和釣魚網域資料庫中的網域存取。",
+ "settings_confirm_clear_query_log": "清除查詢紀錄?",
+ "settings_confirm_clear_query_log_desc": "這將從「查詢紀錄 」中刪除所有請求。統計資料仍將保留。",
+ "settings_confirm_clear_statistics": "清除統計資料嗎?",
+ "settings_confirm_clear_statistics_desc": "這將清除所有統計資料。查詢紀錄 仍將保留。",
+ "settings_confirm_decrease_log_rotation_interval": "縮短紀錄輪替間隔?",
+ "settings_confirm_decrease_log_rotation_interval_desc": "這將刪除所有早於 %value% 的紀錄",
+ "settings_confirm_decrease_stats_rotation_interval": "縮短統計資料保留間隔?",
+ "settings_confirm_decrease_stats_rotation_interval_desc": "這將刪除所有早於 %value% 的統計資料",
+ "settings_custom": "自訂的",
+ "settings_days": "%count% 天 | %count% 天",
+ "settings_domain_names": "網域名稱,每行一個",
+ "settings_filter_requests": "過濾請求",
+ "settings_filter_requests_desc": "您可以使用封鎖清單 、允許清單 和使用者規則 來設定過濾規則。",
+ "settings_filtering_and_security": "過濾與安全",
+ "settings_general_short": "General",
+ "settings_global": "全域的",
+ "settings_hours": "%count% 個小時 | %count% 個小時",
+ "settings_log_dns_requests": "記錄 DNS 請求",
+ "settings_log_rotation_hours": "紀錄輪替時間(小時)",
+ "settings_notify_changes_saved": "更改已儲存",
+ "settings_notify_query_log_cleared": "查詢記錄已清除",
+ "settings_notify_statistics_cleared": "統計資料已清除",
+ "settings_parental_control": "家長控制",
+ "settings_parental_control_desc": "封鎖含有兒童不宜內容的網站。",
+ "settings_rotation_placeholder": "請輸入數字",
+ "settings_rule_syntax": "規則語法",
+ "settings_safe_search": "安全搜尋",
+ "settings_safe_search_desc": "在搜尋結果中屏蔽不當內容。",
+ "settings_statistics": "統計資料",
+ "settings_statistics_clear": "清除統計資料",
+ "settings_statistics_desc": "在儀表板上顯示請求和用戶端統計資訊",
+ "settings_statistics_retention": "統計資料保留",
+ "settings_statistics_retention_hours": "統計資料保留時間(小時)",
+ "settings_tooltip_domain_names": "網域名稱、萬用字元或過濾規則",
+ "settings_tooltip_examples": "範例:",
+ "settings_yes_clear": "是的,清除",
+ "settings_yes_decrease": "確認縮短",
+ "setup_complete": "設定已完成,AdGuard Home 已準備就緒。",
+ "setup_devices_android_list_1": "開啟裝置的「設定」應用程式,選擇「Wi-Fi」。",
+ "setup_devices_android_list_2": "長按目前網路,然後選擇「修改網路」。無法為行動網路連線設定自訂 DNS。",
+ "setup_devices_android_list_3": "在某些裝置上,您可能需要勾選「進階」以查看更多設定。要自訂 Android DNS 設定,您需要將 IP 設定從「DHCP」改為「靜態」。",
+ "setup_devices_android_list_4": "將「DNS 1」和「DNS 2」的值改為 AdGuard Home 伺服器的位址。",
+ "setup_devices_android_title": "Android",
+ "setup_devices_dns_android_list_1": "Android 9 原生支援 DNS-over-TLS 。要設定它,請前往「設定」→「網路和網際網路」→「進階」→「私人 DNS」,然後輸入您的網域名稱。",
+ "setup_devices_dns_android_list_2": "AdGuard Android 版 支援 DNS-over-HTTPS 、DNS-over-TLS 和 DNS-over-QUIC 設定。",
+ "setup_devices_dns_android_list_3": "Intra 為 Android 增加 DNS-over-HTTPS 支援。",
+ "setup_devices_dns_ios_list_1": "AdGuard iOS 版 支援 DNS-over-HTTPS 、DNS-over-TLS 和 DNS-over-QUIC 設定。",
+ "setup_devices_dns_ios_list_2": "DNSCloak 支援 DNS-over-HTTPS ,但要設定它使用您自己的伺服器,需要為其生成一個 DNS Stamp 。",
+ "setup_devices_dns_list_1": "對於 DNS-over-TLS ,使用 ",
+ "setup_devices_dns_list_2": "對於 DNS-over-HTTPS ,使用 ",
+ "setup_devices_dns_list_3": "對於 DNS-over-QUIC ,使用 ",
+ "setup_devices_dns_list_devices": "以下是可以使用的軟體清單。",
+ "setup_devices_dns_macos_desc": "在 iOS 14 或 macOS Big Sur 裝置上,您可以下載一個特殊的 mobileconfig 檔案,在 DNS 設定中新增 DNS-over-HTTPS 或 DNS-over-TLS 伺服器。",
+ "setup_devices_dns_other_list_1": "AdGuard Home 本身可以在任何平台上作為安全的 DNS 用戶端使用。",
+ "setup_devices_dns_other_list_2": "dnsproxy 支援所有已知的安全 DNS 協定。",
+ "setup_devices_dns_other_list_3": "dnscrypt-proxy 支援 DNS-over-HTTPS 。",
+ "setup_devices_dns_other_list_4": "Mozilla Firefox 支援 DNS-over-HTTPS 。",
+ "setup_devices_dns_other_list_5": "更多實作,請參閱 DNSCrypt 和 DNS 隱私 。",
+ "setup_devices_ios_list_1": "開啟裝置的「設定」應用程式,選擇「Wi-Fi」。無法為行動網路連線設定自訂 DNS。",
+ "setup_devices_ios_list_2": "點擊目前活動網路的名稱。",
+ "setup_devices_ios_list_3": "進入「DNS」→「設定 DNS」→「手動」,然後新增 AdGuard Home 伺服器的位址。",
+ "setup_devices_ios_list_4": "點擊「儲存」。",
+ "setup_devices_ios_title": "iOS",
+ "setup_devices_macos_list_1": "點擊 Apple 選單 →「系統偏好設定」→「網路」。",
+ "setup_devices_macos_list_2": "選擇所需的連線,然後點擊「進階」。",
+ "setup_devices_macos_list_3": "選擇「DNS」標籤,然後輸入 AdGuard Home 伺服器的位址。",
+ "setup_devices_macos_title": "macOS",
+ "setup_devices_router_desc_1": "在家庭網路路由器上設定 AdGuard Home,保護所有連線到該路由器的裝置。無需手動設定每台裝置。",
+ "setup_devices_router_desc_2": "某些路由器不允許設定自訂 DNS 伺服器。在這種情況下,您可以嘗試將 AdGuard Home 設定為 DHCP 伺服器 。如果仍然無效,請查閱您的路由器手冊,了解如何為特定路由器自訂 DNS 伺服器。",
+ "setup_devices_router_list_1": "通常可以透過瀏覽器存取類似 http://192.168.0.1 或 http://192.168.1.1 的 URL 進入。可能會要求輸入密碼。通常可以透過按下路由器上的按鈕重設密碼,但請注意,這可能會清除整個路由器的設定。如果路由器需要透過應用程式進行設定,請在手機或電腦上安裝該應用程式,然後使用它存取路由器的設定。",
+ "setup_devices_router_list_1_title": "開啟路由器的設定。",
+ "setup_devices_router_list_2": "要尋找設定,請尋找 DNS 欄位,該欄位通常允許輸入兩組或三組數字,每組分為四個由一到三位數字組成的部分。",
+ "setup_devices_router_list_2_title": "在 DHCP/DNS 設定中,輸入 AdGuard Home 伺服器的位址。",
+ "setup_devices_router_title": "路由器",
+ "setup_devices_windows_list_1": "從「開始功能表」或「Windows 搜尋」開啟「控制面板」。",
+ "setup_devices_windows_list_2": "點擊「網路和網際網路」→「網路和共用中心」→「變更介面卡設定」。",
+ "setup_devices_windows_list_3": "右鍵點擊活動連線,然後選擇「內容」。",
+ "setup_devices_windows_list_4": "在清單中,對於 IPv4 選擇「網際網路通訊協定第 4 版 (TCP/IPv4)」,對於 IPv6 選擇「網際網路通訊協定第 6 版 (TCP/IPv6)」。",
+ "setup_devices_windows_list_5": "點擊「內容」→「使用下列 DNS 伺服器位址」。",
+ "setup_devices_windows_list_6": "輸入 AdGuard Home 伺服器的位址。",
+ "setup_devices_windows_title": "Windows",
+ "setup_dns_desc": "設定您的路由器或其他裝置使用以下 DNS 伺服器位址:",
+ "setup_dns_notice_new": "要使用 DNS-over-HTTPS 或 DNS-over-TLS ,您需要在 AdGuard Home 設定中設定加密 。",
+ "setup_dns_privacy_ioc_mac": "iOS 和 macOS 配置",
+ "setup_dns_privacy_other_title": "其它的執行",
+ "setup_dns_quote_desc": "要正常運作,AdGuard Home DNS 伺服器需要在本機網路上擁有靜態 IP 位址。否則,路由器可能會將其分配為不同的 IP 位址,從而導致 DNS 防護中斷。",
+ "setup_dns_quote_title": "在本機網路上為 AdGuard Home DNS 伺服器分配靜態 IP 位址",
+ "setup_dns_title": "設定 AdGuard Home DNS 伺服器位址",
+ "setup_dns_title_banner": "DNS 伺服器設定",
+ "setup_guide": "設定指南",
+ "setup_guide_auth_privacy": "我接受 AdGuard 網站的隱私政策 和條款與條件 ",
+ "setup_guide_auth_subtitle": "這是保護 AdGuard Home 管理面板免遭未經授權存取所必需的。",
+ "setup_guide_auth_title": "設定密碼保護",
+ "setup_guide_desc": "設定您的裝置使用 AdGuard Home",
+ "setup_guide_device_type": "選擇裝置類型",
+ "setup_guide_greeting_button": "執行精靈",
+ "setup_guide_greeting_desc": "此精靈將幫助您設定 AdGuard Home:",
+ "setup_guide_greeting_list_1": "建立用於存取管理面板的憑證",
+ "setup_guide_greeting_list_2": "設定 AdGuard Home 的 Web UI 位址",
+ "setup_guide_greeting_list_3": "設定 AdGuard Home DNS 伺服器位址",
+ "setup_guide_greeting_list_4": "取得針對您裝置的設定說明",
+ "setup_guide_greeting_list_5": "取得 AdGuard Home 儀表板的存取權限",
+ "setup_guide_greeting_title": "設定 AdGuard Home",
+ "setup_guide_title": "設定您的裝置使用 AdGuard Home",
+ "setup_ui_desc": "使用這些位址存取 AdGuard Home 的 Web 使用者介面。如有需要,可變更連接埠。",
+ "setup_ui_title": "設定 AdGuard Home 的 Web UI 位址",
+ "setup_ui_title_banner": "Web UI 設定",
+ "show_blocked_responses": "已封鎖的",
+ "show_more_count": "另外 %count% 個",
+ "show_processed_responses": "已處理的",
+ "sort_asc": "按 A-Z 排序",
+ "sort_desc": "按 Z-A 排序",
+ "source_label": "來源",
+ "source_tooltip": "來源",
+ "static_dhcp_leases_not_found": "未找到靜態 DHCP 租約",
+ "stats_adult": "已封鎖的成人網站",
+ "stats_query_domain": "熱門已查詢的網域",
+ "status_table_header": "狀態",
+ "sunday": "星期日",
+ "system_host_files": "系統主機檔案",
+ "system_theme": "系統主題",
+ "table_client": "用戶端",
+ "tags_title": "標記",
+ "test_upstreams": "測試上行資料流",
+ "threats_blocked": "已封鎖的威脅",
+ "threats_blocked_tooltip": "被 AdGuard 瀏覽安全模組封鎖的 DNS 查詢數量",
+ "thursday": "星期四",
+ "time_table_header": "時間",
+ "top_blocked_domains": "熱門已封鎖的網域",
+ "top_clients": "熱門用戶端",
+ "top_upstreams": "熱門上游",
+ "total": "總計",
+ "try_again": "再次嘗試",
+ "tuesday": "星期二",
+ "type_value": "類型:%value%",
+ "unavailable_dhcp": "DHCP 為不可用的",
+ "unavailable_dhcp_desc": "AdGuard Home 無法於您的作業系統上執行 DHCP 伺服器",
+ "unblock": "解除封鎖",
+ "unblock_client": "解除封鎖用戶端",
+ "unknown_filter": "未知過濾器 %filterId%",
+ "update_button": "更新",
+ "update_filters_custom_hours": "自訂間隔(小時)",
+ "update_filters_disable": "禁用",
+ "update_filters_interval_custom": "自訂的",
+ "update_filters_interval_daily": "每天",
+ "update_filters_interval_hourly": "每小時",
+ "update_filters_interval_weekly": "每週",
+ "update_filters_title": "自動地更新過濾器",
+ "updated_upstream_dns_toast": "上游伺服器已儲存",
+ "updates_checked": "AdGuard Home 的新版本為可用的",
+ "updates_version_equal": "AdGuard Home 為最新的",
+ "upstream": "上游伺服器",
+ "upstream_bootstrap_dns_faq": "用於解析 DoH/DoT 上游解析器 IP 位址的 DNS 伺服器位址。不允許註解。",
+ "upstream_bootstrap_dns_title": "Bootstrap DNS 伺服器,每行一個",
+ "upstream_dns": "上游的 DNS 伺服器",
+ "upstream_dns_addresses": "伺服器位址,每行一個",
+ "upstream_dns_addresses_faq": "了解更多 關於上游 DNS 伺服器設定的資訊。以下為已知 DNS 提供商列表 供您選擇。",
+ "upstream_dns_configured_in_file": "設定於 %path%",
+ "upstream_dns_fastest_addr": "最快的 IP 位址",
+ "upstream_dns_parallel_requests": "並行的請求",
+ "upstream_dns_placeholder": "IP 位址、sdns://、quic://、https://、h3:// 或 tls://",
+ "upstream_dns_servers_title": "上游的 DNS 伺服器",
+ "upstream_enable_reverse_lookup_desc": "透過向適當的解析器發送 PTR 查詢,將用戶端 IP 位址反向解析為主機名稱:本機用戶端使用私有 DNS 伺服器,公開 IP 位址用戶端使用上游伺服器。",
+ "upstream_enable_reverse_lookup_title": "啟用用戶端 IP 位址反向查詢",
+ "upstream_example_multiple_upstreams_reserved": "%value%:針對特定網域 的多個上游伺服器",
+ "upstream_example_tcp": "%ipv4%、%ipv6%:常規 DNS(基於 TCP)",
+ "upstream_example_tcp_hostname": "%value%:常規 DNS(基於 TCP,主機名稱)",
+ "upstream_example_tcp_port": "%ipv4%、%ipv6%:常規 DNS(基於 TCP,指定連接埠)",
+ "upstream_example_udp": "%ipv4%、%ipv6%:常規 DNS(基於 UDP)",
+ "upstream_example_udp_hostname": "%value%:常規 DNS(基於 UDP,主機名稱)",
+ "upstream_example_udp_port": "%ipv4%、%ipv6%:常規 DNS(基於 UDP,指定連接埠)",
+ "upstream_example_upstream_comment": "%value%:註解",
+ "upstream_example_upstream_doh": "%value%:加密的 DNS-over-HTTPS ",
+ "upstream_example_upstream_doh3": "%value%:強制使用 HTTP/3 且不降級到 HTTP/2 的加密 DNS-over-HTTPS",
+ "upstream_example_upstream_doq": "%value%:加密的 DNS-over-QUIC ",
+ "upstream_example_upstream_dot": "%value%:加密的 DNS-over-TLS ",
+ "upstream_example_upstream_reserved": "%value%:針對特定網域 的上游伺服器",
+ "upstream_example_upstream_sdns": "%value%:適用於 DNSCrypt 或 DNS-over-HTTPS 解析器的 DNS Stamps ",
+ "upstream_examples_title": "範例",
+ "upstream_fallback_title": "備用 DNS 伺服器,每行一個",
+ "upstream_fallback_title_faq": "當上游 DNS 伺服器不可用時使用的備用 DNS 伺服器。語法與上游 DNS 伺服器相同。",
+ "upstream_private_ptr_resolvers_desc": "透過私有上游伺服器、DHCP、/etc/hosts 等方式解析包含私有 IP 位址的 ARPA 網域的 PTR、SOA 和 NS 請求。如果停用,AdGuard Home 將對所有此類請求回應 NXDOMAIN。",
+ "upstream_private_ptr_resolvers_title": "使用私人反向的 DNS 解析器",
+ "upstream_ptr": "私有反向 DNS 伺服器,每行一個",
+ "upstream_ptr_faq_1": "AdGuard Home 使用這些 DNS 伺服器處理私有 PTR、SOA 和 NS 請求。私有請求來自具有私有 IP 位址的用戶端,查詢的 ARPA 網域名稱包含私有 IP 位址範圍內的子網路,例如 %value%",
+ "upstream_ptr_faq_2": "如果留空,將使用作業系統的預設 DNS 解析器(AdGuard Home IP 位址除外)",
+ "upstream_ptr_faq_3": "預設情況下,AdGuard Home 使用以下反向 DNS 解析器:%value_1% 和 %value_2%",
+ "upstream_timeout": "上游逾時時間(秒)",
+ "upstream_timeout_faq": "等待上游伺服器回應的時間",
+ "upstream_timeout_placeholder": "輸入上游伺服器逾時時間",
+ "upstreams": "上游",
+ "url_label": "網址",
+ "use_saved_key": "使用現有私鑰",
+ "user_rules": "使用者規則",
+ "user_rules_add_to_allowlist": "加至允許清單",
+ "user_rules_allow_service": "允許服務",
+ "user_rules_browsing_security_disabled": "瀏覽安全已停用",
+ "user_rules_check_button": "檢查",
+ "user_rules_check_client_label": "用戶端識別碼(名稱、ClientID 或 IP 位址)",
+ "user_rules_check_client_placeholder": "輸入用戶識別碼",
+ "user_rules_check_dns_record_type_label": "DNS 記錄類型",
+ "user_rules_check_hostname_label": "主機名稱或域名",
+ "user_rules_check_hostname_placeholder": "輸入目標",
+ "user_rules_check_title": "檢查全域或針對特定用戶端的網域過濾",
+ "user_rules_cname": "CNAME :%cname%",
+ "user_rules_disable_browsing_security": "停用瀏覽安全",
+ "user_rules_disable_filter": "禁用過濾器",
+ "user_rules_disable_parental_control": "禁用家長控制",
+ "user_rules_disable_safe_search": "停用安全搜尋",
+ "user_rules_dns_record_type_placeholder": "選擇 DNS 記錄類型",
+ "user_rules_dns_rewrite_removed": "規則已從 DNS 重寫中移除",
+ "user_rules_domain": "網域 :%value%",
+ "user_rules_domain_blocked": "網域被封鎖",
+ "user_rules_domain_is_allowed": "網域已被允許",
+ "user_rules_domain_is_processed": "網域正在處理中",
+ "user_rules_edit_dns_rewrite": "編輯 DNS 重寫",
+ "user_rules_example_block": "||%domain% 封鎖對 %domain% 及其所有子網域的存取",
+ "user_rules_example_comment": "! 這是一條註解 要以註解開頭,請使用 ! 或 #",
+ "user_rules_example_comment_2": "# 這也是一條註解 要以註解開頭,請使用 ! 或 #",
+ "user_rules_example_regex": "/REGEX/ 封鎖與指定正規表達式匹配的網域",
+ "user_rules_example_response": "127.0.0.1 %domain% 僅對 %domain% 回應 127.0.0.1(不適用於其子網域)",
+ "user_rules_example_unblock": "@@||%domain% 放行對 %domain% 及其所有子網域的存取",
+ "user_rules_filter_was_disabled": "已停用 %value% ",
+ "user_rules_ip": "IP 位址 :%ip%",
+ "user_rules_learn_more": "了解更多 關於建立自訂 hosts 清單的資訊。",
+ "user_rules_no_rules_matched": "結果 :未匹配到規則",
+ "user_rules_parental_control_disabled": "家長控制已停用",
+ "user_rules_placeholder": "這是一個佔位符",
+ "user_rules_reason": "原因 :%reason%",
+ "user_rules_reason_allowed_by": "被 %source% 允許",
+ "user_rules_reason_blocked_by": "被 %source% 封鎖",
+ "user_rules_reason_filtered_by": "被 %source% 過濾",
+ "user_rules_redirected_to": "已重新導向至 :%value%",
+ "user_rules_remove_dns_rewrite": "移除 DNS 重寫",
+ "user_rules_rewrite_rule_is_applied": "改寫規則被套用",
+ "user_rules_rewritten_to": "已重寫為 :%value%",
+ "user_rules_rule": "規則 :%rule%",
+ "user_rules_rule_added_to_custom_filtering_rules": "規則已新增到自訂過濾規則",
+ "user_rules_rule_removed": "規則已從自訂過濾規則中移除",
+ "user_rules_safe_search_disabled": "安全搜尋已停用",
+ "user_rules_service": "服務 :%service%",
+ "user_rules_service_allowed": "已允許 %value% 服務",
+ "user_rules_source": "來源 :%value%",
+ "user_rules_source_allowlist": "允許清單 :%value%",
+ "user_rules_source_blocklist": "封鎖清單 :%value%",
+ "user_rules_status": "狀態 :%reason%",
+ "username_label": "使用者名稱",
+ "username_placeholder": "輸入使用者名稱",
+ "validated_with_dnssec": "已用網域名稱系統安全性擴充功能(DNSSEC)驗證",
+ "wednesday": "星期三",
+ "whois": "WHOIS",
+ "yes": "是",
+ "yes_disable": "禁用",
+ "yes_reset": "重置"
+}
diff --git a/client_v2/src/__tests__/access-store.test.ts b/client_v2/src/__tests__/access-store.test.ts
new file mode 100644
index 00000000000..259fef3ba00
--- /dev/null
+++ b/client_v2/src/__tests__/access-store.test.ts
@@ -0,0 +1,81 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+
+const mocks = vi.hoisted(() => ({
+ getAccessList: vi.fn(),
+ setAccessList: vi.fn(),
+ addSuccessToast: vi.fn(),
+ addErrorToast: vi.fn(),
+}));
+
+vi.mock('panel/api/Api', () => ({
+ apiClient: {
+ getAccessList: mocks.getAccessList,
+ setAccessList: mocks.setAccessList,
+ },
+}));
+vi.mock('panel/stores/toasts', () => ({
+ addSuccessToast: mocks.addSuccessToast,
+ addErrorToast: mocks.addErrorToast,
+}));
+
+import { toggleClientBlock } from 'panel/stores/access';
+
+describe('toggleClientBlock', () => {
+ beforeEach(() => vi.clearAllMocks());
+
+ it('not-disallowed + allowlist mode with >1 allowed → removes from allowed', async () => {
+ mocks.getAccessList.mockResolvedValue({
+ allowed_clients: ['1.1.1.1', '2.2.2.2'],
+ disallowed_clients: [],
+ blocked_hosts: [],
+ });
+ await toggleClientBlock('1.1.1.1', false, '');
+ expect(mocks.setAccessList).toHaveBeenCalledWith({
+ allowed_clients: ['2.2.2.2'],
+ disallowed_clients: [],
+ blocked_hosts: [],
+ });
+ });
+
+ it('not-disallowed, no allowlist → adds to disallowed', async () => {
+ mocks.getAccessList.mockResolvedValue({
+ allowed_clients: [],
+ disallowed_clients: [],
+ blocked_hosts: [],
+ });
+ await toggleClientBlock('3.3.3.3', false, '');
+ expect(mocks.setAccessList).toHaveBeenCalledWith({
+ allowed_clients: [],
+ disallowed_clients: ['3.3.3.3'],
+ blocked_hosts: [],
+ });
+ });
+
+ it('disallowed + allowlist mode → adds to allowed', async () => {
+ mocks.getAccessList.mockResolvedValue({
+ allowed_clients: ['1.1.1.1'],
+ disallowed_clients: ['2.2.2.2'],
+ blocked_hosts: [],
+ });
+ await toggleClientBlock('2.2.2.2', true, '');
+ expect(mocks.setAccessList).toHaveBeenCalledWith({
+ allowed_clients: ['1.1.1.1', '2.2.2.2'],
+ disallowed_clients: ['2.2.2.2'],
+ blocked_hosts: [],
+ });
+ });
+
+ it('disallowed, no allowlist → removes from disallowed (uses rule)', async () => {
+ mocks.getAccessList.mockResolvedValue({
+ allowed_clients: [],
+ disallowed_clients: ['client:X'],
+ blocked_hosts: [],
+ });
+ await toggleClientBlock('1.2.3.4', true, 'client:X');
+ expect(mocks.setAccessList).toHaveBeenCalledWith({
+ allowed_clients: [],
+ disallowed_clients: [],
+ blocked_hosts: [],
+ });
+ });
+});
diff --git a/client_v2/src/__tests__/app-routing.test.tsx b/client_v2/src/__tests__/app-routing.test.tsx
new file mode 100644
index 00000000000..e1ae84d9d93
--- /dev/null
+++ b/client_v2/src/__tests__/app-routing.test.tsx
@@ -0,0 +1,88 @@
+import { describe, it, expect, vi, beforeAll } from 'vitest';
+import { render, screen } from '@solidjs/testing-library';
+
+// jsdom has no matchMedia; App's theme effect and some hooks depend on it.
+beforeAll(() => {
+ if (!window.matchMedia) {
+ window.matchMedia = (query: string) =>
+ ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addEventListener: () => {},
+ removeEventListener: () => {},
+ addListener: () => {},
+ removeListener: () => {},
+ dispatchEvent: () => false,
+ }) as unknown as MediaQueryList;
+ }
+});
+
+// Keep the test focused on ROUTING: stub the store (no network/effects) and
+// the always-on chrome so the assertion only depends on route registration.
+vi.mock('panel/stores/dashboard', () => ({
+ dashboardState: {
+ processing: false,
+ isCoreRunning: true,
+ language: 'en',
+ theme: undefined,
+ protectionEnabled: false,
+ },
+ getDnsStatus: vi.fn(),
+ getTimerStatus: vi.fn(),
+}));
+
+vi.mock('panel/common/ui/Header', () => ({
+ Header: () =>
,
+}));
+vi.mock('panel/common/ui/Banners', () => ({
+ Banners: () =>
,
+}));
+vi.mock('panel/common/ui/Sidebar', () => ({
+ Sidebar: () =>
,
+}));
+vi.mock('panel/common/ui/Footer', () => ({
+ Footer: () =>
,
+}));
+vi.mock('panel/common/ui/Icons', () => ({ Icons: (): null => null }));
+vi.mock('panel/components/Toasts', () => ({ default: (): null => null }));
+
+// Deterministic marker so the assertion does not depend on Dashboard data/i18n.
+vi.mock('panel/components/Dashboard', () => ({
+ Dashboard: () =>
,
+}));
+
+import App from '../components/App';
+
+describe('App routing', () => {
+ it('registers the /dashboard route and renders matched route content', async () => {
+ window.location.hash = '#/dashboard';
+
+ render(() => );
+
+ expect(await screen.findByTestId('route-dashboard')).toBeInTheDocument();
+ });
+
+ it('mounts Banners between Header and the wrapper in the main entry', async () => {
+ window.location.hash = '#/dashboard';
+
+ render(() => );
+
+ const banners = await screen.findByTestId('chrome-banners');
+ expect(banners).toBeInTheDocument();
+
+ // Verify ordering: Header → Banners → Sidebar (wrapper starts)
+ const header = screen.getByTestId('chrome-header');
+ const sidebar = screen.getByTestId('chrome-sidebar');
+
+ // Banners should be after Header in DOM
+ expect(
+ header.compareDocumentPosition(banners) & Node.DOCUMENT_POSITION_FOLLOWING,
+ ).toBeTruthy();
+
+ // Banners should be before Sidebar in DOM
+ expect(
+ banners.compareDocumentPosition(sidebar) & Node.DOCUMENT_POSITION_FOLLOWING,
+ ).toBeTruthy();
+ });
+});
diff --git a/client_v2/src/__tests__/blocked-services.test.ts b/client_v2/src/__tests__/blocked-services.test.ts
new file mode 100644
index 00000000000..d1c98e5ed47
--- /dev/null
+++ b/client_v2/src/__tests__/blocked-services.test.ts
@@ -0,0 +1,83 @@
+import { describe, test, expect } from 'vitest';
+
+interface WebService {
+ id: string;
+ name: string;
+ icon_svg: string;
+ rules: string[];
+}
+
+const filterServices = (
+ services: WebService[],
+ search: string,
+ groupFilter: string[],
+ serviceGroupMap: Map,
+): WebService[] => {
+ let filtered = services;
+
+ if (groupFilter.length > 0) {
+ const selected = new Set(groupFilter);
+ filtered = filtered.filter((s) => {
+ const groupId = serviceGroupMap.get(s.id);
+ return groupId && selected.has(groupId);
+ });
+ }
+
+ const term = search.trim().toLowerCase();
+ if (term) {
+ filtered = filtered.filter(
+ (s) => s.name.toLowerCase().includes(term) || s.id.toLowerCase().includes(term),
+ );
+ }
+
+ return filtered;
+};
+
+describe('filterServices', () => {
+ const services: WebService[] = [
+ { id: 'telegram', name: 'Telegram', icon_svg: ' ', rules: [] },
+ { id: 'whatsapp', name: 'WhatsApp', icon_svg: ' ', rules: [] },
+ { id: 'steam', name: 'Steam', icon_svg: ' ', rules: [] },
+ { id: 'epic_games', name: 'Epic Games', icon_svg: ' ', rules: [] },
+ ];
+
+ const serviceGroupMap = new Map([
+ ['telegram', 'messaging'],
+ ['whatsapp', 'messaging'],
+ ['steam', 'gaming'],
+ ['epic_games', 'gaming'],
+ ]);
+
+ test('no filters returns all services', () => {
+ expect(filterServices(services, '', [], serviceGroupMap)).toHaveLength(4);
+ });
+
+ test('search by name (case-insensitive)', () => {
+ const result = filterServices(services, 'tel', [], serviceGroupMap);
+ expect(result).toHaveLength(1);
+ expect(result[0].id).toBe('telegram');
+ });
+
+ test('search by id', () => {
+ const result = filterServices(services, 'epic', [], serviceGroupMap);
+ expect(result).toHaveLength(1);
+ expect(result[0].id).toBe('epic_games');
+ });
+
+ test('filter by group', () => {
+ const result = filterServices(services, '', ['gaming'], serviceGroupMap);
+ expect(result).toHaveLength(2);
+ expect(result.map((s) => s.id)).toEqual(['steam', 'epic_games']);
+ });
+
+ test('combined search + group filter', () => {
+ const result = filterServices(services, 'steam', ['gaming'], serviceGroupMap);
+ expect(result).toHaveLength(1);
+ expect(result[0].id).toBe('steam');
+ });
+
+ test('no match returns empty array', () => {
+ const result = filterServices(services, 'xyz', [], serviceGroupMap);
+ expect(result).toHaveLength(0);
+ });
+});
diff --git a/client_v2/src/__tests__/check-result-helpers.test.ts b/client_v2/src/__tests__/check-result-helpers.test.ts
new file mode 100644
index 00000000000..910bcec8493
--- /dev/null
+++ b/client_v2/src/__tests__/check-result-helpers.test.ts
@@ -0,0 +1,162 @@
+import { describe, expect, it } from 'vitest';
+
+import intl from 'panel/common/intl';
+import { FILTERED_STATUS, SPECIAL_FILTER_ID } from 'panel/helpers/constants';
+
+import { getCheckResultMeta } from '../components/UserRules/checkResultHelpers';
+
+describe('getCheckResultMeta', () => {
+ const filters = [
+ {
+ id: 101,
+ name: 'Example Blocklist',
+ url: 'https://filters.example/blocklist.txt',
+ enabled: true,
+ lastUpdated: '',
+ rulesCount: 12,
+ },
+ ];
+ const whitelistFilters = [
+ {
+ id: 201,
+ name: 'Example Allowlist',
+ url: 'https://filters.example/allowlist.txt',
+ enabled: true,
+ lastUpdated: '',
+ rulesCount: 7,
+ },
+ ];
+
+ it('localizes filter and allowlist reasons through translation keys', () => {
+ const blockedMeta = getCheckResultMeta({
+ reason: FILTERED_STATUS.FILTERED_BLACK_LIST,
+ rules: [{ filter_list_id: 101, text: '||filtered.example^' }],
+ filters,
+ whitelistFilters: [],
+ });
+
+ const allowedMeta = getCheckResultMeta({
+ reason: FILTERED_STATUS.NOT_FILTERED_WHITE_LIST,
+ rules: [{ filter_list_id: 201, text: '@@||allowed.example^$important' }],
+ filters: [],
+ whitelistFilters,
+ });
+
+ expect(blockedMeta.reason).toBeUndefined();
+ expect(blockedMeta.source).toBe('Example Blocklist');
+ expect(blockedMeta.sourceListType).toBe('blocklist');
+ expect(allowedMeta.reason).toBeUndefined();
+ expect(allowedMeta.source).toBe('Example Allowlist');
+ expect(allowedMeta.sourceListType).toBe('allowlist');
+ expect(allowedMeta.actions).toEqual([
+ {
+ kind: 'disable-filter',
+ label: intl.getMessage('user_rules_disable_filter'),
+ },
+ ]);
+ });
+
+ it('localizes custom filtering and protection reasons through translation keys', () => {
+ const customMeta = getCheckResultMeta({
+ reason: FILTERED_STATUS.FILTERED_BLACK_LIST,
+ rules: [
+ {
+ filter_list_id: SPECIAL_FILTER_ID.CUSTOM_FILTERING_RULES,
+ text: '||blocked.example^',
+ },
+ ],
+ filters: [],
+ whitelistFilters: [],
+ });
+
+ const safeBrowsingMeta = getCheckResultMeta({
+ reason: FILTERED_STATUS.FILTERED_SAFE_BROWSING,
+ rules: [
+ { filter_list_id: SPECIAL_FILTER_ID.SAFE_BROWSING, text: 'adguard-malware-shavar' },
+ ],
+ filters: [],
+ whitelistFilters: [],
+ });
+
+ const blockedServicesMeta = getCheckResultMeta({
+ reason: FILTERED_STATUS.FILTERED_BLOCKED_SERVICE,
+ rules: [{ filter_list_id: 0, text: '||amemv.com^' }],
+ filters: [],
+ whitelistFilters: [],
+ });
+
+ expect(customMeta.reason).toBe(
+ intl.getMessage('user_rules_reason_blocked_by', {
+ source: intl.getMessage('custom_filtering_rules'),
+ }),
+ );
+ expect(safeBrowsingMeta.reason).toBe(intl.getMessage('blocked_threats'));
+ expect(safeBrowsingMeta.source).toBe(intl.getMessage('safe_browsing'));
+ expect(safeBrowsingMeta.rule).toBe('adguard-malware-shavar');
+ expect(blockedServicesMeta.reason).toBe(intl.getMessage('blocked_services'));
+ expect(blockedServicesMeta.rule).toBe('||amemv.com^');
+ });
+
+ it('renders Safe Search as a rewritten result with the Safe search status', () => {
+ const safeSearchMeta = getCheckResultMeta({
+ reason: FILTERED_STATUS.FILTERED_SAFE_SEARCH,
+ rules: [],
+ filters: [],
+ whitelistFilters: [],
+ });
+
+ expect(safeSearchMeta.tone).toBe('rewritten');
+ expect(safeSearchMeta.title).toBe(intl.getMessage('user_rules_rewrite_rule_is_applied'));
+ expect(safeSearchMeta.reason).toBe(intl.getMessage('settings_safe_search'));
+ expect(safeSearchMeta.actions).toEqual([
+ {
+ kind: 'allow',
+ label: intl.getMessage('user_rules_add_to_allowlist'),
+ },
+ {
+ kind: 'disable-safesearch',
+ label: intl.getMessage('user_rules_disable_safe_search'),
+ },
+ ]);
+ });
+
+ it('omits filter and allowlist reasons when the source name is unavailable', () => {
+ const blockedMeta = getCheckResultMeta({
+ reason: FILTERED_STATUS.FILTERED_BLACK_LIST,
+ rules: [{ filter_list_id: 999, text: '||filtered.example^' }],
+ filters: [],
+ whitelistFilters: [],
+ });
+
+ const allowedMeta = getCheckResultMeta({
+ reason: FILTERED_STATUS.NOT_FILTERED_WHITE_LIST,
+ rules: [{ filter_list_id: 999, text: '@@||allowed.example^$important' }],
+ filters: [],
+ whitelistFilters: [],
+ });
+
+ expect(blockedMeta.reason).toBeUndefined();
+ expect(allowedMeta.reason).toBeUndefined();
+ });
+
+ it('keeps custom allowlist actions limited to block', () => {
+ const customAllowedMeta = getCheckResultMeta({
+ reason: FILTERED_STATUS.NOT_FILTERED_WHITE_LIST,
+ rules: [
+ {
+ filter_list_id: SPECIAL_FILTER_ID.CUSTOM_FILTERING_RULES,
+ text: '@@||allowed.example^$important',
+ },
+ ],
+ filters: [],
+ whitelistFilters: [],
+ });
+
+ expect(customAllowedMeta.actions).toEqual([
+ {
+ kind: 'block',
+ label: intl.getMessage('block'),
+ },
+ ]);
+ });
+});
diff --git a/client_v2/src/__tests__/clientForm/Identifiers.test.tsx b/client_v2/src/__tests__/clientForm/Identifiers.test.tsx
new file mode 100644
index 00000000000..fc47cbff92d
--- /dev/null
+++ b/client_v2/src/__tests__/clientForm/Identifiers.test.tsx
@@ -0,0 +1,89 @@
+import { describe, it, expect, beforeEach } from 'vitest';
+import { render, screen, fireEvent } from '@solidjs/testing-library';
+
+import { Identifiers } from 'panel/components/Clients/AddClient/blocks/Identifiers';
+import { initClientForm, clientFormState } from 'panel/stores/clientForm';
+
+describe('Identifiers', () => {
+ beforeEach(() => {
+ initClientForm(null);
+ });
+
+ it('keeps the entered identifier value (bug #1: value erased after typing)', () => {
+ render(() => );
+ const input = document.querySelector('input[type="text"]') as HTMLInputElement;
+
+ // Simulate typing — fireEvent.input triggers onInput, which calls handleChange.
+ fireEvent.input(input, { target: { value: '1.2.3.4' } });
+
+ expect(input.value).toBe('1.2.3.4');
+ expect(clientFormState.ids).toEqual(['1.2.3.4']);
+ });
+
+ it('adds a new identifier row on button click (bug #2: add button did nothing)', () => {
+ render(() => );
+ expect(document.querySelectorAll('input[type="text"]')).toHaveLength(1);
+
+ fireEvent.click(screen.getByTestId('client-form-add-identifier'));
+
+ const inputs = document.querySelectorAll('input[type="text"]');
+ expect(inputs).toHaveLength(2);
+ expect(clientFormState.ids).toEqual(['', '']);
+ });
+
+ it('keeps existing values when adding a new row', () => {
+ render(() => );
+ const first = document.querySelector('input[type="text"]') as HTMLInputElement;
+ fireEvent.input(first, { target: { value: '1.2.3.4' } });
+
+ fireEvent.click(screen.getByTestId('client-form-add-identifier'));
+
+ const inputs = document.querySelectorAll('input[type="text"]');
+ expect(inputs).toHaveLength(2);
+ expect((inputs[0] as HTMLInputElement).value).toBe('1.2.3.4');
+ expect((inputs[1] as HTMLInputElement).value).toBe('');
+ expect(clientFormState.ids).toEqual(['1.2.3.4', '']);
+ });
+
+ it('removes an identifier row (except the first one which has no remove button)', () => {
+ render(() => );
+ fireEvent.click(screen.getByTestId('client-form-add-identifier'));
+ expect(document.querySelectorAll('input[type="text"]')).toHaveLength(2);
+
+ // The remove button is the suffix of the second row.
+ const removeBtn = document.querySelector('[aria-label]') as HTMLElement;
+ fireEvent.click(removeBtn);
+
+ expect(document.querySelectorAll('input[type="text"]')).toHaveLength(1);
+ expect(clientFormState.ids).toEqual(['']);
+ });
+
+ it('shows a validation error on blur for an invalid identifier', async () => {
+ render(() => );
+ const input = document.querySelector('input[type="text"]') as HTMLInputElement;
+
+ fireEvent.input(input, { target: { value: 'not-a-valid-id' } });
+ fireEvent.blur(input);
+
+ // The error message element is rendered by the Input when there is an error.
+ expect(input.value).toBe('not-a-valid-id');
+ });
+
+ it('clears the local error when the user types in the field', () => {
+ render(() => );
+ const input = document.querySelector('input[type="text"]') as HTMLInputElement;
+
+ // Trigger a validation error on blur.
+ fireEvent.input(input, { target: { value: 'bad!!!' } });
+ fireEvent.blur(input);
+
+ // Now type a valid value — fireEvent.input triggers onInput, which calls
+ // handleChange → clears the local error.
+ fireEvent.input(input, { target: { value: '1.2.3.4' } });
+
+ // The Input component only renders the error div when errorMessage is truthy.
+ // Check that the wrapper does not have the error modifier class.
+ const wrapper = input.closest('[class*="inputWrapper"]');
+ expect(wrapper?.className).not.toContain('error');
+ });
+});
diff --git a/client_v2/src/__tests__/clientForm/buildClientBreadcrumbs.test.ts b/client_v2/src/__tests__/clientForm/buildClientBreadcrumbs.test.ts
new file mode 100644
index 00000000000..3460fa02cf1
--- /dev/null
+++ b/client_v2/src/__tests__/clientForm/buildClientBreadcrumbs.test.ts
@@ -0,0 +1,28 @@
+import { describe, expect, it } from 'vitest';
+import { buildClientBreadcrumbs } from 'panel/helpers/buildClientBreadcrumbs';
+import { RoutePath } from 'panel/components/Routes/Paths';
+
+describe('buildClientBreadcrumbs', () => {
+ it('builds add-mode breadcrumbs with extra links', () => {
+ const result = buildClientBreadcrumbs({ mode: 'add', originalName: '' }, [
+ { path: RoutePath.ClientsBlockedServices, title: 'Blocked Services' },
+ ]);
+ expect(result).toHaveLength(3);
+ expect(result[0].path).toBe(RoutePath.Clients);
+ expect(result[1].path).toBe(RoutePath.ClientsAdd);
+ expect(result[2].path).toBe(RoutePath.ClientsBlockedServices);
+ });
+
+ it('builds edit-mode breadcrumbs with encoded clientName', () => {
+ const result = buildClientBreadcrumbs({ mode: 'edit', originalName: 'My Client' }, []);
+ expect(result).toHaveLength(2);
+ expect(result[0].path).toBe(RoutePath.Clients);
+ expect(result[1].path).toBe(RoutePath.ClientsEdit);
+ expect(result[1].props?.clientName).toBe('My%20Client');
+ });
+
+ it('returns empty array when clientForm is null', () => {
+ const result = buildClientBreadcrumbs(null, []);
+ expect(result).toHaveLength(0);
+ });
+});
diff --git a/client_v2/src/__tests__/clientForm/buildClientConfig.test.ts b/client_v2/src/__tests__/clientForm/buildClientConfig.test.ts
new file mode 100644
index 00000000000..93fcb157983
--- /dev/null
+++ b/client_v2/src/__tests__/clientForm/buildClientConfig.test.ts
@@ -0,0 +1,78 @@
+import { describe, expect, it } from 'vitest';
+
+import { buildClientConfig } from 'panel/stores/clientForm';
+import { getInitialClientFormState } from 'panel/initialState';
+
+describe('buildClientConfig', () => {
+ it('syncs use_global_blocked_services from use_global_settings=true', () => {
+ const form = {
+ ...getInitialClientFormState(),
+ use_global_settings: true,
+ use_global_blocked_services: false,
+ };
+ const config = buildClientConfig(form);
+ expect(config.use_global_blocked_services).toBe(true);
+ expect(config.use_global_settings).toBe(true);
+ });
+
+ it('syncs use_global_blocked_services from use_global_settings=false', () => {
+ const form = {
+ ...getInitialClientFormState(),
+ use_global_settings: false,
+ use_global_blocked_services: true,
+ };
+ const config = buildClientConfig(form);
+ expect(config.use_global_blocked_services).toBe(false);
+ expect(config.use_global_settings).toBe(false);
+ });
+
+ it('filters empty IDs', () => {
+ const form = {
+ ...getInitialClientFormState(),
+ name: 'Test',
+ ids: ['192.168.1.1', '', ' '],
+ };
+ const config = buildClientConfig(form);
+ expect(config.ids).toEqual(['192.168.1.1']);
+ });
+
+ it('splits upstreams by newline and filters blank lines', () => {
+ const form = {
+ ...getInitialClientFormState(),
+ upstreams: '1.1.1.1\n\n8.8.8.8\n \n',
+ };
+ const config = buildClientConfig(form);
+ expect(config.upstreams).toEqual(['1.1.1.1', '8.8.8.8']);
+ });
+
+ it('returns empty array when upstreams is empty string', () => {
+ const form = {
+ ...getInitialClientFormState(),
+ upstreams: '',
+ };
+ const config = buildClientConfig(form);
+ expect(config.upstreams).toEqual([]);
+ });
+
+ it('returns all expected top-level keys', () => {
+ const config = buildClientConfig(getInitialClientFormState());
+ expect(Object.keys(config).sort()).toEqual([
+ 'blocked_services',
+ 'blocked_services_schedule',
+ 'filtering_enabled',
+ 'ids',
+ 'ignore_querylog',
+ 'ignore_statistics',
+ 'name',
+ 'parental_enabled',
+ 'safe_search',
+ 'safebrowsing_enabled',
+ 'tags',
+ 'upstreams',
+ 'upstreams_cache_enabled',
+ 'upstreams_cache_size',
+ 'use_global_blocked_services',
+ 'use_global_settings',
+ ]);
+ });
+});
diff --git a/client_v2/src/__tests__/clientForm/store.test.ts b/client_v2/src/__tests__/clientForm/store.test.ts
new file mode 100644
index 00000000000..0dac70f287f
--- /dev/null
+++ b/client_v2/src/__tests__/clientForm/store.test.ts
@@ -0,0 +1,119 @@
+import { describe, expect, it, vi, beforeEach } from 'vitest';
+
+// vi.hoisted ensures the mock is available before module evaluation.
+const dashboardMocks = vi.hoisted(() => ({
+ getClients: vi.fn(),
+ dashboardState: { clients: [] as any[] },
+}));
+
+vi.mock('panel/stores/dashboard', async () => {
+ const actual =
+ await vi.importActual('panel/stores/dashboard');
+ return {
+ ...actual,
+ getClients: dashboardMocks.getClients,
+ get dashboardState() {
+ return dashboardMocks.dashboardState;
+ },
+ };
+});
+
+import {
+ initClientForm,
+ updateClientFormField,
+ clearClientForm,
+ clientFormState,
+ saveClient,
+} from 'panel/stores/clientForm';
+
+describe('clientForm store', () => {
+ it('returns initial state by default', () => {
+ clearClientForm();
+ expect(clientFormState.mode).toBe('add');
+ expect(clientFormState.name).toBe('');
+ expect(clientFormState.ids).toEqual(['']);
+ });
+
+ it('handles initClientForm with null payload (add mode)', () => {
+ initClientForm(null);
+ expect(clientFormState.mode).toBe('add');
+ expect(clientFormState.name).toBe('');
+ });
+
+ it('handles initClientForm with payload (edit mode)', () => {
+ const payload = {
+ name: 'My Laptop',
+ ids: ['192.168.1.10'],
+ filtering_enabled: true,
+ };
+ initClientForm(payload as any);
+ expect(clientFormState.mode).toBe('edit');
+ expect(clientFormState.name).toBe('My Laptop');
+ expect(clientFormState.ids).toEqual(['192.168.1.10']);
+ expect(clientFormState.filtering_enabled).toBe(true);
+ expect(clientFormState.originalName).toBe('My Laptop');
+ });
+
+ it('handles updateClientFormField', () => {
+ clearClientForm();
+ updateClientFormField({ field: 'name', value: 'Test' });
+ expect(clientFormState.name).toBe('Test');
+ });
+
+ it('handles nested field update via updateClientFormField', () => {
+ clearClientForm();
+ updateClientFormField({
+ field: 'safe_search',
+ value: {
+ enabled: true,
+ google: true,
+ youtube: false,
+ bing: false,
+ duckduckgo: false,
+ yandex: false,
+ pixabay: false,
+ },
+ });
+ expect(clientFormState.safe_search.enabled).toBe(true);
+ expect(clientFormState.safe_search.google).toBe(true);
+ });
+
+ it('handles clearClientForm', () => {
+ initClientForm({ name: 'Test' } as any);
+ expect(clientFormState.name).toBe('Test');
+ clearClientForm();
+ expect(clientFormState.name).toBe('');
+ expect(clientFormState.mode).toBe('add');
+ });
+});
+
+describe('saveClient cross-client validation', () => {
+ beforeEach(() => {
+ clearClientForm();
+ dashboardMocks.dashboardState.clients = [];
+ });
+
+ it('rejects an identifier that already belongs to another client', async () => {
+ // Set up the mocked dashboard store to include a client with a known ID.
+ dashboardMocks.dashboardState.clients = [{ name: 'Other', ids: ['192.168.1.50'] } as any];
+
+ updateClientFormField({ field: 'name', value: 'New Client' });
+ updateClientFormField({ field: 'ids', value: ['192.168.1.50'] });
+
+ const result = await saveClient();
+ expect(result).toBe(false);
+ expect(clientFormState.formErrors.ids).toBeDefined();
+ });
+
+ it('rejects cache size of 0 when cache is enabled', async () => {
+ clearClientForm();
+ updateClientFormField({ field: 'name', value: 'Test' });
+ updateClientFormField({ field: 'ids', value: ['192.168.1.1'] });
+ updateClientFormField({ field: 'upstreams_cache_enabled', value: true });
+ updateClientFormField({ field: 'upstreams_cache_size', value: 0 });
+
+ const result = await saveClient();
+ expect(result).toBe(false);
+ expect(clientFormState.formErrors.upstreams_cache_size).toBeDefined();
+ });
+});
diff --git a/client_v2/src/__tests__/clientForm/validators.test.ts b/client_v2/src/__tests__/clientForm/validators.test.ts
new file mode 100644
index 00000000000..48e08b1f1c0
--- /dev/null
+++ b/client_v2/src/__tests__/clientForm/validators.test.ts
@@ -0,0 +1,552 @@
+import { describe, expect, it } from 'vitest';
+import {
+ validateIdentifier,
+ validateUpstreams,
+ validateRequiredValue,
+ validateIpv4,
+ validatePort,
+ validateInstallPort,
+ validatePlainDns,
+ validateIpForGatewaySubnetMask,
+ validateIpv4InCidr,
+ validatePasswordLength,
+ validateIpNotDuplicate,
+ validateIpPerLine,
+ validateBetween,
+ validateMinValue,
+ validateCacheSize,
+ validateRewriteNotExists,
+ validateRewriteNotSame,
+ validateLeaseTime,
+} from 'panel/helpers/validators';
+
+describe('validateIdentifier', () => {
+ it('returns required error for empty string', () => {
+ const result = validateIdentifier('', [], 0);
+ expect(result).toBeTruthy();
+ });
+
+ it('returns required error for whitespace-only string', () => {
+ const result = validateIdentifier(' ', [], 0);
+ expect(result).toBeTruthy();
+ });
+
+ it('returns undefined for valid IPv4', () => {
+ const result = validateIdentifier('192.168.1.1', ['192.168.1.1'], 0);
+ expect(result).toBeUndefined();
+ });
+
+ it('returns undefined for valid IPv6', () => {
+ const result = validateIdentifier('::1', ['::1'], 0);
+ expect(result).toBeUndefined();
+ });
+
+ it('returns undefined for valid MAC', () => {
+ const result = validateIdentifier('aa:bb:cc:dd:ee:ff', ['aa:bb:cc:dd:ee:ff'], 0);
+ expect(result).toBeUndefined();
+ });
+
+ it('returns undefined for valid CIDR', () => {
+ const result = validateIdentifier('192.168.1.0/24', ['192.168.1.0/24'], 0);
+ expect(result).toBeUndefined();
+ });
+
+ it('returns undefined for valid ClientID', () => {
+ const result = validateIdentifier('my-client-01', ['my-client-01'], 0);
+ expect(result).toBeUndefined();
+ });
+
+ it('returns format error for invalid identifier', () => {
+ const result = validateIdentifier('not a valid id!!!', [], 0);
+ expect(result).toBeTruthy();
+ });
+
+ it('returns duplicate error for repeated identifier', () => {
+ const result = validateIdentifier('test-id', ['other-id', 'test-id'], 0);
+ expect(result).toBeTruthy();
+ });
+
+ it('returns undefined for unique identifiers', () => {
+ const result = validateIdentifier('test-id', ['other-id', 'another-id'], 0);
+ expect(result).toBeUndefined();
+ });
+});
+
+describe('validateUpstreams', () => {
+ it('returns undefined for an empty value', () => {
+ const result = validateUpstreams('');
+ expect(result).toBeUndefined();
+ });
+
+ it('returns undefined for comment lines', () => {
+ const result = validateUpstreams('# this is a comment');
+ expect(result).toBeUndefined();
+ });
+
+ it('returns undefined for blank lines', () => {
+ const result = validateUpstreams('\n\n');
+ expect(result).toBeUndefined();
+ });
+
+ it('returns undefined for a plain IP address', () => {
+ const result = validateUpstreams('1.1.1.1');
+ expect(result).toBeUndefined();
+ });
+
+ it('returns undefined for an IP with port', () => {
+ const result = validateUpstreams('1.1.1.1:53');
+ expect(result).toBeUndefined();
+ });
+
+ it('returns undefined for a protocol URL', () => {
+ const result = validateUpstreams('tls://dns.example.com');
+ expect(result).toBeUndefined();
+ });
+
+ it('returns undefined for a hostname', () => {
+ const result = validateUpstreams('dns.example.com');
+ expect(result).toBeUndefined();
+ });
+
+ it('returns error for a line without dot or colon', () => {
+ const result = validateUpstreams('not-a-valid-upstream');
+ expect(result).toBe('Invalid format');
+ });
+
+ it('returns error on the correct line number for mixed content', () => {
+ const result = validateUpstreams('1.1.1.1\nbadline\ntls://ok.com');
+ expect(result).toBe('Invalid format on line 2');
+ });
+
+ it('skips comments and only flags real lines', () => {
+ const result = validateUpstreams('# comment\nbadline\n1.1.1.1');
+ expect(result).toBe('Invalid format on line 2');
+ });
+
+ it('returns "Invalid format" for single invalid line with trailing newline', () => {
+ const result = validateUpstreams('badline\n');
+ expect(result).toBe('Invalid format');
+ });
+
+ it('returns "Invalid format" for single invalid line with leading newline', () => {
+ const result = validateUpstreams('\nbadline');
+ expect(result).toBe('Invalid format');
+ });
+
+ it('returns "Invalid format on lines 1, 2" when both invalid', () => {
+ const result = validateUpstreams('bad1\nbad2');
+ expect(result).toBe('Invalid format on lines 1, 2');
+ });
+
+ it('returns "Invalid format on line 2" when second line invalid in multi-content', () => {
+ const result = validateUpstreams('1.1.1.1\nbad');
+ expect(result).toBe('Invalid format on line 2');
+ });
+
+ it('handles blank line between two invalid lines', () => {
+ const result = validateUpstreams('bad1\n\nbad2');
+ expect(result).toBe('Invalid format on lines 1, 3');
+ });
+
+ it('returns "Invalid format" for comment-then-invalid (one content line)', () => {
+ const result = validateUpstreams('# comment\nbadline');
+ expect(result).toBe('Invalid format');
+ });
+
+ it('returns "Invalid format" for invalid-then-comment (one content line)', () => {
+ const result = validateUpstreams('badline\n# comment');
+ expect(result).toBe('Invalid format');
+ });
+});
+
+describe('validateRequiredValue', () => {
+ it('returns undefined for non-empty string', () => {
+ expect(validateRequiredValue('hello')).toBeUndefined();
+ });
+
+ it('returns error for empty string', () => {
+ expect(validateRequiredValue('')).toBeTruthy();
+ });
+
+ it('returns error for whitespace-only string', () => {
+ expect(validateRequiredValue(' ')).toBeTruthy();
+ });
+
+ it('returns undefined for 0 (zero is valid)', () => {
+ expect(validateRequiredValue(0)).toBeUndefined();
+ });
+
+ it('returns error for undefined', () => {
+ expect(validateRequiredValue(undefined)).toBeTruthy();
+ });
+
+ it('returns undefined for truthy number', () => {
+ expect(validateRequiredValue(42)).toBeUndefined();
+ });
+
+ it('returns error for false', () => {
+ expect(validateRequiredValue(false)).toBeTruthy();
+ });
+});
+
+describe('validateIpv4', () => {
+ it('returns undefined for valid IPv4', () => {
+ expect(validateIpv4('192.168.1.1')).toBeUndefined();
+ });
+
+ it('returns error for invalid IPv4', () => {
+ expect(validateIpv4('999.999.999.999')).toBeTruthy();
+ });
+
+ it('returns undefined for empty string (skip)', () => {
+ expect(validateIpv4('')).toBeUndefined();
+ });
+
+ it('returns undefined for undefined (skip)', () => {
+ expect(validateIpv4(undefined)).toBeUndefined();
+ });
+});
+
+describe('validatePort', () => {
+ it('returns undefined for port in web range', () => {
+ expect(validatePort(8080)).toBeUndefined();
+ });
+
+ it('returns undefined for boundary 0 (disabled)', () => {
+ expect(validatePort(0)).toBeUndefined();
+ });
+
+ it('returns undefined for boundary 1', () => {
+ expect(validatePort(1)).toBeUndefined();
+ });
+
+ it('returns undefined for boundary 80', () => {
+ expect(validatePort(80)).toBeUndefined();
+ });
+
+ it('returns undefined for boundary 65535', () => {
+ expect(validatePort(65535)).toBeUndefined();
+ });
+
+ it('returns error for port above 65535', () => {
+ expect(validatePort(65536)).toBeTruthy();
+ });
+
+ it('returns error for negative port', () => {
+ expect(validatePort(-1)).toBeTruthy();
+ });
+
+ it('returns undefined for undefined (no value)', () => {
+ expect(validatePort(undefined)).toBeUndefined();
+ });
+});
+
+describe('validateInstallPort', () => {
+ it('returns undefined for valid port', () => {
+ expect(validateInstallPort(80)).toBeUndefined();
+ });
+
+ it('returns undefined for boundary 1', () => {
+ expect(validateInstallPort(1)).toBeUndefined();
+ });
+
+ it('returns undefined for boundary 65535', () => {
+ expect(validateInstallPort(65535)).toBeUndefined();
+ });
+
+ it('returns error for 0 (install requires real port)', () => {
+ expect(validateInstallPort(0)).toBeTruthy();
+ });
+
+ it('returns error for port above 65535', () => {
+ expect(validateInstallPort(65536)).toBeTruthy();
+ });
+});
+
+describe('validatePlainDns', () => {
+ it('returns undefined when both encryption and plain DNS are on', () => {
+ expect(validatePlainDns(true, { enabled: true })).toBeUndefined();
+ });
+
+ it('returns undefined when plain DNS is on, encryption off', () => {
+ expect(validatePlainDns(true, { enabled: false })).toBeUndefined();
+ });
+
+ it('returns undefined when encryption is on, plain DNS off', () => {
+ expect(validatePlainDns(false, { enabled: true })).toBeUndefined();
+ });
+
+ it('returns error when both are off', () => {
+ expect(validatePlainDns(false, { enabled: false })).toBeTruthy();
+ });
+});
+
+describe('validateIpForGatewaySubnetMask', () => {
+ const validContext = {
+ v4: { gateway_ip: '192.168.1.1', subnet_mask: '255.255.255.0' },
+ };
+
+ it('returns undefined for IP in subnet', () => {
+ expect(validateIpForGatewaySubnetMask('192.168.1.5', validContext)).toBeUndefined();
+ });
+
+ it('returns error for IP outside subnet', () => {
+ expect(validateIpForGatewaySubnetMask('10.0.0.1', validContext)).toBeTruthy();
+ });
+
+ it('returns undefined when context is missing', () => {
+ expect(validateIpForGatewaySubnetMask('192.168.1.5', {})).toBeUndefined();
+ });
+
+ it('returns undefined for empty value', () => {
+ expect(validateIpForGatewaySubnetMask('', validContext)).toBeUndefined();
+ });
+
+ // REGRESSION: guard must use nested allValues.v4.gateway_ip, not flat
+ it('validates IP using nested context only (no flat keys)', () => {
+ const nestedOnly = {
+ v4: { gateway_ip: '192.168.1.1', subnet_mask: '255.255.255.0' },
+ };
+ // IP outside subnet should return error even without flat keys
+ expect(validateIpForGatewaySubnetMask('10.0.0.1', nestedOnly)).toBeTruthy();
+ });
+});
+
+describe('validateIpv4InCidr', () => {
+ it('returns undefined for IP within CIDR', () => {
+ expect(validateIpv4InCidr('192.168.1.5', { cidr: '192.168.1.0/24' })).toBeUndefined();
+ });
+
+ it('returns error for IP outside CIDR', () => {
+ expect(validateIpv4InCidr('10.0.0.1', { cidr: '192.168.1.0/24' })).toBeTruthy();
+ });
+});
+
+describe('validatePasswordLength', () => {
+ it('returns true for too-short password (< 8 UTF-8 bytes)', () => {
+ expect(validatePasswordLength('short')).toBe(true);
+ });
+
+ it('returns undefined for valid password', () => {
+ expect(validatePasswordLength('longenough')).toBeUndefined();
+ });
+
+ it('returns undefined for empty string (skipped)', () => {
+ expect(validatePasswordLength('')).toBeUndefined();
+ });
+
+ it('returns undefined for undefined (skipped)', () => {
+ expect(validatePasswordLength(undefined)).toBeUndefined();
+ });
+
+ it('returns true for password exceeding 72 UTF-8 bytes', () => {
+ expect(validatePasswordLength('a'.repeat(73))).toBe(true);
+ });
+
+ it('counts UTF-8 bytes, not characters (multibyte é = 2 bytes)', () => {
+ // 'é' repeated 3 times = 6 UTF-8 bytes < 8 → too short
+ expect(validatePasswordLength('é'.repeat(3))).toBe(true);
+ // 'é' repeated 4 times = 8 UTF-8 bytes → valid
+ expect(validatePasswordLength('é'.repeat(4))).toBeUndefined();
+ });
+});
+
+describe('validateIpNotDuplicate', () => {
+ const leases = [{ ip: '192.168.1.10' }, { ip: '192.168.1.20' }];
+
+ it('returns undefined for unique IP', () => {
+ expect(validateIpNotDuplicate(leases)('192.168.1.30')).toBeUndefined();
+ });
+
+ it('returns error for duplicate IP', () => {
+ expect(validateIpNotDuplicate(leases)('192.168.1.10')).toBeTruthy();
+ });
+
+ it('ignores the edit IP (editing existing lease)', () => {
+ expect(validateIpNotDuplicate(leases, '192.168.1.10')('192.168.1.10')).toBeUndefined();
+ });
+
+ it('returns undefined for empty leases', () => {
+ expect(validateIpNotDuplicate([])('192.168.1.1')).toBeUndefined();
+ });
+
+ it('returns undefined for empty value', () => {
+ expect(validateIpNotDuplicate(leases)('')).toBeUndefined();
+ });
+});
+
+describe('validateIpPerLine', () => {
+ it('returns undefined for valid multi-line IPs', () => {
+ expect(validateIpPerLine('192.168.1.1\n10.0.0.1')).toBeUndefined();
+ });
+
+ it('returns error when a line is invalid', () => {
+ expect(validateIpPerLine('192.168.1.1\nnot-an-ip')).toBeTruthy();
+ });
+
+ it('returns undefined for empty string', () => {
+ expect(validateIpPerLine('')).toBeUndefined();
+ });
+
+ it('skips blank lines', () => {
+ expect(validateIpPerLine('\n\n')).toBeUndefined();
+ });
+
+ it('skips blank lines between valid IPs', () => {
+ expect(validateIpPerLine('192.168.1.1\n\n10.0.0.1')).toBeUndefined();
+ });
+});
+
+describe('numeric range validators', () => {
+ it('validateBetween returns error when value is out of range', () => {
+ expect(validateBetween(-1, 0, 32)).toBeTruthy();
+ expect(validateBetween(33, 0, 32)).toBeTruthy();
+ expect(validateBetween(0, 0, 32)).toBeUndefined();
+ expect(validateBetween(32, 0, 32)).toBeUndefined();
+ expect(validateBetween(16, 0, 32)).toBeUndefined();
+ });
+
+ it('validateMinValue returns error when value is below min', () => {
+ expect(validateMinValue(0, 1)).toBeTruthy();
+ expect(validateMinValue(1, 1)).toBeUndefined();
+ expect(validateMinValue(10, 1)).toBeUndefined();
+ });
+});
+
+describe('validateCacheSize', () => {
+ it('returns undefined when cache is disabled', () => {
+ expect(validateCacheSize(0, false)).toBeUndefined();
+ expect(validateCacheSize(999999999999, false)).toBeUndefined();
+ });
+
+ it('returns error for 0 when enabled', () => {
+ const result = validateCacheSize(0, true);
+ expect(result).toBeTruthy();
+ });
+
+ it('returns undefined for a valid size when enabled', () => {
+ expect(validateCacheSize(1000, true)).toBeUndefined();
+ });
+
+ it('returns error for value exceeding UINT32_MAX', () => {
+ const result = validateCacheSize(4294967296, true);
+ expect(result).toBeTruthy();
+ });
+
+ it('returns undefined at UINT32_MAX boundary', () => {
+ expect(validateCacheSize(4294967295, true)).toBeUndefined();
+ });
+});
+
+describe('validateRewriteNotExists', () => {
+ it('returns undefined for a non-existing domain', () => {
+ const result = validateRewriteNotExists('new.example.com', [
+ { domain: 'existing.example.com' },
+ ]);
+ expect(result).toBeUndefined();
+ });
+
+ it('returns error for a domain that already exists', () => {
+ const result = validateRewriteNotExists('example.com', [{ domain: 'example.com' }]);
+ expect(result).toBeTruthy();
+ });
+
+ it('returns undefined when editing the same rewrite', () => {
+ const result = validateRewriteNotExists(
+ 'example.com',
+ [{ domain: 'example.com' }],
+ 'example.com',
+ );
+ expect(result).toBeUndefined();
+ });
+
+ it('returns error when editing and changing to an existing other domain', () => {
+ const result = validateRewriteNotExists(
+ 'other.example.com',
+ [{ domain: 'example.com' }, { domain: 'other.example.com' }],
+ 'example.com',
+ );
+ expect(result).toBeTruthy();
+ });
+
+ it('returns undefined for an empty domain', () => {
+ const result = validateRewriteNotExists('', [{ domain: 'example.com' }]);
+ expect(result).toBeUndefined();
+ });
+
+ it('case-insensitive duplicate check', () => {
+ const result = validateRewriteNotExists('Example.COM', [{ domain: 'example.com' }]);
+ expect(result).toBeTruthy();
+ });
+});
+
+describe('validateRewriteNotSame', () => {
+ it('returns undefined when domain and answer differ', () => {
+ const result = validateRewriteNotSame('example.com', '192.168.1.1');
+ expect(result).toBeUndefined();
+ });
+
+ it('returns error when domain equals answer', () => {
+ const result = validateRewriteNotSame('example.com', 'example.com');
+ expect(result).toBeTruthy();
+ });
+
+ it('returns error case-insensitively', () => {
+ const result = validateRewriteNotSame('Example.COM', 'example.com');
+ expect(result).toBeTruthy();
+ });
+
+ it('returns undefined for empty values', () => {
+ expect(validateRewriteNotSame('', 'example.com')).toBeUndefined();
+ expect(validateRewriteNotSame('example.com', '')).toBeUndefined();
+ expect(validateRewriteNotSame('', '')).toBeUndefined();
+ });
+});
+
+describe('validateUpstreams with bang comments', () => {
+ it('accepts lines starting with !', () => {
+ expect(validateUpstreams('! comment line\n8.8.8.8')).toBeUndefined();
+ });
+
+ it('accepts only comment lines (!)', () => {
+ expect(validateUpstreams('! first\n! second')).toBeUndefined();
+ });
+
+ it('still accepts # comments', () => {
+ expect(validateUpstreams('# comment\n8.8.8.8')).toBeUndefined();
+ });
+
+ it('rejects invalid non-comment lines alongside comments', () => {
+ expect(validateUpstreams('! good\ninvalidline')).toBeTruthy();
+ });
+});
+
+describe('validateLeaseTime', () => {
+ it('accepts 1 (minimum)', () => {
+ expect(validateLeaseTime(1)).toBeUndefined();
+ });
+
+ it('rejects 0', () => {
+ expect(validateLeaseTime(0)).toBeTruthy();
+ });
+
+ it('accepts UINT32_MAX (4294967295)', () => {
+ expect(validateLeaseTime(4294967295)).toBeUndefined();
+ });
+
+ it('rejects UINT32_MAX + 1 (4294967296)', () => {
+ expect(validateLeaseTime(4294967296)).toBeTruthy();
+ });
+
+ it('rejects empty string', () => {
+ expect(validateLeaseTime('')).toBeTruthy();
+ });
+
+ it('rejects undefined', () => {
+ expect(validateLeaseTime(undefined)).toBeTruthy();
+ });
+
+ it('rejects NaN', () => {
+ expect(validateLeaseTime(NaN)).toBeTruthy();
+ });
+});
diff --git a/client_v2/src/__tests__/common/ui/Banner.test.tsx b/client_v2/src/__tests__/common/ui/Banner.test.tsx
new file mode 100644
index 00000000000..25a4129459a
--- /dev/null
+++ b/client_v2/src/__tests__/common/ui/Banner.test.tsx
@@ -0,0 +1,74 @@
+import { render, screen } from '@solidjs/testing-library';
+import { describe, it, expect, vi } from 'vitest';
+import userEvent from '@testing-library/user-event';
+
+import { Banner } from 'panel/common/ui/Banner';
+
+describe('Banner', () => {
+ it('renders the message slot', () => {
+ render(() => );
+ expect(screen.getByText('Test message')).toBeInTheDocument();
+ });
+
+ it('renders the action slot when provided', () => {
+ render(() => (
+ Click me }
+ />
+ ));
+ expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
+ });
+
+ it('applies the correct variant class', () => {
+ const { container } = render(() => );
+ const banner = container.firstElementChild as HTMLElement;
+ expect(banner.className).toContain('warning');
+ });
+
+ it('sets role="alert" and aria-live="assertive" for critical variant', () => {
+ const { container } = render(() => );
+ const banner = container.firstElementChild as HTMLElement;
+ expect(banner.getAttribute('role')).toBe('alert');
+ expect(banner.getAttribute('aria-live')).toBe('assertive');
+ });
+
+ it('sets role="status" and aria-live="polite" for non-critical variants', () => {
+ const { container } = render(() => );
+ const banner = container.firstElementChild as HTMLElement;
+ expect(banner.getAttribute('role')).toBe('status');
+ expect(banner.getAttribute('aria-live')).toBe('polite');
+ });
+
+ it('forwards the data-testid prop', () => {
+ render(() => );
+ expect(screen.getByTestId('custom-banner')).toBeInTheDocument();
+ });
+
+ it('renders the close button when onClose is provided', () => {
+ const onClose = vi.fn();
+ render(() => (
+
+ ));
+ const closeButton = screen.getByTestId('test-banner-close');
+ expect(closeButton).toBeInTheDocument();
+ expect(closeButton.getAttribute('aria-label')).toBe('Close notification');
+ });
+
+ it('does not render the close button when onClose is not provided', () => {
+ render(() => );
+ expect(screen.queryByTestId('test-banner-close')).not.toBeInTheDocument();
+ });
+
+ it('calls onClose when the close button is clicked', async () => {
+ const user = userEvent.setup();
+ const onClose = vi.fn();
+ render(() => (
+
+ ));
+ const closeButton = screen.getByTestId('test-banner-close');
+ await user.click(closeButton);
+ expect(onClose).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/client_v2/src/__tests__/common/ui/Banners.test.tsx b/client_v2/src/__tests__/common/ui/Banners.test.tsx
new file mode 100644
index 00000000000..a4150a20656
--- /dev/null
+++ b/client_v2/src/__tests__/common/ui/Banners.test.tsx
@@ -0,0 +1,264 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen } from '@solidjs/testing-library';
+import userEvent from '@testing-library/user-event';
+import { HashRouter, Route } from '@solidjs/router';
+
+// We'll mock the stores at the module level and update them per test
+const mockDashboardState = {
+ isUpdateAvailable: false,
+ newVersion: '',
+ announcementUrl: '',
+ canAutoUpdate: false,
+ processingUpdate: false,
+ processingVersion: false,
+ dnsVersion: '',
+};
+
+const mockEncryptionState = {
+ enabled: false,
+ not_after: '',
+ valid_cert: false,
+};
+
+vi.mock('panel/stores/dashboard', () => ({
+ get dashboardState() {
+ return mockDashboardState;
+ },
+ getUpdate: vi.fn(),
+ getVersion: vi.fn(),
+}));
+
+vi.mock('panel/stores/encryption', () => ({
+ get encryptionState() {
+ return mockEncryptionState;
+ },
+}));
+
+vi.mock('panel/common/intl', () => {
+ const intl = {
+ getMessage: (key: string, values?: any) => {
+ const messages: Record = {
+ tls_certificate_expired: 'Your TLS certificate has expired',
+ tls_certificate_expiring: 'Your TLS certificate is about to expire',
+ update_available: `Version ${values?.version || ''} is available. Release notes`,
+ update_button: 'Update',
+ update_how_to: 'How to update',
+ version_number: `Version ${values?.value || ''}`,
+ check_updates_btn: 'Check for updates',
+ };
+ const msg = messages[key] || key;
+ if (values?.a) {
+ // When tag handlers are provided, return the full message
+ return `Version ${values.version} is available. Release notes`;
+ }
+ return msg;
+ },
+ getUILanguage: () => 'en',
+ changeLanguage: vi.fn(),
+ };
+ return { default: intl };
+});
+
+import { Banners } from 'panel/common/ui/Banners';
+import { BANNER_TEST_VALUES } from 'panel/helpers/banners';
+
+const renderBanners = (props?: { forceBanner?: any }) => {
+ return render(() => (
+
+ } />
+
+ ));
+};
+
+const resetStores = () => {
+ mockDashboardState.isUpdateAvailable = false;
+ mockDashboardState.newVersion = '';
+ mockDashboardState.announcementUrl = '';
+ mockDashboardState.canAutoUpdate = false;
+ mockDashboardState.processingUpdate = false;
+ mockDashboardState.processingVersion = false;
+ mockDashboardState.dnsVersion = '';
+ mockEncryptionState.enabled = false;
+ mockEncryptionState.not_after = '';
+ mockEncryptionState.valid_cert = false;
+};
+
+describe('Banners', () => {
+ beforeEach(() => {
+ resetStores();
+ });
+
+ // ── Priority logic cases ──
+
+ it('shows TLS expired banner when cert is expired', () => {
+ mockEncryptionState.enabled = true;
+ mockEncryptionState.valid_cert = true;
+ mockEncryptionState.not_after = new Date(Date.now() - 86400000).toISOString(); // 1 day ago
+
+ renderBanners();
+
+ expect(screen.getByTestId('banner-tls-expired')).toBeInTheDocument();
+ expect(screen.getByText('Your TLS certificate has expired')).toBeInTheDocument();
+ });
+
+ it('shows TLS expiring banner when cert expires within 30 days', () => {
+ mockEncryptionState.enabled = true;
+ mockEncryptionState.valid_cert = true;
+ mockEncryptionState.not_after = new Date(Date.now() + 15 * 86400000).toISOString(); // 15 days
+
+ renderBanners();
+
+ expect(screen.getByTestId('banner-tls-expiring')).toBeInTheDocument();
+ expect(screen.getByText('Your TLS certificate is about to expire')).toBeInTheDocument();
+ });
+
+ it('shows auto-update banner when update available and can auto-update', () => {
+ mockEncryptionState.enabled = true;
+ mockEncryptionState.valid_cert = true;
+ mockEncryptionState.not_after = new Date(Date.now() + 60 * 86400000).toISOString(); // 60 days
+ mockDashboardState.isUpdateAvailable = true;
+ mockDashboardState.newVersion = 'v1.0.0';
+ mockDashboardState.canAutoUpdate = true;
+
+ renderBanners();
+
+ expect(screen.getByTestId('banner-update-auto')).toBeInTheDocument();
+ });
+
+ it('shows manual-update banner when update available but cannot auto-update', () => {
+ mockEncryptionState.enabled = true;
+ mockEncryptionState.valid_cert = true;
+ mockEncryptionState.not_after = new Date(Date.now() + 60 * 86400000).toISOString();
+ mockDashboardState.isUpdateAvailable = true;
+ mockDashboardState.newVersion = 'v1.0.0';
+ mockDashboardState.canAutoUpdate = false;
+
+ renderBanners();
+
+ expect(screen.getByTestId('banner-update-manual')).toBeInTheDocument();
+ });
+
+ it('shows auto-update banner when TLS not enabled', () => {
+ mockDashboardState.isUpdateAvailable = true;
+ mockDashboardState.newVersion = 'v1.0.0';
+ mockDashboardState.canAutoUpdate = true;
+
+ renderBanners();
+
+ expect(screen.getByTestId('banner-update-auto')).toBeInTheDocument();
+ });
+
+ it('falls through to update when not_after is invalid (NaN)', () => {
+ mockEncryptionState.enabled = true;
+ mockEncryptionState.valid_cert = true;
+ mockEncryptionState.not_after = 'not-a-date';
+ mockDashboardState.isUpdateAvailable = true;
+ mockDashboardState.newVersion = 'v1.0.0';
+ mockDashboardState.canAutoUpdate = true;
+
+ renderBanners();
+
+ expect(screen.getByTestId('banner-update-auto')).toBeInTheDocument();
+ });
+
+ it('renders nothing when no conditions met', () => {
+ mockEncryptionState.enabled = true;
+ mockEncryptionState.valid_cert = true;
+ mockEncryptionState.not_after = new Date(Date.now() + 60 * 86400000).toISOString();
+
+ renderBanners();
+
+ expect(screen.queryByTestId('banner-root')).not.toBeInTheDocument();
+ });
+
+ it('TLS expired takes priority over update available', () => {
+ mockEncryptionState.enabled = true;
+ mockEncryptionState.valid_cert = true;
+ mockEncryptionState.not_after = new Date(Date.now() - 86400000).toISOString(); // expired
+ mockDashboardState.isUpdateAvailable = true;
+ mockDashboardState.newVersion = 'v1.0.0';
+ mockDashboardState.canAutoUpdate = true;
+
+ renderBanners();
+
+ // Only TLS expired banner should appear
+ expect(screen.getByTestId('banner-tls-expired')).toBeInTheDocument();
+ expect(screen.queryByTestId('banner-update-auto')).not.toBeInTheDocument();
+ });
+
+ // ── Dismiss cases ──
+
+ it('hides the banner after clicking the close button', async () => {
+ const user = userEvent.setup();
+ mockEncryptionState.enabled = true;
+ mockEncryptionState.valid_cert = true;
+ mockEncryptionState.not_after = new Date(Date.now() - 86400000).toISOString(); // expired
+
+ renderBanners();
+
+ expect(screen.getByTestId('banner-tls-expired')).toBeInTheDocument();
+ const closeButton = screen.getByTestId('banner-tls-expired-close');
+ await user.click(closeButton);
+
+ expect(screen.queryByTestId('banner-tls-expired')).not.toBeInTheDocument();
+ expect(screen.queryByTestId('banner-root')).not.toBeInTheDocument();
+ });
+
+ it('re-shows the banner when the underlying condition changes', async () => {
+ const user = userEvent.setup();
+ mockEncryptionState.enabled = true;
+ mockEncryptionState.valid_cert = true;
+ mockEncryptionState.not_after = new Date(Date.now() - 86400000).toISOString(); // expired
+
+ renderBanners();
+
+ // Dismiss the expired banner
+ const closeButton = screen.getByTestId('banner-tls-expired-close');
+ await user.click(closeButton);
+ expect(screen.queryByTestId('banner-tls-expired')).not.toBeInTheDocument();
+
+ // Change condition: now cert is valid but expiring soon
+ mockEncryptionState.not_after = new Date(Date.now() + 15 * 86400000).toISOString(); // 15 days
+ // Re-render
+ renderBanners();
+
+ // Should now show the expiring banner
+ expect(screen.getByTestId('banner-tls-expiring')).toBeInTheDocument();
+ expect(screen.queryByTestId('banner-tls-expired')).not.toBeInTheDocument();
+ });
+
+ // ── forceBanner (dev test override) ──
+
+ it('renders forced TLS expired banner regardless of store state', () => {
+ renderBanners({ forceBanner: BANNER_TEST_VALUES.tlsExpired });
+ expect(screen.getByTestId('banner-tls-expired')).toBeInTheDocument();
+ });
+
+ it('renders forced TLS expiring banner regardless of store state', () => {
+ renderBanners({ forceBanner: BANNER_TEST_VALUES.tlsExpiring });
+ expect(screen.getByTestId('banner-tls-expiring')).toBeInTheDocument();
+ });
+
+ it('renders forced auto-update banner', () => {
+ renderBanners({ forceBanner: BANNER_TEST_VALUES.updateAuto });
+ expect(screen.getByTestId('banner-update-auto')).toBeInTheDocument();
+ });
+
+ it('renders forced manual-update banner', () => {
+ renderBanners({ forceBanner: BANNER_TEST_VALUES.updateManual });
+ expect(screen.getByTestId('banner-update-manual')).toBeInTheDocument();
+ });
+
+ it('forceBanner overrides real banner conditions', () => {
+ // Set up a real TLS expired condition
+ mockEncryptionState.enabled = true;
+ mockEncryptionState.valid_cert = true;
+ mockEncryptionState.not_after = new Date(Date.now() - 86400000).toISOString();
+
+ // But force auto-update banner instead
+ renderBanners({ forceBanner: BANNER_TEST_VALUES.updateAuto });
+
+ expect(screen.getByTestId('banner-update-auto')).toBeInTheDocument();
+ expect(screen.queryByTestId('banner-tls-expired')).not.toBeInTheDocument();
+ });
+});
diff --git a/client_v2/src/__tests__/common/ui/ConfigDialog.test.tsx b/client_v2/src/__tests__/common/ui/ConfigDialog.test.tsx
new file mode 100644
index 00000000000..d616049d2e0
--- /dev/null
+++ b/client_v2/src/__tests__/common/ui/ConfigDialog.test.tsx
@@ -0,0 +1,119 @@
+import { describe, it, expect, vi } from 'vitest';
+import { render, fireEvent, screen } from '@solidjs/testing-library';
+
+const { themeMock } = vi.hoisted(() => {
+ const proxy: any = new Proxy(
+ {},
+ {
+ get: (_target, prop) => {
+ if (prop === Symbol.toPrimitive || prop === 'toString') {
+ return () => '';
+ }
+ return proxy;
+ },
+ },
+ );
+ return { themeMock: proxy };
+});
+
+vi.mock('panel/lib/theme', () => ({
+ default: themeMock,
+}));
+
+vi.mock('panel/common/intl', () => ({
+ default: {
+ getMessage: (key: string, _values?: Record) => key,
+ },
+}));
+
+import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
+
+describe('ConfigDialog', () => {
+ it('renders children when open', () => {
+ render(() => (
+
+ Body Content
+
+ ));
+ expect(screen.getByText('Body Content')).toBeDefined();
+ expect(screen.getByText('Test Dialog')).toBeDefined();
+ });
+
+ it('save disabled when processing is true', () => {
+ render(() => (
+
+ Content
+
+ ));
+ const saveButton = screen.getByTestId('config-dialog-save');
+ expect(saveButton).toBeDisabled();
+ });
+
+ it('save disabled when submitDisabled is true', () => {
+ render(() => (
+
+ Content
+
+ ));
+ const saveButton = screen.getByTestId('config-dialog-save');
+ expect(saveButton).toBeDisabled();
+ });
+
+ it('clicking save fires onSubmit', () => {
+ const onSubmit = vi.fn();
+ render(() => (
+
+ Content
+
+ ));
+ const saveButton = screen.getByTestId('config-dialog-save');
+ fireEvent.click(saveButton);
+ expect(onSubmit).toHaveBeenCalledTimes(1);
+ });
+
+ it('renders footer before save button', () => {
+ render(() => (
+ Secondary}
+ >
+ Content
+
+ ));
+ expect(screen.getByTestId('secondary-action')).toBeDefined();
+ // The footer div contains both secondary and save button
+ const footer = screen.getByTestId('secondary-action').parentElement;
+ expect(footer?.querySelector('[data-testid="config-dialog-save"]')).toBeDefined();
+ });
+
+ it('processing disables fieldset', () => {
+ render(() => (
+
+
+
+ ));
+ const fieldset = screen.getByTestId('test-input').closest('fieldset');
+ expect(fieldset).toBeDisabled();
+ });
+});
diff --git a/client_v2/src/__tests__/common/ui/Footer.test.tsx b/client_v2/src/__tests__/common/ui/Footer.test.tsx
new file mode 100644
index 00000000000..7776f01953d
--- /dev/null
+++ b/client_v2/src/__tests__/common/ui/Footer.test.tsx
@@ -0,0 +1,121 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen } from '@solidjs/testing-library';
+import userEvent from '@testing-library/user-event';
+
+const mockDashboardState = {
+ dnsVersion: '',
+ processingVersion: true,
+ theme: 'light',
+ language: 'en',
+ name: '',
+};
+
+vi.mock('panel/stores/dashboard', () => ({
+ get dashboardState() {
+ return mockDashboardState;
+ },
+ getVersion: vi.fn(),
+ changeTheme: vi.fn(),
+ changeLanguage: vi.fn(),
+}));
+
+vi.mock('panel/common/intl', () => {
+ const intl = {
+ getMessage: (key: string, values?: any) => {
+ const messages: Record = {
+ privacy_policy: 'Privacy Policy',
+ report_an_issue: 'Report an issue',
+ release_notes: 'Release notes',
+ system_theme: 'System',
+ dark_theme: 'Dark',
+ light_theme: 'Light',
+ version_number: `Version ${values?.value || ''}`,
+ check_updates_btn: 'Check for updates',
+ };
+ return messages[key] || key;
+ },
+ getUILanguage: () => 'en',
+ changeLanguage: vi.fn(),
+ };
+ return { default: intl };
+});
+
+vi.mock('panel/lib/theme', () => ({
+ default: {
+ link: { link: 'linkClass', noDecoration: 'noDecorationClass' },
+ dropdown: {
+ menu: 'dropdownMenu',
+ item: 'dropdownItem',
+ item_active: 'dropdownItemActive',
+ },
+ },
+}));
+
+import { Footer } from 'panel/common/ui/Footer';
+import { getVersion } from 'panel/stores/dashboard';
+
+describe('Footer', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockDashboardState.dnsVersion = '';
+ mockDashboardState.processingVersion = true;
+ });
+
+ it('hides version badge when dnsVersion is empty', () => {
+ mockDashboardState.dnsVersion = '';
+
+ render(() => );
+
+ expect(screen.queryByText(/Version/)).not.toBeInTheDocument();
+ });
+
+ it('shows version badge when dnsVersion is populated', () => {
+ mockDashboardState.dnsVersion = 'v1.0.0';
+
+ render(() => );
+
+ expect(screen.getByText('Version v1.0.0')).toBeInTheDocument();
+ });
+
+ it('disables the check-updates button while processingVersion is true', () => {
+ mockDashboardState.dnsVersion = 'v1.0.0';
+ mockDashboardState.processingVersion = true;
+
+ render(() => );
+
+ const button = screen.getByTestId('footer-check-updates');
+ expect(button).toBeDisabled();
+ });
+
+ it('enables the check-updates button when processingVersion is false', () => {
+ mockDashboardState.dnsVersion = 'v1.0.0';
+ mockDashboardState.processingVersion = false;
+
+ render(() => );
+
+ const button = screen.getByTestId('footer-check-updates');
+ expect(button).not.toBeDisabled();
+ });
+
+ it('calls getVersion(true) when check-updates button is clicked', async () => {
+ const user = userEvent.setup();
+ mockDashboardState.dnsVersion = 'v1.0.0';
+ mockDashboardState.processingVersion = false;
+
+ render(() => );
+
+ const button = screen.getByTestId('footer-check-updates');
+ await user.click(button);
+
+ expect(getVersion).toHaveBeenCalledWith(true);
+ });
+
+ it('has aria-label on the check-updates button', () => {
+ mockDashboardState.dnsVersion = 'v1.0.0';
+
+ render(() => );
+
+ const button = screen.getByTestId('footer-check-updates');
+ expect(button.getAttribute('aria-label')).toBe('Check for updates');
+ });
+});
diff --git a/client_v2/src/__tests__/components/Settings/SettingRow.test.tsx b/client_v2/src/__tests__/components/Settings/SettingRow.test.tsx
new file mode 100644
index 00000000000..1ec3ff929d1
--- /dev/null
+++ b/client_v2/src/__tests__/components/Settings/SettingRow.test.tsx
@@ -0,0 +1,214 @@
+import { describe, it, expect, vi } from 'vitest';
+import { render, fireEvent, screen } from '@solidjs/testing-library';
+
+const { themeMock } = vi.hoisted(() => {
+ const proxy: any = new Proxy(
+ {},
+ {
+ get: (_target, prop) => {
+ if (prop === Symbol.toPrimitive || prop === 'toString') {
+ return () => '';
+ }
+ return proxy;
+ },
+ },
+ );
+ return { themeMock: proxy };
+});
+
+vi.mock('panel/lib/theme', () => ({
+ default: themeMock,
+}));
+
+vi.mock('panel/common/intl', () => ({
+ default: {
+ getMessage: (key: string, _values?: Record) => key,
+ },
+}));
+
+import { SettingRow } from 'panel/common/ui/SettingRow';
+
+describe('SettingRow', () => {
+ describe('switch variant', () => {
+ it('renders title and description', () => {
+ render(() => (
+
+ ));
+ expect(screen.getByText('Test Title')).toBeDefined();
+ expect(screen.getByText('Test Description')).toBeDefined();
+ });
+
+ it('fires onChange when row is clicked', () => {
+ const onChange = vi.fn();
+ render(() => (
+
+ ));
+ const row = screen.getByRole('button');
+ fireEvent.click(row);
+ expect(onChange).toHaveBeenCalledTimes(1);
+ expect(onChange).toHaveBeenCalledWith(true);
+ });
+
+ it('fires onChange when switch is toggled off', () => {
+ const onChange = vi.fn();
+ render(() => (
+
+ ));
+ const row = screen.getByRole('button');
+ fireEvent.click(row);
+ expect(onChange).toHaveBeenCalledWith(false);
+ });
+
+ it('does not fire onChange when disabled', () => {
+ const onChange = vi.fn();
+ render(() => (
+
+ ));
+ const row = screen.getByRole('button');
+ fireEvent.click(row);
+ expect(onChange).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('link variant', () => {
+ it('renders value in semibold', () => {
+ render(() => (
+
+ ));
+ expect(screen.getByText('90 days · 3 ignored domains')).toBeDefined();
+ });
+
+ it('fires onClick when link is clicked', () => {
+ const onClick = vi.fn();
+ render(() => (
+
+ ));
+ const rows = screen.getAllByRole('button');
+ fireEvent.click(rows[0]);
+ expect(onClick).toHaveBeenCalledTimes(1);
+ });
+
+ it('does not fire onClick when disabled', () => {
+ const onClick = vi.fn();
+ render(() => (
+
+ ));
+ const rows = screen.getAllByRole('button');
+ fireEvent.click(rows[0]);
+ expect(onClick).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('switch-link variant', () => {
+ it('renders switch without arrow or configure label', () => {
+ render(() => (
+
+ ));
+ expect(screen.queryByText('settings_configure')).toBeNull();
+ expect(screen.getByText('Enabled · 4 providers')).toBeDefined();
+ });
+
+ it('row click fires onClick, not onChange', () => {
+ const onChange = vi.fn();
+ const onClick = vi.fn();
+ render(() => (
+
+ ));
+ const rows = screen.getAllByRole('button');
+ fireEvent.click(rows[0]);
+ expect(onClick).toHaveBeenCalledTimes(1);
+ expect(onChange).not.toHaveBeenCalled();
+ });
+
+ it('disabled suppresses switch', () => {
+ const onChange = vi.fn();
+ const onClick = vi.fn();
+ render(() => (
+
+ ));
+ const rows = screen.getAllByRole('button');
+ fireEvent.click(rows[0]);
+ expect(onChange).not.toHaveBeenCalled();
+ expect(onClick).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('children slot', () => {
+ it('renders children when provided', () => {
+ render(() => (
+
+ Child Content
+
+ ));
+ expect(screen.getByTestId('child-content')).toBeDefined();
+ expect(screen.getByText('Child Content')).toBeDefined();
+ });
+ });
+});
diff --git a/client_v2/src/__tests__/components/encryption-browse-suffix.test.tsx b/client_v2/src/__tests__/components/encryption-browse-suffix.test.tsx
new file mode 100644
index 00000000000..63b0c0794fc
--- /dev/null
+++ b/client_v2/src/__tests__/components/encryption-browse-suffix.test.tsx
@@ -0,0 +1,54 @@
+import { render, screen, waitFor } from '@solidjs/testing-library';
+import { describe, it, expect, vi } from 'vitest';
+import userEvent from '@testing-library/user-event';
+
+import { FileBrowseButton } from 'panel/components/Encryption/blocks/AddTlsCert/FileBrowseButton';
+
+describe('FileBrowseButton', () => {
+ it('renders a browse button', () => {
+ const onFileSelect = vi.fn();
+ render(() => );
+ expect(screen.getByRole('button')).toBeInTheDocument();
+ });
+
+ it('renders a hidden file input', () => {
+ const onFileSelect = vi.fn();
+ const { container } = render(() => );
+ const fileInput = container.querySelector('input[type="file"]');
+ expect(fileInput).toBeInTheDocument();
+ expect(fileInput?.getAttribute('aria-hidden')).toBe('true');
+ });
+
+ it('calls onFileSelect with file content when a file is selected', async () => {
+ const user = userEvent.setup();
+ const onFileSelect = vi.fn();
+ const { container } = render(() => );
+
+ const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement;
+ const file = new File(
+ ['-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----'],
+ 'cert.pem',
+ { type: 'application/x-pem-file' },
+ );
+
+ await user.upload(fileInput, file);
+
+ await waitFor(() => {
+ expect(onFileSelect).toHaveBeenCalledOnce();
+ });
+ expect(onFileSelect).toHaveBeenCalledWith(
+ '-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----',
+ );
+ });
+
+ it('does not call onFileSelect when no file is selected', async () => {
+ const user = userEvent.setup();
+ const onFileSelect = vi.fn();
+ const { container } = render(() => );
+
+ const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement;
+ await user.upload(fileInput, []);
+
+ expect(onFileSelect).not.toHaveBeenCalled();
+ });
+});
diff --git a/client_v2/src/__tests__/components/encryption-form.test.tsx b/client_v2/src/__tests__/components/encryption-form.test.tsx
new file mode 100644
index 00000000000..8cc64d28049
--- /dev/null
+++ b/client_v2/src/__tests__/components/encryption-form.test.tsx
@@ -0,0 +1,17 @@
+// DEPRECATED: Form.tsx is no longer rendered.
+// The new architecture tests are in:
+// - encryption-encrypted-dns-toggle.test.tsx
+// - encryption-plain-dns-toggle.test.tsx
+// - encryption-server-settings-modal.test.tsx
+// - encryption-add-tls-cert-modal.test.tsx
+// - encryption-helpers.test.ts
+// - encryption-validate.test.ts (kept, unchanged)
+// - encryption-store.test.ts (kept, unchanged)
+
+import { describe, it, expect } from 'vitest';
+
+describe('Encryption Form (deprecated)', () => {
+ it('placeholder test to keep the test suite running', () => {
+ expect(true).toBe(true);
+ });
+});
diff --git a/client_v2/src/__tests__/components/encryption-helpers.test.ts b/client_v2/src/__tests__/components/encryption-helpers.test.ts
new file mode 100644
index 00000000000..66b10ef4ef6
--- /dev/null
+++ b/client_v2/src/__tests__/components/encryption-helpers.test.ts
@@ -0,0 +1,55 @@
+import { describe, it, expect } from 'vitest';
+import { getSubmitValues, defaultTlsValues } from 'panel/components/Encryption/blocks/helpers';
+import { ENCRYPTION_SOURCE } from 'panel/helpers/constants';
+
+describe('getSubmitValues', () => {
+ it('empties certificate_chain when source is PATH', () => {
+ const result = getSubmitValues({
+ enabled: true,
+ certificate_chain: 'cert',
+ certificate_path: '/path/cert.pem',
+ certificate_source: ENCRYPTION_SOURCE.PATH,
+ key_source: ENCRYPTION_SOURCE.CONTENT,
+ private_key: 'key',
+ private_key_saved: false,
+ } as any);
+ expect(result.certificate_chain).toBe('');
+ expect(result.certificate_path).toBe('/path/cert.pem');
+ });
+
+ it('empties certificate_path when source is CONTENT', () => {
+ const result = getSubmitValues({
+ enabled: true,
+ certificate_chain: 'cert',
+ certificate_path: '/path/cert.pem',
+ certificate_source: ENCRYPTION_SOURCE.CONTENT,
+ key_source: ENCRYPTION_SOURCE.PATH,
+ private_key: 'key',
+ private_key_saved: false,
+ } as any);
+ expect(result.certificate_chain).toBe('cert');
+ expect(result.certificate_path).toBe('');
+ });
+
+ it('empties private_key when private_key_saved is true', () => {
+ const result = getSubmitValues({
+ enabled: true,
+ private_key: 'key',
+ private_key_path: '',
+ key_source: ENCRYPTION_SOURCE.SAVED,
+ private_key_saved: true,
+ } as any);
+ expect(result.private_key).toBe('');
+ expect(result.private_key_saved).toBe(true);
+ });
+
+ it('defaultTlsValues has all required fields', () => {
+ expect(defaultTlsValues.enabled).toBe(false);
+ expect(defaultTlsValues.serve_plain_dns).toBe(true);
+ expect(defaultTlsValues.port_https).toBe(443);
+ expect(defaultTlsValues.port_dns_over_tls).toBe(853);
+ expect(defaultTlsValues.port_dns_over_quic).toBe(853);
+ expect(defaultTlsValues.certificate_source).toBe(ENCRYPTION_SOURCE.PATH);
+ expect(defaultTlsValues.private_key_saved).toBe(false);
+ });
+});
diff --git a/client_v2/src/__tests__/components/encryption-validate.test.ts b/client_v2/src/__tests__/components/encryption-validate.test.ts
new file mode 100644
index 00000000000..a89633bf643
--- /dev/null
+++ b/client_v2/src/__tests__/components/encryption-validate.test.ts
@@ -0,0 +1,294 @@
+import { describe, it, expect } from 'vitest';
+
+import {
+ validateCertFields,
+ validateKeyFields,
+ validateCertKeyFields,
+ validateEncryptionForm,
+} from 'panel/components/Encryption/validate';
+import { ENCRYPTION_SOURCE } from 'panel/helpers/constants';
+
+const valid = {
+ enabled: true,
+ serve_plain_dns: true,
+ server_name: 'dns.example.com',
+ force_https: false,
+ port_https: 443,
+ port_dns_over_tls: 853,
+ port_dns_over_quic: 853,
+ certificate_chain: '-----BEGIN CERTIFICATE-----\nabc\n-----END CERTIFICATE-----',
+ private_key: '-----BEGIN PRIVATE KEY-----\nabc\n-----END PRIVATE KEY-----',
+ certificate_path: '',
+ private_key_path: '',
+ certificate_source: ENCRYPTION_SOURCE.CONTENT,
+ key_source: ENCRYPTION_SOURCE.CONTENT,
+ private_key_saved: false,
+};
+
+describe('validateEncryptionForm', () => {
+ it('returns no errors for a fully valid enabled form', () => {
+ expect(validateEncryptionForm(valid)).toEqual({});
+ });
+
+ it('requires certificate_chain when enabled and source is content', () => {
+ const errs = validateEncryptionForm({ ...valid, certificate_chain: '' });
+ expect(errs.certificate_chain).toBeTruthy();
+ });
+
+ it('requires certificate_path when enabled and source is path', () => {
+ const errs = validateEncryptionForm({
+ ...valid,
+ certificate_source: ENCRYPTION_SOURCE.PATH,
+ certificate_path: '',
+ certificate_chain: '',
+ });
+ expect(errs.certificate_path).toBeTruthy();
+ });
+
+ it('requires private_key unless private_key_saved is true', () => {
+ const errs = validateEncryptionForm({ ...valid, private_key: '' });
+ expect(errs.private_key).toBeTruthy();
+
+ const ok = validateEncryptionForm({
+ ...valid,
+ private_key: '',
+ private_key_saved: true,
+ });
+ expect(ok.private_key).toBeUndefined();
+ });
+
+ it('validates cert/key fields even when encryption is disabled', () => {
+ const errs = validateEncryptionForm({
+ ...valid,
+ enabled: false,
+ serve_plain_dns: true,
+ certificate_chain: '',
+ private_key: '',
+ });
+ expect(errs.certificate_chain).toBeTruthy();
+ expect(errs.private_key).toBeTruthy();
+ });
+
+ it('flags an out-of-range https port', () => {
+ const errs = validateEncryptionForm({ ...valid, port_https: 99999 });
+ expect(errs.port_https).toBeTruthy();
+ });
+
+ it('flags an unsafe https port', () => {
+ const errs = validateEncryptionForm({ ...valid, port_https: 22 });
+ expect(errs.port_https).toBeTruthy();
+ });
+
+ it('allows port 0 for DoT/DoQ (disabled)', () => {
+ const errs = validateEncryptionForm({
+ ...valid,
+ port_dns_over_tls: 0,
+ port_dns_over_quic: 0,
+ });
+ expect(errs.port_dns_over_tls).toBeUndefined();
+ expect(errs.port_dns_over_quic).toBeUndefined();
+ });
+
+ it('flags equal https and DoT ports', () => {
+ const errs = validateEncryptionForm({
+ ...valid,
+ port_https: 853,
+ port_dns_over_tls: 853,
+ });
+ expect(errs.port_https).toBeTruthy();
+ expect(errs.port_dns_over_tls).toBeTruthy();
+ });
+
+ it('allows empty-string ports (coerces to 0, valid as disabled)', () => {
+ // Before the store loads, ports may be empty strings.
+ // Number('') || 0 → 0, which is now valid (0 = disabled port).
+ const errs = validateEncryptionForm({
+ ...valid,
+ port_https: '' as any,
+ });
+ expect(errs.port_https).toBeUndefined();
+ });
+
+ it('requires plain DNS when encryption is disabled', () => {
+ const errs = validateEncryptionForm({
+ ...valid,
+ enabled: false,
+ serve_plain_dns: false,
+ certificate_chain: '',
+ private_key: '',
+ });
+ expect(errs.serve_plain_dns).toBeTruthy();
+ });
+
+ it('flags an invalid server name', () => {
+ const errs = validateEncryptionForm({
+ ...valid,
+ server_name: 'not valid!',
+ });
+ expect(errs.server_name).toBeTruthy();
+ });
+});
+
+describe('validateCertFields', () => {
+ it('returns no errors for a valid cert chain', () => {
+ expect(validateCertFields(valid)).toEqual({});
+ });
+
+ it('returns no errors for a valid cert path', () => {
+ const errs = validateCertFields({
+ ...valid,
+ certificate_source: ENCRYPTION_SOURCE.PATH,
+ certificate_path: '/etc/ssl/cert.pem',
+ certificate_chain: '',
+ });
+ expect(errs).toEqual({});
+ });
+
+ it('requires certificate_chain when source is CONTENT', () => {
+ const errs = validateCertFields({ ...valid, certificate_chain: '' });
+ expect(errs.certificate_chain).toBeTruthy();
+ });
+
+ it('requires certificate_path when source is PATH', () => {
+ const errs = validateCertFields({
+ ...valid,
+ certificate_source: ENCRYPTION_SOURCE.PATH,
+ certificate_path: '',
+ certificate_chain: '',
+ });
+ expect(errs.certificate_path).toBeTruthy();
+ });
+
+ it('validates cert fields even when encryption is disabled', () => {
+ const errs = validateCertFields({
+ ...valid,
+ enabled: false,
+ certificate_chain: '',
+ });
+ expect(errs.certificate_chain).toBeTruthy();
+ });
+});
+
+describe('validateKeyFields', () => {
+ it('returns no errors for a valid private key', () => {
+ expect(validateKeyFields(valid)).toEqual({});
+ });
+
+ it('returns no errors for a valid key path', () => {
+ const errs = validateKeyFields({
+ ...valid,
+ key_source: ENCRYPTION_SOURCE.PATH,
+ private_key_path: '/etc/ssl/key.pem',
+ private_key: '',
+ });
+ expect(errs).toEqual({});
+ });
+
+ it('requires private_key when source is CONTENT', () => {
+ const errs = validateKeyFields({ ...valid, private_key: '' });
+ expect(errs.private_key).toBeTruthy();
+ });
+
+ it('requires private_key_path when source is PATH', () => {
+ const errs = validateKeyFields({
+ ...valid,
+ key_source: ENCRYPTION_SOURCE.PATH,
+ private_key_path: '',
+ private_key: '',
+ });
+ expect(errs.private_key_path).toBeTruthy();
+ });
+
+ it('returns no errors when private_key_saved is true', () => {
+ const errs = validateKeyFields({
+ ...valid,
+ private_key: '',
+ private_key_saved: true,
+ });
+ expect(errs).toEqual({});
+ });
+
+ it('validates key fields even when encryption is disabled', () => {
+ const errs = validateKeyFields({
+ ...valid,
+ enabled: false,
+ private_key: '',
+ });
+ expect(errs.private_key).toBeTruthy();
+ });
+
+ it('rejects non-PEM cert content', () => {
+ const errs = validateCertFields({
+ ...valid,
+ certificate_chain: 'not a pem certificate',
+ });
+ expect(errs.certificate_chain).toBeTruthy();
+ });
+
+ it('rejects non-PEM key content', () => {
+ const errs = validateKeyFields({
+ ...valid,
+ private_key: 'not a pem key',
+ });
+ expect(errs.private_key).toBeTruthy();
+ });
+
+ it('rejects invalid cert path', () => {
+ const errs = validateCertFields({
+ ...valid,
+ certificate_source: ENCRYPTION_SOURCE.PATH,
+ certificate_path: 'relative/path/no/leading/slash',
+ certificate_chain: '',
+ });
+ expect(errs.certificate_path).toBeTruthy();
+ });
+
+ it('rejects invalid key path', () => {
+ const errs = validateKeyFields({
+ ...valid,
+ key_source: ENCRYPTION_SOURCE.PATH,
+ private_key_path: 'relative/path/no/leading/slash',
+ private_key: '',
+ });
+ expect(errs.private_key_path).toBeTruthy();
+ });
+});
+
+describe('validateCertKeyFields', () => {
+ it('returns no errors when both cert and key are valid', () => {
+ expect(validateCertKeyFields(valid)).toEqual({});
+ });
+
+ it('returns only cert errors when cert is invalid but key is valid', () => {
+ const errs = validateCertKeyFields({ ...valid, certificate_chain: '' });
+ expect(errs.certificate_chain).toBeTruthy();
+ expect(errs.private_key).toBeUndefined();
+ });
+
+ it('returns only key errors when key is invalid but cert is valid', () => {
+ const errs = validateCertKeyFields({ ...valid, private_key: '' });
+ expect(errs.certificate_chain).toBeUndefined();
+ expect(errs.private_key).toBeTruthy();
+ });
+
+ it('returns both cert and key errors when both are invalid', () => {
+ const errs = validateCertKeyFields({
+ ...valid,
+ certificate_chain: '',
+ private_key: '',
+ });
+ expect(errs.certificate_chain).toBeTruthy();
+ expect(errs.private_key).toBeTruthy();
+ });
+
+ it('validates cert/key fields even when encryption is disabled', () => {
+ const errs = validateCertKeyFields({
+ ...valid,
+ enabled: false,
+ certificate_chain: '',
+ private_key: '',
+ });
+ expect(errs.certificate_chain).toBeTruthy();
+ expect(errs.private_key).toBeTruthy();
+ });
+});
diff --git a/client_v2/src/__tests__/copied-text.test.tsx b/client_v2/src/__tests__/copied-text.test.tsx
new file mode 100644
index 00000000000..3cf6bd0801a
--- /dev/null
+++ b/client_v2/src/__tests__/copied-text.test.tsx
@@ -0,0 +1,59 @@
+import { render, screen } from '@solidjs/testing-library';
+import userEvent from '@testing-library/user-event';
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+
+// Mock copy-to-clipboard to avoid jsdom limitation (window.prompt not implemented)
+vi.mock('copy-to-clipboard', () => ({
+ default: vi.fn(() => true),
+}));
+
+import { CopiedText } from 'panel/common/ui/CopiedText/CopiedText';
+
+describe('CopiedText', () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ it('renders the copy button and text', () => {
+ render(() => );
+ expect(screen.getByText('192.168.1.1:8080')).toBeInTheDocument();
+ const copyButtons = screen.getAllByRole('button');
+ expect(copyButtons.length).toBeGreaterThan(0);
+ });
+
+ it('resets copied state after 2000ms (bug B1 fix verification)', async () => {
+ const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
+ render(() => );
+
+ const copyButtons = screen.getAllByRole('button');
+ await user.click(copyButtons[0]);
+
+ expect(screen.getByText('test-value')).toBeInTheDocument();
+
+ // Before the B1 fix, createSignal(() => {...}) stored the function
+ // as the signal value and the timer never executed.
+ await vi.advanceTimersByTimeAsync(2000);
+
+ expect(screen.getByText('test-value')).toBeInTheDocument();
+ });
+
+ it('calls onCopy callback when text is copied', async () => {
+ const onCopy = vi.fn();
+ const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
+ render(() => );
+
+ const copyButtons = screen.getAllByRole('button');
+ await user.click(copyButtons[0]);
+
+ expect(onCopy).toHaveBeenCalledWith('copy-me');
+ });
+
+ it('cleans up timer on unmount without errors', () => {
+ const { unmount } = render(() => );
+ expect(() => unmount()).not.toThrow();
+ });
+});
diff --git a/client_v2/src/__tests__/dashboard-empty-state.test.tsx b/client_v2/src/__tests__/dashboard-empty-state.test.tsx
new file mode 100644
index 00000000000..f99c52bd23a
--- /dev/null
+++ b/client_v2/src/__tests__/dashboard-empty-state.test.tsx
@@ -0,0 +1,37 @@
+import { render, screen } from '@solidjs/testing-library';
+import { describe, it, expect } from 'vitest';
+import { HashRouter, Route } from '@solidjs/router';
+import type { Component } from 'solid-js';
+
+import { EmptyState } from 'panel/components/Dashboard/blocks/EmptyState/EmptyState';
+
+const renderWithRouter = (component: Component) => {
+ return render(() => (
+
+
+
+ ));
+};
+
+describe('Dashboard EmptyState', () => {
+ it('renders no_stats_yet in default mode', () => {
+ renderWithRouter(() => );
+ expect(screen.getByTestId('dashboard-empty-state')).toBeInTheDocument();
+ expect(screen.getByText(/No stats yet/)).toBeInTheDocument();
+ });
+
+ it('renders period_notify in disabled mode with settings link', () => {
+ renderWithRouter(() => );
+ expect(screen.getByTestId('dashboard-empty-state')).toBeInTheDocument();
+ // period_notify: "To change the displayed periods, edit Statistics retention in General settings"
+ expect(screen.getByText(/Statistics retention/)).toBeInTheDocument();
+ // The "General settings" text is rendered inside a Link
+ // Note: period_notify uses — the link name is "General\u00a0settings"
+ expect(screen.getByRole('link', { name: /General/ })).toBeInTheDocument();
+ });
+
+ it('defaults to default mode when no mode prop provided', () => {
+ renderWithRouter(() => );
+ expect(screen.getByText(/No stats yet/)).toBeInTheDocument();
+ });
+});
diff --git a/client_v2/src/__tests__/dashboard-store.test.ts b/client_v2/src/__tests__/dashboard-store.test.ts
new file mode 100644
index 00000000000..48cad9daac3
--- /dev/null
+++ b/client_v2/src/__tests__/dashboard-store.test.ts
@@ -0,0 +1,59 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+
+const mocks = vi.hoisted(() => ({
+ getTlsStatus: vi.fn(),
+ getGlobalVersion: vi.fn(),
+ getProfile: vi.fn(),
+ addErrorToast: vi.fn(),
+ addSuccessToast: vi.fn(),
+ addNoticeToast: vi.fn(),
+}));
+
+vi.mock('panel/api/Api', () => ({
+ apiClient: {
+ baseUrl: 'http://x',
+ getGlobalVersion: mocks.getGlobalVersion,
+ getProfile: mocks.getProfile,
+ getUpdate: vi.fn(),
+ getTlsStatus: mocks.getTlsStatus,
+ },
+}));
+vi.mock('panel/stores/toasts', () => ({
+ addErrorToast: mocks.addErrorToast,
+ addSuccessToast: mocks.addSuccessToast,
+ addNoticeToast: mocks.addNoticeToast,
+}));
+
+import { getDnsStatus } from 'panel/stores/dashboard';
+
+describe('getDnsStatus', () => {
+ beforeEach(() => vi.clearAllMocks());
+
+ it('fetches TLS status when the core is running (FR-005)', async () => {
+ mocks.getGlobalVersion.mockResolvedValue({
+ disabled: true,
+ new_version: 'x',
+ });
+ mocks.getProfile.mockResolvedValue({ name: 'n', theme: 't' });
+ mocks.getTlsStatus.mockResolvedValue({ enabled: false });
+
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue(
+ new Response(
+ JSON.stringify({
+ running: true,
+ version: 'v',
+ dns_port: 53,
+ dns_addresses: [],
+ protection_enabled: true,
+ http_port: 80,
+ }),
+ { status: 200 },
+ ),
+ );
+
+ await getDnsStatus();
+ // allow microtasks for chained getVersion/getTlsStatus/getProfile
+ await new Promise((r) => setTimeout(r, 0));
+ expect(mocks.getTlsStatus).toHaveBeenCalled();
+ });
+});
diff --git a/client_v2/src/__tests__/dhcp-store.test.ts b/client_v2/src/__tests__/dhcp-store.test.ts
new file mode 100644
index 00000000000..319421d1e71
--- /dev/null
+++ b/client_v2/src/__tests__/dhcp-store.test.ts
@@ -0,0 +1,104 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+
+const mocks = vi.hoisted(() => ({
+ findActiveDhcp: vi.fn(),
+ getDhcpInterfaces: vi.fn(),
+ getDhcpStatus: vi.fn(),
+ setDhcpConfig: vi.fn(),
+ addErrorToast: vi.fn(),
+ addSuccessToast: vi.fn(),
+}));
+
+vi.mock('panel/api/Api', () => ({
+ apiClient: {
+ findActiveDhcp: mocks.findActiveDhcp,
+ getDhcpInterfaces: mocks.getDhcpInterfaces,
+ getDhcpStatus: mocks.getDhcpStatus,
+ getGlobalStatus: vi.fn(),
+ setDhcpConfig: mocks.setDhcpConfig,
+ },
+}));
+vi.mock('panel/stores/toasts', () => ({
+ addErrorToast: mocks.addErrorToast,
+ addSuccessToast: mocks.addSuccessToast,
+}));
+
+import { findActiveDhcp, getDhcpInterfaces, setDhcpConfig, toggleDhcp } from 'panel/stores/dhcp';
+
+describe('findActiveDhcp', () => {
+ beforeEach(() => vi.clearAllMocks());
+
+ it('passes { interface } not a bare string', async () => {
+ mocks.findActiveDhcp.mockResolvedValue({
+ v4: { other_server: { found: 'yes' }, static_ip: { static: 'yes' } },
+ v6: { other_server: {} },
+ });
+ await findActiveDhcp('eth0');
+ expect(mocks.findActiveDhcp).toHaveBeenCalledWith({ interface: 'eth0' });
+ });
+
+ it('shows dhcp_found error with retry action when another DHCP server detected', async () => {
+ mocks.getDhcpInterfaces.mockResolvedValue({
+ eth0: { ipv4_addresses: ['1.1.1.1'], ipv6_addresses: [] },
+ });
+ mocks.findActiveDhcp.mockResolvedValue({
+ v4: {
+ other_server: { found: 'yes' },
+ static_ip: { static: 'yes', ip: 'x' },
+ },
+ v6: { other_server: {} },
+ });
+ await getDhcpInterfaces();
+ await findActiveDhcp('eth0');
+ expect(mocks.addErrorToast).toHaveBeenCalledWith(
+ expect.objectContaining({
+ action: expect.objectContaining({ text: expect.any(String) }),
+ }),
+ );
+ });
+
+ it('shows dhcp_not_found success toast when clean', async () => {
+ mocks.getDhcpInterfaces.mockResolvedValue({
+ eth0: { ipv4_addresses: ['1.1.1.1'], ipv6_addresses: [] },
+ });
+ mocks.findActiveDhcp.mockResolvedValue({
+ v4: {
+ other_server: { found: 'no' },
+ static_ip: { static: 'yes', ip: '1.1.1.1' },
+ },
+ v6: { other_server: { found: 'no' } },
+ });
+ await getDhcpInterfaces();
+ await findActiveDhcp('eth0');
+ expect(mocks.addSuccessToast).toHaveBeenCalled();
+ });
+});
+
+describe('setDhcpConfig', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mocks.setDhcpConfig.mockResolvedValue(undefined);
+ });
+
+ it('shows dhcp_config_saved toast (FR-010)', async () => {
+ await setDhcpConfig({
+ v4: { range_start: 'a', range_end: 'b' },
+ interface_name: 'eth0',
+ });
+ expect(mocks.addSuccessToast).toHaveBeenCalled();
+ });
+});
+
+describe('toggleDhcp', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mocks.setDhcpConfig.mockResolvedValue(undefined);
+ });
+
+ it('computes enabled from passed config, not current state (FR-011)', async () => {
+ await toggleDhcp({ enabled: false, interface_name: 'eth0' });
+ expect(mocks.setDhcpConfig).toHaveBeenCalledWith(
+ expect.objectContaining({ enabled: true }),
+ );
+ });
+});
diff --git a/client_v2/src/__tests__/dhcp-toggle.test.tsx b/client_v2/src/__tests__/dhcp-toggle.test.tsx
new file mode 100644
index 00000000000..1dfaba18dac
--- /dev/null
+++ b/client_v2/src/__tests__/dhcp-toggle.test.tsx
@@ -0,0 +1,87 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, fireEvent } from '@solidjs/testing-library';
+import { DhcpToggle } from 'panel/components/Dhcp/blocks/DhcpToggle';
+
+const mocks = vi.hoisted(() => ({
+ toggleDhcp: vi.fn(),
+ dhcpState: {
+ enabled: false,
+ interface_name: '',
+ processingDhcp: false,
+ processingConfig: false,
+ },
+}));
+
+vi.mock('panel/stores/dhcp', () => ({
+ get dhcpState() {
+ return mocks.dhcpState;
+ },
+ toggleDhcp: mocks.toggleDhcp,
+}));
+
+describe('DhcpToggle', () => {
+ beforeEach(() => vi.clearAllMocks());
+
+ it('renders with current enabled state', () => {
+ mocks.dhcpState = {
+ enabled: true,
+ interface_name: 'eth0',
+ processingDhcp: false,
+ processingConfig: false,
+ };
+ const { container } = render(() => 'eth0'} />);
+ const input = container.querySelector('#dhcp_enabled') as HTMLInputElement;
+ expect(input.checked).toBe(true);
+ });
+
+ it('calls toggleDhcp with enabled:false + interface_name when toggled ON + calls onToggleOn', () => {
+ mocks.dhcpState = {
+ enabled: false,
+ interface_name: 'eth0',
+ processingDhcp: false,
+ processingConfig: false,
+ };
+ const onToggleOn = vi.fn();
+ const { container } = render(() => (
+ 'eth0'} onToggleOn={onToggleOn} />
+ ));
+ const input = container.querySelector('#dhcp_enabled') as HTMLInputElement;
+ fireEvent.change(input, { target: { checked: true } });
+ expect(mocks.toggleDhcp).toHaveBeenCalledWith(
+ expect.objectContaining({
+ enabled: false,
+ interface_name: 'eth0',
+ }),
+ );
+ expect(onToggleOn).toHaveBeenCalledOnce();
+ });
+
+ it('calls toggleDhcp with enabled:true when toggled OFF, does NOT call onToggleOn', () => {
+ mocks.dhcpState = {
+ enabled: true,
+ interface_name: 'eth0',
+ processingDhcp: false,
+ processingConfig: false,
+ };
+ const onToggleOn = vi.fn();
+ const { container } = render(() => (
+ 'eth0'} onToggleOn={onToggleOn} />
+ ));
+ const input = container.querySelector('#dhcp_enabled') as HTMLInputElement;
+ fireEvent.change(input, { target: { checked: false } });
+ expect(mocks.toggleDhcp).toHaveBeenCalledWith({ enabled: true });
+ expect(onToggleOn).not.toHaveBeenCalled();
+ });
+
+ it('is disabled when processing or no interface', () => {
+ mocks.dhcpState = {
+ enabled: false,
+ interface_name: '',
+ processingConfig: true,
+ processingDhcp: false,
+ };
+ const { container } = render(() => ''} />);
+ const input = container.querySelector('#dhcp_enabled') as HTMLInputElement;
+ expect(input.disabled).toBe(true);
+ });
+});
diff --git a/client_v2/src/__tests__/dialog.test.tsx b/client_v2/src/__tests__/dialog.test.tsx
new file mode 100644
index 00000000000..17c007fe86f
--- /dev/null
+++ b/client_v2/src/__tests__/dialog.test.tsx
@@ -0,0 +1,45 @@
+import { render, screen } from '@solidjs/testing-library';
+import userEvent from '@testing-library/user-event';
+import { describe, it, expect, vi } from 'vitest';
+import { Dialog } from 'panel/common/ui/Dialog/Dialog';
+
+describe('Dialog', () => {
+ it('renders when visible', () => {
+ render(() => (
+ {}} title="Test Dialog">
+ Body content
+
+ ));
+ expect(screen.getByText('Test Dialog')).toBeInTheDocument();
+ expect(screen.getByText('Body content')).toBeInTheDocument();
+ });
+
+ it('does not render when not visible', () => {
+ render(() => (
+ {}} title="Hidden">
+ Should not appear
+
+ ));
+ // Ark UI uses Presence — content is in DOM but hidden
+ const title = screen.queryByText('Hidden');
+ expect(title?.closest('[data-state="closed"]')).toBeTruthy();
+ });
+
+ it('calls onClose when close button clicked', async () => {
+ const onClose = vi.fn();
+ render(() => );
+ const closeButton = screen.getByRole('button');
+ await userEvent.click(closeButton);
+ expect(onClose).toHaveBeenCalled();
+ });
+
+ it('renders without mask when mask=false', () => {
+ render(() => {}} title="No Mask" />);
+ expect(document.querySelector('[data-part="backdrop"]')).not.toBeInTheDocument();
+ });
+
+ it('renders mask by default', () => {
+ render(() => {}} title="With Mask" />);
+ expect(document.querySelector('[data-part="backdrop"]')).toBeInTheDocument();
+ });
+});
diff --git a/client_v2/src/__tests__/dns-config-store.test.ts b/client_v2/src/__tests__/dns-config-store.test.ts
new file mode 100644
index 00000000000..cdcf9614bd0
--- /dev/null
+++ b/client_v2/src/__tests__/dns-config-store.test.ts
@@ -0,0 +1,66 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+
+const mocks = vi.hoisted(() => ({
+ getDnsConfig: vi.fn(),
+ setDnsConfig: vi.fn(),
+ addErrorToast: vi.fn(),
+ addSuccessToast: vi.fn(),
+}));
+
+vi.mock('panel/api/Api', () => ({
+ apiClient: {
+ getDnsConfig: mocks.getDnsConfig,
+ setDnsConfig: mocks.setDnsConfig,
+ },
+}));
+vi.mock('panel/stores/toasts', () => ({
+ addErrorToast: mocks.addErrorToast,
+ addSuccessToast: mocks.addSuccessToast,
+}));
+vi.mock('panel/common/intl', () => ({
+ default: { getMessage: vi.fn((key: string) => key) },
+}));
+
+import { getDnsConfig, dnsConfigState, toggleResolveClients } from 'panel/stores/dnsConfig';
+
+describe('getDnsConfig', () => {
+ beforeEach(() => vi.clearAllMocks());
+
+ it('defaults null blocking IPs and empty upstream_mode (FR-012)', async () => {
+ mocks.getDnsConfig.mockResolvedValue({
+ blocking_ipv4: null,
+ blocking_ipv6: null,
+ upstream_mode: '',
+ });
+ await getDnsConfig();
+ expect(dnsConfigState.blocking_ipv4).toBe('0.0.0.0');
+ expect(dnsConfigState.blocking_ipv6).toBe('::');
+ expect(dnsConfigState.upstream_mode).toBe('load_balance');
+ });
+});
+
+describe('toggleResolveClients', () => {
+ beforeEach(() => vi.clearAllMocks());
+
+ it('toggles resolve_clients and persists (FR-032)', async () => {
+ mocks.setDnsConfig.mockResolvedValue({});
+
+ const before = dnsConfigState.resolve_clients;
+ await toggleResolveClients();
+ expect(dnsConfigState.resolve_clients).toBe(!before);
+
+ // Toggle back
+ await toggleResolveClients();
+ expect(dnsConfigState.resolve_clients).toBe(before);
+ });
+
+ it('calls apiClient.setDnsConfig with inverted value (FR-032)', async () => {
+ mocks.setDnsConfig.mockResolvedValue({});
+
+ const before = dnsConfigState.resolve_clients;
+ await toggleResolveClients();
+ expect(mocks.setDnsConfig).toHaveBeenCalledWith({
+ resolve_clients: !before,
+ });
+ });
+});
diff --git a/client_v2/src/__tests__/dynamic-leases-tab.test.tsx b/client_v2/src/__tests__/dynamic-leases-tab.test.tsx
new file mode 100644
index 00000000000..92fc50aa4b9
--- /dev/null
+++ b/client_v2/src/__tests__/dynamic-leases-tab.test.tsx
@@ -0,0 +1,139 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, fireEvent, screen } from '@solidjs/testing-library';
+
+const MOCK_LEASE = { mac: 'AA:BB:CC:DD:EE:FF', ip: '192.168.1.101', hostname: 'dyn1' };
+
+const mocks = vi.hoisted(() => {
+ const lease = { mac: 'AA:BB:CC:DD:EE:FF', ip: '192.168.1.101', hostname: 'dyn1' };
+ return {
+ dhcpState: {
+ leases: [lease],
+ processingUpdating: false,
+ processingDeleting: false,
+ processingConfig: false,
+ },
+ removeStaticLease: vi.fn(),
+ toggleLeaseModal: vi.fn(),
+ getDhcpStatus: vi.fn(),
+ };
+});
+
+vi.mock('panel/stores/dhcp', () => ({
+ get dhcpState() {
+ return mocks.dhcpState;
+ },
+ removeStaticLease: mocks.removeStaticLease,
+ toggleLeaseModal: mocks.toggleLeaseModal,
+ getDhcpStatus: mocks.getDhcpStatus,
+}));
+
+vi.mock('panel/common/intl', () => ({
+ default: {
+ getMessage: (key: string, values?: Record) =>
+ values?.ip !== undefined ? `${key}:${values.ip}` : key,
+ },
+}));
+
+import { DynamicLeasesTab } from 'panel/components/Dhcp/LeasesPage/DynamicLeasesTab';
+
+describe('DynamicLeasesTab', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ // Reset state for each test
+ mocks.dhcpState = {
+ leases: [MOCK_LEASE],
+ processingUpdating: false,
+ processingDeleting: false,
+ processingConfig: false,
+ };
+ });
+
+ it('renders DynamicLeasesTable when leases exist', () => {
+ render(() => );
+
+ expect(screen.getByTestId('dynamic-lease-edit-button')).toBeInTheDocument();
+ expect(screen.getByTestId('dynamic-lease-make-static-button')).toBeInTheDocument();
+ expect(screen.getByTestId('dynamic-lease-refresh-button')).toBeInTheDocument();
+ expect(screen.getByTestId('dynamic-lease-delete-button')).toBeInTheDocument();
+ });
+
+ it('does not render DynamicLeasesTable when leases array is empty', () => {
+ mocks.dhcpState = { ...mocks.dhcpState, leases: [] };
+ render(() => );
+
+ expect(screen.queryByTestId('dynamic-lease-edit-button')).not.toBeInTheDocument();
+ expect(screen.queryByTestId('dynamic-lease-delete-button')).not.toBeInTheDocument();
+ });
+
+ it('does not render DynamicLeasesTable when leases is undefined', () => {
+ mocks.dhcpState = {
+ ...mocks.dhcpState,
+ leases: undefined as unknown as typeof mocks.dhcpState.leases,
+ };
+ render(() => );
+
+ expect(screen.queryByTestId('dynamic-lease-edit-button')).not.toBeInTheDocument();
+ });
+
+ it('calls toggleLeaseModal with ADD_LEASE when edit button is clicked', () => {
+ render(() => );
+
+ fireEvent.click(screen.getByTestId('dynamic-lease-edit-button'));
+ expect(mocks.toggleLeaseModal).toHaveBeenCalledWith('ADD_LEASE', MOCK_LEASE);
+ });
+
+ it('calls toggleLeaseModal with MAKE_STATIC when make-static button is clicked', () => {
+ render(() => );
+
+ fireEvent.click(screen.getByTestId('dynamic-lease-make-static-button'));
+ expect(mocks.toggleLeaseModal).toHaveBeenCalledWith('MAKE_STATIC', MOCK_LEASE);
+ });
+
+ it('calls getDhcpStatus when refresh button is clicked', () => {
+ render(() => );
+
+ fireEvent.click(screen.getByTestId('dynamic-lease-refresh-button'));
+ expect(mocks.getDhcpStatus).toHaveBeenCalledTimes(1);
+ });
+
+ it('shows ConfirmDialog when delete button is clicked', () => {
+ render(() => );
+
+ fireEvent.click(screen.getByTestId('dynamic-lease-delete-button'));
+
+ // ConfirmDialog should appear with the title and description
+ expect(screen.getByText('delete_confirm')).toBeInTheDocument();
+ expect(screen.getByText('delete_confirm_desc:192.168.1.101')).toBeInTheDocument();
+ });
+
+ it('calls removeStaticLease when delete is confirmed', () => {
+ render(() => );
+
+ // Click delete to open the confirmation dialog
+ fireEvent.click(screen.getByTestId('dynamic-lease-delete-button'));
+
+ // Click the confirm button
+ const confirmButton = screen.getByText('delete_table_action_confirm');
+ fireEvent.click(confirmButton);
+
+ expect(mocks.removeStaticLease).toHaveBeenCalledWith(MOCK_LEASE);
+ });
+
+ it('closes ConfirmDialog when cancel is clicked', () => {
+ render(() => );
+
+ // Click delete to open the confirmation dialog
+ fireEvent.click(screen.getByTestId('dynamic-lease-delete-button'));
+
+ // Verify dialog is visible
+ expect(screen.getByText('delete_confirm')).toBeInTheDocument();
+
+ // Click cancel
+ const cancelButton = screen.getByText('cancel');
+ fireEvent.click(cancelButton);
+
+ // Dialog should close — confirm that removeStaticLease was NOT called
+ expect(mocks.removeStaticLease).not.toHaveBeenCalled();
+ expect(screen.queryByText('delete_confirm')).not.toBeInTheDocument();
+ });
+});
diff --git a/client_v2/src/__tests__/dynamic-leases-table.test.tsx b/client_v2/src/__tests__/dynamic-leases-table.test.tsx
new file mode 100644
index 00000000000..cdbc7fcef3d
--- /dev/null
+++ b/client_v2/src/__tests__/dynamic-leases-table.test.tsx
@@ -0,0 +1,78 @@
+import { describe, it, expect, vi } from 'vitest';
+import { render, fireEvent } from '@solidjs/testing-library';
+import { DynamicLeasesTable } from 'panel/components/Dhcp/LeasesPage/DynamicLeasesTable';
+
+const LEASES = [{ mac: 'AA:BB:CC:DD:EE:FF', ip: '192.168.1.101', hostname: 'dyn1' }];
+
+describe('DynamicLeasesTable', () => {
+ it('renders mobile action buttons for all four actions', () => {
+ const { getByTestId } = render(() => (
+ {}}
+ onDelete={() => {}}
+ onMakeStatic={() => {}}
+ onRefresh={() => {}}
+ />
+ ));
+
+ expect(getByTestId('dynamic-lease-edit-button')).toBeInTheDocument();
+ expect(getByTestId('dynamic-lease-make-static-button')).toBeInTheDocument();
+ expect(getByTestId('dynamic-lease-refresh-button')).toBeInTheDocument();
+ expect(getByTestId('dynamic-lease-delete-button')).toBeInTheDocument();
+ });
+
+ it('renders desktop Dropdown trigger', () => {
+ const { getByTestId } = render(() => (
+ {}}
+ onDelete={() => {}}
+ onMakeStatic={() => {}}
+ onRefresh={() => {}}
+ />
+ ));
+
+ expect(getByTestId('dynamic-lease-actions-dropdown')).toBeInTheDocument();
+ });
+
+ it('fires onEdit when mobile edit button clicked', () => {
+ const onEdit = vi.fn();
+ const { getByTestId } = render(() => (
+ {}}
+ onMakeStatic={() => {}}
+ onRefresh={() => {}}
+ />
+ ));
+
+ fireEvent.click(getByTestId('dynamic-lease-edit-button'));
+ expect(onEdit).toHaveBeenCalledWith(LEASES[0]);
+ });
+
+ it('fires onMakeStatic when mobile make-static button clicked', () => {
+ const onMakeStatic = vi.fn();
+ const { getByTestId } = render(() => (
+ {}}
+ onDelete={() => {}}
+ onMakeStatic={onMakeStatic}
+ onRefresh={() => {}}
+ />
+ ));
+
+ fireEvent.click(getByTestId('dynamic-lease-make-static-button'));
+ expect(onMakeStatic).toHaveBeenCalledWith(LEASES[0]);
+ });
+});
diff --git a/client_v2/src/__tests__/encryption-store.test.ts b/client_v2/src/__tests__/encryption-store.test.ts
new file mode 100644
index 00000000000..f68a296ad01
--- /dev/null
+++ b/client_v2/src/__tests__/encryption-store.test.ts
@@ -0,0 +1,111 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+
+const mocks = vi.hoisted(() => ({
+ setTlsConfig: vi.fn(),
+ validateTlsConfig: vi.fn(),
+ addErrorToast: vi.fn(),
+ addSuccessToast: vi.fn(),
+}));
+
+vi.mock('panel/api/Api', () => ({
+ apiClient: {
+ setTlsConfig: mocks.setTlsConfig,
+ validateTlsConfig: mocks.validateTlsConfig,
+ getGlobalStatus: vi.fn(),
+ },
+}));
+vi.mock('panel/stores/toasts', () => ({
+ addErrorToast: mocks.addErrorToast,
+ addSuccessToast: mocks.addSuccessToast,
+}));
+vi.mock('panel/stores/dashboard', () => ({ getDnsStatus: vi.fn() }));
+
+import {
+ setTlsConfig,
+ validateTlsConfig,
+ resetValidationStatus,
+ encryptionState,
+} from 'panel/stores/encryption';
+
+describe('setTlsConfig', () => {
+ beforeEach(() => vi.clearAllMocks());
+
+ it('defaults empty ports to 0 (FR-013)', async () => {
+ mocks.setTlsConfig.mockImplementation(async (v: any) => ({
+ ...v,
+ certificate_chain: '',
+ private_key: '',
+ }));
+ await setTlsConfig({
+ certificate_chain: '',
+ private_key: '',
+ port_https: '',
+ port_dns_over_tls: '',
+ port_dns_over_quic: '',
+ });
+ const sent = mocks.setTlsConfig.mock.calls[0][0];
+ expect(sent.port_https).toBe(0);
+ expect(sent.port_dns_over_tls).toBe(0);
+ expect(sent.port_dns_over_quic).toBe(0);
+ });
+
+ it('clears validation status fields when resetValidationStatus is called', async () => {
+ mocks.validateTlsConfig.mockResolvedValue({
+ valid_chain: true,
+ valid_cert: true,
+ valid_key: true,
+ valid_pair: true,
+ subject: 'CN=example.com',
+ issuer: 'CN=example.com',
+ key_type: 'RSA',
+ not_after: '2027-01-01T00:00:00Z',
+ not_before: '2026-01-01T00:00:00Z',
+ dns_names: ['example.com'],
+ warning_validation: 'self-signed certificate',
+ certificate_chain: '',
+ private_key: '',
+ });
+
+ await validateTlsConfig({
+ enabled: true,
+ certificate_chain: 'x',
+ private_key: 'y',
+ });
+
+ expect(encryptionState.valid_cert).toBe(true);
+ expect(encryptionState.warning_validation).toBe('self-signed certificate');
+
+ resetValidationStatus();
+
+ expect(encryptionState.valid_cert).toBe(false);
+ expect(encryptionState.valid_chain).toBe(false);
+ expect(encryptionState.valid_key).toBe(false);
+ expect(encryptionState.valid_pair).toBe(false);
+ expect(encryptionState.warning_validation).toBe('');
+ expect(encryptionState.subject).toBe('');
+ expect(encryptionState.issuer).toBe('');
+ expect(encryptionState.key_type).toBe('');
+ expect(encryptionState.dns_names).toBeNull();
+ });
+
+ it('reloads when enabled+force_https on http: origin (FR-014)', async () => {
+ const reloadFn = vi.fn();
+ Object.defineProperty(window, 'location', {
+ value: { protocol: 'http:', reload: reloadFn },
+ writable: true,
+ });
+ mocks.setTlsConfig.mockImplementation(async (v: any) => ({
+ ...v,
+ certificate_chain: '',
+ private_key: '',
+ }));
+ await setTlsConfig({
+ certificate_chain: '',
+ private_key: '',
+ enabled: true,
+ force_https: true,
+ port_https: 443,
+ });
+ expect(reloadFn).toHaveBeenCalled();
+ });
+});
diff --git a/client_v2/src/__tests__/filtering-store.test.ts b/client_v2/src/__tests__/filtering-store.test.ts
new file mode 100644
index 00000000000..b80c2f24cb8
--- /dev/null
+++ b/client_v2/src/__tests__/filtering-store.test.ts
@@ -0,0 +1,127 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+import {
+ setRules,
+ addFiltersBatch,
+ getFilteringStatus,
+ filteringState,
+} from 'panel/stores/filtering';
+
+const mocks = vi.hoisted(() => ({
+ apiSetRules: vi.fn(() => Promise.resolve(undefined)),
+ apiAddFilter: vi.fn(() => Promise.resolve(undefined)),
+ apiGetFilteringStatus: vi.fn(() => Promise.resolve({})),
+ addSuccessToast: vi.fn(),
+ addErrorToast: vi.fn(),
+}));
+
+vi.mock('panel/api/Api', () => ({
+ apiClient: {
+ setRules: mocks.apiSetRules,
+ addFilter: mocks.apiAddFilter,
+ getFilteringStatus: mocks.apiGetFilteringStatus,
+ },
+}));
+
+vi.mock('panel/stores/toasts', () => ({
+ addSuccessToast: mocks.addSuccessToast,
+ addErrorToast: mocks.addErrorToast,
+}));
+
+describe('getFilteringStatus', () => {
+ beforeEach(() => vi.clearAllMocks());
+
+ it('normalizes filters to camelCase and joins user_rules array (FR-003)', async () => {
+ mocks.apiGetFilteringStatus.mockResolvedValue({
+ enabled: true,
+ interval: 24,
+ filters: [
+ {
+ id: 1,
+ url: 'u',
+ enabled: true,
+ last_updated: 'x',
+ name: 'n',
+ rules_count: 9,
+ },
+ ],
+ whitelist_filters: [],
+ user_rules: ['||a^', '||b^'],
+ });
+
+ await getFilteringStatus();
+
+ expect(filteringState.userRules).toBe('||a^\n||b^');
+ expect(filteringState.filters[0]).toEqual({
+ id: 1,
+ url: 'u',
+ enabled: true,
+ lastUpdated: 'x',
+ name: 'n',
+ rulesCount: 9,
+ });
+ });
+});
+
+describe('setRules', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('sends user_rules as a string array (FR-021)', async () => {
+ const result = await setRules('||fresh.example^\n');
+
+ expect(mocks.apiSetRules).toHaveBeenCalledWith({
+ rules: ['||fresh.example^'],
+ });
+
+ expect(result).toBe(true);
+ expect(filteringState.userRules).toBe('||fresh.example^\n');
+ expect(filteringState.processingRules).toBe(false);
+ });
+
+ it('returns false and shows error toast on failure', async () => {
+ mocks.apiSetRules.mockRejectedValueOnce(new Error('Network error'));
+ const result = await setRules('||test.example^\n');
+
+ expect(result).toBe(false);
+ expect(filteringState.processingRules).toBe(false);
+ });
+});
+
+describe('addFiltersBatch', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('adds all filters successfully and shows aggregate toast', async () => {
+ const filters = [
+ { url: 'https://example.com/filter1.txt', name: 'Filter One' },
+ { url: 'https://example.com/filter2.txt', name: 'Filter Two' },
+ { url: 'https://example.com/filter3.txt', name: 'Filter Three' },
+ ];
+
+ await addFiltersBatch(filters);
+
+ expect(mocks.apiAddFilter).toHaveBeenCalledTimes(3);
+ expect(mocks.addSuccessToast).toHaveBeenCalled();
+ expect(filteringState.processingAddFilter).toBe(false);
+ });
+
+ it('handles partial failures', async () => {
+ mocks.apiAddFilter
+ .mockResolvedValueOnce(undefined)
+ .mockRejectedValueOnce(new Error('Failed'));
+
+ const filters = [
+ { url: 'https://example.com/filter1.txt', name: 'Filter One' },
+ { url: 'https://example.com/filter2.txt', name: 'Filter Two' },
+ ];
+
+ await addFiltersBatch(filters);
+
+ expect(mocks.addSuccessToast).toHaveBeenCalled();
+ expect(mocks.addErrorToast).toHaveBeenCalled();
+ expect(filteringState.processingAddFilter).toBe(false);
+ });
+});
diff --git a/client_v2/src/__tests__/filteringStore.test.ts b/client_v2/src/__tests__/filteringStore.test.ts
new file mode 100644
index 00000000000..b80c2f24cb8
--- /dev/null
+++ b/client_v2/src/__tests__/filteringStore.test.ts
@@ -0,0 +1,127 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+import {
+ setRules,
+ addFiltersBatch,
+ getFilteringStatus,
+ filteringState,
+} from 'panel/stores/filtering';
+
+const mocks = vi.hoisted(() => ({
+ apiSetRules: vi.fn(() => Promise.resolve(undefined)),
+ apiAddFilter: vi.fn(() => Promise.resolve(undefined)),
+ apiGetFilteringStatus: vi.fn(() => Promise.resolve({})),
+ addSuccessToast: vi.fn(),
+ addErrorToast: vi.fn(),
+}));
+
+vi.mock('panel/api/Api', () => ({
+ apiClient: {
+ setRules: mocks.apiSetRules,
+ addFilter: mocks.apiAddFilter,
+ getFilteringStatus: mocks.apiGetFilteringStatus,
+ },
+}));
+
+vi.mock('panel/stores/toasts', () => ({
+ addSuccessToast: mocks.addSuccessToast,
+ addErrorToast: mocks.addErrorToast,
+}));
+
+describe('getFilteringStatus', () => {
+ beforeEach(() => vi.clearAllMocks());
+
+ it('normalizes filters to camelCase and joins user_rules array (FR-003)', async () => {
+ mocks.apiGetFilteringStatus.mockResolvedValue({
+ enabled: true,
+ interval: 24,
+ filters: [
+ {
+ id: 1,
+ url: 'u',
+ enabled: true,
+ last_updated: 'x',
+ name: 'n',
+ rules_count: 9,
+ },
+ ],
+ whitelist_filters: [],
+ user_rules: ['||a^', '||b^'],
+ });
+
+ await getFilteringStatus();
+
+ expect(filteringState.userRules).toBe('||a^\n||b^');
+ expect(filteringState.filters[0]).toEqual({
+ id: 1,
+ url: 'u',
+ enabled: true,
+ lastUpdated: 'x',
+ name: 'n',
+ rulesCount: 9,
+ });
+ });
+});
+
+describe('setRules', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('sends user_rules as a string array (FR-021)', async () => {
+ const result = await setRules('||fresh.example^\n');
+
+ expect(mocks.apiSetRules).toHaveBeenCalledWith({
+ rules: ['||fresh.example^'],
+ });
+
+ expect(result).toBe(true);
+ expect(filteringState.userRules).toBe('||fresh.example^\n');
+ expect(filteringState.processingRules).toBe(false);
+ });
+
+ it('returns false and shows error toast on failure', async () => {
+ mocks.apiSetRules.mockRejectedValueOnce(new Error('Network error'));
+ const result = await setRules('||test.example^\n');
+
+ expect(result).toBe(false);
+ expect(filteringState.processingRules).toBe(false);
+ });
+});
+
+describe('addFiltersBatch', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('adds all filters successfully and shows aggregate toast', async () => {
+ const filters = [
+ { url: 'https://example.com/filter1.txt', name: 'Filter One' },
+ { url: 'https://example.com/filter2.txt', name: 'Filter Two' },
+ { url: 'https://example.com/filter3.txt', name: 'Filter Three' },
+ ];
+
+ await addFiltersBatch(filters);
+
+ expect(mocks.apiAddFilter).toHaveBeenCalledTimes(3);
+ expect(mocks.addSuccessToast).toHaveBeenCalled();
+ expect(filteringState.processingAddFilter).toBe(false);
+ });
+
+ it('handles partial failures', async () => {
+ mocks.apiAddFilter
+ .mockResolvedValueOnce(undefined)
+ .mockRejectedValueOnce(new Error('Failed'));
+
+ const filters = [
+ { url: 'https://example.com/filter1.txt', name: 'Filter One' },
+ { url: 'https://example.com/filter2.txt', name: 'Filter Two' },
+ ];
+
+ await addFiltersBatch(filters);
+
+ expect(mocks.addSuccessToast).toHaveBeenCalled();
+ expect(mocks.addErrorToast).toHaveBeenCalled();
+ expect(filteringState.processingAddFilter).toBe(false);
+ });
+});
diff --git a/client_v2/src/__tests__/helpers.test.ts b/client_v2/src/__tests__/helpers.test.ts
new file mode 100644
index 00000000000..2a9027a02b9
--- /dev/null
+++ b/client_v2/src/__tests__/helpers.test.ts
@@ -0,0 +1,495 @@
+import { describe, expect, test, afterEach, vi, beforeEach, it } from 'vitest';
+
+import {
+ sortIp,
+ countClientsStatistics,
+ findAddressType,
+ subnetMaskToBitMask,
+} from '../helpers/helpers';
+import { ADDRESS_TYPES } from '../helpers/constants';
+
+describe('sortIp', () => {
+ describe('ipv4', () => {
+ test('one octet differ', () => {
+ const arr = ['127.0.2.0', '127.0.3.0', '127.0.1.0'];
+ const sortedArr = ['127.0.1.0', '127.0.2.0', '127.0.3.0'];
+
+ expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
+ });
+
+ test('few octets differ', () => {
+ const arr = [
+ '192.168.11.10',
+ '192.168.10.0',
+ '192.168.11.11',
+ '192.168.10.10',
+ '192.168.1.10',
+ '192.168.0.1',
+ '192.168.1.0',
+ '192.168.1.1',
+ '192.168.11.0',
+ '192.168.0.10',
+ '192.168.10.11',
+ '192.168.0.11',
+ '192.168.1.11',
+ '192.168.0.0',
+ '192.168.10.1',
+ '192.168.11.1',
+ ];
+ const sortedArr = [
+ '192.168.0.0',
+ '192.168.0.1',
+ '192.168.0.10',
+ '192.168.0.11',
+ '192.168.1.0',
+ '192.168.1.1',
+ '192.168.1.10',
+ '192.168.1.11',
+ '192.168.10.0',
+ '192.168.10.1',
+ '192.168.10.10',
+ '192.168.10.11',
+ '192.168.11.0',
+ '192.168.11.1',
+ '192.168.11.10',
+ '192.168.11.11',
+ ];
+
+ expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
+
+ // Example from issue https://github.com/AdguardTeam/AdGuardHome/issues/1778#issuecomment-640937599
+ const arr2 = [
+ '192.168.2.11',
+ '192.168.3.1',
+ '192.168.2.100',
+ '192.168.2.2',
+ '192.168.2.1',
+ '192.168.2.10',
+ '192.168.2.99',
+ '192.168.2.200',
+ '192.168.2.199',
+ ];
+ const sortedArr2 = [
+ '192.168.2.1',
+ '192.168.2.2',
+ '192.168.2.10',
+ '192.168.2.11',
+ '192.168.2.99',
+ '192.168.2.100',
+ '192.168.2.199',
+ '192.168.2.200',
+ '192.168.3.1',
+ ];
+
+ expect(arr2.sort(sortIp)).toStrictEqual(sortedArr2);
+ });
+ });
+
+ describe('ipv6', () => {
+ test('only long form', () => {
+ const arr = [
+ '2001:db8:11a3:9d7:0:0:0:2',
+ '2001:db8:11a3:9d7:0:0:0:3',
+ '2001:db8:11a3:9d7:0:0:0:1',
+ ];
+ const sortedArr = [
+ '2001:db8:11a3:9d7:0:0:0:1',
+ '2001:db8:11a3:9d7:0:0:0:2',
+ '2001:db8:11a3:9d7:0:0:0:3',
+ ];
+
+ expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
+ });
+
+ test('only short form', () => {
+ const arr = ['2001:db8::', '2001:db7::', '2001:db9::'];
+ const sortedArr = ['2001:db7::', '2001:db8::', '2001:db9::'];
+
+ expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
+ });
+
+ test('long and short forms', () => {
+ const arr = [
+ '2001:db8::',
+ '2001:db7:11a3:9d7:0:0:0:2',
+ '2001:db6:11a3:9d7:0:0:0:1',
+ '2001:db6::',
+ '2001:db7:11a3:9d7:0:0:0:1',
+ '2001:db7::',
+ ];
+ const sortedArr = [
+ '2001:db6::',
+ '2001:db6:11a3:9d7:0:0:0:1',
+ '2001:db7::',
+ '2001:db7:11a3:9d7:0:0:0:1',
+ '2001:db7:11a3:9d7:0:0:0:2',
+ '2001:db8::',
+ ];
+
+ expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
+ });
+ });
+
+ describe('ipv4 and ipv6', () => {
+ test('ipv6 long form', () => {
+ const arr = [
+ '127.0.0.3',
+ '2001:db8:11a3:9d7:0:0:0:1',
+ '2001:db8:11a3:9d7:0:0:0:3',
+ '127.0.0.1',
+ '2001:db8:11a3:9d7:0:0:0:2',
+ '127.0.0.2',
+ ];
+ const sortedArr = [
+ '127.0.0.1',
+ '127.0.0.2',
+ '127.0.0.3',
+ '2001:db8:11a3:9d7:0:0:0:1',
+ '2001:db8:11a3:9d7:0:0:0:2',
+ '2001:db8:11a3:9d7:0:0:0:3',
+ ];
+
+ expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
+ });
+
+ test('ipv6 short form', () => {
+ const arr = [
+ '2001:db8:11a3:9d7::1',
+ '127.0.0.3',
+ '2001:db8:11a3:9d7::3',
+ '127.0.0.1',
+ '2001:db8:11a3:9d7::2',
+ '127.0.0.2',
+ ];
+ const sortedArr = [
+ '127.0.0.1',
+ '127.0.0.2',
+ '127.0.0.3',
+ '2001:db8:11a3:9d7::1',
+ '2001:db8:11a3:9d7::2',
+ '2001:db8:11a3:9d7::3',
+ ];
+
+ expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
+ });
+
+ test('ipv6 long and short forms', () => {
+ const arr = [
+ '2001:db8:11a3:9d7::1',
+ '127.0.0.3',
+ '2001:db8:11a3:9d7:0:0:0:2',
+ '127.0.0.1',
+ '2001:db8:11a3:9d7::3',
+ '127.0.0.2',
+ ];
+ const sortedArr = [
+ '127.0.0.1',
+ '127.0.0.2',
+ '127.0.0.3',
+ '2001:db8:11a3:9d7::1',
+ '2001:db8:11a3:9d7:0:0:0:2',
+ '2001:db8:11a3:9d7::3',
+ ];
+
+ expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
+ });
+
+ test('always put ipv4 before ipv6', () => {
+ const arr = [
+ '::1',
+ '0.0.0.2',
+ '127.0.0.1',
+ '::2',
+ '2001:db8:11a3:9d7:0:0:0:2',
+ '0.0.0.1',
+ '2001:db8:11a3:9d7::1',
+ ];
+ const sortedArr = [
+ '0.0.0.1',
+ '0.0.0.2',
+ '127.0.0.1',
+ '::1',
+ '::2',
+ '2001:db8:11a3:9d7::1',
+ '2001:db8:11a3:9d7:0:0:0:2',
+ ];
+
+ expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
+ });
+ });
+
+ describe('cidr', () => {
+ test('only ipv4 cidr', () => {
+ const arr = ['192.168.0.1/9', '192.168.0.1/7', '192.168.0.1/8'];
+ const sortedArr = ['192.168.0.1/7', '192.168.0.1/8', '192.168.0.1/9'];
+
+ expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
+ });
+
+ test('ipv4 and cidr ipv4', () => {
+ const arr = [
+ '192.168.0.1/9',
+ '192.168.0.1',
+ '192.168.0.1/32',
+ '192.168.0.1/7',
+ '192.168.0.1/8',
+ ];
+ const sortedArr = [
+ '192.168.0.1/7',
+ '192.168.0.1/8',
+ '192.168.0.1/9',
+ '192.168.0.1/32',
+ '192.168.0.1',
+ ];
+
+ expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
+ });
+
+ test('only ipv6 cidr', () => {
+ const arr = [
+ '2001:db8:11a3:9d7::1/32',
+ '2001:db8:11a3:9d7::1/64',
+ '2001:db8:11a3:9d7::1/128',
+ '2001:db8:11a3:9d7::1/24',
+ ];
+ const sortedArr = [
+ '2001:db8:11a3:9d7::1/24',
+ '2001:db8:11a3:9d7::1/32',
+ '2001:db8:11a3:9d7::1/64',
+ '2001:db8:11a3:9d7::1/128',
+ ];
+
+ expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
+ });
+
+ test('ipv6 and cidr ipv6', () => {
+ const arr = [
+ '2001:db8:11a3:9d7::1/32',
+ '2001:db8:11a3:9d7::1',
+ '2001:db8:11a3:9d7::1/64',
+ '2001:db8:11a3:9d7::1/128',
+ '2001:db8:11a3:9d7::1/24',
+ ];
+ const sortedArr = [
+ '2001:db8:11a3:9d7::1/24',
+ '2001:db8:11a3:9d7::1/32',
+ '2001:db8:11a3:9d7::1/64',
+ '2001:db8:11a3:9d7::1/128',
+ '2001:db8:11a3:9d7::1',
+ ];
+
+ expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
+ });
+ });
+
+ describe('invalid input', () => {
+ const originalWarn = console.warn;
+
+ beforeEach(() => {
+ console.warn = vi.fn();
+ });
+
+ afterEach(() => {
+ expect(console.warn).toHaveBeenCalled();
+ console.warn = originalWarn;
+ });
+
+ test('invalid strings', () => {
+ const arr = ['invalid ip', 'invalid cidr'];
+
+ expect(arr.sort(sortIp)).toStrictEqual(arr);
+ });
+
+ test('invalid ip', () => {
+ const arr = ['127.0.0.2.', '.127.0.0.1.', '.2001:db8:11a3:9d7:0:0:0:0'];
+
+ expect(arr.sort(sortIp)).toStrictEqual(arr);
+ });
+
+ test('invalid cidr', () => {
+ const arr = ['127.0.0.2/33', '2001:db8:11a3:9d7:0:0:0:0/129'];
+
+ expect(arr.sort(sortIp)).toStrictEqual(arr);
+ });
+
+ test('valid and invalid ip', () => {
+ const arr = ['127.0.0.4.', '127.0.0.1', '.127.0.0.3', '127.0.0.2'];
+
+ expect(arr.sort(sortIp)).toStrictEqual(arr);
+ });
+ });
+
+ describe('mixed', () => {
+ test('ipv4, ipv6 in short and long forms and cidr', () => {
+ const arr = [
+ '2001:db8:11a3:9d7:0:0:0:1/32',
+ '192.168.1.2',
+ '127.0.0.2',
+ '2001:db8:11a3:9d7::1/128',
+ '2001:db8:11a3:9d7:0:0:0:1',
+ '127.0.0.1/12',
+ '192.168.1.1',
+ '2001:db8::/32',
+ '2001:db8:11a3:9d7::1/24',
+ '192.168.1.2/12',
+ '2001:db7::/32',
+ '127.0.0.1',
+ '2001:db8:11a3:9d7:0:0:0:2',
+ '192.168.1.1/24',
+ '2001:db7::/64',
+ '2001:db7::',
+ '2001:db8::',
+ '2001:db8:11a3:9d7:0:0:0:1/128',
+ '192.168.1.1/12',
+ '127.0.0.1/32',
+ '::1',
+ ];
+ const sortedArr = [
+ '127.0.0.1/12',
+ '127.0.0.1/32',
+ '127.0.0.1',
+ '127.0.0.2',
+ '192.168.1.1/12',
+ '192.168.1.1/24',
+ '192.168.1.1',
+ '192.168.1.2/12',
+ '192.168.1.2',
+ '::1',
+ '2001:db7::/32',
+ '2001:db7::/64',
+ '2001:db7::',
+ '2001:db8::/32',
+ '2001:db8::',
+ '2001:db8:11a3:9d7::1/24',
+ '2001:db8:11a3:9d7:0:0:0:1/32',
+ '2001:db8:11a3:9d7::1/128',
+ '2001:db8:11a3:9d7:0:0:0:1/128',
+ '2001:db8:11a3:9d7:0:0:0:1',
+ '2001:db8:11a3:9d7:0:0:0:2',
+ ];
+
+ expect(arr.sort(sortIp)).toStrictEqual(sortedArr);
+ });
+ });
+});
+
+describe('findAddressType', () => {
+ it('should return IP type for IP addresses', () => {
+ expect(findAddressType('127.0.0.1')).toStrictEqual(ADDRESS_TYPES.IP);
+ });
+
+ it('should return CIDR type for CIDR addresses', () => {
+ expect(findAddressType('127.0.0.1/8')).toStrictEqual(ADDRESS_TYPES.CIDR);
+ });
+
+ it('should return UNKNOWN type for MAC addresses', () => {
+ expect(findAddressType('00:1B:44:11:3A:B7')).toStrictEqual(ADDRESS_TYPES.UNKNOWN);
+ });
+});
+
+describe('countClientsStatistics', () => {
+ test('single ip', () => {
+ expect(
+ countClientsStatistics(['127.0.0.1'], {
+ '127.0.0.1': 1,
+ }),
+ ).toStrictEqual(1);
+ });
+
+ test('multiple ip', () => {
+ expect(
+ countClientsStatistics(['127.0.0.1', '127.0.0.2'], {
+ '127.0.0.1': 1,
+ '127.0.0.2': 2,
+ }),
+ ).toStrictEqual(1 + 2);
+ });
+
+ test('cidr', () => {
+ expect(
+ countClientsStatistics(['127.0.0.0/8'], {
+ '127.0.0.1': 1,
+ '127.0.0.2': 2,
+ }),
+ ).toStrictEqual(1 + 2);
+ });
+
+ test('cidr and multiple ip', () => {
+ expect(
+ countClientsStatistics(['1.1.1.1', '2.2.2.2', '3.3.3.0/24'], {
+ '1.1.1.1': 1,
+ '2.2.2.2': 2,
+ '3.3.3.3': 3,
+ }),
+ ).toStrictEqual(1 + 2 + 3);
+ });
+
+ test('mac', () => {
+ expect(
+ countClientsStatistics(['00:1B:44:11:3A:B7', '2.2.2.2', '3.3.3.0/24'], {
+ '1.1.1.1': 1,
+ '2.2.2.2': 2,
+ '3.3.3.3': 3,
+ }),
+ ).toStrictEqual(2 + 3);
+ });
+
+ test('not found', () => {
+ expect(
+ countClientsStatistics(['4.4.4.4', '5.5.5.5', '6.6.6.6'], {
+ '1.1.1.1': 1,
+ '2.2.2.2': 2,
+ '3.3.3.3': 3,
+ }),
+ ).toStrictEqual(0);
+ });
+});
+
+describe('subnetMaskToBitMask', () => {
+ const subnetMasks = [
+ '0.0.0.0',
+ '128.0.0.0',
+ '192.0.0.0',
+ '224.0.0.0',
+ '240.0.0.0',
+ '248.0.0.0',
+ '252.0.0.0',
+ '254.0.0.0',
+ '255.0.0.0',
+ '255.128.0.0',
+ '255.192.0.0',
+ '255.224.0.0',
+ '255.240.0.0',
+ '255.248.0.0',
+ '255.252.0.0',
+ '255.254.0.0',
+ '255.255.0.0',
+ '255.255.128.0',
+ '255.255.192.0',
+ '255.255.224.0',
+ '255.255.240.0',
+ '255.255.248.0',
+ '255.255.252.0',
+ '255.255.254.0',
+ '255.255.255.0',
+ '255.255.255.128',
+ '255.255.255.192',
+ '255.255.255.224',
+ '255.255.255.240',
+ '255.255.255.248',
+ '255.255.255.252',
+ '255.255.255.254',
+ '255.255.255.255',
+ ];
+
+ test('correct for all subnetMasks', () => {
+ expect(
+ subnetMasks
+ .map((subnetMask) => {
+ const bitmask = subnetMaskToBitMask(subnetMask);
+ return subnetMasks[bitmask] === subnetMask;
+ })
+ .every((res) => res === true),
+ ).toEqual(true);
+ });
+});
diff --git a/client_v2/src/__tests__/helpers/normalize-mac.test.ts b/client_v2/src/__tests__/helpers/normalize-mac.test.ts
new file mode 100644
index 00000000000..43eb34b348b
--- /dev/null
+++ b/client_v2/src/__tests__/helpers/normalize-mac.test.ts
@@ -0,0 +1,36 @@
+import { describe, expect, it } from 'vitest';
+import { normalizeMac } from 'panel/helpers/form';
+
+describe('normalizeMac', () => {
+ it('normalizes bare hex (12 chars)', () => {
+ expect(normalizeMac('AABBCCDDEEFF')).toBe('AA:BB:CC:DD:EE:FF');
+ });
+
+ it('normalizes bare hex lowercase', () => {
+ expect(normalizeMac('aabbccddeeff')).toBe('AA:BB:CC:DD:EE:FF');
+ });
+
+ it('normalizes dash-separated', () => {
+ expect(normalizeMac('AA-BB-CC-DD-EE-FF')).toBe('AA:BB:CC:DD:EE:FF');
+ });
+
+ it('normalizes dash-separated lowercase', () => {
+ expect(normalizeMac('aa-bb-cc-dd-ee-ff')).toBe('AA:BB:CC:DD:EE:FF');
+ });
+
+ it('uppercases colon-separated', () => {
+ expect(normalizeMac('aa:bb:cc:dd:ee:ff')).toBe('AA:BB:CC:DD:EE:FF');
+ });
+
+ it('returns already-normalized MACs unchanged', () => {
+ expect(normalizeMac('AA:BB:CC:DD:EE:FF')).toBe('AA:BB:CC:DD:EE:FF');
+ });
+
+ it('converts dashes to colons (general dash handling)', () => {
+ expect(normalizeMac('not-a-mac')).toBe('NOT:A:MAC');
+ });
+
+ it('handles empty string', () => {
+ expect(normalizeMac('')).toBe('');
+ });
+});
diff --git a/client_v2/src/__tests__/helpers/validate-clients-per-line.test.ts b/client_v2/src/__tests__/helpers/validate-clients-per-line.test.ts
new file mode 100644
index 00000000000..d381d2ce627
--- /dev/null
+++ b/client_v2/src/__tests__/helpers/validate-clients-per-line.test.ts
@@ -0,0 +1,99 @@
+import { describe, it, expect, vi } from 'vitest';
+
+vi.mock('panel/common/intl', () => ({
+ default: {
+ getMessage: vi.fn((key: string, values?: Record) => {
+ if (key === 'form_error_format_line') {
+ return `Invalid format on line ${values?.line}`;
+ }
+ if (key === 'form_error_format_lines') {
+ return `Invalid format on lines ${values?.lines}`;
+ }
+ if (key === 'form_error_format') {
+ return 'Invalid format';
+ }
+ return key;
+ }),
+ },
+}));
+
+import { validateClientsPerLine } from 'panel/helpers/validators';
+
+describe('validateClientsPerLine', () => {
+ it('returns undefined for empty string', () => {
+ expect(validateClientsPerLine('')).toBeUndefined();
+ });
+
+ it('returns undefined for valid IPv4', () => {
+ expect(validateClientsPerLine('192.168.1.1')).toBeUndefined();
+ });
+
+ it('returns undefined for valid IPv6', () => {
+ expect(validateClientsPerLine('::1')).toBeUndefined();
+ expect(validateClientsPerLine('2001:db8::1')).toBeUndefined();
+ });
+
+ it('returns undefined for valid IPv4 CIDR', () => {
+ expect(validateClientsPerLine('192.168.1.0/24')).toBeUndefined();
+ expect(validateClientsPerLine('10.0.0.0/8')).toBeUndefined();
+ });
+
+ it('returns undefined for valid IPv6 CIDR', () => {
+ expect(validateClientsPerLine('2001:db8::/32')).toBeUndefined();
+ });
+
+ it('returns undefined for valid ClientID', () => {
+ expect(validateClientsPerLine('my-client-id')).toBeUndefined();
+ expect(validateClientsPerLine('client-123')).toBeUndefined();
+ });
+
+ it('returns undefined for mixed valid lines', () => {
+ expect(validateClientsPerLine('192.168.1.1\n10.0.0.0/8\nmy-client-id')).toBeUndefined();
+ });
+
+ it('returns "Invalid format" for single invalid line', () => {
+ expect(validateClientsPerLine('BAD ENTRY!')).toBe('Invalid format');
+ });
+
+ it('returns "Invalid format on line N" when specific line is invalid', () => {
+ expect(validateClientsPerLine('192.168.1.1\nBAD!')).toBe('Invalid format on line 2');
+ });
+
+ it('returns "Invalid format on lines N, M" when multiple lines invalid', () => {
+ expect(validateClientsPerLine('BAD1!\n192.168.1.1\nBAD2!')).toBe(
+ 'Invalid format on lines 1, 3',
+ );
+ });
+
+ it('rejects MAC address', () => {
+ expect(validateClientsPerLine('aa:bb:cc:dd:ee:ff')).toBe('Invalid format');
+ });
+
+ it('rejects CIDR with bad prefix', () => {
+ expect(validateClientsPerLine('192.168.1.0/33')).toBe('Invalid format');
+ });
+
+ it('returns "Invalid format" for single invalid line with trailing newline', () => {
+ expect(validateClientsPerLine('BAD ENTRY!\n')).toBe('Invalid format');
+ });
+
+ it('returns "Invalid format" for single invalid line with leading newline', () => {
+ expect(validateClientsPerLine('\nBAD ENTRY!')).toBe('Invalid format');
+ });
+
+ it('returns "Invalid format on lines 1, 2" when both lines invalid', () => {
+ expect(validateClientsPerLine('BAD1!\nBAD2!')).toBe('Invalid format on lines 1, 2');
+ });
+
+ it('returns "Invalid format on line 2" when second line invalid in multi-content input', () => {
+ expect(validateClientsPerLine('192.168.1.1\nBAD!')).toBe('Invalid format on line 2');
+ });
+
+ it('returns undefined for all-blank input', () => {
+ expect(validateClientsPerLine('\n\n')).toBeUndefined();
+ });
+
+ it('handles blank line between two invalid lines', () => {
+ expect(validateClientsPerLine('BAD1!\n\nBAD2!')).toBe('Invalid format on lines 1, 3');
+ });
+});
diff --git a/client_v2/src/__tests__/helpers/validate-domains-per-line.test.ts b/client_v2/src/__tests__/helpers/validate-domains-per-line.test.ts
new file mode 100644
index 00000000000..348e10613e4
--- /dev/null
+++ b/client_v2/src/__tests__/helpers/validate-domains-per-line.test.ts
@@ -0,0 +1,98 @@
+import { describe, it, expect, vi } from 'vitest';
+
+vi.mock('panel/common/intl', () => ({
+ default: {
+ getMessage: vi.fn((key: string, values?: Record) => {
+ if (key === 'form_error_format_line') {
+ return `Invalid format on line ${values?.line}`;
+ }
+ if (key === 'form_error_format_lines') {
+ return `Invalid format on lines ${values?.lines}`;
+ }
+ if (key === 'form_error_format') {
+ return 'Invalid format';
+ }
+ return key;
+ }),
+ },
+}));
+
+import { validateDomainsPerLine } from 'panel/helpers/validators';
+
+describe('validateDomainsPerLine', () => {
+ it('returns undefined for empty string', () => {
+ expect(validateDomainsPerLine('')).toBeUndefined();
+ });
+
+ it('returns undefined for plain domain', () => {
+ expect(validateDomainsPerLine('example.org')).toBeUndefined();
+ });
+
+ it('returns undefined for wildcard domain', () => {
+ expect(validateDomainsPerLine('*.example.org')).toBeUndefined();
+ });
+
+ it('returns undefined for AdGuard URL filter rule', () => {
+ expect(validateDomainsPerLine('||example.org^')).toBeUndefined();
+ });
+
+ it('returns undefined for regex pattern', () => {
+ expect(validateDomainsPerLine('/regex.pattern/')).toBeUndefined();
+ });
+
+ it('returns undefined for comment line', () => {
+ expect(validateDomainsPerLine('# this is a comment')).toBeUndefined();
+ });
+
+ it('returns undefined for mixed valid lines with comments', () => {
+ expect(
+ validateDomainsPerLine('# comment\nexample.org\n||ads.example.org^'),
+ ).toBeUndefined();
+ });
+
+ it('returns "Invalid format" for entry without dot', () => {
+ expect(validateDomainsPerLine('notadomain')).toBe('Invalid format');
+ });
+
+ it('returns "Invalid format on line N" when specific line has no dot', () => {
+ expect(validateDomainsPerLine('example.org\nnodot')).toBe('Invalid format on line 2');
+ });
+
+ it('returns "Invalid format on lines N, M" when multiple lines invalid', () => {
+ expect(validateDomainsPerLine('nodot1\nexample.org\nnodot2')).toBe(
+ 'Invalid format on lines 1, 3',
+ );
+ });
+
+ it('returns "Invalid format" for single invalid line with trailing newline', () => {
+ expect(validateDomainsPerLine('notadomain\n')).toBe('Invalid format');
+ });
+
+ it('returns "Invalid format" for single invalid line with leading newline', () => {
+ expect(validateDomainsPerLine('\nnotadomain')).toBe('Invalid format');
+ });
+
+ it('returns "Invalid format on lines 1, 2" when both lines invalid', () => {
+ expect(validateDomainsPerLine('nodot1\nnodot2')).toBe('Invalid format on lines 1, 2');
+ });
+
+ it('returns "Invalid format on line 2" when second line invalid in multi-content input', () => {
+ expect(validateDomainsPerLine('example.org\nnodot')).toBe('Invalid format on line 2');
+ });
+
+ it('returns undefined for all-blank input', () => {
+ expect(validateDomainsPerLine('\n\n')).toBeUndefined();
+ });
+
+ it('handles blank line between two invalid lines', () => {
+ expect(validateDomainsPerLine('nodot1\n\nnodot2')).toBe('Invalid format on lines 1, 3');
+ });
+
+ it('returns "Invalid format" for comment-then-invalid (one content line)', () => {
+ expect(validateDomainsPerLine('# comment\nnotadomain')).toBe('Invalid format');
+ });
+
+ it('returns "Invalid format" for invalid-then-comment (one content line)', () => {
+ expect(validateDomainsPerLine('notadomain\n# comment')).toBe('Invalid format');
+ });
+});
diff --git a/client_v2/src/__tests__/helpers/validate-ip-per-line.test.ts b/client_v2/src/__tests__/helpers/validate-ip-per-line.test.ts
new file mode 100644
index 00000000000..f9dcb65032b
--- /dev/null
+++ b/client_v2/src/__tests__/helpers/validate-ip-per-line.test.ts
@@ -0,0 +1,68 @@
+import { describe, it, expect, vi } from 'vitest';
+
+vi.mock('panel/common/intl', () => ({
+ default: {
+ getMessage: vi.fn((key: string, values?: Record) => {
+ if (key === 'form_error_format_line') {
+ return `Invalid format on line ${values?.line}`;
+ }
+ if (key === 'form_error_format_lines') {
+ return `Invalid format on lines ${values?.lines}`;
+ }
+ if (key === 'form_error_format') {
+ return 'Invalid format';
+ }
+ return key;
+ }),
+ },
+}));
+
+import { validateIpPerLine } from 'panel/helpers/validators';
+
+describe('validateIpPerLine', () => {
+ it('returns undefined for empty string', () => {
+ expect(validateIpPerLine('')).toBeUndefined();
+ });
+
+ it('returns undefined for valid IPs', () => {
+ expect(validateIpPerLine('192.168.1.1')).toBeUndefined();
+ expect(validateIpPerLine('192.168.1.1\n10.0.0.1')).toBeUndefined();
+ expect(validateIpPerLine('::1\n192.168.1.1')).toBeUndefined();
+ });
+
+ it('returns "Invalid format" for single invalid line', () => {
+ expect(validateIpPerLine('not-an-ip')).toBe('Invalid format');
+ });
+
+ it('returns "Invalid format on line 2" when second line is invalid', () => {
+ expect(validateIpPerLine('192.168.1.1\nbad-ip')).toBe('Invalid format on line 2');
+ });
+
+ it('returns "Invalid format on lines 1, 3" when multiple lines invalid', () => {
+ expect(validateIpPerLine('bad1\n192.168.1.1\nbad2')).toBe('Invalid format on lines 1, 3');
+ });
+
+ it('returns "Invalid format" for single invalid line with trailing newline', () => {
+ expect(validateIpPerLine('bad\n')).toBe('Invalid format');
+ });
+
+ it('returns "Invalid format" for single invalid line with leading newline', () => {
+ expect(validateIpPerLine('\nbad')).toBe('Invalid format');
+ });
+
+ it('returns "Invalid format on lines 1, 2" when both lines invalid', () => {
+ expect(validateIpPerLine('bad\nbad2')).toBe('Invalid format on lines 1, 2');
+ });
+
+ it('returns "Invalid format on line 2" when second line invalid in multi-content input', () => {
+ expect(validateIpPerLine('192.168.1.1\nbad')).toBe('Invalid format on line 2');
+ });
+
+ it('returns undefined for all-blank input', () => {
+ expect(validateIpPerLine('\n\n')).toBeUndefined();
+ });
+
+ it('handles blank line between two invalid lines', () => {
+ expect(validateIpPerLine('bad1\n\nbad2')).toBe('Invalid format on lines 1, 3');
+ });
+});
diff --git a/client_v2/src/__tests__/helpers/validate-ipv6.test.ts b/client_v2/src/__tests__/helpers/validate-ipv6.test.ts
new file mode 100644
index 00000000000..cee42a08148
--- /dev/null
+++ b/client_v2/src/__tests__/helpers/validate-ipv6.test.ts
@@ -0,0 +1,73 @@
+import { describe, expect, it } from 'vitest';
+import { isValidIpv6 } from 'panel/helpers/helpers';
+
+describe('isValidIpv6', () => {
+ it('accepts standard IPv6 address', () => {
+ expect(isValidIpv6('2001:db8::1')).toBe(true);
+ });
+
+ it('accepts IPv6 with zone ID', () => {
+ expect(isValidIpv6('fe80::1%eth0')).toBe(true);
+ expect(isValidIpv6('fe80::1%wlan0')).toBe(true);
+ });
+
+ it('accepts link-local IPv6 without zone', () => {
+ expect(isValidIpv6('fe80::1')).toBe(true);
+ });
+
+ it('accepts loopback', () => {
+ expect(isValidIpv6('::1')).toBe(true);
+ });
+
+ it('rejects invalid IPv6', () => {
+ expect(isValidIpv6('not-an-ip')).toBe(false);
+ expect(isValidIpv6('192.168.1.1')).toBe(false);
+ expect(isValidIpv6('gggg::1')).toBe(false);
+ });
+
+ it('rejects empty string', () => {
+ expect(isValidIpv6('')).toBe(false);
+ });
+});
+
+import { validateIpv6, validateIp } from 'panel/helpers/validators';
+
+describe('validateIpv6 with ipaddr.js', () => {
+ it('passes for IPv6 with zone ID', () => {
+ expect(validateIpv6('fe80::1%eth0')).toBeUndefined();
+ });
+
+ it('passes for IPv6 with zone ID (non-link-local)', () => {
+ expect(validateIpv6('2001:db8::1%eth0')).toBeUndefined();
+ });
+
+ it('passes for IPv6 without zone', () => {
+ expect(validateIpv6('2001:db8::1')).toBeUndefined();
+ });
+
+ it('fails for invalid IPv6', () => {
+ expect(validateIpv6('not-an-ip')).toBeTruthy();
+ });
+
+ it('returns undefined for empty value', () => {
+ expect(validateIpv6('')).toBeUndefined();
+ });
+});
+
+describe('validateIp with ipaddr.js', () => {
+ it('passes for IPv6 with zone ID', () => {
+ expect(validateIp('fe80::1%eth0')).toBeUndefined();
+ });
+
+ it('passes for IPv6 with zone ID (non-link-local)', () => {
+ expect(validateIp('2001:db8::1%eth0')).toBeUndefined();
+ });
+
+ it('passes for IPv4', () => {
+ expect(validateIp('192.168.1.1')).toBeUndefined();
+ });
+
+ it('fails for invalid IP', () => {
+ expect(validateIp('not-an-ip')).toBeTruthy();
+ });
+});
diff --git a/client_v2/src/__tests__/helpers/validate-pem-content.test.ts b/client_v2/src/__tests__/helpers/validate-pem-content.test.ts
new file mode 100644
index 00000000000..391dcd0d393
--- /dev/null
+++ b/client_v2/src/__tests__/helpers/validate-pem-content.test.ts
@@ -0,0 +1,147 @@
+import { describe, it, expect, vi } from 'vitest';
+
+vi.mock('panel/common/intl', () => ({
+ default: {
+ getMessage: (key: string) => key,
+ },
+}));
+
+import { validatePemContent } from 'panel/helpers/validators';
+
+const makePem = (
+ label: string,
+ body: string,
+ opts?: { lineEndings?: string; trailing?: string },
+): string => {
+ const le = opts?.lineEndings ?? '\n';
+ const trailing = opts?.trailing ?? '';
+ return `-----BEGIN ${label}-----` + le + body + le + `-----END ${label}-----` + trailing;
+};
+
+const CERT_BODY = 'MIIBxTCCAS6gAwIBAgIUNW1eQ0p0q5p0YfGq3Gq3Gq3Gq3EwCgYIKoZIzj0EAwIw';
+const KEY_BODY = 'MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7VJTUt9Us8cKj';
+
+describe('validatePemContent', () => {
+ // ── Positive cases ──────────────────────────────────────────────
+
+ it('accepts a standard CERTIFICATE PEM block', () => {
+ const pem = makePem('CERTIFICATE', CERT_BODY);
+ expect(validatePemContent(pem)).toBeUndefined();
+ });
+
+ it('accepts an RSA PRIVATE KEY PEM block', () => {
+ const pem = makePem('RSA PRIVATE KEY', KEY_BODY);
+ expect(validatePemContent(pem)).toBeUndefined();
+ });
+
+ it('accepts an EC PRIVATE KEY PEM block', () => {
+ const pem = makePem('EC PRIVATE KEY', KEY_BODY);
+ expect(validatePemContent(pem)).toBeUndefined();
+ });
+
+ it('accepts an ENCRYPTED PRIVATE KEY PEM block', () => {
+ const pem = makePem('ENCRYPTED PRIVATE KEY', KEY_BODY);
+ expect(validatePemContent(pem)).toBeUndefined();
+ });
+
+ it('accepts body lines with trailing spaces', () => {
+ const pem = makePem('CERTIFICATE', CERT_BODY + ' ');
+ expect(validatePemContent(pem)).toBeUndefined();
+ });
+
+ it('accepts body lines with trailing tabs', () => {
+ const pem = makePem('CERTIFICATE', CERT_BODY + '\t\t');
+ expect(validatePemContent(pem)).toBeUndefined();
+ });
+
+ it('accepts blank lines between body rows', () => {
+ const pem = makePem('CERTIFICATE', CERT_BODY + '\n\n' + KEY_BODY);
+ expect(validatePemContent(pem)).toBeUndefined();
+ });
+
+ it('accepts trailing newlines at end of file', () => {
+ const pem = makePem('CERTIFICATE', CERT_BODY, { trailing: '\n\n' });
+ expect(validatePemContent(pem)).toBeUndefined();
+ });
+
+ it('accepts trailing spaces and tabs at end of file', () => {
+ const pem = makePem('CERTIFICATE', CERT_BODY, { trailing: ' \t ' });
+ expect(validatePemContent(pem)).toBeUndefined();
+ });
+
+ it('accepts CRLF (\\r\\n) line endings', () => {
+ const pem = makePem('CERTIFICATE', CERT_BODY, { lineEndings: '\r\n' });
+ expect(validatePemContent(pem)).toBeUndefined();
+ });
+
+ it('accepts undefined input', () => {
+ expect(validatePemContent(undefined)).toBeUndefined();
+ });
+
+ it('accepts two concatenated CERTIFICATE blocks', () => {
+ const pem =
+ makePem('CERTIFICATE', CERT_BODY, { trailing: '\n' }) +
+ makePem('CERTIFICATE', KEY_BODY);
+ expect(validatePemContent(pem)).toBeUndefined();
+ });
+
+ it('accepts three concatenated CERTIFICATE blocks', () => {
+ const pem =
+ makePem('CERTIFICATE', CERT_BODY, { trailing: '\n' }) +
+ makePem('CERTIFICATE', KEY_BODY, { trailing: '\n' }) +
+ makePem('CERTIFICATE', CERT_BODY);
+ expect(validatePemContent(pem)).toBeUndefined();
+ });
+
+ it('accepts a CERTIFICATE block concatenated with a PRIVATE KEY block', () => {
+ const pem =
+ makePem('CERTIFICATE', CERT_BODY, { trailing: '\n' }) +
+ makePem('RSA PRIVATE KEY', KEY_BODY);
+ expect(validatePemContent(pem)).toBeUndefined();
+ });
+
+ // ── Negative cases ──────────────────────────────────────────────
+
+ it('rejects a plain non-PEM string', () => {
+ expect(validatePemContent('not a pem')).toBe('encryption_invalid_data');
+ });
+
+ it('rejects a whitespace-only string', () => {
+ expect(validatePemContent(' ')).toBe('encryption_invalid_data');
+ });
+
+ it('rejects lowercase labels', () => {
+ const pem = makePem('begin certificate', CERT_BODY);
+ expect(validatePemContent(pem)).toBe('encryption_invalid_data');
+ });
+
+ it('rejects PEM with no body between BEGIN and END', () => {
+ const pem = '-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----';
+ expect(validatePemContent(pem)).toBe('encryption_invalid_data');
+ });
+
+ it('rejects PEM missing the END line', () => {
+ const pem = '-----BEGIN CERTIFICATE-----\n' + CERT_BODY;
+ expect(validatePemContent(pem)).toBe('encryption_invalid_data');
+ });
+
+ it('rejects PEM missing the BEGIN line', () => {
+ const pem = CERT_BODY + '\n-----END CERTIFICATE-----';
+ expect(validatePemContent(pem)).toBe('encryption_invalid_data');
+ });
+
+ it('rejects PEM with no newline after BEGIN label', () => {
+ const pem = '-----BEGIN CERTIFICATE-----' + CERT_BODY + '\n-----END CERTIFICATE-----';
+ expect(validatePemContent(pem)).toBe('encryption_invalid_data');
+ });
+
+ it('rejects valid PEM with junk text before it', () => {
+ const pem = 'junk text\n' + makePem('CERTIFICATE', CERT_BODY);
+ expect(validatePemContent(pem)).toBe('encryption_invalid_data');
+ });
+
+ it('rejects valid PEM with junk text after it', () => {
+ const pem = makePem('CERTIFICATE', CERT_BODY) + '\njunk text';
+ expect(validatePemContent(pem)).toBe('encryption_invalid_data');
+ });
+});
diff --git a/client_v2/src/__tests__/install-store.test.ts b/client_v2/src/__tests__/install-store.test.ts
new file mode 100644
index 00000000000..b4f33f1dc45
--- /dev/null
+++ b/client_v2/src/__tests__/install-store.test.ts
@@ -0,0 +1,67 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+
+// Mock the API client.
+const mockCheckConfig = vi.fn();
+vi.mock('panel/api/Api', () => ({
+ apiClient: {
+ checkConfig: (...args: unknown[]) => mockCheckConfig(...args),
+ getDefaultAddresses: vi.fn(),
+ setAllSettings: vi.fn(),
+ },
+}));
+
+vi.mock('panel/stores/toasts', () => ({ addErrorToast: vi.fn() }));
+vi.mock('panel/helpers/constants', () => ({
+ ALL_INTERFACES_IP: '0.0.0.0',
+ INSTALL_FIRST_STEP: 1,
+ STANDARD_DNS_PORT: 53,
+ STANDARD_WEB_PORT: 80,
+}));
+
+import { checkConfig, installState } from '../stores/install';
+
+describe('checkConfig', () => {
+ beforeEach(() => {
+ mockCheckConfig.mockReset();
+ });
+
+ it('persists the request web/dns ip and port to the store', async () => {
+ // Simulate the API response — only status and can_autofix, no ip/port.
+ mockCheckConfig.mockResolvedValue({
+ web: { status: '', can_autofix: false },
+ dns: { status: '', can_autofix: false },
+ static_ip: { static: 'no', ip: '', error: '' },
+ });
+
+ // User changed web port to 8080 and dns port to 5353.
+ await checkConfig({
+ web: { ip: '192.168.1.1', port: 8080 },
+ dns: { ip: '192.168.1.1', port: 5353 },
+ set_static_ip: false,
+ });
+
+ expect(installState.web.ip).toBe('192.168.1.1');
+ expect(installState.web.port).toBe(8080);
+ expect(installState.dns.ip).toBe('192.168.1.1');
+ expect(installState.dns.port).toBe(5353);
+ });
+
+ it('does not lose ip/port when the API returns only status fields', async () => {
+ mockCheckConfig.mockResolvedValue({
+ web: { status: 'address already in use', can_autofix: true },
+ dns: { status: '', can_autofix: false },
+ static_ip: { static: 'no', ip: '', error: '' },
+ });
+
+ await checkConfig({
+ web: { ip: '0.0.0.0', port: 3000 },
+ dns: { ip: '0.0.0.0', port: 53 },
+ set_static_ip: false,
+ });
+
+ // Port must still be the user's value, not the old store default.
+ expect(installState.web.port).toBe(3000);
+ expect(installState.web.status).toBe('address already in use');
+ expect(installState.web.can_autofix).toBe(true);
+ });
+});
diff --git a/client_v2/src/__tests__/intl.test.ts b/client_v2/src/__tests__/intl.test.ts
new file mode 100644
index 00000000000..a9a40d94ab0
--- /dev/null
+++ b/client_v2/src/__tests__/intl.test.ts
@@ -0,0 +1,207 @@
+import { describe, it, expect, vi } from 'vitest';
+import { translate } from '@adguard/translate';
+import { createSolidDefaultValues, solidMessageConstructor } from '../common/intl/index';
+
+/** Helper to create real DOM elements for test assertions (SolidJS has no h() export) */
+const h = (tag: string, props: Record | null, children?: string) => {
+ const el = document.createElement(tag);
+ if (props) {
+ Object.entries(props).forEach(([key, value]) => {
+ el.setAttribute(key, value);
+ });
+ }
+ if (children !== undefined) {
+ el.textContent = children;
+ }
+ return el;
+};
+
+// Minimal i18n mock for testing the translator layer
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const mockI18n = (locale: string): any => ({
+ getMessage: (key: string) => {
+ const messages: Record = {
+ plain_only: 'Hello %name%',
+ with_code: 'Use %value% for DNS queries',
+ with_link: 'Learn more at our docs ',
+ with_mixed: '%ipv4%,%ipv6%: regular DNS',
+ with_strong: 'This is important ',
+ with_b: 'This is bold ',
+ with_i: 'This is italic ',
+ with_p: 'Paragraph text
',
+ };
+ return messages[key] || '';
+ },
+ getUILanguage: () => locale,
+ getBaseMessage: (key: string) => mockI18n('en').getMessage(key),
+ getBaseUILanguage: () => 'en',
+});
+
+describe('solidMessageConstructor', () => {
+ it('joins arrays of only strings', () => {
+ const result = solidMessageConstructor(['Hello ', 'world', '!']);
+ expect(result).toBe('Hello world!');
+ });
+
+ it('keeps arrays with non-string elements', () => {
+ const el = h('code', null, 'test');
+ const result = solidMessageConstructor(['Use ', el, ' for DNS']);
+ expect(Array.isArray(result)).toBe(true);
+ expect((result as unknown[])[1]).toBe(el);
+ });
+});
+
+describe('createSolidDefaultValues', () => {
+ const getTranslator = () =>
+ translate.createTranslator(
+ mockI18n('en'),
+ solidMessageConstructor,
+ createSolidDefaultValues(),
+ );
+
+ it('returns plain string for translation without tags', () => {
+ const t = getTranslator();
+ const result = t.getMessage('plain_only', { name: 'World' });
+ expect(result).toBe('Hello World');
+ });
+
+ it('renders tag as SolidJS element (not HTML string)', () => {
+ const t = getTranslator();
+ const result = t.getMessage('with_code', { value: '1.2.3.4' });
+
+ // Should be an array because of the code element
+ expect(Array.isArray(result)).toBe(true);
+ const arr = result as unknown[];
+ expect(arr).toHaveLength(3); // 'Use ', , ' for DNS queries'
+
+ // The code element should be a real h() element, not a string
+ expect(arr[1]).not.toBe('1.2.3.4');
+ expect(typeof arr[1]).not.toBe('string');
+ });
+
+ it('supports per-call tag overrides (e.g., with JSX handler)', () => {
+ const t = getTranslator();
+ const result = t.getMessage('with_link', {
+ a: (text: string) => h('a', { href: 'https://example.com' }, text),
+ });
+
+ expect(Array.isArray(result)).toBe(true);
+ const arr = result as unknown[];
+ // The element should be the SolidJS element from our handler
+ expect(typeof arr[2]).not.toBe('string');
+ });
+
+ it('renders as SolidJS element', () => {
+ const t = getTranslator();
+ const result = t.getMessage('with_strong');
+ expect(Array.isArray(result)).toBe(true);
+ });
+
+ it('renders fallback without per-call handler', () => {
+ const t = getTranslator();
+ // Only pass ipv4, ipv6 — no 'code' handler
+ const result = t.getMessage('with_mixed', {
+ ipv4: '94.140.14.140',
+ ipv6: '2a10:50c0::1:ff',
+ });
+
+ expect(Array.isArray(result)).toBe(true);
+ const arr = result as unknown[];
+ // Both elements should be SolidJS elements from defaults
+ expect(typeof arr[0]).not.toBe('string'); // first code
+ expect(typeof arr[2]).not.toBe('string'); // second code
+ });
+
+ it('handles , , tags via defaults', () => {
+ const t = getTranslator();
+ const boldResult = t.getMessage('with_b');
+ expect(Array.isArray(boldResult)).toBe(true);
+
+ const italicResult = t.getMessage('with_i');
+ expect(Array.isArray(italicResult)).toBe(true);
+
+ const pResult = t.getMessage('with_p');
+ expect(Array.isArray(pResult)).toBe(true);
+ });
+});
+
+describe('intl.getMessage — missing placeholder fallback', () => {
+ it('returns the raw key and warns when a placeholder value is missing', () => {
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
+
+ const t = translate.createTranslator(
+ mockI18n('en'),
+ solidMessageConstructor,
+ createSolidDefaultValues(),
+ );
+
+ // Mirror the try-catch pattern from intl's getMessage
+ const safeGetMessage = (key: string, values?: any) => {
+ try {
+ return t.getMessage(key, values);
+ } catch (e) {
+ console.warn('[i18n] Missing placeholder value for key:', key, e);
+ return key;
+ }
+ };
+
+ // 'plain_only' has %name% but we pass no values → should return the key
+ const result = safeGetMessage('plain_only');
+ expect(result).toBe('plain_only');
+ expect(warnSpy).toHaveBeenCalledTimes(1);
+ expect(warnSpy.mock.calls[0][1]).toBe('plain_only');
+
+ warnSpy.mockRestore();
+ });
+
+ it('still returns formatted string when all values are provided', () => {
+ const t = translate.createTranslator(
+ mockI18n('en'),
+ solidMessageConstructor,
+ createSolidDefaultValues(),
+ );
+
+ const safeGetMessage = (key: string, values?: any) => {
+ try {
+ return t.getMessage(key, values);
+ } catch {
+ return key;
+ }
+ };
+
+ const result = safeGetMessage('plain_only', { name: 'World' });
+ expect(result).toBe('Hello World');
+ });
+
+ it('returns the raw key when getPlural is missing a required placeholder', () => {
+ const mockPluralI18n = (locale: string): any => ({
+ getMessage: (key: string) => {
+ const messages: Record = {
+ plural_with_value: 'Found %count% results for %query%',
+ };
+ return messages[key] || '';
+ },
+ getUILanguage: () => locale,
+ getBaseMessage: (key: string) => mockPluralI18n('en').getMessage(key),
+ getBaseUILanguage: () => 'en',
+ });
+
+ const pt = translate.createTranslator(
+ mockPluralI18n('en'),
+ solidMessageConstructor,
+ createSolidDefaultValues(),
+ );
+
+ const safeGetPlural = (key: string, number: number, values?: any) => {
+ try {
+ return pt.getPlural(key, number, values);
+ } catch {
+ return key;
+ }
+ };
+
+ // 'query' is missing from values → should return the key
+ const result = safeGetPlural('plural_with_value', 5);
+ expect(result).toBe('plural_with_value');
+ });
+});
diff --git a/client_v2/src/__tests__/login-store.test.ts b/client_v2/src/__tests__/login-store.test.ts
new file mode 100644
index 00000000000..66eb27235e0
--- /dev/null
+++ b/client_v2/src/__tests__/login-store.test.ts
@@ -0,0 +1,46 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+
+// Mock the API client before importing the store.
+const mockLogin = vi.fn().mockResolvedValue(undefined);
+vi.mock('panel/api/Api', () => ({
+ apiClient: {
+ login: (...args: unknown[]) => mockLogin(...args),
+ },
+}));
+
+// Mock addErrorToast so it does not depend on other stores.
+vi.mock('panel/stores/toasts', () => ({
+ addErrorToast: vi.fn(),
+}));
+
+// Mock HTML_PAGES constant.
+vi.mock('panel/helpers/constants', () => ({
+ HTML_PAGES: { LOGIN: '/login.html', MAIN: '/dashboard.html' },
+}));
+
+import { processLogin } from '../stores/login';
+
+describe('processLogin', () => {
+ beforeEach(() => {
+ mockLogin.mockClear();
+ // Prevent window.location.replace from throwing in tests.
+ Object.defineProperty(window, 'location', {
+ value: { replace: vi.fn(), origin: 'http://localhost', pathname: '/login.html' },
+ writable: true,
+ });
+ });
+
+ it('sends { name, password } to the API (not username)', async () => {
+ await processLogin({ name: 'admin', password: 'supersecret' });
+
+ expect(mockLogin).toHaveBeenCalledTimes(1);
+ expect(mockLogin).toHaveBeenCalledWith({ name: 'admin', password: 'supersecret' });
+ });
+
+ it('does not send username field', async () => {
+ await processLogin({ name: 'admin', password: 'supersecret' });
+
+ const sentArg = mockLogin.mock.calls[0][0];
+ expect(sentArg).not.toHaveProperty('username');
+ });
+});
diff --git a/client_v2/src/__tests__/pagination.test.tsx b/client_v2/src/__tests__/pagination.test.tsx
new file mode 100644
index 00000000000..3c103b5f8ab
--- /dev/null
+++ b/client_v2/src/__tests__/pagination.test.tsx
@@ -0,0 +1,167 @@
+import { describe, it, expect, vi } from 'vitest';
+import { render, fireEvent } from '@solidjs/testing-library';
+import { createSignal } from 'solid-js';
+
+// CSS modules return {} under css:false, so theme class names would be
+// undefined. Mock theme with identity class names for deterministic asserts.
+vi.mock('panel/lib/theme', () => {
+ const make = (names: string[]) => Object.fromEntries(names.map((n) => [n, n]));
+ return {
+ default: {
+ pagination: make([
+ 'wrapper',
+ 'pagesContainer',
+ 'button',
+ 'button_active',
+ 'arrow',
+ 'arrow_left',
+ 'arrow_right',
+ 'summary',
+ 'dropdownText',
+ 'dropdownShowOnPage',
+ 'limitContainer',
+ ]),
+ dropdown: make(['menu', 'item', 'item_active', 'icon', 'flexDropdownWrap', 'dropdown']),
+ },
+ };
+});
+
+vi.mock('panel/common/intl', () => ({
+ default: {
+ getMessage: (key: string, values?: { value?: number }) =>
+ values?.value !== undefined ? `${key}:${values.value}` : key,
+ },
+}));
+
+vi.mock('panel/common/ui/Dropdown', () => ({
+ Dropdown: (props: any) => (
+ <>
+ props.onOpenChange?.(!props.open)}
+ >
+ {props.children}
+
+ {props.open && {props.menu}
}
+ >
+ ),
+}));
+
+import { generatePageNumbers, Pagination } from '../common/ui/Table/blocks/Pagination/Pagination';
+
+describe('generatePageNumbers', () => {
+ it('returns a single page when totalPages is 1', () => {
+ expect(generatePageNumbers(0, 1)).toEqual([1]);
+ });
+
+ it('shows all pages without ellipsis for a small page count', () => {
+ expect(generatePageNumbers(0, 5)).toEqual([1, 2, 3, 4, 5]);
+ });
+
+ it('inserts an ellipsis at the end when on the first page of many', () => {
+ expect(generatePageNumbers(0, 20)).toEqual([1, 2, 3, 'ellipsis', 20]);
+ });
+
+ it('inserts ellipses on both sides when in the middle of many pages', () => {
+ expect(generatePageNumbers(9, 20)).toEqual([
+ 1,
+ 'ellipsis',
+ 8,
+ 9,
+ 10,
+ 11,
+ 12,
+ 'ellipsis',
+ 20,
+ ]);
+ });
+});
+
+const baseProps = {
+ pageSize: 10,
+ totalItems: 100,
+ pageSizeOptions: [10, 20, 30],
+ onPageChange: vi.fn(),
+ onPageSizeChange: vi.fn(),
+};
+
+const pageButton = (container: HTMLElement, text: string) =>
+ [...container.querySelectorAll('button')].find((b) => b.textContent === text)!;
+
+describe('Pagination', () => {
+ it('applies the active class only to the current page button', () => {
+ const { container } = render(() => (
+
+ ));
+ // currentPage is 0-based (2) → 1-based page "3" is active
+ expect(pageButton(container, '3').className).toContain('button_active');
+ expect(pageButton(container, '4').className).not.toContain('button_active');
+ expect(pageButton(container, '1').className).not.toContain('button_active');
+ });
+
+ it('moves the active class when currentPage changes reactively', () => {
+ const [current, setCurrent] = createSignal(2);
+ const { container } = render(() => (
+
+ ));
+ expect(pageButton(container, '3').className).toContain('button_active');
+ setCurrent(3);
+ expect(pageButton(container, '3').className).not.toContain('button_active');
+ expect(pageButton(container, '4').className).toContain('button_active');
+ });
+
+ it('calls onPageChange with the 0-based index when a page button is clicked', () => {
+ const { container } = render(() => (
+
+ ));
+ fireEvent.click(pageButton(container, '2'));
+ expect(baseProps.onPageChange).toHaveBeenCalledWith(1);
+ });
+
+ it('prev/next buttons call onPageChange with currentPage - 1 / + 1', () => {
+ const { container } = render(() => (
+
+ ));
+ const buttons = container.querySelectorAll('button');
+ // buttons: prev, 1..5, next, page-size-trigger (mock)
+ fireEvent.click(buttons[0]); // prev
+ expect(baseProps.onPageChange).toHaveBeenLastCalledWith(1);
+ fireEvent.click(buttons[buttons.length - 2]); // next (before mock trigger)
+ expect(baseProps.onPageChange).toHaveBeenLastCalledWith(3);
+ });
+
+ it('disables prev on the first page and next on the last page', () => {
+ const { container: first } = render(() => (
+
+ ));
+ // buttons: prev, 1..5, next, page-size-trigger (mock)
+ const firstBtns = first.querySelectorAll('button');
+ expect(firstBtns[0]).toBeDisabled();
+ expect(firstBtns[firstBtns.length - 2]).not.toBeDisabled();
+
+ const { container: last } = render(() => (
+
+ ));
+ const lastBtns = last.querySelectorAll('button');
+ expect(lastBtns[0]).not.toBeDisabled();
+ expect(lastBtns[lastBtns.length - 2]).toBeDisabled();
+ });
+
+ it('does not render page buttons when totalPages <= 1', () => {
+ const { container } = render(() => (
+
+ ));
+ // Only the page-size trigger button exists (no prev/next/page buttons).
+ const buttons = container.querySelectorAll('button');
+ expect(buttons).toHaveLength(1);
+ });
+
+ it('calls onPageSizeChange when a page-size option is selected', () => {
+ const { getByTestId } = render(() => (
+
+ ));
+ fireEvent.click(getByTestId('page-size-trigger'));
+ fireEvent.click(getByTestId('page-size-menu').querySelectorAll('[class~="item"]')[1]);
+ expect(baseProps.onPageSizeChange).toHaveBeenCalledWith(20);
+ });
+});
diff --git a/client_v2/src/__tests__/query-log-filters.test.ts b/client_v2/src/__tests__/query-log-filters.test.ts
new file mode 100644
index 00000000000..84ebc0c69d6
--- /dev/null
+++ b/client_v2/src/__tests__/query-log-filters.test.ts
@@ -0,0 +1,31 @@
+import { describe, expect, test } from 'vitest';
+
+import {
+ DEFAULT_LOGS_FILTER,
+ QUERY_LOG_REASON_FILTER,
+ QUERY_LOG_STATUS_FILTER,
+} from '../helpers/constants';
+import { getLogsUrlParams } from '../helpers/helpers';
+
+describe('Query Log filter model', () => {
+ test('stores search, status, and reason separately', () => {
+ expect(DEFAULT_LOGS_FILTER).toEqual({
+ search: '',
+ status: 'all',
+ reason: 'all',
+ });
+ });
+
+ test('serializes all three filter fields into the URL', () => {
+ expect(getLogsUrlParams('example.org', 'blocked', 'FilteredBlockedService')).toBe(
+ '?search=example.org&status=blocked&reason=FilteredBlockedService',
+ );
+ });
+
+ test('keeps status and reason option sets separate', () => {
+ expect(QUERY_LOG_STATUS_FILTER.REWRITTEN.QUERY).toBe('rewritten');
+ expect(Object.keys(QUERY_LOG_STATUS_FILTER)).not.toContain('ERROR');
+ expect(QUERY_LOG_REASON_FILTER.BLOCKED_BY_FILTER.QUERY).toBe('FilteredBlackList');
+ expect(QUERY_LOG_REASON_FILTER.SAFE_SEARCH.QUERY).toBe('FilteredSafeSearch');
+ });
+});
diff --git a/client_v2/src/__tests__/query-log-helpers.test.ts b/client_v2/src/__tests__/query-log-helpers.test.ts
new file mode 100644
index 00000000000..b6b0468b8fd
--- /dev/null
+++ b/client_v2/src/__tests__/query-log-helpers.test.ts
@@ -0,0 +1,195 @@
+import { afterEach, describe, expect, test, vi } from 'vitest';
+
+import intl from 'panel/common/intl';
+import { FILTERED_STATUS, SPECIAL_FILTER_ID } from '../helpers/constants';
+import {
+ filterLogsByStatus,
+ getQueryReasonLabel,
+ getQueryReasonDetails,
+ getQueryReasonKey,
+ getQueryStatusLabel,
+ getQueryStatusKey,
+} from '../components/QueryLog/helpers';
+
+afterEach(() => {
+ vi.restoreAllMocks();
+});
+
+describe('getQueryStatusKey', () => {
+ test('maps safe search and rewrites to rewritten status', () => {
+ expect(getQueryStatusKey(FILTERED_STATUS.FILTERED_SAFE_SEARCH)).toBe('rewritten');
+ expect(getQueryStatusKey(FILTERED_STATUS.REWRITE)).toBe('rewritten');
+ });
+
+ test('maps allowlisted and plain answers to allowed/processed', () => {
+ expect(getQueryStatusKey(FILTERED_STATUS.NOT_FILTERED_WHITE_LIST)).toBe('allowed');
+ expect(getQueryStatusKey(FILTERED_STATUS.NOT_FILTERED_NOT_FOUND)).toBe('processed');
+ });
+
+ test('falls back to rewritten when the row has an original response', () => {
+ expect(
+ getQueryStatusKey('UnexpectedReason', [{ value: '203.0.113.10', type: 'A', ttl: 300 }]),
+ ).toBe('rewritten');
+ });
+});
+
+describe('getQueryReasonKey', () => {
+ test('keeps custom rules separate from blocklist rules', () => {
+ expect(
+ getQueryReasonKey(FILTERED_STATUS.FILTERED_BLACK_LIST, [
+ {
+ filter_list_id: SPECIAL_FILTER_ID.CUSTOM_FILTERING_RULES,
+ text: '||example.org^',
+ },
+ ]),
+ ).toBe('custom_filtering_rules');
+
+ expect(
+ getQueryReasonKey(FILTERED_STATUS.FILTERED_BLACK_LIST, [
+ { filter_list_id: 42, text: '||example.org^' },
+ ]),
+ ).toBe('blocked_by_filter');
+ });
+
+ test('maps threat, parental, safe-search, allowlist, and rewrite reasons', () => {
+ expect(getQueryReasonKey(FILTERED_STATUS.FILTERED_SAFE_BROWSING, [])).toBe(
+ 'blocked_threats',
+ );
+ expect(getQueryReasonKey(FILTERED_STATUS.FILTERED_PARENTAL, [])).toBe(
+ 'blocked_by_parental_control',
+ );
+ expect(getQueryReasonKey(FILTERED_STATUS.FILTERED_SAFE_SEARCH, [])).toBe('safe_search');
+ expect(getQueryReasonKey(FILTERED_STATUS.NOT_FILTERED_WHITE_LIST, [])).toBe('allowlists');
+ expect(getQueryReasonKey(FILTERED_STATUS.REWRITE, [])).toBe('dns_rewrites');
+ });
+});
+
+describe('query log label helpers', () => {
+ test('maps status keys through static intl keys', () => {
+ const getMessageSpy = vi.spyOn(intl, 'getMessage').mockImplementation((key) => key);
+
+ expect(getQueryStatusLabel('blocked')).toBe('query_log_blocked');
+ expect(getQueryStatusLabel('allowed')).toBe('query_log_allowed');
+ expect(getQueryStatusLabel('processed')).toBe('query_log_processed');
+ expect(getQueryStatusLabel('rewritten')).toBe('query_log_rewritten');
+ expect(getQueryStatusLabel('error')).toBe('error');
+
+ expect(getMessageSpy).toHaveBeenNthCalledWith(1, 'query_log_blocked');
+ expect(getMessageSpy).toHaveBeenNthCalledWith(2, 'query_log_allowed');
+ expect(getMessageSpy).toHaveBeenNthCalledWith(3, 'query_log_processed');
+ expect(getMessageSpy).toHaveBeenNthCalledWith(4, 'query_log_rewritten');
+ expect(getMessageSpy).toHaveBeenNthCalledWith(5, 'error');
+ });
+
+ test('maps reason keys through static intl keys and keeps none literal', () => {
+ const getMessageSpy = vi.spyOn(intl, 'getMessage').mockImplementation((key) => key);
+
+ expect(getQueryReasonLabel('none')).toBe('-');
+ expect(getQueryReasonLabel('blocked_by_filter')).toBe('query_log_blocked_by_filter');
+ expect(getQueryReasonLabel('custom_filtering_rules')).toBe(
+ 'query_log_custom_filtering_rules',
+ );
+ expect(getQueryReasonLabel('blocked_services')).toBe('query_log_blocked_services');
+ expect(getQueryReasonLabel('blocked_threats')).toBe('query_log_blocked_threats');
+ expect(getQueryReasonLabel('blocked_by_parental_control')).toBe(
+ 'query_log_blocked_by_parental_control',
+ );
+ expect(getQueryReasonLabel('safe_search')).toBe('query_log_safe_search');
+ expect(getQueryReasonLabel('dns_rewrites')).toBe('dns_rewrites');
+ expect(getQueryReasonLabel('allowlists')).toBe('allowlists');
+ expect(getQueryReasonLabel('error')).toBe('error');
+
+ expect(getMessageSpy).toHaveBeenNthCalledWith(1, 'query_log_blocked_by_filter');
+ expect(getMessageSpy).toHaveBeenNthCalledWith(2, 'query_log_custom_filtering_rules');
+ expect(getMessageSpy).toHaveBeenNthCalledWith(3, 'query_log_blocked_services');
+ expect(getMessageSpy).toHaveBeenNthCalledWith(4, 'query_log_blocked_threats');
+ expect(getMessageSpy).toHaveBeenNthCalledWith(5, 'query_log_blocked_by_parental_control');
+ expect(getMessageSpy).toHaveBeenNthCalledWith(6, 'query_log_safe_search');
+ expect(getMessageSpy).toHaveBeenNthCalledWith(7, 'dns_rewrites');
+ expect(getMessageSpy).toHaveBeenNthCalledWith(8, 'allowlists');
+ expect(getMessageSpy).toHaveBeenNthCalledWith(9, 'error');
+ });
+});
+
+describe('getQueryReasonDetails', () => {
+ const filters = [
+ {
+ enabled: true,
+ id: 42,
+ lastUpdated: '',
+ name: 'AdGuard DNS filter',
+ rulesCount: 0,
+ url: '',
+ },
+ ];
+
+ const whitelistFilters = [
+ {
+ enabled: true,
+ id: 7,
+ lastUpdated: '',
+ name: "Dan Pollock's List",
+ rulesCount: 0,
+ url: '',
+ },
+ ];
+
+ const services = [{ id: 'amazon', name: 'Amazon' }];
+
+ test('returns filter names for blocklist and allowlist reasons', () => {
+ expect(
+ getQueryReasonDetails({
+ elapsedMs: '17',
+ filters,
+ reason: FILTERED_STATUS.FILTERED_BLACK_LIST,
+ rules: [{ filter_list_id: 42, text: '||example.org^' }],
+ serviceName: '',
+ services,
+ whitelistFilters,
+ }),
+ ).toBe('AdGuard DNS filter');
+
+ expect(
+ getQueryReasonDetails({
+ elapsedMs: '31',
+ filters,
+ reason: FILTERED_STATUS.NOT_FILTERED_WHITE_LIST,
+ rules: [{ filter_list_id: 7, text: '@@||example.org^' }],
+ serviceName: '',
+ services,
+ whitelistFilters,
+ }),
+ ).toBe("Dan Pollock's List");
+ });
+
+ test('returns the service name for blocked-services rows', () => {
+ expect(
+ getQueryReasonDetails({
+ elapsedMs: '37',
+ filters,
+ reason: FILTERED_STATUS.FILTERED_BLOCKED_SERVICE,
+ rules: [],
+ serviceName: 'amazon',
+ services,
+ whitelistFilters,
+ }),
+ ).toBe('Amazon');
+ });
+});
+
+describe('filterLogsByStatus', () => {
+ const logs = [
+ { reason: FILTERED_STATUS.NOT_FILTERED_NOT_FOUND, originalResponse: [] },
+ { reason: FILTERED_STATUS.FILTERED_BLACK_LIST, originalResponse: [] },
+ { reason: FILTERED_STATUS.NOT_FILTERED_WHITE_LIST, originalResponse: [] },
+ { reason: FILTERED_STATUS.REWRITE, originalResponse: [] },
+ ] as any[];
+
+ test('filters logs by generic status category', () => {
+ expect(filterLogsByStatus(logs, 'processed')).toHaveLength(1);
+ expect(filterLogsByStatus(logs, 'blocked')).toHaveLength(1);
+ expect(filterLogsByStatus(logs, 'allowed')).toHaveLength(1);
+ expect(filterLogsByStatus(logs, 'rewritten')).toHaveLength(1);
+ expect(filterLogsByStatus(logs, 'all')).toHaveLength(4);
+ });
+});
diff --git a/client_v2/src/__tests__/queryLogStore.test.ts b/client_v2/src/__tests__/queryLogStore.test.ts
new file mode 100644
index 00000000000..82551e457f1
--- /dev/null
+++ b/client_v2/src/__tests__/queryLogStore.test.ts
@@ -0,0 +1,118 @@
+import { describe, expect, it, vi, beforeEach } from 'vitest';
+import { getAdditionalLogs, setFilteredLogs, queryLogsState } from 'panel/stores/queryLogs';
+import { apiClient } from 'panel/api/Api';
+
+vi.mock('panel/api/Api', () => ({
+ apiClient: {
+ getQueryLog: vi.fn(),
+ },
+}));
+
+vi.mock('panel/stores/toasts', () => ({
+ addErrorToast: vi.fn(),
+ addSuccessToast: vi.fn(),
+}));
+
+describe('queryLogs store', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('getAdditionalLogs appends filtered rows using the oldest cursor and filter', async () => {
+ (apiClient.getQueryLog as any).mockReset();
+
+ // Seed state: a full first page (20 rewritten) with more behind it.
+ const fullPage = Array.from({ length: 20 }, () => ({
+ reason: 'Rewrite',
+ question: {},
+ }));
+ (apiClient.getQueryLog as any).mockResolvedValueOnce({
+ data: fullPage,
+ oldest: 'cur',
+ });
+ await setFilteredLogs({ search: '', status: 'rewritten', reason: 'all' });
+ expect(queryLogsState.isEntireLog).toBe(false);
+ expect(queryLogsState.logs).toHaveLength(20);
+
+ // Load more: one extra rewritten entry, then end of log.
+ (apiClient.getQueryLog as any).mockResolvedValueOnce({
+ data: [{ reason: 'Rewrite', question: {} }],
+ oldest: '',
+ });
+
+ await getAdditionalLogs();
+
+ // The load-more request must carry the cursor and the reason filter.
+ expect(apiClient.getQueryLog).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ older_than: 'cur',
+ reason: expect.arrayContaining([
+ 'Rewrite',
+ 'RewriteEtcHosts',
+ 'RewriteRule',
+ 'FilteredSafeSearch',
+ ]),
+ }),
+ );
+ // Must NOT send the deprecated response_status
+ const lastCall = (apiClient.getQueryLog as any).mock.calls.at(-1)[0];
+ expect(lastCall).not.toHaveProperty('response_status');
+
+ expect(queryLogsState.logs).toHaveLength(21);
+ expect(queryLogsState.isEntireLog).toBe(true);
+ expect(queryLogsState.processingAdditionalLogs).toBe(false);
+ });
+
+ it('setFilteredLogs sends reason strings for blocked status', async () => {
+ (apiClient.getQueryLog as any).mockReset();
+ (apiClient.getQueryLog as any).mockResolvedValue({
+ data: [{ reason: 'FilteredBlackList', question: {} }],
+ oldest: '',
+ });
+
+ await setFilteredLogs({ search: '', status: 'blocked', reason: 'all' });
+
+ const lastCall = (apiClient.getQueryLog as any).mock.calls.at(-1)[0];
+ expect(apiClient.getQueryLog).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ reason: expect.arrayContaining([
+ 'FilteredBlackList',
+ 'FilteredSafeBrowsing',
+ 'FilteredParental',
+ 'FilteredBlockedService',
+ ]),
+ }),
+ );
+ expect(lastCall).not.toHaveProperty('response_status');
+ });
+
+ it('does not mark the log as complete when additional loading stops', async () => {
+ (apiClient.getQueryLog as any).mockResolvedValue({
+ data: [],
+ oldest: 'next-cursor',
+ });
+
+ await getAdditionalLogs();
+
+ expect(queryLogsState.processingAdditionalLogs).toBe(false);
+ expect(queryLogsState.isEntireLog).toBe(false);
+ });
+
+ it('accumulates pages until oldest is empty (short-polling)', async () => {
+ (apiClient.getQueryLog as any)
+ .mockResolvedValueOnce({
+ data: [{ reason: 'Rewrite' }],
+ oldest: 'cur',
+ is_entire_log: false,
+ })
+ .mockResolvedValueOnce({
+ data: [{ reason: 'Rewrite' }],
+ oldest: '',
+ is_entire_log: true,
+ });
+
+ await setFilteredLogs({ search: '', status: 'rewritten', reason: 'all' });
+ expect(apiClient.getQueryLog).toHaveBeenCalledTimes(2);
+ expect(queryLogsState.processingGetLogs).toBe(false);
+ });
+});
diff --git a/client_v2/src/__tests__/schedule-helpers.test.ts b/client_v2/src/__tests__/schedule-helpers.test.ts
new file mode 100644
index 00000000000..c2446caebff
--- /dev/null
+++ b/client_v2/src/__tests__/schedule-helpers.test.ts
@@ -0,0 +1,143 @@
+import { describe, test, expect } from 'vitest';
+import {
+ msToTime,
+ timeToMs,
+ formatTimePeriod,
+ isFullDay,
+ FULL_DAY_END_MS,
+ DAYS_OF_WEEK,
+ getNextTimeValue,
+ getNormalizedEndTime,
+ getEndTimeOptions,
+} from '../components/BlockedServices/InactivitySchedule/helpers';
+
+describe('schedule helpers', () => {
+ describe('msToTime', () => {
+ test('converts 0ms to 00:00', () => {
+ expect(msToTime(0)).toEqual({ hours: 0, minutes: 0 });
+ });
+
+ test('converts 3600000ms to 01:00', () => {
+ expect(msToTime(3600000)).toEqual({ hours: 1, minutes: 0 });
+ });
+
+ test('converts 86340000ms to 23:59', () => {
+ expect(msToTime(86340000)).toEqual({ hours: 23, minutes: 59 });
+ });
+
+ test('converts 45060000ms to 12:31', () => {
+ expect(msToTime(45060000)).toEqual({ hours: 12, minutes: 31 });
+ });
+ });
+
+ describe('timeToMs', () => {
+ test('converts 00:00 to 0ms', () => {
+ expect(timeToMs(0, 0)).toBe(0);
+ });
+
+ test('converts 01:00 to 3600000ms', () => {
+ expect(timeToMs(1, 0)).toBe(3600000);
+ });
+
+ test('converts 23:59 to 86340000ms', () => {
+ expect(timeToMs(23, 59)).toBe(86340000);
+ });
+ });
+
+ describe('formatTimePeriod', () => {
+ test('formats start and end as HH:MM – HH:MM', () => {
+ expect(formatTimePeriod(3600000, 64800000)).toBe('01:00 \u2013 18:00');
+ });
+
+ test('pads single digits', () => {
+ expect(formatTimePeriod(0, 3660000)).toBe('00:00 \u2013 01:01');
+ });
+ });
+
+ describe('getNextTimeValue', () => {
+ test('returns the next minute within the same hour', () => {
+ expect(getNextTimeValue({ hours: 10, minutes: 15 })).toEqual({
+ hours: 10,
+ minutes: 16,
+ });
+ });
+
+ test('rolls over to the next hour', () => {
+ expect(getNextTimeValue({ hours: 10, minutes: 59 })).toEqual({
+ hours: 11,
+ minutes: 0,
+ });
+ });
+
+ test('returns null for the last minute of the day', () => {
+ expect(getNextTimeValue({ hours: 23, minutes: 59 })).toBeNull();
+ });
+ });
+
+ describe('getNormalizedEndTime', () => {
+ test('keeps a valid end time unchanged', () => {
+ expect(
+ getNormalizedEndTime({ hours: 10, minutes: 15 }, { hours: 11, minutes: 0 }),
+ ).toEqual({ hours: 11, minutes: 0 });
+ });
+
+ test('moves an invalid end time to the next available minute', () => {
+ expect(
+ getNormalizedEndTime({ hours: 10, minutes: 15 }, { hours: 10, minutes: 15 }),
+ ).toEqual({ hours: 10, minutes: 16 });
+ });
+
+ test('returns null when no later end time exists', () => {
+ expect(
+ getNormalizedEndTime({ hours: 23, minutes: 59 }, { hours: 23, minutes: 59 }),
+ ).toBeNull();
+ });
+ });
+
+ describe('getEndTimeOptions', () => {
+ test('disables end hours earlier than the selected start hour', () => {
+ const options = getEndTimeOptions({ hours: 10, minutes: 15 }, 11);
+
+ expect(options.hours[9].isDisabled).toBe(true);
+ expect(options.hours[10].isDisabled).toBe(false);
+ expect(options.hours[11].isDisabled).toBe(false);
+ });
+
+ test('disables end minutes not later than the selected start minute in the same hour', () => {
+ const options = getEndTimeOptions({ hours: 10, minutes: 15 }, 10);
+
+ expect(options.minutes[15].isDisabled).toBe(true);
+ expect(options.minutes[16].isDisabled).toBe(false);
+ });
+
+ test('disables all end options when start time is 23:59', () => {
+ const options = getEndTimeOptions({ hours: 23, minutes: 59 }, 23);
+
+ expect(options.hasAvailableEndTime).toBe(false);
+ expect(options.hours.every((option) => option.isDisabled)).toBe(true);
+ expect(options.minutes.every((option) => option.isDisabled)).toBe(true);
+ });
+ });
+
+ describe('isFullDay', () => {
+ test('returns true for start=0, end=86340000', () => {
+ expect(isFullDay(0, 86340000)).toBe(true);
+ });
+
+ test('returns false for partial day', () => {
+ expect(isFullDay(0, 3600000)).toBe(false);
+ });
+ });
+
+ describe('FULL_DAY_END_MS', () => {
+ test('equals 86340000', () => {
+ expect(FULL_DAY_END_MS).toBe(86340000);
+ });
+ });
+
+ describe('DAYS_OF_WEEK', () => {
+ test('has 7 days in correct order', () => {
+ expect(DAYS_OF_WEEK).toEqual(['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']);
+ });
+ });
+});
diff --git a/client_v2/src/__tests__/select-helpers.test.ts b/client_v2/src/__tests__/select-helpers.test.ts
new file mode 100644
index 00000000000..e3f21c4632c
--- /dev/null
+++ b/client_v2/src/__tests__/select-helpers.test.ts
@@ -0,0 +1,117 @@
+import { describe, it, expect } from 'vitest';
+import { optionToValue, filterOptions, getItemTestId } from '../common/controls/Select/helpers';
+
+const OPTIONS = [
+ { value: 'apple', label: 'Apple' },
+ { value: 'banana', label: 'Banana' },
+ { value: null, label: 'No value' },
+ { value: 42, label: 'Number' },
+];
+
+describe('optionToValue', () => {
+ it('returns empty string for null', () => {
+ expect(optionToValue(null)).toBe('');
+ });
+
+ it('returns empty string for undefined', () => {
+ expect(optionToValue(undefined)).toBe('');
+ });
+
+ it('returns the string for string values', () => {
+ expect(optionToValue('hello')).toBe('hello');
+ });
+
+ it('returns the string representation for numbers', () => {
+ expect(optionToValue(42)).toBe('42');
+ });
+
+ it('preserves falsy number 0 (does NOT return empty string)', () => {
+ // Bug #1 regression test: 0 is falsy but a valid value — must not be
+ // coerced to '' (which would break selection matching).
+ expect(optionToValue(0)).toBe('0');
+ });
+
+ it('returns "true" for boolean true', () => {
+ expect(optionToValue(true)).toBe('true');
+ });
+
+ it('returns "false" for boolean false', () => {
+ // false is falsy but a valid value — must not be coerced to ''.
+ expect(optionToValue(false)).toBe('false');
+ });
+
+ it('stringifies objects via toString()', () => {
+ expect(optionToValue({})).toBe('[object Object]');
+ });
+});
+
+describe('filterOptions', () => {
+ it('returns all options when query is empty', () => {
+ expect(filterOptions(OPTIONS, '')).toEqual(OPTIONS);
+ });
+
+ it('returns the same array reference when query is empty (no copy)', () => {
+ const result = filterOptions(OPTIONS, '');
+ expect(result).toBe(OPTIONS);
+ });
+
+ it('matches by label (case-insensitive)', () => {
+ const result = filterOptions(OPTIONS, 'app');
+ expect(result).toEqual([{ value: 'apple', label: 'Apple' }]);
+ });
+
+ it('matches partial labels', () => {
+ const result = filterOptions(OPTIONS, 'ban');
+ expect(result).toHaveLength(1);
+ expect(result[0].label).toBe('Banana');
+ });
+
+ it('returns empty array when no match', () => {
+ const result = filterOptions(OPTIONS, 'xyz');
+ expect(result).toEqual([]);
+ });
+
+ it('does NOT match by value (only by label)', () => {
+ const opts = [{ value: 'hidden-id', label: 'Visible label' }];
+ const result = filterOptions(opts, 'hidden');
+ expect(result).toEqual([]);
+ });
+
+ it('matches case-insensitively (uppercase query)', () => {
+ const result = filterOptions(OPTIONS, 'BANANA');
+ expect(result).toHaveLength(1);
+ });
+
+ it('handles options with non-string labels by stringifying them', () => {
+ const opts = [{ value: 1, label: 123 }];
+ const result = filterOptions(opts, '12');
+ expect(result).toHaveLength(1);
+ });
+});
+
+describe('getItemTestId', () => {
+ it('returns undefined when no prefix is given', () => {
+ expect(getItemTestId(undefined, 'apple')).toBeUndefined();
+ });
+
+ it('returns "prefix-value" for string values', () => {
+ expect(getItemTestId('option', 'apple')).toBe('option-apple');
+ });
+
+ it('returns "prefix-" for null values (empty value after dash)', () => {
+ // Uses optionToValue which converts null → ''.
+ expect(getItemTestId('option', null)).toBe('option-');
+ });
+
+ it('returns "prefix-42" for number values', () => {
+ expect(getItemTestId('option', 42)).toBe('option-42');
+ });
+
+ it('preserves falsy number 0', () => {
+ expect(getItemTestId('option', 0)).toBe('option-0');
+ });
+
+ it('returns undefined when prefix is empty string', () => {
+ expect(getItemTestId('', 'apple')).toBeUndefined();
+ });
+});
diff --git a/client_v2/src/__tests__/select.test.tsx b/client_v2/src/__tests__/select.test.tsx
new file mode 100644
index 00000000000..50113372969
--- /dev/null
+++ b/client_v2/src/__tests__/select.test.tsx
@@ -0,0 +1,300 @@
+import { render } from '@solidjs/testing-library';
+import { describe, it, expect, vi } from 'vitest';
+import { Select } from '../common/controls/Select/Select';
+
+const OPTIONS = [
+ { value: '0.0.0.0', label: 'All interfaces' },
+ { value: '127.0.0.1', label: 'Loopback' },
+];
+
+const MANY_OPTIONS = Array.from({ length: 15 }, (_, i) => ({
+ value: `opt-${i}`,
+ label: `Option ${i}`,
+}));
+
+describe('Select', () => {
+ it('renders the indicator inside the trigger button', () => {
+ render(() => (
+ {}}
+ placeholder="Select interface"
+ />
+ ));
+
+ const trigger = document.querySelector(
+ '[data-scope="select"][data-part="trigger"]',
+ ) as HTMLElement;
+ const indicator = trigger?.querySelector('[data-part="indicator"]');
+
+ // The indicator (arrow icon) should be inside the trigger.
+ expect(indicator).toBeTruthy();
+ });
+});
+
+describe('Select — single-select (searchable combobox)', () => {
+ it('shows the selected value via an overlay above the input', () => {
+ // react-select parity: the value text is rendered behind the search
+ // input (same grid cell) so it stays visible until the user types.
+ render(() => (
+ {}}
+ isSearchable
+ placeholder="Select interface"
+ />
+ ));
+
+ const overlay = document.querySelector('.solid-combobox-single-value');
+ expect(overlay).toBeInTheDocument();
+ expect(overlay?.textContent).toBe(OPTIONS[1].label);
+
+ // The overlay and the input share the same grid wrapper.
+ const wrapper = document.querySelector('.solid-combobox-single-value-wrapper');
+ expect(wrapper).toBeInTheDocument();
+ expect(
+ wrapper?.querySelector('[data-scope="combobox"][data-part="input"]'),
+ ).toBeInTheDocument();
+
+ // The input's own placeholder is empty — the overlay shows the value.
+ const input = document.querySelector(
+ '[data-scope="combobox"][data-part="input"]',
+ ) as HTMLInputElement;
+ expect(input.placeholder).toBe('');
+ });
+
+ it('shows the placeholder overlay when no value is selected', () => {
+ render(() => (
+ {}}
+ isSearchable
+ placeholder="Select interface"
+ />
+ ));
+
+ // No value overlay.
+ expect(document.querySelector('.solid-combobox-single-value')).not.toBeInTheDocument();
+
+ // Placeholder text is rendered instead.
+ const placeholder = document.querySelector('.solid-select-placeholder');
+ expect(placeholder).toBeInTheDocument();
+ expect(placeholder?.textContent).toBe('Select interface');
+ });
+});
+
+describe('Select — multi-select (searchable combobox)', () => {
+ it('places the search input inline inside the multi-value container', () => {
+ render(() => (
+ {}}
+ isMulti
+ isSearchable
+ placeholder="Pick tags"
+ />
+ ));
+
+ const container = document.querySelector('.solid-select-multi-value-container');
+ expect(container).toBeInTheDocument();
+
+ // The search input must be a descendant of the multi-value container so
+ // it flows inline after the pills (mimics react-select).
+ const inputInside = container?.querySelector('[data-scope="combobox"][data-part="input"]');
+ expect(inputInside).toBeInTheDocument();
+ });
+
+ it('hides the dropdown arrow and shows the clear button when values exist', () => {
+ // NOTE: isClearable is intentionally NOT passed. react-select v2's
+ // CustomClearIndicator only checks hasValue, and no consumer passes
+ // isClearable, so the clear button must appear for multi-select
+ // whenever values exist.
+ render(() => (
+ {}}
+ isMulti
+ isSearchable
+ />
+ ));
+
+ // Dropdown arrow is replaced by the clear button (react-select parity).
+ const trigger = document.querySelector('[data-scope="combobox"][data-part="trigger"]');
+ expect(trigger).not.toBeInTheDocument();
+
+ const clearTrigger = document.querySelector(
+ '[data-scope="combobox"][data-part="clear-trigger"]',
+ );
+ expect(clearTrigger).toBeInTheDocument();
+ });
+});
+
+describe('Select — multi-select (non-searchable) regression', () => {
+ it('keeps the multi-value container that wraps the pills', () => {
+ // SelectMultiValue no longer renders its own container div; both the
+ // searchable and non-searchable branches must provide it (see C1 fix).
+ render(() => (
+ {}}
+ isMulti
+ isSearchable={false}
+ />
+ ));
+
+ const container = document.querySelector('.solid-select-multi-value-container');
+ expect(container).toBeInTheDocument();
+ });
+});
+
+describe('Select — empty state (nothing found)', () => {
+ it('renders a nothing-found message when the search yields no options', () => {
+ render(() => (
+ {}} isSearchable menuIsOpen placeholder="Search" />
+ ));
+
+ const empty = document.querySelector('[data-scope="combobox"][data-part="empty"]');
+ expect(empty).toBeInTheDocument();
+ expect(empty?.textContent).toContain('Nothing found');
+ });
+});
+
+describe('Select — menuFooter', () => {
+ const Footer = () => Footer content
;
+
+ it('renders menuFooter in non-searchable mode', () => {
+ render(() => (
+ {}}
+ isSearchable={false}
+ menuIsOpen
+ menuFooter={}
+ />
+ ));
+
+ // The footer must appear inside the dropdown content.
+ const content = document.querySelector('[data-scope="select"][data-part="content"]');
+ expect(content).toBeInTheDocument();
+ expect(content?.querySelector('[data-testid="test-footer"]')).toBeInTheDocument();
+ });
+
+ it('renders menuFooter in searchable non-lazy mode (Bug #2 regression)', () => {
+ // Previously, menuFooter was only rendered in the lazy branch for the
+ // searchable Combobox — the non-lazy branch omitted it entirely.
+ render(() => (
+ {}}
+ isSearchable
+ menuIsOpen
+ menuFooter={}
+ />
+ ));
+
+ const content = document.querySelector('[data-scope="combobox"][data-part="content"]');
+ expect(content).toBeInTheDocument();
+ expect(content?.querySelector('[data-testid="test-footer"]')).toBeInTheDocument();
+ });
+
+ it('renders menuFooter in searchable lazy mode', () => {
+ render(() => (
+ {}}
+ isSearchable
+ lazyList
+ menuIsOpen
+ menuFooter={}
+ />
+ ));
+
+ const content = document.querySelector('[data-scope="combobox"][data-part="content"]');
+ expect(content).toBeInTheDocument();
+ expect(content?.querySelector('[data-testid="test-footer"]')).toBeInTheDocument();
+ });
+});
+
+describe('Select — autoFocus', () => {
+ it('calls focus on the trigger in non-searchable mode', () => {
+ // jsdom may not actually move focus, but we can verify that
+ // the ref callback calls `el.focus()` when `autoFocus` is set.
+ const focusSpy = vi.spyOn(HTMLElement.prototype, 'focus');
+
+ render(() => (
+ {}}
+ isSearchable={false}
+ autoFocus
+ />
+ ));
+
+ const trigger = document.querySelector('[data-scope="select"][data-part="trigger"]');
+ expect(trigger).toBeTruthy();
+ expect(focusSpy).toHaveBeenCalled();
+
+ focusSpy.mockRestore();
+ });
+});
+
+describe('Select — onMenuScrollToBottom', () => {
+ it('fires when the lazy menu list is scrolled (Bug #3 regression)', () => {
+ // Bug #3: in lazy mode, the scrollable inner div (data-part="menu-list")
+ // had no onScroll handler — scroll events don't bubble to the Ark UI
+ // Content element, so the callback never fired. The fix attaches
+ // onScroll to both Content and the inner div.
+ const onScrollToBottom = vi.fn();
+
+ render(() => (
+ {}}
+ isSearchable
+ lazyList
+ menuIsOpen
+ onMenuScrollToBottom={onScrollToBottom}
+ />
+ ));
+
+ // The lazy list renders a scrollable inner div identified by
+ // data-part="menu-list" (CSS module class names are not available in
+ // the test environment with css: false).
+ const menuList = document.querySelector('[data-part="menu-list"]') as HTMLElement;
+ expect(menuList).toBeInTheDocument();
+
+ // In jsdom, all scroll dimensions are 0 so the "near bottom" condition
+ // (scrollHeight - scrollTop <= clientHeight + 50) is always true.
+ menuList.dispatchEvent(new Event('scroll'));
+
+ expect(onScrollToBottom).toHaveBeenCalledTimes(1);
+ });
+});
+
+describe('Select — auto-searchable threshold', () => {
+ it('uses non-searchable ArkSelect for <= 10 options by default', () => {
+ render(() => {}} />);
+
+ // ArkSelect renders data-scope="select".
+ expect(document.querySelector('[data-scope="select"]')).toBeInTheDocument();
+ expect(document.querySelector('[data-scope="combobox"]')).not.toBeInTheDocument();
+ });
+
+ it('auto-enables search for > 10 options by default (single-select)', () => {
+ render(() => {}} />);
+
+ expect(document.querySelector('[data-scope="combobox"]')).toBeInTheDocument();
+ expect(document.querySelector('[data-scope="select"]')).not.toBeInTheDocument();
+ });
+});
diff --git a/client_v2/src/__tests__/setup.ts b/client_v2/src/__tests__/setup.ts
new file mode 100644
index 00000000000..23433121063
--- /dev/null
+++ b/client_v2/src/__tests__/setup.ts
@@ -0,0 +1,21 @@
+import '@testing-library/jest-dom/vitest';
+import { afterEach } from 'vitest';
+import { cleanup } from '@solidjs/testing-library';
+
+afterEach(() => cleanup());
+
+// Mock window.matchMedia for components that use useIsMobile
+Object.defineProperty(window, 'matchMedia', {
+ writable: true,
+ value: (query: string): MediaQueryList =>
+ ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addListener: () => {},
+ removeListener: () => {},
+ addEventListener: () => {},
+ removeEventListener: () => {},
+ dispatchEvent: () => false,
+ }) as MediaQueryList,
+});
diff --git a/client_v2/src/__tests__/static-leases-table.test.tsx b/client_v2/src/__tests__/static-leases-table.test.tsx
new file mode 100644
index 00000000000..e16d91ff1c7
--- /dev/null
+++ b/client_v2/src/__tests__/static-leases-table.test.tsx
@@ -0,0 +1,90 @@
+import { describe, it, expect, vi } from 'vitest';
+import { render, fireEvent } from '@solidjs/testing-library';
+import { StaticLeasesTable } from 'panel/components/Dhcp/LeasesPage/StaticLeasesTable';
+
+const LEASES = [{ mac: 'AA:BB:CC:DD:EE:FF', ip: '192.168.1.100', hostname: 'host1' }];
+
+describe('StaticLeasesTable', () => {
+ it('renders mobile action buttons for all three actions', () => {
+ const { getByTestId } = render(() => (
+ {}}
+ onDelete={() => {}}
+ onRefresh={() => {}}
+ />
+ ));
+
+ expect(getByTestId('static-lease-edit-button')).toBeInTheDocument();
+ expect(getByTestId('static-lease-refresh-button')).toBeInTheDocument();
+ expect(getByTestId('static-lease-delete-button')).toBeInTheDocument();
+ });
+
+ it('renders desktop Dropdown trigger', () => {
+ const { getByTestId } = render(() => (
+ {}}
+ onDelete={() => {}}
+ onRefresh={() => {}}
+ />
+ ));
+
+ expect(getByTestId('static-lease-actions-dropdown')).toBeInTheDocument();
+ });
+
+ it('fires onEdit when mobile edit button clicked', () => {
+ const onEdit = vi.fn();
+ const { getByTestId } = render(() => (
+ {}}
+ onRefresh={() => {}}
+ />
+ ));
+
+ fireEvent.click(getByTestId('static-lease-edit-button'));
+ expect(onEdit).toHaveBeenCalledWith(LEASES[0]);
+ });
+
+ it('fires onRefresh when mobile refresh button clicked', () => {
+ const onRefresh = vi.fn();
+ const { getByTestId } = render(() => (
+ {}}
+ onDelete={() => {}}
+ onRefresh={onRefresh}
+ />
+ ));
+
+ fireEvent.click(getByTestId('static-lease-refresh-button'));
+ expect(onRefresh).toHaveBeenCalled();
+ });
+
+ it('fires onDelete when mobile delete button clicked', () => {
+ const onDelete = vi.fn();
+ const { getByTestId } = render(() => (
+ {}}
+ onDelete={onDelete}
+ onRefresh={() => {}}
+ />
+ ));
+
+ fireEvent.click(getByTestId('static-lease-delete-button'));
+ expect(onDelete).toHaveBeenCalledWith(LEASES[0]);
+ });
+});
diff --git a/client_v2/src/__tests__/stats-store.test.ts b/client_v2/src/__tests__/stats-store.test.ts
new file mode 100644
index 00000000000..94b76574353
--- /dev/null
+++ b/client_v2/src/__tests__/stats-store.test.ts
@@ -0,0 +1,56 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+
+const mocks = vi.hoisted(() => ({
+ getStats: vi.fn(),
+ searchClients: vi.fn(),
+ addErrorToast: vi.fn(),
+}));
+
+vi.mock('panel/api/Api', () => ({
+ apiClient: {
+ getStats: mocks.getStats,
+ searchClients: mocks.searchClients,
+ },
+}));
+vi.mock('panel/stores/toasts', () => ({
+ addErrorToast: mocks.addErrorToast,
+}));
+
+import { getStats, statsState } from 'panel/stores/stats';
+
+describe('getStats', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mocks.searchClients.mockResolvedValue([]);
+ });
+
+ it('enriches top clients and stores normalizedTopClients (FR-001/019)', async () => {
+ mocks.getStats.mockResolvedValue({
+ top_clients: [{ '1.2.3.4': 5 }],
+ avg_processing_time: 0.012,
+ top_blocked_domains: [],
+ top_queried_domains: [],
+ top_upstreams_avg_time: [],
+ top_upstreams_responses: [],
+ });
+
+ await getStats();
+
+ // searchClients was called with discovered client ids
+ expect(mocks.searchClients).toHaveBeenCalledWith({
+ clients: [{ id: '1.2.3.4' }],
+ });
+ // normalizedTopClients is populated (configured bucket carries info name)
+ expect(Object.keys(statsState.normalizedTopClients.auto)).toContain('1.2.3.4');
+ });
+
+ it('converts avg_processing_time to milliseconds, falsy passthrough (FR-002)', async () => {
+ mocks.getStats.mockResolvedValue({ avg_processing_time: 0.012 });
+ await getStats();
+ expect(statsState.avgProcessingTime).toBe(12);
+
+ mocks.getStats.mockResolvedValue({ avg_processing_time: 0 });
+ await getStats();
+ expect(statsState.avgProcessingTime).toBe(0); // not NaN
+ });
+});
diff --git a/client_v2/src/__tests__/toast.test.tsx b/client_v2/src/__tests__/toast.test.tsx
new file mode 100644
index 00000000000..f6e4d342c46
--- /dev/null
+++ b/client_v2/src/__tests__/toast.test.tsx
@@ -0,0 +1,10 @@
+import { describe, it, expect } from 'vitest';
+import { render } from '@solidjs/testing-library';
+import Toast from '../components/Toasts/Toast';
+
+describe('Toast', () => {
+ it('renders the plain message when no options', () => {
+ const { getByText } = render(() => );
+ expect(getByText('hello')).toBeTruthy();
+ });
+});
diff --git a/client_v2/src/__tests__/toasts-store.test.ts b/client_v2/src/__tests__/toasts-store.test.ts
new file mode 100644
index 00000000000..1be967704cd
--- /dev/null
+++ b/client_v2/src/__tests__/toasts-store.test.ts
@@ -0,0 +1,44 @@
+import { describe, it, expect, beforeEach } from 'vitest';
+import {
+ addErrorToast,
+ addSuccessToast,
+ addNoticeToast,
+ removeToast,
+ toastsState,
+} from '../stores/toasts';
+
+describe('toasts store', () => {
+ beforeEach(() => {
+ // Clear all notices between tests.
+ toastsState.notices.forEach((n: any) => removeToast(n.id));
+ });
+
+ it('addNoticeToast stores the message', () => {
+ addNoticeToast('update_failed');
+ const last = toastsState.notices[toastsState.notices.length - 1];
+ expect(last.message).toBe('update_failed');
+ expect(last.type).toBe('notice');
+ });
+
+ it('addErrorToast preserves options and action', () => {
+ const action = { text: 'retry', callback: () => {} };
+ addErrorToast({ error: 'boom', options: { x: 1 }, action });
+ const last = toastsState.notices[toastsState.notices.length - 1];
+ expect(last.message).toBe('boom');
+ expect(last.options).toEqual({ x: 1 });
+ expect(last.action).toEqual(action);
+ expect(last.type).toBe('error');
+ });
+
+ it('addSuccessToast preserves code on object payload', () => {
+ addSuccessToast({ message: 'notify_updated', code: 'notify_updated' });
+ const last = toastsState.notices[toastsState.notices.length - 1];
+ expect(last.code).toBe('notify_updated');
+ });
+
+ it('addSuccessToast accepts a bare string', () => {
+ addSuccessToast('config_successfully_saved');
+ const last = toastsState.notices[toastsState.notices.length - 1];
+ expect(last.message).toBe('config_successfully_saved');
+ });
+});
diff --git a/client_v2/src/__tests__/toggle-blocking.test.ts b/client_v2/src/__tests__/toggle-blocking.test.ts
new file mode 100644
index 00000000000..53ec146ef4e
--- /dev/null
+++ b/client_v2/src/__tests__/toggle-blocking.test.ts
@@ -0,0 +1,84 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { toggleBlocking, BLOCK_ACTIONS, filteringState } from 'panel/stores/filtering';
+
+let lastSetRules = '';
+
+const mocks = vi.hoisted(() => ({
+ apiSetRules: vi.fn((params: any) => {
+ lastSetRules = params?.rules || '';
+ return Promise.resolve(undefined);
+ }),
+ apiGetFilteringStatus: vi.fn(() =>
+ Promise.resolve({
+ user_rules: lastSetRules,
+ filters: [],
+ whitelist_filters: [],
+ interval: 24,
+ enabled: true,
+ }),
+ ),
+ addSuccessToast: vi.fn(),
+ addErrorToast: vi.fn(),
+}));
+
+vi.mock('panel/api/Api', () => ({
+ apiClient: {
+ setRules: mocks.apiSetRules,
+ getFilteringStatus: mocks.apiGetFilteringStatus,
+ },
+}));
+
+vi.mock('panel/stores/toasts', () => ({
+ addSuccessToast: mocks.addSuccessToast,
+ addErrorToast: mocks.addErrorToast,
+ createUndoToast: (message: any, actionLabel: string, onUndo: () => void | Promise) => ({
+ message,
+ actionLabel,
+ undoId: 'test-undo-id',
+ onUndo,
+ }),
+}));
+
+describe('toggleBlocking', () => {
+ beforeEach(async () => {
+ lastSetRules = '';
+ mocks.apiSetRules.mockClear();
+ mocks.addSuccessToast.mockClear();
+ mocks.addErrorToast.mockClear();
+ // Reset store state - the default mock impl returns lastSetRules which is ''
+ const { setRules } = await import('panel/stores/filtering');
+ await setRules('');
+ });
+
+ it('adds a blocking rule when no rule exists', async () => {
+ const result = await toggleBlocking(BLOCK_ACTIONS.BLOCK, 'example.com');
+
+ expect(result).toBe(true);
+ expect(mocks.apiSetRules).toHaveBeenCalled();
+ expect(filteringState.userRules).toContain('||example.com^$important');
+ });
+
+ it('replaces an allowlist rule with a blocking rule', async () => {
+ // Set up initial state with an allowlist rule
+ const { setRules } = await import('panel/stores/filtering');
+ await setRules('@@||allowed.example^$important\n');
+ // After setRules, lastSetRules = '@@||allowed.example^$important\n'
+ // and filteringState.userRules should match
+
+ const result = await toggleBlocking(BLOCK_ACTIONS.BLOCK, 'allowed.example');
+
+ expect(result).toBe(true);
+ expect(filteringState.userRules).toContain('||allowed.example^$important');
+ });
+
+ it('replaces a blocking rule with an allowlist rule', async () => {
+ const { setRules } = await import('panel/stores/filtering');
+ await setRules('||blocked.example^$important\n');
+
+ const result = await toggleBlocking(BLOCK_ACTIONS.UNBLOCK, 'blocked.example');
+
+ expect(result).toBe(true);
+ expect(filteringState.userRules).toContain('@@||blocked.example^$important');
+ });
+});
diff --git a/client_v2/src/api/Api.ts b/client_v2/src/api/Api.ts
new file mode 100644
index 00000000000..228229d4609
--- /dev/null
+++ b/client_v2/src/api/Api.ts
@@ -0,0 +1,746 @@
+import type { LocalesType } from 'panel/common/intl';
+
+const BASE_URL = 'control';
+
+import { getPathWithQueryString } from '../helpers/helpers';
+import { QUERY_LOGS_PAGE_LIMIT, HTML_PAGES, R_PATH_LAST_PART, THEMES } from '../helpers/constants';
+import intl from '../common/intl';
+
+type Theme = (typeof THEMES)[keyof typeof THEMES];
+
+class Api {
+ baseUrl: string;
+
+ constructor(baseUrl: string) {
+ this.baseUrl = baseUrl;
+ }
+
+ async makeRequest(path: any, method = 'POST', config: any = {}) {
+ const pathWithBase = `${this.baseUrl}/${path}`;
+ const url = config.params
+ ? getPathWithQueryString(pathWithBase, config.params)
+ : pathWithBase;
+
+ const fetchConfig: RequestInit = { method };
+ const headers: Record = {};
+
+ if (method !== 'GET' && config.data) {
+ headers['Content-Type'] = config.headers?.['Content-Type'] || 'application/json';
+ fetchConfig.body =
+ typeof config.data === 'string' ? config.data : JSON.stringify(config.data);
+ }
+
+ fetchConfig.headers = headers;
+
+ try {
+ const response = await fetch(url, fetchConfig);
+ const text = await response.text();
+ const data = text
+ ? (() => {
+ try {
+ return JSON.parse(text);
+ } catch {
+ return text;
+ }
+ })()
+ : '';
+
+ if (!response.ok) {
+ const errorPath = url;
+ const { pathname } = document.location;
+ const shouldRedirect =
+ pathname !== HTML_PAGES.LOGIN && pathname !== HTML_PAGES.INSTALL;
+
+ if (response.status === 403 && shouldRedirect) {
+ const loginPageUrl = window.location.href.replace(
+ R_PATH_LAST_PART,
+ HTML_PAGES.LOGIN,
+ );
+ window.location.replace(loginPageUrl);
+ return false;
+ }
+
+ throw new Error(`${errorPath} | ${data} | ${response.status}`);
+ }
+
+ return data;
+ } catch (error) {
+ const errorPath = url;
+
+ if (error instanceof Error && error.message.includes('|')) {
+ throw error;
+ }
+
+ throw new Error(`${errorPath} | ${error.message || error}`);
+ }
+ }
+
+ // Global methods
+ GLOBAL_STATUS = { path: 'status', method: 'GET' };
+
+ GLOBAL_TEST_UPSTREAM_DNS = { path: 'test_upstream_dns', method: 'POST' };
+
+ GLOBAL_VERSION = { path: 'version.json', method: 'POST' };
+
+ GLOBAL_UPDATE = { path: 'update', method: 'POST' };
+
+ getGlobalStatus() {
+ const { path, method } = this.GLOBAL_STATUS;
+
+ return this.makeRequest(path, method);
+ }
+
+ testUpstream(servers: any) {
+ const { path, method } = this.GLOBAL_TEST_UPSTREAM_DNS;
+ const config = {
+ data: servers,
+ };
+ return this.makeRequest(path, method, config);
+ }
+
+ getGlobalVersion(data: any) {
+ const { path, method } = this.GLOBAL_VERSION;
+ const config = {
+ data,
+ };
+ return this.makeRequest(path, method, config);
+ }
+
+ getUpdate() {
+ const { path, method } = this.GLOBAL_UPDATE;
+
+ return this.makeRequest(path, method);
+ }
+
+ // Filtering
+ FILTERING_STATUS = { path: 'filtering/status', method: 'GET' };
+
+ FILTERING_ADD_FILTER = { path: 'filtering/add_url', method: 'POST' };
+
+ FILTERING_REMOVE_FILTER = { path: 'filtering/remove_url', method: 'POST' };
+
+ FILTERING_SET_RULES = { path: 'filtering/set_rules', method: 'POST' };
+
+ FILTERING_REFRESH = { path: 'filtering/refresh', method: 'POST' };
+
+ FILTERING_SET_URL = { path: 'filtering/set_url', method: 'POST' };
+
+ FILTERING_CONFIG = { path: 'filtering/config', method: 'POST' };
+
+ FILTERING_CHECK_HOST = { path: 'filtering/check_host', method: 'GET' };
+
+ getFilteringStatus() {
+ const { path, method } = this.FILTERING_STATUS;
+
+ return this.makeRequest(path, method);
+ }
+
+ refreshFilters(config: any) {
+ const { path, method } = this.FILTERING_REFRESH;
+ const parameters = {
+ data: config,
+ };
+
+ return this.makeRequest(path, method, parameters);
+ }
+
+ addFilter(config: any) {
+ const { path, method } = this.FILTERING_ADD_FILTER;
+ const parameters = {
+ data: config,
+ };
+
+ return this.makeRequest(path, method, parameters);
+ }
+
+ removeFilter(config: any) {
+ const { path, method } = this.FILTERING_REMOVE_FILTER;
+ const parameters = {
+ data: config,
+ };
+
+ return this.makeRequest(path, method, parameters);
+ }
+
+ setRules(rules: any) {
+ const { path, method } = this.FILTERING_SET_RULES;
+ const parameters = {
+ data: rules,
+ };
+ return this.makeRequest(path, method, parameters);
+ }
+
+ setFiltersConfig(config: any) {
+ const { path, method } = this.FILTERING_CONFIG;
+ const parameters = {
+ data: config,
+ };
+ return this.makeRequest(path, method, parameters);
+ }
+
+ setFilterUrl(config: any) {
+ const { path, method } = this.FILTERING_SET_URL;
+ const parameters = {
+ data: config,
+ };
+ return this.makeRequest(path, method, parameters);
+ }
+
+ checkHost(params: any) {
+ const { path, method } = this.FILTERING_CHECK_HOST;
+ const url = getPathWithQueryString(path, params);
+
+ return this.makeRequest(url, method);
+ }
+
+ // Parental
+ PARENTAL_STATUS = { path: 'parental/status', method: 'GET' };
+
+ PARENTAL_ENABLE = { path: 'parental/enable', method: 'POST' };
+
+ PARENTAL_DISABLE = { path: 'parental/disable', method: 'POST' };
+
+ getParentalStatus() {
+ const { path, method } = this.PARENTAL_STATUS;
+
+ return this.makeRequest(path, method);
+ }
+
+ enableParentalControl() {
+ const { path, method } = this.PARENTAL_ENABLE;
+
+ return this.makeRequest(path, method);
+ }
+
+ disableParentalControl() {
+ const { path, method } = this.PARENTAL_DISABLE;
+
+ return this.makeRequest(path, method);
+ }
+
+ // Safebrowsing
+ SAFEBROWSING_STATUS = { path: 'safebrowsing/status', method: 'GET' };
+
+ SAFEBROWSING_ENABLE = { path: 'safebrowsing/enable', method: 'POST' };
+
+ SAFEBROWSING_DISABLE = { path: 'safebrowsing/disable', method: 'POST' };
+
+ getSafebrowsingStatus() {
+ const { path, method } = this.SAFEBROWSING_STATUS;
+
+ return this.makeRequest(path, method);
+ }
+
+ enableSafebrowsing() {
+ const { path, method } = this.SAFEBROWSING_ENABLE;
+
+ return this.makeRequest(path, method);
+ }
+
+ disableSafebrowsing() {
+ const { path, method } = this.SAFEBROWSING_DISABLE;
+
+ return this.makeRequest(path, method);
+ }
+
+ // Safesearch
+ SAFESEARCH_STATUS = { path: 'safesearch/status', method: 'GET' };
+
+ SAFESEARCH_UPDATE = { path: 'safesearch/settings', method: 'PUT' };
+
+ getSafesearchStatus() {
+ const { path, method } = this.SAFESEARCH_STATUS;
+
+ return this.makeRequest(path, method);
+ }
+
+ /**
+ * interface SafeSearchConfig {
+ "enabled": boolean,
+ "bing": boolean,
+ "duckduckgo": boolean,
+ "google": boolean,
+ "pixabay": boolean,
+ "yandex": boolean,
+ "youtube": boolean
+ * }
+ * @param {*} data - SafeSearchConfig
+ * @returns 200 ok
+ */
+ updateSafesearch(data: any) {
+ const { path, method } = this.SAFESEARCH_UPDATE;
+ return this.makeRequest(path, method, { data });
+ }
+
+ // enableSafesearch() {
+ // const { path, method } = this.SAFESEARCH_ENABLE;
+ // return this.makeRequest(path, method);
+ // }
+
+ // disableSafesearch() {
+ // const { path, method } = this.SAFESEARCH_DISABLE;
+ // return this.makeRequest(path, method);
+ // }
+
+ // Language
+ async changeLanguage(config: { language: LocalesType }) {
+ const profile = await this.getProfile();
+ profile.language = config.language;
+
+ return this.setProfile(profile);
+ }
+
+ // Theme
+ async changeTheme(config: { theme: Theme }) {
+ const profile = await this.getProfile();
+ profile.theme = config.theme;
+
+ return this.setProfile(profile);
+ }
+
+ // DHCP
+ DHCP_STATUS = { path: 'dhcp/status', method: 'GET' };
+
+ DHCP_SET_CONFIG = { path: 'dhcp/set_config', method: 'POST' };
+
+ DHCP_FIND_ACTIVE = { path: 'dhcp/find_active_dhcp', method: 'POST' };
+
+ DHCP_INTERFACES = { path: 'dhcp/interfaces', method: 'GET' };
+
+ DHCP_ADD_STATIC_LEASE = { path: 'dhcp/add_static_lease', method: 'POST' };
+
+ DHCP_REMOVE_STATIC_LEASE = { path: 'dhcp/remove_static_lease', method: 'POST' };
+
+ DHCP_UPDATE_STATIC_LEASE = { path: 'dhcp/update_static_lease', method: 'POST' };
+
+ DHCP_RESET = { path: 'dhcp/reset', method: 'POST' };
+
+ DHCP_LEASES_RESET = { path: 'dhcp/reset_leases', method: 'POST' };
+
+ getDhcpStatus() {
+ const { path, method } = this.DHCP_STATUS;
+
+ return this.makeRequest(path, method);
+ }
+
+ getDhcpInterfaces() {
+ const { path, method } = this.DHCP_INTERFACES;
+
+ return this.makeRequest(path, method);
+ }
+
+ setDhcpConfig(config: any) {
+ const { path, method } = this.DHCP_SET_CONFIG;
+ const parameters = {
+ data: config,
+ };
+ return this.makeRequest(path, method, parameters);
+ }
+
+ findActiveDhcp(req: any) {
+ const { path, method } = this.DHCP_FIND_ACTIVE;
+ const parameters = {
+ data: req,
+ };
+ return this.makeRequest(path, method, parameters);
+ }
+
+ addStaticLease(config: any) {
+ const { path, method } = this.DHCP_ADD_STATIC_LEASE;
+ const parameters = {
+ data: config,
+ };
+ return this.makeRequest(path, method, parameters);
+ }
+
+ removeStaticLease(config: any) {
+ const { path, method } = this.DHCP_REMOVE_STATIC_LEASE;
+ const parameters = {
+ data: config,
+ };
+ return this.makeRequest(path, method, parameters);
+ }
+
+ updateStaticLease(config: any) {
+ const { path, method } = this.DHCP_UPDATE_STATIC_LEASE;
+ const parameters = {
+ data: config,
+ };
+ return this.makeRequest(path, method, parameters);
+ }
+
+ resetDhcp() {
+ const { path, method } = this.DHCP_RESET;
+
+ return this.makeRequest(path, method);
+ }
+
+ resetDhcpLeases() {
+ const { path, method } = this.DHCP_LEASES_RESET;
+
+ return this.makeRequest(path, method);
+ }
+
+ // Installation
+ INSTALL_GET_ADDRESSES = { path: 'install/get_addresses', method: 'GET' };
+
+ INSTALL_CONFIGURE = { path: 'install/configure', method: 'POST' };
+
+ INSTALL_CHECK_CONFIG = { path: 'install/check_config', method: 'POST' };
+
+ getDefaultAddresses() {
+ const { path, method } = this.INSTALL_GET_ADDRESSES;
+
+ return this.makeRequest(path, method);
+ }
+
+ setAllSettings(config: any) {
+ const { path, method } = this.INSTALL_CONFIGURE;
+ const parameters = {
+ data: config,
+ };
+ return this.makeRequest(path, method, parameters);
+ }
+
+ checkConfig(config: any) {
+ const { path, method } = this.INSTALL_CHECK_CONFIG;
+ const parameters = {
+ data: config,
+ };
+ return this.makeRequest(path, method, parameters);
+ }
+
+ // DNS-over-HTTPS and DNS-over-TLS
+ TLS_STATUS = { path: 'tls/status', method: 'GET' };
+
+ TLS_CONFIG = { path: 'tls/configure', method: 'POST' };
+
+ TLS_VALIDATE = { path: 'tls/validate', method: 'POST' };
+
+ getTlsStatus() {
+ const { path, method } = this.TLS_STATUS;
+
+ return this.makeRequest(path, method);
+ }
+
+ setTlsConfig(config: any) {
+ const { path, method } = this.TLS_CONFIG;
+ const parameters = {
+ data: config,
+ };
+ return this.makeRequest(path, method, parameters);
+ }
+
+ validateTlsConfig(config: any) {
+ const { path, method } = this.TLS_VALIDATE;
+ const parameters = {
+ data: config,
+ };
+ return this.makeRequest(path, method, parameters);
+ }
+
+ // Per-client settings
+ GET_CLIENTS = { path: 'clients', method: 'GET' };
+
+ SEARCH_CLIENTS = { path: 'clients/search', method: 'POST' };
+
+ ADD_CLIENT = { path: 'clients/add', method: 'POST' };
+
+ DELETE_CLIENT = { path: 'clients/delete', method: 'POST' };
+
+ UPDATE_CLIENT = { path: 'clients/update', method: 'POST' };
+
+ getClients() {
+ const { path, method } = this.GET_CLIENTS;
+
+ return this.makeRequest(path, method);
+ }
+
+ addClient(config: any) {
+ const { path, method } = this.ADD_CLIENT;
+ const parameters = {
+ data: config,
+ };
+ return this.makeRequest(path, method, parameters);
+ }
+
+ deleteClient(config: any) {
+ const { path, method } = this.DELETE_CLIENT;
+ const parameters = {
+ data: config,
+ };
+ return this.makeRequest(path, method, parameters);
+ }
+
+ updateClient(config: any) {
+ const { path, method } = this.UPDATE_CLIENT;
+ const parameters = {
+ data: config,
+ };
+ return this.makeRequest(path, method, parameters);
+ }
+
+ searchClients(config: any) {
+ const { path, method } = this.SEARCH_CLIENTS;
+ const parameters = {
+ data: config,
+ };
+ return this.makeRequest(path, method, parameters);
+ }
+
+ // DNS access settings
+ ACCESS_LIST = { path: 'access/list', method: 'GET' };
+
+ ACCESS_SET = { path: 'access/set', method: 'POST' };
+
+ getAccessList() {
+ const { path, method } = this.ACCESS_LIST;
+
+ return this.makeRequest(path, method);
+ }
+
+ setAccessList(config: any) {
+ const { path, method } = this.ACCESS_SET;
+ const parameters = {
+ data: config,
+ };
+ return this.makeRequest(path, method, parameters);
+ }
+
+ // DNS rewrites
+ REWRITES_LIST = { path: 'rewrite/list', method: 'GET' };
+
+ REWRITE_ADD = { path: 'rewrite/add', method: 'POST' };
+
+ REWRITE_UPDATE = { path: 'rewrite/update', method: 'PUT' };
+
+ REWRITE_DELETE = { path: 'rewrite/delete', method: 'POST' };
+
+ getRewritesList() {
+ const { path, method } = this.REWRITES_LIST;
+
+ return this.makeRequest(path, method);
+ }
+
+ addRewrite(config: any) {
+ const { path, method } = this.REWRITE_ADD;
+ const parameters = {
+ data: config,
+ };
+ return this.makeRequest(path, method, parameters);
+ }
+
+ updateRewrite(config: any) {
+ const { path, method } = this.REWRITE_UPDATE;
+ const parameters = {
+ data: config,
+ };
+ return this.makeRequest(path, method, parameters);
+ }
+
+ deleteRewrite(config: any) {
+ const { path, method } = this.REWRITE_DELETE;
+ const parameters = {
+ data: config,
+ };
+ return this.makeRequest(path, method, parameters);
+ }
+
+ REWRITE_SETTINGS = { path: 'rewrite/settings', method: 'GET' };
+
+ REWRITE_SETTINGS_UPDATE = { path: 'rewrite/settings/update', method: 'PUT' };
+
+ getRewriteSettings() {
+ const { path, method } = this.REWRITE_SETTINGS;
+ return this.makeRequest(path, method);
+ }
+
+ updateRewriteSettings(config: { enabled: boolean }) {
+ const { path, method } = this.REWRITE_SETTINGS_UPDATE;
+ const parameters = {
+ data: config,
+ };
+ return this.makeRequest(path, method, parameters);
+ }
+
+ // Blocked services
+ BLOCKED_SERVICES_GET = { path: 'blocked_services/get', method: 'GET' };
+
+ BLOCKED_SERVICES_UPDATE = { path: 'blocked_services/update', method: 'PUT' };
+
+ BLOCKED_SERVICES_ALL = { path: 'blocked_services/all', method: 'GET' };
+
+ getAllBlockedServices() {
+ const { path, method } = this.BLOCKED_SERVICES_ALL;
+
+ return this.makeRequest(path, method);
+ }
+
+ getBlockedServices() {
+ const { path, method } = this.BLOCKED_SERVICES_GET;
+
+ return this.makeRequest(path, method);
+ }
+
+ updateBlockedServices(config: any) {
+ const { path, method } = this.BLOCKED_SERVICES_UPDATE;
+ const parameters = {
+ data: config,
+ };
+ return this.makeRequest(path, method, parameters);
+ }
+
+ // Settings for statistics
+ GET_STATS = { path: 'stats', method: 'GET' };
+
+ GET_STATS_CONFIG = { path: 'stats/config', method: 'GET' };
+
+ UPDATE_STATS_CONFIG = { path: 'stats/config/update', method: 'PUT' };
+
+ STATS_RESET = { path: 'stats_reset', method: 'POST' };
+
+ getStats(recent?: number) {
+ const { path, method } = this.GET_STATS;
+ const config = recent ? { params: { recent } } : undefined;
+
+ return this.makeRequest(path, method, config);
+ }
+
+ getStatsConfig() {
+ const { path, method } = this.GET_STATS_CONFIG;
+
+ return this.makeRequest(path, method);
+ }
+
+ setStatsConfig(data: any) {
+ const { path, method } = this.UPDATE_STATS_CONFIG;
+ const config = {
+ data,
+ };
+ return this.makeRequest(path, method, config);
+ }
+
+ resetStats() {
+ const { path, method } = this.STATS_RESET;
+
+ return this.makeRequest(path, method);
+ }
+
+ // Query log
+ GET_QUERY_LOG = { path: 'querylog', method: 'GET' };
+
+ UPDATE_QUERY_LOG_CONFIG = { path: 'querylog/config/update', method: 'PUT' };
+
+ GET_QUERY_LOG_CONFIG = { path: 'querylog/config', method: 'GET' };
+
+ QUERY_LOG_CLEAR = { path: 'querylog_clear', method: 'POST' };
+
+ getQueryLog(params: any) {
+ const { path, method } = this.GET_QUERY_LOG;
+ const url = getPathWithQueryString(path, {
+ ...params,
+ limit: QUERY_LOGS_PAGE_LIMIT,
+ });
+
+ return this.makeRequest(url, method);
+ }
+
+ getQueryLogConfig() {
+ const { path, method } = this.GET_QUERY_LOG_CONFIG;
+
+ return this.makeRequest(path, method);
+ }
+
+ setQueryLogConfig(data: any) {
+ const { path, method } = this.UPDATE_QUERY_LOG_CONFIG;
+ const config = {
+ data,
+ };
+ return this.makeRequest(path, method, config);
+ }
+
+ clearQueryLog() {
+ const { path, method } = this.QUERY_LOG_CLEAR;
+
+ return this.makeRequest(path, method);
+ }
+
+ // Login
+ LOGIN = { path: 'login', method: 'POST' };
+
+ login(data: any) {
+ const { path, method } = this.LOGIN;
+ const config = {
+ data,
+ };
+ return this.makeRequest(path, method, config);
+ }
+
+ // Logout
+ LOGOUT_PATH = 'logout';
+
+ getLogoutUrl() {
+ return `${this.baseUrl}/${this.LOGOUT_PATH}`;
+ }
+
+ // Profile
+ GET_PROFILE = { path: 'profile', method: 'GET' };
+
+ UPDATE_PROFILE = { path: 'profile/update', method: 'PUT' };
+
+ getProfile() {
+ const { path, method } = this.GET_PROFILE;
+
+ return this.makeRequest(path, method);
+ }
+
+ setProfile(data: any) {
+ const theme = data.theme ? data.theme : THEMES.auto;
+ const defaultLanguage = intl.getUILanguage() ? intl.getUILanguage() : 'en';
+ const language = data.language ? data.language : defaultLanguage;
+
+ const { path, method } = this.UPDATE_PROFILE;
+ const config = { data: { theme, language } };
+
+ return this.makeRequest(path, method, config);
+ }
+
+ // DNS config
+ GET_DNS_CONFIG = { path: 'dns_info', method: 'GET' };
+
+ SET_DNS_CONFIG = { path: 'dns_config', method: 'POST' };
+
+ getDnsConfig() {
+ const { path, method } = this.GET_DNS_CONFIG;
+
+ return this.makeRequest(path, method);
+ }
+
+ setDnsConfig(data: any) {
+ const { path, method } = this.SET_DNS_CONFIG;
+ const config = {
+ data,
+ };
+ return this.makeRequest(path, method, config);
+ }
+
+ SET_PROTECTION = { path: 'protection', method: 'POST' };
+
+ setProtection(data: any) {
+ const { enabled, duration } = data;
+ const { path, method } = this.SET_PROTECTION;
+
+ return this.makeRequest(path, method, { data: { enabled, duration } });
+ }
+
+ // Cache
+ CLEAR_CACHE = { path: 'cache_clear', method: 'POST' };
+
+ clearCache() {
+ const { path, method } = this.CLEAR_CACHE;
+
+ return this.makeRequest(path, method);
+ }
+}
+
+export const apiClient = new Api(BASE_URL);
diff --git a/client_v2/src/common/controls/Checkbox/Checkbox.module.pcss b/client_v2/src/common/controls/Checkbox/Checkbox.module.pcss
new file mode 100644
index 00000000000..01515b37fc5
--- /dev/null
+++ b/client_v2/src/common/controls/Checkbox/Checkbox.module.pcss
@@ -0,0 +1,65 @@
+.checkbox {
+ display: inline-flex;
+ align-items: center;
+ cursor: pointer;
+ gap: 8px;
+
+ &.disabled {
+ cursor: default;
+ }
+
+ &.center {
+ align-items: center;
+ }
+
+ &.start {
+ align-items: flex-start;
+ }
+
+ &.end {
+ align-items: flex-end;
+ }
+}
+
+.input {
+ display: none;
+}
+
+.input:disabled + .handler .icon {
+ color: var(--disabled-gray-icons);
+}
+
+.input:disabled + .handler .icon_active {
+ color: var(--disabled-main-button);
+}
+
+.handler {
+ flex-shrink: 0;
+ position: relative;
+ width: 24px;
+ height: 24px;
+
+ &.plus {
+ background-color: transparent;
+ border-color: transparent;
+ }
+}
+
+.label {
+ display: grid;
+ align-items: center;
+ font-size: 16px;
+ font-weight: var(--weight-regular);
+ user-select: none;
+}
+
+.icon {
+ position: absolute;
+ width: 24px;
+ height: 24px;
+ color: var(--default-gray-icons);
+}
+
+.active {
+ color: var(--default-product-icon);
+}
diff --git a/client_v2/src/common/controls/Checkbox/Checkbox.tsx b/client_v2/src/common/controls/Checkbox/Checkbox.tsx
new file mode 100644
index 00000000000..ef977f4363e
--- /dev/null
+++ b/client_v2/src/common/controls/Checkbox/Checkbox.tsx
@@ -0,0 +1,76 @@
+import { type JSX, Show } from 'solid-js';
+import cn from 'clsx';
+import theme from 'panel/lib/theme';
+import { Icon } from 'panel/common/ui/Icon';
+
+import s from './Checkbox.module.pcss';
+
+type Props = JSX.InputHTMLAttributes & {
+ class?: string;
+ labelClass?: string;
+ overflow?: boolean;
+ onClick?: (e: MouseEvent) => void;
+ children?: JSX.Element;
+ plusStyle?: boolean;
+ verticalAlign?: 'center' | 'start' | 'end';
+ ref?: HTMLInputElement | ((el: HTMLInputElement) => void);
+};
+
+export const Checkbox = (props: Props) => {
+ const setRef = (el: HTMLInputElement) => {
+ if (typeof props.ref === 'function') {
+ props.ref(el);
+ }
+ };
+
+ return (
+ (props.onClick as any)?.(e)}
+ >
+ setRef(el)}
+ id={props.id}
+ name={props.name}
+ type="checkbox"
+ class={s.input}
+ onChange={(e) => (props.onChange as any)?.(e)}
+ checked={props.checked}
+ disabled={props.disabled}
+ />
+
+
+ }
+ >
+
+
+
+
+
+ {props.children}
+
+
+
+ );
+};
diff --git a/client_v2/src/common/controls/Checkbox/index.ts b/client_v2/src/common/controls/Checkbox/index.ts
new file mode 100644
index 00000000000..8f59e4fbb54
--- /dev/null
+++ b/client_v2/src/common/controls/Checkbox/index.ts
@@ -0,0 +1 @@
+export { Checkbox } from './Checkbox';
diff --git a/client_v2/src/common/controls/Input/Input.module.pcss b/client_v2/src/common/controls/Input/Input.module.pcss
new file mode 100644
index 00000000000..14255c644fc
--- /dev/null
+++ b/client_v2/src/common/controls/Input/Input.module.pcss
@@ -0,0 +1,166 @@
+.inputWrapper {
+ display: flex;
+ align-items: center;
+ position: relative;
+ background-color: var(--default-page-background);
+ height: 48px;
+ overflow: hidden;
+ font-size: 16px;
+ border: 1px solid var(--default-item-divider);
+ border-radius: 8px;
+
+ &.focused {
+ border-color: var(--pressed-link);
+ }
+
+ &.disabled {
+ background-color: var(--pressed-page-background);
+
+ .input {
+ background-color: var(--pressed-page-background);
+ color: var(--default-description-text);
+ cursor: default;
+
+ &:-webkit-autofill,
+ &:-webkit-autofill:hover,
+ &:-webkit-autofill:focus {
+ box-shadow: 0 0 0 100px var(--pressed-page-background) inset;
+ }
+ }
+ }
+
+ &.small {
+ height: 40px;
+ }
+
+ &.large {
+ height: 56px;
+ }
+
+ &.borderless {
+ border: 0;
+ }
+
+ &.error {
+ border-color: var(--default-error-icon);
+ }
+
+ &.invalid {
+ border: 1px solid var(--default-error-icon);
+
+ &:focus,
+ &:hover,
+ &:active {
+ border: 1px solid var(--default-error-icon);
+ }
+ }
+
+ .input {
+ color: var(--default-main-text);
+ font-size: 16px;
+ line-height: 1.3;
+ max-width: 100%;
+ height: 100%;
+ width: 100%;
+ outline: none;
+ border: 0;
+ padding: 8px 16px;
+ border-radius: 8px;
+ background-color: var(--default-page-background);
+
+ &::placeholder {
+ color: var(--default-placeholder);
+ }
+
+ &:focus,
+ &:hover,
+ &:active {
+ outline: none;
+ }
+
+ &:-webkit-autofill,
+ &:-webkit-autofill:hover,
+ &:-webkit-autofill:focus {
+ box-shadow: 0 0 0 100px var(--default-page-background) inset;
+ caret-color: var(--default-main-text);
+ transition: background-color 5000s ease-in-out 0s;
+ }
+ }
+}
+
+.prefix {
+ padding-left: 8px;
+}
+
+.suffix {
+ padding-right: 8px;
+}
+
+.postfix {
+ padding-right: 8px;
+}
+
+.actions {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ height: 100%;
+ padding-right: 4px;
+}
+
+.clearButton {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+ flex-shrink: 0;
+ padding: 0;
+ border: 0;
+ background: transparent;
+ color: var(--default-gray-icons);
+ cursor: pointer;
+}
+
+.suffixIcon {
+ display: flex;
+ align-items: center;
+}
+
+.inputLabel {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 4px;
+ font-size: 14px;
+ line-height: 24px;
+ color: var(--default-description-text);
+}
+
+.inputError {
+ font-size: 14px;
+ color: var(--default-error-icon);
+ margin-top: 4px;
+}
+
+.inputSuffix {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.inputIconButton {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ border: 0;
+ background: transparent;
+ color: var(--default-description-text);
+ cursor: pointer;
+}
+
+.inputIconButton:disabled {
+ opacity: 0.4;
+ cursor: default;
+}
diff --git a/client_v2/src/common/controls/Input/Input.tsx b/client_v2/src/common/controls/Input/Input.tsx
new file mode 100644
index 00000000000..8ca2c4fa9c0
--- /dev/null
+++ b/client_v2/src/common/controls/Input/Input.tsx
@@ -0,0 +1,178 @@
+import { type JSX, createSignal, createEffect, Show, untrack } from 'solid-js';
+import cn from 'clsx';
+import { Icon } from 'panel/common/ui/Icon';
+
+import intl from 'panel/common/intl';
+import s from './Input.module.pcss';
+
+type InputChangeEvent = Event & {
+ currentTarget: HTMLInputElement;
+ target: HTMLInputElement;
+};
+
+type Props = Omit, 'size' | 'onChange' | 'onBlur'> & {
+ label?: JSX.Element;
+ class?: string;
+ innerClass?: string;
+ prefixIcon?: JSX.Element;
+ suffixIcon?: JSX.Element;
+ borderless?: boolean;
+ invalid?: boolean;
+ maxLength?: number;
+ error?: boolean;
+ errorMessage?: string;
+ isClearable?: boolean;
+ onClear?: () => void;
+ inputError?: string;
+ size?: 'small' | 'medium' | 'large';
+ value?: string | number | readonly string[];
+ defaultValue?: string;
+ onChange?: (event: InputChangeEvent) => void;
+ onBlur?: (event: FocusEvent) => void;
+ ref?: HTMLInputElement | ((el: HTMLInputElement) => void);
+ onKeyDown?: (e: KeyboardEvent) => void;
+};
+
+const hasInputValue = (value: string | number | readonly string[] | undefined) => {
+ if (Array.isArray(value)) {
+ return value.length > 0;
+ }
+ return String(value ?? '').length > 0;
+};
+
+export const Input = (props: Props) => {
+ let inputRef: HTMLInputElement | undefined;
+ const [focused, setFocused] = createSignal(false);
+ const [hasValue, setHasValue] = createSignal(
+ hasInputValue(untrack(() => props.value) ?? untrack(() => props.defaultValue)),
+ );
+
+ createEffect(() => {
+ if (props.value !== undefined) {
+ setHasValue(hasInputValue(props.value));
+ }
+ });
+
+ const setInputRef = (el: HTMLInputElement) => {
+ inputRef = el;
+ if (typeof props.ref === 'function') {
+ props.ref(el);
+ }
+ };
+
+ const showClearButton = () =>
+ Boolean(props.isClearable && !props.disabled && !props.readOnly && hasValue());
+ const hasActions = () => Boolean(props.suffixIcon || showClearButton());
+
+ const handleChange = (event: InputChangeEvent) => {
+ setHasValue((event.currentTarget as HTMLInputElement).value.length > 0);
+ props.onChange?.(event);
+ };
+
+ const handleClear = () => {
+ if (!inputRef) {
+ props.onClear?.();
+ setHasValue(false);
+ return;
+ }
+
+ inputRef.value = '';
+ props.onChange?.({
+ target: inputRef,
+ currentTarget: inputRef,
+ } as unknown as InputChangeEvent);
+
+ setHasValue(false);
+ props.onClear?.();
+ };
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ // Prevent minus key entry in number inputs to avoid negative values
+ if (props.type === 'number' && e.key === '-') {
+ e.preventDefault();
+ }
+ // Forward to any external onKeyDown handler
+ props.onKeyDown?.(e);
+ };
+
+ const computedErrorMessage = () => props.inputError ?? props.errorMessage;
+
+ return (
+ <>
+
+
+ {props.label}
+
+
+
+
{props.prefixIcon}
+
setInputRef(el)}
+ accept={props.accept}
+ autofocus={props.autofocus}
+ class={cn(s.input, props.innerClass, {
+ [s.prefix]: props.prefixIcon,
+ [s.postfix]: hasActions(),
+ })}
+ onChange={handleChange}
+ onInput={(e) => (props.onInput as any)?.(e)}
+ onKeyDown={handleKeyDown}
+ type={props.type}
+ id={props.id}
+ name={props.name}
+ placeholder={props.placeholder}
+ value={props.value as string | number}
+ onFocus={() => setFocused(true)}
+ onBlur={(e) => {
+ props.onBlur?.(e);
+ setFocused(false);
+ }}
+ maxLength={props.maxLength}
+ disabled={props.disabled}
+ autocomplete={props.autocomplete}
+ />
+
+
+
+ event.preventDefault()}
+ onClick={handleClear}
+ >
+
+
+
+
+ {props.suffixIcon}
+
+
+
+
+
+ {computedErrorMessage()}
+
+ >
+ );
+};
diff --git a/client_v2/src/common/controls/Input/PasswordInput.tsx b/client_v2/src/common/controls/Input/PasswordInput.tsx
new file mode 100644
index 00000000000..b4e781e2a26
--- /dev/null
+++ b/client_v2/src/common/controls/Input/PasswordInput.tsx
@@ -0,0 +1,53 @@
+import { createSignal, Show } from 'solid-js';
+
+import { Input } from 'panel/common/controls/Input/index';
+import { Icon } from 'panel/common/ui/Icon';
+
+import styles from './Input.module.pcss';
+
+type Props = Omit[0], 'type' | 'suffixIcon' | 'onChange' | 'value'> & {
+ value: string;
+ onChange: (value: string) => void;
+ ref?: HTMLInputElement | ((el: HTMLInputElement) => void);
+};
+
+export const PasswordInput = (props: Props) => {
+ const [isPasswordVisible, setIsPasswordVisible] = createSignal(false);
+
+ return (
+ props.onChange(e.target.value)}
+ type={isPasswordVisible() ? 'text' : 'password'}
+ suffixIcon={
+
+
+ e.preventDefault()}
+ onClick={() => {
+ setIsPasswordVisible(false);
+ props.onChange('');
+ }}
+ >
+
+
+
+ e.preventDefault()}
+ onClick={() => setIsPasswordVisible((v) => !v)}
+ >
+
+
+
+ }
+ />
+ );
+};
diff --git a/client_v2/src/common/controls/Input/index.ts b/client_v2/src/common/controls/Input/index.ts
new file mode 100644
index 00000000000..6322cf3241a
--- /dev/null
+++ b/client_v2/src/common/controls/Input/index.ts
@@ -0,0 +1 @@
+export { Input } from './Input';
diff --git a/client_v2/src/common/controls/Radio/Radio.module.pcss b/client_v2/src/common/controls/Radio/Radio.module.pcss
new file mode 100644
index 00000000000..c51b933d5b3
--- /dev/null
+++ b/client_v2/src/common/controls/Radio/Radio.module.pcss
@@ -0,0 +1,91 @@
+.wrap {
+ display: flex;
+ flex-direction: column;
+}
+
+.radio {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ margin: 0;
+ padding: 8px 0;
+ cursor: pointer;
+
+ &.center {
+ align-items: center;
+ }
+
+ &.start {
+ align-items: flex-start;
+ }
+
+ &.end {
+ align-items: flex-end;
+ }
+
+ &.modal {
+ align-items: flex-start;
+
+ .text {
+ max-width: 100%;
+ font-weight: var(--weight-bold);
+ }
+
+ .description {
+ font-size: var(--fs-text-t3);
+ line-height: var(--lh-t3-normal);
+ font-weight: var(--weight-regular);
+
+ p {
+ margin-bottom: 8px;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+ }
+
+ @media (min-width: 768px) {
+ padding: 16px 0;
+ }
+ }
+}
+
+.input {
+ display: none;
+}
+
+.text {
+ max-width: 250px;
+ flex: 1;
+
+ &.fullWidth {
+ max-width: 100%;
+ }
+
+ &.disabled {
+ color: var(--disabled-main-text);
+ }
+}
+
+.description {
+ margin-top: 2px;
+ color: var(--default-description-text);
+}
+
+.icon {
+ color: var(--default-gray-icons);
+}
+
+.active {
+ transition: none;
+ color: var(--default-product-icon);
+}
+
+.input:disabled + .handler .icon {
+ color: var(--disabled-gray-icons);
+}
+
+.input:disabled:checked + .handler .icon {
+ color: var(--disabled-main-button);
+}
diff --git a/client_v2/src/common/controls/Radio/Radio.tsx b/client_v2/src/common/controls/Radio/Radio.tsx
new file mode 100644
index 00000000000..cd10714f4e3
--- /dev/null
+++ b/client_v2/src/common/controls/Radio/Radio.tsx
@@ -0,0 +1,70 @@
+import { type JSX, Show, For } from 'solid-js';
+import cn from 'clsx';
+
+import { Icon } from 'panel/common/ui/Icon';
+import theme from 'panel/lib/theme';
+import s from './Radio.module.pcss';
+
+type Props = {
+ class?: string;
+ wrapClass?: string;
+ disabled?: boolean;
+ handleChange: (e: T) => void;
+ value: T;
+ options: { text: string; value: T; description?: JSX.Element; disabled?: boolean }[];
+ name?: string;
+ textClass?: string;
+ verticalAlign?: 'center' | 'start' | 'end';
+ ref?: HTMLDivElement | ((el: HTMLDivElement) => void);
+ inModal?: boolean;
+};
+
+export const Radio = (props: Props) => {
+ const setRef = (el: HTMLDivElement) => {
+ if (typeof props.ref === 'function') {
+ props.ref(el);
+ }
+ };
+
+ return (
+ setRef(el)} class={cn(s.wrap, props.wrapClass)}>
+
+ {(o) => (
+
+ props.handleChange(o.value)}
+ checked={props.value === o.value}
+ disabled={props.disabled || o.disabled}
+ />
+
+
+
+
+
{o.text}
+
+ {o.description}
+
+
+
+ )}
+
+
+ );
+};
diff --git a/client_v2/src/common/controls/Radio/index.ts b/client_v2/src/common/controls/Radio/index.ts
new file mode 100644
index 00000000000..de3697b1a22
--- /dev/null
+++ b/client_v2/src/common/controls/Radio/index.ts
@@ -0,0 +1 @@
+export { Radio } from './Radio';
diff --git a/client_v2/src/common/controls/Select/CheckIcons.tsx b/client_v2/src/common/controls/Select/CheckIcons.tsx
new file mode 100644
index 00000000000..a3e195557ed
--- /dev/null
+++ b/client_v2/src/common/controls/Select/CheckIcons.tsx
@@ -0,0 +1,19 @@
+import { type JSX } from 'solid-js';
+import { useSelectItemContext, useComboboxItemContext } from '@ark-ui/solid';
+import { Icon } from 'panel/common/ui/Icon';
+
+const renderCheckIcon = (selected: boolean): JSX.Element => (
+
+);
+
+// Check/dot icon for ArkSelect.Item (reads context reactively in JSX).
+export const SelectCheckIcon = () => {
+ const itemCtx = useSelectItemContext();
+ return renderCheckIcon(itemCtx().selected);
+};
+
+// Check/dot icon for ArkCombobox.Item.
+export const ComboboxCheckIcon = () => {
+ const itemCtx = useComboboxItemContext();
+ return renderCheckIcon(itemCtx().selected);
+};
diff --git a/client_v2/src/common/controls/Select/ComboboxInputReset.tsx b/client_v2/src/common/controls/Select/ComboboxInputReset.tsx
new file mode 100644
index 00000000000..bc10c93491f
--- /dev/null
+++ b/client_v2/src/common/controls/Select/ComboboxInputReset.tsx
@@ -0,0 +1,20 @@
+import { type JSX, createEffect, on } from 'solid-js';
+import { useComboboxContext } from '@ark-ui/solid';
+
+export const ComboboxInputReset = (): JSX.Element => {
+ const comboCtx = useComboboxContext();
+
+ createEffect(
+ on(
+ () => comboCtx().open,
+ (nowOpen) => {
+ if (!nowOpen) {
+ comboCtx().setInputValue('');
+ }
+ },
+ { defer: true },
+ ),
+ );
+
+ return null;
+};
diff --git a/client_v2/src/common/controls/Select/ComboboxItemContent.tsx b/client_v2/src/common/controls/Select/ComboboxItemContent.tsx
new file mode 100644
index 00000000000..511f2ac63c8
--- /dev/null
+++ b/client_v2/src/common/controls/Select/ComboboxItemContent.tsx
@@ -0,0 +1,28 @@
+import { type JSX, Show } from 'solid-js';
+import { Combobox as ArkCombobox } from '@ark-ui/solid';
+import { Icon } from 'panel/common/ui/Icon';
+import { ComboboxCheckIcon } from './CheckIcons';
+import s from './MenuList.module.pcss';
+
+interface ItemContentProps {
+ option: any;
+ showOptionIcon?: boolean;
+ showIcons?: boolean;
+}
+
+// Option row for the searchable Combobox branch (mirrors SelectItemContent).
+export const ComboboxItemContent = (props: ItemContentProps): JSX.Element => (
+ <>
+
+
+
+
+
+
+
+
+
+
+ {props.option.label}
+ >
+);
diff --git a/client_v2/src/common/controls/Select/ComboboxMultiValueDisplay.tsx b/client_v2/src/common/controls/Select/ComboboxMultiValueDisplay.tsx
new file mode 100644
index 00000000000..4da5c53169d
--- /dev/null
+++ b/client_v2/src/common/controls/Select/ComboboxMultiValueDisplay.tsx
@@ -0,0 +1,31 @@
+import { type JSX, Show } from 'solid-js';
+import { Combobox as ArkCombobox, useComboboxContext } from '@ark-ui/solid';
+import { SelectMultiValue } from './SelectMultiValue';
+import { optionToValue } from './helpers';
+
+interface ComboboxMultiValueDisplayProps {
+ placeholder?: string;
+ inputId?: string;
+ onInputRef?: (el: HTMLInputElement) => void;
+}
+
+// Multi-value pills + inline search input for searchable Combobox.
+export const ComboboxMultiValueDisplay = (props: ComboboxMultiValueDisplayProps): JSX.Element => {
+ const comboCtx = useComboboxContext();
+
+ return (
+ <>
+
+ comboCtx().clearValue(optionToValue(item.value))}
+ />
+
+
+ >
+ );
+};
diff --git a/client_v2/src/common/controls/Select/MenuList.module.pcss b/client_v2/src/common/controls/Select/MenuList.module.pcss
new file mode 100644
index 00000000000..8f989d46d21
--- /dev/null
+++ b/client_v2/src/common/controls/Select/MenuList.module.pcss
@@ -0,0 +1,25 @@
+.menuList {
+ overflow-y: auto;
+ width: 100%;
+ max-height: 340px;
+
+ &::-webkit-scrollbar {
+ width: 6px;
+ }
+
+ &::-webkit-scrollbar-track {
+ background-color: transparent;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background-color: var(--default-gray-icons);
+ border-radius: 10px;
+ }
+}
+
+.selectIconContainer {
+ width: 24px;
+ height: 24px;
+ margin-right: 8px;
+ color: var(--default-gray-icons);
+}
diff --git a/client_v2/src/common/controls/Select/Select.pcss b/client_v2/src/common/controls/Select/Select.pcss
new file mode 100644
index 00000000000..d000748399e
--- /dev/null
+++ b/client_v2/src/common/controls/Select/Select.pcss
@@ -0,0 +1,479 @@
+.solid-select {
+ position: relative;
+ width: 100%;
+
+ &.solid-select-small {
+ width: 68px;
+
+ [data-scope='select'][data-part='control'],
+ [data-scope='combobox'][data-part='control'] {
+ padding-right: 0;
+ }
+ }
+
+ &.solid-select-medium {
+ width: 200px;
+ }
+
+ &.solid-select-big {
+ width: 100%;
+
+ @media (min-width: 768px) {
+ width: 288px;
+ }
+ }
+
+ &.solid-select--menu-right {
+ [data-scope='select'][data-part='content'],
+ [data-scope='combobox'][data-part='content'] {
+ @media (min-width: 1024px) {
+ /* Right-anchor the dropdown so wide menus expand leftward and stay on screen. */
+ position: absolute;
+ right: 0;
+ left: auto;
+ }
+ }
+ }
+
+ &.solid-select-responsive {
+ width: 100%;
+
+ @media (min-width: 1024px) {
+ width: 100%;
+ }
+ }
+
+ &.solid-select--menu-small {
+ [data-scope='select'][data-part='content'],
+ [data-scope='combobox'][data-part='content'] {
+ min-width: 137px;
+ }
+ }
+
+ &.solid-select--menu-medium {
+ [data-scope='select'][data-part='content'],
+ [data-scope='combobox'][data-part='content'] {
+ min-width: 250px;
+ }
+ }
+
+ &.solid-select--menu-big {
+ [data-scope='select'][data-part='content'],
+ [data-scope='combobox'][data-part='content'] {
+ min-width: 284px;
+
+ @media (min-width: 1024px) {
+ min-width: 344px;
+ }
+ }
+ }
+
+ &.solid-select--borderless {
+ [data-scope='select'][data-part='control'],
+ [data-scope='combobox'][data-part='control'] {
+ border: 0;
+ }
+ }
+
+ &.solid-select--adaptive-height {
+ [data-scope='select'][data-part='control'],
+ [data-scope='combobox'][data-part='control'] {
+ height: 100%;
+ min-height: 40px;
+ }
+
+ [data-scope='select'][data-part='value-text'],
+ [data-scope='combobox'][data-part='value-text'] {
+ width: 100%;
+ }
+ }
+
+ &.solid-select--height-small {
+ [data-scope='select'][data-part='control'],
+ [data-scope='combobox'][data-part='control'] {
+ min-height: 40px;
+ font-size: 16px;
+ }
+ }
+
+ &.solid-select--height-medium {
+ [data-scope='select'][data-part='control'],
+ [data-scope='combobox'][data-part='control'] {
+ min-height: 40px;
+ font-size: 16px;
+
+ @media (min-width: 1024px) {
+ min-height: 48px;
+ }
+ }
+ }
+
+ &.solid-select--height-big {
+ [data-scope='select'][data-part='control'],
+ [data-scope='combobox'][data-part='control'] {
+ min-height: 56px;
+ font-size: 16px;
+
+ @media (min-width: 1024px) {
+ min-height: 56px;
+ }
+ }
+ }
+
+ &.solid-select--height-big-mobile {
+ [data-scope='select'][data-part='control'],
+ [data-scope='combobox'][data-part='control'] {
+ height: 56px;
+ font-size: 16px;
+
+ @media (min-width: 1024px) {
+ height: 40px;
+ }
+ }
+ }
+
+ &.solid-select--dropdown {
+ [data-scope='select'][data-part='content'],
+ [data-scope='combobox'][data-part='content'] {
+ position: relative;
+ top: 0;
+ margin: 0;
+ box-shadow: none;
+ width: 100%;
+ max-width: 100%;
+ min-width: auto;
+ }
+
+ [data-scope='select'][data-part='control'],
+ [data-scope='combobox'][data-part='control'] {
+ display: none;
+ }
+ }
+
+ @media (min-width: 1024px) {
+ width: 240px;
+ }
+
+ [data-scope='select'][data-part='content'],
+ [data-scope='combobox'][data-part='content'] {
+ color: var(--default-main-text);
+ padding: 0;
+ margin-top: 4px;
+ overflow: hidden;
+ box-shadow: 8px 8px 24px 0 rgba(0, 0, 0, 0.1);
+ background-color: var(--default-dropdown-menu-background);
+ z-index: 3;
+
+ @media (min-width: 1024px) {
+ left: initial;
+ right: initial;
+ width: 100%;
+ }
+ }
+
+ [data-scope='combobox'][data-part='empty'] {
+ padding: 16px 12px;
+ color: var(--default-description-text);
+ font-size: 16px;
+ text-align: center;
+ }
+
+ [data-scope='select'][data-part='control'],
+ [data-scope='combobox'][data-part='control'] {
+ min-height: 40px;
+ border: 1px solid var(--default-item-divider);
+ box-shadow: none;
+ border-radius: 8px;
+ transition: border var(--t2);
+ cursor: pointer;
+ font-size: 16px;
+ background-color: var(--default-page-background);
+ display: flex;
+ align-items: center;
+
+ &:hover,
+ &:focus-within {
+ border-color: var(--hovered-main-button);
+ }
+
+ &[data-disabled] {
+ background-color: var(--pressed-page-background);
+ cursor: default;
+
+ &:hover,
+ &:focus-within {
+ border-color: var(--default-item-divider);
+ }
+
+ [data-scope='select'][data-part='value-text'],
+ [data-scope='combobox'][data-part='value-text'] {
+ color: var(--default-description-text);
+ }
+ }
+ }
+
+ [data-scope='select'][data-part='trigger'] {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ padding-left: 16px;
+ overflow: hidden;
+ min-height: inherit;
+ background: none;
+ border: none;
+ cursor: pointer;
+ text-align: left;
+ color: var(--default-main-text);
+ font-size: inherit;
+
+ &:focus {
+ outline: none;
+ }
+ }
+
+ [data-scope='combobox'][data-part='trigger'] {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0 12px;
+ background: none;
+ border: none;
+ cursor: pointer;
+ color: var(--default-gray-icons);
+ transition: transform var(--t2);
+
+ svg {
+ transform-origin: center center;
+ transition: transform var(--t2);
+ }
+
+ &[data-state='open'] svg {
+ transform: rotate(180deg);
+ }
+ }
+
+ [data-scope='select'][data-part='value-text'],
+ [data-scope='combobox'][data-part='value-text'] {
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ color: var(--default-main-text);
+
+ &:empty {
+ }
+ }
+
+ [data-scope='select'][data-part='clear-trigger'],
+ [data-scope='combobox'][data-part='clear-trigger'] {
+ display: flex;
+ align-items: center;
+ padding: 8px 0;
+ cursor: pointer;
+ border: none;
+ background: none;
+ color: var(--default-gray-icons);
+
+ @media (min-width: 1024px) {
+ padding: 8px;
+ }
+ }
+
+ &.solid-select--multi [data-scope='combobox'][data-part='clear-trigger'] {
+ padding-right: 12px;
+ }
+
+ [data-scope='select'][data-part='indicator'] {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ padding-right: 12px;
+ color: var(--default-gray-icons);
+ transition: transform var(--t2);
+
+ svg {
+ transform-origin: center center;
+ transition: transform var(--t2);
+ }
+
+ &:focus,
+ &:hover {
+ color: var(--default-gray-icons);
+ }
+
+ &[data-state='open'] svg {
+ transform: rotate(180deg);
+ }
+ }
+
+ [data-scope='combobox'][data-part='input'] {
+ flex: 1;
+ min-width: 80px;
+ border: none;
+ background: transparent;
+ font-size: inherit;
+ color: var(--default-main-text);
+ padding-left: 16px;
+ outline: none;
+
+ &::placeholder {
+ color: var(--default-description-text);
+ }
+ }
+
+ [data-scope='select'][data-part='item'],
+ [data-scope='combobox'][data-part='item'] {
+ color: var(--default-main-text);
+ transition: all var(--t2);
+ cursor: pointer;
+ padding: 16px 12px;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+
+ &[data-highlighted] {
+ background-color: var(--hovered-dropdown-menu-background);
+ }
+
+ &[data-state='checked'] {
+ background-color: var(--pressed-dropdown-menu-background);
+ }
+
+ &[data-disabled] {
+ color: var(--disabled-main-text);
+ cursor: default;
+
+ &[data-highlighted] {
+ background-color: transparent;
+ }
+ }
+ }
+
+ [data-scope='select'][data-part='item-text'],
+ [data-scope='combobox'][data-part='item-text'] {
+ flex: 1;
+ font-size: 16px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ [data-scope='select'][data-part='item-indicator'],
+ [data-scope='combobox'][data-part='item-indicator'] {
+ display: flex;
+ align-items: center;
+ flex-shrink: 0;
+ color: var(--default-gray-icons);
+
+ &[data-state='checked'] {
+ color: var(--default-product-icon);
+ }
+ }
+
+ [data-scope='select'][data-part='item-group'],
+ [data-scope='combobox'][data-part='item-group'] {
+ padding: 0;
+ overflow-y: auto;
+ overflow-x: hidden;
+ max-height: 340px;
+
+ &::-webkit-scrollbar {
+ width: 6px;
+ }
+
+ &::-webkit-scrollbar-track {
+ background-color: transparent;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background-color: var(--default-gray-icons);
+ border-radius: 10px;
+ }
+ }
+
+ .solid-select-multi-value-container {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ flex: 1;
+ min-width: 0;
+ padding: 8px 12px 8px 0;
+
+ [data-scope='combobox'][data-part='input'] {
+ flex: 1 1 80px;
+ min-width: 80px;
+ max-width: 100%;
+ border: none;
+ background: transparent;
+ font-size: inherit;
+ color: var(--default-main-text);
+ padding: 6px 0;
+ margin: 0;
+ outline: none;
+
+ &::placeholder {
+ color: var(--default-description-text);
+ }
+ }
+ }
+
+ [data-scope='combobox'][data-part='control'] .solid-select-multi-value-container {
+ padding-left: 16px;
+ }
+
+ /* Single-select: value overlay + input share a grid cell. */
+ .solid-combobox-single-value-wrapper {
+ position: relative;
+ display: grid;
+ flex: 1;
+ min-width: 0;
+ align-items: center;
+
+ .solid-combobox-single-value,
+ .solid-select-placeholder,
+ [data-scope='combobox'][data-part='input'] {
+ grid-column: 1;
+ grid-row: 1;
+ }
+
+ .solid-combobox-single-value,
+ .solid-select-placeholder {
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ padding: 0 16px;
+ pointer-events: none;
+ }
+
+ .solid-combobox-single-value {
+ color: var(--default-main-text);
+ }
+
+ .solid-select-placeholder {
+ color: var(--default-description-text);
+ }
+
+ [data-scope='combobox'][data-part='input'] {
+ width: 100%;
+ min-width: 0;
+ border: none;
+ background: transparent;
+ font-size: inherit;
+ color: var(--default-main-text);
+ padding: 0 16px;
+ outline: none;
+
+ /* Placeholder shown by the overlay, not the input. */
+ &::placeholder {
+ color: transparent;
+ }
+ }
+ }
+
+ .solid-select-placeholder {
+ color: var(--default-description-text);
+ }
+}
diff --git a/client_v2/src/common/controls/Select/Select.tsx b/client_v2/src/common/controls/Select/Select.tsx
new file mode 100644
index 00000000000..0083032bd0d
--- /dev/null
+++ b/client_v2/src/common/controls/Select/Select.tsx
@@ -0,0 +1,440 @@
+import { type JSX, For, Show, createMemo, createSignal } from 'solid-js';
+import cn from 'clsx';
+import { Select as ArkSelect, Combobox as ArkCombobox, createListCollection } from '@ark-ui/solid';
+import { Icon } from 'panel/common/ui/Icon';
+import intl from 'panel/common/intl';
+import theme from 'panel/lib/theme';
+
+import { SelectMultiValueDisplay } from './SelectMultiValueDisplay';
+import { ComboboxMultiValueDisplay } from './ComboboxMultiValueDisplay';
+import { ComboboxInputReset } from './ComboboxInputReset';
+import { SelectItemContent } from './SelectItemContent';
+import { ComboboxItemContent } from './ComboboxItemContent';
+
+import { SelectProps, SEARCH_ENABLE_LIMIT } from './types';
+import { optionToValue, filterOptions, getItemTestId } from './helpers';
+
+import './Select.pcss';
+import s from './MenuList.module.pcss';
+
+export type {
+ ISelectSize,
+ ISelectHeight,
+ ISelectMenuSize,
+ ISelectValue,
+ SelectProps,
+} from './types';
+
+export const Select = <
+ T,
+ Multi extends boolean = false,
+ ExtendOption extends Record = object,
+>(
+ props: SelectProps,
+): JSX.Element => {
+ /* ---- Derived state ---- */
+ const selectClass = createMemo(() =>
+ cn(
+ { 'desktop-select-always': props.mobile === false },
+ { 'mobile-select-always': props.mobile },
+ 'solid-select',
+ { 'solid-select--multi': props.isMulti },
+ { 'solid-select-big': props.size === 'big' },
+ { 'solid-select-medium': props.size === 'medium' },
+ { 'solid-select-small': props.size === 'small' },
+ { 'solid-select-responsive': props.size === 'responsive' },
+ { 'solid-select--big-limit': props.size === 'big-limit' },
+ { 'solid-select--menu-small': props.menuSize === 'small' },
+ { 'solid-select--menu-medium': props.menuSize === 'medium' },
+ { 'solid-select--menu-big': props.menuSize === 'big' },
+ { 'solid-select--menu-right': props.menuPosition === 'right' },
+ { 'solid-select--borderless': props.borderless },
+ { 'solid-select--adaptive-height': props.adaptiveHeight },
+ { 'solid-select--height-small': props.height === 'small' },
+ { 'solid-select--height-medium': props.height === 'medium' },
+ { 'solid-select--height-big': props.height === 'big' },
+ { 'solid-select--height-big-mobile': props.height === 'big-mobile' },
+ { 'solid-select--dropdown': props.isDropdownSelect },
+ { low: props.low },
+ props.class,
+ ),
+ );
+
+ const isSearchable = createMemo(() => {
+ // Explicit isSearchable prop takes precedence over defaults.
+ if (props.isSearchable !== undefined) {
+ return props.isSearchable;
+ }
+ // Default: only non-multi selects with > SEARCH_ENABLE_LIMIT options are searchable.
+ return !(props.isMulti ?? false) && props.options.length > SEARCH_ENABLE_LIMIT;
+ });
+
+ const isDisabled = createMemo(() => props.isDisabled ?? false);
+ const isMulti = createMemo(() => props.isMulti ?? false);
+ const isClearable = createMemo(() => props.isClearable ?? false);
+
+ const [searchInput, setSearchInput] = createSignal('');
+ // Label shown immediately after selection, before the async onChange round-trip
+ // updates props.value. Always overridden by props.value when available.
+ const [pendingLabel, setPendingLabel] = createSignal();
+ // DOM ref for defensive input clear in multi-select (avoids onInputValueChange loop).
+ let inputRef: HTMLInputElement | undefined;
+
+ const filteredOptions = createMemo(() => filterOptions(props.options, searchInput()));
+
+ const collection = createMemo(() =>
+ createListCollection({
+ items: filteredOptions() as any[],
+ itemToString: (item: any) => item.label,
+ itemToValue: (item: any) => optionToValue(item.value),
+ isItemDisabled: (item: any) => item.isDisabled ?? item.disabled ?? false,
+ }),
+ );
+
+ const currentValue = createMemo(() => {
+ if (!props.value) return [];
+ const arr = Array.isArray(props.value) ? props.value : [props.value];
+ return arr.map((v: any) => optionToValue(v.value));
+ });
+
+ const handleValueChange = (details: { items: any[] }) => {
+ if (isMulti()) {
+ props.onChange(details.items as any);
+ } else {
+ const item = details.items[0];
+ setPendingLabel(item?.label);
+ props.onChange(item as any);
+ }
+ };
+
+ // Attached to both Content and inner scroll div (scroll events don't bubble).
+ const onContentScroll = (e: Event) => {
+ if (!props.onMenuScrollToBottom) return;
+ const el = e.currentTarget as HTMLElement;
+ if (el.scrollHeight - el.scrollTop <= el.clientHeight + 50) {
+ props.onMenuScrollToBottom();
+ }
+ };
+
+ const positioning = createMemo(() => ({
+ placement: (props.menuPlacement === 'top' ? 'top-start' : 'bottom-start') as
+ | 'top-start'
+ | 'bottom-start',
+ sameWidth: true,
+ fitViewport: true,
+ flip: (props.menuPlacement === 'auto'
+ ? true
+ : props.menuPlacement === 'top'
+ ? ['top-start', 'bottom-start']
+ : ['bottom-start', 'top-start']) as boolean | ('top-start' | 'bottom-start')[],
+ }));
+
+ const testId = (option: any) => getItemTestId(props.optionTestIdPrefix, option.value);
+
+ const clearAriaLabel = intl.getMessage('clear_btn');
+
+ // Reads the label from the controlled value prop (option object).
+ // Falls back to pendingLabel (set on selection) before props.value updates.
+ const singleValueLabel = createMemo(() => {
+ const value = props.value;
+ if (value) {
+ const arr = Array.isArray(value) ? value : [value];
+ const label = (arr[0] as any)?.label;
+ if (label) return label;
+ }
+ return pendingLabel() ?? '';
+ });
+
+ return (
+
+ {
+ if (details.open) {
+ props.onMenuOpen?.();
+ } else {
+ props.onMenuClose?.();
+ }
+ }}
+ positioning={positioning()}
+ >
+
+
+ {
+ if (props.autoFocus) el.focus();
+ }}
+ >
+
+
+
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ >
+ {/* ===== Searchable (Combobox) mode: Ark UI Combobox ===== */}
+
+
{
+ if (details.reason === 'input-change') {
+ setSearchInput(details.inputValue);
+ } else {
+ // Non-typing change (select/clear/close): reset filter.
+ setSearchInput('');
+ if (isMulti() && inputRef) {
+ inputRef.value = '';
+ }
+ }
+ }}
+ onOpenChange={(details) => {
+ if (details.open) {
+ setSearchInput('');
+ props.onMenuOpen?.();
+ } else {
+ // Stale search cleared by ComboboxInputReset.
+ props.onMenuClose?.();
+ }
+ }}
+ positioning={positioning()}
+ >
+
+
+ {
+ // Click the input when clicking the control's dead zone.
+ if (e.target === e.currentTarget) {
+ const input = (e.currentTarget as HTMLElement).querySelector(
+ 'input',
+ );
+ input?.click();
+ }
+ }}
+ >
+
+
+ {
+ inputRef = el;
+ }}
+ />
+
+ {/* Clear button shown whenever values exist. */}
+ 0}>
+
+
+
+
+
+
+
+
+
+
+
+
+
}
+ >
+
+ {props.placeholder as string}
+
+ }
+ >
+
+ {singleValueLabel()}
+
+
+
+
{
+ inputRef = el;
+ }}
+ />
+
+ 0}>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ >
+
+
+ {(option) => (
+
+
+
+ )}
+
+ }
+ >
+
+
+
+ {props.menuFooter}
+
+ {intl.getMessage('nothing_found')}
+
+
+
+
+
+
+
+ );
+};
diff --git a/client_v2/src/common/controls/Select/SelectItemContent.tsx b/client_v2/src/common/controls/Select/SelectItemContent.tsx
new file mode 100644
index 00000000000..39614dc0576
--- /dev/null
+++ b/client_v2/src/common/controls/Select/SelectItemContent.tsx
@@ -0,0 +1,28 @@
+import { type JSX, Show } from 'solid-js';
+import { Select as ArkSelect } from '@ark-ui/solid';
+import { Icon } from 'panel/common/ui/Icon';
+import { SelectCheckIcon } from './CheckIcons';
+import s from './MenuList.module.pcss';
+
+interface ItemContentProps {
+ option: any;
+ showOptionIcon?: boolean;
+ showIcons?: boolean;
+}
+
+// Option row for the non-searchable Select branch.
+export const SelectItemContent = (props: ItemContentProps): JSX.Element => (
+ <>
+
+
+
+
+
+
+
+
+
+
+ {props.option.label}
+ >
+);
diff --git a/client_v2/src/common/controls/Select/SelectMultiValue.module.pcss b/client_v2/src/common/controls/Select/SelectMultiValue.module.pcss
new file mode 100644
index 00000000000..0cf43289853
--- /dev/null
+++ b/client_v2/src/common/controls/Select/SelectMultiValue.module.pcss
@@ -0,0 +1,34 @@
+.pill {
+ display: inline-flex;
+ align-items: center;
+ gap: 12px;
+ padding: 6px 8px 6px 16px;
+ border: 1px solid var(--default-item-divider);
+ border-radius: 200px;
+ background-color: var(--default-page-background);
+ cursor: default;
+ white-space: nowrap;
+}
+
+.label {
+ font-size: 14px;
+ line-height: 1.5;
+ color: var(--default-main-text);
+ max-width: 160px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.removeBtn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 20px;
+ height: 20px;
+ padding: 0;
+ border: none;
+ background: transparent;
+ cursor: pointer;
+ flex-shrink: 0;
+ color: var(--default-gray-icons);
+}
diff --git a/client_v2/src/common/controls/Select/SelectMultiValue.tsx b/client_v2/src/common/controls/Select/SelectMultiValue.tsx
new file mode 100644
index 00000000000..37950774708
--- /dev/null
+++ b/client_v2/src/common/controls/Select/SelectMultiValue.tsx
@@ -0,0 +1,37 @@
+import { For } from 'solid-js';
+import { Icon } from 'panel/common/ui/Icon';
+import intl from 'panel/common/intl';
+
+import s from './SelectMultiValue.module.pcss';
+
+interface SelectMultiValueProps {
+ items: {
+ value: string | null;
+ label: string;
+ [key: string]: any;
+ }[];
+ onRemove: (item: any) => void;
+}
+
+export const SelectMultiValue = (props: SelectMultiValueProps) => (
+
+ {(item) => (
+
+ {item.label}
+ {
+ e.stopPropagation();
+ props.onRemove(item);
+ }}
+ aria-label={intl.getMessage('remove_tag', {
+ value: item.label,
+ })}
+ >
+
+
+
+ )}
+
+);
diff --git a/client_v2/src/common/controls/Select/SelectMultiValueDisplay.tsx b/client_v2/src/common/controls/Select/SelectMultiValueDisplay.tsx
new file mode 100644
index 00000000000..a977b224006
--- /dev/null
+++ b/client_v2/src/common/controls/Select/SelectMultiValueDisplay.tsx
@@ -0,0 +1,25 @@
+import { type JSX, Show } from 'solid-js';
+import { useSelectContext } from '@ark-ui/solid';
+import { SelectMultiValue } from './SelectMultiValue';
+import { optionToValue } from './helpers';
+
+interface DisplayProps {
+ placeholder?: string;
+}
+
+// Multi-value pills for non-searchable Select (reads from Select context).
+export const SelectMultiValueDisplay = (props: DisplayProps): JSX.Element => {
+ const selectCtx = useSelectContext();
+
+ return (
+ {props.placeholder ?? ''}}
+ >
+ selectCtx().clearValue(optionToValue(item.value))}
+ />
+
+ );
+};
diff --git a/client_v2/src/common/controls/Select/helpers.ts b/client_v2/src/common/controls/Select/helpers.ts
new file mode 100644
index 00000000000..c06478c59d8
--- /dev/null
+++ b/client_v2/src/common/controls/Select/helpers.ts
@@ -0,0 +1,13 @@
+// Converts an option value to string; null/undefined → '' (not "null"/"undefined").
+export const optionToValue = (value: unknown): string => (value == null ? '' : String(value));
+
+// Case-insensitive label filter; empty query returns options unchanged.
+export const filterOptions = (options: T[], query: string): T[] => {
+ const q = query.toLowerCase();
+ if (!q) return options;
+ return options.filter((opt) => String(opt.label).toLowerCase().includes(q));
+};
+
+// Returns `${prefix}-${value}` for test IDs, or undefined when no prefix.
+export const getItemTestId = (prefix: string | undefined, value: unknown): string | undefined =>
+ prefix ? `${prefix}-${optionToValue(value)}` : undefined;
diff --git a/client_v2/src/common/controls/Select/index.ts b/client_v2/src/common/controls/Select/index.ts
new file mode 100644
index 00000000000..5ba2e764534
--- /dev/null
+++ b/client_v2/src/common/controls/Select/index.ts
@@ -0,0 +1,8 @@
+export type {
+ ISelectSize,
+ ISelectHeight,
+ ISelectMenuSize,
+ ISelectValue,
+ SelectProps,
+} from './types';
+export { Select } from './Select';
diff --git a/client_v2/src/common/controls/Select/types.ts b/client_v2/src/common/controls/Select/types.ts
new file mode 100644
index 00000000000..e21da7f2171
--- /dev/null
+++ b/client_v2/src/common/controls/Select/types.ts
@@ -0,0 +1,52 @@
+import { type JSX } from 'solid-js';
+import { IOption } from 'panel/lib/helpers/utils';
+
+export const SEARCH_ENABLE_LIMIT = 10;
+
+export type ISelectSize = 'auto' | 'small' | 'medium' | 'big' | 'big-limit' | 'responsive';
+export type ISelectHeight = 'small' | 'medium' | 'big' | 'big-mobile';
+export type ISelectMenuSize = 'small' | 'medium' | 'big' | 'large';
+export type ISelectValue = Multi extends true ? IOption[] : IOption;
+
+export interface SelectProps<
+ T,
+ Multi extends boolean = false,
+ ExtendOption extends Record = object,
+> {
+ size?: ISelectSize;
+ height?: ISelectHeight;
+ menuSize?: ISelectMenuSize;
+ menuPosition?: 'right';
+ mobile?: boolean;
+ isDisabled?: boolean;
+ isMulti?: Multi;
+ menuIsOpen?: boolean;
+ placeholder?: JSX.Element;
+ class?: string;
+ autoFocus?: boolean;
+ low?: boolean;
+ options: (IOption & ExtendOption)[];
+ onChange: (
+ value: Multi extends true ? (IOption & ExtendOption)[] : IOption & ExtendOption,
+ ) => void;
+ value?: (IOption & ExtendOption) | (IOption & ExtendOption)[];
+ isSearchable?: boolean;
+ id?: string;
+ inputId?: string;
+ borderless?: boolean;
+ adaptiveHeight?: boolean;
+ lazyList?: boolean;
+ closeMenuOnSelect?: boolean;
+ menuPlacement?: 'top' | 'bottom' | 'auto';
+ isClearable?: boolean;
+ onMenuScrollToBottom?: () => void;
+ isLoading?: boolean;
+ isDropdownSelect?: boolean;
+ onMenuOpen?: () => void;
+ onMenuClose?: () => void;
+ showIcons?: boolean;
+ showOptionIcon?: boolean;
+ optionTestIdPrefix?: string;
+ /** Optional footer rendered after the scrollable item list inside the dropdown. */
+ menuFooter?: JSX.Element;
+}
diff --git a/client_v2/src/common/controls/Switch/Switch.module.pcss b/client_v2/src/common/controls/Switch/Switch.module.pcss
new file mode 100644
index 00000000000..b9e17aed3b7
--- /dev/null
+++ b/client_v2/src/common/controls/Switch/Switch.module.pcss
@@ -0,0 +1,88 @@
+.switch {
+ display: inline-flex;
+ align-items: center;
+ cursor: pointer;
+ gap: 8px;
+
+ &.disabled {
+ cursor: default;
+ }
+}
+
+.input {
+ display: none;
+}
+
+.label {
+ font-size: 16px;
+ font-weight: var(--weight-medium);
+ user-select: none;
+}
+
+.handler {
+ position: relative;
+ width: 40px;
+ height: 20px;
+ border-radius: 24px;
+ background-color: var(--fills-switch-off-default);
+ transition: background-color var(--t2);
+
+ &:hover,
+ &:focus {
+ background-color: var(--fills-switch-off-hovered);
+ }
+
+ &:before {
+ content: '';
+ position: absolute;
+ top: 2px;
+ left: 2px;
+ width: 16px;
+ height: 16px;
+ border-radius: 16px;
+ background-color: var(--fills-switch-knob);
+ transition:
+ transform var(--t2),
+ background-color var(--t2);
+ }
+}
+
+.input:checked + .handler {
+ background-color: var(--fills-switch-on-default);
+
+ &:hover,
+ &:focus {
+ background-color: var(--fills-switch-on-hovered);
+ }
+
+ &:before {
+ transform: translateX(20px);
+ }
+}
+
+.input:disabled + .handler {
+ cursor: default;
+ background-color: var(--fills-switch-off-disabled);
+
+ &:hover,
+ &:focus {
+ background-color: var(--fills-switch-off-disabled);
+ }
+
+ &:before {
+ color: var(--fills-switch-knob-disabled);
+ }
+}
+
+.input:checked:disabled + .handler {
+ background-color: var(--fills-switch-on-disabled);
+
+ &:hover,
+ &:focus {
+ background-color: var(--fills-switch-on-disabled);
+ }
+
+ &:before {
+ color: var(--fills-switch-knob-disabled);
+ }
+}
diff --git a/client_v2/src/common/controls/Switch/Switch.tsx b/client_v2/src/common/controls/Switch/Switch.tsx
new file mode 100644
index 00000000000..286d25128b2
--- /dev/null
+++ b/client_v2/src/common/controls/Switch/Switch.tsx
@@ -0,0 +1,50 @@
+import { type JSX, Show } from 'solid-js';
+import cn from 'clsx';
+
+import s from './Switch.module.pcss';
+
+type Props = Omit, 'onChange' | 'type'> & {
+ id: string;
+ checked: boolean;
+ disabled?: boolean;
+ labelClass?: string;
+ class?: string;
+ wrapperClass?: string;
+ onChange: (e: Event) => void;
+ children?: JSX.Element;
+ ref?: HTMLInputElement | ((el: HTMLInputElement) => void);
+};
+
+export const Switch = (props: Props) => {
+ const setRef = (el: HTMLInputElement) => {
+ if (typeof props.ref === 'function') {
+ props.ref(el);
+ }
+ };
+
+ const switchControls = (
+ <>
+ props.onChange?.(e)}
+ checked={props.checked}
+ disabled={props.disabled}
+ ref={(el) => setRef(el)}
+ />
+
+
+ {props.children}
+
+ >
+ );
+
+ return (
+
+
+ {switchControls}
+
+
+ );
+};
diff --git a/client_v2/src/common/controls/Switch/index.ts b/client_v2/src/common/controls/Switch/index.ts
new file mode 100644
index 00000000000..cee89a18b17
--- /dev/null
+++ b/client_v2/src/common/controls/Switch/index.ts
@@ -0,0 +1 @@
+export { Switch } from './Switch';
diff --git a/client_v2/src/common/controls/Textarea/Textarea.tsx b/client_v2/src/common/controls/Textarea/Textarea.tsx
new file mode 100644
index 00000000000..5fcf1a5b86f
--- /dev/null
+++ b/client_v2/src/common/controls/Textarea/Textarea.tsx
@@ -0,0 +1,67 @@
+import { type JSX, Show } from 'solid-js';
+import cn from 'clsx';
+
+import s from './styles.module.pcss';
+
+type TextareaChangeEvent = Event & {
+ currentTarget: HTMLTextAreaElement;
+ target: HTMLTextAreaElement;
+};
+
+type Props = Omit, 'onChange' | 'onBlur'> & {
+ label?: JSX.Element;
+ size?: 'small' | 'medium' | 'large';
+ errorMessage?: string;
+ ref?: HTMLTextAreaElement | ((el: HTMLTextAreaElement) => void);
+ onChange?: (event: TextareaChangeEvent) => void;
+ onBlur?: (event: FocusEvent) => void;
+};
+
+export const Textarea = (props: Props) => {
+ const setRef = (el: HTMLTextAreaElement) => {
+ if (typeof props.ref === 'function') {
+ props.ref(el);
+ }
+ };
+
+ const handleChange = (e: TextareaChangeEvent) => {
+ props.onChange?.(e);
+ };
+
+ const handleBlur = (e: FocusEvent) => {
+ props.onBlur?.(e);
+ };
+
+ return (
+
+
+
+ {props.label}
+
+
+
+ );
+};
diff --git a/client_v2/src/common/controls/Textarea/index.ts b/client_v2/src/common/controls/Textarea/index.ts
new file mode 100644
index 00000000000..f4212c043e1
--- /dev/null
+++ b/client_v2/src/common/controls/Textarea/index.ts
@@ -0,0 +1 @@
+export { Textarea } from './Textarea';
diff --git a/client_v2/src/common/controls/Textarea/styles.module.pcss b/client_v2/src/common/controls/Textarea/styles.module.pcss
new file mode 100644
index 00000000000..2b09ad8afc5
--- /dev/null
+++ b/client_v2/src/common/controls/Textarea/styles.module.pcss
@@ -0,0 +1,68 @@
+.textareaWrapper {
+ display: flex;
+ flex-direction: column;
+}
+
+.textarea {
+ font-size: 16px;
+ line-height: 1.3;
+ max-width: 100%;
+ padding: 16px;
+ border: 1px solid var(--default-item-divider);
+ outline: none;
+ color: var(--default-main-text);
+ background-color: var(--default-page-background);
+ border-radius: 8px;
+ transition:
+ border var(--t2),
+ background-color var(--t2);
+ resize: none;
+
+ &:focus {
+ border-color: var(--pressed-link);
+ }
+
+ &::placeholder {
+ color: var(--default-placeholder);
+ }
+
+ &.error {
+ border-color: var(--default-error-icon);
+ }
+
+ &:disabled {
+ background-color: var(--pressed-page-background);
+ color: var(--default-description-text);
+ cursor: default;
+
+ &:-webkit-autofill,
+ &:-webkit-autofill:hover,
+ &:-webkit-autofill:focus {
+ box-shadow: 0 0 0 100px var(--pressed-page-background) inset;
+ }
+ }
+
+ &.medium {
+ min-height: 120px;
+ }
+
+ &.large {
+ min-height: 200px;
+ }
+}
+
+.label {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 4px;
+ font-size: 14px;
+ line-height: 24px;
+ color: var(--default-description-text);
+}
+
+.errorMessage {
+ margin-top: 8px;
+ font-size: 14px;
+ color: var(--default-error-icon);
+}
diff --git a/client_v2/src/common/intl/index.ts b/client_v2/src/common/intl/index.ts
new file mode 100644
index 00000000000..ab808bb3f99
--- /dev/null
+++ b/client_v2/src/common/intl/index.ts
@@ -0,0 +1,229 @@
+import { createSignal } from 'solid-js';
+
+import { I18nInterface, translate } from '@adguard/translate';
+import { BASE_LOCALE } from 'panel/helpers/twosky';
+
+import en from 'panel/__locales/en.json';
+import de from 'panel/__locales/de.json';
+import es from 'panel/__locales/es.json';
+import fr from 'panel/__locales/fr.json';
+import it from 'panel/__locales/it.json';
+import ja from 'panel/__locales/ja.json';
+import ko from 'panel/__locales/ko.json';
+import ptBr from 'panel/__locales/pt-br.json';
+import ptPt from 'panel/__locales/pt-pt.json';
+import ru from 'panel/__locales/ru.json';
+import zhCn from 'panel/__locales/zh-cn.json';
+import zhTw from 'panel/__locales/zh-tw.json';
+import { LOCAL_STORAGE_KEYS, LocalStorageHelper } from 'panel/helpers/localStorageHelper';
+
+export type LocalesType = ReturnType;
+
+type LocalesTypes = Partial>>;
+
+const LOCALES = {
+ en,
+ de,
+ es,
+ fr,
+ it,
+ ja,
+ ko,
+ ru,
+ 'pt-br': ptBr,
+ 'pt-pt': ptPt,
+ 'zh-cn': zhCn,
+ 'zh-tw': zhTw,
+};
+
+const messages: LocalesTypes = LOCALES;
+
+const resolveLanguage = (lng: string): LocalesType => {
+ const l = lng.toLowerCase();
+
+ if (messages[l as LocalesType]) {
+ return l as LocalesType;
+ }
+
+ // Chinese locales
+ if (l.startsWith('zh')) {
+ if (l.includes('tw') || l.includes('hk') || l.includes('mo')) {
+ return 'zh-tw' as LocalesType;
+ }
+ return 'zh-cn' as LocalesType;
+ }
+
+ // Portuguese locales
+ if (l.startsWith('pt')) {
+ if (l.includes('br')) {
+ return 'pt-br' as LocalesType;
+ }
+ return 'pt-pt' as LocalesType;
+ }
+
+ // Try base language (e.g., en-us -> en)
+ const base = l.split('-')[0] as LocalesType;
+ if (messages[base]) {
+ return base;
+ }
+
+ return BASE_LOCALE as LocalesType;
+};
+
+export const i18n = (lang: LocalesType) => {
+ const resolved = resolveLanguage(lang);
+ return {
+ getMessage: (key: string) => messages[resolved]?.[key] || '',
+ getUILanguage: () => resolved,
+ getBaseMessage: (key: string) => messages.en![key] || key,
+ getBaseUILanguage: () => BASE_LOCALE as LocalesType,
+ };
+};
+
+const detectedLanguage = ((typeof window !== 'undefined' &&
+ typeof localStorage !== 'undefined' &&
+ LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.LANGUAGE)) ||
+ (typeof navigator !== 'undefined' && (navigator.language as string)) ||
+ BASE_LOCALE) as LocalesType;
+
+const initialLanguage: LocalesType = resolveLanguage(detectedLanguage);
+
+// Solid-reactive language signal
+const [lang, setLang] = createSignal(initialLanguage);
+
+/**
+ * Creates default value functions for common HTML tags.
+ * Mirrors createReactTranslator's createDefaultValues(), but returns real
+ * DOM nodes via document.createElement instead of React element descriptors.
+ *
+ * Why document.createElement and not solid-js/h or :
+ * @adguard/translate is built around the React/Preact model where
+ * createElement() returns a cheap descriptor object the framework reconciles
+ * later. SolidJS has no equivalent — its JSX compiles to fine-grained template
+ * cloning, and h()/Dynamic need a reactive owner. A plain DOM node has no
+ * ownership entanglement: SolidJS just inserts and removes it
+ * (normalizeIncomingArray keeps anything with a `.nodeType` as-is), which is
+ * the most robust option for these static styling tags.
+ *
+ * ` ` is intentionally NOT provided: the parser rejects ` `/` ` as
+ * unbalanced tags, and void tags must be plain strings (not functions), so a
+ * `br` handler could never be invoked anyway.
+ *
+ * These are fallbacks for when a component doesn't pass a per-call handler.
+ * The library merges user-supplied values over these, with user values winning.
+ * For interactive tags (, ), always pass a per-call JSX handler so
+ * events go through SolidJS's delegation system.
+ */
+export const createSolidDefaultValues = () => ({
+ // Inline elements
+ strong: (children: string) => {
+ const el = document.createElement('strong');
+ el.textContent = children;
+ return el;
+ },
+ code: (children: string) => {
+ const el = document.createElement('code');
+ el.textContent = children;
+ return el;
+ },
+ b: (children: string) => {
+ const el = document.createElement('b');
+ el.textContent = children;
+ return el;
+ },
+ i: (children: string) => {
+ const el = document.createElement('i');
+ el.textContent = children;
+ return el;
+ },
+ em: (children: string) => {
+ const el = document.createElement('em');
+ el.textContent = children;
+ return el;
+ },
+
+ // Block elements
+ p: (children: string) => {
+ const el = document.createElement('p');
+ el.textContent = children;
+ return el;
+ },
+});
+
+/**
+ * Custom message constructor for SolidJS.
+ * When all formatted elements are strings, join them (backward compatible).
+ * When JSX elements are present, return the array — SolidJS renders
+ * arrays of strings + JSX elements natively.
+ */
+export const solidMessageConstructor = (formatted: unknown[]): string | unknown[] => {
+ if (formatted.every((child) => typeof child === 'string')) {
+ return (formatted as string[]).join('');
+ }
+ return formatted;
+};
+
+/**
+ * Builds a @adguard/translate translator configured for SolidJS.
+ *
+ * Hand-rolled `createSolidTranslator` adapter: the library ships React/Preact
+ * factories but no Solid one, so we reuse the generic createTranslator and
+ * inject:
+ * - solidMessageConstructor: keeps arrays of nodes intact (the default
+ * join('') would stringify DOM/JSX nodes to "[object Object]");
+ * - createSolidDefaultValues(): renders styling tags as real DOM nodes.
+ *
+ * See createSolidDefaultValues for why document.createElement is used.
+ */
+const createSolidTranslator = (i18nInstance: ReturnType) =>
+ translate.createTranslator(
+ i18nInstance,
+ solidMessageConstructor,
+ createSolidDefaultValues(),
+ );
+
+let translator = createSolidTranslator(i18n(initialLanguage));
+
+const intl = {
+ getMessage: (key: string, values?: any) => {
+ lang(); // track the language signal for reactivity
+ try {
+ return translator.getMessage(key, values);
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.warn('[i18n] Missing placeholder value for key:', key, e);
+ return key;
+ }
+ },
+
+ getPlural: (key: string, number: number, values?: any) => {
+ lang(); // track the language signal for reactivity
+ try {
+ return translator.getPlural(key, number, values);
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.warn('[i18n] Missing placeholder value for plural key:', key, e);
+ return key;
+ }
+ },
+
+ getUILanguage: () => lang(),
+
+ getBaseMessage: (key: string) => {
+ return messages.en?.[key] || key;
+ },
+
+ getBaseUILanguage: () => BASE_LOCALE as LocalesType,
+
+ changeLanguage: (newLang: LocalesType) => {
+ const resolved = resolveLanguage(newLang);
+ translator = createSolidTranslator(i18n(resolved));
+ setLang(resolved);
+ },
+
+ get translator() {
+ return translator;
+ },
+};
+
+export default intl;
diff --git a/client_v2/src/common/styles/colors/adg.css b/client_v2/src/common/styles/colors/adg.css
new file mode 100644
index 00000000000..c60d9c911ed
--- /dev/null
+++ b/client_v2/src/common/styles/colors/adg.css
@@ -0,0 +1,68 @@
+:root {
+ --product-primary-0: #f7fbf8;
+ --product-primary-10: #d9ecde;
+ --product-primary-20: #bedfc6;
+ --product-primary-30: #a2d0ad;
+ --product-primary-40: #84c193;
+ --product-primary-50: #67b279;
+ --product-primary-60: #5b9f6b;
+ --product-primary-70: #4e8c5d;
+ --product-primary-80: #42794f;
+ --product-primary-90: #366642;
+ --product-primary-100: #22482c;
+ --product-secondary-0: #f5edf5;
+ --product-secondary-10: #dec8e1;
+ --product-secondary-20: #ccaad1;
+ --product-secondary-30: #c096c7;
+ --product-secondary-40: #b586be;
+ --product-secondary-50: #a870b2;
+ --product-secondary-60: #9f61aa;
+ --product-secondary-70: #92549c;
+ --product-secondary-80: #804886;
+ --product-secondary-90: #6d3e72;
+ --product-secondary-100: #502d53;
+ --product-tertiary-0: #defdf2;
+ --product-tertiary-10: #ecedf6;
+ --product-tertiary-20: #d0d4ea;
+ --product-tertiary-30: #b6bbdd;
+ --product-tertiary-40: #9ba2d1;
+ --product-tertiary-50: #7884cb;
+ --product-tertiary-60: #6b76bd;
+ --product-tertiary-70: #5c67ae;
+ --product-tertiary-80: #525b97;
+ --product-tertiary-90: #424979;
+ --product-tertiary-100: #303659;
+ --gray-0: #ffffff;
+ --gray-10: #f6f6f6;
+ --gray-20: #e4e4e4;
+ --gray-30: #d2d2d2;
+ --gray-40: #c0c0c0;
+ --gray-50: #a4a4a4;
+ --gray-60: #7f7f7f;
+ --gray-70: #5b5b5b;
+ --gray-80: #3d3d3d;
+ --gray-90: #1f1f1f;
+ --gray-100: #0a0a0a;
+ --orange-0: #fdf9f2;
+ --orange-10: #f9ecd8;
+ --orange-20: #eecb92;
+ --orange-30: #e5b460;
+ --orange-40: #de9e33;
+ --orange-50: #d58500;
+ --orange-60: #c77901;
+ --orange-70: #b76c01;
+ --orange-80: #a85f01;
+ --orange-90: #985201;
+ --orange-100: #503200;
+ --red-0: #fcece6;
+ --red-10: #f6c2b1;
+ --red-20: #f2a288;
+ --red-30: #ef9071;
+ --red-40: #ec7b55;
+ --red-50: #e9653a;
+ --red-60: #e75727;
+ --red-70: #dc4918;
+ --red-80: #bc3f15;
+ --red-90: #a13612;
+ --red-100: #77280d;
+}
diff --git a/client_v2/src/common/styles/colors/dark.css b/client_v2/src/common/styles/colors/dark.css
new file mode 100644
index 00000000000..d999e30021c
--- /dev/null
+++ b/client_v2/src/common/styles/colors/dark.css
@@ -0,0 +1,135 @@
+[data-theme='dark'] {
+ /* Page background */
+ --default-page-background: var(--gray-90);
+ --hovered-page-background: var(--gray-80);
+ --pressed-page-background: var(--gray-70);
+ --page-background-additional: var(--gray-80);
+ --page-background-additional-pressed: var(--gray-70);
+ /* Cards background */
+ --default-cards-background: var(--gray-80);
+ /* Popup background */
+ --default-popup-background: var(--gray-90);
+ /* Footer background */
+ --default-footer-background: var(--gray-80);
+ /* Item divider */
+ --default-item-divider: var(--gray-70);
+ /* Main text */
+ --default-main-text: var(--gray-20);
+ --disabled-main-text: var(--gray-50);
+ /* Description text */
+ --default-description-text: var(--gray-30);
+ /* Forms */
+ --default-labels: var(--gray-30);
+ /* Input */
+ --default-input-background: var(--gray-80);
+ --default-active-input-stroke: var(--gray-50);
+ --default-inactive-input-stroke: var(--gray-70);
+ --default-placeholder: var(--gray-50);
+ --default-input-on-card-background: var(--gray-70);
+ --disabled-input-on-card-background: var(--gray-70);
+ /* Dropdown */
+ --default-dropdown-menu-background: var(--gray-80);
+ --hovered-dropdown-menu-background: var(--gray-70);
+ --pressed-dropdown-menu-background: var(--gray-60);
+ /* Main button */
+ --default-main-button: var(--product-primary-50);
+ --hovered-main-button: var(--product-primary-60);
+ --pressed-main-button: var(--product-primary-70);
+ --disabled-main-button: var(--product-primary-90);
+ --default-primary-button-text: var(--gray-0);
+ --disabled-primary-button-text: var(--product-primary-20);
+ --default-primary-button-icon: var(--gray-0);
+ /* Secondary button */
+ --default-secondary-button: var(--gray-90);
+ --hovered-secondary-button: var(--gray-80);
+ --pressed-secondary-button: var(--gray-70);
+ --disabled-secondary-button: var(--gray-90);
+ --default-secondary-button-stroke: var(--gray-20);
+ --disabled-secondary-button-stroke: var(--gray-50);
+ /* Secondary card button */
+ --default-secondary-card-button: var(--gray-80);
+ --hovered-secondary-card-button: var(--gray-70);
+ --pressed-secondary-card-button: var(--gray-60);
+ --disabled-secondary-card-button: var(--gray-80);
+ /* Danger button */
+ --default-danger-button: var(--red-50);
+ --hovered-danger-button: var(--red-60);
+ --pressed-danger-button: var(--red-70);
+ --disabled-danger-button: var(--red-30);
+ /* Link */
+ --default-link: var(--product-primary-50);
+ --hovered-link: var(--product-primary-60);
+ --pressed-link: var(--product-primary-70);
+ --visited-link: var(--product-primary-30);
+ /* Attention link */
+ --default-attention-link: var(--orange-50);
+ --hovered-attention-link: var(--orange-60);
+ --pressed-attention-link: var(--orange-70);
+ --disabled-attention-link: var(--orange-30);
+ /* Additional link */
+ --default-additional-link: var(--product-secondary-50);
+ /* Error link */
+ --default-error-link: var(--red-50);
+ /* Icons */
+ --default-product-icon: var(--product-primary-50);
+ --default-black-icons: var(--gray-20);
+ --default-gray-icons: var(--gray-40);
+ --disabled-gray-icons: var(--gray-70);
+ --default-error-icon: var(--red-50);
+ --stroke-icons-white-icons-default: var(--gray-0);
+ --stroke-icons-secondary-icon-disabled: var(--product-secondary-100);
+ --stroke-icons-tertiary-icon-disabled: var(--product-tertiary-100);
+ --default-product-secondary-icon-stroke: var(--product-secondary-50);
+ --default-attention-icon-stroke: var(--orange-50);
+ /* Stat */
+ --default-stats-background: var(--gray-70);
+ --default-red-stat: var(--red-50);
+ --default-tertiary-stat: var(--gray-60);
+ /* Modal */
+ --modal-iframe-overlay: #00000075;
+ --modal-overlay: #00000075;
+ /* Notifications */
+ --default-notifications-attention: var(--orange-100);
+ /* Logo key */
+ --default-logo-key-color: var(--gray-10);
+ /* Loaders */
+ --default-loaders-background: var(--gray-70);
+ --default-loaders-background-dark: var(--gray-70);
+ --default-loaders-primary: var(--product-primary-50);
+ /* Toplines */
+ --default-text-toplines-main: var(--gray-0);
+ --disabled-text-toplines-main: var(--gray-20);
+ --default-fills-toplines-adblocker: var(--gray-100);
+ --default-fills-toplines-vpn: var(--gray-80);
+ /* Breadcrumbs */
+ --default-breadcrumbs: var(--gray-50);
+ /* Promo top line */
+ --fills-toplines-topline-background: var(--gray-100);
+ --text-toplines-topline-title: var(--gray-0);
+ --text-toplines-topline-description: var(--gray-0);
+ --text-toplines-topline-button-text-default: var(--gray-0);
+ --fills-toplines-topline-background-image: none;
+ --fills-toplines-topline-button-default: var(--product-primary-50);
+ --fills-toplines-topline-button-hovered: var(--product-primary-60);
+ --fills-toplines-topline-button-pressed: var(--product-primary-70);
+ --stroke-toplines-button-stroke-default: var(--product-primary-50);
+ --stroke-toplines-button-stroke-hovered: var(--product-primary-60);
+ --stroke-toplines-button-stroke-pressed: var(--product-primary-70);
+ --stroke-toplines-close-icon-default: var(--gray-0);
+ --stroke-toplines-close-icon-hovered: var(--gray-10);
+ --stroke-toplines-close-icon-pressed: var(--gray-20);
+ --fills-backgrounds-recent-activity: var(--product-primary-90);
+ --fills-backgrounds-recent-activity-changed: var(--orange-90);
+ --fills-backgrounds-recent-activity-blocked: var(--red-90);
+ /* Switch */
+ --fills-switch-on-default: var(--product-primary-50);
+ --fills-switch-on-hovered: var(--product-primary-60);
+ --fills-switch-on-disabled: var(--product-primary-90);
+ --fills-switch-off-default: var(--gray-60);
+ --fills-switch-off-hovered: var(--gray-70);
+ --fills-switch-off-disabled: var(--gray-70);
+ --fills-switch-knob: var(--gray-0);
+ --fills-switch-knob-disabled: var(--gray-50);
+ /* Logo */
+ --default-logo-color: var(--gray-0);
+}
diff --git a/client_v2/src/common/styles/colors/light.css b/client_v2/src/common/styles/colors/light.css
new file mode 100644
index 00000000000..f5daae511c6
--- /dev/null
+++ b/client_v2/src/common/styles/colors/light.css
@@ -0,0 +1,135 @@
+:root {
+ /* Page background */
+ --default-page-background: var(--gray-0);
+ --hovered-page-background: var(--gray-10);
+ --pressed-page-background: var(--gray-20);
+ --page-background-additional: var(--gray-10);
+ --page-background-additional-pressed: var(--gray-30);
+ /* Cards background */
+ --default-cards-background: var(--gray-0);
+ /* Popup background */
+ --default-popup-background: var(--gray-0);
+ /* Footer background */
+ --default-footer-background: var(--gray-80);
+ /* Item divider */
+ --default-item-divider: var(--gray-30);
+ /* Main text */
+ --default-main-text: var(--gray-80);
+ --disabled-main-text: var(--gray-50);
+ /* Description text */
+ --default-description-text: var(--gray-70);
+ /* Forms */
+ --default-labels: var(--gray-70);
+ /* Input */
+ --default-input-background: var(--gray-10);
+ --default-active-input-stroke: var(--gray-50);
+ --default-inactive-input-stroke: var(--gray-30);
+ --default-placeholder: var(--gray-50);
+ --default-input-on-card-background: var(--gray-10);
+ --disabled-input-on-card-background: var(--gray-10);
+ /* Dropdown */
+ --default-dropdown-menu-background: var(--gray-0);
+ --hovered-dropdown-menu-background: var(--gray-10);
+ --pressed-dropdown-menu-background: var(--gray-20);
+ /* Main button */
+ --default-main-button: var(--product-primary-50);
+ --hovered-main-button: var(--product-primary-60);
+ --pressed-main-button: var(--product-primary-70);
+ --disabled-main-button: var(--product-primary-30);
+ --default-primary-button-text: var(--gray-0);
+ --disabled-primary-button-text: var(--product-primary-0);
+ --default-primary-button-icon: var(--gray-0);
+ /* Secondary button */
+ --default-secondary-button: var(--gray-0);
+ --hovered-secondary-button: var(--gray-10);
+ --pressed-secondary-button: var(--gray-20);
+ --disabled-secondary-button: var(--gray-0);
+ --default-secondary-button-stroke: var(--gray-80);
+ --disabled-secondary-button-stroke: var(--gray-50);
+ /* Secondary card button */
+ --default-secondary-card-button: var(--gray-0);
+ --hovered-secondary-card-button: var(--gray-10);
+ --pressed-secondary-card-button: var(--gray-20);
+ --disabled-secondary-card-button: var(--gray-0);
+ /* Danger button */
+ --default-danger-button: var(--red-50);
+ --hovered-danger-button: var(--red-60);
+ --pressed-danger-button: var(--red-70);
+ --disabled-danger-button: var(--red-30);
+ /* Link */
+ --default-link: var(--product-primary-50);
+ --hovered-link: var(--product-primary-60);
+ --pressed-link: var(--product-primary-70);
+ --visited-link: var(--product-primary-80);
+ /* Attention link */
+ --default-attention-link: var(--orange-50);
+ --hovered-attention-link: var(--orange-60);
+ --pressed-attention-link: var(--orange-70);
+ --disabled-attention-link: var(--orange-30);
+ /* Additional link */
+ --default-additional-link: var(--product-secondary-50);
+ /* Error link */
+ --default-error-link: var(--red-50);
+ /* Icons */
+ --default-product-icon: var(--product-primary-50);
+ --default-black-icons: var(--gray-80);
+ --default-gray-icons: var(--gray-60);
+ --disabled-gray-icons: var(--gray-30);
+ --default-error-icon: var(--red-50);
+ --stroke-icons-white-icons-default: var(--gray-0);
+ --stroke-icons-tertiary-icon-disabled: var(--product-tertiary-30);
+ --stroke-icons-secondary-icon-disabled: var(--product-secondary-30);
+ --default-product-secondary-icon-stroke: var(--product-secondary-50);
+ --default-attention-icon-stroke: var(--orange-50);
+ /* Stat */
+ --default-stats-background: var(--gray-20);
+ --default-red-stat: var(--red-50);
+ --default-tertiary-stat: var(--gray-60);
+ /* Modal */
+ --modal-iframe-overlay: #ffffff50;
+ --modal-overlay: #00000050;
+ /* Notifications */
+ --default-notifications-attention: var(--orange-10);
+ /* Logo key */
+ --default-logo-key-color: var(--gray-90);
+ /* Loaders */
+ --default-loaders-background: var(--gray-20);
+ --default-loaders-background-dark: var(--gray-70);
+ --default-loaders-primary: var(--product-primary-50);
+ /* Toplines */
+ --default-text-toplines-main: var(--gray-0);
+ --disabled-text-toplines-main: var(--gray-20);
+ --default-fills-toplines-adblocker: var(--gray-100);
+ --default-fills-toplines-vpn: var(--gray-10);
+ /* Breadcrumbs */
+ --default-breadcrumbs: var(--gray-50);
+ /* Promo top line */
+ --fills-toplines-topline-background: var(--gray-100);
+ --text-toplines-topline-title: var(--gray-0);
+ --text-toplines-topline-description: var(--gray-0);
+ --text-toplines-topline-button-text-default: var(--gray-0);
+ --fills-toplines-topline-background-image: none;
+ --fills-toplines-topline-button-default: var(--product-primary-50);
+ --fills-toplines-topline-button-hovered: var(--product-primary-60);
+ --fills-toplines-topline-button-pressed: var(--product-primary-70);
+ --stroke-toplines-button-stroke-default: var(--product-primary-50);
+ --stroke-toplines-button-stroke-hovered: var(--product-primary-60);
+ --stroke-toplines-button-stroke-pressed: var(--product-primary-70);
+ --stroke-toplines-close-icon-default: var(--gray-0);
+ --stroke-toplines-close-icon-hovered: var(--gray-10);
+ --stroke-toplines-close-icon-pressed: var(--gray-20);
+ --fills-backgrounds-recent-activity: var(--product-primary-10);
+ --fills-backgrounds-recent-activity-changed: var(--orange-10);
+ --fills-backgrounds-recent-activity-blocked: var(--red-10);
+ /* Switch */
+ --fills-switch-on-default: var(--product-primary-50);
+ --fills-switch-on-hovered: var(--product-primary-60);
+ --fills-switch-on-disabled: var(--product-primary-30);
+ --fills-switch-off-default: var(--gray-40);
+ --fills-switch-off-hovered: var(--gray-50);
+ --fills-switch-off-disabled: var(--gray-30);
+ --fills-switch-knob: var(--gray-0);
+ --fills-switch-knob-disabled: var(--gray-50);
+ /* Logo */
+ --default-logo-color: var(--gray-100);
+}
diff --git a/client_v2/src/common/styles/vars.css b/client_v2/src/common/styles/vars.css
new file mode 100644
index 00000000000..43248dd0ffe
--- /dev/null
+++ b/client_v2/src/common/styles/vars.css
@@ -0,0 +1,64 @@
+:root {
+ /* CONTAINER */
+
+ --column-width: 76px;
+ --column-gap: 24px;
+ --container-width-12-col: calc((var(--column-width) * 12) + (var(--column-gap) * 11));
+ --container-width-10-col: calc((var(--column-width) * 10) + (var(--column-gap) * 9));
+ --container-width-8-col: calc((var(--column-width) * 8) + (var(--column-gap) * 7));
+ --container-width-6-col: calc((var(--column-width) * 6) + (var(--column-gap) * 5));
+
+ /* FONT WEIGHT */
+
+ --weight-regular: 400;
+ --weight-medium: 500;
+ --weight-semi-bold: 600;
+ --weight-bold: 700;
+
+ /* TITLES LINE HEIGHT */
+
+ --lh-h0-normal: 76px;
+ --lh-h1-normal: 56px;
+ --lh-h2-normal: 48px;
+ --lh-h3-normal: 40px;
+ --lh-h4-normal: 28px;
+ --lh-h5-normal: 24px;
+ --lh-h6-normal: 20px;
+
+ /* TEXTS LINE HEIGHT */
+
+ --lh-t1-normal: 28px;
+ --lh-t2-normal: 24px;
+ --lh-t3-normal: 20px;
+ --lh-t4-normal: 16px;
+
+ --lh-t1-condenced: 24px;
+ --lh-t2-condenced: 20px;
+ --lh-t3-condenced: 16px;
+ --lh-t4-condenced: 16px;
+
+ /* FONT SIZE */
+
+ --fs-title-h0: 64px;
+ --fs-title-h1: 48px;
+ --fs-title-h2: 40px;
+ --fs-title-h3: 32px;
+ --fs-title-h4: 24px;
+ --fs-title-h5: 20px;
+ --fs-title-h6: 18px;
+
+ --fs-text-t1: 18px;
+ --fs-text-t2: 16px;
+ --fs-text-t3: 14px;
+ --fs-text-t4: 12px;
+
+ /* TRANSITION */
+
+ --t3: 0.3s ease;
+ --t2: 0.2s ease;
+
+ /* BOX-SHADOW */
+
+ --main-box-shadow: rgba(166, 166, 166, 0.2);
+ --dropdown-box-shadow: rgba(23, 39, 75, 0.12);
+}
diff --git a/client_v2/src/common/ui/Accordion/Accordion.module.pcss b/client_v2/src/common/ui/Accordion/Accordion.module.pcss
new file mode 100644
index 00000000000..4c48be0ad6b
--- /dev/null
+++ b/client_v2/src/common/ui/Accordion/Accordion.module.pcss
@@ -0,0 +1,34 @@
+.accordion {
+ overflow: hidden;
+}
+
+.header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ width: 100%;
+ padding: 8px 16px;
+ border: none;
+ cursor: pointer;
+ text-align: left;
+ background-color: transparent;
+ outline: none;
+
+ &.compact {
+ padding-left: 0;
+ padding-right: 0;
+ }
+}
+
+.title {
+ color: var(--default-main-text);
+}
+
+.arrow {
+ transition: transform var(--t2);
+ color: var(--default-gray-icons);
+}
+
+.arrowOpen {
+ transform: rotate(180deg);
+}
diff --git a/client_v2/src/common/ui/Accordion/Accordion.tsx b/client_v2/src/common/ui/Accordion/Accordion.tsx
new file mode 100644
index 00000000000..df8c860391a
--- /dev/null
+++ b/client_v2/src/common/ui/Accordion/Accordion.tsx
@@ -0,0 +1,41 @@
+import { type JSX, createSignal, Show } from 'solid-js';
+import cn from 'clsx';
+
+import { Icon } from 'panel/common/ui/Icon';
+import theme from 'panel/lib/theme';
+import s from './Accordion.module.pcss';
+
+type Props = {
+ title: JSX.Element;
+ children: JSX.Element;
+ defaultOpen?: boolean;
+ class?: string;
+ compact?: boolean;
+};
+
+export const Accordion = (props: Props) => {
+ const [isOpen, setIsOpen] = createSignal(props.defaultOpen ?? false);
+
+ const toggleOpen = () => {
+ setIsOpen(!isOpen());
+ };
+
+ return (
+
+
+
+
+ {props.children}
+
+
+ );
+};
diff --git a/client_v2/src/common/ui/Accordion/index.ts b/client_v2/src/common/ui/Accordion/index.ts
new file mode 100644
index 00000000000..feee0c4f098
--- /dev/null
+++ b/client_v2/src/common/ui/Accordion/index.ts
@@ -0,0 +1 @@
+export { Accordion } from './Accordion';
diff --git a/client_v2/src/common/ui/Banner/Banner.module.pcss b/client_v2/src/common/ui/Banner/Banner.module.pcss
new file mode 100644
index 00000000000..e687a7c9eac
--- /dev/null
+++ b/client_v2/src/common/ui/Banner/Banner.module.pcss
@@ -0,0 +1,71 @@
+.banner {
+ position: relative;
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 12px;
+ padding: 16px 42px 16px 16px;
+ border-radius: 0;
+ width: 100%;
+
+ @media (min-width: 768px) {
+ padding: 8px 16px;
+ align-items: center;
+ flex-wrap: nowrap;
+ justify-content: center;
+ }
+}
+
+.action {
+ width: auto;
+ min-width: 110px;
+ max-width: max-content;
+
+ @media (min-width: 768px) {
+ max-width: none;
+ }
+}
+
+.close {
+ position: absolute;
+ top: 16px;
+ right: 16px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+ padding: 0;
+ border: none;
+ background: transparent;
+ color: var(--default-gray-icons);
+ cursor: pointer;
+ border-radius: 4px;
+ flex-shrink: 0;
+
+ @media (min-width: 768px) {
+ position: static;
+ }
+
+ &:focus-visible {
+ outline: 2px solid var(--product-primary-90);
+ outline-offset: 2px;
+ }
+}
+
+.closeIcon {
+ width: 24px;
+ height: 24px;
+}
+
+.info {
+ background-color: var(--page-background-additional);
+}
+
+.warning {
+ background-color: var(--fills-backgrounds-recent-activity-changed);
+}
+
+.critical {
+ background-color: var(--fills-backgrounds-recent-activity-blocked);
+}
diff --git a/client_v2/src/common/ui/Banner/Banner.tsx b/client_v2/src/common/ui/Banner/Banner.tsx
new file mode 100644
index 00000000000..0eefc29ef10
--- /dev/null
+++ b/client_v2/src/common/ui/Banner/Banner.tsx
@@ -0,0 +1,40 @@
+import { type JSX } from 'solid-js';
+import cn from 'clsx';
+
+import { Icon } from 'panel/common/ui/Icon';
+
+import s from './Banner.module.pcss';
+import theme from 'panel/lib/theme';
+
+export type BannerVariant = 'info' | 'warning' | 'critical';
+
+type Props = {
+ variant: BannerVariant;
+ message: JSX.Element;
+ action?: JSX.Element;
+ onClose?: () => void;
+ 'data-testid'?: string;
+};
+
+export const Banner = (props: Props) => (
+
+
{props.message}
+ {props.action &&
{props.action}
}
+ {props.onClose && (
+
+
+
+ )}
+
+);
diff --git a/client_v2/src/common/ui/Banner/index.ts b/client_v2/src/common/ui/Banner/index.ts
new file mode 100644
index 00000000000..be23ad968f2
--- /dev/null
+++ b/client_v2/src/common/ui/Banner/index.ts
@@ -0,0 +1,2 @@
+export { Banner } from './Banner';
+export type { BannerVariant } from './Banner';
diff --git a/client_v2/src/common/ui/Banners/Banners.module.pcss b/client_v2/src/common/ui/Banners/Banners.module.pcss
new file mode 100644
index 00000000000..98a88539618
--- /dev/null
+++ b/client_v2/src/common/ui/Banners/Banners.module.pcss
@@ -0,0 +1,9 @@
+.actionButton {
+ width: auto;
+ min-width: 110px;
+}
+
+.actionLink {
+ font-size: 14px;
+ white-space: nowrap;
+}
diff --git a/client_v2/src/common/ui/Banners/Banners.tsx b/client_v2/src/common/ui/Banners/Banners.tsx
new file mode 100644
index 00000000000..d44af497f5c
--- /dev/null
+++ b/client_v2/src/common/ui/Banners/Banners.tsx
@@ -0,0 +1,196 @@
+import { Show, createMemo, createSignal, type JSX } from 'solid-js';
+import { useNavigate, useSearchParams } from '@solidjs/router';
+
+import { Banner } from 'panel/common/ui/Banner';
+import { Button } from 'panel/common/ui/Button';
+import { MANUAL_UPDATE_LINK } from 'panel/helpers/constants';
+import { Paths } from 'panel/components/Routes/Paths';
+import intl from 'panel/common/intl';
+import theme from 'panel/lib/theme';
+
+import { dashboardState, getUpdate } from 'panel/stores/dashboard';
+import { encryptionState } from 'panel/stores/encryption';
+import {
+ type BannerSpec,
+ type BannerType,
+ bannerSpecsEqual,
+ BANNER_TEST_VALUES,
+} from 'panel/helpers/banners';
+
+import s from './Banners.module.pcss';
+
+const TLS_EXPIRY_WARNING_MS = 30 * 24 * 60 * 60 * 1000;
+
+type Props = {
+ forceBanner?: BannerSpec;
+};
+
+export const Banners = (props: Props) => {
+ const navigate = useNavigate();
+ const [searchParams] = useSearchParams<{ forceBanner?: string }>();
+ const [dismissed, setDismissed] = createSignal(null);
+
+ const forceFromQuery = createMemo(() => {
+ const key = searchParams.forceBanner;
+ if (key && key in BANNER_TEST_VALUES) {
+ return BANNER_TEST_VALUES[key as BannerType];
+ }
+ return null;
+ });
+
+ const computeActiveBanner = (): BannerSpec | null => {
+ if (encryptionState.enabled && encryptionState.valid_cert && encryptionState.not_after) {
+ const expiry = new Date(encryptionState.not_after).getTime();
+ if (!Number.isNaN(expiry)) {
+ if (Date.now() > expiry) {
+ return { type: 'tls_expired' };
+ }
+ if (Date.now() > expiry - TLS_EXPIRY_WARNING_MS) {
+ return { type: 'tls_expiring' };
+ }
+ }
+ }
+
+ if (dashboardState.isUpdateAvailable) {
+ const spec = {
+ version: dashboardState.newVersion,
+ announcementUrl: dashboardState.announcementUrl,
+ };
+ return dashboardState.canAutoUpdate
+ ? { type: 'update_auto', ...spec }
+ : { type: 'update_manual', ...spec };
+ }
+
+ return null;
+ };
+
+ const banner = createMemo(() => {
+ const active = props.forceBanner ?? forceFromQuery() ?? computeActiveBanner();
+ if (!active) return null;
+ const dismissedValue = dismissed();
+ if (dismissedValue && bannerSpecsEqual(dismissedValue, active)) {
+ return null;
+ }
+ return active;
+ });
+
+ const announcementLinkHandler = (announcementUrl: string) => (text: string) => (
+
+ {text}
+
+ );
+
+ return (
+
+ {(spec) => {
+ const current = spec();
+
+ const renderBanner = (): JSX.Element => {
+ switch (current.type) {
+ case 'tls_expired':
+ return (
+ navigate(Paths.Encryption)}
+ class={s.actionButton}
+ >
+ {intl.getMessage('update_button')}
+
+ }
+ onClose={() => setDismissed(current)}
+ data-testid="banner-tls-expired"
+ />
+ );
+
+ case 'tls_expiring':
+ return (
+ navigate(Paths.Encryption)}
+ class={s.actionButton}
+ >
+ {intl.getMessage('update_button')}
+
+ }
+ onClose={() => setDismissed(current)}
+ data-testid="banner-tls-expiring"
+ />
+ );
+
+ case 'update_auto':
+ return (
+ getUpdate()}
+ class={s.actionButton}
+ >
+ {intl.getMessage('update_button')}
+
+ }
+ onClose={() => setDismissed(current)}
+ data-testid="banner-update-auto"
+ />
+ );
+
+ case 'update_manual':
+ return (
+
+
+ {intl.getMessage('update_how_to')}
+
+
+ }
+ onClose={() => setDismissed(current)}
+ data-testid="banner-update-manual"
+ />
+ );
+
+ default:
+ return null;
+ }
+ };
+
+ return {renderBanner()}
;
+ }}
+
+ );
+};
diff --git a/client_v2/src/common/ui/Banners/index.ts b/client_v2/src/common/ui/Banners/index.ts
new file mode 100644
index 00000000000..0e4e48f6559
--- /dev/null
+++ b/client_v2/src/common/ui/Banners/index.ts
@@ -0,0 +1,3 @@
+export { Banners } from './Banners';
+export { BANNER_TEST_VALUES, getForceBannerFromQuery } from 'panel/helpers/banners';
+export type { BannerSpec, BannerType } from 'panel/helpers/banners';
diff --git a/client_v2/src/common/ui/Breadcrumbs/Breadcrumbs.tsx b/client_v2/src/common/ui/Breadcrumbs/Breadcrumbs.tsx
new file mode 100644
index 00000000000..4f6fbe6be0f
--- /dev/null
+++ b/client_v2/src/common/ui/Breadcrumbs/Breadcrumbs.tsx
@@ -0,0 +1,45 @@
+import { For } from 'solid-js';
+import cn from 'clsx';
+import { Icon } from 'panel/common/ui/Icon';
+import { Link } from 'panel/common/ui/Link';
+import { type RoutePathKey } from 'panel/components/Routes/Paths';
+import theme from 'panel/lib/theme';
+
+import s from './styles.module.pcss';
+
+export type BreadcrumbLink = {
+ path: RoutePathKey;
+ title: string;
+ props?: Partial>;
+};
+
+type Props = {
+ parentLinks: BreadcrumbLink[];
+ currentTitle: string;
+};
+
+export const Breadcrumbs = (props: Props) => (
+
+
+ {({ path, title, props: linkProps }) => (
+
+
+ {title}
+
+
+
+ )}
+
+
+ {props.currentTitle}
+
+
+);
diff --git a/client_v2/src/common/ui/Breadcrumbs/index.ts b/client_v2/src/common/ui/Breadcrumbs/index.ts
new file mode 100644
index 00000000000..9ab64eefbb8
--- /dev/null
+++ b/client_v2/src/common/ui/Breadcrumbs/index.ts
@@ -0,0 +1,2 @@
+export { Breadcrumbs } from './Breadcrumbs';
+export type { BreadcrumbLink } from './Breadcrumbs';
diff --git a/client_v2/src/common/ui/Breadcrumbs/styles.module.pcss b/client_v2/src/common/ui/Breadcrumbs/styles.module.pcss
new file mode 100644
index 00000000000..2bf5d246cbe
--- /dev/null
+++ b/client_v2/src/common/ui/Breadcrumbs/styles.module.pcss
@@ -0,0 +1,50 @@
+.wrapper {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ font-size: 14px;
+ font-weight: var(--weight-semi-bold);
+ line-height: 24px;
+ gap: 8px;
+ color: var(--default-description-text);
+ padding: 16px 0;
+ overflow: hidden;
+
+ @media (min-width: 768px) {
+ padding: 32px 0 24px;
+ font-size: 16px;
+ gap: 16px;
+ }
+
+ @media (min-width: 1200px) {
+ max-width: calc(var(--container) + 24px);
+ }
+
+ @media (min-width: 1400px) {
+ margin: 0 auto;
+ min-width: 0;
+ width: 100%;
+ }
+}
+
+.link {
+ display: flex;
+ align-items: center;
+ overflow: hidden;
+ min-width: 0;
+}
+
+.current {
+ min-width: 0;
+}
+
+.arrow {
+ margin-left: 8px;
+ transform: rotate(-90deg);
+ cursor: pointer;
+ color: var(--default-gray-icons);
+
+ @media (min-width: 768px) {
+ margin-left: 16px;
+ }
+}
diff --git a/client_v2/src/common/ui/Button/Button.module.pcss b/client_v2/src/common/ui/Button/Button.module.pcss
new file mode 100644
index 00000000000..9b59ea628a5
--- /dev/null
+++ b/client_v2/src/common/ui/Button/Button.module.pcss
@@ -0,0 +1,189 @@
+.button {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 10px;
+ width: 100%;
+ min-width: 200px;
+ max-width: 400px;
+ height: 48px;
+ padding: 16px 24px;
+ font-size: 16px;
+ font-weight: var(--weight-medium);
+ color: var(--default-main-text);
+ border: 1px solid transparent;
+ border-radius: 8px;
+ transition:
+ var(--t2) opacity,
+ var(--t2) color,
+ var(--t2) border,
+ var(--t2) background-color;
+ background: none;
+ outline: none;
+ user-select: none;
+ text-decoration: none;
+ white-space: nowrap;
+ cursor: pointer;
+ overflow: hidden;
+
+ &:disabled {
+ cursor: default;
+ }
+
+ &:focus {
+ outline: none;
+ }
+}
+
+.height_xs {
+ height: 24px;
+ padding: 0 12px;
+}
+
+.height_s {
+ height: 48px;
+}
+
+.height_m {
+ height: 56px;
+}
+
+.height_l {
+ height: 64px;
+}
+
+.primary {
+ color: var(--gray-0);
+ background: var(--default-main-button);
+ border-color: var(--default-main-button);
+
+ &:not(:disabled):hover,
+ &:not(:disabled):focus {
+ background: var(--hovered-main-button);
+ border-color: var(--hovered-main-button);
+ }
+
+ &:not(:disabled):active {
+ background: var(--pressed-main-button);
+ border-color: var(--pressed-main-button);
+ }
+
+ &:disabled {
+ background: var(--disabled-main-button);
+ border-color: var(--disabled-main-button);
+ }
+}
+
+.secondary {
+ color: var(--default-secondary-button-stroke);
+ background: var(--default-secondary-button);
+ border-color: var(--default-secondary-button-stroke);
+
+ &:hover,
+ &:focus {
+ color: var(--default-secondary-button-stroke);
+ background: var(--hovered-secondary-button);
+ border-color: var(--default-secondary-button-stroke);
+ }
+
+ &:active {
+ color: var(--default-secondary-button-stroke);
+ background: var(--pressed-secondary-button);
+ border-color: var(--default-secondary-button-stroke);
+ }
+
+ &:disabled {
+ color: var(--default-secondary-button-stroke);
+ border-color: var(--default-secondary-button-stroke);
+ background-color: var(--disabled-secondary-button);
+ opacity: 0.5;
+ }
+}
+
+.ghost {
+ color: var(--default-main-text);
+ background: none;
+ border-color: transparent;
+
+ &:hover,
+ &:focus {
+ background: var(--hovered-secondary-button);
+ }
+
+ &:active {
+ background: var(--pressed-secondary-button);
+ }
+
+ &:disabled {
+ background: var(--disabled-secondary-button);
+ }
+}
+
+.secondary-danger {
+ color: var(--default-danger-button);
+ border-color: var(--default-danger-button);
+
+ &:hover,
+ &:focus {
+ color: var(--hovered-danger-button);
+ border-color: var(--hovered-danger-button);
+ }
+
+ &:active {
+ color: var(--pressed-danger-button);
+ border-color: var(--pressed-danger-button);
+ }
+
+ &:disabled {
+ color: var(--disabled-danger-button);
+ border-color: var(--disabled-danger-button);
+ }
+}
+
+.danger {
+ color: var(--gray-0);
+ background: var(--default-danger-button);
+ border-color: var(--default-danger-button);
+
+ &:hover,
+ &:focus {
+ background: var(--hovered-danger-button);
+ border-color: var(--hovered-danger-button);
+ }
+
+ &:active {
+ background: var(--pressed-danger-button);
+ border-color: var(--pressed-danger-button);
+ }
+
+ &:disabled {
+ background: var(--disabled-danger-button);
+ border-color: var(--disabled-danger-button);
+ }
+}
+
+.icon {
+ & + .text {
+ position: relative;
+ left: -14px;
+ max-width: 200px;
+ margin: 0 auto;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+}
+
+.leftAddon,
+.rightAddon {
+ width: 24px;
+ height: 24px;
+}
+
+.leftAddon {
+ margin-right: auto;
+}
+
+.rightAddon {
+ margin-left: auto;
+}
diff --git a/client_v2/src/common/ui/Button/Button.tsx b/client_v2/src/common/ui/Button/Button.tsx
new file mode 100644
index 00000000000..30ad27e0c69
--- /dev/null
+++ b/client_v2/src/common/ui/Button/Button.tsx
@@ -0,0 +1,55 @@
+import { type JSX, splitProps, Show } from 'solid-js';
+import cn from 'clsx';
+
+import s from './Button.module.pcss';
+
+export type ButtonProps = JSX.ButtonHTMLAttributes & {
+ size?: 'very-small' | 'small' | 'medium' | 'big';
+ variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'secondary-danger';
+ leftAddon?: JSX.Element;
+ rightAddon?: JSX.Element;
+ compact?: boolean;
+ className?: string;
+};
+
+export const Button = (props: ButtonProps) => {
+ const [local, rest] = splitProps(props, [
+ 'variant',
+ 'size',
+ 'class',
+ 'className',
+ 'children',
+ 'disabled',
+ 'leftAddon',
+ 'rightAddon',
+ 'compact',
+ ]);
+
+ return (
+
+
+ {local.leftAddon}
+
+ {local.children}
+
+ {local.rightAddon}
+
+
+ );
+};
diff --git a/client_v2/src/common/ui/Button/index.ts b/client_v2/src/common/ui/Button/index.ts
new file mode 100644
index 00000000000..9cad84da169
--- /dev/null
+++ b/client_v2/src/common/ui/Button/index.ts
@@ -0,0 +1,2 @@
+export type { ButtonProps } from './Button';
+export { Button } from './Button';
diff --git a/client_v2/src/common/ui/ConfigDialog/ConfigDialog.module.pcss b/client_v2/src/common/ui/ConfigDialog/ConfigDialog.module.pcss
new file mode 100644
index 00000000000..b17737767b3
--- /dev/null
+++ b/client_v2/src/common/ui/ConfigDialog/ConfigDialog.module.pcss
@@ -0,0 +1,39 @@
+.configDialog {
+ :global(.rc-dialog-wrap) {
+ padding: 0;
+ }
+}
+
+.body {
+ overflow-y: auto;
+ border: none;
+ padding: 0 16px;
+}
+
+.footer {
+ display: flex;
+ gap: 8px;
+ padding: 16px;
+}
+
+.saveButton {
+ width: 100%;
+ max-width: none;
+ min-width: 200px;
+
+ @media (min-width: 768px) {
+ width: auto;
+ }
+}
+
+.description {
+ padding: 8px 16px;
+
+ p {
+ margin: 0;
+ }
+
+ p + p {
+ margin-top: 12px;
+ }
+}
diff --git a/client_v2/src/common/ui/ConfigDialog/ConfigDialog.tsx b/client_v2/src/common/ui/ConfigDialog/ConfigDialog.tsx
new file mode 100644
index 00000000000..2ebfa81ca71
--- /dev/null
+++ b/client_v2/src/common/ui/ConfigDialog/ConfigDialog.tsx
@@ -0,0 +1,56 @@
+import { type JSX, Show } from 'solid-js';
+import cn from 'clsx';
+
+import { Dialog } from 'panel/common/ui/Dialog';
+import { Button } from 'panel/common/ui/Button';
+import intl from 'panel/common/intl';
+
+import s from './ConfigDialog.module.pcss';
+
+type Props = {
+ open: boolean;
+ title: string;
+ onClose: () => void;
+ onSubmit: () => void;
+ processing?: boolean;
+ submitDisabled?: boolean;
+ class?: string;
+ children?: JSX.Element;
+ footer?: JSX.Element;
+ description?: JSX.Element;
+ hideSubmit?: boolean;
+ buttonText?: string;
+};
+
+export const ConfigDialog = (props: Props) => {
+ const isDisabled = () => !!props.processing || !!props.submitDisabled;
+
+ return (
+
+ {props.description && {props.description}
}
+
+
+ {props.children}
+
+
+
+ );
+};
diff --git a/client_v2/src/common/ui/ConfigDialog/index.ts b/client_v2/src/common/ui/ConfigDialog/index.ts
new file mode 100644
index 00000000000..3113bc7622a
--- /dev/null
+++ b/client_v2/src/common/ui/ConfigDialog/index.ts
@@ -0,0 +1 @@
+export { ConfigDialog } from './ConfigDialog';
diff --git a/client_v2/src/common/ui/ConfirmDialog/ConfirmDialog.module.pcss b/client_v2/src/common/ui/ConfirmDialog/ConfirmDialog.module.pcss
new file mode 100644
index 00000000000..1758e4ee8b1
--- /dev/null
+++ b/client_v2/src/common/ui/ConfirmDialog/ConfirmDialog.module.pcss
@@ -0,0 +1,9 @@
+.сonfirmDialogClass {
+ max-width: 370px;
+ width: 100%;
+
+ @media (min-width: 768px) {
+ max-width: 100%;
+ width: 720px;
+ }
+}
diff --git a/client_v2/src/common/ui/ConfirmDialog/ConfirmDialog.tsx b/client_v2/src/common/ui/ConfirmDialog/ConfirmDialog.tsx
new file mode 100644
index 00000000000..5cbfc4ed569
--- /dev/null
+++ b/client_v2/src/common/ui/ConfirmDialog/ConfirmDialog.tsx
@@ -0,0 +1,71 @@
+import { type JSX, Show } from 'solid-js';
+import cn from 'clsx';
+import { Button, type ButtonProps } from 'panel/common/ui/Button';
+import { Dialog } from 'panel/common/ui/Dialog';
+import theme from 'panel/lib/theme';
+
+import s from './ConfirmDialog.module.pcss';
+
+type Props = {
+ onClose: () => void;
+ title?: JSX.Element;
+ text?: JSX.Element;
+ buttonText: string;
+ cancelText: string;
+ onConfirm?: () => void;
+ submitDisabled?: boolean;
+ buttonVariant?: ButtonProps['variant'];
+ submitId?: string;
+ cancelId?: string;
+ customFooter?: JSX.Element;
+ wrapClass?: string;
+ submitTestId?: string;
+ cancelTestId?: string;
+};
+
+export const ConfirmDialog = (props: Props) => (
+
+
+ {props.text}
+
+
+
+
+);
diff --git a/client_v2/src/common/ui/ConfirmDialog/index.ts b/client_v2/src/common/ui/ConfirmDialog/index.ts
new file mode 100644
index 00000000000..c177c8be2b4
--- /dev/null
+++ b/client_v2/src/common/ui/ConfirmDialog/index.ts
@@ -0,0 +1 @@
+export { ConfirmDialog } from './ConfirmDialog';
diff --git a/client_v2/src/common/ui/CopiedText/CopiedText.module.pcss b/client_v2/src/common/ui/CopiedText/CopiedText.module.pcss
new file mode 100644
index 00000000000..9df68c7205d
--- /dev/null
+++ b/client_v2/src/common/ui/CopiedText/CopiedText.module.pcss
@@ -0,0 +1,61 @@
+.container {
+ position: relative;
+ display: inline-flex;
+ cursor: pointer;
+ align-items: center;
+ gap: 8px;
+ padding: 4px 8px;
+ background: var(--default-input-background);
+ border: 1px solid var(--default-inactive-input-stroke);
+ border-radius: 4px;
+ transition: var(--t2) border-color, var(--t2) background-color;
+ width: fit-content;
+}
+
+.container {
+ &:hover {
+ border-color: var(--default-active-input-stroke);
+ background: var(--hovered-page-background);
+ }
+
+ &:hover .icon {
+ color: var(--default-main-text);
+ transform: scale(1.1);
+ }
+
+ &:focus {
+ border-color: var(--default-active-input-stroke);
+ }
+
+ &:focus .icon {
+ color: var(--default-main-text);
+ }
+
+ &:active .icon {
+ transform: scale(0.95);
+ }
+}
+
+.text {
+ font-size: 14px;
+ color: var(--default-main-text);
+ cursor: pointer;
+ user-select: none;
+ word-break: break-all;
+ line-height: 1.4;
+}
+
+.icon {
+ width: 20px;
+ height: 20px;
+ color: var(--default-gray-icons);
+ cursor: pointer;
+ transition: var(--t2) color, var(--t2) transform;
+ flex-shrink: 0;
+ pointer-events: none;
+}
+
+.copied {
+ color: var(--product-primary-50) !important;
+ transform: scale(1.1) !important;
+}
diff --git a/client_v2/src/common/ui/CopiedText/CopiedText.tsx b/client_v2/src/common/ui/CopiedText/CopiedText.tsx
new file mode 100644
index 00000000000..dceed99c166
--- /dev/null
+++ b/client_v2/src/common/ui/CopiedText/CopiedText.tsx
@@ -0,0 +1,69 @@
+import { createSignal, createEffect, onCleanup } from 'solid-js';
+import cn from 'clsx';
+import copy from 'copy-to-clipboard';
+import intl from 'panel/common/intl';
+import { addSuccessToast } from 'panel/stores/toasts';
+import { Icon } from '../Icon';
+
+import s from './CopiedText.module.pcss';
+
+export type CopiedTextProps = {
+ text: string;
+ class?: string;
+ onCopy?: (text: string) => void;
+};
+
+export const CopiedText = (props: CopiedTextProps) => {
+ const [isCopied, setIsCopied] = createSignal(false);
+ let timer: ReturnType | undefined;
+
+ const handleCopy = async () => {
+ try {
+ const ok = copy(props.text);
+
+ if (!ok) {
+ throw new Error('Failed to copy text');
+ }
+
+ setIsCopied(true);
+ addSuccessToast(intl.getMessage('copied'));
+ props.onCopy?.(props.text);
+ } catch (error) {
+ console.error('Failed to copy text:', error);
+ }
+ };
+
+ const resetTimer = () => {
+ if (timer) {
+ clearTimeout(timer);
+ }
+ if (isCopied()) {
+ timer = setTimeout(() => {
+ setIsCopied(false);
+ }, 2000);
+ }
+ };
+
+ // Reset timer when isCopied changes
+ createEffect(() => {
+ if (isCopied()) resetTimer();
+ });
+
+ onCleanup(() => {
+ if (timer) {
+ clearTimeout(timer);
+ }
+ });
+
+ return (
+
+ {props.text}
+
+
+ );
+};
diff --git a/client_v2/src/common/ui/CopiedText/index.ts b/client_v2/src/common/ui/CopiedText/index.ts
new file mode 100644
index 00000000000..cb8d2e3e445
--- /dev/null
+++ b/client_v2/src/common/ui/CopiedText/index.ts
@@ -0,0 +1 @@
+export { CopiedText, type CopiedTextProps } from './CopiedText';
diff --git a/client_v2/src/common/ui/Dialog/Dialog.pcss b/client_v2/src/common/ui/Dialog/Dialog.pcss
new file mode 100644
index 00000000000..0d9ec690a14
--- /dev/null
+++ b/client_v2/src/common/ui/Dialog/Dialog.pcss
@@ -0,0 +1,101 @@
+.dialog-overlay {
+ position: fixed;
+ top: 0;
+ right: 0;
+ left: 0;
+ bottom: 0;
+ background-color: rgba(19, 19, 19, 0.8);
+ height: 100%;
+ z-index: 1100;
+}
+
+.rc-dialog-wrap {
+ position: fixed;
+ overflow: auto;
+ top: 65px;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ padding: 0 8px;
+ z-index: 1100;
+ -webkit-overflow-scrolling: touch;
+ outline: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ @media (min-width: 1024px) {
+ top: 0;
+ }
+}
+
+.rc-dialog-update {
+ top: 16px;
+
+ @media (min-width: 1024px) {
+ top: 0;
+ }
+}
+
+.dialog-content {
+ position: relative;
+ width: 100%;
+ margin: 0 auto;
+ max-width: 720px;
+ max-height: calc(100vh - 32px);
+ background-color: var(--default-page-background);
+ border: none;
+ border-radius: 16px;
+ background-clip: border-box;
+ padding: 16px 0;
+ color: var(--default-main-text);
+ text-align: left;
+ overflow: auto;
+
+ @media (min-width: 768px) {
+ border-radius: 16px;
+ padding: 24px 16px;
+ }
+}
+
+.dialog-content-no-overflow {
+ overflow: visible;
+}
+
+.dialog-title {
+ margin: 0;
+ padding: 0;
+ font-weight: var(--weight-bold);
+ font-size: 24px;
+ line-height: 28px;
+ color: var(--default-main-text);
+}
+
+.dialog-header {
+ margin: 0;
+ padding: 8px 48px 8px 16px;
+ flex-shrink: 0;
+}
+
+.dialog-close-button {
+ cursor: pointer;
+ border: 0;
+ background: transparent;
+ position: absolute;
+ right: 16px;
+ top: 16px;
+ padding: 0;
+ transition: 0.3s opacity;
+}
+
+.dialog-close-button:hover {
+ opacity: 1;
+}
+
+.dialog-body {
+ padding: 8px 0;
+
+ @media (min-width: 1024px) {
+ padding: 0;
+ }
+}
diff --git a/client_v2/src/common/ui/Dialog/Dialog.tsx b/client_v2/src/common/ui/Dialog/Dialog.tsx
new file mode 100644
index 00000000000..0ded8715b26
--- /dev/null
+++ b/client_v2/src/common/ui/Dialog/Dialog.tsx
@@ -0,0 +1,52 @@
+import { type JSX, Show } from 'solid-js';
+import { Dialog as ArkDialog } from '@ark-ui/solid';
+import cn from 'clsx';
+
+import { Icon } from 'panel/common/ui/Icon';
+import theme from 'panel/lib/theme';
+
+import './Dialog.pcss';
+
+type Props = {
+ visible: boolean;
+ mask?: boolean;
+ class?: string;
+ onClose: () => void;
+ title?: JSX.Element;
+ wrapClass?: string;
+ children?: JSX.Element;
+ noOverflowContent?: boolean;
+};
+
+export const Dialog = (props: Props) => {
+ return (
+ {
+ if (!details.open) props.onClose();
+ }}
+ closeOnInteractOutside={true}
+ >
+
+
+
+
+
+
+
+
+ {props.children}
+
+
+
+ );
+};
diff --git a/client_v2/src/common/ui/Dialog/index.ts b/client_v2/src/common/ui/Dialog/index.ts
new file mode 100644
index 00000000000..991d7e30fad
--- /dev/null
+++ b/client_v2/src/common/ui/Dialog/index.ts
@@ -0,0 +1 @@
+export { Dialog } from './Dialog';
diff --git a/client_v2/src/common/ui/Dropdown/Dropdown.module.pcss b/client_v2/src/common/ui/Dropdown/Dropdown.module.pcss
new file mode 100644
index 00000000000..9c1427f84b5
--- /dev/null
+++ b/client_v2/src/common/ui/Dropdown/Dropdown.module.pcss
@@ -0,0 +1,55 @@
+.overlay {
+ background: var(--default-dropdown-menu-background);
+ padding: 0;
+ cursor: pointer;
+ max-width: 327px;
+ z-index: 1504;
+ overflow: hidden;
+}
+
+.selectOverlay {
+ width: 100%;
+ max-width: calc(100% - 48px);
+ -webkit-tap-highlight-color: transparent;
+
+ @media (min-width: 768px) {
+ max-width: 528px;
+ }
+
+ @media (min-width: 1024px) {
+ max-width: 344px;
+ }
+}
+
+.open {
+ display: flex;
+ width: 100%;
+ cursor: default;
+}
+
+.disabled {
+ cursor: default;
+}
+
+.linkArrow {
+ margin-left: -8px;
+}
+
+.widthAuto {
+ min-width: auto !important;
+}
+
+.arrow {
+ transition: transform var(--t2);
+ color: var(--default-gray-icons);
+
+ &.active {
+ transform: rotate(180deg);
+ }
+}
+
+.wrapper {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
diff --git a/client_v2/src/common/ui/Dropdown/Dropdown.pcss b/client_v2/src/common/ui/Dropdown/Dropdown.pcss
new file mode 100644
index 00000000000..22b679446cb
--- /dev/null
+++ b/client_v2/src/common/ui/Dropdown/Dropdown.pcss
@@ -0,0 +1,21 @@
+/*
+ * Scope every selector to [data-scope="popover"]. Dropdown.pcss is a global
+ * (side-effect) stylesheet, so a bare [data-part="..."] would also match the
+ * already-migrated Select ([data-scope="select"]) and any future Ark UI widget.
+ */
+[data-scope='popover'][data-part='anchor'] {
+ min-width: 0;
+ overflow: hidden;
+}
+
+[data-scope='popover'][data-part='positioner'] {
+ z-index: 1504;
+}
+
+[data-scope='popover'][data-part='content'] {
+ border-radius: 8px;
+ box-shadow:
+ 0 8px 18px -6px var(--dropdown-box-shadow),
+ 0 12px 42px -4px var(--dropdown-box-shadow);
+ outline: none;
+}
diff --git a/client_v2/src/common/ui/Dropdown/Dropdown.tsx b/client_v2/src/common/ui/Dropdown/Dropdown.tsx
new file mode 100644
index 00000000000..19a283716ab
--- /dev/null
+++ b/client_v2/src/common/ui/Dropdown/Dropdown.tsx
@@ -0,0 +1,195 @@
+import { type JSX, createSignal, createEffect, onCleanup, Show, untrack } from 'solid-js';
+import cn from 'clsx';
+import { Icon } from 'panel/common/ui/Icon';
+import { Popover } from '@ark-ui/solid';
+
+import './Dropdown.pcss';
+import s from './Dropdown.module.pcss';
+
+const TIMEOUT_HIDE_TOOLTIP = 1000;
+const HIDE_DELAY = 200;
+
+type Props = {
+ overlayClass?: string;
+ menu: JSX.Element;
+ position?: 'bottomRight' | 'bottomLeft' | 'topRight' | 'topLeft' | 'top';
+ trigger: 'click' | 'hover';
+ noIcon?: true;
+ iconClass?: string;
+ class?: string;
+ openClass?: string;
+ open?: boolean;
+ onOpenChange?: (e: boolean) => void;
+ widthAuto?: boolean;
+ flex?: boolean;
+ minOverlayWidthMatchTrigger?: boolean;
+ flexWrapper?: boolean;
+ childrenClass?: string;
+ wrapClass?: string;
+ children?: JSX.Element;
+ isSelect?: boolean;
+ disableAnimation?: boolean;
+ disabled?: boolean;
+ autoClose?: boolean;
+};
+
+export const Dropdown = (props: Props) => {
+ let timer: ReturnType | null = null;
+ let showTimer: ReturnType | null = null;
+ let hideTimer: ReturnType | null = null;
+ const [visible, setVisible] = createSignal(!!untrack(() => props.open));
+
+ const onVisibleChange = (details: { open: boolean }) => {
+ if (props.disabled) {
+ return;
+ }
+
+ props.onOpenChange?.(details.open);
+ setVisible(details.open);
+ };
+
+ createEffect(() => {
+ if (typeof props.open === 'boolean') {
+ setVisible(props.open);
+ }
+ });
+
+ onCleanup(() => {
+ setVisible(false);
+ if (timer) clearTimeout(timer);
+ if (showTimer) clearTimeout(showTimer);
+ if (hideTimer) clearTimeout(hideTimer);
+ });
+
+ const handleOverlayClick = () => {
+ if (!props.autoClose) {
+ return;
+ }
+
+ if (timer) {
+ clearTimeout(timer);
+ }
+ timer = setTimeout(() => onVisibleChange({ open: false }), TIMEOUT_HIDE_TOOLTIP);
+ };
+
+ // Ark UI uses floating-ui placement tokens. corvu collapsed every position
+ // to plain `bottom`/`top` (ignoring left/right), so mapping `bottomRight` ->
+ // `bottom-end` is an intentional behavior change that honors the `position`
+ // prop. `flip` keeps the popover on-screen near viewport edges.
+ const placement = () => {
+ const position = untrack(() => props.position);
+ switch (position) {
+ case 'topRight':
+ return 'top-end';
+ case 'topLeft':
+ return 'top-start';
+ case 'bottomRight':
+ return 'bottom-end';
+ case 'bottomLeft':
+ return 'bottom-start';
+ case 'top':
+ return 'top';
+ default:
+ return 'bottom-end';
+ }
+ };
+
+ const scheduleHide = () => {
+ if (showTimer) {
+ clearTimeout(showTimer);
+ showTimer = null;
+ }
+ if (hideTimer) {
+ clearTimeout(hideTimer);
+ }
+ hideTimer = setTimeout(() => onVisibleChange({ open: false }), HIDE_DELAY);
+ };
+
+ const cancelHideAndShow = () => {
+ if (hideTimer) {
+ clearTimeout(hideTimer);
+ hideTimer = null;
+ }
+ if (!visible()) {
+ onVisibleChange({ open: true });
+ }
+ };
+
+ return (
+
+
+
+ {props.trigger === 'click' ? (
+ // Click mode MUST use Popover.Trigger: it tags the element with
+ // `data-part="trigger"` so Ark UI excludes it from outside-click
+ // detection. A plain div would race (outside-click closes, then the
+ // click reopens). Ark UI has no `as` prop — use `asChild`.
+
(
+
+ {props.children}
+
+ )}
+ />
+ ) : (
+ // Hover mode: a plain div (no Trigger) so a click does NOT toggle.
+ // Enter/leave go through onVisibleChange so `disabled` still guards.
+ cancelHideAndShow()}
+ onMouseLeave={() => scheduleHide()}
+ >
+ {props.children}
+
+ )}
+
+
+
+
+
+
+ cancelHideAndShow()}
+ onMouseLeave={() => scheduleHide()}
+ >
+ {props.menu}
+
+
+
+ );
+};
diff --git a/client_v2/src/common/ui/Dropdown/index.ts b/client_v2/src/common/ui/Dropdown/index.ts
new file mode 100644
index 00000000000..c0ad316fc26
--- /dev/null
+++ b/client_v2/src/common/ui/Dropdown/index.ts
@@ -0,0 +1 @@
+export { Dropdown } from './Dropdown';
diff --git a/client_v2/src/common/ui/FaqTooltip/FaqTooltip.tsx b/client_v2/src/common/ui/FaqTooltip/FaqTooltip.tsx
new file mode 100644
index 00000000000..ca1d52b2452
--- /dev/null
+++ b/client_v2/src/common/ui/FaqTooltip/FaqTooltip.tsx
@@ -0,0 +1,49 @@
+import { type JSX } from 'solid-js';
+import cn from 'clsx';
+
+import { Dropdown } from 'panel/common/ui/Dropdown';
+import { useIsMobile } from 'panel/hooks/useIsMobile';
+import theme from 'panel/lib/theme';
+import { Icon } from 'panel/common/ui/Icon';
+
+import s from './styles.module.pcss';
+
+type Props = {
+ text: JSX.Element;
+ menuSize?: 'small' | 'large';
+ spacing?: boolean;
+ menuClass?: string;
+ overlayClass?: string;
+ position?: 'bottomLeft' | 'bottomRight' | 'bottom';
+};
+
+export const FaqTooltip = (props: Props) => {
+ const isMobile = useIsMobile();
+
+ const currentPosition = () => (isMobile() ? 'bottom' : 'bottomLeft');
+ const position = () => props.position ?? currentPosition();
+
+ return (
+
+ );
+};
diff --git a/client_v2/src/common/ui/FaqTooltip/index.ts b/client_v2/src/common/ui/FaqTooltip/index.ts
new file mode 100644
index 00000000000..1e21f015f3f
--- /dev/null
+++ b/client_v2/src/common/ui/FaqTooltip/index.ts
@@ -0,0 +1 @@
+export { FaqTooltip } from './FaqTooltip';
diff --git a/client_v2/src/common/ui/FaqTooltip/styles.module.pcss b/client_v2/src/common/ui/FaqTooltip/styles.module.pcss
new file mode 100644
index 00000000000..1ecc1ee88ec
--- /dev/null
+++ b/client_v2/src/common/ui/FaqTooltip/styles.module.pcss
@@ -0,0 +1,48 @@
+.trigger {
+ cursor: pointer;
+ color: var(--default-gray-icons);
+}
+
+.overlay_mobile {
+ @media (max-width: 767px) {
+ max-width: calc(100vw - 32px) !important;
+ overflow: visible !important;
+ }
+}
+
+.menu {
+ padding: 16px;
+ background-color: var(--page-background-additional);
+
+ @media (max-width: 767px) {
+ max-width: calc(100vw - 32px);
+ }
+
+ @media (min-width: 768px) {
+ max-width: 280px;
+ }
+
+ &_spacing {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ }
+
+ &_large {
+ @media (min-width: 768px) {
+ max-width: 340px;
+ }
+ }
+
+ a {
+ color: var(--default-link);
+ transition: color var(--t2);
+ text-decoration: underline;
+
+ &:hover,
+ &:focus {
+ color: var(--hovered-link);
+ text-decoration: none;
+ }
+ }
+}
diff --git a/client_v2/src/common/ui/Footer/Footer.tsx b/client_v2/src/common/ui/Footer/Footer.tsx
new file mode 100644
index 00000000000..4a6a33bcced
--- /dev/null
+++ b/client_v2/src/common/ui/Footer/Footer.tsx
@@ -0,0 +1,170 @@
+import { createSignal, createMemo, For, Show } from 'solid-js';
+import cn from 'clsx';
+
+import theme from 'panel/lib/theme';
+import { Dropdown } from 'panel/common/ui/Dropdown';
+import { Icon } from 'panel/common/ui/Icon';
+import intl, { LocalesType } from 'panel/common/intl';
+
+import { LOCAL_STORAGE_KEYS, LocalStorageHelper } from 'panel/helpers/localStorageHelper';
+import { LanguageDropdown } from '../LanguageDropdown/LanguageDropdown';
+import { REPOSITORY, PRIVACY_POLICY_LINK, THEMES } from 'panel/helpers/constants';
+import { LANGUAGES, LANGUAGE_NAMES } from 'panel/helpers/twosky';
+import { setHtmlLangAttr, setUITheme } from 'panel/helpers/helpers';
+import {
+ changeTheme,
+ changeLanguage as changeLanguageAction,
+ getVersion,
+} from 'panel/stores/dashboard';
+import { dashboardState } from 'panel/stores/dashboard';
+
+import s from './styles.module.pcss';
+
+export const Footer = () => {
+ const currentTheme = () => dashboardState.theme || THEMES.auto;
+ const profileName = () => dashboardState.name || '';
+ const currentLanguage = () => dashboardState.language || intl.getUILanguage();
+ const isLoggedIn = () => profileName() !== '';
+
+ const linksData = createMemo(() => [
+ { href: PRIVACY_POLICY_LINK, name: intl.getMessage('privacy_policy') },
+ { href: REPOSITORY.ISSUES, name: intl.getMessage('report_an_issue') },
+ { href: REPOSITORY.RELEASE_NOTES, name: intl.getMessage('release_notes') },
+ ]);
+
+ const themeTranslations = createMemo>(() => ({
+ auto: intl.getMessage('system_theme'),
+ dark: intl.getMessage('dark_theme'),
+ light: intl.getMessage('light_theme'),
+ }));
+
+ const [currentThemeLocal, setCurrentThemeLocal] = createSignal(THEMES.auto);
+ const [themeDropdownOpen, setThemeDropdownOpen] = createSignal(false);
+
+ const getYear = () => new Date().getFullYear();
+
+ const getThemeIcon = () => {
+ const activeTheme = isLoggedIn() ? currentTheme() : currentThemeLocal();
+ if (activeTheme === THEMES.auto) return 'theme_auto';
+ if (activeTheme === THEMES.dark) return 'theme_dark';
+ return 'theme_light';
+ };
+
+ const changeLanguage = async (newLang: LocalesType) => {
+ setHtmlLangAttr(newLang);
+ try {
+ await changeLanguageAction(newLang);
+ LocalStorageHelper.setItem(LOCAL_STORAGE_KEYS.LANGUAGE, newLang);
+ window.location.reload();
+ } catch (error) {
+ console.error('Failed to save language preference:', error);
+ }
+ };
+
+ const onThemeChange = (value: string) => {
+ if (isLoggedIn()) {
+ changeTheme(value);
+ } else {
+ setUITheme(value);
+ setCurrentThemeLocal(value);
+ }
+ setThemeDropdownOpen(false);
+ };
+
+ return (
+
+ );
+};
diff --git a/client_v2/src/common/ui/Footer/index.ts b/client_v2/src/common/ui/Footer/index.ts
new file mode 100644
index 00000000000..65e2506faf5
--- /dev/null
+++ b/client_v2/src/common/ui/Footer/index.ts
@@ -0,0 +1 @@
+export { Footer } from './Footer';
diff --git a/client_v2/src/common/ui/Footer/styles.module.pcss b/client_v2/src/common/ui/Footer/styles.module.pcss
new file mode 100644
index 00000000000..b1f04f04a90
--- /dev/null
+++ b/client_v2/src/common/ui/Footer/styles.module.pcss
@@ -0,0 +1,144 @@
+.footer {
+ padding: 32px 16px;
+ border-top: 1px solid var(--default-item-divider);
+
+ @media (min-width: 1024px) {
+ padding: 16px 32px;
+ }
+}
+
+.container {
+ display: flex;
+ flex-direction: column-reverse;
+ justify-content: center;
+ align-items: center;
+ gap: 24px;
+ width: 100%;
+
+ @media (min-width: 1024px) {
+ flex-direction: row;
+ gap: 24px;
+ }
+
+ @media (min-width: 1200px) {
+ gap: 40px;
+ }
+}
+
+.leftGroup {
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+ gap: 24px;
+ justify-content: center;
+
+ @media (min-width: 1024px) {
+ column-gap: 40px;
+ row-gap: 24px;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: flex-start;
+ margin-right: auto;
+ }
+}
+
+.copyright {
+ order: 1;
+
+ @media (min-width: 1024px) {
+ order: 0;
+ }
+}
+
+.version {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ order: 2;
+
+ @media (min-width: 1024px) {
+ order: 0;
+ }
+}
+
+.checkUpdateBtn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+ padding: 0;
+ border: none;
+ background: transparent;
+ cursor: pointer;
+ border-radius: 4px;
+ color: var(--default-product-icon);
+ flex-shrink: 0;
+
+ &:hover,
+ &:focus {
+ background-color: var(--page-background-additional);
+ }
+
+ &:focus-visible {
+ outline: 2px solid var(--product-primary-90);
+ outline-offset: 2px;
+ }
+
+ &:disabled {
+ cursor: default;
+ opacity: 0.5;
+
+ &:hover {
+ background-color: transparent;
+ }
+ }
+}
+
+.checkUpdateBtn_loading {
+ cursor: default;
+}
+
+.links {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 16px;
+
+ @media (min-width: 1024px) {
+ flex-wrap: nowrap;
+ gap: 24px;
+ flex-shrink: 0;
+ }
+
+ @media (min-width: 1200px) {
+ gap: 40px;
+ }
+}
+
+.dropdownWrapper {
+ flex-shrink: 0;
+}
+
+.dropdown {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-shrink: 0;
+ white-space: nowrap;
+}
+
+.dropdownTrigger {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ cursor: pointer;
+}
+
+.icon {
+ color: var(--default-gray-icons);
+}
+
+.langOverlay {
+ max-width: 100%;
+}
diff --git a/client_v2/src/common/ui/Guide/Guide.module.pcss b/client_v2/src/common/ui/Guide/Guide.module.pcss
new file mode 100644
index 00000000000..0f5396ad00f
--- /dev/null
+++ b/client_v2/src/common/ui/Guide/Guide.module.pcss
@@ -0,0 +1,116 @@
+.title {
+ margin-bottom: 12px;
+ font-size: 18px;
+ font-weight: 700;
+}
+
+.inlineCopiedText {
+ display: inline-flex;
+}
+
+.dhcpLink {
+ color: var(--default-link);
+ font-size: 16px;
+ font-weight: 400;
+}
+
+.dhcpLink:hover {
+ text-decoration: underline;
+}
+
+.guideText {
+ line-height: 1.7;
+}
+
+.guideText li,
+.guideText p {
+ margin-bottom: 5px;
+}
+
+.guideText ul,
+.guideText ol {
+ padding-left: 25px;
+}
+
+.guideParagraph {
+ margin-bottom: 10px;
+}
+
+.guideTitle {
+ margin-bottom: 20px;
+}
+
+.strongNumbers {
+ counter-reset: strong-counter;
+}
+
+.guideList {
+ margin-top: 0;
+}
+
+.strongNumbers li {
+ counter-increment: strong-counter;
+ list-style: none;
+ position: relative;
+}
+
+.strongNumbers li::before {
+ content: counter(strong-counter) '.';
+ font-weight: bold;
+ position: absolute;
+ left: -25px;
+}
+
+.dnsLink {
+ color: var(--default-link);
+ font-size: 16px;
+ font-weight: 400;
+}
+
+.dnsLink:hover {
+ text-decoration: underline;
+}
+
+.selectorDesc {
+ margin-bottom: 4px;
+ font-size: 14px;
+ color: var(--default-description-text);
+}
+
+.deviceSelector {
+ margin-bottom: 32px;
+}
+
+.deviceDnsListItem,
+.guideBulletItem {
+ list-style: none;
+ position: relative;
+
+ &::before {
+ content: '';
+ width: 8px;
+ height: 8px;
+ background-color: var(--default-main-text);
+ border-radius: 50%;
+ display: inline-block;
+ position: absolute;
+ left: -16px;
+ top: 8px;
+ }
+}
+
+.deviceDnsListItem {
+ margin-top: 8px;
+}
+
+.guideBulletItem {
+ margin-bottom: 16px;
+
+ .guideItemTitle {
+ display: block;
+ }
+}
+
+.guideList {
+ padding-left: 24px;
+}
diff --git a/client_v2/src/common/ui/Guide/Guide.tsx b/client_v2/src/common/ui/Guide/Guide.tsx
new file mode 100644
index 00000000000..0d58d2648ca
--- /dev/null
+++ b/client_v2/src/common/ui/Guide/Guide.tsx
@@ -0,0 +1,466 @@
+import { type JSX, createSignal, createMemo, Show } from 'solid-js';
+import { A } from '@solidjs/router';
+import intl from 'panel/common/intl';
+import cn from 'clsx';
+
+import { encryptionState } from 'panel/stores/encryption';
+import { MOBILE_CONFIG_LINKS } from 'panel/helpers/constants';
+import { MobileConfigForm } from 'panel/components/SetupGuide/MobileConfigForm';
+import { Select } from 'panel/common/controls/Select';
+import { Paths } from 'panel/components/Routes/Paths';
+import { type IconType } from '../Icons';
+import { CopiedText } from '../CopiedText';
+import s from './Guide.module.pcss';
+
+type PlatformLayoutProps = {
+ serverName?: string;
+ portHttps?: number;
+ dnsAddresses?: string[];
+};
+
+type PlatformLayout = {
+ title: string;
+ icon: IconType;
+ component: JSX.Element;
+};
+
+type PlatformLayouts = Record;
+
+const RouterLayout = () => (
+
+
{intl.getMessage('setup_devices_router_title')}
+
+
{intl.getMessage('setup_devices_router_desc_1')}
+
+
+
+ {intl.getMessage('setup_devices_router_list_1_title')}
+
+ {intl.getMessage('setup_devices_router_list_1', {
+ code: () => ,
+ code2: () => ,
+ })}
+
+
+
+ {intl.getMessage('setup_devices_router_list_2_title')}
+
+ {intl.getMessage('setup_devices_router_list_2')}
+
+
+
+ {intl.getMessage('setup_devices_router_desc_2', {
+ a: (text: string) => (
+
+ {text}
+
+ ),
+ })}
+
+
+
+);
+
+const WindowsLayout = () => (
+
+
{intl.getMessage('setup_devices_windows_title')}
+
+
+ {intl.getMessage('setup_devices_windows_list_1')}
+ {intl.getMessage('setup_devices_windows_list_2')}
+ {intl.getMessage('setup_devices_windows_list_3')}
+ {intl.getMessage('setup_devices_windows_list_4')}
+ {intl.getMessage('setup_devices_windows_list_5')}
+ {intl.getMessage('setup_devices_windows_list_6')}
+
+
+
+);
+
+const MacOSLayout = () => (
+
+
macOS
+
+
+ {intl.getMessage('setup_devices_macos_list_1')}
+ {intl.getMessage('setup_devices_macos_list_2')}
+ {intl.getMessage('setup_devices_macos_list_3')}
+
+
+
+);
+
+const AndroidLayout = () => (
+
+
Android
+
+
+ {intl.getMessage('setup_devices_android_list_1')}
+ {intl.getMessage('setup_devices_android_list_2')}
+ {intl.getMessage('setup_devices_android_list_3')}
+ {intl.getMessage('setup_devices_android_list_4')}
+
+
+
+);
+
+const IOSLayout = () => (
+
+
iOS
+
+
+ {intl.getMessage('setup_devices_ios_list_1')}
+ {intl.getMessage('setup_devices_ios_list_2')}
+ {intl.getMessage('setup_devices_ios_list_3')}
+ {intl.getMessage('setup_devices_ios_list_4')}
+
+
+
+);
+
+const renderDnsDevicesList = () => (
+
+
+
+ Android
+
+
+
+ {intl.getMessage('setup_devices_dns_android_list_1')}
+
+
+ {intl.getMessage('setup_devices_dns_android_list_2', {
+ a: (text: string) => (
+
+ {text}
+
+ ),
+ })}
+
+
+ {intl.getMessage('setup_devices_dns_android_list_3', {
+ a: (text: string) => (
+
+ {text}
+
+ ),
+ })}
+
+
+
+
+
+
+ iOS
+
+
+
+ {intl.getMessage('setup_devices_dns_ios_list_1', {
+ a: (text: string) => (
+
+ {text}
+
+ ),
+ })}
+
+
+ {intl.getMessage('setup_devices_dns_ios_list_2', {
+ a: (text: string) => (
+
+ {text}
+
+ ),
+ b: (text: string) => (
+
+ {text}
+
+ ),
+ })}
+
+
+
+
+
+
+ {intl.getMessage('setup_dns_privacy_other_title')}
+
+
+
+ {intl.getMessage('setup_devices_dns_other_list_1', {
+ a: (text: string) => (
+
+ {text}
+
+ ),
+ })}
+
+
+ {intl.getMessage('setup_devices_dns_other_list_2', {
+ a: (text: string) => (
+
+ {text}
+
+ ),
+ })}
+
+
+ {intl.getMessage('setup_devices_dns_other_list_3', {
+ a: (text: string) => (
+
+ {text}
+
+ ),
+ })}
+
+
+ {intl.getMessage('setup_devices_dns_other_list_4', {
+ a: (text: string) => (
+
+ {text}
+
+ ),
+ })}
+
+
+ {intl.getMessage('setup_devices_dns_other_list_5', {
+ a: (text: string) => (
+
+ {text}
+
+ ),
+ b: (text: string) => (
+
+ {text}
+
+ ),
+ })}
+
+
+
+
+);
+
+const getDnsSettingsContent = (
+ dnsAddresses: string[] | undefined,
+ serverName?: string,
+ portHttps?: number,
+) => {
+ const tlsAddress = dnsAddresses?.filter((addr: string) => addr.includes('tls://')) ?? [];
+ const httpsAddress = dnsAddresses?.filter((addr: string) => addr.includes('https://')) ?? [];
+ const quicAddress = dnsAddresses?.find((addr: string) => addr.includes('quic://'));
+
+ const showDnsPrivacyNotice = httpsAddress.length < 1 && tlsAddress.length < 1;
+
+ return showDnsPrivacyNotice ? (
+
+ {intl.getMessage('setup_dns_notice_new', {
+ a: (text: string) => (
+
+ {text}
+
+ ),
+ })}
+
+ ) : (
+
+
+ 0}>
+
+ {intl.getMessage('setup_devices_dns_list_1', {
+ code: () => ,
+ })}
+
+
+ 0}>
+
+ {intl.getMessage('setup_devices_dns_list_2', {
+ code: () => ,
+ })}
+
+
+
+
+ {intl.getMessage('setup_devices_dns_list_3', {
+ code: () => ,
+ })}
+
+
+
+
+
{intl.getMessage('setup_devices_dns_list_devices')}
+
+ {renderDnsDevicesList()}
+
+
+
+ {intl.getMessage('setup_dns_privacy_ioc_mac')}
+
+
{intl.getMessage('setup_devices_dns_macos_desc')}
+
+
+
+
+
+ );
+};
+
+const DnsPrivacyLayout = (props: PlatformLayoutProps) => (
+
+
{intl.getMessage('dns_privacy')}
+
+ {getDnsSettingsContent(props.dnsAddresses, props.serverName, props.portHttps)}
+
+
+);
+
+const getPlatformLayouts = (params: PlatformLayoutProps): PlatformLayouts => ({
+ Router: {
+ title: intl.getMessage('setup_devices_router_title'),
+ icon: 'router',
+ component: ,
+ },
+ Windows: {
+ title: intl.getMessage('setup_devices_windows_title'),
+ icon: 'windows',
+ component: ,
+ },
+ macOS: {
+ title: intl.getMessage('setup_devices_macos_title'),
+ icon: 'mac',
+ component: ,
+ },
+ Android: {
+ title: intl.getMessage('setup_devices_android_title'),
+ icon: 'android',
+ component: ,
+ },
+ iOS: {
+ title: intl.getMessage('setup_devices_ios_title'),
+ icon: 'ios',
+ component: ,
+ },
+ dns_privacy: {
+ title: intl.getMessage('dns_privacy'),
+ icon: 'dns_privacy',
+ component: (
+
+ ),
+ },
+});
+
+type Props = {
+ dnsAddresses?: string[];
+};
+
+export const Guide = (props: Props) => {
+ const serverName = () => encryptionState.server_name;
+ const portHttps = () => encryptionState.port_https;
+
+ const [activeTabLabel, setActiveTabLabel] = createSignal('Router');
+
+ const platformLayouts = () =>
+ getPlatformLayouts({
+ serverName: serverName(),
+ portHttps: portHttps(),
+ dnsAddresses: props.dnsAddresses,
+ });
+
+ const selectOptions = createMemo(() =>
+ Object.entries(platformLayouts()).map(([key, value]) => ({
+ value: key,
+ label: value.title,
+ icon: value.icon,
+ })),
+ );
+
+ const selectedOption = createMemo(() =>
+ selectOptions().find((option) => option.value === activeTabLabel()),
+ );
+
+ const activeLayout = () => platformLayouts()[activeTabLabel() as keyof typeof platformLayouts];
+
+ return (
+
+
{intl.getMessage('device_type')}
+
+ setActiveTabLabel(option.value)}
+ showIcons={true}
+ size="responsive"
+ height="big"
+ />
+
+
+ {activeLayout().component}
+
+
+ );
+};
diff --git a/client_v2/src/common/ui/Guide/index.ts b/client_v2/src/common/ui/Guide/index.ts
new file mode 100644
index 00000000000..f9411b5b10a
--- /dev/null
+++ b/client_v2/src/common/ui/Guide/index.ts
@@ -0,0 +1 @@
+export * from './Guide';
diff --git a/client_v2/src/common/ui/Header/Header.module.pcss b/client_v2/src/common/ui/Header/Header.module.pcss
new file mode 100644
index 00000000000..1ebebf1cd6c
--- /dev/null
+++ b/client_v2/src/common/ui/Header/Header.module.pcss
@@ -0,0 +1,115 @@
+.header {
+ z-index: 1049;
+ background: var(--default-page-background);
+ height: 64px;
+ box-shadow: var(--main-box-shadow);
+
+ @media (min-width: 1200px) {
+ display: none;
+ }
+}
+
+.container {
+ display: flex;
+ align-items: center;
+ margin: 0 auto;
+ height: 100%;
+ padding: 0 16px;
+
+ @media (min-width: 768px) {
+ padding: 0 24px;
+ }
+
+ @media (min-width: 1024px) {
+ padding: 0 40px;
+ }
+}
+
+.logoWrap {
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ margin-right: 24px;
+}
+
+.logo {
+ width: 173px;
+ height: 28px;
+ margin-right: 8px;
+ flex-shrink: 1;
+}
+
+.navigation {
+ display: none;
+}
+
+.langs {
+ cursor: pointer;
+}
+
+.right {
+ margin-left: auto;
+ color: var(--default-main-text);
+}
+
+.user {
+ display: none;
+}
+
+.linkArrow {
+ margin-left: -8px;
+}
+
+.burgerIcon {
+ margin-right: 16px;
+}
+
+.burgerMenuMask {
+ position: fixed;
+ top: 0;
+ left: -100%;
+ width: 100%;
+ height: 100%;
+ z-index: 1501;
+ transition: left 0.4s ease;
+
+ &:after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: -100%;
+ transition: background-color 0.4s ease;
+ transition-delay: 0.3s;
+ height: 100%;
+ width: 100%;
+ }
+
+ &.open {
+ left: 0;
+
+ &:after {
+ left: 0;
+ background-color: rgba(0, 0, 0, 0.5);
+ }
+ }
+}
+
+.burgerMenu {
+ position: relative;
+ top: 0;
+ left: 0;
+ width: 240px;
+ height: 100%;
+ z-index: 1502;
+ padding: 16px 0;
+ background-color: var(--page-background-additional);
+ overflow-y: auto;
+}
+
+.linkWrapper {
+ display: flex;
+}
+
+.link {
+ text-decoration: none;
+}
diff --git a/client_v2/src/common/ui/Header/Header.tsx b/client_v2/src/common/ui/Header/Header.tsx
new file mode 100644
index 00000000000..1a4c5079763
--- /dev/null
+++ b/client_v2/src/common/ui/Header/Header.tsx
@@ -0,0 +1,68 @@
+import { createSignal } from 'solid-js';
+import cn from 'clsx';
+import { Icon } from 'panel/common/ui/Icon';
+import { Link } from 'panel/common/ui/Link';
+import { Menu } from 'panel/common/ui/Menu';
+import { RoutePath } from 'panel/components/Routes/Paths';
+import theme from 'panel/lib/theme';
+
+import s from './Header.module.pcss';
+import { Logo } from '../Sidebar/Logo';
+
+const BURGER_MENU_ID = 'linksMenu';
+
+export const Header = () => {
+ const [burgerModal, setBurgerModal] = createSignal(false);
+ const [accountSubMenu, setAccountSubMenu] = createSignal(false);
+
+ const closeBurgerMenu = (event: any) => {
+ const target = event.target as HTMLDivElement;
+
+ if (!target.closest(`#${BURGER_MENU_ID}`)) {
+ setBurgerModal(false);
+ document.body.classList.remove('block-scroll');
+ }
+ };
+
+ const closeSubMenu = () => {
+ document.body.classList.remove('block-scroll');
+ setBurgerModal(false);
+ setAccountSubMenu(false);
+ };
+
+ const openBurgerMenu = () => {
+ document.body.classList.add('block-scroll');
+ setBurgerModal(true);
+ };
+
+ return (
+
+ );
+};
diff --git a/client_v2/src/common/ui/Header/index.ts b/client_v2/src/common/ui/Header/index.ts
new file mode 100644
index 00000000000..29429dc97e8
--- /dev/null
+++ b/client_v2/src/common/ui/Header/index.ts
@@ -0,0 +1 @@
+export { Header } from './Header';
diff --git a/client_v2/src/common/ui/Icon/Icon.module.pcss b/client_v2/src/common/ui/Icon/Icon.module.pcss
new file mode 100644
index 00000000000..16d3a0c2172
--- /dev/null
+++ b/client_v2/src/common/ui/Icon/Icon.module.pcss
@@ -0,0 +1,30 @@
+.icon {
+ display: inline-block;
+ vertical-align: middle;
+ width: 24px;
+ height: 24px;
+ flex-shrink: 0;
+ user-select: none;
+ -webkit-tap-highlight-color: transparent;
+}
+
+.smallIcon {
+ width: 16px;
+ height: 16px;
+}
+
+.green {
+ color: var(--default-product-icon);
+}
+
+.gray {
+ color: var(--default-gray-icons);
+}
+
+.red {
+ color: var(--default-error-icon);
+}
+
+.black {
+ color: var(--default-black-icons);
+}
diff --git a/client_v2/src/common/ui/Icon/Icon.tsx b/client_v2/src/common/ui/Icon/Icon.tsx
new file mode 100644
index 00000000000..e1758260433
--- /dev/null
+++ b/client_v2/src/common/ui/Icon/Icon.tsx
@@ -0,0 +1,25 @@
+import cn from 'clsx';
+import { IconType } from 'panel/common/ui/Icons';
+
+import s from './Icon.module.pcss';
+
+export type IconColor = 'green' | 'gray' | 'red' | 'black';
+
+type Props = {
+ icon: IconType;
+ color?: IconColor;
+ class?: string;
+ onClick?: (e: MouseEvent) => void;
+};
+
+export const Icon = (props: Props) => {
+ const iconClass = () => cn(s.icon, s[props.color!], props.class);
+
+ return (
+ props.onClick?.(e)}>
+
+
+ );
+};
+
+export type { IconType } from 'panel/common/ui/Icons';
diff --git a/client_v2/src/common/ui/Icon/index.ts b/client_v2/src/common/ui/Icon/index.ts
new file mode 100644
index 00000000000..0eabe9cb9ef
--- /dev/null
+++ b/client_v2/src/common/ui/Icon/index.ts
@@ -0,0 +1,2 @@
+export type { IconType, IconColor } from './Icon';
+export { Icon } from './Icon';
diff --git a/client_v2/src/common/ui/Icons/Icons.pcss b/client_v2/src/common/ui/Icons/Icons.pcss
new file mode 100644
index 00000000000..a77a74a7fd4
--- /dev/null
+++ b/client_v2/src/common/ui/Icons/Icons.pcss
@@ -0,0 +1,3 @@
+.icons {
+ display: none;
+}
diff --git a/client_v2/src/common/ui/Icons/arrows_sort.svg b/client_v2/src/common/ui/Icons/arrows_sort.svg
new file mode 100644
index 00000000000..f11f14fe280
--- /dev/null
+++ b/client_v2/src/common/ui/Icons/arrows_sort.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client_v2/src/common/ui/Icons/index.tsx b/client_v2/src/common/ui/Icons/index.tsx
new file mode 100644
index 00000000000..ef3501dacf8
--- /dev/null
+++ b/client_v2/src/common/ui/Icons/index.tsx
@@ -0,0 +1,1060 @@
+import './Icons.pcss';
+
+export const ICONS = {
+ checkbox_off: 'checkbox_off',
+ checkbox_on: 'checkbox_on',
+ checkbox_plus: 'checkbox_plus',
+ checkbox_minus: 'checkbox_minus',
+ radio_on: 'radio_on',
+ radio_off: 'radio_off',
+ dashboard: 'dashboard',
+ settings: 'settings',
+ tune: 'tune',
+ log: 'log',
+ faq: 'faq',
+ logout: 'logout',
+ lang: 'lang',
+ theme_auto: 'theme_auto',
+ theme_dark: 'theme_dark',
+ theme_light: 'theme_light',
+ cross: 'cross',
+ arrow_bottom: 'arrow_bottom',
+ arrows_sort: 'arrows_sort',
+ butter: 'butter',
+ loader: 'loader',
+ check: 'check',
+ dot: 'dot',
+ attention: 'attention',
+ arrow: 'arrow',
+ edit: 'edit',
+ delete: 'delete',
+ plus: 'plus',
+ refresh: 'refresh',
+ bullets: 'bullets',
+ link: 'link',
+ not_found_search: 'not_found_search',
+ settings_info: 'settings_info',
+ label: 'label',
+ copy: 'copy',
+ router: 'router',
+ windows: 'windows',
+ mac: 'mac',
+ android: 'android',
+ ios: 'ios',
+ dns_privacy: 'dns_privacy',
+ location: 'location',
+ connections: 'connections',
+ adblocking: 'adblocking',
+ tracking: 'tracking',
+ parental: 'parental',
+ wifi: 'wifi',
+ lock: 'lock',
+ search: 'search',
+ time: 'time',
+ eye_open: 'eye_open',
+ eye_close: 'eye_close',
+ check_tiny: 'check_tiny',
+ user: 'user',
+} as const;
+
+export type IconType = keyof typeof ICONS;
+
+export const ICON_VALUES: IconType[] = Object.values(ICONS);
+
+export const Icons = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
diff --git a/client_v2/src/common/ui/LanguageDropdown/LanguageDropdown.module.pcss b/client_v2/src/common/ui/LanguageDropdown/LanguageDropdown.module.pcss
new file mode 100644
index 00000000000..6170946ad22
--- /dev/null
+++ b/client_v2/src/common/ui/LanguageDropdown/LanguageDropdown.module.pcss
@@ -0,0 +1,19 @@
+.langOverlay {
+ max-width: 100%;
+}
+
+.langButton {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 0;
+ border: 0;
+ background: none;
+ cursor: pointer;
+ color: inherit;
+ font: inherit;
+}
+
+.langLabel {
+ margin: 0 8px;
+}
diff --git a/client_v2/src/common/ui/LanguageDropdown/LanguageDropdown.tsx b/client_v2/src/common/ui/LanguageDropdown/LanguageDropdown.tsx
new file mode 100644
index 00000000000..e08b0935f48
--- /dev/null
+++ b/client_v2/src/common/ui/LanguageDropdown/LanguageDropdown.tsx
@@ -0,0 +1,87 @@
+import { createSignal, createMemo, For, untrack } from 'solid-js';
+import cn from 'clsx';
+
+import theme from 'panel/lib/theme';
+import intl from 'panel/common/intl';
+import { Dropdown } from 'panel/common/ui/Dropdown';
+import { Icon } from 'panel/common/ui/Icon';
+import s from './LanguageDropdown.module.pcss';
+
+type LanguageDropdownProps = {
+ value: string;
+ languages: Record;
+ languageNames?: Record;
+ onChange: (lang: string) => void | Promise;
+ position?: 'bottomRight' | 'bottomLeft' | 'topRight' | 'topLeft';
+ class?: string;
+ sort?: boolean;
+};
+
+const getLanguageShortLabel = (lang: string) => {
+ const l = lang || '';
+
+ const base = (() => {
+ if (typeof Intl !== 'undefined' && 'Locale' in Intl) {
+ return new (Intl as any).Locale(l).language || '';
+ }
+
+ return l.split('-')[0] || '';
+ })();
+
+ return base.slice(0, 2).toLocaleUpperCase();
+};
+
+export const LanguageDropdown = (props: LanguageDropdownProps) => {
+ const [open, setOpen] = createSignal(false);
+
+ const sortedKeys = createMemo(() => {
+ const keys = Object.keys(props.languages);
+ if (!props.sort) {
+ return keys;
+ }
+ const languages = props.languages;
+ return keys.sort((a, b) => (languages[a] || '').localeCompare(languages[b] || ''));
+ });
+
+ const currentLabel = () => getLanguageShortLabel(props.value);
+
+ return (
+
+ );
+};
diff --git a/client_v2/src/common/ui/Link/Link.tsx b/client_v2/src/common/ui/Link/Link.tsx
new file mode 100644
index 00000000000..45fb6235c00
--- /dev/null
+++ b/client_v2/src/common/ui/Link/Link.tsx
@@ -0,0 +1,77 @@
+import { type JSX, Show, untrack } from 'solid-js';
+import { A } from '@solidjs/router';
+import cn from 'clsx';
+import {
+ LinkParams,
+ linkPathBuilder,
+ RoutePathKey,
+ SCROLL_QUERY_KEY,
+} from 'panel/components/Routes/Paths';
+import theme from 'panel/lib/theme';
+
+type Props = {
+ to: RoutePathKey;
+ hash?: string;
+ props?: LinkParams;
+ class?: string;
+ type?: string;
+ stop?: boolean;
+ disabled?: boolean;
+ onClick?: JSX.EventHandler;
+ id?: string;
+ query?: Record;
+ children?: JSX.Element;
+};
+
+export const Link = (linkProps: Props) => {
+ untrack(() => {
+ if (linkProps.props) {
+ Object.keys(linkProps.props).forEach((key: string) => {
+ if (!(linkProps.props as any)[key]) {
+ throw new Error(`Wrong key value: ${key} for route: ${linkProps.to}`);
+ }
+ });
+ }
+ });
+
+ const handleClick = (e: MouseEvent) => {
+ // Don't scroll to top when navigating to a specific section (scroll handled by the target page)
+ if (!linkProps.query?.[SCROLL_QUERY_KEY]) {
+ setTimeout(() => {
+ window.scrollTo({ top: 0 });
+ }, 100);
+ }
+
+ if (linkProps.stop) {
+ e.stopPropagation();
+ }
+ if (linkProps.onClick) {
+ (linkProps.onClick as any)(e);
+ }
+ };
+
+ return (
+
+ {linkProps.children}
+
+ }
+ >
+
+ {linkProps.children}
+
+
+ );
+};
diff --git a/client_v2/src/common/ui/Link/index.ts b/client_v2/src/common/ui/Link/index.ts
new file mode 100644
index 00000000000..61fe08c6f49
--- /dev/null
+++ b/client_v2/src/common/ui/Link/index.ts
@@ -0,0 +1 @@
+export { Link } from './Link';
diff --git a/client_v2/src/common/ui/Loader/InlineLoader.tsx b/client_v2/src/common/ui/Loader/InlineLoader.tsx
new file mode 100644
index 00000000000..8411f1a6989
--- /dev/null
+++ b/client_v2/src/common/ui/Loader/InlineLoader.tsx
@@ -0,0 +1,13 @@
+import cn from 'clsx';
+import { Icon, IconType } from 'panel/common/ui/Icon';
+
+import s from './Loader.module.pcss';
+
+type Props = {
+ class?: string;
+ icon?: IconType;
+};
+
+export const InlineLoader = (props: Props) => (
+
+);
diff --git a/client_v2/src/common/ui/Loader/Loader.module.pcss b/client_v2/src/common/ui/Loader/Loader.module.pcss
new file mode 100644
index 00000000000..3c7147f4b99
--- /dev/null
+++ b/client_v2/src/common/ui/Loader/Loader.module.pcss
@@ -0,0 +1,61 @@
+.loader {
+ transform: translateZ(0);
+ animation: load 2s infinite linear;
+}
+
+@keyframes load {
+ 0% {
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ transform: rotate(360deg);
+ }
+}
+
+.overlayWrapper {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 200px;
+}
+
+.overlay {
+ background: var(--default-page-background);
+
+ .loader {
+ width: 56px;
+ height: 56px;
+ }
+}
+
+.chartOverlay,
+.pageOverlay {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 128px;
+ padding: 8px 16px;
+}
+
+.pageOverlay {
+ min-height: 440px;
+}
+
+.chartLoader {
+ width: 72px;
+ height: 72px;
+ color: var(--default-product-icon);
+
+ @media (min-width: 768px) {
+ width: 96px;
+ height: 96px;
+ }
+}
+
+.pageLoader {
+ width: 72px;
+ height: 72px;
+ color: var(--default-product-icon);
+}
diff --git a/client_v2/src/common/ui/Loader/Loader.tsx b/client_v2/src/common/ui/Loader/Loader.tsx
new file mode 100644
index 00000000000..2032ff982ca
--- /dev/null
+++ b/client_v2/src/common/ui/Loader/Loader.tsx
@@ -0,0 +1,24 @@
+import cn from 'clsx';
+import { Icon, type IconColor, type IconType } from 'panel/common/ui/Icon';
+
+import s from './Loader.module.pcss';
+
+type Props = {
+ color?: IconColor;
+ class?: string;
+ overlay?: boolean;
+ overlayClass?: string;
+ icon?: IconType;
+};
+
+export const Loader = (props: Props) => (
+
+);
diff --git a/client_v2/src/common/ui/Loader/PageLoader.tsx b/client_v2/src/common/ui/Loader/PageLoader.tsx
new file mode 100644
index 00000000000..31d28716fe7
--- /dev/null
+++ b/client_v2/src/common/ui/Loader/PageLoader.tsx
@@ -0,0 +1,7 @@
+import { Loader } from './Loader';
+
+import s from './Loader.module.pcss';
+
+export const PageLoader = () => (
+
+);
diff --git a/client_v2/src/common/ui/Loader/index.ts b/client_v2/src/common/ui/Loader/index.ts
new file mode 100644
index 00000000000..01a1822e7e0
--- /dev/null
+++ b/client_v2/src/common/ui/Loader/index.ts
@@ -0,0 +1,3 @@
+export { InlineLoader } from './InlineLoader';
+export { Loader } from './Loader';
+export { PageLoader } from './PageLoader';
diff --git a/client_v2/src/common/ui/Menu/AccordionSection.tsx b/client_v2/src/common/ui/Menu/AccordionSection.tsx
new file mode 100644
index 00000000000..6ec95c710aa
--- /dev/null
+++ b/client_v2/src/common/ui/Menu/AccordionSection.tsx
@@ -0,0 +1,78 @@
+import { createSignal, createEffect, Show, For } from 'solid-js';
+import cn from 'clsx';
+import { Icon, type IconType } from 'panel/common/ui/Icon';
+import { Link } from 'panel/common/ui/Link';
+import { type RoutePathKey } from 'panel/components/Routes/Paths';
+import theme from 'panel/lib/theme';
+
+import s from './styles.module.pcss';
+
+type AccordionItem = {
+ label: string;
+ path: string;
+ routePath: RoutePathKey;
+};
+
+type Props = {
+ title: string;
+ icon: IconType;
+ items: AccordionItem[];
+ isActive: (path: string | string[], full?: boolean) => boolean;
+ class?: string;
+};
+
+export const AccordionSection = (props: Props) => {
+ const [isOpen, setIsOpen] = createSignal(false);
+
+ const isAnyItemActive = () => props.items.some((item) => props.isActive(item.path));
+
+ createEffect(() => {
+ if (!isOpen() && isAnyItemActive()) {
+ setIsOpen(true);
+ }
+ });
+
+ const toggleAccordion = (e: MouseEvent) => {
+ e.stopPropagation();
+ setIsOpen(!isOpen());
+ };
+
+ return (
+
+ );
+};
diff --git a/client_v2/src/common/ui/Menu/Menu.tsx b/client_v2/src/common/ui/Menu/Menu.tsx
new file mode 100644
index 00000000000..a3ba95b22f2
--- /dev/null
+++ b/client_v2/src/common/ui/Menu/Menu.tsx
@@ -0,0 +1,158 @@
+import { useLocation } from '@solidjs/router';
+import cn from 'clsx';
+import { Icon } from 'panel/common/ui/Icon';
+import { Link } from 'panel/common/ui/Link';
+import { Paths, RoutePath } from 'panel/components/Routes/Paths';
+import theme from 'panel/lib/theme';
+import intl from 'panel/common/intl';
+import { apiClient } from 'panel/api/Api';
+import { AccordionSection } from './AccordionSection';
+
+import s from './styles.module.pcss';
+
+type Props = {
+ accountSubMenu: boolean;
+ setAccountSubMenu: (value: boolean) => void;
+ burgerMenuId?: string;
+ closeSubMenu?: () => void;
+ rightSideDropdown?: boolean;
+ headerMenu?: boolean;
+};
+
+export const Menu = (props: Props) => {
+ const location = useLocation();
+
+ const isActive = (path: string | string[], full = false) => {
+ const currentPath = location.pathname;
+ const paths = Array.isArray(path) ? path : [path];
+
+ return paths.some((p) => {
+ if (currentPath === p) {
+ return true;
+ }
+
+ if (full) {
+ return false;
+ }
+
+ const normalizedPath = p.endsWith('/') ? p : `${p}/`;
+ return currentPath.startsWith(normalizedPath);
+ });
+ };
+
+ return (
+
+ );
+};
diff --git a/client_v2/src/common/ui/Menu/index.ts b/client_v2/src/common/ui/Menu/index.ts
new file mode 100644
index 00000000000..81b93712f16
--- /dev/null
+++ b/client_v2/src/common/ui/Menu/index.ts
@@ -0,0 +1 @@
+export { Menu } from './Menu';
diff --git a/client_v2/src/common/ui/Menu/styles.module.pcss b/client_v2/src/common/ui/Menu/styles.module.pcss
new file mode 100644
index 00000000000..672c0e26e2d
--- /dev/null
+++ b/client_v2/src/common/ui/Menu/styles.module.pcss
@@ -0,0 +1,113 @@
+.menu {
+ padding-top: 8px;
+ display: none;
+ box-shadow: var(--main-box-shadow);
+ min-height: 100vh;
+ max-width: 240px;
+ flex-shrink: 0;
+ z-index: 1;
+
+ @media (min-width: 1200px) {
+ display: flex;
+ flex-direction: column;
+ }
+}
+
+.menuWrapper {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ min-height: calc(100vh - 80px);
+ padding-bottom: env(safe-area-inset-bottom);
+
+ @media (min-width: 1200px) {
+ min-height: 0;
+ flex: 1;
+ }
+
+ &.headerMenu {
+ min-height: 100%;
+ justify-content: flex-start;
+ gap: 80px;
+ }
+}
+
+.menuLink,
+.accordionLink {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px 16px;
+ font-size: 16px;
+ line-height: 21px;
+ font-weight: var(--weight-regular);
+ color: var(--default-main-text);
+ text-decoration: none;
+ max-width: 100%;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ vertical-align: middle;
+ transition:
+ background-color var(--t2),
+ color var(--t2);
+ outline: none;
+
+ &:hover,
+ &:focus {
+ color: var(--default-main-text);
+ background-color: var(--pressed-page-background);
+ text-decoration: none;
+ }
+
+ &.activeLink {
+ color: var(--default-link);
+
+ .linkIcon {
+ color: var(--default-link);
+ }
+ }
+}
+
+.accordionLink {
+ padding: 12px 16px 12px 52px;
+}
+
+.linkIcon {
+ color: var(--default-gray-icons);
+ transition: color var(--t2);
+}
+
+.withConfirmIcon {
+ display: flex;
+ align-items: center;
+}
+
+.topMenuWrapper {
+ margin-bottom: auto;
+
+ @media (min-width: 1200px) {
+ margin-bottom: 0;
+ }
+}
+
+.referenceWrapper {
+ margin-top: auto;
+}
+
+.accordionHeader {
+ cursor: pointer;
+ user-select: none;
+}
+
+.accordionArrow {
+ margin-left: auto;
+ color: var(--default-gray-icons);
+ transition:
+ transform var(--t2),
+ color var(--t2);
+}
+
+.accordionArrowOpen {
+ transform: rotate(180deg);
+}
diff --git a/client_v2/src/common/ui/ModalWrapper/ModalWrapper.tsx b/client_v2/src/common/ui/ModalWrapper/ModalWrapper.tsx
new file mode 100644
index 00000000000..4d27b8138a4
--- /dev/null
+++ b/client_v2/src/common/ui/ModalWrapper/ModalWrapper.tsx
@@ -0,0 +1,11 @@
+import { type JSX, Show } from 'solid-js';
+import { modalsState } from 'panel/stores/modals';
+
+interface Props {
+ id: string;
+ children: JSX.Element;
+}
+
+export const ModalWrapper = (props: Props) => {
+ return {props.children} ;
+};
diff --git a/client_v2/src/common/ui/ModalWrapper/index.ts b/client_v2/src/common/ui/ModalWrapper/index.ts
new file mode 100644
index 00000000000..8dd09026577
--- /dev/null
+++ b/client_v2/src/common/ui/ModalWrapper/index.ts
@@ -0,0 +1 @@
+export { ModalWrapper } from './ModalWrapper';
diff --git a/client_v2/src/common/ui/PlusButton/PlusButton.module.pcss b/client_v2/src/common/ui/PlusButton/PlusButton.module.pcss
new file mode 100644
index 00000000000..6f021609939
--- /dev/null
+++ b/client_v2/src/common/ui/PlusButton/PlusButton.module.pcss
@@ -0,0 +1,35 @@
+.plusButton {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 0;
+ font-size: 16px;
+ color: var(--default-link);
+ border: 0;
+ background: none;
+ cursor: pointer;
+
+ &:hover,
+ &:focus {
+ color: var(--hovered-link);
+ background: none;
+ }
+
+ &:active {
+ color: var(--pressed-link);
+ background: none;
+ }
+
+ &:disabled {
+ color: var(--disabled-main-button);
+ cursor: default;
+ }
+
+ &.bold {
+ font-weight: var(--weight-bold);
+ }
+
+ &.semi {
+ font-weight: var(--weight-semi-bold);
+ }
+}
diff --git a/client_v2/src/common/ui/PlusButton/PlusButton.tsx b/client_v2/src/common/ui/PlusButton/PlusButton.tsx
new file mode 100644
index 00000000000..cd6b1122416
--- /dev/null
+++ b/client_v2/src/common/ui/PlusButton/PlusButton.tsx
@@ -0,0 +1,27 @@
+import { type JSX } from 'solid-js';
+import cn from 'clsx';
+
+import { Icon } from 'panel/common/ui/Icon';
+import s from './PlusButton.module.pcss';
+
+type Props = {
+ children: JSX.Element;
+ class?: string;
+ onClick: () => void;
+ disabled?: boolean;
+ testId?: string;
+ weight?: 'normal' | 'semi' | 'bold';
+};
+
+export const PlusButton = (props: Props) => (
+ (props.onClick as any)?.(e)}
+ disabled={props.disabled}
+ data-testid={props.testId}
+ >
+
+ {props.children}
+
+);
diff --git a/client_v2/src/common/ui/PlusButton/index.ts b/client_v2/src/common/ui/PlusButton/index.ts
new file mode 100644
index 00000000000..32fbe0c7cf5
--- /dev/null
+++ b/client_v2/src/common/ui/PlusButton/index.ts
@@ -0,0 +1 @@
+export { PlusButton } from './PlusButton';
diff --git a/client_v2/src/common/ui/PublicHeader/PublicHeader.module.pcss b/client_v2/src/common/ui/PublicHeader/PublicHeader.module.pcss
new file mode 100644
index 00000000000..d4091fd0af9
--- /dev/null
+++ b/client_v2/src/common/ui/PublicHeader/PublicHeader.module.pcss
@@ -0,0 +1,53 @@
+.header {
+ box-shadow: 4px 4px 8px 0 rgba(0, 0, 0, 0.10);
+}
+
+.headerContent {
+ display: flex;
+ height: 80px;
+ justify-content: space-between;
+ align-items: center;
+ align-self: stretch;
+ width: 100%;
+ max-width: 1176px;
+ margin: 0 auto;
+}
+
+.languageWrap {
+ display: flex;
+ align-items: center;
+}
+
+.headerContent {
+ height: auto;
+ flex-wrap: wrap;
+ row-gap: 12px;
+ padding: 24px 16px;
+
+ @media screen and (min-width: 1024px) {
+ height: 80px;
+ flex-wrap: nowrap;
+ row-gap: 0;
+ padding: 0;
+ }
+}
+
+.logoWrap {
+ order: 1;
+ padding: 0;
+
+ @media screen and (min-width: 1024px) {
+ order: 0;
+ padding: 0 0 0 16px;
+ }
+}
+
+.languageWrap {
+ order: 2;
+ margin-left: auto;
+
+ @media screen and (min-width: 1024px) {
+ order: 0;
+ margin-left: 0;
+ }
+}
diff --git a/client_v2/src/common/ui/PublicHeader/PublicHeader.tsx b/client_v2/src/common/ui/PublicHeader/PublicHeader.tsx
new file mode 100644
index 00000000000..9d998f2f09b
--- /dev/null
+++ b/client_v2/src/common/ui/PublicHeader/PublicHeader.tsx
@@ -0,0 +1,60 @@
+import { type JSX } from 'solid-js';
+import { Logo } from 'panel/common/ui/Sidebar';
+import intl, { type LocalesType } from 'panel/common/intl';
+import { LanguageDropdown } from 'panel/common/ui/LanguageDropdown/LanguageDropdown';
+import { setHtmlLangAttr } from 'panel/helpers/helpers';
+import { changeLanguage as changeLanguageAction, dashboardState } from 'panel/stores/dashboard';
+
+import { LOCAL_STORAGE_KEYS, LocalStorageHelper } from 'panel/helpers/localStorageHelper';
+import { LANGUAGES, LANGUAGE_NAMES } from 'panel/helpers/twosky';
+import styles from './PublicHeader.module.pcss';
+
+type Props = {
+ dropdownClass?: string;
+ dropdownPosition?: 'bottomRight' | 'bottomLeft' | 'topRight' | 'topLeft';
+ center?: JSX.Element;
+ useLocalLanguage?: boolean;
+};
+
+export const PublicHeader = (props: Props) => {
+ const changeLanguage = async (newLang: LocalesType) => {
+ setHtmlLangAttr(newLang);
+
+ if (props.useLocalLanguage) {
+ LocalStorageHelper.setItem(LOCAL_STORAGE_KEYS.LANGUAGE, newLang);
+ window.location.reload();
+ return;
+ }
+
+ try {
+ await changeLanguageAction(newLang);
+ LocalStorageHelper.setItem(LOCAL_STORAGE_KEYS.LANGUAGE, newLang);
+ window.location.reload();
+ } catch (error) {
+ console.error('Failed to save language preference:', error);
+ }
+ };
+
+ const currentLanguage = () => dashboardState.language || intl.getUILanguage();
+
+ return (
+
+ );
+};
diff --git a/client_v2/src/common/ui/PublicHeader/index.ts b/client_v2/src/common/ui/PublicHeader/index.ts
new file mode 100644
index 00000000000..49ca156ed8e
--- /dev/null
+++ b/client_v2/src/common/ui/PublicHeader/index.ts
@@ -0,0 +1 @@
+export { PublicHeader } from './PublicHeader';
diff --git a/client_v2/src/common/ui/SettingRow/SettingRow.module.pcss b/client_v2/src/common/ui/SettingRow/SettingRow.module.pcss
new file mode 100644
index 00000000000..88dca55fc26
--- /dev/null
+++ b/client_v2/src/common/ui/SettingRow/SettingRow.module.pcss
@@ -0,0 +1,142 @@
+.switch {
+ width: 100%;
+ padding: 8px 16px;
+ transition: background-color var(--t2);
+ border-radius: 0;
+ outline: none;
+
+ @media (min-width: 1024px) {
+ border-radius: 8px;
+ }
+
+ &:hover,
+ &:focus-visible {
+ background-color: var(--page-background-additional);
+ }
+
+ &:active {
+ background-color: transparent;
+ }
+}
+
+.switchDisabled {
+ cursor: default;
+
+ &:hover,
+ &:focus {
+ background-color: transparent;
+ }
+
+ .title,
+ .desc,
+ .value {
+ color: var(--disabled-main-text);
+ }
+
+ .row {
+ cursor: default;
+ }
+}
+
+.row {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 12px;
+ cursor: pointer;
+
+ &.rowCenter {
+ align-items: center;
+ }
+}
+
+.divider {
+ margin: 8px 8px 8px auto;
+ width: 1px;
+ align-self: stretch;
+ background-color: var(--default-item-divider);
+ flex-shrink: 0;
+}
+
+.text {
+ padding: 8px 0;
+}
+
+.title {
+ margin-bottom: 4px;
+ font-weight: var(--weight-semi-bold);
+ color: var(--default-main-text);
+}
+
+.titleDisabled {
+ color: var(--disabled-main-text);
+}
+
+.desc {
+ color: var(--default-description-text);
+
+ p {
+ margin: 0;
+ }
+
+ p + p {
+ margin-top: 12px;
+ }
+}
+
+.descDisabled {
+ color: var(--disabled-main-text);
+}
+
+.value {
+ margin-top: 8px;
+ font-weight: var(--weight-semi-bold);
+ color: var(--default-description-text);
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.valueDisabled {
+ color: var(--disabled-main-text);
+}
+
+.input {
+ padding: 8px 0;
+ display: flex;
+ align-items: flex-start;
+ gap: 12px;
+ align-self: stretch;
+
+ &.largeTitle {
+ padding-top: 16px;
+ }
+}
+
+.link {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ color: var(--default-gray-icons);
+ cursor: pointer;
+ white-space: nowrap;
+ border: none;
+ background: none;
+ padding: 0;
+ font-size: inherit;
+
+ &:disabled {
+ cursor: default;
+ opacity: 0.5;
+ }
+}
+
+.arrow {
+ color: var(--default-description-text);
+}
+
+.content {
+ padding: 8px 0;
+}
diff --git a/client_v2/src/common/ui/SettingRow/SettingRow.tsx b/client_v2/src/common/ui/SettingRow/SettingRow.tsx
new file mode 100644
index 00000000000..f99d4eed209
--- /dev/null
+++ b/client_v2/src/common/ui/SettingRow/SettingRow.tsx
@@ -0,0 +1,177 @@
+import { type JSX, Show, createMemo } from 'solid-js';
+import cn from 'clsx';
+
+import { Switch } from 'panel/common/controls/Switch';
+import { Icon } from 'panel/common/ui/Icon';
+
+import s from './SettingRow.module.pcss';
+import theme from 'panel/lib/theme';
+
+type SettingRowVariant = 'switch' | 'link' | 'switch-link';
+
+type Props = {
+ id: string;
+ title: string;
+ titleClass?: string;
+ description?: string | JSX.Element;
+ descriptionClass?: string;
+ value?: string;
+ variant: SettingRowVariant;
+ checked?: boolean;
+ disabled?: boolean;
+ onChange?: (checked: boolean) => void;
+ onClick?: () => void;
+ class?: string;
+ children?: JSX.Element;
+ divider?: boolean;
+ align?: 'top' | 'center';
+ largeTitle?: boolean;
+ inputClass?: string;
+};
+
+export const SettingRow = (props: Props) => {
+ let inputRef: HTMLInputElement | undefined;
+
+ const isSwitch = createMemo(() => props.variant === 'switch');
+ const isLink = createMemo(() => props.variant === 'link');
+ const isSwitchLink = createMemo(() => props.variant === 'switch-link');
+
+ const handleRowClick = (e?: MouseEvent) => {
+ if (props.disabled) {
+ return;
+ }
+ // Skip programmatic click if the user already clicked the switch/label
+ // — the native label behaviour already toggles it.
+ if (e) {
+ const target = e.target as HTMLElement;
+ if (target.tagName === 'INPUT' || target.closest('label')) {
+ return;
+ }
+ }
+ if (isSwitch()) {
+ inputRef?.click();
+ } else if (isLink() || isSwitchLink()) {
+ props.onClick?.();
+ }
+ };
+
+ const handleSwitchChange = (e: Event) => {
+ e.stopPropagation();
+ if (props.disabled) {
+ return;
+ }
+ const target = e.target as HTMLInputElement;
+ props.onChange?.(target.checked);
+ };
+
+ const handleLinkClick = (e: Event) => {
+ e.stopPropagation();
+ if (props.disabled) {
+ return;
+ }
+ props.onClick?.();
+ };
+
+ const handleInputClick = (e: MouseEvent) => {
+ const target = e.target as HTMLElement;
+ const isSwitchClick = target.tagName === 'INPUT' || !!target.closest('label');
+
+ // Native label click already toggled the switch — don't double-fire.
+ if (isSwitchClick) {
+ return;
+ }
+
+ if ((isSwitch() || isSwitchLink()) && !props.disabled) {
+ e.stopPropagation();
+ inputRef?.click();
+ }
+ };
+
+ const isSwitchVariant = () => isSwitch() || isSwitchLink();
+
+ const isLinkVariant = () => isLink();
+
+ return (
+ {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ handleRowClick();
+ }
+ }}
+ >
+
+
+
+ {props.title}
+
+
+
+ {props.description}
+
+
+
+
+ {props.value}
+
+
+
+
+
+
+
+
+ {
+ inputRef = el;
+ }}
+ />
+
+
+
+
+
+
+
+
+
+ {props.children}
+
+
+ );
+};
diff --git a/client_v2/src/common/ui/SettingRow/index.ts b/client_v2/src/common/ui/SettingRow/index.ts
new file mode 100644
index 00000000000..ad9a32b5ac4
--- /dev/null
+++ b/client_v2/src/common/ui/SettingRow/index.ts
@@ -0,0 +1 @@
+export { SettingRow } from './SettingRow';
diff --git a/client_v2/src/common/ui/SettingsGroup/RadioGroup.tsx b/client_v2/src/common/ui/SettingsGroup/RadioGroup.tsx
new file mode 100644
index 00000000000..4c9d7ef5f75
--- /dev/null
+++ b/client_v2/src/common/ui/SettingsGroup/RadioGroup.tsx
@@ -0,0 +1,52 @@
+import { type JSX, Show } from 'solid-js';
+import cn from 'clsx';
+
+import { Radio } from 'panel/common/controls/Radio';
+import theme from 'panel/lib/theme';
+
+import s from './styles.module.pcss';
+
+type Option = { text: string; value: T };
+
+type Props = {
+ title?: string;
+ description?: string;
+ disabled?: boolean;
+ value: T;
+ options: Option[];
+ onChange: (value: T) => void;
+ class?: string;
+ children?: JSX.Element;
+ name?: string;
+};
+
+export const RadioGroup = (props: Props) => {
+ return (
+
+
+
+
+
+ {props.title}
+
+
+ {props.description}
+
+
+
+
+
+
+
+
+ {props.children}
+
+
+ );
+};
diff --git a/client_v2/src/common/ui/SettingsGroup/SwitchGroup.tsx b/client_v2/src/common/ui/SettingsGroup/SwitchGroup.tsx
new file mode 100644
index 00000000000..b5b1dfb9ade
--- /dev/null
+++ b/client_v2/src/common/ui/SettingsGroup/SwitchGroup.tsx
@@ -0,0 +1,66 @@
+import { type JSX, Show } from 'solid-js';
+import cn from 'clsx';
+
+import { Switch } from 'panel/common/controls/Switch';
+import theme from 'panel/lib/theme';
+
+import s from './styles.module.pcss';
+
+type Props = {
+ title: string;
+ description?: string;
+ id: string;
+ class?: string;
+ checked: boolean;
+ onChange: (e: Event) => void;
+ disabled?: boolean;
+ children?: JSX.Element;
+};
+
+export const SwitchGroup = (props: Props) => {
+ let inputRef: HTMLInputElement | undefined;
+
+ const handleRowClick = () => {
+ if (props.disabled || !inputRef) {
+ return;
+ }
+
+ inputRef?.click();
+ };
+
+ return (
+
+
+
+
+ {props.title}
+
+
+ {props.description}
+
+
+
e.stopPropagation()}>
+ {
+ inputRef = el;
+ }}
+ />
+
+
+
+
+ {props.children}
+
+
+ );
+};
diff --git a/client_v2/src/common/ui/SettingsGroup/index.ts b/client_v2/src/common/ui/SettingsGroup/index.ts
new file mode 100644
index 00000000000..b1ed7a809f2
--- /dev/null
+++ b/client_v2/src/common/ui/SettingsGroup/index.ts
@@ -0,0 +1,2 @@
+export { RadioGroup } from './RadioGroup';
+export { SwitchGroup } from './SwitchGroup';
diff --git a/client_v2/src/common/ui/SettingsGroup/styles.module.pcss b/client_v2/src/common/ui/SettingsGroup/styles.module.pcss
new file mode 100644
index 00000000000..6bd272869d5
--- /dev/null
+++ b/client_v2/src/common/ui/SettingsGroup/styles.module.pcss
@@ -0,0 +1,55 @@
+.switch {
+ width: 100%;
+ padding: 8px 16px;
+ transition: background-color var(--t2);
+ border-radius: 8px;
+
+ &:hover,
+ &:focus {
+ background-color: var(--page-background-additional);
+ }
+}
+
+.switchDisabled {
+ &:hover,
+ &:focus {
+ background-color: transparent;
+ }
+}
+
+.radio {
+ width: 100%;
+}
+
+.row {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 24px;
+ cursor: pointer;
+
+ @media (min-width: 1024px) {
+ gap: 40px;
+ }
+}
+
+.text {
+ padding: 8px 0;
+}
+
+.title {
+ color: var(--default-main-text);
+}
+
+.titleDisabled {
+ color: var(--disabled-main-text);
+}
+
+.desc {
+ color: var(--default-description-text);
+}
+
+.input,
+.content {
+ padding: 8px 0;
+}
diff --git a/client_v2/src/common/ui/Sidebar/Logo.tsx b/client_v2/src/common/ui/Sidebar/Logo.tsx
new file mode 100644
index 00000000000..4df60f87c87
--- /dev/null
+++ b/client_v2/src/common/ui/Sidebar/Logo.tsx
@@ -0,0 +1,89 @@
+import s from './styles.module.pcss';
+
+type Props = {
+ id: string;
+};
+
+export const Logo = (props: Props) => {
+ const id = () => props.id || 'sidebar';
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/client_v2/src/common/ui/Sidebar/Sidebar.tsx b/client_v2/src/common/ui/Sidebar/Sidebar.tsx
new file mode 100644
index 00000000000..4fb4de9ba0c
--- /dev/null
+++ b/client_v2/src/common/ui/Sidebar/Sidebar.tsx
@@ -0,0 +1,32 @@
+import { createSignal } from 'solid-js';
+import { Link } from 'panel/common/ui/Link';
+import { Menu } from 'panel/common/ui/Menu';
+import { RoutePath } from 'panel/components/Routes/Paths';
+
+import s from './styles.module.pcss';
+import { Logo } from './Logo';
+
+export const Sidebar = () => {
+ const [accountSubMenu, setAccountSubMenu] = createSignal(false);
+
+ return (
+
+ );
+};
diff --git a/client_v2/src/common/ui/Sidebar/index.ts b/client_v2/src/common/ui/Sidebar/index.ts
new file mode 100644
index 00000000000..1981438ce6c
--- /dev/null
+++ b/client_v2/src/common/ui/Sidebar/index.ts
@@ -0,0 +1,2 @@
+export { Sidebar } from './Sidebar';
+export { Logo } from './Logo';
diff --git a/client_v2/src/common/ui/Sidebar/styles.module.pcss b/client_v2/src/common/ui/Sidebar/styles.module.pcss
new file mode 100644
index 00000000000..44666c9a8b0
--- /dev/null
+++ b/client_v2/src/common/ui/Sidebar/styles.module.pcss
@@ -0,0 +1,61 @@
+.sidebarWrapper {
+ display: block;
+ min-width: 240px;
+
+ @media (min-width: 1200px) {
+ align-self: stretch;
+ background-color: var(--page-background-additional);
+ }
+}
+
+.sidebarContainer {
+ display: none;
+ flex-shrink: 0;
+
+ @media (min-width: 1200px) {
+ display: flex;
+ flex-direction: column;
+ position: sticky;
+ top: 0;
+ width: 100%;
+ max-width: 240px;
+ height: 100vh;
+ padding: 16px 0;
+ overflow-y: auto;
+ z-index: 1;
+ background-color: var(--page-background-additional);
+ }
+
+ &::-webkit-scrollbar {
+ display: none;
+ }
+}
+
+.sidebar {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ min-height: 0;
+}
+
+.logo {
+ width: 160px;
+ height: 24px;
+}
+
+.linkWrapper {
+ display: flex;
+ padding: 12px 16px 32px;
+}
+
+.link {
+ text-decoration: none;
+}
+
+.logoCompany {
+ fill: var(--default-logo-color);
+}
+
+.logoProduct {
+ fill: var(--default-description-text);
+}
diff --git a/client_v2/src/common/ui/SortSelect/SortSelect.module.pcss b/client_v2/src/common/ui/SortSelect/SortSelect.module.pcss
new file mode 100644
index 00000000000..868905ac635
--- /dev/null
+++ b/client_v2/src/common/ui/SortSelect/SortSelect.module.pcss
@@ -0,0 +1,3 @@
+.wrapper {
+ width: 100%;
+}
diff --git a/client_v2/src/common/ui/SortSelect/SortSelect.tsx b/client_v2/src/common/ui/SortSelect/SortSelect.tsx
new file mode 100644
index 00000000000..f8a52a63f73
--- /dev/null
+++ b/client_v2/src/common/ui/SortSelect/SortSelect.tsx
@@ -0,0 +1,31 @@
+import cn from 'clsx';
+
+import intl from 'panel/common/intl';
+import { Select } from 'panel/common/controls/Select';
+import { type IOption } from 'panel/lib/helpers/utils';
+import s from './SortSelect.module.pcss';
+
+type Props = {
+ value: 'asc' | 'desc';
+ onChange: (value: 'asc' | 'desc') => void;
+ class?: string;
+};
+
+export const SortSelect = (props: Props) => {
+ const options: IOption[] = [
+ { value: 'asc', label: intl.getMessage('sort_asc') },
+ { value: 'desc', label: intl.getMessage('sort_desc') },
+ ];
+
+ return (
+
+
+ options={options}
+ value={options.find((o) => o.value === props.value)}
+ onChange={(option: any) => props.onChange(option.value as 'asc' | 'desc')}
+ height="medium"
+ isSearchable={false}
+ />
+
+ );
+};
diff --git a/client_v2/src/common/ui/SortSelect/index.ts b/client_v2/src/common/ui/SortSelect/index.ts
new file mode 100644
index 00000000000..fdadd149736
--- /dev/null
+++ b/client_v2/src/common/ui/SortSelect/index.ts
@@ -0,0 +1 @@
+export { SortSelect } from './SortSelect';
diff --git a/client_v2/src/common/ui/Table/Table.module.pcss b/client_v2/src/common/ui/Table/Table.module.pcss
new file mode 100644
index 00000000000..11d120be83d
--- /dev/null
+++ b/client_v2/src/common/ui/Table/Table.module.pcss
@@ -0,0 +1,137 @@
+.tableContainer {
+ flex: 1 0 auto;
+ display: flex;
+ flex-direction: column;
+}
+
+.tableMain {
+ flex: 1 0 auto;
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+}
+
+.table {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+
+ @media (min-width: 768px) {
+ gap: 0;
+ }
+}
+
+.tableHeader {
+ display: none;
+
+ @media (min-width: 768px) {
+ display: grid;
+ grid-template-columns: var(--table-columns);
+ background-color: var(--page-background-additional);
+ border-radius: 8px;
+ overflow: hidden;
+ }
+}
+
+.tableCell {
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+
+ &.sortable {
+ cursor: pointer;
+ }
+
+ &.fitContent {
+ width: fit-content;
+ }
+}
+
+.tableHeaderCell {
+ @media (min-width: 768px) {
+ min-height: 48px;
+ padding: 12px;
+ }
+}
+
+.tableBodyCell {
+ @media (min-width: 768px) {
+ min-height: 56px;
+ padding: 12px;
+ align-items: flex-start;
+ }
+}
+
+.tableRow {
+ display: flex;
+ flex-direction: column;
+ border-radius: 8px;
+ padding: 8px 16px;
+ background-color: var(--default-cards-background);
+ box-shadow: 8px 8px 24px 0 rgba(0, 0, 0, 0.1);
+ transition: background-color var(--t2);
+
+ @media (min-width: 768px) {
+ display: grid;
+ grid-template-columns: var(--table-columns);
+ border-radius: 0;
+ padding: 0;
+ background-color: transparent;
+ box-shadow: none;
+
+ &:hover:not(:has([data-table-action]:hover)),
+ &:focus:not(:has([data-table-action]:focus-visible)) {
+ background-color: var(--page-background-additional);
+ }
+ }
+
+ &:last-child {
+ @media (min-width: 768px) {
+ border-radius: 0 0 8px 8px;
+ overflow: hidden;
+ }
+ }
+}
+
+.clickableCell {
+ cursor: pointer;
+}
+
+.sortAsc {
+ transform: rotate(90deg);
+}
+
+.sortDesc {
+ transform: rotate(-90deg);
+}
+
+.tablePagination {
+ margin-top: auto;
+}
+
+.emptyTableWrapper {
+ flex: 1 0 auto;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 24px 16px;
+
+ @media (min-width: 768px) {
+ padding: 48px 64px;
+ }
+}
+
+.loading {
+ flex: 1 0 auto;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 200px;
+}
+
+.tableLoader {
+ width: 48px;
+ height: 48px;
+ color: var(--default-product-icon);
+}
diff --git a/client_v2/src/common/ui/Table/Table.tsx b/client_v2/src/common/ui/Table/Table.tsx
new file mode 100644
index 00000000000..df040f32933
--- /dev/null
+++ b/client_v2/src/common/ui/Table/Table.tsx
@@ -0,0 +1,331 @@
+import { type JSX, createMemo, For, Show, untrack } from 'solid-js';
+import { createStore } from 'solid-js/store';
+import cn from 'clsx';
+
+import { Loader } from 'panel/common/ui/Loader';
+import theme from 'panel/lib/theme';
+import { Pagination } from './blocks/Pagination/Pagination';
+
+import s from './Table.module.pcss';
+
+import { Icon } from '../Icon';
+
+const DEFAULT_PAGE_SIZE_OPTIONS = [10, 20, 30, 40, 50];
+export const DEFAULT_PAGE_SIZE = DEFAULT_PAGE_SIZE_OPTIONS[0];
+
+export interface TableColumn {
+ key: string;
+ header: {
+ text: string;
+ class?: string;
+ render?: () => JSX.Element;
+ };
+ accessor?: keyof T | ((row: T) => any);
+ render?: (value: any, row: T, index: number) => JSX.Element;
+ sortable?: boolean;
+ sortFn?: (a: any, b: any) => number;
+ fitContent?: boolean;
+ width?: number | string;
+ minWidth?: number;
+ maxWidth?: number;
+ class?: string;
+}
+
+export interface TableProps {
+ data: T[];
+ columns: TableColumn[];
+ emptyTable?: JSX.Element;
+ loading?: boolean;
+ class?: string;
+ pagination?: boolean;
+ pageSize?: number;
+ onPageSizeChange?: (size: number) => void;
+ pageSizeOptions?: number[];
+ sortable?: boolean;
+ defaultSort?: {
+ key: string;
+ direction: 'asc' | 'desc';
+ };
+ onSortChange?: (key: string, direction: 'asc' | 'desc') => void;
+ getRowId?: (row: T, index: number) => string | number;
+ onRowClick?: (row: T) => void;
+ tableHeaderClass?: string;
+ tableRowClass?: string;
+}
+
+export const Table = >(props: TableProps) => {
+ const [state, setState] = createStore({
+ currentPage: 0,
+ pageSize: untrack(() => props.pageSize) ?? DEFAULT_PAGE_SIZE_OPTIONS[0],
+ sortKey: props.defaultSort?.key ?? (null as string | null),
+ sortDirection: props.defaultSort?.direction ?? ('asc' as 'asc' | 'desc'),
+ });
+
+ const sortedData = createMemo(() => {
+ const data = props.data;
+ const columns = props.columns;
+ const sortKey = state.sortKey;
+ const sortDirection = state.sortDirection;
+ const sortable = props.sortable ?? true;
+
+ if (!sortKey || !sortable) {
+ return data;
+ }
+
+ const column = columns.find((col) => col.key === sortKey);
+ if (!column || column.sortable === false) {
+ return data;
+ }
+
+ const sorted = [...data].sort((a, b) => {
+ let aValue: any;
+ let bValue: any;
+
+ if (typeof column.accessor === 'function') {
+ aValue = column.accessor(a);
+ bValue = column.accessor(b);
+ } else if (column.accessor) {
+ aValue = a[column.accessor];
+ bValue = b[column.accessor];
+ } else {
+ return 0;
+ }
+
+ if (aValue == null && bValue == null) return 0;
+ if (aValue == null) return 1;
+ if (bValue == null) return -1;
+
+ if (typeof aValue === 'string' && typeof bValue === 'string') {
+ aValue = aValue.toLowerCase();
+ bValue = bValue.toLowerCase();
+ }
+
+ let comparison: number;
+ if (column.sortFn) {
+ comparison = column.sortFn(aValue, bValue);
+ } else {
+ comparison = 0;
+ if (aValue < bValue) comparison = -1;
+ else if (aValue > bValue) comparison = 1;
+ }
+
+ return sortDirection === 'desc' ? -comparison : comparison;
+ });
+ return sorted;
+ });
+
+ const paginatedData = createMemo(() => {
+ const pagination = props.pagination ?? true;
+ if (!pagination) {
+ return sortedData();
+ }
+ const startIndex = state.currentPage * state.pageSize;
+ return sortedData().slice(startIndex, startIndex + state.pageSize);
+ });
+
+ const totalPages = () => Math.ceil(sortedData().length / state.pageSize);
+
+ const tableGridTemplate = createMemo(() =>
+ props.columns
+ .map((column) => {
+ if (typeof column.width === 'number') return `${column.width}px`;
+ if (typeof column.width === 'string') return column.width;
+ if (column.fitContent) return 'fit-content(100%)';
+ if (column.minWidth && column.maxWidth)
+ return `minmax(${column.minWidth}px, ${column.maxWidth}px)`;
+ if (column.minWidth) return `minmax(${column.minWidth}px, 1fr)`;
+ return 'minmax(0, 1fr)';
+ })
+ .join(' '),
+ );
+
+ const tableStyle = () =>
+ ({
+ '--table-columns': tableGridTemplate(),
+ }) as Record;
+
+ const handleSort = (columnKey: string) => {
+ const column = props.columns.find((col) => col.key === columnKey);
+ if (!column || column.sortable === false) return;
+
+ let newDirection: 'asc' | 'desc' = 'asc';
+ if (state.sortKey === columnKey && state.sortDirection === 'asc') {
+ newDirection = 'desc';
+ }
+
+ setState('sortKey', columnKey);
+ setState('sortDirection', newDirection);
+ setState('currentPage', 0);
+
+ props.onSortChange?.(columnKey, newDirection);
+ };
+
+ const handlePageChange = (page: number) => {
+ setState('currentPage', page);
+ };
+
+ const handlePageSizeChange = (newSize: number) => {
+ setState('pageSize', newSize);
+ setState('currentPage', 0);
+ props.onPageSizeChange?.(newSize);
+ };
+
+ const renderCell = (column: TableColumn, row: T, rowIndex: number) => {
+ if (column.render) {
+ let value = null;
+ if (typeof column.accessor === 'function') {
+ value = column.accessor(row);
+ } else if (column.accessor) {
+ value = row[column.accessor];
+ }
+ return column.render(value, row, rowIndex);
+ }
+
+ if (typeof column.accessor === 'function') {
+ return column.accessor(row) as JSX.Element;
+ }
+
+ if (column.accessor) {
+ const value = row[column.accessor];
+ return value != null ? String(value) : '';
+ }
+
+ return '';
+ };
+
+ const hasData = () => paginatedData().length > 0;
+
+ return (
+
+
+
+ }
+ >
+
+
+
+
+
+
+
+ {(row, index) => {
+ return (
+ props.onRowClick?.(row)}
+ >
+
+ {(column) => (
+
+ {renderCell(column, row, index())}
+
+ )}
+
+
+ );
+ }}
+
+
+
+
+
+ {props.emptyTable}
+
+
+
+
= DEFAULT_PAGE_SIZE}>
+
+
+
+
+ );
+};
diff --git a/client_v2/src/common/ui/Table/blocks/Pagination/Pagination.tsx b/client_v2/src/common/ui/Table/blocks/Pagination/Pagination.tsx
new file mode 100644
index 00000000000..7a0c46f2f91
--- /dev/null
+++ b/client_v2/src/common/ui/Table/blocks/Pagination/Pagination.tsx
@@ -0,0 +1,166 @@
+import { createSignal, createMemo, For } from 'solid-js';
+import cn from 'clsx';
+
+import { Icon } from 'panel/common/ui/Icon';
+import theme from 'panel/lib/theme';
+import { Dropdown } from 'panel/common/ui/Dropdown';
+import intl from 'panel/common/intl';
+
+type Props = {
+ currentPage: number;
+ totalPages: number;
+ pageSize: number;
+ totalItems: number;
+ pageSizeOptions: number[];
+ onPageChange: (page: number) => void;
+ onPageSizeChange: (size: number) => void;
+ limitButtonDescription?: string;
+};
+
+export const generatePageNumbers = (
+ currentPage: number,
+ totalPages: number,
+): (number | 'ellipsis')[] => {
+ const delta = 2;
+ const range: number[] = [];
+
+ range.push(1);
+
+ const start = Math.max(2, currentPage + 1 - delta);
+ const end = Math.min(totalPages - 1, currentPage + 1 + delta);
+
+ Array.from({ length: Math.max(0, end - start + 1) }, (_, i) => start + i).forEach((page) =>
+ range.push(page),
+ );
+
+ if (totalPages > 1) {
+ range.push(totalPages);
+ }
+
+ const uniqueRange = [...new Set(range)].sort((a, b) => a - b);
+
+ const rangeWithDots: (number | 'ellipsis')[] = [];
+ uniqueRange.forEach((page, index) => {
+ if (index > 0) {
+ const prevPage = uniqueRange[index - 1];
+ if (page - prevPage === 2) {
+ rangeWithDots.push(prevPage + 1);
+ } else if (page - prevPage > 2) {
+ rangeWithDots.push('ellipsis');
+ }
+ }
+ rangeWithDots.push(page);
+ });
+
+ return rangeWithDots;
+};
+
+export const Pagination = (props: Props) => {
+ const [limitMenuOpen, setLimitMenuOpen] = createSignal(false);
+
+ const canPreviousPage = () => props.currentPage > 0;
+ const canNextPage = () => props.currentPage < props.totalPages - 1;
+
+ const pageNumbers = createMemo(() => generatePageNumbers(props.currentPage, props.totalPages));
+
+ const limitMenu = (
+
+ );
+
+ const renderPages = () => {
+ if (props.totalPages <= 1) {
+ return null;
+ }
+
+ return (
+
+ );
+ };
+
+ return (
+
+ );
+};
diff --git a/client_v2/src/common/ui/Table/blocks/Pagination/index.ts b/client_v2/src/common/ui/Table/blocks/Pagination/index.ts
new file mode 100644
index 00000000000..0a1fd4dad6c
--- /dev/null
+++ b/client_v2/src/common/ui/Table/blocks/Pagination/index.ts
@@ -0,0 +1 @@
+export { Pagination } from './Pagination';
diff --git a/client_v2/src/common/ui/Table/index.ts b/client_v2/src/common/ui/Table/index.ts
new file mode 100644
index 00000000000..e2408e402af
--- /dev/null
+++ b/client_v2/src/common/ui/Table/index.ts
@@ -0,0 +1,3 @@
+export { Table } from './Table';
+export { Pagination } from './blocks/Pagination';
+export type { TableProps, TableColumn } from './Table';
diff --git a/client_v2/src/common/ui/Tabs/Tabs.module.pcss b/client_v2/src/common/ui/Tabs/Tabs.module.pcss
new file mode 100644
index 00000000000..c631db62bd3
--- /dev/null
+++ b/client_v2/src/common/ui/Tabs/Tabs.module.pcss
@@ -0,0 +1,66 @@
+.tabs {
+ width: 100%;
+}
+
+.nav {
+ display: flex;
+}
+
+.button {
+ padding: 16px;
+ font-size: 16px;
+ background: none;
+ border: none;
+ border-bottom: 3px solid transparent;
+ color: var(--default-main-text);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ transition: all 0.2s ease;
+
+ &:hover {
+ background-color: var(--card-hover-color);
+ }
+
+ &_active {
+ border-bottom-color: var(--default-main-button);
+ color: var(--default-main-button);
+ }
+}
+
+.icon {
+ width: 20px;
+ height: 20px;
+}
+
+.content {
+ padding: 12px 16px 0;
+ max-width: 584px;
+}
+
+.tabs_filled {
+ .button {
+ display: block;
+ border-radius: 8px;
+ border-bottom: none;
+ padding: 8px 16px;
+ min-height: 40px;
+ margin-bottom: 0;
+ min-width: 0;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+
+ &_active {
+ background-color: var(--page-background-additional);
+ color: var(--default-main-text);
+ border-bottom-color: transparent;
+ font-weight: var(--weight-semi-bold);
+ }
+ }
+}
+
+.content_fullWidth {
+ max-width: 100%;
+}
diff --git a/client_v2/src/common/ui/Tabs/Tabs.tsx b/client_v2/src/common/ui/Tabs/Tabs.tsx
new file mode 100644
index 00000000000..dfba1c54249
--- /dev/null
+++ b/client_v2/src/common/ui/Tabs/Tabs.tsx
@@ -0,0 +1,79 @@
+import { type JSX, createSignal, createMemo, For, Show, untrack } from 'solid-js';
+import cn from 'clsx';
+
+import { Icon } from '../Icon/Icon';
+import { IconType } from '../Icons';
+import s from './Tabs.module.pcss';
+
+type TabItem = {
+ id: string;
+ label: string;
+ content: JSX.Element;
+ icon?: IconType;
+};
+
+type Props = {
+ tabs: TabItem[];
+ defaultActiveTab?: string;
+ activeTab?: string;
+ onTabChange?: (tabId: string) => void;
+ class?: string;
+ contentClass?: string;
+ fullWidth?: boolean;
+ variant?: 'default' | 'filled';
+};
+
+export const Tabs = (props: Props) => {
+ const [internalActiveTab, setInternalActiveTab] = createSignal(
+ untrack(() => props.defaultActiveTab || props.tabs[0]?.id || ''),
+ );
+
+ const activeTab = createMemo(() =>
+ props.activeTab !== undefined ? props.activeTab : internalActiveTab(),
+ );
+
+ const handleTabClick = (tabId: string) => {
+ if (props.activeTab === undefined) {
+ setInternalActiveTab(tabId);
+ }
+ props.onTabChange?.(tabId);
+ };
+
+ const activeTabContent = createMemo(
+ () => props.tabs.find((tab) => tab.id === activeTab())?.content,
+ );
+
+ return (
+
+
+
+ {(tab) => (
+ handleTabClick(tab.id)}
+ >
+
+
+
+ {tab.label}
+
+ )}
+
+
+
+ {activeTabContent()}
+
+
+ );
+};
diff --git a/client_v2/src/common/ui/Tabs/index.ts b/client_v2/src/common/ui/Tabs/index.ts
new file mode 100644
index 00000000000..941e6baba57
--- /dev/null
+++ b/client_v2/src/common/ui/Tabs/index.ts
@@ -0,0 +1 @@
+export { Tabs } from './Tabs';
diff --git a/client_v2/src/components/App/index.tsx b/client_v2/src/components/App/index.tsx
new file mode 100644
index 00000000000..10eade817bc
--- /dev/null
+++ b/client_v2/src/components/App/index.tsx
@@ -0,0 +1,162 @@
+import { createEffect, onMount, onCleanup, Show } from 'solid-js';
+import { HashRouter, Route, Navigate } from '@solidjs/router';
+
+import { Sidebar } from 'panel/common/ui/Sidebar';
+import { Icons } from 'panel/common/ui/Icons';
+import { Footer } from 'panel/common/ui/Footer';
+import { Header } from 'panel/common/ui/Header';
+import { Banners } from 'panel/common/ui/Banners';
+import { Settings } from 'panel/components/Settings';
+import intl, { LocalesType } from 'panel/common/intl';
+import { Encryption } from 'panel/components/Encryption';
+import { Blocklists } from 'panel/components/FilterLists/Blocklists';
+import { LOCAL_STORAGE_KEYS, LocalStorageHelper } from 'panel/helpers/localStorageHelper';
+
+import { Allowlists } from 'panel/components/FilterLists/Allowlists';
+import { DNSRewrites } from 'panel/components/FilterLists/DNSRewrites';
+import { SetupGuide } from 'panel/components/SetupGuide';
+import { Dashboard } from 'panel/components/Dashboard';
+import { Dhcp } from 'panel/components/Dhcp';
+import { LeasesPage } from 'panel/components/Dhcp/LeasesPage';
+import { QueryLog } from 'panel/components/QueryLog';
+import Toasts from '../Toasts';
+import { THEMES } from '../../helpers/constants';
+import { setHtmlLangAttr, setUITheme } from '../../helpers/helpers';
+import { getDnsStatus, getTimerStatus, dashboardState } from '../../stores/dashboard';
+
+import s from './styles.module.pcss';
+import { DnsSettings } from '../DnsSettings';
+import { PrivateReverse } from '../DnsSettings/PrivateReverse';
+import { UserRules } from '../UserRules';
+import { BlockedServices } from '../BlockedServices';
+import { Clients } from '../Clients/Clients';
+import { InactivitySchedule } from '../BlockedServices/InactivitySchedule';
+import { AddClient } from '../Clients/AddClient';
+import { Protection } from '../Clients/AddClient/blocks/Protection/Protection';
+import { ClientBlockedServices } from '../Clients/AddClient/blocks/ClientBlockedServices';
+import { ClientSchedule } from '../Clients/AddClient/blocks/ClientSchedule';
+
+const SetupGuideRoute = () => ;
+const BlockedServicesRoute = () => ;
+const InactivityScheduleRoute = () => ;
+const ClientScheduleRoute = () => ;
+const ClientBlockedServicesRoute = () => ;
+const ProtectionRoute = () => ;
+const AddClientRoute = () => ;
+
+const App = () => {
+ onMount(() => {
+ getDnsStatus();
+
+ const handleVisibilityChange = () => {
+ if (document.visibilityState === 'visible') {
+ getTimerStatus();
+ }
+ };
+
+ document.addEventListener('visibilitychange', handleVisibilityChange);
+
+ onCleanup(() => {
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
+ });
+ });
+
+ // React to language changes
+ createEffect(() => {
+ const language = dashboardState.language;
+ const processing = dashboardState.processing;
+ if (!processing && language) {
+ intl.changeLanguage(language as LocalesType);
+ setHtmlLangAttr(language);
+ LocalStorageHelper.setItem(LOCAL_STORAGE_KEYS.LANGUAGE, language);
+ }
+ });
+
+ // React to theme changes
+ createEffect(() => {
+ const theme = dashboardState.theme;
+
+ if (!theme) return;
+
+ if (theme !== THEMES.auto) {
+ setUITheme(theme);
+ return;
+ }
+
+ const colorSchemeMedia = window.matchMedia('(prefers-color-scheme: dark)');
+ setUITheme(theme);
+
+ const handleChange = (e: MediaQueryListEvent) => {
+ if (e.matches) {
+ setUITheme(THEMES.dark);
+ } else {
+ setUITheme(THEMES.light);
+ }
+ };
+
+ colorSchemeMedia.addEventListener('change', handleChange);
+ onCleanup(() => {
+ colorSchemeMedia.removeEventListener('change', handleChange);
+ });
+ });
+
+ return (
+ (
+ <>
+
+
+
+
+
+
+
+
+ {props.children}
+
+
+
+
+
+
+
+
+ >
+ )}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ } />
+
+ );
+};
+
+export default App;
diff --git a/client_v2/src/components/App/styles.module.pcss b/client_v2/src/components/App/styles.module.pcss
new file mode 100644
index 00000000000..44efd8e4040
--- /dev/null
+++ b/client_v2/src/components/App/styles.module.pcss
@@ -0,0 +1,22 @@
+.wrapper {
+ display: block;
+
+ @media (min-width: 1200px) {
+ display: flex;
+ flex-direction: row;
+ align-items: flex-start;
+ }
+}
+
+.bodyWrapper {
+ width: 100%;
+ min-width: 0;
+ min-height: calc(100vh - 64px);
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+
+ @media (min-width: 1200px) {
+ min-height: 100vh;
+ }
+}
diff --git a/client_v2/src/components/BlockedServices/BlockedServices.module.pcss b/client_v2/src/components/BlockedServices/BlockedServices.module.pcss
new file mode 100644
index 00000000000..b1ba7d2efbf
--- /dev/null
+++ b/client_v2/src/components/BlockedServices/BlockedServices.module.pcss
@@ -0,0 +1,188 @@
+.header {
+ margin-bottom: 8px;
+}
+
+.breadcrumbs {
+ padding: 0 16px;
+}
+
+.title {
+ font-size: 32px;
+ font-weight: 700;
+ line-height: 1.2;
+ color: var(--default-main-text);
+ margin-bottom: 8px;
+}
+
+.clientsTitle {
+ padding-top: 24px;
+
+ @media (min-width: 768px) {
+ padding-top: 48px;
+ }
+}
+
+.description {
+ margin: 0;
+ font-size: 16px;
+ line-height: 1.5;
+ color: var(--default-description-text);
+ padding: 12px 16px;
+
+ @media (min-width: 768px) {
+ padding: 16px;
+ }
+}
+
+.navItem {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ padding: 16px;
+ cursor: pointer;
+ transition: background-color 0.2s;
+ text-decoration: none;
+ border-radius: 8px;
+
+ &:hover,
+ &:focus {
+ background-color: var(--hovered-page-background);
+ }
+}
+
+.navItemContent {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.navItemTitle {
+ font-size: 16px;
+ font-weight: 600;
+ line-height: 1.3;
+ color: var(--default-main-text);
+}
+
+.navItemDesc {
+ font-size: 14px;
+ line-height: 1.3;
+ color: var(--default-description-text);
+}
+
+.search {
+ padding: 16px 16px 8px;
+}
+
+.searchIcon {
+ margin-left: 8px;
+}
+
+.groups {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px;
+ padding: 8px 16px 16px;
+}
+
+.groupButton {
+ min-height: 33px;
+ padding: 4px 16px;
+ font-size: 14px;
+ color: var(--default-main-text);
+ border: 1px solid var(--default-inactive-input-stroke);
+ border-radius: 200px;
+ background-color: transparent;
+ transition:
+ background-color 0.2s,
+ border-color 0.2s;
+ cursor: pointer;
+ outline: none;
+
+ &:hover,
+ &:focus {
+ background-color: var(--page-background-additional);
+ }
+}
+
+.groupButtonActive {
+ border-color: var(--default-link);
+
+ &:hover,
+ &:focus {
+ background-color: transparent;
+ }
+}
+
+.servicesList {
+ display: flex;
+ flex-direction: column;
+}
+
+.serviceRow {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ padding: 12px 16px;
+ cursor: pointer;
+ transition: background-color 0.2s;
+ border-radius: 8px;
+
+ &:hover,
+ &:focus {
+ background-color: var(--hovered-page-background);
+ }
+}
+
+.serviceIcon {
+ width: 24px;
+ height: 24px;
+ flex-shrink: 0;
+
+ & svg {
+ width: 100%;
+ height: 100%;
+ }
+}
+
+.serviceName {
+ flex: 1;
+ font-size: 16px;
+ font-weight: 400;
+ line-height: 1.3;
+ color: var(--default-main-text);
+}
+
+.nothingFound {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 48px 16px;
+ gap: 16px;
+ font-size: 14px;
+ color: var(--default-description-text);
+}
+
+.nothingFoundIcon {
+ width: 64px;
+ height: 64px;
+}
+
+.clearButton {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ margin-right: 8px;
+ border: none;
+ background: none;
+ cursor: pointer;
+ color: var(--default-gray-icons);
+}
+
+.switchWrap {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+}
diff --git a/client_v2/src/components/BlockedServices/BlockedServices.tsx b/client_v2/src/components/BlockedServices/BlockedServices.tsx
new file mode 100644
index 00000000000..491258609ca
--- /dev/null
+++ b/client_v2/src/components/BlockedServices/BlockedServices.tsx
@@ -0,0 +1,277 @@
+import { createSignal, createEffect, createMemo, onMount, Show, For } from 'solid-js';
+import cn from 'clsx';
+import intl from 'panel/common/intl';
+import { Input } from 'panel/common/controls/Input';
+import { Icon } from 'panel/common/ui/Icon';
+import { Link } from 'panel/common/ui/Link';
+import { updateClientFormField, clientFormState } from 'panel/stores/clientForm';
+import theme from 'panel/lib/theme';
+import {
+ getBlockedServices,
+ getAllBlockedServices,
+ updateBlockedServices,
+ servicesState,
+} from 'panel/stores/services';
+
+import { Breadcrumbs } from 'panel/common/ui/Breadcrumbs';
+import { GroupFilter } from './GroupFilter';
+import { ServiceRow } from './ServiceRow';
+import { NothingFound } from './NothingFound';
+
+import s from './BlockedServices.module.pcss';
+import { RoutePath, type RoutePathKey } from '../Routes/Paths';
+
+type WebService = {
+ id: string;
+ name: string;
+ icon_svg: string;
+ group_id: string;
+ rules: string[];
+};
+
+type Props = {
+ clientScope?: boolean;
+ class?: string;
+ breadcrumbs?: {
+ parentLinks: {
+ path: RoutePathKey;
+ title: string;
+ props?: Partial>;
+ }[];
+ currentTitle: string;
+ };
+};
+
+export const BlockedServices = (props: Props) => {
+ const [search, setSearch] = createSignal('');
+ const [groupFilter, setGroupFilter] = createSignal([]);
+ const [togglingId, setTogglingId] = createSignal(null);
+
+ onMount(() => {
+ if (!props.clientScope) {
+ getBlockedServices();
+ getAllBlockedServices();
+ } else {
+ getAllBlockedServices();
+ }
+ });
+
+ createEffect(() => {
+ if (!servicesState.processingSet) {
+ setTogglingId(null);
+ }
+ });
+
+ const blockedSet = createMemo(() => {
+ if (props.clientScope) {
+ return new Set(clientFormState.blocked_services || []);
+ }
+ return new Set(servicesState.list?.ids || []);
+ });
+
+ const serviceGroupMap = createMemo(() => {
+ const map = new Map();
+ const allServices = servicesState.allServices;
+ if (!allServices || allServices.length === 0) {
+ return map;
+ }
+ (allServices as WebService[]).forEach((service) => {
+ if (service.group_id) {
+ map.set(service.id, service.group_id);
+ }
+ });
+ return map;
+ });
+
+ const filteredServices = createMemo(() => {
+ const allServices = servicesState.allServices;
+ if (!allServices || allServices.length === 0) {
+ return [];
+ }
+ let filtered = [...(allServices as WebService[])];
+ const gf = groupFilter();
+ if (gf.length > 0) {
+ const selected = new Set(gf);
+ const sgMap = serviceGroupMap();
+ filtered = filtered.filter((service) => {
+ const groupId = sgMap.get(service.id);
+ return groupId && selected.has(groupId);
+ });
+ }
+ const term = search().trim().toLowerCase();
+ if (term) {
+ filtered = filtered.filter(
+ (service) =>
+ service.name.toLowerCase().includes(term) ||
+ service.id.toLowerCase().includes(term),
+ );
+ }
+ filtered.sort((a, b) => a.name.localeCompare(b.name));
+ return filtered;
+ });
+
+ const handleToggleGroup = (groupId: string) => {
+ setGroupFilter((current) =>
+ current.includes(groupId)
+ ? current.filter((g) => g !== groupId)
+ : [...current, groupId],
+ );
+ };
+
+ const handleToggleService = (serviceId: string, checked: boolean) => {
+ if (props.clientScope) {
+ const currentIds: string[] = clientFormState.blocked_services || [];
+ const newIds = checked
+ ? [...currentIds, serviceId]
+ : currentIds.filter((id: string) => id !== serviceId);
+ updateClientFormField('blocked_services', newIds);
+ return;
+ }
+ setTogglingId(serviceId);
+ const currentIds = servicesState.list?.ids || [];
+ const newIds = checked
+ ? [...currentIds, serviceId]
+ : currentIds.filter((id: string) => id !== serviceId);
+ updateBlockedServices({ ids: newIds, schedule: servicesState.list?.schedule });
+ };
+
+ const handleSearchChange = (e: Event) => {
+ setSearch((e.target as HTMLInputElement).value);
+ };
+
+ const handleSearchClear = () => {
+ setSearch('');
+ };
+
+ const isInitialLoading = () =>
+ (servicesState.allServices == null || servicesState.allServices.length === 0) &&
+ (servicesState.processingAll || servicesState.processing);
+ const isGloballyDisabled = () =>
+ props.clientScope ? clientFormState.use_global_blocked_services : false;
+
+ const getScheduleRoute = () => {
+ if (!props.clientScope) {
+ return RoutePath.InactivitySchedule;
+ }
+ return clientFormState.mode === 'edit'
+ ? RoutePath.ClientsEditSchedule
+ : RoutePath.ClientsSchedule;
+ };
+
+ const scheduleRouteProps = () =>
+ props.clientScope && clientFormState.mode === 'edit'
+ ? { clientName: encodeURIComponent(clientFormState.originalName) }
+ : undefined;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {intl.getMessage('blocked_services')}
+
+ {intl.getMessage('blocked_services_desc')}
+
+
+
+
+
+ {intl.getMessage('inactivity_schedule')}
+
+
+ {intl.getMessage('inactivity_schedule_desc')}
+
+
+
+
+
+
+ }
+ suffixIcon={
+
+
+
+
+
+ }
+ />
+
+
+
+
+
+
+ {(service) => (
+
+ )}
+
+ }
+ >
+
+
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/BlockedServices/GroupFilter.tsx b/client_v2/src/components/BlockedServices/GroupFilter.tsx
new file mode 100644
index 00000000000..f9819875e61
--- /dev/null
+++ b/client_v2/src/components/BlockedServices/GroupFilter.tsx
@@ -0,0 +1,76 @@
+import { Show, For } from 'solid-js';
+import cn from 'clsx';
+import intl from 'panel/common/intl';
+
+import s from './BlockedServices.module.pcss';
+
+interface ServiceGroup {
+ id: string;
+}
+
+type Props = {
+ groups: ServiceGroup[];
+ activeGroups: string[];
+ onToggleGroup: (groupId: string) => void;
+};
+
+const getGroupName = (id: string): string => {
+ switch (id) {
+ case 'ai':
+ return intl.getMessage('parental_group_ai');
+ case 'cdn':
+ return intl.getMessage('parental_group_cdn');
+ case 'dating':
+ return intl.getMessage('parental_group_dating');
+ case 'gambling':
+ return intl.getMessage('parental_group_gambling');
+ case 'gaming':
+ return intl.getMessage('parental_group_gaming');
+ case 'messaging':
+ return intl.getMessage('parental_group_messaging');
+ case 'privacy':
+ return intl.getMessage('parental_group_privacy');
+ case 'shopping':
+ return intl.getMessage('parental_group_shopping');
+ case 'social':
+ return intl.getMessage('parental_group_social');
+ case 'development':
+ return intl.getMessage('parental_group_development');
+ case 'streaming':
+ return intl.getMessage('parental_group_streaming');
+ case 'hosting':
+ return intl.getMessage('parental_group_hosting');
+ case 'messenger':
+ return intl.getMessage('parental_group_messenger');
+ case 'social_network':
+ return intl.getMessage('parental_group_social_network');
+ case 'software':
+ return intl.getMessage('parental_group_software');
+ default:
+ return id;
+ }
+};
+
+export const GroupFilter = (props: Props) => {
+ return (
+ 0}>
+
+
+ {(group) => (
+ props.onToggleGroup(group.id)}
+ aria-pressed={props.activeGroups.includes(group.id)}
+ data-testid={`blocked-services-group-${group.id}`}
+ class={cn(s.groupButton, {
+ [s.groupButtonActive]: props.activeGroups.includes(group.id),
+ })}
+ >
+ {getGroupName(group.id)}
+
+ )}
+
+
+
+ );
+};
diff --git a/client_v2/src/components/BlockedServices/InactivitySchedule/InactivitySchedule.module.pcss b/client_v2/src/components/BlockedServices/InactivitySchedule/InactivitySchedule.module.pcss
new file mode 100644
index 00000000000..5d7e1dd04b4
--- /dev/null
+++ b/client_v2/src/components/BlockedServices/InactivitySchedule/InactivitySchedule.module.pcss
@@ -0,0 +1,145 @@
+.container {
+ padding-top: 0;
+}
+
+.title {
+ padding-top: 40px;
+
+ @media (min-width: 768px) {
+ padding-top: 48px;
+ }
+}
+
+.breadcrumbs {
+ padding: 0 16px;
+}
+
+.timezoneWrapper {
+ padding: 16px;
+
+ @media (min-width: 768px) {
+ padding: 24px 16px;
+ }
+}
+
+.timezoneWrapperClientScope {
+ padding-top: 0;
+
+ @media (min-width: 768px) {
+ padding-top: 0;
+ }
+}
+
+.timezoneLabel {
+ font-size: 14px;
+ color: var(--default-description-text);
+ margin-bottom: 8px;
+}
+
+.scheduleList {
+ display: flex;
+ flex-direction: column;
+}
+
+.scheduleRow {
+ display: grid;
+ grid-template-columns: 1fr 1fr 56px;
+ align-items: center;
+ min-height: 48px;
+ padding: 0 16px;
+
+ @media (min-width: 768px) {
+ grid-template-columns: 1fr 1fr 56px 56px;
+ }
+}
+
+.dayName {
+ font-size: 16px;
+ font-weight: 400;
+ line-height: 42px;
+ color: var(--default-main-text);
+}
+
+.timeDisplay {
+ font-size: 16px;
+ font-weight: 400;
+ line-height: 42px;
+ color: var(--default-main-text);
+}
+
+.noScheduleTextDesktop {
+ display: none;
+ font-size: 16px;
+ line-height: 42px;
+ color: var(--default-description-text);
+
+ @media (min-width: 768px) {
+ display: inline;
+ }
+}
+
+.noScheduleTextMobile {
+ display: inline;
+ font-size: 16px;
+ line-height: 42px;
+ color: var(--default-description-text);
+
+ @media (min-width: 768px) {
+ display: none;
+ }
+}
+
+.actions {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 56px;
+}
+
+.actionButton {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 32px;
+ height: 32px;
+ padding: 0;
+ border: none;
+ background: none;
+ border-radius: 4px;
+ cursor: pointer;
+ color: var(--default-gray-icons);
+ transition: background-color 0.2s;
+
+ &:hover,
+ &:focus {
+ background-color: var(--hovered-page-background);
+ }
+}
+
+.actionButtonAdd {
+ color: var(--default-link);
+}
+
+.actionButtonDelete {
+ color: var(--default-error-icon);
+}
+
+.actionsDesktop {
+ display: none;
+
+ @media (min-width: 768px) {
+ display: flex;
+ }
+}
+
+.actionsMobile {
+ display: flex;
+
+ @media (min-width: 768px) {
+ display: none;
+ }
+}
+
+.dropdownItemDanger {
+ color: var(--default-error-icon);
+}
diff --git a/client_v2/src/components/BlockedServices/InactivitySchedule/InactivitySchedule.tsx b/client_v2/src/components/BlockedServices/InactivitySchedule/InactivitySchedule.tsx
new file mode 100644
index 00000000000..ae815224ad7
--- /dev/null
+++ b/client_v2/src/components/BlockedServices/InactivitySchedule/InactivitySchedule.tsx
@@ -0,0 +1,319 @@
+import { createSignal, createMemo, onMount, Show, For } from 'solid-js';
+import { getAllTimezones, type Timezone } from 'countries-and-timezones';
+import cn from 'clsx';
+import intl from 'panel/common/intl';
+import { Breadcrumbs } from 'panel/common/ui/Breadcrumbs';
+import { ConfirmDialog } from 'panel/common/ui/ConfirmDialog';
+import { PageLoader } from 'panel/common/ui/Loader';
+import { Select } from 'panel/common/controls/Select';
+import { updateClientFormField, clientFormState } from 'panel/stores/clientForm';
+import { getBlockedServices, updateBlockedServices, servicesState } from 'panel/stores/services';
+import theme from 'panel/lib/theme';
+
+import { RoutePath } from 'panel/components/Routes/Paths';
+import { buildClientBreadcrumbs } from 'panel/helpers/buildClientBreadcrumbs';
+import { ScheduleRow } from './ScheduleRow';
+import { ScheduleModal } from './ScheduleModal';
+import { type DayKey, DAYS_OF_WEEK, type ScheduleData, type ScheduleDayData } from './helpers';
+import { getDayName } from './getDayName';
+import s from './InactivitySchedule.module.pcss';
+
+const timezones = getAllTimezones();
+
+const TIMEZONE_OPTIONS = [
+ { label: 'Local', value: 'Local' },
+ { label: 'UTC (GMT+00:00)', value: 'UTC' },
+ ...Object.entries(timezones).map(([tz, data]: [string, Timezone]) => ({
+ label: `${tz} (GMT${data.utcOffsetStr})`,
+ value: tz,
+ })),
+];
+
+type Props = {
+ clientScope?: boolean;
+};
+
+export const InactivitySchedule = (props: Props) => {
+ const [modalVisible, setModalVisible] = createSignal(false);
+ const [editDay, setEditDay] = createSignal();
+ const [deleteDay, setDeleteDay] = createSignal(null);
+
+ onMount(() => {
+ if (!props.clientScope && !servicesState.list?.ids) {
+ getBlockedServices();
+ }
+ });
+
+ const schedule = createMemo(() => {
+ return props.clientScope
+ ? (clientFormState.blocked_services_schedule as unknown as ScheduleData)
+ : servicesState.list?.schedule;
+ });
+
+ const currentTimezone = () => schedule()?.time_zone;
+
+ const timezoneValue = createMemo(() => {
+ const tz = currentTimezone();
+ if (!tz) return undefined;
+ return (
+ TIMEZONE_OPTIONS.find((opt) => opt.value === tz) || {
+ label: tz,
+ value: tz,
+ }
+ );
+ });
+
+ const isLoading = createMemo(() => {
+ if (props.clientScope) return false;
+ return servicesState.processing && !servicesState.list?.ids;
+ });
+
+ const handleTimezoneChange = (option: { value: string }) => {
+ if (!option) {
+ return;
+ }
+ const newSchedule = props.clientScope
+ ? { ...(clientFormState.blocked_services_schedule as any), time_zone: option.value }
+ : { ...schedule(), time_zone: option.value };
+ if (props.clientScope) {
+ updateClientFormField('blocked_services_schedule', newSchedule, true);
+ } else {
+ updateBlockedServices({ ids: servicesState.list?.ids || [], schedule: newSchedule });
+ }
+ };
+
+ const handleAdd = (day: DayKey) => {
+ setEditDay(day);
+ setModalVisible(true);
+ };
+
+ const handleEdit = (day: DayKey) => {
+ setEditDay(day);
+ setModalVisible(true);
+ };
+
+ const handleDelete = (day: DayKey) => {
+ setDeleteDay(day);
+ };
+
+ const confirmDelete = () => {
+ const dayToDelete = deleteDay();
+ if (!dayToDelete) {
+ return;
+ }
+ const newSchedule: Record = { time_zone: currentTimezone() };
+ DAYS_OF_WEEK.forEach((d) => {
+ if (d !== dayToDelete && schedule()?.[d]) {
+ newSchedule[d] = schedule()![d];
+ }
+ });
+ if (props.clientScope) {
+ updateClientFormField('blocked_services_schedule', newSchedule, true);
+ } else {
+ updateBlockedServices({ ids: servicesState.list?.ids || [], schedule: newSchedule });
+ }
+ setDeleteDay(null);
+ };
+
+ const handleModalSave = (day: DayKey, start: number, end: number) => {
+ const newSchedule: Record = { time_zone: currentTimezone() };
+ DAYS_OF_WEEK.forEach((d) => {
+ if (schedule()?.[d]) {
+ newSchedule[d] = schedule()![d];
+ }
+ });
+ newSchedule[day] = { start, end };
+ if (props.clientScope) {
+ updateClientFormField('blocked_services_schedule', newSchedule, true);
+ } else {
+ updateBlockedServices({ ids: servicesState.list?.ids || [], schedule: newSchedule });
+ }
+ setModalVisible(false);
+ setEditDay(undefined);
+ };
+
+ const handleModalClose = () => {
+ setModalVisible(false);
+ setEditDay(undefined);
+ };
+
+ const parentLinks = () =>
+ props.clientScope
+ ? buildClientBreadcrumbs(clientFormState, [
+ clientFormState.mode === 'edit'
+ ? {
+ path: RoutePath.ClientsEditBlockedServices,
+ title: intl.getMessage('blocked_services'),
+ props: { clientName: encodeURIComponent(clientFormState.originalName) },
+ }
+ : {
+ path: RoutePath.ClientsBlockedServices,
+ title: intl.getMessage('blocked_services'),
+ },
+ ])
+ : [
+ {
+ path: RoutePath.BlockedServices,
+ title: intl.getMessage('blocked_services'),
+ },
+ ];
+
+ return (
+
+
+
+ }
+ >
+
+
+
+
+
+
+
+ {intl.getMessage('inactivity_schedule')}
+
+
+
+
+ {intl.getMessage('inactivity_schedule_timezone')}
+
+
+
+
+
+
+ {(day) => (
+
+ )}
+
+
+
+
+
+
+
+
+ setDeleteDay(null)}
+ buttonVariant="danger"
+ />
+
+
+
+
+ }
+ >
+ {/* clientScope - render inline */}
+
+
{intl.getMessage('inactivity_schedule_timezone')}
+
+
+
+
+
+ {(day) => (
+
+ )}
+
+
+
+
+
+
+
+
+ setDeleteDay(null)}
+ buttonVariant="danger"
+ />
+
+
+ );
+};
diff --git a/client_v2/src/components/BlockedServices/InactivitySchedule/ScheduleModal.module.pcss b/client_v2/src/components/BlockedServices/InactivitySchedule/ScheduleModal.module.pcss
new file mode 100644
index 00000000000..441a42ca36b
--- /dev/null
+++ b/client_v2/src/components/BlockedServices/InactivitySchedule/ScheduleModal.module.pcss
@@ -0,0 +1,74 @@
+.content {
+ padding: 16px;
+}
+
+.dayLabel {
+ padding: 8px 0;
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--default-main-text);
+}
+
+.allDayRow {
+ padding: 16px 0;
+}
+
+.timeWrapper {
+ display: flex;
+ align-items: flex-start;
+ flex-direction: column;
+ gap: 16px;
+ padding: 8px 0;
+
+ @media (min-width: 768px) {
+ flex-direction: row;
+ }
+}
+
+.timeField {
+ flex: 1;
+ min-width: 0;
+ width: 100%;
+}
+
+.timeLabel {
+ display: block;
+ font-size: 14px;
+ color: var(--default-description-text);
+ margin-bottom: 8px;
+}
+
+.error {
+ font-size: 14px;
+ color: var(--default-error-icon);
+ margin-bottom: 16px;
+}
+
+.notice {
+ margin-top: 12px;
+ padding-left: 16px;
+ position: relative;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 3px;
+ height: 100%;
+ background-color: var(--default-attention-link);
+ border-radius: 4px;
+ }
+}
+
+.selects {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.selects :global(.solid-select) {
+ width: 100%;
+ flex: 1;
+ min-width: 0;
+}
diff --git a/client_v2/src/components/BlockedServices/InactivitySchedule/ScheduleModal.tsx b/client_v2/src/components/BlockedServices/InactivitySchedule/ScheduleModal.tsx
new file mode 100644
index 00000000000..486b637b496
--- /dev/null
+++ b/client_v2/src/components/BlockedServices/InactivitySchedule/ScheduleModal.tsx
@@ -0,0 +1,229 @@
+import { createSignal, createMemo, createEffect } from 'solid-js';
+import cn from 'clsx';
+import intl from 'panel/common/intl';
+import { Dialog } from 'panel/common/ui/Dialog';
+import { Button } from 'panel/common/ui/Button';
+import { Select } from 'panel/common/controls/Select';
+import { Checkbox } from 'panel/common/controls/Checkbox';
+import theme from 'panel/lib/theme';
+
+import {
+ type DayKey,
+ FULL_DAY_END_MS,
+ HOURS_OPTIONS,
+ MINUTES_OPTIONS,
+ type ScheduleDayData,
+ getEndTimeOptions,
+ getNormalizedEndTime,
+ isFullDay,
+ msToTime,
+ timeToMs,
+} from './helpers';
+import { getDayName } from './getDayName';
+import s from './ScheduleModal.module.pcss';
+
+type Props = {
+ visible: boolean;
+ currentDay?: DayKey;
+ currentData?: ScheduleDayData;
+ onClose: () => void;
+ onSave: (day: DayKey, start: number, end: number) => void;
+};
+
+export const ScheduleModal = (props: Props) => {
+ const [startHour, setStartHour] = createSignal(0);
+ const [startMinute, setStartMinute] = createSignal(0);
+ const [endHour, setEndHour] = createSignal(23);
+ const [endMinute, setEndMinute] = createSignal(59);
+ const [allDay, setAllDay] = createSignal(false);
+
+ createEffect(() => {
+ const data = props.currentData;
+ if (!data) {
+ setAllDay(false);
+ setStartHour(0);
+ setStartMinute(0);
+ setEndHour(23);
+ setEndMinute(59);
+ return;
+ }
+
+ const isFullDayData = isFullDay(data.start, data.end);
+ const start = msToTime(data.start);
+ const end = msToTime(data.end);
+ const normalizedEnd = getNormalizedEndTime(start, end) ?? end;
+
+ setAllDay(isFullDayData);
+ setStartHour(start.hours);
+ setStartMinute(start.minutes);
+ setEndHour(normalizedEnd.hours);
+ setEndMinute(normalizedEnd.minutes);
+ });
+
+ const handleAllDayChange = (e: Event) => {
+ const checked = (e.target as HTMLInputElement).checked;
+ setAllDay(checked);
+ if (checked) {
+ setStartHour(0);
+ setStartMinute(0);
+ setEndHour(23);
+ setEndMinute(59);
+ }
+ };
+
+ const applyStartTime = (nextStartHour: number, nextStartMinute: number) => {
+ setStartHour(nextStartHour);
+ setStartMinute(nextStartMinute);
+
+ if (allDay()) {
+ return;
+ }
+
+ const normalizedEnd = getNormalizedEndTime(
+ { hours: nextStartHour, minutes: nextStartMinute },
+ { hours: endHour(), minutes: endMinute() },
+ );
+
+ if (!normalizedEnd) {
+ return;
+ }
+
+ setEndHour(normalizedEnd.hours);
+ setEndMinute(normalizedEnd.minutes);
+ };
+
+ const applyEndTime = (nextEndHour: number, nextEndMinute: number) => {
+ const normalizedEnd = getNormalizedEndTime(
+ { hours: startHour(), minutes: startMinute() },
+ { hours: nextEndHour, minutes: nextEndMinute },
+ );
+
+ if (!normalizedEnd) {
+ return;
+ }
+
+ setEndHour(normalizedEnd.hours);
+ setEndMinute(normalizedEnd.minutes);
+ };
+
+ const endTimeOptions = createMemo(() =>
+ getEndTimeOptions({ hours: startHour(), minutes: startMinute() }, endHour()),
+ );
+
+ const isDisabled = () =>
+ !props.currentDay || (!allDay() && !endTimeOptions().hasAvailableEndTime);
+
+ const handleSave = () => {
+ if (isDisabled() || !props.currentDay) return;
+ const saveStart = allDay() ? 0 : timeToMs(startHour(), startMinute());
+ const saveEnd = allDay() ? FULL_DAY_END_MS : timeToMs(endHour(), endMinute());
+ props.onSave(props.currentDay, saveStart, saveEnd);
+ };
+
+ const dayLabel = () => getDayName(props.currentDay);
+ const title = () =>
+ props.currentData
+ ? intl.getMessage('inactivity_schedule_edit')
+ : intl.getMessage('inactivity_schedule_add');
+
+ return (
+
+
+
{dayLabel()}
+
+
+
+
+ {intl.getMessage('inactivity_schedule_time_from')}
+
+
+ applyStartTime(opt.value, startMinute())}
+ isDisabled={allDay()}
+ height="big"
+ size="responsive"
+ isSearchable={false}
+ />
+ applyStartTime(startHour(), opt.value)}
+ isDisabled={allDay()}
+ height="big"
+ size="responsive"
+ isSearchable={false}
+ />
+
+
+
+
+ {intl.getMessage('inactivity_schedule_time_to')}
+
+
+ applyEndTime(opt.value, endMinute())}
+ isDisabled={allDay()}
+ height="big"
+ size="responsive"
+ isSearchable={false}
+ />
+ applyEndTime(endHour(), opt.value)}
+ isDisabled={allDay()}
+ height="big"
+ size="responsive"
+ isSearchable={false}
+ />
+
+
+
+
+
+
+ {intl.getMessage('inactivity_schedule_all_day')}
+
+
+
+
+ {intl.getMessage('inactivity_schedule_replace_desc')}
+
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/BlockedServices/InactivitySchedule/ScheduleRow.tsx b/client_v2/src/components/BlockedServices/InactivitySchedule/ScheduleRow.tsx
new file mode 100644
index 00000000000..6e63090fdc8
--- /dev/null
+++ b/client_v2/src/components/BlockedServices/InactivitySchedule/ScheduleRow.tsx
@@ -0,0 +1,133 @@
+import { createSignal, Show } from 'solid-js';
+import cn from 'clsx';
+import intl from 'panel/common/intl';
+import { Icon } from 'panel/common/ui/Icon';
+import { Dropdown } from 'panel/common/ui/Dropdown';
+import theme from 'panel/lib/theme';
+
+import { DayKey, ScheduleDayData, formatTimePeriod, isFullDay } from './helpers';
+import { getDayName } from './getDayName';
+import s from './InactivitySchedule.module.pcss';
+
+type Props = {
+ day: DayKey;
+ data: ScheduleDayData | undefined;
+ onEdit: (day: DayKey) => void;
+ onDelete: (day: DayKey) => void;
+ onAdd: (day: DayKey) => void;
+};
+
+export const ScheduleRow = (props: Props) => {
+ const dayName = () => getDayName(props.day);
+ const isConfigured = () => !!props.data;
+ const [dropdownOpen, setDropdownOpen] = createSignal(false);
+
+ const handleEditClick = () => {
+ setDropdownOpen(false);
+ props.onEdit(props.day);
+ };
+
+ const handleDeleteClick = () => {
+ setDropdownOpen(false);
+ props.onDelete(props.day);
+ };
+
+ const getTimeDisplay = () => {
+ if (!props.data) {
+ return (
+ <>
+
+ {intl.getMessage('inactivity_schedule_no_schedules')}
+
+ –
+ >
+ );
+ }
+
+ if (isFullDay(props.data.start, props.data.end)) {
+ return intl.getMessage('inactivity_schedule_all_day');
+ }
+
+ return formatTimePeriod(props.data.start, props.data.end);
+ };
+
+ return (
+
+
{dayName()}
+
{getTimeDisplay()}
+
+
+
+ props.onAdd(props.day)}
+ aria-label={intl.getMessage('inactivity_schedule_add')}
+ data-testid={`schedule-row-${props.day}-add`}
+ >
+
+
+
+ >
+ }
+ >
+
+ props.onDelete(props.day)}
+ aria-label={intl.getMessage('delete_table_action')}
+ data-testid={`schedule-row-${props.day}-delete`}
+ >
+
+
+
+
+ props.onEdit(props.day)}
+ aria-label={intl.getMessage('inactivity_schedule_edit')}
+ data-testid={`schedule-row-${props.day}-edit`}
+ >
+
+
+
+
+
+ }
+ >
+
+
+
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/BlockedServices/InactivitySchedule/getDayName.ts b/client_v2/src/components/BlockedServices/InactivitySchedule/getDayName.ts
new file mode 100644
index 00000000000..6be7e8e4312
--- /dev/null
+++ b/client_v2/src/components/BlockedServices/InactivitySchedule/getDayName.ts
@@ -0,0 +1,24 @@
+import intl from 'panel/common/intl';
+
+import { DayKey } from './helpers';
+
+export const getDayName = (day: DayKey): string => {
+ switch (day) {
+ case 'mon':
+ return intl.getMessage('monday');
+ case 'tue':
+ return intl.getMessage('tuesday');
+ case 'wed':
+ return intl.getMessage('wednesday');
+ case 'thu':
+ return intl.getMessage('thursday');
+ case 'fri':
+ return intl.getMessage('friday');
+ case 'sat':
+ return intl.getMessage('saturday');
+ case 'sun':
+ return intl.getMessage('sunday');
+ default:
+ return day;
+ }
+};
diff --git a/client_v2/src/components/BlockedServices/InactivitySchedule/helpers.ts b/client_v2/src/components/BlockedServices/InactivitySchedule/helpers.ts
new file mode 100644
index 00000000000..3b3b51aca9e
--- /dev/null
+++ b/client_v2/src/components/BlockedServices/InactivitySchedule/helpers.ts
@@ -0,0 +1,120 @@
+export const FULL_DAY_END_MS = 86340000;
+
+export const DAYS_OF_WEEK = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] as const;
+
+export type DayKey = (typeof DAYS_OF_WEEK)[number];
+
+export type TimeValue = {
+ hours: number;
+ minutes: number;
+};
+
+export type ScheduleTimeOption = {
+ label: string;
+ value: number;
+ isDisabled?: boolean;
+};
+
+export type ScheduleDayData = {
+ start: number;
+ end: number;
+};
+
+export type ScheduleData = {
+ time_zone: string;
+ mon?: ScheduleDayData;
+ tue?: ScheduleDayData;
+ wed?: ScheduleDayData;
+ thu?: ScheduleDayData;
+ fri?: ScheduleDayData;
+ sat?: ScheduleDayData;
+ sun?: ScheduleDayData;
+};
+
+export const msToTime = (ms: number): TimeValue => {
+ const totalMinutes = Math.floor(ms / 60000);
+ const hours = Math.floor(totalMinutes / 60);
+ const minutes = totalMinutes % 60;
+ return { hours, minutes };
+};
+
+export const timeToMs = (hours: number, minutes: number): number => {
+ return (hours * 60 + minutes) * 60000;
+};
+
+export const padTime = (value: number): string => {
+ return value.toString().padStart(2, '0');
+};
+
+export const HOURS_OPTIONS: ScheduleTimeOption[] = Array.from({ length: 24 }, (_, i) => ({
+ label: padTime(i),
+ value: i,
+}));
+
+export const MINUTES_OPTIONS: ScheduleTimeOption[] = Array.from({ length: 60 }, (_, i) => ({
+ label: padTime(i),
+ value: i,
+}));
+
+export const formatTimePeriod = (startMs: number, endMs: number): string => {
+ const start = msToTime(startMs);
+ const end = msToTime(endMs);
+ return `${padTime(start.hours)}:${padTime(start.minutes)} \u2013 ${padTime(end.hours)}:${padTime(end.minutes)}`;
+};
+
+export const getNextTimeValue = ({ hours, minutes }: TimeValue): TimeValue | null => {
+ if (hours === 23 && minutes === 59) {
+ return null;
+ }
+
+ if (minutes === 59) {
+ return { hours: hours + 1, minutes: 0 };
+ }
+
+ return { hours, minutes: minutes + 1 };
+};
+
+export const getNormalizedEndTime = (start: TimeValue, end: TimeValue): TimeValue | null => {
+ const startMs = timeToMs(start.hours, start.minutes);
+ const endMs = timeToMs(end.hours, end.minutes);
+
+ if (endMs > startMs) {
+ return end;
+ }
+
+ return getNextTimeValue(start);
+};
+
+export const getEndTimeOptions = (
+ start: TimeValue,
+ selectedEndHour: number,
+): {
+ hours: ScheduleTimeOption[];
+ minutes: ScheduleTimeOption[];
+ hasAvailableEndTime: boolean;
+} => {
+ const hasAvailableEndTime = getNextTimeValue(start) !== null;
+
+ return {
+ hasAvailableEndTime,
+ hours: HOURS_OPTIONS.map((option) => ({
+ ...option,
+ isDisabled: !hasAvailableEndTime || option.value < start.hours,
+ })),
+ minutes: MINUTES_OPTIONS.map((option) => ({
+ ...option,
+ isDisabled:
+ !hasAvailableEndTime ||
+ selectedEndHour < start.hours ||
+ (selectedEndHour === start.hours && option.value <= start.minutes),
+ })),
+ };
+};
+
+export const isFullDay = (start: number, end: number): boolean => {
+ return start === 0 && end === FULL_DAY_END_MS;
+};
+
+export const getLocalTimezone = (): string => {
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
+};
diff --git a/client_v2/src/components/BlockedServices/InactivitySchedule/index.ts b/client_v2/src/components/BlockedServices/InactivitySchedule/index.ts
new file mode 100644
index 00000000000..c2ee75309a7
--- /dev/null
+++ b/client_v2/src/components/BlockedServices/InactivitySchedule/index.ts
@@ -0,0 +1 @@
+export { InactivitySchedule } from './InactivitySchedule';
diff --git a/client_v2/src/components/BlockedServices/NothingFound.tsx b/client_v2/src/components/BlockedServices/NothingFound.tsx
new file mode 100644
index 00000000000..826bc298ddc
--- /dev/null
+++ b/client_v2/src/components/BlockedServices/NothingFound.tsx
@@ -0,0 +1,13 @@
+import intl from 'panel/common/intl';
+import { Icon } from 'panel/common/ui/Icon';
+
+import s from './BlockedServices.module.pcss';
+
+export const NothingFound = () => {
+ return (
+
+
+ {intl.getMessage('nothing_found')}
+
+ );
+};
diff --git a/client_v2/src/components/BlockedServices/ServiceRow.tsx b/client_v2/src/components/BlockedServices/ServiceRow.tsx
new file mode 100644
index 00000000000..4c3ca0f2f43
--- /dev/null
+++ b/client_v2/src/components/BlockedServices/ServiceRow.tsx
@@ -0,0 +1,61 @@
+import { createMemo } from 'solid-js';
+import { Switch } from 'panel/common/controls/Switch';
+import { decodeSvg } from 'panel/helpers/helpers';
+
+import s from './BlockedServices.module.pcss';
+
+type Props = {
+ id: string;
+ name: string;
+ iconSvg: string;
+ checked: boolean;
+ disabled: boolean;
+ onChange: (id: string, checked: boolean) => void;
+};
+
+export const ServiceRow = (props: Props) => {
+ const handleChange = (e: Event) => {
+ const target = e.target as HTMLInputElement;
+ props.onChange(props.id, target.checked);
+ };
+
+ const handleRowClick = () => {
+ if (props.disabled) {
+ return;
+ }
+ props.onChange(props.id, !props.checked);
+ };
+
+ const decodedSvg = createMemo(() => decodeSvg(props.iconSvg));
+
+ return (
+ {
+ if (e.key === 'Enter' || e.key === ' ') {
+ handleRowClick();
+ }
+ }}
+ >
+ {/* eslint-disable-next-line solid/no-innerhtml */}
+
+
{props.name}
+
e.stopPropagation()}
+ role="presentation"
+ >
+
+
+
+ );
+};
diff --git a/client_v2/src/components/BlockedServices/index.ts b/client_v2/src/components/BlockedServices/index.ts
new file mode 100644
index 00000000000..b7ed5df0182
--- /dev/null
+++ b/client_v2/src/components/BlockedServices/index.ts
@@ -0,0 +1 @@
+export { BlockedServices } from './BlockedServices';
diff --git a/client_v2/src/components/Clients/AddClient/AddClient.module.pcss b/client_v2/src/components/Clients/AddClient/AddClient.module.pcss
new file mode 100644
index 00000000000..ee4c64a0775
--- /dev/null
+++ b/client_v2/src/components/Clients/AddClient/AddClient.module.pcss
@@ -0,0 +1,107 @@
+.section {
+ padding: 8px 16px;
+ width: 100%;
+}
+
+.fieldGroup {
+ width: 100%;
+}
+
+.fieldGroupInput {
+ padding: 16px;
+ width: 100%;
+}
+
+.fieldLabel {
+ color: var(--default-main-text);
+ margin-bottom: 4px;
+}
+
+.fieldDesc {
+ color: var(--default-description-text);
+ margin-bottom: 12px;
+
+ a {
+ color: var(--default-link);
+ }
+}
+
+.navLink {
+ display: block;
+ text-decoration: none;
+ padding: 16px;
+ border-radius: 8px;
+ width: 100%;
+ cursor: pointer;
+ transition: background-color 0.2s;
+
+ &:hover,
+ &:focus {
+ background-color: var(--hovered-page-background);
+ }
+}
+
+.navLinkDisabled {
+ cursor: default;
+ pointer-events: none;
+
+ .navTitle,
+ .navDesc {
+ color: var(--disabled-main-text);
+ }
+
+ &:hover,
+ &:focus {
+ background-color: transparent;
+ }
+}
+
+.navItem {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+}
+
+.navItemContent {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.navTitle {
+ font-size: 16px;
+ font-weight: 600;
+ line-height: 1.3;
+ color: var(--default-main-text);
+}
+
+.navDesc {
+ font-size: 14px;
+ line-height: 1.3;
+ color: var(--default-description-text);
+}
+
+.actions {
+ display: flex;
+ gap: 16px;
+ padding: 16px;
+ flex-direction: column;
+
+ @media (min-width: 768px) {
+ flex-direction: row;
+ }
+}
+
+.actionButton {
+ width: auto;
+ min-width: 200px;
+ max-width: 100%;
+}
+
+.containerOverride {
+ padding-top: 0;
+}
+
+.disabledText {
+ color: var(--disabled-main-text);
+}
diff --git a/client_v2/src/components/Clients/AddClient/AddClient.tsx b/client_v2/src/components/Clients/AddClient/AddClient.tsx
new file mode 100644
index 00000000000..91a2259dc83
--- /dev/null
+++ b/client_v2/src/components/Clients/AddClient/AddClient.tsx
@@ -0,0 +1,379 @@
+import { createMemo, createEffect, createSignal, Show, onMount } from 'solid-js';
+import { useNavigate, useParams, useLocation } from '@solidjs/router';
+import cn from 'clsx';
+
+import intl from 'panel/common/intl';
+import { Input } from 'panel/common/controls/Input';
+import { Select } from 'panel/common/controls/Select';
+import type { IOption } from 'panel/lib/helpers/utils';
+import { Textarea } from 'panel/common/controls/Textarea';
+import { Button } from 'panel/common/ui/Button';
+import { Icon } from 'panel/common/ui/Icon';
+import { Link } from 'panel/common/ui/Link';
+import { PageLoader } from 'panel/common/ui/Loader';
+import { SwitchGroup } from 'panel/common/ui/SettingsGroup';
+import type { Client } from 'panel/initialState';
+import {
+ clientFormState,
+ updateClientFormField,
+ clearClientForm,
+ saveClient,
+ initClientForm,
+ buildFormPayload,
+ setFormErrors,
+} from 'panel/stores/clientForm';
+import { dashboardState, getClients } from 'panel/stores/dashboard';
+import { validateUpstreams, validateCacheSize } from 'panel/helpers/validators';
+import { RoutePath, Paths } from 'panel/components/Routes/Paths';
+import theme from 'panel/lib/theme';
+
+import { ClientsHeader } from './blocks/ClientsHeader';
+import { Identifiers } from './blocks/Identifiers/Identifiers';
+
+import s from './AddClient.module.pcss';
+
+export const AddClient = () => {
+ const navigate = useNavigate();
+ const params = useParams<{ clientName?: string }>();
+ const location = useLocation();
+
+ const [nameError, setNameError] = createSignal();
+ const [cacheSizeError, setCacheSizeError] = createSignal();
+ const [upstreamsError, setUpstreamsError] = createSignal();
+
+ createEffect(() => {
+ const formErrors = clientFormState.formErrors;
+ setNameError(typeof formErrors.name === 'string' ? formErrors.name : undefined);
+ setCacheSizeError(
+ typeof formErrors.upstreams_cache_size === 'string'
+ ? formErrors.upstreams_cache_size
+ : undefined,
+ );
+ setUpstreamsError(
+ typeof formErrors.upstreams === 'string' ? formErrors.upstreams : undefined,
+ );
+ });
+
+ onMount(() => {
+ getClients();
+ });
+
+ const handleNameChange = (e: Event) => {
+ const value = (e.target as HTMLInputElement).value;
+ updateClientFormField({ field: 'name', value });
+ setNameError(undefined);
+ };
+ createEffect(() => {
+ const searchParams = new URLSearchParams(location.search);
+ const id = searchParams.get('id');
+
+ if (!id || clientFormState.mode !== 'add') {
+ return;
+ }
+
+ updateClientFormField({ field: 'ids', value: [id] });
+ });
+
+ // When on the edit route and clients are loaded, initialize the form
+ // from the URL param. This handles page refreshes on the edit page.
+ createEffect(() => {
+ const urlClientName = params.clientName;
+ const clients = dashboardState.clients || [];
+
+ if (!urlClientName || !clients.length) {
+ return;
+ }
+
+ if (clientFormState.mode !== 'add' || clientFormState.originalName) {
+ return;
+ }
+
+ const decodedName = decodeURIComponent(urlClientName);
+ const client = clients.find((c: Client) => c.name === decodedName);
+
+ if (!client) {
+ clearClientForm();
+ navigate(Paths.Clients, { replace: true });
+ return;
+ }
+
+ initClientForm(buildFormPayload(client));
+ });
+
+ const isEdit = createMemo(() => clientFormState.mode === 'edit');
+
+ const handleCancel = () => {
+ clearClientForm();
+ navigate(Paths.Clients);
+ };
+
+ const handleSave = async () => {
+ const err = validateUpstreams(clientFormState.upstreams);
+ if (err) {
+ setFormErrors({ ...clientFormState.formErrors, upstreams: err });
+ return;
+ }
+ const result = await saveClient();
+ if (result) {
+ navigate(Paths.Clients);
+ }
+ };
+
+ const handleUseGlobalSettings = (e: Event) => {
+ updateClientFormField({
+ field: 'use_global_settings',
+ value: (e.target as HTMLInputElement).checked,
+ });
+ };
+
+ const handleUpstreamsCacheEnabled = (e: Event) => {
+ updateClientFormField({
+ field: 'upstreams_cache_enabled',
+ value: (e.target as HTMLInputElement).checked,
+ });
+ };
+
+ const handleUpstreamsBlur = () => {
+ const err = validateUpstreams(clientFormState.upstreams);
+ if (err) {
+ setFormErrors({ ...clientFormState.formErrors, upstreams: err });
+ }
+ };
+
+ const handleUpstreamsChange = (e: Event) => {
+ updateClientFormField({
+ field: 'upstreams',
+ value: (e.target as HTMLTextAreaElement).value,
+ });
+ setUpstreamsError(undefined);
+ };
+
+ const handleCacheSizeChange = (e: Event) => {
+ const value = Number((e.target as HTMLInputElement).value) || 0;
+ updateClientFormField({ field: 'upstreams_cache_size', value });
+ setCacheSizeError(undefined);
+ };
+
+ const handleCacheSizeBlur = () => {
+ if (!clientFormState.upstreams_cache_enabled) return;
+ const err = validateCacheSize(clientFormState.upstreams_cache_size, true);
+ setCacheSizeError(err);
+ if (err) {
+ setFormErrors({ ...clientFormState.formErrors, upstreams_cache_size: err });
+ }
+ };
+
+ const tagOptions = createMemo(() =>
+ (dashboardState.supportedTags || []).map((tag: string) => ({
+ label: tag,
+ value: tag,
+ })),
+ );
+
+ const tagValue = createMemo(() =>
+ clientFormState.tags.map((t: string) => ({ label: t, value: t })),
+ );
+
+ const handleTagChange = (selected: IOption | IOption[] | null) => {
+ const tags = Array.isArray(selected) ? selected.map((s) => s.value) : [];
+ updateClientFormField({ field: 'tags', value: tags });
+ };
+
+ const protectionRoute = createMemo(() =>
+ isEdit() ? RoutePath.ClientsEditProtection : RoutePath.ClientsProtection,
+ );
+
+ const blockedServicesRoute = createMemo(() =>
+ isEdit() ? RoutePath.ClientsEditBlockedServices : RoutePath.ClientsBlockedServices,
+ );
+
+ const routeProps = createMemo(() =>
+ isEdit() ? { clientName: encodeURIComponent(clientFormState.originalName) } : undefined,
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {intl.getMessage('clients_tags')}
+
+
+ {intl.getMessage('clients_tags_desc', {
+ a: (text: string) => (
+
+ {text}
+
+ ),
+ })}
+
+
+
+
+
+ {intl.getMessage('settings')}
+
+
+
+
+
+
+
+
+ {intl.getMessage('clients_protection')}
+
+
+ {intl.getMessage('clients_protection_desc')}
+
+
+
+
+
+
+
+
+
+
+ {intl.getMessage('blocked_services')}
+
+
+ {intl.getMessage('blocked_services_desc')}
+
+
+
+
+
+
+
+ {intl.getMessage('upstream_dns_servers_title')}
+
+
+
+
+
+
+
+ {clientFormState.upstreams_cache_enabled && (
+
+ )}
+
+
+
+
+ {intl.getMessage('save_btn')}
+
+
+ {intl.getMessage('cancel_btn')}
+
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/Clients/AddClient/blocks/ClientBlockedServices.module.pcss b/client_v2/src/components/Clients/AddClient/blocks/ClientBlockedServices.module.pcss
new file mode 100644
index 00000000000..f0af84cf08a
--- /dev/null
+++ b/client_v2/src/components/Clients/AddClient/blocks/ClientBlockedServices.module.pcss
@@ -0,0 +1,3 @@
+.containerOverride {
+ padding-top: 0;
+}
diff --git a/client_v2/src/components/Clients/AddClient/blocks/ClientBlockedServices.tsx b/client_v2/src/components/Clients/AddClient/blocks/ClientBlockedServices.tsx
new file mode 100644
index 00000000000..73262ec44b4
--- /dev/null
+++ b/client_v2/src/components/Clients/AddClient/blocks/ClientBlockedServices.tsx
@@ -0,0 +1,35 @@
+import { createMemo } from 'solid-js';
+import intl from 'panel/common/intl';
+import { clientFormState } from 'panel/stores/clientForm';
+import { RoutePath } from 'panel/components/Routes/Paths';
+
+import { BlockedServices } from 'panel/components/BlockedServices/BlockedServices';
+
+import s from './ClientBlockedServices.module.pcss';
+
+export const ClientBlockedServices = () => {
+ const isEdit = createMemo(() => clientFormState.mode === 'edit');
+
+ const clientPageLink = createMemo(() =>
+ isEdit()
+ ? {
+ path: RoutePath.ClientsEdit,
+ title: clientFormState.name || intl.getMessage('clients_edit'),
+ props: { clientName: encodeURIComponent(clientFormState.originalName) },
+ }
+ : {
+ path: RoutePath.ClientsAdd,
+ title: intl.getMessage('clients_add'),
+ },
+ );
+
+ const breadcrumbs = createMemo(() => ({
+ parentLinks: [
+ { path: RoutePath.Clients, title: intl.getMessage('client_settings') },
+ clientPageLink(),
+ ],
+ currentTitle: intl.getMessage('blocked_services'),
+ }));
+
+ return ;
+};
diff --git a/client_v2/src/components/Clients/AddClient/blocks/ClientSchedule.module.pcss b/client_v2/src/components/Clients/AddClient/blocks/ClientSchedule.module.pcss
new file mode 100644
index 00000000000..f0af84cf08a
--- /dev/null
+++ b/client_v2/src/components/Clients/AddClient/blocks/ClientSchedule.module.pcss
@@ -0,0 +1,3 @@
+.containerOverride {
+ padding-top: 0;
+}
diff --git a/client_v2/src/components/Clients/AddClient/blocks/ClientSchedule.tsx b/client_v2/src/components/Clients/AddClient/blocks/ClientSchedule.tsx
new file mode 100644
index 00000000000..ed3c2c2359b
--- /dev/null
+++ b/client_v2/src/components/Clients/AddClient/blocks/ClientSchedule.tsx
@@ -0,0 +1,35 @@
+import { createMemo } from 'solid-js';
+import cn from 'clsx';
+import intl from 'panel/common/intl';
+import { clientFormState } from 'panel/stores/clientForm';
+import { RoutePath, type RoutePathKey } from 'panel/components/Routes/Paths';
+import theme from 'panel/lib/theme';
+import { InactivitySchedule } from 'panel/components/BlockedServices/InactivitySchedule/InactivitySchedule';
+import s from './ClientSchedule.module.pcss';
+
+import { ClientsHeader } from './ClientsHeader';
+
+export const ClientSchedule = () => {
+ const isEdit = createMemo(() => clientFormState.mode === 'edit');
+
+ const blockedServicesPath = createMemo(() =>
+ isEdit() ? RoutePath.ClientsEditBlockedServices : RoutePath.ClientsBlockedServices,
+ );
+
+ return (
+
+ );
+};
diff --git a/client_v2/src/components/Clients/AddClient/blocks/ClientsHeader.module.pcss b/client_v2/src/components/Clients/AddClient/blocks/ClientsHeader.module.pcss
new file mode 100644
index 00000000000..211d7fda92a
--- /dev/null
+++ b/client_v2/src/components/Clients/AddClient/blocks/ClientsHeader.module.pcss
@@ -0,0 +1,11 @@
+.headerWrapper {
+ padding: 0 16px;
+}
+
+.title {
+ padding: 24px 0 16px;
+
+ @media (min-width: 768px) {
+ padding-top: 48px;
+ }
+}
diff --git a/client_v2/src/components/Clients/AddClient/blocks/ClientsHeader.tsx b/client_v2/src/components/Clients/AddClient/blocks/ClientsHeader.tsx
new file mode 100644
index 00000000000..797a0c720fa
--- /dev/null
+++ b/client_v2/src/components/Clients/AddClient/blocks/ClientsHeader.tsx
@@ -0,0 +1,74 @@
+import { createMemo } from 'solid-js';
+import { useMatch } from '@solidjs/router';
+import cn from 'clsx';
+import intl from 'panel/common/intl';
+import { Breadcrumbs } from 'panel/common/ui/Breadcrumbs';
+import { clientFormState } from 'panel/stores/clientForm';
+import { Paths, RoutePath, type RoutePathKey } from 'panel/components/Routes/Paths';
+import theme from 'panel/lib/theme';
+
+import s from './ClientsHeader.module.pcss';
+
+type ClientsHeaderProps = {
+ currentTitle: string;
+ extraLinks?: {
+ path: RoutePathKey;
+ title: string;
+ props?: Partial>;
+ }[];
+};
+
+export const ClientsHeader = (props: ClientsHeaderProps) => {
+ const extraLinks = () => props.extraLinks || [];
+ const isEdit = createMemo(() => clientFormState.mode === 'edit');
+
+ const clientPageLink = createMemo(() =>
+ isEdit()
+ ? {
+ path: RoutePath.ClientsEdit,
+ title: clientFormState.originalName || intl.getMessage('clients_edit'),
+ props: { clientName: encodeURIComponent(clientFormState.originalName) },
+ }
+ : {
+ path: RoutePath.ClientsAdd,
+ title: intl.getMessage('clients_add'),
+ },
+ );
+
+ const isAddMatch = useMatch(() => Paths.ClientsAdd);
+ const isEditMatch = useMatch(() => Paths.ClientsEdit);
+ const isMainFormPage = createMemo(
+ () => isAddMatch() !== undefined || isEditMatch() !== undefined,
+ );
+
+ const pageTitle = createMemo(() =>
+ isEdit() ? intl.getMessage('clients_edit') : props.currentTitle,
+ );
+
+ const breadcrumbCurrent = createMemo(() =>
+ isEdit() && isMainFormPage() ? clientFormState.originalName : pageTitle(),
+ );
+
+ const parentLinks = createMemo(() => [
+ { path: RoutePath.Clients, title: intl.getMessage('client_settings') },
+ ...(!isMainFormPage() ? [clientPageLink()] : []),
+ ...extraLinks(),
+ ]);
+
+ return (
+
+ );
+};
diff --git a/client_v2/src/components/Clients/AddClient/blocks/Identifiers/Identifiers.module.pcss b/client_v2/src/components/Clients/AddClient/blocks/Identifiers/Identifiers.module.pcss
new file mode 100644
index 00000000000..aaf98b29be5
--- /dev/null
+++ b/client_v2/src/components/Clients/AddClient/blocks/Identifiers/Identifiers.module.pcss
@@ -0,0 +1,70 @@
+.wrapper {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ width: 100%;
+}
+
+.label {
+ color: var(--default-main-text);
+}
+
+.desc {
+ color: var(--default-description-text);
+ margin-bottom: 8px;
+
+ a {
+ color: var(--default-link);
+ }
+}
+
+.row {
+ display: flex;
+ align-items: flex-start;
+ gap: 8px;
+ width: 100%;
+}
+
+.row > :first-child {
+ flex: 1;
+}
+
+.inputCell {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+}
+
+.removeButton {
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 4px;
+ flex-shrink: 0;
+}
+
+.removeSuffixBtn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+ padding: 0;
+ border: none;
+ background: none;
+ cursor: pointer;
+ flex-shrink: 0;
+}
+
+.addButton {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 8px 0;
+ font-size: 16px;
+ color: var(--default-link);
+}
diff --git a/client_v2/src/components/Clients/AddClient/blocks/Identifiers/Identifiers.tsx b/client_v2/src/components/Clients/AddClient/blocks/Identifiers/Identifiers.tsx
new file mode 100644
index 00000000000..a7a55ffdaa7
--- /dev/null
+++ b/client_v2/src/components/Clients/AddClient/blocks/Identifiers/Identifiers.tsx
@@ -0,0 +1,146 @@
+import { createSignal, createMemo, createEffect, Index } from 'solid-js';
+import cn from 'clsx';
+import intl from 'panel/common/intl';
+import { Input } from 'panel/common/controls/Input';
+import { Icon } from 'panel/common/ui/Icon';
+import {
+ clientFormState,
+ updateClientFormField,
+ computeExistingClientIds,
+} from 'panel/stores/clientForm';
+import { validateIdentifier } from 'panel/helpers/validators';
+import theme from 'panel/lib/theme';
+
+import s from './Identifiers.module.pcss';
+
+export const Identifiers = () => {
+ const [errors, setErrors] = createSignal<(string | undefined)[]>([]);
+
+ // The store (`clientFormState.ids`) is the single source of truth for the
+ // identifiers list. We bind to it directly (the same pattern the Name field
+ // uses) and update it through `updateClientFormField`.
+ //
+ // A previous implementation kept a local `ids` signal and tried to sync it
+ // with the store via a `createEffect`. In Solid, `setIds` notifies effects
+ // synchronously, so that effect ran *before* `syncToStore` had a chance to
+ // update the store, saw a mismatch and reset the local value back to the
+ // stale store value — which made typed values erase immediately and the
+ // "add identifier" button appear to do nothing.
+
+ // Sync external (server) errors into the local per-index error state.
+ createEffect(() => {
+ const formErrors = clientFormState.formErrors;
+ if (Array.isArray(formErrors.ids)) {
+ setErrors(formErrors.ids);
+ } else {
+ // Store errors were cleared — clear stale local errors too.
+ setErrors((prev) => prev.map((): undefined => undefined));
+ }
+ });
+
+ const existingClientIds = createMemo(() => computeExistingClientIds());
+
+ const handleAdd = () => {
+ updateClientFormField('ids', [...clientFormState.ids, '']);
+ };
+
+ const handleRemove = (index: number) => {
+ const newIds = clientFormState.ids.filter((_, i) => i !== index);
+ updateClientFormField('ids', newIds);
+ const newErrors = errors().filter((_, i) => i !== index);
+ setErrors(newErrors);
+ };
+
+ const handleChange = (index: number, value: string) => {
+ const newIds = [...clientFormState.ids];
+ newIds[index] = value;
+ updateClientFormField('ids', newIds);
+ // Clear the local error for this index — re-validated on blur.
+ setErrors((prev) => {
+ const next = [...prev];
+ next[index] = undefined;
+ return next;
+ });
+ };
+
+ const handleBlur = (index: number) => {
+ const ids = clientFormState.ids;
+ const value = ids[index];
+ const err = validateIdentifier(value, ids, index, existingClientIds());
+ const newErrors = [...errors()];
+ newErrors[index] = err || undefined;
+ setErrors(newErrors);
+ };
+
+ return (
+
+
+ {intl.getMessage('clients_identifiers')}
+
+
+ {intl.getMessage('clients_identifiers_desc', {
+ a: (text: string) => (
+
+ {text}
+
+ ),
+ })}
+
+
+
+ {(value, index) => {
+ const activeError = createMemo(() => errors()[index]);
+
+ return (
+
+
+
+ handleChange(index, (e.target as HTMLInputElement).value)
+ }
+ onInput={(e: Event) =>
+ handleChange(index, (e.target as HTMLInputElement).value)
+ }
+ onBlur={() => handleBlur(index)}
+ placeholder={intl.getMessage('clients_identifier_format_error')}
+ error={!!activeError()}
+ errorMessage={activeError()}
+ size="large"
+ suffixIcon={
+ index > 0 ? (
+ handleRemove(index)}
+ aria-label={intl.getMessage('delete_btn')}
+ >
+
+
+ ) : undefined
+ }
+ />
+
+
+ );
+ }}
+
+
+
+
+ {intl.getMessage('clients_add_identifier')}
+
+
+ );
+};
diff --git a/client_v2/src/components/Clients/AddClient/blocks/Identifiers/index.ts b/client_v2/src/components/Clients/AddClient/blocks/Identifiers/index.ts
new file mode 100644
index 00000000000..004a5b04b52
--- /dev/null
+++ b/client_v2/src/components/Clients/AddClient/blocks/Identifiers/index.ts
@@ -0,0 +1 @@
+export { Identifiers } from './Identifiers';
diff --git a/client_v2/src/components/Clients/AddClient/blocks/Protection/Protection.module.pcss b/client_v2/src/components/Clients/AddClient/blocks/Protection/Protection.module.pcss
new file mode 100644
index 00000000000..8cab0d61779
--- /dev/null
+++ b/client_v2/src/components/Clients/AddClient/blocks/Protection/Protection.module.pcss
@@ -0,0 +1,7 @@
+.containerOverride {
+ padding-top: 0;
+}
+
+.checkboxRow {
+ padding: 8px 0;
+}
diff --git a/client_v2/src/components/Clients/AddClient/blocks/Protection/Protection.tsx b/client_v2/src/components/Clients/AddClient/blocks/Protection/Protection.tsx
new file mode 100644
index 00000000000..e6213c95758
--- /dev/null
+++ b/client_v2/src/components/Clients/AddClient/blocks/Protection/Protection.tsx
@@ -0,0 +1,119 @@
+import { createMemo, For } from 'solid-js';
+import cn from 'clsx';
+import intl from 'panel/common/intl';
+import { Checkbox } from 'panel/common/controls/Checkbox';
+import { SwitchGroup } from 'panel/common/ui/SettingsGroup';
+import { clientFormState, updateClientFormField } from 'panel/stores/clientForm';
+import {
+ SAFE_SEARCH_PROVIDERS,
+ SAFE_SEARCH_PROVIDER_KEYS,
+ type SafeSearchProviderKey,
+} from 'panel/helpers/constants';
+import theme from 'panel/lib/theme';
+
+import { ClientsHeader } from '../ClientsHeader';
+
+import s from './Protection.module.pcss';
+
+export const Protection = () => {
+ const disabled = createMemo(() => clientFormState.use_global_settings);
+
+ const handleToggle = (field: keyof typeof clientFormState) => (e: Event) => {
+ updateClientFormField(field, (e.target as HTMLInputElement).checked);
+ };
+
+ const handleSafeSearchEnabled = (e: Event) => {
+ const enabled = (e.target as HTMLInputElement).checked;
+ const providersUpdate: Record = {};
+ if (enabled) {
+ SAFE_SEARCH_PROVIDER_KEYS.forEach((key) => {
+ providersUpdate[key] = true;
+ });
+ }
+ updateClientFormField('safe_search', {
+ ...clientFormState.safe_search,
+ enabled,
+ ...providersUpdate,
+ });
+ };
+
+ const handleSafeSearchProvider = (provider: SafeSearchProviderKey) => (e: Event) => {
+ updateClientFormField('safe_search', {
+ ...clientFormState.safe_search,
+ [provider]: (e.target as HTMLInputElement).checked,
+ });
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {(key) => (
+
+
+ {SAFE_SEARCH_PROVIDERS[key]}
+
+
+ )}
+
+
+
+
+ {intl.getMessage('clients_logs_and_statistics')}
+
+
+
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/Clients/AddClient/blocks/Protection/index.ts b/client_v2/src/components/Clients/AddClient/blocks/Protection/index.ts
new file mode 100644
index 00000000000..7e0eae54a13
--- /dev/null
+++ b/client_v2/src/components/Clients/AddClient/blocks/Protection/index.ts
@@ -0,0 +1 @@
+export { Protection } from './Protection';
diff --git a/client_v2/src/components/Clients/AddClient/index.ts b/client_v2/src/components/Clients/AddClient/index.ts
new file mode 100644
index 00000000000..862914cb907
--- /dev/null
+++ b/client_v2/src/components/Clients/AddClient/index.ts
@@ -0,0 +1 @@
+export { AddClient } from './AddClient';
diff --git a/client_v2/src/components/Clients/Clients.module.pcss b/client_v2/src/components/Clients/Clients.module.pcss
new file mode 100644
index 00000000000..a904cb2b204
--- /dev/null
+++ b/client_v2/src/components/Clients/Clients.module.pcss
@@ -0,0 +1,57 @@
+.desc {
+ width: 100%;
+ max-width: 632px;
+ padding: 16px 0;
+}
+
+.button {
+ width: fit-content;
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px;
+ font-size: 16px;
+ color: var(--default-main-text);
+ border-radius: 8px;
+ transition: var(--t2) background-color;
+ border: 0;
+ background: none;
+ cursor: pointer;
+
+ &:hover,
+ &:focus {
+ background: var(--hovered-secondary-button);
+ }
+
+ &:active {
+ background: var(--pressed-secondary-button);
+ }
+
+ &:disabled {
+ background: var(--disabled-secondary-button);
+ color: var(--disabled-secondary-button-stroke);
+ cursor: default;
+ }
+
+ &_add {
+ margin-top: 8px;
+ padding: 8px 16px;
+ color: var(--default-link);
+
+ @media (min-width: 768px) {
+ padding: 8px;
+ }
+ }
+}
+
+.tabs {
+ padding: 24px 16px 0;
+}
+
+.tabContent {
+ padding: 16px 0 0 0;
+}
+
+.tableSection {
+ padding-top: 16px;
+}
diff --git a/client_v2/src/components/Clients/Clients.tsx b/client_v2/src/components/Clients/Clients.tsx
new file mode 100644
index 00000000000..adf85e70591
--- /dev/null
+++ b/client_v2/src/components/Clients/Clients.tsx
@@ -0,0 +1,178 @@
+import { createSignal, createMemo, Show, onMount } from 'solid-js';
+import { useNavigate, useSearchParams } from '@solidjs/router';
+import cn from 'clsx';
+
+import intl from 'panel/common/intl';
+import { ConfirmDialog } from 'panel/common/ui/ConfirmDialog';
+import { Tabs } from 'panel/common/ui/Tabs';
+import { dashboardState, getClients } from 'panel/stores/dashboard';
+import { statsState, getStats } from 'panel/stores/stats';
+import { clientsState, deleteClient } from 'panel/stores/clients';
+import { servicesState, getAllBlockedServices } from 'panel/stores/services';
+import { initClientForm, buildFormPayload } from 'panel/stores/clientForm';
+import type { Client } from 'panel/initialState';
+import { linkPathBuilder, RoutePath, Paths } from 'panel/components/Routes/Paths';
+import theme from 'panel/lib/theme';
+import type { WebService } from './blocks/PersistentClientsTable/ServiceIcons';
+
+import { PersistentClientsTable } from './blocks/PersistentClientsTable';
+import { RuntimeClientsTable } from './blocks/RuntimeClientsTable';
+import s from './Clients.module.pcss';
+import { PlusButton } from 'panel/common/ui/PlusButton';
+
+const CLIENT_TABS = {
+ PERSISTENT: 'persistent',
+ RUNTIME: 'runtime',
+} as const;
+
+export const Clients = () => {
+ const navigate = useNavigate();
+ const [clientNameToDelete, setClientNameToDelete] = createSignal('');
+
+ const [searchParams, setSearchParams] = useSearchParams<{ tab?: string }>();
+
+ const activeTab = createMemo(() =>
+ searchParams.tab === CLIENT_TABS.RUNTIME ? CLIENT_TABS.RUNTIME : CLIENT_TABS.PERSISTENT,
+ );
+
+ const handleTabChange = (tabId: string) => {
+ setSearchParams({ tab: tabId }, { replace: true });
+ };
+
+ onMount(() => {
+ getClients();
+ getStats();
+ getAllBlockedServices();
+ });
+
+ const handleAddClient = () => {
+ initClientForm(null);
+ navigate(Paths.ClientsAdd);
+ };
+
+ const handleEditClient = (client: Client) => {
+ initClientForm(buildFormPayload(client));
+ navigate(
+ linkPathBuilder(RoutePath.ClientsEdit, {
+ clientName: encodeURIComponent(client.name),
+ }),
+ );
+ };
+
+ const handleDeleteClient = (name: string) => {
+ setClientNameToDelete(name);
+ };
+
+ const handleDeleteClose = () => {
+ setClientNameToDelete('');
+ };
+
+ const handleDeleteConfirm = () => {
+ deleteClient(clientNameToDelete());
+ handleDeleteClose();
+ };
+
+ const isLoading = createMemo(
+ () => dashboardState.processingClients || statsState.processingStats,
+ );
+
+ const serviceMap = createMemo(() => {
+ const map = new Map();
+ (servicesState.allServices || []).forEach((svc) => {
+ map.set(svc.id, svc);
+ });
+ return map;
+ });
+
+ return (
+
+
+
+ {intl.getMessage('clients')}
+
+
+
+ {intl.getMessage('clients_desc')}
+
+
+ {intl.getMessage('clients_add')}
+
+
+ {dashboardState.clients?.length > 0 && (
+
+ )}
+ >
+ ),
+ },
+ {
+ id: CLIENT_TABS.RUNTIME,
+ label: intl.getMessage('auto_clients_title'),
+ content: (
+ <>
+ {intl.getMessage('auto_clients_desc')}
+
+ {dashboardState.autoClients?.length > 0 && (
+
+
+
+ )}
+ >
+ ),
+ },
+ ]}
+ />
+
+
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/Clients/blocks/PersistentClientsTable/PersistentClientsTable.module.pcss b/client_v2/src/components/Clients/blocks/PersistentClientsTable/PersistentClientsTable.module.pcss
new file mode 100644
index 00000000000..a42e9d7e69b
--- /dev/null
+++ b/client_v2/src/components/Clients/blocks/PersistentClientsTable/PersistentClientsTable.module.pcss
@@ -0,0 +1,293 @@
+.headerCell {
+ @media (min-width: 768px) {
+ width: 100%;
+ min-height: 42px;
+ padding: 0 12px;
+ }
+}
+
+.nameDropdown {
+ min-width: 0;
+ max-width: 100%;
+ overflow: hidden;
+}
+
+.nameDropdownInner {
+ min-width: 0;
+ overflow: hidden;
+}
+
+.nameTrigger {
+ cursor: pointer;
+ display: block;
+
+ @media (max-width: 767px) {
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+}
+
+.nameTooltipOverlay {
+ max-width: 280px;
+
+ @media (min-width: 768px) {
+ max-width: 360px;
+ }
+}
+
+.nameTooltip {
+ display: flex;
+ align-items: flex-start;
+ gap: 8px;
+ padding: 12px 16px;
+ background: var(--page-background-additional);
+ border-radius: 8px;
+ max-width: 100%;
+ overflow: hidden;
+}
+
+.nameTooltipText {
+ font-size: 14px;
+ line-height: 1.5;
+ color: var(--default-main-text);
+ word-break: break-all;
+ overflow-wrap: break-word;
+ min-width: 0;
+}
+
+.copyBtn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+ padding: 0;
+ border: none;
+ background: none;
+ cursor: pointer;
+ flex-shrink: 0;
+ color: var(--default-gray-icons);
+ transition: color var(--t2);
+ outline: none;
+
+ &:hover {
+ color: var(--default-main-text);
+ }
+}
+
+.copyBtnGreen {
+ color: var(--default-product-icon);
+
+ &:hover {
+ opacity: 0.8;
+ }
+}
+
+.copyBtnTopRight {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+}
+
+.servicesIcons {
+ display: flex;
+ align-items: center;
+ gap: 2px;
+}
+
+.servicesIconsList {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ flex-wrap: nowrap;
+ min-width: 0;
+ flex: 1;
+ overflow: hidden;
+
+ @media (min-width: 768px) {
+ gap: 2px;
+ }
+}
+
+.serviceIcon {
+ width: 24px;
+ height: 24px;
+ flex-shrink: 0;
+ color: var(--default-black-icons);
+ border-radius: 4px;
+ overflow: hidden;
+
+ svg {
+ width: 100%;
+ height: 100%;
+ display: block;
+ }
+}
+
+.servicesTooltipOverlay {
+ max-width: 280px;
+}
+
+.servicesTooltip {
+ padding: 12px;
+ background: var(--page-background-additional);
+ border-radius: 8px;
+}
+
+.servicesTooltipGrid {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ align-items: center;
+}
+
+.countLabel {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 24px;
+ min-height: 24px;
+ padding: 0 2px;
+ border-radius: 200px;
+ background-color: transparent;
+ border: 1px solid var(--default-item-divider);
+ font-size: 13px;
+ line-height: 1;
+ color: var(--default-main-text);
+ cursor: default;
+ flex-shrink: 0;
+ box-sizing: border-box;
+ aspect-ratio: 1;
+}
+
+.countDropdown {
+ flex-shrink: 0;
+}
+
+.tagsRow {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ flex-wrap: nowrap;
+ min-width: 0;
+ overflow: hidden;
+
+ @media (min-width: 768px) {
+ gap: 2px;
+ }
+}
+
+.tagsText {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ min-width: 0;
+
+ @media (min-width: 768px) {
+ color: var(--default-main-text);
+ }
+}
+
+.tagsTooltipOverlay {
+ max-width: 280px;
+}
+
+.tagsTooltip {
+ position: relative;
+ padding: 8px 0;
+ padding-right: 36px;
+ background: var(--page-background-additional);
+ border-radius: 8px;
+ text-align: left;
+ max-height: 240px;
+ overflow-y: auto;
+}
+
+.tagsTooltipItem {
+ display: block;
+ padding: 8px 16px;
+ font-size: 14px;
+ line-height: 1.4;
+ color: var(--default-main-text);
+ white-space: nowrap;
+}
+
+.userIcon {
+ width: 16px;
+ height: 16px;
+ flex-shrink: 0;
+}
+
+.userIconRight {
+ width: 16px;
+ height: 16px;
+ flex-shrink: 0;
+ margin-left: 8px;
+}
+
+.table {
+ @media (min-width: 768px) {
+ grid-template-columns: 2fr 1fr 1fr 1fr 1fr 1fr 1fr 80px;
+ }
+}
+
+.ids {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ gap: 2px;
+
+ @media (min-width: 768px) {
+ align-items: flex-start;
+ }
+}
+
+.idsRow {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ flex-wrap: nowrap;
+ min-width: 0;
+ overflow: hidden;
+}
+
+.idsText {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ min-width: 0;
+
+ @media (min-width: 768px) {
+ color: var(--default-main-text);
+ }
+}
+
+.idsTooltipOverlay {
+ max-width: 280px;
+
+ @media (min-width: 768px) {
+ max-width: 360px;
+ }
+}
+
+.idsTooltip {
+ position: relative;
+ padding: 8px 0;
+ padding-right: 36px;
+ background: var(--page-background-additional);
+ border-radius: 8px;
+ max-height: 240px;
+ overflow-y: auto;
+}
+
+.idsTooltipItem {
+ display: block;
+ padding: 8px 16px;
+ font-size: 14px;
+ line-height: 1.4;
+ color: var(--default-main-text);
+ word-break: break-all;
+ white-space: normal;
+}
diff --git a/client_v2/src/components/Clients/blocks/PersistentClientsTable/PersistentClientsTable.tsx b/client_v2/src/components/Clients/blocks/PersistentClientsTable/PersistentClientsTable.tsx
new file mode 100644
index 00000000000..4f47ada0253
--- /dev/null
+++ b/client_v2/src/components/Clients/blocks/PersistentClientsTable/PersistentClientsTable.tsx
@@ -0,0 +1,323 @@
+import { createMemo, Show, For } from 'solid-js';
+import cn from 'clsx';
+import copy from 'copy-to-clipboard';
+
+import intl from 'panel/common/intl';
+import type { Client, NormalizedTopClients } from 'panel/initialState';
+import { LOCAL_STORAGE_KEYS, LocalStorageHelper } from 'panel/helpers/localStorageHelper';
+import { Table, type TableColumn } from 'panel/common/ui/Table';
+import { Icon } from 'panel/common/ui/Icon';
+import { Dropdown } from 'panel/common/ui/Dropdown';
+import { addSuccessToast } from 'panel/stores/toasts';
+import theme from 'panel/lib/theme';
+
+import { ServiceCell } from './ServiceCell';
+import { TagCell } from './TagCell';
+import type { WebService } from './ServiceIcons';
+
+import { TableEmptyState } from '../TableEmptyState/TableEmptyState';
+import s from './PersistentClientsTable.module.pcss';
+
+type Props = {
+ clients: Client[];
+ normalizedTopClients?: NormalizedTopClients;
+ loading?: boolean;
+ onEdit: (client: Client) => void;
+ onDelete: (name: string) => void;
+ editDisabled?: boolean;
+ deleteDisabled?: boolean;
+ serviceMap: Map;
+};
+
+export const PersistentClientsTable = (props: Props) => {
+ const pageSize = createMemo(
+ () => LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.CLIENTS_PAGE_SIZE) || undefined,
+ );
+
+ const handleCopy = (text: string) => {
+ copy(text);
+ addSuccessToast(intl.getMessage('copied'));
+ };
+
+ const columns = createMemo[]>(() => {
+ // Access serviceMap here so the memo re-runs when it changes.
+ // It's used inside render closures below, which SolidJS doesn't track.
+ const svcMap = props.serviceMap;
+
+ return [
+ {
+ key: 'ids',
+ header: {
+ text: intl.getMessage('client_identifier'),
+ className: s.headerCell,
+ },
+ accessor: (row: Client) => row.ids.filter((id) => id.trim() !== '').join(','),
+ sortable: true,
+ render: (_value: string, row: Client) => {
+ const { ids } = row;
+ // Filter out empty strings — the backend may return trailing empty
+ // entries that would inflate hiddenCount and cause a spurious comma.
+ const nonEmpty = ids.filter((id) => id.trim() !== '');
+ const firstId = nonEmpty[0] || '';
+ const hiddenCount = nonEmpty.length - 1;
+
+ return (
+
+
+ {intl.getMessage('client_identifier')}
+
+
+
+
+
+ {firstId}
+
+ 0}>
+
+
+ {(id) => (
+
+ {id}
+
+ )}
+
+
+ handleCopy(nonEmpty.join(', '))
+ }
+ title={intl.getMessage('copy')}
+ >
+
+
+
+ }
+ >
+
{hiddenCount}
+
+
+
+
+
+ );
+ },
+ },
+ {
+ key: 'name',
+ header: {
+ text: intl.getMessage('name'),
+ className: s.headerCell,
+ },
+ accessor: 'name',
+ sortable: true,
+ render: (value: string) => (
+
+
{intl.getMessage('name')}
+
+
+
+ {value}
+ handleCopy(value)}
+ title={intl.getMessage('copy')}
+ >
+
+
+
+ }
+ class={s.nameDropdown}
+ childrenClass={s.nameDropdownInner}
+ >
+
+ {value}
+
+
+
+
+ ),
+ },
+ {
+ key: 'settings',
+ header: {
+ text: intl.getMessage('settings'),
+ className: s.headerCell,
+ },
+ accessor: 'use_global_settings',
+ sortable: true,
+ render: (value: boolean) => (
+
+
{intl.getMessage('settings')}
+
+
+
+ {value
+ ? intl.getMessage('settings_global')
+ : intl.getMessage('settings_custom')}
+
+
+
+
+
+
+ ),
+ },
+ {
+ key: 'blocked_services',
+ header: {
+ text: intl.getMessage('blocked_services'),
+ className: s.headerCell,
+ },
+ accessor: 'use_global_blocked_services',
+ sortable: true,
+ minWidth: 120,
+ render: (_value: boolean, row: Client) => (
+
+ ),
+ },
+ {
+ key: 'upstreams',
+ header: {
+ text: intl.getMessage('upstreams'),
+ className: s.headerCell,
+ },
+ accessor: (row: Client) => row.upstreams.length > 0,
+ sortable: true,
+ render: (_value: boolean, row: Client) => (
+
+
{intl.getMessage('upstreams')}
+
+
+
+ {row.upstreams.length > 0
+ ? intl.getMessage('settings_custom')
+ : intl.getMessage('settings_global')}
+
+
+
+ ),
+ },
+ {
+ key: 'tags',
+ header: {
+ text: intl.getMessage('tags_title'),
+ className: s.headerCell,
+ },
+ accessor: (row: Client) => row.tags.join(','),
+ sortable: true,
+ render: (_value: string, row: Client) => (
+
+ ),
+ },
+ {
+ key: 'requests',
+ header: {
+ text: intl.getMessage('requests_table_header'),
+ className: s.headerCell,
+ },
+ accessor: (row: Client) => props.normalizedTopClients?.configured[row.name] || 0,
+ sortable: true,
+ render: (_value: unknown, row: Client) => (
+
+
+ {intl.getMessage('requests_table_header')}
+
+
+
+
+ {(
+ props.normalizedTopClients?.configured[row.name] || 0
+ ).toLocaleString()}
+
+
+
+ ),
+ },
+ {
+ key: 'actions',
+ header: {
+ text: '',
+ className: s.headerCell,
+ },
+ sortable: false,
+ width: 80,
+ render: (_value: unknown, row: Client) => (
+
+
+
+ props.onEdit(row)}
+ disabled={props.editDisabled}
+ class={theme.table.action}
+ title={intl.getMessage('edit_table_action')}
+ aria-label={intl.getMessage('edit_table_action')}
+ data-testid="clients-edit-button"
+ data-table-action
+ >
+
+
+ {intl.getMessage('edit_table_action')}
+
+
+
+ props.onDelete(row.name)}
+ disabled={props.deleteDisabled}
+ class={cn(theme.table.action, theme.table.action_danger)}
+ title={intl.getMessage('delete_table_action')}
+ aria-label={intl.getMessage('delete_table_action')}
+ data-testid="clients-delete-button"
+ data-table-action
+ >
+
+
+ {intl.getMessage('delete_table_action')}
+
+
+
+
+
+ ),
+ },
+ ];
+ });
+
+ const handlePageSizeChange = (newSize: number) => {
+ LocalStorageHelper.setItem(LOCAL_STORAGE_KEYS.CLIENTS_PAGE_SIZE, newSize);
+ };
+
+ return (
+
+ data={props.clients}
+ class={s.table}
+ columns={columns()}
+ emptyTable={ }
+ loading={props.loading}
+ pageSize={pageSize()}
+ onPageSizeChange={handlePageSizeChange}
+ getRowId={(row) => row.name}
+ />
+ );
+};
diff --git a/client_v2/src/components/Clients/blocks/PersistentClientsTable/ServiceCell.tsx b/client_v2/src/components/Clients/blocks/PersistentClientsTable/ServiceCell.tsx
new file mode 100644
index 00000000000..05cfa270d75
--- /dev/null
+++ b/client_v2/src/components/Clients/blocks/PersistentClientsTable/ServiceCell.tsx
@@ -0,0 +1,35 @@
+import { Show } from 'solid-js';
+
+import intl from 'panel/common/intl';
+import theme from 'panel/lib/theme';
+
+import { ServiceIcons, type WebService } from './ServiceIcons';
+
+type ServiceCellProps = {
+ serviceIds: string[];
+ useGlobal: boolean;
+ serviceMap: Map;
+};
+
+export const ServiceCell = (props: ServiceCellProps) => {
+ return (
+
+
{intl.getMessage('blocked_services')}
+
+ 0} fallback={- }>
+
+
+ }
+ >
+ {intl.getMessage('settings_global')}
+
+
+
+ );
+};
diff --git a/client_v2/src/components/Clients/blocks/PersistentClientsTable/ServiceIcons.tsx b/client_v2/src/components/Clients/blocks/PersistentClientsTable/ServiceIcons.tsx
new file mode 100644
index 00000000000..9e7711e15d2
--- /dev/null
+++ b/client_v2/src/components/Clients/blocks/PersistentClientsTable/ServiceIcons.tsx
@@ -0,0 +1,82 @@
+import { createMemo, Show, For } from 'solid-js';
+
+import { Dropdown } from 'panel/common/ui/Dropdown';
+import { decodeSvg } from 'panel/helpers/helpers';
+
+import s from './PersistentClientsTable.module.pcss';
+
+const MAX_VISIBLE_SERVICES = 3;
+
+export type WebService = {
+ id: string;
+ name: string;
+ icon_svg: string;
+ group_id: string;
+ rules: string[];
+};
+
+type ServiceIconsProps = {
+ serviceIds: string[];
+ serviceMap: Map;
+ maxVisible?: number;
+};
+
+export const ServiceIcons = (props: ServiceIconsProps) => {
+ const maxVisible = () => props.maxVisible ?? MAX_VISIBLE_SERVICES;
+ const visibleIds = createMemo(() => props.serviceIds.slice(0, maxVisible()));
+ const hiddenCount = createMemo(() => props.serviceIds.length - maxVisible());
+
+ return (
+
+
+
+ {(svcId) => {
+ const svc = props.serviceMap.get(svcId);
+ if (!svc) return null;
+ /* eslint-disable solid/no-innerhtml */
+ return (
+
+ );
+ /* eslint-enable solid/no-innerhtml */
+ }}
+
+
+
0}>
+
+ }
+ >
+ {hiddenCount()}
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/Clients/blocks/PersistentClientsTable/TagCell.tsx b/client_v2/src/components/Clients/blocks/PersistentClientsTable/TagCell.tsx
new file mode 100644
index 00000000000..c943c1d9ab9
--- /dev/null
+++ b/client_v2/src/components/Clients/blocks/PersistentClientsTable/TagCell.tsx
@@ -0,0 +1,24 @@
+import { Show } from 'solid-js';
+
+import intl from 'panel/common/intl';
+import theme from 'panel/lib/theme';
+
+import { TagsRow } from './TagsRow';
+
+type TagCellProps = {
+ tags: string[];
+ onCopy?: (text: string) => void;
+};
+
+export const TagCell = (props: TagCellProps) => {
+ return (
+
+
{intl.getMessage('tags_title')}
+
+ 0} fallback={- }>
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/Clients/blocks/PersistentClientsTable/TagsRow.tsx b/client_v2/src/components/Clients/blocks/PersistentClientsTable/TagsRow.tsx
new file mode 100644
index 00000000000..6c136a3a60a
--- /dev/null
+++ b/client_v2/src/components/Clients/blocks/PersistentClientsTable/TagsRow.tsx
@@ -0,0 +1,61 @@
+import { createMemo, Show, For } from 'solid-js';
+import cn from 'clsx';
+
+import intl from 'panel/common/intl';
+import { Dropdown } from 'panel/common/ui/Dropdown';
+import { Icon } from 'panel/common/ui/Icon';
+
+import s from './PersistentClientsTable.module.pcss';
+
+const MAX_VISIBLE_TAGS = 1;
+
+type TagsRowProps = {
+ tags: string[];
+ maxVisible?: number;
+ onCopy?: (text: string) => void;
+};
+
+export const TagsRow = (props: TagsRowProps) => {
+ const maxVisible = () => props.maxVisible ?? MAX_VISIBLE_TAGS;
+ const visible = createMemo(() => props.tags.slice(0, maxVisible()));
+ const hiddenCount = createMemo(() => props.tags.length - maxVisible());
+
+ return (
+
+
+
+ );
+};
diff --git a/client_v2/src/components/Clients/blocks/PersistentClientsTable/index.ts b/client_v2/src/components/Clients/blocks/PersistentClientsTable/index.ts
new file mode 100644
index 00000000000..54b0d69eaad
--- /dev/null
+++ b/client_v2/src/components/Clients/blocks/PersistentClientsTable/index.ts
@@ -0,0 +1 @@
+export { PersistentClientsTable } from './PersistentClientsTable';
diff --git a/client_v2/src/components/Clients/blocks/RuntimeClientsTable/RuntimeClientsTable.module.pcss b/client_v2/src/components/Clients/blocks/RuntimeClientsTable/RuntimeClientsTable.module.pcss
new file mode 100644
index 00000000000..b84040403b9
--- /dev/null
+++ b/client_v2/src/components/Clients/blocks/RuntimeClientsTable/RuntimeClientsTable.module.pcss
@@ -0,0 +1,121 @@
+.table {
+ @media (min-width: 768px) {
+ grid-template-columns: 2fr 2fr 1fr 2fr 1fr;
+ }
+}
+
+.headerCell {
+ @media (min-width: 768px) {
+ width: 100%;
+ min-height: 42px;
+ padding: 0 12px;
+ }
+}
+
+.cell {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ padding: 4px 0;
+
+ @media (min-width: 768px) {
+ padding: 0;
+ }
+}
+
+.cellLabel {
+ width: 100%;
+
+ @media (min-width: 768px) {
+ display: none;
+ }
+}
+
+.cellValue {
+ width: 100%;
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ text-align: right;
+
+ @media (min-width: 768px) {
+ text-align: left;
+ justify-content: initial;
+ }
+}
+
+.whoisCell {
+ position: relative;
+ display: inline-flex;
+ min-width: 0;
+}
+
+.whoisInline {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ min-width: 0;
+}
+
+.whoisRow {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ min-width: 0;
+}
+
+.whoisIcon {
+ width: 16px;
+ height: 16px;
+ flex-shrink: 0;
+}
+
+.whoisText {
+ font-size: 14px;
+ line-height: 1.4;
+ color: var(--default-main-text);
+ min-width: 0;
+}
+
+.tooltip {
+ position: absolute;
+ bottom: calc(100% + 8px);
+ left: 50%;
+ min-width: 180px;
+ max-width: 280px;
+ transform: translateX(-50%);
+ padding: 12px 16px;
+ background: var(--page-background-additional);
+ color: var(--default-main-text);
+ border-radius: 6px;
+ box-shadow: 8px 8px 24px 0 #0000001a;
+ font-size: 12px;
+ line-height: 1.5;
+ white-space: normal;
+ z-index: 10;
+ pointer-events: none;
+ opacity: 0;
+ transition: opacity 0.15s;
+}
+
+.whoisCell:hover .tooltip,
+.whoisCell:focus-within .tooltip {
+ opacity: 1;
+}
+
+.tooltipTitle {
+ font-size: 12px;
+ font-weight: 600;
+ line-height: 1.5;
+ color: var(--default-main-text);
+ margin-bottom: 4px;
+}
+
+.tooltipRow {
+ font-size: 12px;
+ line-height: 1.5;
+ color: var(--default-main-text);
+}
diff --git a/client_v2/src/components/Clients/blocks/RuntimeClientsTable/RuntimeClientsTable.tsx b/client_v2/src/components/Clients/blocks/RuntimeClientsTable/RuntimeClientsTable.tsx
new file mode 100644
index 00000000000..62a863c31f2
--- /dev/null
+++ b/client_v2/src/components/Clients/blocks/RuntimeClientsTable/RuntimeClientsTable.tsx
@@ -0,0 +1,141 @@
+import { createMemo } from 'solid-js';
+
+import intl from 'panel/common/intl';
+import { sortIp } from 'panel/helpers/helpers';
+import type { AutoClient, NormalizedTopClients, WhoisInfo } from 'panel/initialState';
+import { LOCAL_STORAGE_KEYS, LocalStorageHelper } from 'panel/helpers/localStorageHelper';
+import { Table, type TableColumn } from 'panel/common/ui/Table';
+import theme from 'panel/lib/theme';
+
+import { WhoisCell } from './WhoisCell';
+import { TableEmptyState } from '../TableEmptyState/TableEmptyState';
+import s from './RuntimeClientsTable.module.pcss';
+
+type Props = {
+ autoClients: AutoClient[];
+ normalizedTopClients?: NormalizedTopClients;
+ loading?: boolean;
+};
+
+export const RuntimeClientsTable = (props: Props) => {
+ const pageSize = createMemo(
+ () => LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.AUTO_CLIENTS_PAGE_SIZE) || undefined,
+ );
+
+ const columns = createMemo[]>(() => [
+ {
+ key: 'ip',
+ header: {
+ text: intl.getMessage('ip_address'),
+ className: s.headerCell,
+ },
+ accessor: 'ip',
+ sortable: true,
+ sortFn: sortIp,
+ render: (value: string) => (
+
+
{intl.getMessage('ip_address')}
+
+
+
+ {value}
+
+
+
+ ),
+ },
+ {
+ key: 'name',
+ header: {
+ text: intl.getMessage('name_table_header'),
+ className: s.headerCell,
+ },
+ accessor: 'name',
+ sortable: true,
+ render: (value: string) => (
+
+
{intl.getMessage('name_table_header')}
+
+
+
+ {value || '-'}
+
+
+
+ ),
+ },
+ {
+ key: 'source',
+ header: {
+ text: intl.getMessage('source_label'),
+ className: s.headerCell,
+ },
+ accessor: 'source',
+ sortable: true,
+ render: (value: string) => (
+
+
{intl.getMessage('source_label')}
+
+
+ {value || '-'}
+
+
+ ),
+ },
+ {
+ key: 'whois',
+ header: {
+ text: intl.getMessage('whois'),
+ className: s.headerCell,
+ },
+ accessor: 'whois_info',
+ sortable: false,
+ render: (value: WhoisInfo, row: AutoClient) => (
+
+
{intl.getMessage('whois')}
+
+
+
+
+
+ ),
+ },
+ {
+ key: 'requests',
+ header: {
+ text: intl.getMessage('requests_table_header'),
+ className: s.headerCell,
+ },
+ accessor: (row: AutoClient) => props.normalizedTopClients?.auto[row.ip] || 0,
+ sortable: true,
+ render: (_value: unknown, row: AutoClient) => (
+
+
{intl.getMessage('requests_table_header')}
+
+
+
+ {(props.normalizedTopClients?.auto[row.ip] || 0).toLocaleString()}
+
+
+
+ ),
+ },
+ ]);
+
+ const handlePageSizeChange = (newSize: number) => {
+ LocalStorageHelper.setItem(LOCAL_STORAGE_KEYS.AUTO_CLIENTS_PAGE_SIZE, newSize);
+ };
+
+ return (
+
+ data={props.autoClients}
+ class={s.table}
+ columns={columns()}
+ emptyTable={ }
+ loading={props.loading}
+ pageSize={pageSize()}
+ onPageSizeChange={handlePageSizeChange}
+ getRowId={(row) => row.ip}
+ />
+ );
+};
diff --git a/client_v2/src/components/Clients/blocks/RuntimeClientsTable/WhoisCell.tsx b/client_v2/src/components/Clients/blocks/RuntimeClientsTable/WhoisCell.tsx
new file mode 100644
index 00000000000..3b45b552b4d
--- /dev/null
+++ b/client_v2/src/components/Clients/blocks/RuntimeClientsTable/WhoisCell.tsx
@@ -0,0 +1,77 @@
+import { createMemo, Show } from 'solid-js';
+import cn from 'clsx';
+import intl from 'panel/common/intl';
+import { Icon } from 'panel/common/ui/Icon';
+import theme from 'panel/lib/theme';
+
+import type { WhoisInfo } from 'panel/initialState';
+import s from './RuntimeClientsTable.module.pcss';
+
+type Props = {
+ whoisInfo: WhoisInfo;
+ ip: string;
+};
+
+const stripHtml = (str: string) => str.replace(/<\/?span>/g, '');
+
+export const WhoisCell = (props: Props) => {
+ const raw = createMemo(() => props.whoisInfo || {});
+ const country = createMemo(() => raw().country);
+ const orgname = createMemo(() => raw().orgname || raw().org);
+ const hasData = createMemo(() => country() || orgname());
+
+ return (
+ -}>
+
+
+
+
+
+ {country()}
+
+
+
+
+
+
+ {orgname()}
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/Clients/blocks/RuntimeClientsTable/index.ts b/client_v2/src/components/Clients/blocks/RuntimeClientsTable/index.ts
new file mode 100644
index 00000000000..8541aaf8496
--- /dev/null
+++ b/client_v2/src/components/Clients/blocks/RuntimeClientsTable/index.ts
@@ -0,0 +1 @@
+export { RuntimeClientsTable } from './RuntimeClientsTable';
diff --git a/client_v2/src/components/Clients/blocks/TableEmptyState/TableEmptyState.module.pcss b/client_v2/src/components/Clients/blocks/TableEmptyState/TableEmptyState.module.pcss
new file mode 100644
index 00000000000..fb546023166
--- /dev/null
+++ b/client_v2/src/components/Clients/blocks/TableEmptyState/TableEmptyState.module.pcss
@@ -0,0 +1,15 @@
+.emptyTableContent {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+}
+
+.emptyTableIcon {
+ width: 64px;
+ height: 64px;
+}
+
+.emptyTableDesc {
+ color: var(--default-description-text);
+}
diff --git a/client_v2/src/components/Clients/blocks/TableEmptyState/TableEmptyState.tsx b/client_v2/src/components/Clients/blocks/TableEmptyState/TableEmptyState.tsx
new file mode 100644
index 00000000000..b1748282f32
--- /dev/null
+++ b/client_v2/src/components/Clients/blocks/TableEmptyState/TableEmptyState.tsx
@@ -0,0 +1,15 @@
+import cn from 'clsx';
+import intl from 'panel/common/intl';
+import { Icon } from 'panel/common/ui/Icon';
+import theme from 'panel/lib/theme';
+
+import s from './TableEmptyState.module.pcss';
+
+export const TableEmptyState = () => (
+
+
+
+ {intl.getMessage('clients_not_found')}
+
+
+);
diff --git a/client_v2/src/components/Dashboard/Dashboard.module.pcss b/client_v2/src/components/Dashboard/Dashboard.module.pcss
new file mode 100644
index 00000000000..35626bf2300
--- /dev/null
+++ b/client_v2/src/components/Dashboard/Dashboard.module.pcss
@@ -0,0 +1,22 @@
+.statContainer {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 24px;
+ padding: 0 16px;
+
+ @media (min-width: 1024px) {
+ grid-template-columns: 1fr 1fr;
+ padding: 0;
+ }
+}
+
+.loader {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 200px;
+}
+
+.emptyState {
+ flex: 1;
+}
diff --git a/client_v2/src/components/Dashboard/Dashboard.tsx b/client_v2/src/components/Dashboard/Dashboard.tsx
new file mode 100644
index 00000000000..dd302689d74
--- /dev/null
+++ b/client_v2/src/components/Dashboard/Dashboard.tsx
@@ -0,0 +1,200 @@
+import { createSignal, createMemo, createEffect, onCleanup, Show } from 'solid-js';
+
+import theme from 'panel/lib/theme';
+import { PageLoader } from 'panel/common/ui/Loader';
+import { dashboardState, toggleProtection, getClients } from 'panel/stores/dashboard';
+import { statsState, getStats, getStatsConfig } from 'panel/stores/stats';
+import { accessState, getAccessList } from 'panel/stores/access';
+import { ONE_SECOND_IN_MS, HOUR, DAY, STATS_INTERVALS_DAYS } from 'panel/helpers/constants';
+
+import { Header, getPeriodLabel } from './blocks/Header/Header';
+import { StatCards } from './blocks/StatCards';
+import { EmptyState } from './blocks/EmptyState/EmptyState';
+import { GeneralStatistics } from './blocks/GeneralStatistics';
+import { TopClients } from './blocks/TopClients';
+import { TopQueriedDomains } from './blocks/TopQueriedDomains';
+import { TopBlockedDomains } from './blocks/TopBlockedDomains';
+import { TopUpstreams } from './blocks/TopUpstreams';
+import { UpstreamAvgTime } from './blocks/UpstreamAvgTime';
+
+import s from './Dashboard.module.pcss';
+
+export const Dashboard = () => {
+ const [remainingTime, setRemainingTime] = createSignal(null);
+ const [selectedPeriod, setSelectedPeriod] = createSignal(DAY);
+ let timerRef: ReturnType | null = null;
+
+ const startCountdown = (duration: number) => {
+ if (timerRef) {
+ clearInterval(timerRef);
+ }
+ setRemainingTime(duration);
+ timerRef = setInterval(() => {
+ const prev = remainingTime();
+ if (prev !== null && prev > ONE_SECOND_IN_MS) {
+ setRemainingTime(prev - ONE_SECOND_IN_MS);
+ } else {
+ if (timerRef) {
+ clearInterval(timerRef);
+ timerRef = null;
+ }
+ toggleProtection(null);
+ setRemainingTime(null);
+ }
+ }, ONE_SECOND_IN_MS);
+ };
+
+ createEffect(() => {
+ const protectionDisabledDuration = dashboardState.protectionDisabledDuration;
+ if (protectionDisabledDuration && protectionDisabledDuration > 0 && timerRef === null) {
+ startCountdown(protectionDisabledDuration);
+ }
+ });
+
+ onCleanup(() => {
+ if (timerRef) {
+ clearInterval(timerRef);
+ }
+ });
+
+ const effectiveMaxStatsInterval = createMemo(() => {
+ const maxStatsInterval = statsState.interval || DAY;
+ return maxStatsInterval >= HOUR ? maxStatsInterval : DAY;
+ });
+
+ const periodIntervals = createMemo(() => {
+ const intervals = STATS_INTERVALS_DAYS.filter(
+ (interval) => interval <= effectiveMaxStatsInterval(),
+ );
+
+ if (!intervals.includes(effectiveMaxStatsInterval())) {
+ intervals.push(effectiveMaxStatsInterval());
+ }
+
+ return intervals.sort((a, b) => a - b);
+ });
+
+ const periodOptions = createMemo(() =>
+ periodIntervals().map((interval) => ({
+ value: interval,
+ label: getPeriodLabel(interval),
+ })),
+ );
+
+ createEffect(() => {
+ const maxAvailable = periodIntervals()[periodIntervals().length - 1];
+ if (maxAvailable && selectedPeriod() > maxAvailable) {
+ setSelectedPeriod(maxAvailable);
+ }
+ });
+
+ createEffect(() => {
+ const period = selectedPeriod();
+ getStats(period);
+ getStatsConfig();
+ getClients();
+ getAccessList();
+ });
+
+ const handleRefreshStats = () => {
+ getStats(selectedPeriod());
+ getStatsConfig();
+ getClients();
+ getAccessList();
+ };
+
+ const handleToggleProtection = (enabled: boolean, duration?: number) => {
+ if (!enabled && timerRef) {
+ clearInterval(timerRef);
+ timerRef = null;
+ setRemainingTime(null);
+ }
+ toggleProtection(enabled ? duration : null);
+ };
+
+ const handlePeriodChange = (period: number) => {
+ setSelectedPeriod(period);
+ };
+
+ const isLoading = () =>
+ statsState.processingStats || statsState.processingGetConfig || accessState.processing;
+
+ return (
+
+
+ }
+ >
+
+
+
}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/Dashboard/blocks/EmptyState/EmptyState.module.pcss b/client_v2/src/components/Dashboard/blocks/EmptyState/EmptyState.module.pcss
new file mode 100644
index 00000000000..d0c6a1c7bd3
--- /dev/null
+++ b/client_v2/src/components/Dashboard/blocks/EmptyState/EmptyState.module.pcss
@@ -0,0 +1,19 @@
+.emptyState {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ padding-bottom: 24px;
+ text-align: center;
+}
+
+.emptyStateIcon {
+ width: 64px;
+ height: 64px;
+ color: var(--default-gray-icons);
+}
+
+.emptyStateText {
+ max-width: 300px;
+}
diff --git a/client_v2/src/components/Dashboard/blocks/EmptyState/EmptyState.tsx b/client_v2/src/components/Dashboard/blocks/EmptyState/EmptyState.tsx
new file mode 100644
index 00000000000..649c29c1dde
--- /dev/null
+++ b/client_v2/src/components/Dashboard/blocks/EmptyState/EmptyState.tsx
@@ -0,0 +1,49 @@
+import cn from 'clsx';
+
+import intl from 'panel/common/intl';
+import theme from 'panel/lib/theme';
+import { Icon, IconType } from 'panel/common/ui/Icon';
+import { Link } from 'panel/common/ui/Link';
+import { RoutePath, SCROLL_QUERY_KEY } from 'panel/components/Routes/Paths';
+
+import s from './EmptyState.module.pcss';
+
+export type EmptyStateMode = 'default' | 'disabled';
+
+type Props = {
+ class?: string;
+ mode?: EmptyStateMode;
+};
+
+const getEmptyState = (mode: EmptyStateMode) => {
+ if (mode === 'disabled') {
+ return {
+ message: intl.getMessage('period_notify', {
+ a: (text: string) => (
+
+ {text}
+
+ ),
+ }),
+ icon: 'settings_info' as IconType,
+ };
+ }
+ return {
+ message: intl.getMessage('no_stats_yet'),
+ icon: 'not_found_search' as IconType,
+ };
+};
+
+export const EmptyState = (props: Props) => {
+ const state = () => getEmptyState(props.mode || 'default');
+
+ return (
+
+ );
+};
diff --git a/client_v2/src/components/Dashboard/blocks/EmptyState/index.ts b/client_v2/src/components/Dashboard/blocks/EmptyState/index.ts
new file mode 100644
index 00000000000..89798dbd685
--- /dev/null
+++ b/client_v2/src/components/Dashboard/blocks/EmptyState/index.ts
@@ -0,0 +1 @@
+export { EmptyState } from './EmptyState';
diff --git a/client_v2/src/components/Dashboard/blocks/GeneralStatistics/GeneralStatistics.module.pcss b/client_v2/src/components/Dashboard/blocks/GeneralStatistics/GeneralStatistics.module.pcss
new file mode 100644
index 00000000000..9ce3f43862a
--- /dev/null
+++ b/client_v2/src/components/Dashboard/blocks/GeneralStatistics/GeneralStatistics.module.pcss
@@ -0,0 +1,41 @@
+.card {
+ display: flex;
+ flex-direction: column;
+ border-radius: 16px;
+ padding: 8px 8px 0;
+ border: 1px solid var(--default-item-divider);
+ overflow: hidden;
+
+ @media (min-width: 768px) {
+ height: 374px;
+ }
+}
+
+.cardHeader {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16px 16px 24px;
+}
+
+.tableRows {
+ height: 100%;
+ overflow-y: auto;
+}
+
+.rowDivider {
+ display: none;
+ padding: 16px;
+
+ @media (min-width: 768px) {
+ display: block;
+ }
+
+ &:before {
+ content: "";
+ display: block;
+ width: 100%;
+ height: 1px;
+ background-color: var(--default-item-divider);
+ }
+}
diff --git a/client_v2/src/components/Dashboard/blocks/GeneralStatistics/GeneralStatistics.tsx b/client_v2/src/components/Dashboard/blocks/GeneralStatistics/GeneralStatistics.tsx
new file mode 100644
index 00000000000..58bc1f23eeb
--- /dev/null
+++ b/client_v2/src/components/Dashboard/blocks/GeneralStatistics/GeneralStatistics.tsx
@@ -0,0 +1,111 @@
+import { Show } from 'solid-js';
+
+import intl from 'panel/common/intl';
+import theme from 'panel/lib/theme';
+import cn from 'clsx';
+import { EmptyState } from '../EmptyState';
+
+import s from './GeneralStatistics.module.pcss';
+import { StatRow } from '../StatRow';
+
+type Props = {
+ numDnsQueries: number;
+ numBlockedFiltering: number;
+ numReplacedSafebrowsing: number;
+ numReplacedParental: number;
+ numReplacedSafesearch: number;
+ avgProcessingTime: number;
+};
+
+export const GeneralStatistics = (props: Props) => {
+ const blockedPercent = () =>
+ props.numDnsQueries > 0 ? (props.numBlockedFiltering / props.numDnsQueries) * 100 : 0;
+ const safebrowsingPercent = () =>
+ props.numDnsQueries > 0 ? (props.numReplacedSafebrowsing / props.numDnsQueries) * 100 : 0;
+ const parentalPercent = () =>
+ props.numDnsQueries > 0 ? (props.numReplacedParental / props.numDnsQueries) * 100 : 0;
+ const safesearchPercent = () =>
+ props.numDnsQueries > 0 ? (props.numReplacedSafesearch / props.numDnsQueries) * 100 : 0;
+
+ const hasStats = () => props.numDnsQueries > 0;
+
+ return (
+
+
+
+
}>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/Dashboard/blocks/GeneralStatistics/index.ts b/client_v2/src/components/Dashboard/blocks/GeneralStatistics/index.ts
new file mode 100644
index 00000000000..d76d073ce4d
--- /dev/null
+++ b/client_v2/src/components/Dashboard/blocks/GeneralStatistics/index.ts
@@ -0,0 +1 @@
+export { GeneralStatistics } from './GeneralStatistics';
diff --git a/client_v2/src/components/Dashboard/blocks/Header/Header.module.pcss b/client_v2/src/components/Dashboard/blocks/Header/Header.module.pcss
new file mode 100644
index 00000000000..813e6c9965a
--- /dev/null
+++ b/client_v2/src/components/Dashboard/blocks/Header/Header.module.pcss
@@ -0,0 +1,209 @@
+.header {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ padding: 0 16px;
+ margin: 0 0 32px;
+
+ @media (min-width: 1024px) {
+ flex-direction: row;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+ gap: 32px 16px;
+ padding: 0;
+ }
+}
+
+.headerLeft {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+
+ @media (min-width: 768px) {
+ flex-direction: row;
+ align-items: center;
+ }
+
+ @media (min-width: 1024px) {
+ flex: 1;
+ }
+}
+
+.headerRight {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+
+ @media (min-width: 768px) {
+ align-items: center;
+ flex-direction: row;
+ }
+}
+
+.toggleRow {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+
+ @media (min-width: 768px) {
+ position: relative;
+ top: 2px;
+ margin-left: 8px;
+ }
+}
+
+.periodSelect {
+ width: 100%;
+ min-width: 0;
+
+ @media (min-width: 768px) {
+ width: auto;
+ max-width: 100%;
+
+ &:global(.react-select.react-select-responsive) {
+ width: fit-content;
+ max-width: 100%;
+ min-width: 0;
+ }
+ }
+
+ & :global(.select__control) {
+ min-width: 100%;
+ max-width: 100%;
+ }
+
+ & :global(.select__value-container) {
+ min-width: 0;
+ max-width: 100%;
+ }
+
+ & :global(.select__single-value) {
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+}
+
+.protectionMenu {
+ padding: 8px 0;
+ min-width: 200px;
+}
+
+.periodSettingsFooter {
+ align-items: flex-start;
+ padding: 24px 16px;
+ border-top: 1px solid var(--default-item-divider);
+}
+
+.titleRow {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+
+ @media (min-width: 768px) {
+ display: none;
+ }
+}
+
+.refreshButton {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ padding: 8px;
+ font-size: 16px;
+ color: var(--default-main-text);
+ border: 0;
+ background: none;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: var(--t2) background-color;
+
+ &:hover,
+ &:focus {
+ background: var(--hovered-secondary-button);
+ }
+
+ &:active {
+ background: var(--pressed-secondary-button);
+ }
+
+ &:disabled {
+ background: var(--disabled-secondary-button);
+ color: var(--disabled-secondary-button-stroke);
+ cursor: default;
+ }
+
+ & svg {
+ width: 24px;
+ height: 24px;
+ flex-shrink: 0;
+ }
+}
+
+.refreshDesktopButton {
+ width: fit-content;
+}
+
+.refreshMobileButton {
+ width: 32px;
+ height: 32px;
+ padding: 0;
+}
+
+.dropdownTrigger {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 4px;
+ background: none;
+ border: 0;
+ border-radius: 8px;
+ cursor: pointer;
+ color: var(--default-secondary-text);
+ transition: var(--t2) background-color;
+
+ &:hover,
+ &:focus {
+ background: var(--hovered-secondary-button);
+ }
+
+ &:active {
+ background: var(--pressed-secondary-button);
+ }
+
+ &:disabled {
+ background: var(--disabled-secondary-button);
+ color: var(--disabled-secondary-button-stroke);
+ cursor: default;
+ }
+}
+
+.onlyMobile {
+ @media (min-width: 768px) {
+ display: none;
+ }
+}
+
+.onlyDesktop {
+ display: none;
+
+ @media (min-width: 768px) {
+ display: flex;
+ }
+}
+
+.protectionToggle {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+}
+
+.cardSubtitle {
+ font-size: 14px;
+ color: var(--default-secondary-text);
+}
diff --git a/client_v2/src/components/Dashboard/blocks/Header/Header.tsx b/client_v2/src/components/Dashboard/blocks/Header/Header.tsx
new file mode 100644
index 00000000000..413084bf129
--- /dev/null
+++ b/client_v2/src/components/Dashboard/blocks/Header/Header.tsx
@@ -0,0 +1,246 @@
+import { createSignal, Show, For } from 'solid-js';
+import cn from 'clsx';
+
+import intl from 'panel/common/intl';
+import theme from 'panel/lib/theme';
+import { Switch } from 'panel/common/controls/Switch';
+import { Dropdown } from 'panel/common/ui/Dropdown';
+import { Select } from 'panel/common/controls/Select';
+import { Icon } from 'panel/common/ui/Icon';
+import { Link } from 'panel/common/ui/Link';
+import { RoutePath, SCROLL_QUERY_KEY } from 'panel/components/Routes/Paths';
+import { useIsMobile } from 'panel/hooks/useIsMobile';
+import { DISABLE_PROTECTION_TIMINGS, ONE_SECOND_IN_MS } from 'panel/helpers/constants';
+import { msToSeconds, msToMinutes, msToHours } from 'panel/helpers/helpers';
+
+import s from './Header.module.pcss';
+
+const DISABLE_PROTECTION_ITEMS = [
+ { key: 'half_minute', time: DISABLE_PROTECTION_TIMINGS.HALF_MINUTE },
+ { key: 'minute', time: DISABLE_PROTECTION_TIMINGS.MINUTE },
+ { key: 'ten_minutes', time: DISABLE_PROTECTION_TIMINGS.TEN_MINUTES },
+ { key: 'hour', time: DISABLE_PROTECTION_TIMINGS.HOUR },
+ { key: 'tomorrow', time: DISABLE_PROTECTION_TIMINGS.TOMORROW },
+];
+
+export const getPeriodLabel = (interval: number) => {
+ const hours = interval / (60 * 60 * 1000);
+ if (hours === 24) {
+ return intl.getPlural('last_hours', 24);
+ }
+ const days = hours / 24;
+ if (Number.isInteger(days)) {
+ if (days === 7) return intl.getPlural('last_days', 7);
+ if (days === 30) return intl.getPlural('last_days', 30);
+ if (days === 90) return intl.getPlural('last_days', 90);
+ return intl.getPlural('last_days', days);
+ }
+ return intl.getPlural('last_hours', Math.floor(hours));
+};
+
+const getDisableText = (key: string, time: number) => {
+ switch (key) {
+ case 'half_minute':
+ return intl.getPlural('pause_for_seconds', msToSeconds(time));
+ case 'minute':
+ case 'ten_minutes':
+ return intl.getPlural('pause_for_minutes', msToMinutes(time));
+ case 'hour':
+ return intl.getMessage('pause_for_hour', { count: msToHours(time) });
+ case 'tomorrow': {
+ const now = new Date();
+ const tomorrowTime = now.toLocaleTimeString([], {
+ hour: '2-digit',
+ minute: '2-digit',
+ });
+ return intl.getMessage('pause_until_tomorrow', { time: tomorrowTime });
+ }
+ default:
+ return '';
+ }
+};
+
+const getRemainingTimeText = (milliseconds: number) => {
+ if (!milliseconds) return '';
+
+ const date = new Date(milliseconds);
+ const hh = date.getUTCHours();
+ const mm = `0${date.getUTCMinutes()}`.slice(-2);
+ const ss = `0${date.getUTCSeconds()}`.slice(-2);
+ const formattedHH = `0${hh}`.slice(-2);
+
+ return hh ? `${formattedHH}:${mm}:${ss}` : `${mm}:${ss}`;
+};
+
+type Props = {
+ protectionEnabled: boolean;
+ processingProtection: boolean;
+ remainingTime: number | null;
+ selectedPeriod: number;
+ periodOptions: Array<{ value: number; label: string }>;
+ isLoading: boolean;
+ onToggleProtection: (enabled: boolean, duration?: number) => void;
+ onRefreshStats: () => void;
+ onPeriodChange: (period: number) => void;
+};
+
+export const Header = (props: Props) => {
+ const [protectionMenuOpen, setProtectionMenuOpen] = createSignal(false);
+ const [selectedDisableTime, setSelectedDisableTime] = createSignal(null);
+ const isMobile = useIsMobile();
+
+ const handleToggleProtection = () => {
+ props.onToggleProtection(props.protectionEnabled);
+ };
+
+ const handleDisableProtection = (time: number) => {
+ const duration = time - ONE_SECOND_IN_MS;
+ setSelectedDisableTime(time);
+ props.onToggleProtection(props.protectionEnabled, duration);
+ setProtectionMenuOpen(false);
+ };
+
+ const protectionMenu = (
+
+ );
+
+ const periodSettingsFooter = (
+
+ );
+
+ return (
+
+ );
+};
diff --git a/client_v2/src/components/Dashboard/blocks/SortableTableHeader/SortableTableHeader.module.pcss b/client_v2/src/components/Dashboard/blocks/SortableTableHeader/SortableTableHeader.module.pcss
new file mode 100644
index 00000000000..d49a193a8f7
--- /dev/null
+++ b/client_v2/src/components/Dashboard/blocks/SortableTableHeader/SortableTableHeader.module.pcss
@@ -0,0 +1,32 @@
+.tableHeader {
+ display: none;
+ justify-content: space-between;
+ padding: 0 16px 8px;
+
+ @media (min-width: 768px) {
+ display: flex;
+ }
+}
+
+.sortableHeader {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ cursor: pointer;
+ user-select: none;
+ padding: 0;
+ border: 0;
+ background: none;
+ color: inherit;
+ font: inherit;
+}
+
+.sortIcon {
+ width: 16px;
+ height: 16px;
+ transition: transform var(--t2);
+}
+
+.sortIconAsc {
+ transform: rotate(180deg);
+}
diff --git a/client_v2/src/components/Dashboard/blocks/SortableTableHeader/SortableTableHeader.tsx b/client_v2/src/components/Dashboard/blocks/SortableTableHeader/SortableTableHeader.tsx
new file mode 100644
index 00000000000..94328186e50
--- /dev/null
+++ b/client_v2/src/components/Dashboard/blocks/SortableTableHeader/SortableTableHeader.tsx
@@ -0,0 +1,44 @@
+import cn from 'clsx';
+
+import { Icon } from 'panel/common/ui/Icon';
+import theme from 'panel/lib/theme';
+
+import s from './SortableTableHeader.module.pcss';
+
+type SortField = 'name' | 'count';
+type SortDirection = 'asc' | 'desc';
+
+type Props = {
+ nameLabel: string;
+ countLabel: string;
+ sortField: SortField;
+ sortDirection: SortDirection;
+ onSort: (field: SortField) => void;
+};
+
+export const SortableTableHeader = (props: Props) => (
+
+
+
+
+);
diff --git a/client_v2/src/components/Dashboard/blocks/SortableTableHeader/index.ts b/client_v2/src/components/Dashboard/blocks/SortableTableHeader/index.ts
new file mode 100644
index 00000000000..ff14ba75d4f
--- /dev/null
+++ b/client_v2/src/components/Dashboard/blocks/SortableTableHeader/index.ts
@@ -0,0 +1 @@
+export { SortableTableHeader } from './SortableTableHeader';
diff --git a/client_v2/src/components/Dashboard/blocks/StatCard/StatCard.module.pcss b/client_v2/src/components/Dashboard/blocks/StatCard/StatCard.module.pcss
new file mode 100644
index 00000000000..6bf371639bc
--- /dev/null
+++ b/client_v2/src/components/Dashboard/blocks/StatCard/StatCard.module.pcss
@@ -0,0 +1,140 @@
+.statsCards {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 24px;
+ padding: 0 16px;
+ margin-bottom: 32px;
+
+ @media (min-width: 1024px) {
+ grid-template-columns: repeat(4, 1fr);
+ padding: 0;
+ }
+}
+
+.statCard {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.statCardAds {
+ & .statCardPercent,
+ & .statCardLabel {
+ color: var(--default-error-link);
+ }
+}
+
+.statCardThreats {
+ & .statCardPercent,
+ & .statCardLabel {
+ color: var(--default-attention-link);
+ }
+}
+
+.statCardAdult {
+ & .statCardPercent,
+ & .statCardLabel {
+ color: var(--default-additional-link);
+ }
+}
+
+.statCardInner {
+ display: flex;
+ flex-direction: column;
+ border-radius: 8px;
+ border: 1px solid var(--default-item-divider);
+}
+
+.statCardHeader {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ flex-wrap: wrap;
+ padding: 8px 8px 0;
+
+ @media (min-width: 768px) {
+ padding: 16px 16px 0;
+ }
+
+ & .statCardLabel {
+ display: block;
+ width: 100%;
+
+ @media (min-width: 768px) {
+ display: none;
+ }
+ }
+}
+
+.statCardHeaderLeft {
+ display: flex;
+ flex-direction: column;
+}
+
+.statCardLabel {
+ display: none;
+
+ @media (min-width: 768px) {
+ display: block;
+ }
+}
+
+.statLabelLink {
+ color: inherit;
+ text-decoration: none;
+
+ &:hover,
+ &:focus {
+ color: inherit;
+ text-decoration: underline;
+ }
+}
+
+.statCardValue {
+ font-size: 16px;
+ color: var(--default-main-text);
+ font-weight: var(--weight-bold);
+
+ @media (min-width: 768px) {
+ font-size: 28px;
+ font-weight: var(--weight-regular);
+ }
+}
+
+.statCardChart {
+ min-height: 16px;
+ height: 60px;
+ margin-top: auto;
+ padding: 0 8px;
+ overflow: hidden;
+
+ @media (min-width: 768px) {
+ padding: 0 16px;
+ }
+
+ canvas {
+ width: 100% !important;
+ height: 100% !important;
+ display: block;
+ }
+}
+
+.chartTooltip {
+ background: var(--default-cards-background);
+ border: 1px solid var(--default-item-divider);
+ border-radius: 8px;
+ padding: 8px 12px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+}
+
+.chartTooltipValue {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--default-main-text);
+}
+
+.chartTooltipDate {
+ font-size: 12px;
+ color: var(--default-secondary-text);
+ margin-top: 2px;
+}
diff --git a/client_v2/src/components/Dashboard/blocks/StatCard/StatCard.tsx b/client_v2/src/components/Dashboard/blocks/StatCard/StatCard.tsx
new file mode 100644
index 00000000000..41488fd2434
--- /dev/null
+++ b/client_v2/src/components/Dashboard/blocks/StatCard/StatCard.tsx
@@ -0,0 +1,211 @@
+import { createMemo, Show, untrack } from 'solid-js';
+import cn from 'clsx';
+import {
+ Chart,
+ LineController,
+ LineElement,
+ PointElement,
+ LinearScale,
+ CategoryScale,
+ Tooltip,
+ Filler,
+ type ScriptableContext,
+} from 'chart.js';
+import { Link } from 'panel/common/ui/Link';
+import { type RoutePathKey } from 'panel/components/Routes/Paths';
+
+import { formatNumber } from 'panel/helpers/helpers';
+import {
+ useChart,
+ createCursorLinePlugin,
+ createExternalTooltipHandler,
+} from 'panel/helpers/useChart';
+import intl from 'panel/common/intl';
+import theme from 'panel/lib/theme';
+
+import s from './StatCard.module.pcss';
+
+Chart.register(
+ LineController,
+ LineElement,
+ PointElement,
+ LinearScale,
+ CategoryScale,
+ Tooltip,
+ Filler,
+);
+
+export const CARDS_THEME = {
+ QUERIES: 'queries',
+ ADS: 'ads',
+ THREATS: 'threats',
+ ADULT: 'adult',
+};
+
+export const CARDS_COLORS = {
+ QUERIES: '#7F7F7F',
+ ADS: '#E07575',
+ THREATS: '#F5A623',
+ ADULT: '#9B59B6',
+};
+
+const formatDate = (date: Date): string => {
+ return date.toLocaleDateString(intl.getUILanguage(), {
+ day: 'numeric',
+ month: 'short',
+ year: 'numeric',
+ });
+};
+
+export type StatCardProps = {
+ value: number;
+ label: string;
+ data: number[];
+ color: string;
+ percentValue?: number;
+ cardTheme: (typeof CARDS_THEME)[keyof typeof CARDS_THEME];
+ linkTo?: RoutePathKey;
+ query?: Record;
+};
+
+export const StatCard = (props: StatCardProps) => {
+ // Ensure the chart has at least 2 data points
+ const paddedData = () => (props.data.length < 2 ? [0, ...props.data] : props.data);
+
+ const chartData = createMemo(() => {
+ const data = paddedData();
+ const labels = data.map((_, i) => {
+ const date = new Date();
+ date.setDate(date.getDate() - (data.length - 1 - i));
+ return formatDate(date);
+ });
+ return {
+ labels,
+ datasets: [
+ {
+ data: data,
+ borderColor: props.color,
+ borderWidth: 1,
+ backgroundColor: (context: ScriptableContext<'line'>) => {
+ const ctx = context.chart.ctx;
+ const gradient = ctx.createLinearGradient(
+ 0,
+ 0,
+ 0,
+ context.chart.height || 100,
+ );
+ gradient.addColorStop(0, `${props.color}4D`);
+ gradient.addColorStop(1, `${props.color}00`);
+ return gradient;
+ },
+ fill: true,
+ clip: false as const,
+ pointRadius: 0,
+ pointHoverRadius: 4,
+ pointHoverBackgroundColor: props.color,
+ tension: 0.4,
+ },
+ ],
+ };
+ });
+
+ const cursorLinePlugin = createCursorLinePlugin(untrack(() => props.color));
+
+ const externalTooltipHandler = createExternalTooltipHandler(
+ () => tooltipEl,
+ (dataPoint) => {
+ const raw = dataPoint.raw as number;
+ const label = dataPoint.label || '';
+ return `${formatNumber(raw)}
${label}
`;
+ },
+ );
+
+ let tooltipEl!: HTMLDivElement;
+ const setTooltipRef = (el: HTMLDivElement) => {
+ tooltipEl = el;
+ };
+
+ const chartOptions = createMemo(() => ({
+ responsive: true,
+ maintainAspectRatio: false,
+ animation: false as const,
+ layout: {
+ padding: { bottom: 12 },
+ },
+ plugins: {
+ tooltip: {
+ enabled: false,
+ external: externalTooltipHandler,
+ },
+ legend: { display: false },
+ },
+ scales: {
+ x: { display: false },
+ y: { display: false, min: 0 },
+ },
+ interaction: {
+ intersect: false,
+ mode: 'index' as const,
+ },
+ elements: {
+ line: { tension: 0.4 },
+ },
+ }));
+
+ const percent = () => props.percentValue ?? 0;
+
+ const setCanvasRef = untrack(() => useChart(chartData, chartOptions, [cursorLinePlugin]));
+
+ return (
+
+
+
+
+
+ {props.label}
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/Dashboard/blocks/StatCard/index.ts b/client_v2/src/components/Dashboard/blocks/StatCard/index.ts
new file mode 100644
index 00000000000..be803e5dc7f
--- /dev/null
+++ b/client_v2/src/components/Dashboard/blocks/StatCard/index.ts
@@ -0,0 +1,2 @@
+export { StatCard, CARDS_THEME, CARDS_COLORS } from './StatCard';
+export type { StatCardProps } from './StatCard';
diff --git a/client_v2/src/components/Dashboard/blocks/StatCards/StatCards.tsx b/client_v2/src/components/Dashboard/blocks/StatCards/StatCards.tsx
new file mode 100644
index 00000000000..cfbbc3ea8ca
--- /dev/null
+++ b/client_v2/src/components/Dashboard/blocks/StatCards/StatCards.tsx
@@ -0,0 +1,69 @@
+import intl from 'panel/common/intl';
+import { RoutePath } from 'panel/components/Routes/Paths';
+import { QUERY_LOG_REASON_FILTER, QUERY_LOG_STATUS_FILTER } from 'panel/helpers/constants';
+
+import s from '../StatCard/StatCard.module.pcss';
+import { StatCard, CARDS_THEME, CARDS_COLORS } from '../StatCard';
+
+type Props = {
+ numDnsQueries: number;
+ numBlockedFiltering: number;
+ numReplacedSafebrowsing: number;
+ numReplacedParental: number;
+ dnsQueries: number[];
+ blockedFiltering: number[];
+ replacedSafebrowsing: number[];
+ replacedParental: number[];
+};
+
+export const StatCards = (props: Props) => {
+ const blockedPercent = () =>
+ props.numDnsQueries > 0 ? (props.numBlockedFiltering / props.numDnsQueries) * 100 : 0;
+ const threatsPercent = () =>
+ props.numDnsQueries > 0 ? (props.numReplacedSafebrowsing / props.numDnsQueries) * 100 : 0;
+ const parentalPercent = () =>
+ props.numDnsQueries > 0 ? (props.numReplacedParental / props.numDnsQueries) * 100 : 0;
+
+ return (
+
+
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/Dashboard/blocks/StatCards/index.ts b/client_v2/src/components/Dashboard/blocks/StatCards/index.ts
new file mode 100644
index 00000000000..149871d1384
--- /dev/null
+++ b/client_v2/src/components/Dashboard/blocks/StatCards/index.ts
@@ -0,0 +1 @@
+export { StatCards } from './StatCards';
diff --git a/client_v2/src/components/Dashboard/blocks/StatRow/StatRow.module.pcss b/client_v2/src/components/Dashboard/blocks/StatRow/StatRow.module.pcss
new file mode 100644
index 00000000000..747d21562a5
--- /dev/null
+++ b/client_v2/src/components/Dashboard/blocks/StatRow/StatRow.module.pcss
@@ -0,0 +1,173 @@
+.statRow {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 0 24px;
+ padding: 16px;
+ transition: background-color var(--t2);
+
+ @media (min-width: 768px) {
+ padding: 8px 16px;
+ flex-wrap: nowrap;
+ }
+
+ &.dnsQueries {
+ & .tableRowIcon {
+ stroke: var(--default-gray-icons);
+ }
+ }
+
+ &.adsBlocked {
+ & .queryCount {
+ color: var(--default-error-link);
+ }
+
+ & .tableRowIcon {
+ stroke: var(--default-error-icon);
+ }
+
+ & .queryBarFill {
+ background: var(--default-error-icon);
+ }
+ }
+
+ &.threatsBlocked {
+ & .queryCount {
+ color: var(--default-attention-icon-stroke);
+ }
+
+ & .tableRowIcon {
+ stroke: var(--default-attention-icon-stroke);
+ }
+
+ & .queryBarFill {
+ background: var(--default-attention-icon-stroke);
+ }
+ }
+
+ &.adultWebsitesBlocked {
+ & .queryCount {
+ color: var(--default-product-secondary-icon-stroke);
+ }
+
+ & .tableRowIcon {
+ stroke: var(--default-product-secondary-icon-stroke);
+ }
+
+ & .queryBarFill {
+ background: var(--default-product-secondary-icon-stroke);
+ }
+ }
+
+ &.safeSearchUsed {
+ & .queryCount {
+ color: var(--default-product-icon);
+ }
+
+ & .tableRowIcon {
+ stroke: var(--default-product-icon);
+ }
+
+ & .queryBarFill {
+ background: var(--default-product-icon);
+ }
+ }
+}
+
+.statRowLeft {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.statRowIcon {
+ width: 20px;
+ height: 20px;
+ color: var(--default-secondary-text);
+}
+
+.statRowLabel {
+ font-size: 14px;
+ color: var(--default-main-text);
+}
+
+.statRowValue {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+
+ & .queryBar {
+ min-width: 141px;
+ }
+}
+
+.statRowPercent {
+ font-size: 12px;
+ color: var(--default-secondary-text);
+ margin-left: 4px;
+}
+
+.tableRowIcon {
+ width: 24px;
+ height: 24px;
+ stroke: var(--default-gray-icons);
+ color: var(--default-gray-icons);
+}
+
+.queryCount {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ white-space: nowrap;
+}
+
+.dropdownWrapper {
+ flex-shrink: 0;
+}
+
+.queryPercent {
+ font-size: 12px;
+ color: var(--default-description-text);
+}
+
+.queryBar {
+ display: none;
+ width: 100%;
+ min-width: 102px;
+ height: 3px;
+ background: var(--default-item-divider);
+ border-radius: 2px;
+
+ @media (min-width: 768px) {
+ display: block;
+ }
+}
+
+.queryBarMobile {
+ display: block;
+ margin: 16px 0 0;
+
+ @media (min-width: 768px) {
+ display: none;
+ }
+}
+
+.queryBarFill {
+ height: 100%;
+ background: var(--default-tertiary-stat);
+}
+
+.queryTooltip {
+ padding: 8px 12px;
+ white-space: nowrap;
+}
+
+.queryTooltipOverlay {
+ pointer-events: none;
+}
+
+.statTooltip {
+ max-width: 280px;
+ padding: 16px;
+}
diff --git a/client_v2/src/components/Dashboard/blocks/StatRow/StatRow.tsx b/client_v2/src/components/Dashboard/blocks/StatRow/StatRow.tsx
new file mode 100644
index 00000000000..2bc0f74e015
--- /dev/null
+++ b/client_v2/src/components/Dashboard/blocks/StatRow/StatRow.tsx
@@ -0,0 +1,122 @@
+import { Show } from 'solid-js';
+
+import intl from 'panel/common/intl';
+import { Icon, type IconType } from 'panel/common/ui/Icon';
+import theme from 'panel/lib/theme';
+import { Dropdown } from 'panel/common/ui/Dropdown';
+import cn from 'clsx';
+import { formatCompactNumber, formatNumber } from 'panel/helpers/helpers';
+
+import s from './StatRow.module.pcss';
+
+export type StatRowProps = {
+ icon: IconType;
+ label: string;
+ value: string | number;
+ percent?: number;
+ isTotal?: boolean;
+ isQueriesValue?: boolean;
+ tooltip: string;
+ rowTheme:
+ | 'dnsQueries'
+ | 'adsBlocked'
+ | 'threatsBlocked'
+ | 'adultWebsitesBlocked'
+ | 'safeSearchUsed'
+ | 'averageProcessingTime';
+};
+
+export const StatRow = (props: StatRowProps) => {
+ const isQueriesValue = () => props.isQueriesValue !== false;
+
+ return (
+
+ {props.tooltip}
}
+ >
+
+
+ {props.label}
+
+
+
+
+
+ {props.value}
+
+ }
+ >
+
+
+ {typeof props.value === 'number'
+ ? formatNumber(props.value)
+ : props.value}{' '}
+ {intl.getMessage('queries').toLowerCase()}
+
+ }
+ >
+
+ {typeof props.value === 'number'
+ ? formatCompactNumber(props.value)
+ : props.value}
+
+
+ 0
+ }
+ >
+ ({props.percent!.toFixed(1)}%)
+
+ }
+ >
+ ({intl.getMessage('total')})
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/Dashboard/blocks/StatRow/index.ts b/client_v2/src/components/Dashboard/blocks/StatRow/index.ts
new file mode 100644
index 00000000000..5b4cff6a7b4
--- /dev/null
+++ b/client_v2/src/components/Dashboard/blocks/StatRow/index.ts
@@ -0,0 +1 @@
+export { StatRow } from './StatRow';
diff --git a/client_v2/src/components/Dashboard/blocks/TableCard.module.pcss b/client_v2/src/components/Dashboard/blocks/TableCard.module.pcss
new file mode 100644
index 00000000000..cccacc3bc6b
--- /dev/null
+++ b/client_v2/src/components/Dashboard/blocks/TableCard.module.pcss
@@ -0,0 +1,183 @@
+.card {
+ display: flex;
+ flex-direction: column;
+ border-radius: 16px;
+ padding: 8px 0 0;
+ border: 1px solid var(--default-item-divider);
+ overflow: hidden;
+
+ @media (min-width: 768px) {
+ height: 374px;
+ padding: 8px 8px 0;
+ }
+}
+
+.cardBlocked {
+ & .cardSubtitle {
+ color: var(--default-error-link);
+ }
+
+ & .queryCount {
+ color: var(--default-error-link);
+ }
+
+ & .queryBarFill {
+ background: var(--default-error-link);
+ }
+}
+
+.cardHeader {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16px 24px 24px;
+
+ @media (min-width: 768px) {
+ padding: 16px 16px 24px;
+ }
+}
+
+.cardSubtitle {
+ color: var(--default-description-text);
+}
+
+.tableRow {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ align-items: center;
+ gap: 0 24px;
+ padding: 16px 24px 24px;
+ transition: background-color var(--t2);
+ border-bottom: 1px solid var(--default-item-divider);
+
+ @media (min-width: 768px) {
+ padding: 8px 16px;
+ border: none;
+ }
+
+ &:last-child {
+ border: none;
+ }
+
+ & > .queryBar {
+ display: block;
+ width: 100%;
+
+ @media (min-width: 768px) {
+ display: none;
+ }
+ }
+}
+
+.tableRowLeft {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex: 1;
+ min-width: 0;
+}
+
+.tableRowRight {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+}
+
+.tableRowIcon {
+ width: 24px;
+ height: 24px;
+ stroke: var(--default-gray-icons);
+ color: var(--default-gray-icons);
+}
+
+.tableRows {
+ height: 100%;
+
+ @media (min-width: 768px) {
+ overflow-y: auto;
+ }
+}
+
+.domainIcon {
+ width: 16px;
+ height: 16px;
+ flex-shrink: 0;
+}
+
+.domainName {
+ color: var(--default-main-text);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.queryCount {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ white-space: nowrap;
+ flex-shrink: 0;
+}
+
+.dropdownWrapper {
+ flex-shrink: 0;
+}
+
+.queryPercent {
+ font-size: 12px;
+ color: var(--default-description-text);
+}
+
+.queryBar {
+ display: none;
+ width: 100%;
+ min-width: 102px;
+ height: 3px;
+ background: var(--default-item-divider);
+ border-radius: 2px;
+
+ @media (min-width: 768px) {
+ display: block;
+ }
+}
+
+.queryBarFill {
+ height: 100%;
+ background: var(--default-tertiary-stat);
+}
+
+.queryTooltip {
+ padding: 8px 12px;
+ white-space: nowrap;
+}
+
+.queryTooltipOverlay {
+ pointer-events: none;
+}
+
+.statRowValue {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+
+ & .queryBar {
+ min-width: 141px;
+ }
+}
+
+.tableRowDot {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+
+ &:before {
+ content: '';
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background-color: var(--default-gray-icons);
+ }
+}
diff --git a/client_v2/src/components/Dashboard/blocks/TopBlockedDomains/TopBlockedDomains.tsx b/client_v2/src/components/Dashboard/blocks/TopBlockedDomains/TopBlockedDomains.tsx
new file mode 100644
index 00000000000..ae54b7f3a02
--- /dev/null
+++ b/client_v2/src/components/Dashboard/blocks/TopBlockedDomains/TopBlockedDomains.tsx
@@ -0,0 +1,164 @@
+import { Show, For, createMemo } from 'solid-js';
+import cn from 'clsx';
+
+import { useIsDesktop } from 'panel/helpers/useMediaQuery';
+import { MOBILE_TABLE_MAX_ROWS } from 'panel/helpers/constants';
+import intl from 'panel/common/intl';
+import { Icon } from 'panel/common/ui/Icon';
+import { Dropdown } from 'panel/common/ui/Dropdown';
+import { formatNumber, formatCompactNumber } from 'panel/helpers/helpers';
+import theme from 'panel/lib/theme';
+import { getTrackerData } from 'panel/helpers/trackers/trackers';
+import { SortableTableHeader } from '../SortableTableHeader';
+import { TrackerTooltip } from '../TrackerTooltip';
+import { EmptyState } from '../EmptyState';
+import { useSortedData } from '../../hooks/useSortedData';
+
+import s from '../TableCard.module.pcss';
+
+type DomainInfo = {
+ name: string;
+ count: number;
+};
+
+type Props = {
+ topBlockedDomains: DomainInfo[];
+ numBlockedFiltering: number;
+};
+
+export const TopBlockedDomains = (props: Props) => {
+ const isDesktop = useIsDesktop();
+ const {
+ sortedData: sortedDomains,
+ sortField,
+ sortDirection,
+ handleSort,
+ } = useSortedData(() => props.topBlockedDomains);
+ const visibleDomains = createMemo(() =>
+ isDesktop() ? sortedDomains() : sortedDomains().slice(0, MOBILE_TABLE_MAX_ROWS),
+ );
+
+ const hasStats = createMemo(() => props.topBlockedDomains.length > 0);
+
+ return (
+
+
+
+
+
+
+
+
+
}>
+
+ {(domain) => {
+ const percent = createMemo(() =>
+ props.numBlockedFiltering > 0
+ ? (domain.count / props.numBlockedFiltering) * 100
+ : 0,
+ );
+ const trackerData = getTrackerData(domain.name);
+
+ return (
+
+
+ }
+ >
+ }
+ trigger="hover"
+ position="bottomLeft"
+ noIcon
+ >
+
+
+
+ {domain.name}
+
+
+
+
+
+ {formatNumber(domain.count)}{' '}
+ {intl.getMessage('queries').toLowerCase()}
+
+ }
+ >
+
+ {formatCompactNumber(domain.count)}
+
+
+ ({percent().toFixed(2)}%)
+
+
+
+
+
+
+
+
+
+
+ );
+ }}
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/Dashboard/blocks/TopBlockedDomains/index.ts b/client_v2/src/components/Dashboard/blocks/TopBlockedDomains/index.ts
new file mode 100644
index 00000000000..ae34ad80b86
--- /dev/null
+++ b/client_v2/src/components/Dashboard/blocks/TopBlockedDomains/index.ts
@@ -0,0 +1 @@
+export { TopBlockedDomains } from './TopBlockedDomains';
diff --git a/client_v2/src/components/Dashboard/blocks/TopClients/TopClients.module.pcss b/client_v2/src/components/Dashboard/blocks/TopClients/TopClients.module.pcss
new file mode 100644
index 00000000000..fc9f0deb792
--- /dev/null
+++ b/client_v2/src/components/Dashboard/blocks/TopClients/TopClients.module.pcss
@@ -0,0 +1,282 @@
+.card {
+ display: flex;
+ flex-direction: column;
+ border-radius: 16px;
+ padding: 8px 0 0;
+ border: 1px solid var(--default-item-divider);
+ overflow: hidden;
+
+ @media (min-width: 768px) {
+ max-height: 374px;
+ padding: 8px 8px 0;
+ }
+}
+
+.cardHeader {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16px 24px 24px;
+
+ @media (min-width: 768px) {
+ padding: 16px 16px 24px;
+ }
+}
+
+.cardSubtitle {
+ color: var(--default-description-text);
+}
+
+.tableRows {
+ height: 100%;
+ overflow-y: auto;
+}
+
+.tableRowRight {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+
+ & .clientBlocked {
+ display: block;
+
+ @media (min-width: 768px) {
+ display: none;
+ }
+ }
+}
+
+.tableRowIcon {
+ width: 24px;
+ height: 24px;
+ stroke: var(--default-gray-icons);
+ color: var(--default-gray-icons);
+}
+
+.tableRowInfo {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ margin: 8px 0 0;
+
+ @media (min-width: 768px) {
+ margin: 0;
+ padding-left: 32px;
+ }
+}
+
+.tableRowDot {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+
+ &:before {
+ content: '';
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background-color: var(--default-gray-icons);
+ }
+}
+
+.queryCount {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ white-space: nowrap;
+}
+
+.dropdownWrapper {
+ flex-shrink: 0;
+}
+
+.queryPercent {
+ font-size: 12px;
+ color: var(--default-description-text);
+}
+
+.queryBar {
+ display: none;
+ width: 100%;
+ min-width: 102px;
+ height: 3px;
+ background: var(--default-item-divider);
+ border-radius: 2px;
+
+ @media (min-width: 768px) {
+ display: block;
+ }
+}
+
+.queryBarFill {
+ height: 100%;
+ background: var(--default-tertiary-stat);
+}
+
+.queryTooltip {
+ padding: 8px 12px;
+ white-space: nowrap;
+}
+
+.queryTooltipOverlay {
+ pointer-events: none;
+}
+
+.protectionMenu {
+ padding: 8px 0;
+ min-width: 200px;
+}
+
+.protectionMenuItem {
+ padding: 8px 16px;
+ cursor: pointer;
+ color: var(--default-link);
+}
+
+.protectionMenuItemRed {
+ color: var(--default-error-link);
+}
+
+.clientRow {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 0 24px;
+ padding: 16px 24px 24px;
+ transition: background-color var(--t2);
+ border-bottom: 1px solid var(--default-item-divider);
+
+ @media (min-width: 768px) {
+ padding: 8px 16px;
+ border: none;
+ }
+
+ &:hover {
+ background-color: var(--hovered-page-background);
+ }
+
+ &:last-child {
+ border: none;
+ }
+}
+
+.clientInfo {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ flex: 1;
+ min-width: 0;
+}
+
+.clientIp {
+ font-size: 14px;
+ color: var(--default-main-text);
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.clientName {
+ color: var(--default-description-text);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.clientBlocked {
+ display: none;
+ color: var(--default-error-link);
+
+ @media (min-width: 768px) {
+ display: block;
+ }
+}
+
+.clientActions {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.actionButton {
+ padding: 4px;
+ background: none;
+ border: none;
+ cursor: pointer;
+ color: var(--default-secondary-text);
+ border-radius: 4px;
+ transition: background-color 0.2s;
+
+ &:hover {
+ background-color: var(--hovered-secondary-button);
+ }
+}
+
+.clientActionsDropdown {
+ display: none;
+
+ @media (min-width: 768px) {
+ display: block;
+ }
+}
+
+.tableRowQueriesInfo {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ margin: 16px 0 0;
+
+ @media (min-width: 768px) {
+ display: none;
+ }
+
+ & .queryBar {
+ display: block;
+ flex: 3;
+ }
+
+ & .queryPercent {
+ font-size: 14px;
+ }
+
+ & .queryCount {
+ flex: 1;
+ }
+}
+
+.tableRowActions {
+ margin: 16px 0 0;
+
+ @media (min-width: 768px) {
+ display: none;
+ }
+
+ & .protectionMenu {
+ padding: 0;
+
+ & .protectionMenuItem {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 40px;
+ padding: 0 24px;
+ border: 1px solid var(--default-link);
+ border-radius: 8px;
+
+ &.protectionMenuItemRed {
+ border-color: var(--default-error-link);
+ }
+ }
+ }
+}
+
+.queryTooltipWrap {
+ display: none;
+
+ @media (min-width: 768px) {
+ display: block;
+ }
+}
diff --git a/client_v2/src/components/Dashboard/blocks/TopClients/TopClients.tsx b/client_v2/src/components/Dashboard/blocks/TopClients/TopClients.tsx
new file mode 100644
index 00000000000..354933df67b
--- /dev/null
+++ b/client_v2/src/components/Dashboard/blocks/TopClients/TopClients.tsx
@@ -0,0 +1,369 @@
+import { Show, For, createSignal, createMemo, onCleanup } from 'solid-js';
+import { useIsDesktop } from 'panel/helpers/useMediaQuery';
+import { MOBILE_TABLE_MAX_ROWS } from 'panel/helpers/constants';
+
+import intl from 'panel/common/intl';
+import { Icon } from 'panel/common/ui/Icon';
+import { Dropdown } from 'panel/common/ui/Dropdown';
+import { ConfirmDialog } from 'panel/common/ui/ConfirmDialog';
+import { formatNumber, formatCompactNumber } from 'panel/helpers/helpers';
+import { addErrorToast } from 'panel/stores/toasts';
+import { accessState, toggleClientBlock } from 'panel/stores/access';
+import theme from 'panel/lib/theme';
+import cn from 'clsx';
+import { useSortedData } from '../../hooks/useSortedData';
+import { SortableTableHeader } from '../SortableTableHeader';
+import { EmptyState } from '../EmptyState';
+
+import s from './TopClients.module.pcss';
+
+type ClientInfo = {
+ name: string;
+ count: number;
+ info?: {
+ name?: string;
+ whois_info?: {
+ orgname?: string;
+ country?: string;
+ };
+ disallowed?: boolean;
+ };
+};
+
+type Props = {
+ topClients: ClientInfo[];
+ numDnsQueries: number;
+};
+
+export const TopClients = (props: Props) => {
+ let isMounted = true;
+ onCleanup(() => {
+ isMounted = false;
+ });
+
+ const disallowedClientsList = createMemo(() => {
+ const str = accessState.disallowed_clients || '';
+ return str ? str.split('\n').filter(Boolean) : [];
+ });
+
+ const [confirmDialog, setConfirmDialog] = createSignal<{
+ open: boolean;
+ client: string;
+ action: 'block' | 'unblock';
+ }>({ open: false, client: '', action: 'block' });
+ const [openMenuClient, setOpenMenuClient] = createSignal(null);
+
+ const isDesktop = useIsDesktop();
+ const {
+ sortedData: sortedClients,
+ sortField,
+ sortDirection,
+ handleSort,
+ } = useSortedData(() => props.topClients);
+ const visibleClients = createMemo(() =>
+ isDesktop() ? sortedClients() : sortedClients().slice(0, MOBILE_TABLE_MAX_ROWS),
+ );
+
+ const isClientBlocked = (clientName: string) => disallowedClientsList().includes(clientName);
+
+ const handleBlockClient = async (clientIp: string) => {
+ const disallowedList = accessState.disallowed_clients
+ ? accessState.disallowed_clients.split('\n').filter(Boolean)
+ : [];
+ const isDisallowed = disallowedList.includes(clientIp);
+ if (isDisallowed) {
+ addErrorToast({
+ error: new Error(intl.getMessage('client_already_blocked', { ip: clientIp })),
+ });
+ if (isMounted) {
+ setConfirmDialog({ open: false, client: '', action: 'block' });
+ }
+ return;
+ }
+ await toggleClientBlock(clientIp, false, '');
+ if (isMounted) {
+ setConfirmDialog({ open: false, client: '', action: 'block' });
+ }
+ };
+
+ const handleUnblockClient = async (clientIp: string) => {
+ const disallowedList = accessState.disallowed_clients
+ ? accessState.disallowed_clients.split('\n').filter(Boolean)
+ : [];
+ const isDisallowed = disallowedList.includes(clientIp);
+ await toggleClientBlock(clientIp, isDisallowed, isDisallowed ? clientIp : '');
+ if (isMounted) {
+ setConfirmDialog({ open: false, client: '', action: 'unblock' });
+ }
+ };
+
+ const openConfirmDialog = (client: string, action: 'block' | 'unblock') => {
+ setOpenMenuClient(null);
+ setConfirmDialog({ open: true, client, action });
+ };
+
+ const getClientMenu = (client: ClientInfo) => {
+ const isBlocked = isClientBlocked(client.name);
+
+ return (
+
+ }
+ >
+ openConfirmDialog(client.name, 'unblock')}
+ >
+ {intl.getMessage('unblock_client')}
+
+
+
+ );
+ };
+
+ const hasStats = createMemo(() => props.topClients.length > 0);
+
+ return (
+
+
+
+
+
+
+
+
+
}>
+
+ {(client) => {
+ const percent = createMemo(() =>
+ props.numDnsQueries > 0
+ ? (client.count / props.numDnsQueries) * 100
+ : 0,
+ );
+ const isBlocked = isClientBlocked(client.name);
+
+ return (
+
+
+
+ }
+ >
+
+
+
+ {client.name}
+
+
+
+
+
+
+ {formatNumber(client.count)}{' '}
+ {intl.getMessage('queries').toLowerCase()}
+
+ }
+ >
+
+ {formatCompactNumber(client.count)}
+
+
+ ({percent().toFixed(1)}%)
+
+
+
+
+
+
+
+ setOpenMenuClient(isOpen ? client.name : null)
+ }
+ >
+
+
+
+
+
+
+
+ {intl.getMessage('blocked')}
+
+
+
+
+
+
+
+ {client.info!.name}
+
+
+
+
+ {intl.getMessage('blocked')}
+
+
+
+
+ {formatCompactNumber(client.count)}
+
+
+ ({percent().toFixed(1)}%)
+
+
+
+
+
+
+
{getClientMenu(client)}
+
+
+ );
+ }}
+
+
+
+
+ {(() => {
+ const dialog = confirmDialog();
+ const isBlock = dialog.action === 'block';
+
+ return (
+
+ setConfirmDialog({ open: false, client: '', action: 'block' })
+ }
+ title={
+ isBlock
+ ? intl.getMessage('confirm_client_block_title', {
+ ip: dialog.client,
+ })
+ : intl.getMessage('confirm_client_unblock_title', {
+ ip: dialog.client,
+ })
+ }
+ text={
+ isBlock
+ ? intl.getMessage('confirm_client_block_desc', {
+ ip: dialog.client,
+ })
+ : intl.getMessage('confirm_client_unblock_desc', {
+ ip: dialog.client,
+ })
+ }
+ buttonText={
+ isBlock ? intl.getMessage('block') : intl.getMessage('unblock')
+ }
+ cancelText={intl.getMessage('cancel')}
+ buttonVariant={isBlock ? 'danger' : 'primary'}
+ onConfirm={() => {
+ if (isBlock) {
+ handleBlockClient(dialog.client);
+ } else {
+ handleUnblockClient(dialog.client);
+ }
+ }}
+ />
+ );
+ })()}
+
+
+
+ );
+};
diff --git a/client_v2/src/components/Dashboard/blocks/TopClients/index.ts b/client_v2/src/components/Dashboard/blocks/TopClients/index.ts
new file mode 100644
index 00000000000..9430adc1d96
--- /dev/null
+++ b/client_v2/src/components/Dashboard/blocks/TopClients/index.ts
@@ -0,0 +1 @@
+export { TopClients } from './TopClients';
diff --git a/client_v2/src/components/Dashboard/blocks/TopQueriedDomains/TopQueriedDomains.tsx b/client_v2/src/components/Dashboard/blocks/TopQueriedDomains/TopQueriedDomains.tsx
new file mode 100644
index 00000000000..eb663d6a334
--- /dev/null
+++ b/client_v2/src/components/Dashboard/blocks/TopQueriedDomains/TopQueriedDomains.tsx
@@ -0,0 +1,156 @@
+import { Show, For, createMemo } from 'solid-js';
+
+import { useIsDesktop } from 'panel/helpers/useMediaQuery';
+import { MOBILE_TABLE_MAX_ROWS } from 'panel/helpers/constants';
+import intl from 'panel/common/intl';
+import { Icon } from 'panel/common/ui/Icon';
+import { Dropdown } from 'panel/common/ui/Dropdown';
+import { formatNumber, formatCompactNumber } from 'panel/helpers/helpers';
+import { getTrackerData } from 'panel/helpers/trackers/trackers';
+import theme from 'panel/lib/theme';
+import cn from 'clsx';
+import { SortableTableHeader } from '../SortableTableHeader';
+import { TrackerTooltip } from '../TrackerTooltip';
+import { EmptyState } from '../EmptyState';
+import { useSortedData } from '../../hooks/useSortedData';
+
+import s from '../TableCard.module.pcss';
+
+type DomainInfo = {
+ name: string;
+ count: number;
+};
+
+type Props = {
+ topQueriedDomains: DomainInfo[];
+ numDnsQueries: number;
+};
+
+export const TopQueriedDomains = (props: Props) => {
+ const isDesktop = useIsDesktop();
+ const {
+ sortedData: sortedDomains,
+ sortField,
+ sortDirection,
+ handleSort,
+ } = useSortedData(() => props.topQueriedDomains);
+ const visibleDomains = createMemo(() =>
+ isDesktop() ? sortedDomains() : sortedDomains().slice(0, MOBILE_TABLE_MAX_ROWS),
+ );
+
+ const hasStats = createMemo(() => props.topQueriedDomains.length > 0);
+
+ return (
+
+
+
+
+
+
+
+
+
}>
+
+ {(domain) => {
+ const percent = createMemo(() =>
+ props.numDnsQueries > 0
+ ? (domain.count / props.numDnsQueries) * 100
+ : 0,
+ );
+ const trackerData = getTrackerData(domain.name);
+
+ return (
+
+
+ }
+ >
+ }
+ trigger="hover"
+ position="bottomLeft"
+ noIcon
+ >
+
+
+
+ {domain.name}
+
+
+
+
+
+ {formatNumber(domain.count)}{' '}
+ {intl.getMessage('queries').toLowerCase()}
+
+ }
+ >
+
+ {formatCompactNumber(domain.count)}
+
+
+ ({percent().toFixed(1)}%)
+
+
+
+
+
+
+
+
+
+
+ );
+ }}
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/Dashboard/blocks/TopQueriedDomains/index.ts b/client_v2/src/components/Dashboard/blocks/TopQueriedDomains/index.ts
new file mode 100644
index 00000000000..8fd46268c87
--- /dev/null
+++ b/client_v2/src/components/Dashboard/blocks/TopQueriedDomains/index.ts
@@ -0,0 +1 @@
+export { TopQueriedDomains } from './TopQueriedDomains';
diff --git a/client_v2/src/components/Dashboard/blocks/TopUpstreams/TopUpstreams.tsx b/client_v2/src/components/Dashboard/blocks/TopUpstreams/TopUpstreams.tsx
new file mode 100644
index 00000000000..8c5d45a203c
--- /dev/null
+++ b/client_v2/src/components/Dashboard/blocks/TopUpstreams/TopUpstreams.tsx
@@ -0,0 +1,139 @@
+import { Show, For, createMemo } from 'solid-js';
+
+import { useIsDesktop } from 'panel/helpers/useMediaQuery';
+import { MOBILE_TABLE_MAX_ROWS } from 'panel/helpers/constants';
+import intl from 'panel/common/intl';
+import { formatNumber, formatCompactNumber } from 'panel/helpers/helpers';
+import theme from 'panel/lib/theme';
+import { Dropdown } from 'panel/common/ui/Dropdown';
+import cn from 'clsx';
+import { SortableTableHeader } from '../SortableTableHeader';
+import { EmptyState } from '../EmptyState';
+import { useSortedData } from '../../hooks/useSortedData';
+
+import s from '../TableCard.module.pcss';
+
+type UpstreamInfo = {
+ name: string;
+ count: number;
+};
+
+type Props = {
+ topUpstreamsResponses: UpstreamInfo[];
+ numDnsQueries: number;
+};
+
+export const TopUpstreams = (props: Props) => {
+ const isDesktop = useIsDesktop();
+ const {
+ sortedData: sortedUpstreams,
+ sortField,
+ sortDirection,
+ handleSort,
+ } = useSortedData(() => props.topUpstreamsResponses);
+ const visibleUpstreams = createMemo(() =>
+ isDesktop() ? sortedUpstreams() : sortedUpstreams().slice(0, MOBILE_TABLE_MAX_ROWS),
+ );
+
+ const hasStats = createMemo(() => props.topUpstreamsResponses.length > 0);
+
+ return (
+
+
+
+
+
+
+
+
+
}>
+
+ {(upstream) => {
+ const percent = createMemo(() =>
+ props.numDnsQueries > 0
+ ? (upstream.count / props.numDnsQueries) * 100
+ : 0,
+ );
+
+ return (
+
+
+ {upstream.name}
+
+
+
+
+
+ {formatNumber(upstream.count)}{' '}
+ {intl.getMessage('queries').toLowerCase()}
+
+ }
+ >
+
+ {formatCompactNumber(upstream.count)}
+
+
+ ({percent().toFixed(2)}%)
+
+
+
+
+
+
+
+
+
+
+ );
+ }}
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/Dashboard/blocks/TopUpstreams/index.ts b/client_v2/src/components/Dashboard/blocks/TopUpstreams/index.ts
new file mode 100644
index 00000000000..5fc2578dc48
--- /dev/null
+++ b/client_v2/src/components/Dashboard/blocks/TopUpstreams/index.ts
@@ -0,0 +1 @@
+export { TopUpstreams } from './TopUpstreams';
diff --git a/client_v2/src/components/Dashboard/blocks/TrackerTooltip/TrackerTooltip.module.pcss b/client_v2/src/components/Dashboard/blocks/TrackerTooltip/TrackerTooltip.module.pcss
new file mode 100644
index 00000000000..82885f15b3c
--- /dev/null
+++ b/client_v2/src/components/Dashboard/blocks/TrackerTooltip/TrackerTooltip.module.pcss
@@ -0,0 +1,38 @@
+.tooltip {
+ border-radius: 8px;
+ padding: 16px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ cursor: default;
+}
+
+.tooltipTitle {
+ margin-bottom: 8px;
+}
+
+.tooltipRow {
+ display: flex;
+ align-items: center;
+ margin-bottom: 8px;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+}
+
+.tooltipLabel {
+ margin-right: 2px;
+}
+
+.tooltipValue {
+ font-weight: 500;
+ color: var(--default-main-text);
+}
+
+.tooltipLink {
+ color: var(--default-link);
+ text-decoration: underline;
+
+ &:hover {
+ color: var(--hovered-link);
+ }
+}
diff --git a/client_v2/src/components/Dashboard/blocks/TrackerTooltip/TrackerTooltip.tsx b/client_v2/src/components/Dashboard/blocks/TrackerTooltip/TrackerTooltip.tsx
new file mode 100644
index 00000000000..80bde826be4
--- /dev/null
+++ b/client_v2/src/components/Dashboard/blocks/TrackerTooltip/TrackerTooltip.tsx
@@ -0,0 +1,74 @@
+import { Show } from 'solid-js';
+
+import intl from 'panel/common/intl';
+import { captitalizeWords } from 'panel/helpers/helpers';
+import { getSourceData } from 'panel/helpers/trackers/trackers';
+import theme from 'panel/lib/theme';
+import cn from 'clsx';
+
+import s from './TrackerTooltip.module.pcss';
+
+export type TrackerData = {
+ id: string;
+ name: string;
+ url: string;
+ category: string;
+ source: number;
+ sourceData: { name: string; url: string } | null;
+};
+
+type Props = {
+ trackerData: TrackerData;
+};
+
+export const TrackerTooltip = (props: Props) => {
+ const sourceData = () => getSourceData(props.trackerData);
+
+ return (
+
+ );
+};
diff --git a/client_v2/src/components/Dashboard/blocks/TrackerTooltip/index.ts b/client_v2/src/components/Dashboard/blocks/TrackerTooltip/index.ts
new file mode 100644
index 00000000000..9d7483644dd
--- /dev/null
+++ b/client_v2/src/components/Dashboard/blocks/TrackerTooltip/index.ts
@@ -0,0 +1 @@
+export { TrackerTooltip } from './TrackerTooltip';
diff --git a/client_v2/src/components/Dashboard/blocks/UpstreamAvgTime/UpstreamAvgTime.tsx b/client_v2/src/components/Dashboard/blocks/UpstreamAvgTime/UpstreamAvgTime.tsx
new file mode 100644
index 00000000000..70c0bbbe2a4
--- /dev/null
+++ b/client_v2/src/components/Dashboard/blocks/UpstreamAvgTime/UpstreamAvgTime.tsx
@@ -0,0 +1,92 @@
+import { Show, For, createMemo } from 'solid-js';
+
+import { useIsDesktop } from 'panel/helpers/useMediaQuery';
+import { MOBILE_TABLE_MAX_ROWS } from 'panel/helpers/constants';
+import intl from 'panel/common/intl';
+import theme from 'panel/lib/theme';
+import cn from 'clsx';
+import { SortableTableHeader } from '../SortableTableHeader';
+import { EmptyState } from '../EmptyState';
+import { useSortedData } from '../../hooks/useSortedData';
+
+import s from '../TableCard.module.pcss';
+
+type UpstreamInfo = {
+ name: string;
+ count: number;
+};
+
+type Props = {
+ topUpstreamsAvgTime: UpstreamInfo[];
+ avgProcessingTime: number;
+};
+
+export const UpstreamAvgTime = (props: Props) => {
+ const isDesktop = useIsDesktop();
+ const {
+ sortedData: sortedUpstreams,
+ sortField,
+ sortDirection,
+ handleSort,
+ } = useSortedData(() => props.topUpstreamsAvgTime);
+ const visibleUpstreams = createMemo(() =>
+ isDesktop() ? sortedUpstreams() : sortedUpstreams().slice(0, MOBILE_TABLE_MAX_ROWS),
+ );
+
+ const hasStats = createMemo(() => props.topUpstreamsAvgTime.length > 0);
+
+ return (
+
+
+
+
+
+
+
+
+
}>
+
+ {(upstream) => (
+
+
+ {upstream.name}
+
+
+
+ {(upstream.count ?? 0).toFixed(0)}{' '}
+ {intl.getMessage('milliseconds_abbreviation')}
+
+
+
+ )}
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/Dashboard/blocks/UpstreamAvgTime/index.ts b/client_v2/src/components/Dashboard/blocks/UpstreamAvgTime/index.ts
new file mode 100644
index 00000000000..48173ce22cb
--- /dev/null
+++ b/client_v2/src/components/Dashboard/blocks/UpstreamAvgTime/index.ts
@@ -0,0 +1 @@
+export { UpstreamAvgTime } from './UpstreamAvgTime';
diff --git a/client_v2/src/components/Dashboard/hooks/useSortedData.ts b/client_v2/src/components/Dashboard/hooks/useSortedData.ts
new file mode 100644
index 00000000000..78706c1a2ee
--- /dev/null
+++ b/client_v2/src/components/Dashboard/hooks/useSortedData.ts
@@ -0,0 +1,60 @@
+import { createSignal, createMemo, untrack } from 'solid-js';
+
+export type SortField = 'name' | 'count';
+export type SortDirection = 'asc' | 'desc';
+
+type SortableItem = {
+ name: string;
+ count: number;
+};
+
+type UseSortedDataResult = {
+ sortedData: () => T[];
+ sortField: () => SortField;
+ sortDirection: () => SortDirection;
+ handleSort: (field: SortField) => void;
+};
+
+export const DEFAULT_VISIBLE_ITEMS = 10;
+
+export const useSortedData = (
+ data: () => T[],
+ defaultSortField: SortField = 'count',
+ defaultSortDirection: SortDirection = 'desc',
+ limit: number = DEFAULT_VISIBLE_ITEMS,
+): UseSortedDataResult => {
+ const [sortField, setSortField] = createSignal(defaultSortField);
+ const [sortDirection, setSortDirection] = createSignal(defaultSortDirection);
+
+ const sortedData = createMemo(() => {
+ const direction = sortDirection();
+ const field = sortField();
+ return data()
+ .toSorted((a, b) => {
+ const modifier = direction === 'asc' ? 1 : -1;
+ if (field === 'name') {
+ return a.name.localeCompare(b.name) * modifier;
+ }
+ return (a.count - b.count) * modifier;
+ })
+ .slice(0, limit);
+ });
+
+ const handleSort = (field: SortField) => {
+ untrack(() => {
+ if (sortField() === field) {
+ setSortDirection(sortDirection() === 'asc' ? 'desc' : 'asc');
+ } else {
+ setSortField(field);
+ setSortDirection(field === 'name' ? 'asc' : 'desc');
+ }
+ });
+ };
+
+ return {
+ sortedData,
+ sortField,
+ sortDirection,
+ handleSort,
+ };
+};
diff --git a/client_v2/src/components/Dashboard/index.ts b/client_v2/src/components/Dashboard/index.ts
new file mode 100644
index 00000000000..41a54b59751
--- /dev/null
+++ b/client_v2/src/components/Dashboard/index.ts
@@ -0,0 +1 @@
+export { Dashboard } from './Dashboard';
diff --git a/client_v2/src/components/Dhcp/Dhcp.tsx b/client_v2/src/components/Dhcp/Dhcp.tsx
new file mode 100644
index 00000000000..4e61793f136
--- /dev/null
+++ b/client_v2/src/components/Dhcp/Dhcp.tsx
@@ -0,0 +1,231 @@
+import { createSignal, createEffect, onMount, Show } from 'solid-js';
+import cn from 'clsx';
+import { useNavigate } from '@solidjs/router';
+
+import { Dropdown } from 'panel/common/ui/Dropdown';
+import { Icon } from 'panel/common/ui/Icon';
+import { PageLoader } from 'panel/common/ui/Loader';
+import { SettingRow } from 'panel/common/ui/SettingRow';
+import { ConfirmDialog } from 'panel/common/ui/ConfirmDialog';
+import intl from 'panel/common/intl';
+import theme from 'panel/lib/theme';
+import { useDialog } from 'panel/hooks/useDialog';
+import { SETTINGS_URLS } from 'panel/helpers/constants';
+import {
+ dhcpState,
+ getDhcpStatus,
+ getDhcpInterfaces,
+ setDhcpConfig,
+ resetDhcp,
+} from 'panel/stores/dhcp';
+import { addWarningToast } from 'panel/stores/toasts';
+
+import { DhcpToggle } from './blocks/DhcpToggle';
+import { InterfaceSelector } from './blocks/InterfaceSelector';
+import { DhcpV4Modal } from './blocks/DhcpV4Modal';
+import { DhcpV6Modal } from './blocks/DhcpV6Modal';
+import type { V4Config } from './blocks/DhcpV4Modal';
+import type { V6Config } from './blocks/DhcpV6Modal';
+import s from './styles.module.pcss';
+
+export const Dhcp = () => {
+ const navigate = useNavigate();
+ const v4Dialog = useDialog();
+ const v6Dialog = useDialog();
+ const resetDialog = useDialog();
+
+ const [selectedInterface, setSelectedInterface] = createSignal(dhcpState.interface_name || '');
+ const [showAllIps, setShowAllIps] = createSignal(false);
+ const [menuOpen, setMenuOpen] = createSignal(false);
+
+ createEffect(() => {
+ if (dhcpState.interface_name) {
+ setSelectedInterface(dhcpState.interface_name);
+ }
+ });
+
+ onMount(async () => {
+ await getDhcpStatus();
+
+ if (dhcpState.dhcp_available) {
+ await getDhcpInterfaces();
+ if (!selectedInterface() && dhcpState.interfaces) {
+ const firstIface = Object.keys(dhcpState.interfaces)[0];
+ if (firstIface) setSelectedInterface(firstIface);
+ }
+ }
+ });
+
+ const handleSaveV4Config = (values: V4Config) => {
+ setDhcpConfig({ interface_name: selectedInterface(), v4: values });
+ v4Dialog.closeDialog();
+ };
+
+ const handleSaveV6Config = (values: V6Config) => {
+ setDhcpConfig({ interface_name: selectedInterface(), v6: values });
+ v6Dialog.closeDialog();
+ };
+
+ const handleInterfaceChange = (name: string) => {
+ setSelectedInterface(name);
+ setShowAllIps(false);
+ };
+
+ createEffect(() => {
+ const check = dhcpState.check;
+ if (
+ !dhcpState.enabled &&
+ check &&
+ (check.v4?.other_server?.found === 'yes' || check.v6?.other_server?.found === 'yes')
+ ) {
+ addWarningToast({ error: intl.getMessage('dhcp_warning') });
+ }
+ });
+
+ const hasIpv4 = () =>
+ !!(dhcpState.interfaces && dhcpState.interfaces[selectedInterface()]?.ipv4_addresses);
+
+ const hasIpv6 = () =>
+ !!(dhcpState.interfaces && dhcpState.interfaces[selectedInterface()]?.ipv6_addresses);
+
+ const handleResetClick = () => {
+ setMenuOpen(false);
+ resetDialog.openDialog();
+ };
+
+ const resetMenu = (
+
+ {intl.getMessage('reset_dhcp_settings')}
+
+ );
+
+ const isLoaded = () => !dhcpState.processing && !dhcpState.processingInterfaces;
+
+ return (
+
+
+
}>
+
+
+
+ {intl.getMessage('unavailable_dhcp')}
+
+
+ {intl.getMessage('unavailable_dhcp_desc')}
+
+
+
+
+
+
+
+
+
+
+ setShowAllIps(true)}
+ />
+
+
+
+
+ hasIpv6() && v6Dialog.openDialog()}
+ />
+ navigate(SETTINGS_URLS.dhcpLeases)}
+ />
+
+
+
+
+
+
+
+ {
+ resetDhcp();
+ getDhcpStatus();
+ resetDialog.closeDialog();
+ }}
+ onClose={resetDialog.closeDialog}
+ />
+
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/Dhcp/LeasesPage/DynamicLeasesTab.tsx b/client_v2/src/components/Dhcp/LeasesPage/DynamicLeasesTab.tsx
new file mode 100644
index 00000000000..3fb47ad3505
--- /dev/null
+++ b/client_v2/src/components/Dhcp/LeasesPage/DynamicLeasesTab.tsx
@@ -0,0 +1,70 @@
+import { createSignal, Show } from 'solid-js';
+
+import intl from 'panel/common/intl';
+import { ConfirmDialog } from 'panel/common/ui/ConfirmDialog';
+import { DynamicLeasesTable } from './DynamicLeasesTable';
+import { dhcpState, removeStaticLease, toggleLeaseModal, getDhcpStatus } from 'panel/stores/dhcp';
+
+type LeaseData = {
+ mac: string;
+ ip: string;
+ hostname: string;
+};
+
+export const DynamicLeasesTab = () => {
+ const [confirmDeleteLease, setConfirmDeleteLease] = createSignal(null);
+
+ const handleEditDynamicLease = (lease: LeaseData) => {
+ toggleLeaseModal('ADD_LEASE', lease);
+ };
+
+ const handleDeleteDynamicLease = (lease: LeaseData) => {
+ setConfirmDeleteLease(lease);
+ };
+
+ const handleConfirmDeleteLease = () => {
+ const lease = confirmDeleteLease();
+ if (lease) {
+ removeStaticLease(lease);
+ setConfirmDeleteLease(null);
+ }
+ };
+
+ const handleMakeStatic = (lease: LeaseData) => {
+ toggleLeaseModal('MAKE_STATIC', lease);
+ };
+
+ const handleRefreshLeases = () => {
+ getDhcpStatus();
+ };
+
+ return (
+ <>
+ 0}>
+
+
+
+
+ setConfirmDeleteLease(null)}
+ />
+
+ >
+ );
+};
diff --git a/client_v2/src/components/Dhcp/LeasesPage/DynamicLeasesTable.tsx b/client_v2/src/components/Dhcp/LeasesPage/DynamicLeasesTable.tsx
new file mode 100644
index 00000000000..04ddd125b56
--- /dev/null
+++ b/client_v2/src/components/Dhcp/LeasesPage/DynamicLeasesTable.tsx
@@ -0,0 +1,254 @@
+import { createSignal, createMemo } from 'solid-js';
+import cn from 'clsx';
+
+import intl from 'panel/common/intl';
+import theme from 'panel/lib/theme';
+import { Table, type TableColumn } from 'panel/common/ui/Table';
+import { Icon } from 'panel/common/ui/Icon';
+import { Dropdown } from 'panel/common/ui/Dropdown';
+
+import s from './LeasesTable.module.pcss';
+
+type DynamicLease = {
+ mac: string;
+ ip: string;
+ hostname: string;
+ expires?: string;
+};
+
+type Props = {
+ leases: DynamicLease[];
+ processingUpdating: boolean;
+ processingDeleting: boolean;
+ onEdit: (lease: DynamicLease) => void;
+ onDelete: (lease: DynamicLease) => void;
+ onMakeStatic: (lease: DynamicLease) => void;
+ onRefresh: () => void;
+};
+
+export const DynamicLeasesTable = (props: Props) => {
+ const [openMenuId, setOpenMenuId] = createSignal(null);
+
+ const handleEdit = (row: DynamicLease) => {
+ props.onEdit(row);
+ setOpenMenuId(null);
+ };
+
+ const handleMakeStatic = (row: DynamicLease) => {
+ props.onMakeStatic(row);
+ setOpenMenuId(null);
+ };
+
+ const handleRefresh = () => {
+ props.onRefresh();
+ setOpenMenuId(null);
+ };
+
+ const handleDelete = (row: DynamicLease) => {
+ props.onDelete(row);
+ setOpenMenuId(null);
+ };
+
+ const columns = createMemo[]>(() => [
+ {
+ key: 'mac',
+ header: {
+ text: intl.getMessage('dhcp_table_mac_address'),
+ className: s.headerCell,
+ },
+ accessor: 'mac',
+ sortable: true,
+ render: (value: string) => (
+
+
+ {intl.getMessage('dhcp_table_mac_address')}
+
+
+ {value}
+
+
+ ),
+ },
+ {
+ key: 'ip',
+ header: {
+ text: intl.getMessage('dhcp_table_ip_address'),
+ className: s.headerCell,
+ },
+ accessor: 'ip',
+ sortable: true,
+ render: (value: string) => (
+
+
+ {intl.getMessage('dhcp_table_ip_address')}
+
+
+ {value}
+
+
+ ),
+ },
+ {
+ key: 'hostname',
+ header: {
+ text: intl.getMessage('dhcp_table_hostname'),
+ className: s.headerCell,
+ },
+ accessor: 'hostname',
+ sortable: true,
+ render: (value: string) => (
+
+
+ {intl.getMessage('dhcp_table_hostname')}
+
+
+ {value}
+
+
+ ),
+ },
+ {
+ key: 'actions',
+ header: {
+ text: '',
+ className: s.headerCell,
+ },
+ accessor: 'mac',
+ sortable: false,
+ width: 48,
+ render: (_value: unknown, row: DynamicLease) => {
+ const rowId = `${row.mac}-${row.ip}`;
+ return (
+
+
+
+ props.onEdit(row)}
+ disabled={props.processingUpdating}
+ class={theme.table.action}
+ title={intl.getMessage('edit_table_action')}
+ aria-label={intl.getMessage('edit_table_action')}
+ data-testid="dynamic-lease-edit-button"
+ data-table-action
+ >
+
+
+ {intl.getMessage('edit_table_action')}
+
+
+
+ props.onRefresh()}
+ class={theme.table.action}
+ title={intl.getMessage('refresh_btn')}
+ aria-label={intl.getMessage('refresh_btn')}
+ data-testid="dynamic-lease-refresh-button"
+ data-table-action
+ >
+
+
+ {intl.getMessage('refresh_btn')}
+
+
+
+ props.onMakeStatic(row)}
+ class={theme.table.action}
+ title={intl.getMessage('make_static')}
+ aria-label={intl.getMessage('make_static')}
+ data-testid="dynamic-lease-make-static-button"
+ data-table-action
+ >
+
+
+ {intl.getMessage('make_static')}
+
+
+
+ props.onDelete(row)}
+ disabled={props.processingDeleting}
+ class={cn(theme.table.action, theme.table.action_danger)}
+ title={intl.getMessage('delete_table_action')}
+ aria-label={intl.getMessage('delete_table_action')}
+ data-testid="dynamic-lease-delete-button"
+ data-table-action
+ >
+
+
+ {intl.getMessage('delete_table_action')}
+
+
+
+
+
+
+ }
+ trigger="click"
+ position="bottomRight"
+ noIcon
+ open={openMenuId() === rowId}
+ onOpenChange={(isOpen: boolean) =>
+ setOpenMenuId(isOpen ? rowId : null)
+ }
+ >
+
+
+
+
+
+
+
+ );
+ },
+ },
+ ]);
+
+ return (
+ `${row.mac}-${row.ip}`}
+ />
+ );
+};
diff --git a/client_v2/src/components/Dhcp/LeasesPage/LeasesPage.module.pcss b/client_v2/src/components/Dhcp/LeasesPage/LeasesPage.module.pcss
new file mode 100644
index 00000000000..cafe2373eec
--- /dev/null
+++ b/client_v2/src/components/Dhcp/LeasesPage/LeasesPage.module.pcss
@@ -0,0 +1,49 @@
+.tabs {
+ padding: 24px 16px 0;
+}
+
+.breadcrumbs {
+ padding: 0 16px;
+}
+
+.tabContent {
+ padding: 40px 0 0 0;
+
+ &_static {
+ padding-top: 16px;
+ }
+}
+
+.title {
+ padding: 0;
+}
+
+.header {
+ display: flex;
+ align-items: center;
+ padding: 16px 0;
+}
+
+.headerActions {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.menuButton {
+ position: relative;
+ top: 4px;
+ border: none;
+ background: transparent;
+ cursor: pointer;
+ color: var(--default-gray-icons);
+ padding: 4px;
+ border-radius: 4px;
+ &:hover {
+ color: var(--default-black-icons);
+ }
+}
+
+.addButton {
+ padding-bottom: 24px;
+}
diff --git a/client_v2/src/components/Dhcp/LeasesPage/LeasesPage.tsx b/client_v2/src/components/Dhcp/LeasesPage/LeasesPage.tsx
new file mode 100644
index 00000000000..4edfc6179d3
--- /dev/null
+++ b/client_v2/src/components/Dhcp/LeasesPage/LeasesPage.tsx
@@ -0,0 +1,188 @@
+import { createMemo, createSignal, onMount, Show } from 'solid-js';
+import { useSearchParams } from '@solidjs/router';
+import cn from 'clsx';
+
+import intl from 'panel/common/intl';
+import theme from 'panel/lib/theme';
+import { Breadcrumbs } from 'panel/common/ui/Breadcrumbs';
+import { ConfirmDialog } from 'panel/common/ui/ConfirmDialog';
+import { Dropdown } from 'panel/common/ui/Dropdown';
+import { Icon } from 'panel/common/ui/Icon';
+import { Tabs } from 'panel/common/ui/Tabs';
+import { RoutePath } from 'panel/components/Routes/Paths';
+import {
+ dhcpState,
+ addStaticLease,
+ updateStaticLease,
+ resetDhcpLeases,
+ toggleLeaseModal,
+ getDhcpStatus,
+} from 'panel/stores/dhcp';
+
+import { StaticLeasesTab } from './StaticLeasesTab';
+import { DynamicLeasesTab } from './DynamicLeasesTab';
+import { StaticLeaseModal } from './StaticLeaseModal';
+
+import s from './LeasesPage.module.pcss';
+
+const LEASE_TABS = {
+ STATIC: 'static',
+ DYNAMIC: 'dynamic',
+} as const;
+
+type LeaseData = {
+ mac: string;
+ ip: string;
+ hostname: string;
+};
+
+export const LeasesPage = () => {
+ const [searchParams, setSearchParams] = useSearchParams<{
+ tab?: string;
+ }>();
+
+ const [menuOpen, setMenuOpen] = createSignal(false);
+ const [confirmResetLeases, setConfirmResetLeases] = createSignal(false);
+
+ const activeTab = createMemo(() =>
+ searchParams.tab === LEASE_TABS.DYNAMIC ? LEASE_TABS.DYNAMIC : LEASE_TABS.STATIC,
+ );
+
+ const handleTabChange = (tabId: string) => {
+ setSearchParams({ tab: tabId }, { replace: true });
+ };
+
+ onMount(() => {
+ getDhcpStatus();
+ });
+
+ const handleLeaseModalSubmit = (data: LeaseData) => {
+ if (dhcpState.modalType === 'EDIT_LEASE') {
+ updateStaticLease(data);
+ } else {
+ addStaticLease(data);
+ }
+ };
+
+ const handleLeaseModalClose = () => {
+ toggleLeaseModal();
+ };
+
+ const handleResetClick = () => {
+ setMenuOpen(false);
+ setConfirmResetLeases(true);
+ };
+
+ const handleResetLeases = () => {
+ resetDhcpLeases();
+ setConfirmResetLeases(false);
+ };
+
+ const resetMenu = (
+
+ {intl.getMessage('dhcp_reset_leases')}
+
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
,
+ },
+ {
+ id: LEASE_TABS.DYNAMIC,
+ label: intl.getMessage('dhcp_leases'),
+ content:
,
+ },
+ ]}
+ />
+
+
+
+
+
+
+ setConfirmResetLeases(false)}
+ />
+
+
+
+ );
+};
diff --git a/client_v2/src/components/Dhcp/LeasesPage/LeasesTable.module.pcss b/client_v2/src/components/Dhcp/LeasesPage/LeasesTable.module.pcss
new file mode 100644
index 00000000000..5fc0c46f5db
--- /dev/null
+++ b/client_v2/src/components/Dhcp/LeasesPage/LeasesTable.module.pcss
@@ -0,0 +1,70 @@
+.headerCell {
+ background-color: var(--page-background-additional);
+
+ @media (min-width: 768px) {
+ width: 100%;
+ min-height: 42px;
+ padding: 0 12px;
+ }
+
+ &:first-child {
+ border-radius: 8px 0 0 8px;
+ }
+
+ &:last-child {
+ border-radius: 0 8px 8px 0;
+ }
+}
+
+.actionButton {
+ width: 24px;
+ height: 24px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border: 0;
+ outline: 0;
+ cursor: pointer;
+ background: none;
+ color: var(--default-gray-icons);
+}
+
+.emptyTableContent {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+}
+
+.emptyTableIcon {
+ width: 64px;
+ height: 64px;
+}
+
+.emptyTableDesc {
+ color: var(--default-description-text);
+}
+
+.staticTable {
+ @media (min-width: 768px) {
+ grid-template-columns: 1fr 1fr 1fr fit-content(100%);
+ }
+}
+
+.dynamicTable {
+ @media (min-width: 768px) {
+ grid-template-columns: 1fr 1fr 1fr fit-content(100%);
+ }
+}
+
+.mobileActions {
+ @media (min-width: 768px) {
+ display: none !important;
+ }
+}
+
+.desktopActions {
+ @media (max-width: 767px) {
+ display: none !important;
+ }
+}
diff --git a/client_v2/src/components/Dhcp/LeasesPage/StaticLeaseModal.tsx b/client_v2/src/components/Dhcp/LeasesPage/StaticLeaseModal.tsx
new file mode 100644
index 00000000000..410f8dc6b20
--- /dev/null
+++ b/client_v2/src/components/Dhcp/LeasesPage/StaticLeaseModal.tsx
@@ -0,0 +1,218 @@
+import { createSignal, createEffect, createMemo, Show } from 'solid-js';
+
+import intl from 'panel/common/intl';
+import theme from 'panel/lib/theme';
+import { Input } from 'panel/common/controls/Input';
+import { Button } from 'panel/common/ui/Button';
+import { Dialog } from 'panel/common/ui/Dialog';
+import {
+ validateRequiredValue,
+ validateIp,
+ validateMac as validateMacFormat,
+ validateHostname,
+ validateIpNotDuplicate,
+ validateMacNotDuplicate,
+ validateIpv4InCidr,
+ validateIpGateway,
+} from 'panel/helpers/validators';
+import { parseSubnetMask } from 'panel/helpers/helpers';
+import { normalizeMac } from 'panel/helpers/form';
+
+type LeaseData = {
+ mac: string;
+ ip: string;
+ hostname: string;
+};
+
+type Props = {
+ isOpen: boolean;
+ isEdit: boolean;
+ isMakeStatic?: boolean;
+ initialData?: LeaseData;
+ processingAdding: boolean;
+ processingUpdating: boolean;
+ staticLeases: LeaseData[];
+ dhcpConfig?: { gatewayIp: string; subnetMask: string };
+ onSubmit: (data: LeaseData) => void;
+ onClose: () => void;
+};
+
+export const StaticLeaseModal = (props: Props) => {
+ const [mac, setMac] = createSignal('');
+ const [ip, setIp] = createSignal('');
+ const [hostname, setHostname] = createSignal('');
+
+ const [macError, setMacError] = createSignal('');
+ const [ipError, setIpError] = createSignal('');
+ const [hostnameError, setHostnameError] = createSignal('');
+
+ // Reset form when initialData changes
+ createEffect(() => {
+ setMac(props.initialData?.mac || '');
+ setIp(props.initialData?.ip || '');
+ setHostname(props.initialData?.hostname || '');
+ });
+
+ const cidr = createMemo(() => {
+ if (!props.dhcpConfig?.gatewayIp || !props.dhcpConfig?.subnetMask) {
+ return undefined;
+ }
+ const prefix = parseSubnetMask(props.dhcpConfig.subnetMask);
+ if (prefix === null) {
+ return undefined;
+ }
+ return `${props.dhcpConfig.gatewayIp}/${prefix}`;
+ });
+
+ const isProcessing = createMemo(() => props.processingAdding || props.processingUpdating);
+
+ const getTitle = () => {
+ if (props.isMakeStatic) {
+ return intl.getMessage('make_static');
+ }
+ if (props.isEdit) {
+ return intl.getMessage('dhcp_edit_static_lease');
+ }
+ return intl.getMessage('dhcp_new_static_lease');
+ };
+
+ const submitLabel = () =>
+ props.isMakeStatic ? intl.getMessage('make_static') : intl.getMessage('save');
+
+ const validateMac = () => {
+ const err =
+ validateRequiredValue(mac()) ||
+ validateMacFormat(mac()) ||
+ validateMacNotDuplicate(
+ props.staticLeases,
+ props.isEdit ? props.initialData?.mac : undefined,
+ )(mac());
+ setMacError(err || '');
+ };
+
+ const validateHostnameField = () => {
+ const err = validateHostname(hostname());
+ setHostnameError(err || '');
+ };
+
+ const validateIpField = () => {
+ const val = ip();
+ const cidrVal = cidr();
+ const gatewayIp = props.dhcpConfig?.gatewayIp;
+
+ const err =
+ validateRequiredValue(val) ||
+ validateIp(val) ||
+ validateIpNotDuplicate(
+ props.staticLeases,
+ props.isEdit ? props.initialData?.ip : undefined,
+ )(val) ||
+ (cidrVal && validateIpv4InCidr(val, { cidr: cidrVal })) ||
+ (gatewayIp && validateIpGateway(val, { gatewayIp }));
+ setIpError(err || '');
+ };
+
+ const isValid = createMemo(
+ () => !macError() && !ipError() && !hostnameError() && mac() && ip(),
+ );
+
+ const handleSubmit = (e: Event) => {
+ e.preventDefault();
+ validateMac();
+ validateHostnameField();
+ validateIpField();
+
+ if (!isValid()) {
+ return;
+ }
+
+ props.onSubmit({
+ mac: normalizeMac(mac().trim()),
+ ip: ip().trim(),
+ hostname: hostname().trim(),
+ });
+ };
+
+ return (
+
+
+
+ );
+};
diff --git a/client_v2/src/components/Dhcp/LeasesPage/StaticLeasesTab.tsx b/client_v2/src/components/Dhcp/LeasesPage/StaticLeasesTab.tsx
new file mode 100644
index 00000000000..d8109178192
--- /dev/null
+++ b/client_v2/src/components/Dhcp/LeasesPage/StaticLeasesTab.tsx
@@ -0,0 +1,81 @@
+import { createSignal, Show } from 'solid-js';
+
+import intl from 'panel/common/intl';
+import { ConfirmDialog } from 'panel/common/ui/ConfirmDialog';
+import { PlusButton } from 'panel/common/ui/PlusButton';
+import { StaticLeasesTable } from './StaticLeasesTable';
+import { dhcpState, removeStaticLease, toggleLeaseModal, getDhcpStatus } from 'panel/stores/dhcp';
+
+import s from './LeasesPage.module.pcss';
+
+type LeaseData = {
+ mac: string;
+ ip: string;
+ hostname: string;
+};
+
+export const StaticLeasesTab = () => {
+ const [confirmDeleteLease, setConfirmDeleteLease] = createSignal(null);
+
+ const handleAddStaticLease = () => {
+ toggleLeaseModal('ADD_LEASE');
+ };
+
+ const handleEditStaticLease = (lease: LeaseData) => {
+ toggleLeaseModal('EDIT_LEASE', lease);
+ };
+
+ const handleDeleteStaticLease = (lease: LeaseData) => {
+ setConfirmDeleteLease(lease);
+ };
+
+ const handleConfirmDeleteLease = () => {
+ const lease = confirmDeleteLease();
+ if (lease) {
+ removeStaticLease(lease);
+ setConfirmDeleteLease(null);
+ }
+ };
+
+ const handleRefreshLeases = () => {
+ getDhcpStatus();
+ };
+
+ return (
+ <>
+
+
+ 0}>
+
+
+
+
+ setConfirmDeleteLease(null)}
+ />
+
+ >
+ );
+};
diff --git a/client_v2/src/components/Dhcp/LeasesPage/StaticLeasesTable.tsx b/client_v2/src/components/Dhcp/LeasesPage/StaticLeasesTable.tsx
new file mode 100644
index 00000000000..3d583941185
--- /dev/null
+++ b/client_v2/src/components/Dhcp/LeasesPage/StaticLeasesTable.tsx
@@ -0,0 +1,226 @@
+import { createSignal, createMemo } from 'solid-js';
+import cn from 'clsx';
+
+import intl from 'panel/common/intl';
+import theme from 'panel/lib/theme';
+import { Table, type TableColumn } from 'panel/common/ui/Table';
+import { Icon } from 'panel/common/ui/Icon';
+import { Dropdown } from 'panel/common/ui/Dropdown';
+
+import s from './LeasesTable.module.pcss';
+
+type StaticLease = {
+ mac: string;
+ ip: string;
+ hostname: string;
+};
+
+type Props = {
+ staticLeases: StaticLease[];
+ processingDeleting: boolean;
+ processingUpdating: boolean;
+ onEdit: (lease: StaticLease) => void;
+ onDelete: (lease: StaticLease) => void;
+ onRefresh: () => void;
+};
+
+export const StaticLeasesTable = (props: Props) => {
+ const [openMenuId, setOpenMenuId] = createSignal(null);
+
+ const handleEdit = (row: StaticLease) => {
+ props.onEdit(row);
+ setOpenMenuId(null);
+ };
+
+ const handleRefresh = () => {
+ props.onRefresh();
+ setOpenMenuId(null);
+ };
+
+ const handleDelete = (row: StaticLease) => {
+ props.onDelete(row);
+ setOpenMenuId(null);
+ };
+
+ const columns = createMemo[]>(() => [
+ {
+ key: 'mac',
+ header: {
+ text: intl.getMessage('dhcp_table_mac_address'),
+ className: s.headerCell,
+ },
+ accessor: 'mac',
+ sortable: true,
+ render: (value: string) => (
+
+
+ {intl.getMessage('dhcp_table_mac_address')}
+
+
+ {value}
+
+
+ ),
+ },
+ {
+ key: 'ip',
+ header: {
+ text: intl.getMessage('dhcp_table_ip_address'),
+ className: s.headerCell,
+ },
+ accessor: 'ip',
+ sortable: true,
+ render: (value: string) => (
+
+
+ {intl.getMessage('dhcp_table_ip_address')}
+
+
+ {value}
+
+
+ ),
+ },
+ {
+ key: 'hostname',
+ header: {
+ text: intl.getMessage('dhcp_table_hostname'),
+ className: s.headerCell,
+ },
+ accessor: 'hostname',
+ sortable: true,
+ render: (value: string) => (
+
+
+ {intl.getMessage('dhcp_table_hostname')}
+
+
+ {value}
+
+
+ ),
+ },
+ {
+ key: 'actions',
+ header: {
+ text: '',
+ className: s.headerCell,
+ },
+ accessor: 'mac',
+ sortable: false,
+ width: 48,
+ render: (_value: unknown, row: StaticLease) => {
+ const rowId = `${row.mac}-${row.ip}`;
+ return (
+
+
+
+ props.onEdit(row)}
+ disabled={props.processingUpdating}
+ class={theme.table.action}
+ title={intl.getMessage('edit_table_action')}
+ aria-label={intl.getMessage('edit_table_action')}
+ data-testid="static-lease-edit-button"
+ data-table-action
+ >
+
+
+ {intl.getMessage('edit_table_action')}
+
+
+
+ props.onRefresh()}
+ class={theme.table.action}
+ title={intl.getMessage('refresh_btn')}
+ aria-label={intl.getMessage('refresh_btn')}
+ data-testid="static-lease-refresh-button"
+ data-table-action
+ >
+
+
+ {intl.getMessage('refresh_btn')}
+
+
+
+ props.onDelete(row)}
+ disabled={props.processingDeleting}
+ class={cn(theme.table.action, theme.table.action_danger)}
+ title={intl.getMessage('delete_table_action')}
+ aria-label={intl.getMessage('delete_table_action')}
+ data-testid="static-lease-delete-button"
+ data-table-action
+ >
+
+
+ {intl.getMessage('delete_table_action')}
+
+
+
+
+
+
+ }
+ trigger="click"
+ position="bottomRight"
+ noIcon
+ open={openMenuId() === rowId}
+ onOpenChange={(isOpen: boolean) =>
+ setOpenMenuId(isOpen ? rowId : null)
+ }
+ >
+
+
+
+
+
+
+
+ );
+ },
+ },
+ ]);
+
+ return (
+ `${row.mac}-${row.ip}`}
+ />
+ );
+};
diff --git a/client_v2/src/components/Dhcp/LeasesPage/index.ts b/client_v2/src/components/Dhcp/LeasesPage/index.ts
new file mode 100644
index 00000000000..7cd1bd47786
--- /dev/null
+++ b/client_v2/src/components/Dhcp/LeasesPage/index.ts
@@ -0,0 +1 @@
+export { LeasesPage } from './LeasesPage';
diff --git a/client_v2/src/components/Dhcp/blocks/DhcpToggle.tsx b/client_v2/src/components/Dhcp/blocks/DhcpToggle.tsx
new file mode 100644
index 00000000000..fd969606de2
--- /dev/null
+++ b/client_v2/src/components/Dhcp/blocks/DhcpToggle.tsx
@@ -0,0 +1,57 @@
+import type { Accessor } from 'solid-js';
+import { SettingRow } from 'panel/common/ui/SettingRow';
+import intl from 'panel/common/intl';
+import { dhcpState, toggleDhcp } from 'panel/stores/dhcp';
+
+type Props = {
+ selectedInterface: Accessor;
+ onToggleOn?: () => void;
+};
+
+type DhcpToggleConfig = {
+ enabled: boolean;
+ interface_name: string;
+ v4?: typeof dhcpState.v4;
+ v6?: typeof dhcpState.v6;
+};
+
+export const DhcpToggle = (props: Props) => {
+ const disabled = () =>
+ dhcpState.processingDhcp ||
+ dhcpState.processingConfig ||
+ (!dhcpState.enabled && !props.selectedInterface());
+
+ const onChange = (checked: boolean) => {
+ if (checked) {
+ const v4 = dhcpState.v4;
+ const v6 = dhcpState.v6;
+ const hasV4Config = !!(v4 && Object.values(v4).some(Boolean));
+ const hasV6Config = !!(v6 && Object.values(v6).some(Boolean));
+
+ const values: DhcpToggleConfig = {
+ enabled: false,
+ interface_name: props.selectedInterface() || dhcpState.interface_name,
+ };
+ if (hasV4Config) values.v4 = v4;
+ if (hasV6Config) values.v6 = v6;
+
+ toggleDhcp(values);
+ if (!hasV4Config) {
+ props.onToggleOn?.();
+ }
+ } else {
+ toggleDhcp({ enabled: true });
+ }
+ };
+
+ return (
+
+ );
+};
diff --git a/client_v2/src/components/Dhcp/blocks/DhcpV4Modal/DhcpV4Modal.module.pcss b/client_v2/src/components/Dhcp/blocks/DhcpV4Modal/DhcpV4Modal.module.pcss
new file mode 100644
index 00000000000..2dbbe9a93de
--- /dev/null
+++ b/client_v2/src/components/Dhcp/blocks/DhcpV4Modal/DhcpV4Modal.module.pcss
@@ -0,0 +1,20 @@
+.formField {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ padding: 8px 0;
+}
+
+.formFieldLabel {
+ color: var(--default-labels);
+}
+
+.rangeRow {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 16px;
+
+ @media (min-width: 768px) {
+ grid-template-columns: 1fr 1fr;
+ }
+}
diff --git a/client_v2/src/components/Dhcp/blocks/DhcpV4Modal/DhcpV4Modal.tsx b/client_v2/src/components/Dhcp/blocks/DhcpV4Modal/DhcpV4Modal.tsx
new file mode 100644
index 00000000000..dd3ff6dd753
--- /dev/null
+++ b/client_v2/src/components/Dhcp/blocks/DhcpV4Modal/DhcpV4Modal.tsx
@@ -0,0 +1,240 @@
+import { createSignal, createEffect, createMemo } from 'solid-js';
+import type { Accessor } from 'solid-js';
+import cn from 'clsx';
+
+import intl from 'panel/common/intl';
+import theme from 'panel/lib/theme';
+import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
+import { Input } from 'panel/common/controls/Input';
+import { dhcpState } from 'panel/stores/dhcp';
+import {
+ validateIpv4,
+ validateIpv4RangeEnd,
+ validateNotInRange,
+ validateGatewaySubnetMask,
+ validateIpForGatewaySubnetMask,
+ validateLeaseTime,
+} from 'panel/helpers/validators';
+import s from './DhcpV4Modal.module.pcss';
+
+export type V4Config = {
+ gateway_ip: string;
+ subnet_mask: string;
+ range_start: string;
+ range_end: string;
+ lease_duration: number;
+};
+
+type Props = {
+ open: boolean;
+ selectedInterface: Accessor;
+ onClose: () => void;
+ onSave: (values: V4Config) => void;
+};
+
+export const DhcpV4Modal = (props: Props) => {
+ const [gatewayIp, setGatewayIp] = createSignal('');
+ const [subnetMask, setSubnetMask] = createSignal('');
+ const [rangeStart, setRangeStart] = createSignal('');
+ const [rangeEnd, setRangeEnd] = createSignal('');
+ const [leaseDuration, setLeaseDuration] = createSignal('');
+ const [gatewayIpError, setGatewayIpError] = createSignal('');
+ const [subnetMaskError, setSubnetMaskError] = createSignal('');
+ const [rangeStartError, setRangeStartError] = createSignal('');
+ const [rangeEndError, setRangeEndError] = createSignal('');
+ const [leaseDurationError, setLeaseDurationError] = createSignal('');
+
+ createEffect(() => {
+ if (props.open) {
+ const v4 = dhcpState.v4;
+ setGatewayIp(v4?.gateway_ip || '');
+ setSubnetMask(v4?.subnet_mask || '');
+ setRangeStart(v4?.range_start || '');
+ setRangeEnd(v4?.range_end || '');
+ setLeaseDuration(v4?.lease_duration ? String(v4.lease_duration) : '');
+ setGatewayIpError('');
+ setSubnetMaskError('');
+ setRangeStartError('');
+ setRangeEndError('');
+ setLeaseDurationError('');
+ }
+ });
+
+ const hasIpv4 = createMemo(
+ () =>
+ !!(
+ dhcpState.interfaces &&
+ dhcpState.interfaces[props.selectedInterface()]?.ipv4_addresses
+ ),
+ );
+
+ const allValues = createMemo(() => ({
+ v4: {
+ gateway_ip: gatewayIp(),
+ subnet_mask: subnetMask(),
+ range_start: rangeStart(),
+ range_end: rangeEnd(),
+ },
+ }));
+
+ const isEmptyConfig = createMemo(
+ () => !gatewayIp() && !subnetMask() && !rangeStart() && !rangeEnd() && !leaseDuration(),
+ );
+
+ const validateGatewayIp = () => {
+ const err = validateIpv4(gatewayIp()) || validateNotInRange(gatewayIp(), allValues());
+ setGatewayIpError(err || '');
+ };
+ const validateSubnetMask = () => {
+ const err = validateGatewaySubnetMask(undefined, allValues());
+ setSubnetMaskError(err || '');
+ };
+ const validateRangeStart = () => {
+ const err =
+ validateIpv4(rangeStart()) || validateIpForGatewaySubnetMask(rangeStart(), allValues());
+ setRangeStartError(err || '');
+ };
+ const validateRangeEnd = () => {
+ const err =
+ validateIpv4(rangeEnd()) ||
+ validateIpv4RangeEnd(undefined, allValues()) ||
+ validateIpForGatewaySubnetMask(rangeEnd(), allValues());
+ setRangeEndError(err || '');
+ };
+
+ const onGatewayBlur = () => {
+ validateGatewayIp();
+ validateRangeStart();
+ validateRangeEnd();
+ };
+ const onRangeStartBlur = () => {
+ validateRangeStart();
+ validateGatewayIp();
+ validateRangeEnd();
+ };
+ const onRangeEndBlur = () => {
+ validateRangeEnd();
+ validateGatewayIp();
+ };
+ const onSubnetBlur = () => {
+ validateSubnetMask();
+ validateRangeStart();
+ validateRangeEnd();
+ };
+
+ const validateLeaseDuration = () => {
+ const err = validateLeaseTime(leaseDuration());
+ setLeaseDurationError(err || '');
+ };
+
+ const handleSave = () => {
+ validateGatewayIp();
+ validateSubnetMask();
+ validateRangeStart();
+ validateRangeEnd();
+ validateLeaseDuration();
+ if (
+ gatewayIpError() ||
+ subnetMaskError() ||
+ rangeStartError() ||
+ rangeEndError() ||
+ leaseDurationError()
+ ) {
+ return;
+ }
+ props.onSave({
+ gateway_ip: gatewayIp().trim(),
+ subnet_mask: subnetMask().trim(),
+ range_start: rangeStart().trim(),
+ range_end: rangeEnd().trim(),
+ lease_duration: leaseDuration() ? Number(leaseDuration().trim()) : 0,
+ });
+ };
+
+ return (
+
+
+ setGatewayIp((e.target as HTMLInputElement).value)}
+ onBlur={onGatewayBlur}
+ id="v4_gateway_ip"
+ label={intl.getMessage('dhcp_form_gateway_address')}
+ placeholder="192.168.1.1"
+ disabled={!hasIpv4()}
+ errorMessage={gatewayIpError()}
+ size="large"
+ />
+
+
+
+ setSubnetMask((e.target as HTMLInputElement).value)}
+ onBlur={onSubnetBlur}
+ id="v4_subnet_mask"
+ label={intl.getMessage('dhcp_form_subnet_input')}
+ placeholder="255.255.255.0"
+ disabled={!hasIpv4()}
+ errorMessage={subnetMaskError()}
+ size="large"
+ />
+
+
+ setLeaseDuration((e.target as HTMLInputElement).value)}
+ onBlur={() => validateLeaseDuration()}
+ id="v4_lease_duration"
+ inputMode="numeric"
+ label={intl.getMessage('dhcp_form_lease_title')}
+ placeholder="86400"
+ disabled={!hasIpv4()}
+ size="large"
+ inputError={leaseDurationError()}
+ />
+
+
+ );
+};
diff --git a/client_v2/src/components/Dhcp/blocks/DhcpV4Modal/index.ts b/client_v2/src/components/Dhcp/blocks/DhcpV4Modal/index.ts
new file mode 100644
index 00000000000..912ad61fd28
--- /dev/null
+++ b/client_v2/src/components/Dhcp/blocks/DhcpV4Modal/index.ts
@@ -0,0 +1,2 @@
+export { DhcpV4Modal } from './DhcpV4Modal';
+export type { V4Config } from './DhcpV4Modal';
diff --git a/client_v2/src/components/Dhcp/blocks/DhcpV6Modal.tsx b/client_v2/src/components/Dhcp/blocks/DhcpV6Modal.tsx
new file mode 100644
index 00000000000..84673489407
--- /dev/null
+++ b/client_v2/src/components/Dhcp/blocks/DhcpV6Modal.tsx
@@ -0,0 +1,106 @@
+import { createSignal, createEffect, createMemo } from 'solid-js';
+import type { Accessor } from 'solid-js';
+
+import intl from 'panel/common/intl';
+import theme from 'panel/lib/theme';
+import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
+import { Input } from 'panel/common/controls/Input';
+import { dhcpState } from 'panel/stores/dhcp';
+import { validateIpv6, validateLeaseTime } from 'panel/helpers/validators';
+
+export type V6Config = {
+ range_start: string;
+ lease_duration: number;
+};
+
+type Props = {
+ open: boolean;
+ selectedInterface: Accessor;
+ onClose: () => void;
+ onSave: (values: V6Config) => void;
+};
+
+export const DhcpV6Modal = (props: Props) => {
+ const [rangeStart, setRangeStart] = createSignal('');
+ const [leaseDuration, setLeaseDuration] = createSignal('');
+ const [rangeStartError, setRangeStartError] = createSignal('');
+ const [leaseDurationError, setLeaseDurationError] = createSignal('');
+
+ createEffect(() => {
+ if (props.open) {
+ const v6 = dhcpState.v6;
+ setRangeStart(v6?.range_start || '');
+ setLeaseDuration(v6?.lease_duration ? String(v6.lease_duration) : '');
+ setRangeStartError('');
+ setLeaseDurationError('');
+ }
+ });
+
+ const hasIpv6 = createMemo(
+ () =>
+ !!(
+ dhcpState.interfaces &&
+ dhcpState.interfaces[props.selectedInterface()]?.ipv6_addresses
+ ),
+ );
+
+ const isEmptyConfig = createMemo(() => !rangeStart() && !leaseDuration());
+
+ const validateRangeStart = () => {
+ const err = validateIpv6(rangeStart());
+ setRangeStartError(err || '');
+ };
+
+ const validateLeaseDuration = () => {
+ const err = validateLeaseTime(leaseDuration());
+ setLeaseDurationError(err || '');
+ };
+
+ const handleSave = () => {
+ validateRangeStart();
+ validateLeaseDuration();
+ if (rangeStartError() || leaseDurationError()) {
+ return;
+ }
+ props.onSave({
+ range_start: rangeStart().trim(),
+ lease_duration: leaseDuration() ? Number(leaseDuration()) : 0,
+ });
+ };
+
+ return (
+
+
+ setRangeStart((e.target as HTMLInputElement).value)}
+ onBlur={validateRangeStart}
+ errorMessage={rangeStartError()}
+ />
+
+
+
+ setLeaseDuration((e.target as HTMLInputElement).value)}
+ onBlur={() => validateLeaseDuration()}
+ inputError={leaseDurationError()}
+ />
+
+
+ );
+};
diff --git a/client_v2/src/components/Dhcp/blocks/InterfaceSelector/InterfaceSelector.module.pcss b/client_v2/src/components/Dhcp/blocks/InterfaceSelector/InterfaceSelector.module.pcss
new file mode 100644
index 00000000000..bf1d15b5989
--- /dev/null
+++ b/client_v2/src/components/Dhcp/blocks/InterfaceSelector/InterfaceSelector.module.pcss
@@ -0,0 +1,40 @@
+.section {
+ padding: 0 16px;
+}
+
+.selectWrap {
+ padding: 8px 0;
+}
+
+.label {
+ margin-bottom: 4px;
+ color: var(--default-labels);
+}
+
+.info {
+ padding: 8px 0;
+}
+
+.row {
+ font-size: var(--fs-text-t3);
+ line-height: var(--lh-t2-normal);
+
+ & strong {
+ font-weight: var(--weight-semi-bold);
+ }
+}
+
+.interfaceInfoMore {
+ color: var(--default-product-icon);
+ cursor: pointer;
+ margin-left: 4px;
+}
+
+.buttonWrap {
+ padding: 16px 0;
+}
+
+.button {
+ width: auto;
+ min-width: 232px;
+}
diff --git a/client_v2/src/components/Dhcp/blocks/InterfaceSelector/InterfaceSelector.tsx b/client_v2/src/components/Dhcp/blocks/InterfaceSelector/InterfaceSelector.tsx
new file mode 100644
index 00000000000..3451a32696b
--- /dev/null
+++ b/client_v2/src/components/Dhcp/blocks/InterfaceSelector/InterfaceSelector.tsx
@@ -0,0 +1,127 @@
+import type { Accessor } from 'solid-js';
+import cn from 'clsx';
+import { Show } from 'solid-js';
+import { useNavigate } from '@solidjs/router';
+import { Select } from 'panel/common/controls/Select';
+import { Button } from 'panel/common/ui/Button';
+import intl from 'panel/common/intl';
+import { dhcpState, findActiveDhcp } from 'panel/stores/dhcp';
+import s from './InterfaceSelector.module.pcss';
+import theme from 'panel/lib/theme';
+
+type Props = {
+ selectedInterface: Accessor;
+ onInterfaceChange: (name: string) => void;
+ showAllIps: Accessor;
+ onShowAllIps: () => void;
+};
+
+const MAX_VISIBLE_IPS = 2;
+
+export const InterfaceSelector = (props: Props) => {
+ const navigate = useNavigate();
+ const interfaces = () => dhcpState.interfaces || {};
+
+ const selectOptions = () =>
+ Object.keys(interfaces()).map((name) => {
+ const iface = interfaces()[name];
+ const ipv4 = iface?.ipv4_addresses?.join(', ') || '';
+ const ipv6 = iface?.ipv6_addresses?.join(', ') || '';
+ let label = name;
+ if (ipv4) label += ` - ${ipv4}`;
+ if (ipv6) label += ` - ${ipv6}`;
+ return { label, value: name };
+ });
+
+ const selectedIface = () => interfaces()[props.selectedInterface()];
+
+ const gatewayIp = () => selectedIface()?.gateway_ip || '';
+ const hardwareAddress = () => selectedIface()?.hardware_address || '';
+ const ipAddresses = () => selectedIface()?.ip_addresses || [];
+
+ const displayIps = () => {
+ const ips = ipAddresses();
+ if (ips.length <= MAX_VISIBLE_IPS || props.showAllIps()) return ips;
+ return ips.slice(0, MAX_VISIBLE_IPS);
+ };
+
+ const remainingCount = () => {
+ const ips = ipAddresses();
+ if (ips.length <= MAX_VISIBLE_IPS || props.showAllIps()) return 0;
+ return ips.length - MAX_VISIBLE_IPS;
+ };
+
+ const handleCheck = () => {
+ if (props.selectedInterface()) {
+ findActiveDhcp(props.selectedInterface(), navigate);
+ }
+ };
+
+ const selectedOption = () => selectOptions().find((o) => o.value === props.selectedInterface());
+
+ return (
+
+
+
+ {intl.getMessage('dhcp_interface_select')}
+
+
props.onInterfaceChange(option.value)}
+ size="responsive"
+ height="big"
+ />
+
+
+
+
+
+ {intl.getMessage('dhcp_form_gateway_address_value', {
+ value: gatewayIp(),
+ })}
+
+
+
+
+ {intl.getMessage('dhcp_hardware_address_value', {
+ value: hardwareAddress(),
+ })}
+
+
+
0}>
+
+ {intl.getMessage('dhcp_ip_addresses_value', {
+ value: displayIps().join(', '),
+ })}
+
+ 0}>
+ props.onShowAllIps()}
+ >
+ {intl.getMessage('show_more_count', {
+ count: String(remainingCount()),
+ })}
+
+
+
+
+
+
+
+ {intl.getMessage('check_dhcp_servers')}
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/Dhcp/blocks/InterfaceSelector/index.ts b/client_v2/src/components/Dhcp/blocks/InterfaceSelector/index.ts
new file mode 100644
index 00000000000..bcf8c4698ce
--- /dev/null
+++ b/client_v2/src/components/Dhcp/blocks/InterfaceSelector/index.ts
@@ -0,0 +1 @@
+export { InterfaceSelector } from './InterfaceSelector';
diff --git a/client_v2/src/components/Dhcp/index.ts b/client_v2/src/components/Dhcp/index.ts
new file mode 100644
index 00000000000..8c187b52358
--- /dev/null
+++ b/client_v2/src/components/Dhcp/index.ts
@@ -0,0 +1 @@
+export { Dhcp } from './Dhcp';
diff --git a/client_v2/src/components/Dhcp/styles.module.pcss b/client_v2/src/components/Dhcp/styles.module.pcss
new file mode 100644
index 00000000000..0fefdb60e0f
--- /dev/null
+++ b/client_v2/src/components/Dhcp/styles.module.pcss
@@ -0,0 +1,22 @@
+.header {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ padding: 0 16px 16px;
+}
+
+.menuButton {
+ position: relative;
+ top: 4px;
+}
+
+.title {
+ padding: 0;
+}
+
+.unavailable {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ padding: 16px;
+}
diff --git a/client_v2/src/components/DnsSettings/Access/Access.tsx b/client_v2/src/components/DnsSettings/Access/Access.tsx
new file mode 100644
index 00000000000..18a7d58209b
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/Access/Access.tsx
@@ -0,0 +1,80 @@
+import { createMemo } from 'solid-js';
+import cn from 'clsx';
+
+import { accessState } from 'panel/stores/access';
+import intl from 'panel/common/intl';
+import { SettingRow } from 'panel/common/ui/SettingRow';
+import { useDialog } from 'panel/hooks/useDialog';
+import { getListSummary } from '../helpers';
+import theme from 'panel/lib/theme';
+
+import { AllowedClientsDialog } from './blocks/AllowedClientsDialog';
+import { DisallowedClientsDialog } from './blocks/DisallowedClientsDialog';
+import { DisallowedDomainsDialog } from './blocks/DisallowedDomainsDialog';
+
+export const Access = () => {
+ const allowedDialog = useDialog();
+ const disallowedClientsDialog = useDialog();
+ const disallowedDomainsDialog = useDialog();
+
+ const allowedClientsOn = createMemo(() => accessState.allowed_clients.trim().length > 0);
+ const processing = () => accessState.processingSet;
+
+ const allowedClientsValue = createMemo(() => getListSummary(accessState.allowed_clients));
+ const disallowedClientsValue = createMemo(() => getListSummary(accessState.disallowed_clients));
+ const disallowedDomainsValue = createMemo(() => getListSummary(accessState.blocked_hosts));
+
+ return (
+
+
+ {intl.getMessage('dns_access_settings_title')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/DnsSettings/Access/blocks/AllowedClientsDialog.tsx b/client_v2/src/components/DnsSettings/Access/blocks/AllowedClientsDialog.tsx
new file mode 100644
index 00000000000..7aa9f0452ee
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/Access/blocks/AllowedClientsDialog.tsx
@@ -0,0 +1,70 @@
+import { untrack, type Accessor } from 'solid-js';
+
+import { accessState, setAccessList } from 'panel/stores/access';
+import intl from 'panel/common/intl';
+import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
+import { Textarea } from 'panel/common/controls/Textarea';
+import { validateClientsPerLine } from 'panel/helpers/validators';
+import { useField } from 'panel/hooks/useField';
+import theme from 'panel/lib/theme';
+
+type Props = {
+ open: Accessor;
+ onClose: () => void;
+ processing: boolean;
+};
+
+export const AllowedClientsDialog = (props: Props) => {
+ const field = useField(
+ () => props.open(),
+ () => accessState.allowed_clients,
+ { validate: (v) => (v ? validateClientsPerLine(v) || '' : '') },
+ );
+
+ return (
+
+ {intl.getMessage('dns_allowed_clients_desc')}
+
+ {intl.getMessage('dns_allowed_clients_desc_2', {
+ a: (text: string) => (
+
+ {text}
+
+ ),
+ })}
+
+ >
+ }
+ onClose={props.onClose}
+ onSubmit={() => {
+ field.submitIfValid((v) => {
+ setAccessList({ allowed_clients: v });
+ untrack(() => props.onClose());
+ });
+ }}
+ processing={props.processing}
+ >
+
+
+
+ );
+};
diff --git a/client_v2/src/components/DnsSettings/Access/blocks/DisallowedClientsDialog.tsx b/client_v2/src/components/DnsSettings/Access/blocks/DisallowedClientsDialog.tsx
new file mode 100644
index 00000000000..3a1841c51aa
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/Access/blocks/DisallowedClientsDialog.tsx
@@ -0,0 +1,69 @@
+import { untrack, type Accessor } from 'solid-js';
+
+import { accessState, setAccessList } from 'panel/stores/access';
+import intl from 'panel/common/intl';
+import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
+import { Textarea } from 'panel/common/controls/Textarea';
+import { validateClientsPerLine } from 'panel/helpers/validators';
+import { useField } from 'panel/hooks/useField';
+import theme from 'panel/lib/theme';
+
+type Props = {
+ open: Accessor;
+ onClose: () => void;
+ processing: boolean;
+};
+
+export const DisallowedClientsDialog = (props: Props) => {
+ const field = useField(
+ () => props.open(),
+ () => accessState.disallowed_clients,
+ { validate: (v) => (v ? validateClientsPerLine(v) || '' : '') },
+ );
+
+ return (
+
+ {intl.getMessage('dns_disallowed_clients_desc')}
+
+ {intl.getMessage('dns_allowed_clients_desc_2', {
+ a: (text: string) => (
+
+ {text}
+
+ ),
+ })}
+
+ >
+ }
+ onClose={props.onClose}
+ onSubmit={() => {
+ field.submitIfValid((v) => {
+ setAccessList({ disallowed_clients: v });
+ untrack(() => props.onClose());
+ });
+ }}
+ processing={props.processing}
+ >
+
+
+
+ );
+};
diff --git a/client_v2/src/components/DnsSettings/Access/blocks/DisallowedDomainsDialog.tsx b/client_v2/src/components/DnsSettings/Access/blocks/DisallowedDomainsDialog.tsx
new file mode 100644
index 00000000000..4adb6704680
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/Access/blocks/DisallowedDomainsDialog.tsx
@@ -0,0 +1,56 @@
+import { untrack, type Accessor } from 'solid-js';
+
+import { accessState, setAccessList } from 'panel/stores/access';
+import intl from 'panel/common/intl';
+import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
+import { Textarea } from 'panel/common/controls/Textarea';
+import { validateDomainsPerLine } from 'panel/helpers/validators';
+import { useField } from 'panel/hooks/useField';
+import theme from 'panel/lib/theme';
+
+type Props = {
+ open: Accessor;
+ onClose: () => void;
+ processing: boolean;
+};
+
+export const DisallowedDomainsDialog = (props: Props) => {
+ const field = useField(
+ () => props.open(),
+ () => accessState.blocked_hosts,
+ { validate: (v) => (v ? validateDomainsPerLine(v) || '' : '') },
+ );
+
+ return (
+
+ {intl.getMessage('dns_disallowed_domains_desc')}
+ {intl.getMessage('dns_disallowed_domains_desc_2')}
+ >
+ }
+ onClose={props.onClose}
+ onSubmit={() => {
+ field.submitIfValid((v) => {
+ setAccessList({ blocked_hosts: v });
+ untrack(() => props.onClose());
+ });
+ }}
+ processing={props.processing}
+ >
+
+
+
+ );
+};
diff --git a/client_v2/src/components/DnsSettings/Access/index.ts b/client_v2/src/components/DnsSettings/Access/index.ts
new file mode 100644
index 00000000000..2145b5a255c
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/Access/index.ts
@@ -0,0 +1 @@
+export { Access } from './Access';
diff --git a/client_v2/src/components/DnsSettings/Cache/Cache.module.pcss b/client_v2/src/components/DnsSettings/Cache/Cache.module.pcss
new file mode 100644
index 00000000000..9154e039124
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/Cache/Cache.module.pcss
@@ -0,0 +1,13 @@
+.section {
+ padding-top: 24px;
+}
+
+.title {
+ margin-bottom: 8px;
+ font-weight: var(--weight-bold);
+}
+
+.description {
+ font-size: var(--fs-t2-normal);
+ line-height: var(--lh-t2-normal);
+}
diff --git a/client_v2/src/components/DnsSettings/Cache/Cache.tsx b/client_v2/src/components/DnsSettings/Cache/Cache.tsx
new file mode 100644
index 00000000000..adfddc145d9
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/Cache/Cache.tsx
@@ -0,0 +1,136 @@
+import { createSignal, createMemo, Show } from 'solid-js';
+import cn from 'clsx';
+
+import {
+ dnsConfigState,
+ clearDnsCache,
+ toggleCacheEnabled,
+ toggleOptimisticCaching,
+} from 'panel/stores/dnsConfig';
+import intl from 'panel/common/intl';
+import { Button } from 'panel/common/ui/Button';
+import { SettingRow } from 'panel/common/ui/SettingRow';
+import { ConfirmDialog } from 'panel/common/ui/ConfirmDialog';
+import { useDialog } from 'panel/hooks/useDialog';
+import { getCacheSizeSummary, getTtlSummary } from '../helpers';
+import theme from 'panel/lib/theme';
+
+import s from './Cache.module.pcss';
+import { CacheInputDialog } from './blocks/CacheInputDialog';
+
+export const Cache = () => {
+ const [showClearConfirm, setShowClearConfirm] = createSignal(false);
+
+ const cacheSizeDialog = useDialog();
+ const minTtlDialog = useDialog();
+ const maxTtlDialog = useDialog();
+
+ const cacheSizeValue = createMemo(() => getCacheSizeSummary(dnsConfigState.cache_size));
+ const minTtlValue = createMemo(() => getTtlSummary(dnsConfigState.cache_ttl_min));
+ const maxTtlValue = createMemo(() => getTtlSummary(dnsConfigState.cache_ttl_max));
+
+ const processing = () => dnsConfigState.processingSetConfig;
+
+ return (
+
+
toggleCacheEnabled()}
+ />
+
+
+
+
+
+
+
+ toggleOptimisticCaching()}
+ />
+
+
+ setShowClearConfirm(true)}
+ class={theme.form.actionButton}
+ compact
+ >
+ {intl.getMessage('dns_clear_cache')}
+
+
+
+
+ setShowClearConfirm(false)}
+ onConfirm={() => {
+ clearDnsCache();
+ setShowClearConfirm(false);
+ }}
+ />
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/DnsSettings/Cache/blocks/CacheInputDialog.tsx b/client_v2/src/components/DnsSettings/Cache/blocks/CacheInputDialog.tsx
new file mode 100644
index 00000000000..bef778214ff
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/Cache/blocks/CacheInputDialog.tsx
@@ -0,0 +1,111 @@
+import { untrack, type Accessor } from 'solid-js';
+import { dnsConfigState, setDnsConfig } from 'panel/stores/dnsConfig';
+import intl from 'panel/common/intl';
+import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
+import { Input } from 'panel/common/controls/Input';
+import { UINT32_RANGE } from 'panel/helpers/constants';
+import { validateBetween, validateRequiredValue } from 'panel/helpers/validators';
+import { useField } from 'panel/hooks/useField';
+import theme from 'panel/lib/theme';
+
+type CacheConfigKey = 'cache_size' | 'cache_ttl_min' | 'cache_ttl_max';
+
+type Props = {
+ configKey: CacheConfigKey;
+ open: Accessor;
+ onClose: () => void;
+ processing: boolean;
+};
+
+const CACHE_DIALOG_CONFIG: Record<
+ CacheConfigKey,
+ {
+ title: () => string;
+ description: () => string;
+ label: () => string;
+ placeholder?: () => string;
+ validate: (v: string) => string;
+ }
+> = {
+ cache_size: {
+ title: () => intl.getMessage('dns_cache_size_title'),
+ description: () => intl.getMessage('dns_cache_size_desc'),
+ label: () => intl.getMessage('dns_cache_size_label'),
+ validate: (v) =>
+ validateRequiredValue(v) ||
+ validateBetween(Number(v), UINT32_RANGE.MIN, UINT32_RANGE.MAX) ||
+ '',
+ },
+ cache_ttl_min: {
+ title: () => intl.getMessage('dns_override_min_ttl_title'),
+ description: () => intl.getMessage('dns_override_min_ttl_desc'),
+ label: () => intl.getMessage('dns_override_min_ttl_label'),
+ placeholder: () => intl.getMessage('dns_override_min_ttl_placeholder'),
+ validate: (v) =>
+ validateRequiredValue(v) ||
+ validateBetween(Number(v), UINT32_RANGE.MIN, UINT32_RANGE.MAX) ||
+ '',
+ },
+ cache_ttl_max: {
+ title: () => intl.getMessage('dns_override_max_ttl_title'),
+ description: () => intl.getMessage('dns_override_max_ttl_desc'),
+ label: () => intl.getMessage('dns_override_max_ttl_label'),
+ placeholder: () => intl.getMessage('dns_override_max_ttl_placeholder'),
+ validate: (v) => {
+ const requiredErr = validateRequiredValue(v);
+ if (requiredErr) return requiredErr;
+ const num = Number(v);
+ const rangeErr = validateBetween(num, UINT32_RANGE.MIN, UINT32_RANGE.MAX);
+ if (rangeErr) return rangeErr;
+ // Cross-field TTL check: min > max
+ const minVal = Number(dnsConfigState.cache_ttl_min);
+ if (minVal > 0 && num > 0 && minVal > num) {
+ return intl.getMessage('cache_config_ttl_validation');
+ }
+ return '';
+ },
+ },
+};
+
+export const CacheInputDialog = (props: Props) => {
+ const config = () => CACHE_DIALOG_CONFIG[props.configKey];
+
+ const field = useField(
+ () => props.open(),
+ () => String(dnsConfigState[props.configKey] ?? 0),
+ { validate: config().validate },
+ );
+
+ return (
+ {
+ const configKey = props.configKey;
+ field.submitIfValid((v) => {
+ setDnsConfig({ [configKey]: v === '' ? 0 : Number(v) });
+ untrack(() => props.onClose());
+ });
+ }}
+ processing={props.processing}
+ >
+
+ field.setValue((e.target as HTMLInputElement).value)}
+ onBlur={() => field.validate()}
+ id={props.configKey}
+ label={config().label()}
+ placeholder={config().placeholder?.()}
+ min={UINT32_RANGE.MIN}
+ max={UINT32_RANGE.MAX}
+ errorMessage={field.error()}
+ size="large"
+ />
+
+
+ );
+};
diff --git a/client_v2/src/components/DnsSettings/Cache/index.ts b/client_v2/src/components/DnsSettings/Cache/index.ts
new file mode 100644
index 00000000000..006dd2c92a7
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/Cache/index.ts
@@ -0,0 +1 @@
+export { Cache } from './Cache';
diff --git a/client_v2/src/components/DnsSettings/DnsSettings.tsx b/client_v2/src/components/DnsSettings/DnsSettings.tsx
new file mode 100644
index 00000000000..7f0aae4c005
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/DnsSettings.tsx
@@ -0,0 +1,40 @@
+import { Show, onMount } from 'solid-js';
+
+import { dnsConfigState, getDnsConfig } from 'panel/stores/dnsConfig';
+import { accessState, getAccessList } from 'panel/stores/access';
+import intl from 'panel/common/intl';
+import cn from 'clsx';
+
+import { PageLoader } from 'panel/common/ui/Loader';
+import theme from 'panel/lib/theme';
+import { Upstream } from './Upstream';
+import { Access } from './Access';
+import { ServerConfig } from './ServerConfig';
+import { Cache } from './Cache';
+
+export const DnsSettings = () => {
+ onMount(() => {
+ getAccessList();
+ getDnsConfig();
+ });
+
+ return (
+
+
+
+ {intl.getMessage('dns_settings')}
+
+
+
}
+ >
+
+
+
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/DnsSettings/PrivateReverse/PrivateReverse.module.pcss b/client_v2/src/components/DnsSettings/PrivateReverse/PrivateReverse.module.pcss
new file mode 100644
index 00000000000..3f7d1735c35
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/PrivateReverse/PrivateReverse.module.pcss
@@ -0,0 +1,27 @@
+.title {
+ margin-bottom: 8px;
+ font-weight: var(--weight-bold);
+}
+
+.desc {
+ margin-bottom: 12px;
+ font-size: var(--fs-text-t2);
+ line-height: var(--lh-t2-normal);
+ color: var(--default-main-text);
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+}
+
+.container {
+ padding-top: 0;
+}
+
+.form {
+ padding-top: 24px;
+}
+
+.breadcrumbs {
+ padding: 0 16px;
+}
diff --git a/client_v2/src/components/DnsSettings/PrivateReverse/PrivateReverse.tsx b/client_v2/src/components/DnsSettings/PrivateReverse/PrivateReverse.tsx
new file mode 100644
index 00000000000..933a1aa36b8
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/PrivateReverse/PrivateReverse.tsx
@@ -0,0 +1,92 @@
+import { createMemo } from 'solid-js';
+import cn from 'clsx';
+
+import {
+ dnsConfigState,
+ togglePrivatePtrResolvers,
+ toggleResolveClients,
+} from 'panel/stores/dnsConfig';
+import intl from 'panel/common/intl';
+import { Breadcrumbs } from 'panel/common/ui/Breadcrumbs';
+import { SettingRow } from 'panel/common/ui/SettingRow';
+import { RoutePath } from 'panel/components/Routes/Paths';
+import { useDialog } from 'panel/hooks/useDialog';
+import { PrivateReverseServersDialog } from '../Upstream/blocks/PrivateReverseServersDialog';
+import { getUpstreamServersSummary } from '../helpers';
+import theme from 'panel/lib/theme';
+
+import s from './PrivateReverse.module.pcss';
+
+export const PrivateReverse = () => {
+ const serversDialog = useDialog();
+
+ const processing = () => dnsConfigState.processingSetConfig;
+
+ const privateReverseValue = createMemo(() =>
+ getUpstreamServersSummary(dnsConfigState.local_ptr_upstreams),
+ );
+
+ return (
+
+ );
+};
diff --git a/client_v2/src/components/DnsSettings/PrivateReverse/index.ts b/client_v2/src/components/DnsSettings/PrivateReverse/index.ts
new file mode 100644
index 00000000000..5c50e395361
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/PrivateReverse/index.ts
@@ -0,0 +1 @@
+export { PrivateReverse } from './PrivateReverse';
diff --git a/client_v2/src/components/DnsSettings/ServerConfig/ServerConfig.tsx b/client_v2/src/components/DnsSettings/ServerConfig/ServerConfig.tsx
new file mode 100644
index 00000000000..d5dded5c6cc
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/ServerConfig/ServerConfig.tsx
@@ -0,0 +1,160 @@
+import { createMemo } from 'solid-js';
+import cn from 'clsx';
+
+import {
+ dnsConfigState,
+ toggleDnssecEnabled,
+ toggleDisableIPv6,
+ toggleEdnsCsEnabled,
+} from 'panel/stores/dnsConfig';
+import intl from 'panel/common/intl';
+import { SettingRow } from 'panel/common/ui/SettingRow';
+import { getRateLimitSummary, getBlockingModeSummary, getUpstreamServersSummary } from '../helpers';
+import theme from 'panel/lib/theme';
+import { IP_VERSION, IPV4_SUBNET_PREFIX, IPV6_SUBNET_PREFIX } from 'panel/helpers/constants';
+import { useDialog } from 'panel/hooks/useDialog';
+
+import { RateLimitDialog } from './blocks/RateLimitDialog';
+import { SubnetPrefixV4Dialog } from './blocks/SubnetPrefixV4Dialog';
+import { SubnetPrefixV6Dialog } from './blocks/SubnetPrefixV6Dialog';
+import { BlockingModeDialog } from './blocks/BlockingModeDialog';
+import { RateLimitAllowlistDialog } from './blocks/RateLimitAllowlistDialog';
+import { EdnsDialog } from './blocks/EdnsDialog';
+
+export const ServerConfig = () => {
+ const rateLimitDialog = useDialog();
+ const subnetV4Dialog = useDialog();
+ const subnetV6Dialog = useDialog();
+ const blockingModeDialog = useDialog();
+ const allowlistDialog = useDialog();
+ const ednsDialog = useDialog();
+
+ const rateLimitValue = createMemo(() => getRateLimitSummary(dnsConfigState.ratelimit));
+ const blockingModeValue = createMemo(() =>
+ getBlockingModeSummary(dnsConfigState.blocking_mode),
+ );
+
+ const processing = () => dnsConfigState.processingSetConfig;
+
+ return (
+
+
+ {intl.getMessage('dns_config')}
+
+
+
+
+
+
+
+
+
+
+
+
+ toggleEdnsCsEnabled()}
+ onClick={ednsDialog.openDialog}
+ divider
+ />
+
+ toggleDnssecEnabled()}
+ />
+
+ toggleDisableIPv6()}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/DnsSettings/ServerConfig/blocks/BlockingModeDialog.tsx b/client_v2/src/components/DnsSettings/ServerConfig/blocks/BlockingModeDialog.tsx
new file mode 100644
index 00000000000..c390d6b3a0e
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/ServerConfig/blocks/BlockingModeDialog.tsx
@@ -0,0 +1,173 @@
+import { createSignal, createEffect, type Accessor, Show } from 'solid-js';
+
+import { dnsConfigState, setDnsConfig } from 'panel/stores/dnsConfig';
+import intl from 'panel/common/intl';
+import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
+import { Input } from 'panel/common/controls/Input';
+import { Radio } from 'panel/common/controls/Radio';
+import { BLOCKING_MODES, UINT32_RANGE } from 'panel/helpers/constants';
+import { getBlockingModeOptions } from '../../helpers';
+import {
+ validateRequiredValue,
+ validateBetween,
+ validateIpv4,
+ validateIpv6,
+} from 'panel/helpers/validators';
+import { useField } from 'panel/hooks/useField';
+import { FaqTooltip } from 'panel/common/ui/FaqTooltip';
+import theme from 'panel/lib/theme';
+
+type Props = {
+ open: Accessor;
+ onClose: () => void;
+ processing: boolean;
+};
+
+export const BlockingModeDialog = (props: Props) => {
+ const blockingModeOptions = getBlockingModeOptions();
+
+ const [blockingMode, setBlockingMode] = createSignal(dnsConfigState.blocking_mode);
+ createEffect(() => {
+ if (props.open()) {
+ setBlockingMode(dnsConfigState.blocking_mode);
+ }
+ });
+
+ const blockingIpv4 = useField(
+ () => props.open(),
+ () => dnsConfigState.blocking_ipv4,
+ {
+ validate: (v) => validateRequiredValue(v) || validateIpv4(v) || '',
+ },
+ );
+ const blockingIpv6 = useField(
+ () => props.open(),
+ () => dnsConfigState.blocking_ipv6,
+ {
+ validate: (v) => validateRequiredValue(v) || validateIpv6(v) || '',
+ },
+ );
+ const ttl = useField(
+ () => props.open(),
+ () => dnsConfigState.blocked_response_ttl,
+ {
+ validate: (v) => {
+ if (Number.isNaN(v)) {
+ return intl.getMessage('form_error_required');
+ }
+ return validateBetween(v, UINT32_RANGE.MIN, UINT32_RANGE.MAX) || '';
+ },
+ },
+ );
+
+ const handleSubmit = () => {
+ const isCustom = blockingMode() === BLOCKING_MODES.custom_ip;
+ if (isCustom && (blockingIpv4.validate() || blockingIpv6.validate())) return;
+ if (ttl.validate()) return;
+
+ const payload: Record = {
+ blocking_mode: blockingMode(),
+ blocked_response_ttl: ttl.value(),
+ };
+ if (blockingMode() === BLOCKING_MODES.custom_ip) {
+ payload.blocking_ipv4 = blockingIpv4.value();
+ payload.blocking_ipv6 = blockingIpv6.value();
+ }
+ setDnsConfig(payload);
+ props.onClose();
+ };
+
+ return (
+
+ setBlockingMode(v)}
+ inModal
+ />
+
+ <>
+
+
+ {intl.getMessage('dns_blocking_mode_ipv4_label')}
+
+ >
+ }
+ placeholder={intl.getMessage('dns_blocking_mode_ipv4_placeholder')}
+ value={blockingIpv4.value()}
+ onChange={(e) => blockingIpv4.setValue(e.target.value)}
+ onBlur={() => blockingIpv4.validate()}
+ errorMessage={blockingIpv4.error()}
+ disabled={blockingMode() !== BLOCKING_MODES.custom_ip}
+ size="large"
+ />
+
+
+
+ {intl.getMessage('dns_blocking_mode_ipv6_label')}
+
+ >
+ }
+ placeholder={intl.getMessage('dns_blocking_mode_ipv6_placeholder')}
+ value={blockingIpv6.value()}
+ onChange={(e) => blockingIpv6.setValue(e.target.value)}
+ onBlur={() => blockingIpv6.validate()}
+ errorMessage={blockingIpv6.error()}
+ disabled={blockingMode() !== BLOCKING_MODES.custom_ip}
+ size="large"
+ />
+
+ >
+
+
+
+ {intl.getMessage('dns_blocking_mode_ttl_label')}
+
+ >
+ }
+ placeholder={intl.getMessage('dns_blocking_mode_ttl_placeholder')}
+ value={ttl.value()}
+ onChange={(e) =>
+ ttl.setValue(e.target.value === '' ? NaN : Number(e.target.value))
+ }
+ onBlur={() => ttl.validate()}
+ min={UINT32_RANGE.MIN}
+ max={UINT32_RANGE.MAX}
+ errorMessage={ttl.error()}
+ size="large"
+ />
+
+
+ );
+};
diff --git a/client_v2/src/components/DnsSettings/ServerConfig/blocks/EdnsDialog.tsx b/client_v2/src/components/DnsSettings/ServerConfig/blocks/EdnsDialog.tsx
new file mode 100644
index 00000000000..446a75f4aa2
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/ServerConfig/blocks/EdnsDialog.tsx
@@ -0,0 +1,97 @@
+import { createSignal, createEffect, type Accessor } from 'solid-js';
+
+import { dnsConfigState, setDnsConfig } from 'panel/stores/dnsConfig';
+import intl from 'panel/common/intl';
+import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
+import { Input } from 'panel/common/controls/Input';
+import { Radio } from 'panel/common/controls/Radio';
+import { EDNS_MODES } from 'panel/helpers/constants';
+import { getEdnsOptions } from '../../helpers';
+import { validateRequiredValue, validateIp } from 'panel/helpers/validators';
+import { useField } from 'panel/hooks/useField';
+import theme from 'panel/lib/theme';
+
+type Props = {
+ open: Accessor;
+ onClose: () => void;
+ processing: boolean;
+};
+
+export const EdnsDialog = (props: Props) => {
+ const ednsOptions = getEdnsOptions();
+
+ // EDNS custom flag — plain signal synced from store
+ const [ednsCsUseCustom, setEdnsCsUseCustom] = createSignal(
+ dnsConfigState.edns_cs_use_custom ?? false,
+ );
+ createEffect(() => {
+ if (props.open()) {
+ setEdnsCsUseCustom(dnsConfigState.edns_cs_use_custom ?? false);
+ }
+ });
+
+ // Custom IP field — conditionally validated
+ const ednsCsCustomIp = useField(
+ () => props.open(),
+ () => dnsConfigState.edns_cs_custom_ip ?? '',
+ {
+ validate: (v) =>
+ ednsCsUseCustom() ? validateRequiredValue(v) || validateIp(v) || '' : '',
+ },
+ );
+
+ // Clear custom IP error when switching away from custom mode
+ createEffect(() => {
+ if (!ednsCsUseCustom()) {
+ ednsCsCustomIp.setError('');
+ }
+ });
+
+ const handleSubmit = () => {
+ // Bugfix: submit-time validation
+ if (ednsCsUseCustom() && ednsCsCustomIp.validate()) return;
+
+ const payload: Record = {
+ edns_cs_use_custom: ednsCsUseCustom(),
+ };
+ if (ednsCsUseCustom()) {
+ payload.edns_cs_custom_ip = ednsCsCustomIp.value();
+ }
+ setDnsConfig(payload);
+ props.onClose();
+ };
+
+ return (
+
+
+ setEdnsCsUseCustom(v === EDNS_MODES.custom)}
+ inModal
+ />
+
+ ednsCsCustomIp.setValue((e.target as HTMLInputElement).value)
+ }
+ onBlur={() => ednsCsCustomIp.validate()}
+ disabled={!ednsCsUseCustom()}
+ errorMessage={ednsCsCustomIp.error()}
+ size="large"
+ />
+
+
+ );
+};
diff --git a/client_v2/src/components/DnsSettings/ServerConfig/blocks/RateLimitAllowlistDialog.tsx b/client_v2/src/components/DnsSettings/ServerConfig/blocks/RateLimitAllowlistDialog.tsx
new file mode 100644
index 00000000000..c3c5d9581ed
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/ServerConfig/blocks/RateLimitAllowlistDialog.tsx
@@ -0,0 +1,54 @@
+import { untrack, type Accessor } from 'solid-js';
+
+import { dnsConfigState, setDnsConfig } from 'panel/stores/dnsConfig';
+import intl from 'panel/common/intl';
+import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
+import { Textarea } from 'panel/common/controls/Textarea';
+import { validateIpPerLine } from 'panel/helpers/validators';
+import { useField } from 'panel/hooks/useField';
+import theme from 'panel/lib/theme';
+
+type Props = {
+ open: Accessor;
+ onClose: () => void;
+ processing: boolean;
+};
+
+export const RateLimitAllowlistDialog = (props: Props) => {
+ const field = useField(
+ () => props.open(),
+ () => dnsConfigState.ratelimit_whitelist,
+ {
+ validate: (v) => (v ? validateIpPerLine(v) || '' : ''),
+ },
+ );
+
+ return (
+ {
+ field.submitIfValid((v) => {
+ setDnsConfig({ ratelimit_whitelist: v });
+ untrack(() => props.onClose());
+ });
+ }}
+ processing={props.processing}
+ >
+
+
+
+ );
+};
diff --git a/client_v2/src/components/DnsSettings/ServerConfig/blocks/RateLimitDialog.tsx b/client_v2/src/components/DnsSettings/ServerConfig/blocks/RateLimitDialog.tsx
new file mode 100644
index 00000000000..e35a06c0736
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/ServerConfig/blocks/RateLimitDialog.tsx
@@ -0,0 +1,68 @@
+import { untrack, type Accessor } from 'solid-js';
+
+import { dnsConfigState, setDnsConfig } from 'panel/stores/dnsConfig';
+import intl from 'panel/common/intl';
+import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
+import { Input } from 'panel/common/controls/Input';
+import { RATE_LIMIT } from 'panel/helpers/constants';
+import { validateRequiredValue, validateBetween } from 'panel/helpers/validators';
+import { useField } from 'panel/hooks/useField';
+import theme from 'panel/lib/theme';
+
+type Props = {
+ open: Accessor;
+ onClose: () => void;
+ processing: boolean;
+};
+
+export const RateLimitDialog = (props: Props) => {
+ const field = useField(
+ () => props.open(),
+ () => dnsConfigState.ratelimit,
+ {
+ validate: (v) =>
+ validateRequiredValue(String(v)) ||
+ validateBetween(v, RATE_LIMIT.MIN, RATE_LIMIT.MAX) ||
+ '',
+ },
+ );
+
+ return (
+
+ {intl.getMessage('dns_rate_limit_desc')}
+ {intl.getMessage('dns_rate_limit_desc_2')}
+ >
+ }
+ onClose={props.onClose}
+ onSubmit={() => {
+ field.submitIfValid((v) => {
+ setDnsConfig({ ratelimit: v });
+ untrack(() => props.onClose());
+ });
+ }}
+ processing={props.processing}
+ >
+
+
+ field.setValue(Number((e.target as HTMLInputElement).value))
+ }
+ onBlur={() => field.validate()}
+ id="ratelimit"
+ label={intl.getMessage('server_config_rate_limit')}
+ placeholder={intl.getMessage('dns_rate_limit_placeholder')}
+ min={RATE_LIMIT.MIN}
+ max={RATE_LIMIT.MAX}
+ errorMessage={field.error()}
+ size="large"
+ />
+
+
+ );
+};
diff --git a/client_v2/src/components/DnsSettings/ServerConfig/blocks/SubnetPrefixV4Dialog.tsx b/client_v2/src/components/DnsSettings/ServerConfig/blocks/SubnetPrefixV4Dialog.tsx
new file mode 100644
index 00000000000..8193d85f825
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/ServerConfig/blocks/SubnetPrefixV4Dialog.tsx
@@ -0,0 +1,63 @@
+import { untrack, type Accessor } from 'solid-js';
+
+import { dnsConfigState, setDnsConfig } from 'panel/stores/dnsConfig';
+import intl from 'panel/common/intl';
+import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
+import { Input } from 'panel/common/controls/Input';
+import { IPV4_SUBNET_PREFIX, IP_VERSION } from 'panel/helpers/constants';
+import { validateRequiredValue, validateBetween } from 'panel/helpers/validators';
+import { useField } from 'panel/hooks/useField';
+import theme from 'panel/lib/theme';
+
+type Props = {
+ open: Accessor;
+ onClose: () => void;
+ processing: boolean;
+};
+
+export const SubnetPrefixV4Dialog = (props: Props) => {
+ const field = useField(
+ () => props.open(),
+ () => dnsConfigState.ratelimit_subnet_len_ipv4 ?? IPV4_SUBNET_PREFIX.DEFAULT,
+ {
+ validate: (v) =>
+ validateRequiredValue(String(v)) ||
+ validateBetween(v, IPV4_SUBNET_PREFIX.MIN, IPV4_SUBNET_PREFIX.MAX) ||
+ '',
+ },
+ );
+
+ return (
+ {
+ field.submitIfValid((v) => {
+ setDnsConfig({ ratelimit_subnet_len_ipv4: v });
+ untrack(() => props.onClose());
+ });
+ }}
+ processing={props.processing}
+ >
+
+
+ field.setValue(Number((e.target as HTMLInputElement).value))
+ }
+ onBlur={() => field.validate()}
+ id="ratelimit_subnet_len_ipv4"
+ label={intl.getMessage('dns_subnet_prefix_title', { value: IP_VERSION.V4 })}
+ placeholder={intl.getMessage('dns_subnet_placeholder')}
+ min={IPV4_SUBNET_PREFIX.MIN}
+ max={IPV4_SUBNET_PREFIX.MAX}
+ errorMessage={field.error()}
+ size="large"
+ />
+
+
+ );
+};
diff --git a/client_v2/src/components/DnsSettings/ServerConfig/blocks/SubnetPrefixV6Dialog.tsx b/client_v2/src/components/DnsSettings/ServerConfig/blocks/SubnetPrefixV6Dialog.tsx
new file mode 100644
index 00000000000..6990c9852f4
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/ServerConfig/blocks/SubnetPrefixV6Dialog.tsx
@@ -0,0 +1,63 @@
+import { untrack, type Accessor } from 'solid-js';
+
+import { dnsConfigState, setDnsConfig } from 'panel/stores/dnsConfig';
+import intl from 'panel/common/intl';
+import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
+import { Input } from 'panel/common/controls/Input';
+import { IPV6_SUBNET_PREFIX, IP_VERSION } from 'panel/helpers/constants';
+import { validateRequiredValue, validateBetween } from 'panel/helpers/validators';
+import { useField } from 'panel/hooks/useField';
+import theme from 'panel/lib/theme';
+
+type Props = {
+ open: Accessor;
+ onClose: () => void;
+ processing: boolean;
+};
+
+export const SubnetPrefixV6Dialog = (props: Props) => {
+ const field = useField(
+ () => props.open(),
+ () => dnsConfigState.ratelimit_subnet_len_ipv6 ?? IPV6_SUBNET_PREFIX.DEFAULT,
+ {
+ validate: (v) =>
+ validateRequiredValue(String(v)) ||
+ validateBetween(v, IPV6_SUBNET_PREFIX.MIN, IPV6_SUBNET_PREFIX.MAX) ||
+ '',
+ },
+ );
+
+ return (
+ {
+ field.submitIfValid((v) => {
+ setDnsConfig({ ratelimit_subnet_len_ipv6: v });
+ untrack(() => props.onClose());
+ });
+ }}
+ processing={props.processing}
+ >
+
+
+ field.setValue(Number((e.target as HTMLInputElement).value))
+ }
+ onBlur={() => field.validate()}
+ id="ratelimit_subnet_len_ipv6"
+ label={intl.getMessage('dns_subnet_prefix_title', { value: IP_VERSION.V6 })}
+ placeholder={intl.getMessage('dns_subnet_placeholder')}
+ min={IPV6_SUBNET_PREFIX.MIN}
+ max={IPV6_SUBNET_PREFIX.MAX}
+ errorMessage={field.error()}
+ size="large"
+ />
+
+
+ );
+};
diff --git a/client_v2/src/components/DnsSettings/ServerConfig/index.ts b/client_v2/src/components/DnsSettings/ServerConfig/index.ts
new file mode 100644
index 00000000000..72a769dcdc1
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/ServerConfig/index.ts
@@ -0,0 +1 @@
+export { ServerConfig } from './ServerConfig';
diff --git a/client_v2/src/components/DnsSettings/Upstream/Upstream.tsx b/client_v2/src/components/DnsSettings/Upstream/Upstream.tsx
new file mode 100644
index 00000000000..afa55d2c919
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/Upstream/Upstream.tsx
@@ -0,0 +1,162 @@
+import { createMemo } from 'solid-js';
+import { useNavigate } from '@solidjs/router';
+import cn from 'clsx';
+
+import intl from 'panel/common/intl';
+import { dnsConfigState, togglePrivatePtrResolvers } from 'panel/stores/dnsConfig';
+import { settingsState, testUpstreamWithFormValues } from 'panel/stores/settings';
+import { Button } from 'panel/common/ui/Button';
+import { SettingRow } from 'panel/common/ui/SettingRow';
+import { useDialog } from 'panel/hooks/useDialog';
+import { getUpstreamModeSummary, getUpstreamServersSummary, getTtlSummary } from '../helpers';
+import theme from 'panel/lib/theme';
+
+import { UpstreamModeDialog } from './blocks/UpstreamModeDialog';
+import { ServerAddressesDialog } from './blocks/ServerAddressesDialog';
+import { FallbackDnsDialog } from './blocks/FallbackDnsDialog';
+import { BootstrapDnsDialog } from './blocks/BootstrapDnsDialog';
+import { TimeoutDialog } from './blocks/TimeoutDialog';
+import { Paths } from 'panel/components/Routes/Paths';
+
+export const Upstream = () => {
+ const navigate = useNavigate();
+
+ const upstreamModeDialog = useDialog();
+ const serverAddressesDialog = useDialog();
+ const fallbackDialog = useDialog();
+ const bootstrapDialog = useDialog();
+ const timeoutDialog = useDialog();
+
+ const upstreamModeValue = createMemo(() =>
+ getUpstreamModeSummary(dnsConfigState.upstream_mode),
+ );
+ const serverAddressesValue = createMemo(() =>
+ getUpstreamServersSummary(dnsConfigState.upstream_dns, dnsConfigState.upstream_dns_file),
+ );
+ const fallbackServersValue = createMemo(() =>
+ getUpstreamServersSummary(dnsConfigState.fallback_dns),
+ );
+ const bootstrapServersValue = createMemo(() =>
+ getUpstreamServersSummary(dnsConfigState.bootstrap_dns),
+ );
+ const timeoutValue = createMemo(() => getTtlSummary(dnsConfigState.upstream_timeout));
+
+ const processing = () => dnsConfigState.processingSetConfig;
+
+ return (
+
+
+ {intl.getMessage('upstream_dns')}
+
+
+
+
+
+
+
+
+
+
+
+ {intl.getMessage('dns_private_reverse_resolvers_desc')}
+ {intl.getMessage('dns_private_reverse_resolvers_disabled_desc')}
+ >
+ }
+ checked={dnsConfigState.use_private_ptr_resolvers}
+ onChange={() => togglePrivatePtrResolvers()}
+ onClick={() => navigate(Paths.DnsPrivateReverse)}
+ divider
+ />
+
+
+
+
+
+ testUpstreamWithFormValues(
+ {
+ bootstrap_dns: dnsConfigState.bootstrap_dns,
+ upstream_dns: dnsConfigState.upstream_dns,
+ local_ptr_upstreams: dnsConfigState.local_ptr_upstreams,
+ fallback_dns: dnsConfigState.fallback_dns,
+ },
+ dnsConfigState.upstream_dns_file,
+ )
+ }
+ class={theme.form.actionButton}
+ compact
+ >
+ {intl.getMessage('dns_test_upstreams')}
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/DnsSettings/Upstream/blocks/BootstrapDnsDialog.tsx b/client_v2/src/components/DnsSettings/Upstream/blocks/BootstrapDnsDialog.tsx
new file mode 100644
index 00000000000..8130168698d
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/Upstream/blocks/BootstrapDnsDialog.tsx
@@ -0,0 +1,57 @@
+import { untrack, type Accessor } from 'solid-js';
+import { dnsConfigState, setDnsConfig } from 'panel/stores/dnsConfig';
+import intl from 'panel/common/intl';
+import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
+import { Textarea } from 'panel/common/controls/Textarea';
+import { validateUpstreams } from 'panel/helpers/validators';
+import { useField } from 'panel/hooks/useField';
+
+import theme from 'panel/lib/theme';
+
+type Props = {
+ open: Accessor;
+ onClose: () => void;
+ processing: boolean;
+};
+
+export const BootstrapDnsDialog = (props: Props) => {
+ const field = useField(
+ () => props.open(),
+ () => dnsConfigState.bootstrap_dns,
+ { validate: (v) => (v ? validateUpstreams(v) || '' : '') },
+ );
+
+ return (
+
+ {intl.getMessage('dns_bootstrap_dns_desc')}
+ {intl.getMessage('dns_bootstrap_dns_desc_2')}
+ >
+ }
+ onClose={props.onClose}
+ onSubmit={() => {
+ field.submitIfValid((v) => {
+ setDnsConfig({ bootstrap_dns: v });
+ untrack(() => props.onClose());
+ });
+ }}
+ processing={props.processing}
+ >
+
+
+
+ );
+};
diff --git a/client_v2/src/components/DnsSettings/Upstream/blocks/Examples/Examples.module.pcss b/client_v2/src/components/DnsSettings/Upstream/blocks/Examples/Examples.module.pcss
new file mode 100644
index 00000000000..4f171dbf7d9
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/Upstream/blocks/Examples/Examples.module.pcss
@@ -0,0 +1,40 @@
+.list {
+ margin: 0;
+ padding: 0;
+
+ code {
+ display: inline-block;
+ vertical-align: middle;
+ padding: 2px 4px;
+ background-color: var(--page-background-additional);
+ font-family: inherit;
+ line-height: var(--lh-t3-normal);
+ border: 1px solid var(--default-item-divider);
+ border-radius: 4px;
+ }
+
+ a {
+ color: var(--default-link);
+ transition: color var(--t2);
+ text-decoration: underline;
+
+ &:hover,
+ &:focus {
+ color: var(--hovered-link);
+ text-decoration: none;
+ }
+ }
+}
+
+.listItem {
+ position: relative;
+ padding: 8px 16px 8px 24px;
+ line-height: var(--lh-t1-normal);
+}
+
+.icon {
+ position: absolute;
+ top: 12px;
+ left: 0;
+ color: var(--default-black-icons);
+}
diff --git a/client_v2/src/components/DnsSettings/Upstream/blocks/Examples/Examples.tsx b/client_v2/src/components/DnsSettings/Upstream/blocks/Examples/Examples.tsx
new file mode 100644
index 00000000000..e7602f31ba4
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/Upstream/blocks/Examples/Examples.tsx
@@ -0,0 +1,150 @@
+import { For } from 'solid-js';
+import cn from 'clsx';
+
+import intl from 'panel/common/intl';
+import { COMMENT_LINE_DEFAULT_TOKEN } from 'panel/helpers/constants';
+import { Accordion } from 'panel/common/ui/Accordion';
+
+import theme from 'panel/lib/theme';
+import { Icon } from 'panel/common/ui/Icon';
+import s from './Examples.module.pcss';
+
+export const Examples = () => {
+ const examples = [
+ intl.getMessage('upstream_example_udp', {
+ ipv4: '94.140.14.140',
+ ipv6: '2a10:50c0::1:ff',
+ }),
+ intl.getMessage('upstream_example_udp_port', {
+ ipv4: '94.140.14.140:53',
+ ipv6: '[2a10:50c0::1:ff]:53',
+ }),
+ intl.getMessage('upstream_example_udp_hostname', {
+ value: 'udp://unfiltered.adguard-dns.com',
+ }),
+ intl.getMessage('upstream_example_tcp', {
+ ipv4: 'tcp://94.140.14.140',
+ ipv6: 'tcp://[2a10:50c0::1:ff]',
+ }),
+ intl.getMessage('upstream_example_tcp_port', {
+ ipv4: 'tcp://94.140.14.140:53',
+ ipv6: 'tcp://[2a10:50c0::1:ff]:53',
+ }),
+ intl.getMessage('upstream_example_tcp_hostname', {
+ value: 'tcp://unfiltered.adguard-dns.com',
+ }),
+ intl.getMessage('upstream_example_upstream_dot', {
+ value: 'tls://unfiltered.adguard-dns.com',
+ a: (text: string) => (
+
+ {text}
+
+ ),
+ }),
+ intl.getMessage('upstream_example_upstream_doh', {
+ value: 'https://unfiltered.adguard-dns.com/dns-query',
+ a: (text: string) => (
+
+ {text}
+
+ ),
+ }),
+ intl.getMessage('upstream_example_upstream_doh3', {
+ value: 'h3://unfiltered.adguard-dns.com/dns-query',
+ a: (text: string) => (
+
+ {text}
+
+ ),
+ }),
+ intl.getMessage('upstream_example_upstream_doq', {
+ value: 'quic://unfiltered.adguard-dns.com',
+ a: (text: string) => (
+
+ {text}
+
+ ),
+ }),
+ intl.getMessage('upstream_example_upstream_sdns', {
+ value: 'sdns://...',
+ a: (text: string) => (
+
+ {text}
+
+ ),
+ b: (text: string) => (
+
+ {text}
+
+ ),
+ c: (text: string) => (
+
+ {text}
+
+ ),
+ }),
+ intl.getMessage('upstream_example_upstream_reserved', {
+ value: '[/example.local/]94.140.14.140',
+ a: (text: string) => (
+
+ {text}
+
+ ),
+ }),
+ intl.getMessage('upstream_example_multiple_upstreams_reserved', {
+ value: '[/example.local/]94.140.14.140 2a10:50c0::1:ff',
+ a: (text: string) => (
+
+ {text}
+
+ ),
+ }),
+ intl.getMessage('upstream_example_upstream_comment', {
+ value: `${COMMENT_LINE_DEFAULT_TOKEN} comment`,
+ }),
+ ];
+
+ return (
+
+
+
+ {(example) => (
+
+
+
+ {example}
+
+ )}
+
+
+
+ );
+};
diff --git a/client_v2/src/components/DnsSettings/Upstream/blocks/Examples/index.ts b/client_v2/src/components/DnsSettings/Upstream/blocks/Examples/index.ts
new file mode 100644
index 00000000000..f98549303ad
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/Upstream/blocks/Examples/index.ts
@@ -0,0 +1 @@
+export { Examples } from './Examples';
diff --git a/client_v2/src/components/DnsSettings/Upstream/blocks/FallbackDnsDialog.tsx b/client_v2/src/components/DnsSettings/Upstream/blocks/FallbackDnsDialog.tsx
new file mode 100644
index 00000000000..f85c3c4ad30
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/Upstream/blocks/FallbackDnsDialog.tsx
@@ -0,0 +1,71 @@
+import { untrack, type Accessor } from 'solid-js';
+import { dnsConfigState, setDnsConfig } from 'panel/stores/dnsConfig';
+import intl from 'panel/common/intl';
+import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
+import { Textarea } from 'panel/common/controls/Textarea';
+import { validateUpstreams } from 'panel/helpers/validators';
+import { useField } from 'panel/hooks/useField';
+import { Examples } from './Examples';
+import theme from 'panel/lib/theme';
+
+type Props = {
+ open: Accessor;
+ onClose: () => void;
+ processing: boolean;
+};
+
+export const FallbackDnsDialog = (props: Props) => {
+ const field = useField(
+ () => props.open(),
+ () => dnsConfigState.fallback_dns,
+ { validate: (v) => (v ? validateUpstreams(v) || '' : '') },
+ );
+
+ return (
+
+ {intl.getMessage('dns_fallback_dns_desc')}
+
+ {intl.getMessage('dns_fallback_dns_desc_2', {
+ a: (text: string) => (
+
+ {text}
+
+ ),
+ })}
+
+ >
+ }
+ onClose={props.onClose}
+ onSubmit={() => {
+ field.submitIfValid((v) => {
+ setDnsConfig({ fallback_dns: v });
+ untrack(() => props.onClose());
+ });
+ }}
+ processing={props.processing}
+ >
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/DnsSettings/Upstream/blocks/PrivateReverseServersDialog.tsx b/client_v2/src/components/DnsSettings/Upstream/blocks/PrivateReverseServersDialog.tsx
new file mode 100644
index 00000000000..7372e66393a
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/Upstream/blocks/PrivateReverseServersDialog.tsx
@@ -0,0 +1,65 @@
+import { untrack, type Accessor } from 'solid-js';
+import { dnsConfigState, setDnsConfig } from 'panel/stores/dnsConfig';
+import intl from 'panel/common/intl';
+import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
+import { Textarea } from 'panel/common/controls/Textarea';
+import { validateUpstreams } from 'panel/helpers/validators';
+import { useField } from 'panel/hooks/useField';
+import { Examples } from './Examples';
+import theme from 'panel/lib/theme';
+
+type Props = {
+ open: Accessor;
+ onClose: () => void;
+ processing: boolean;
+};
+
+export const PrivateReverseServersDialog = (props: Props) => {
+ const field = useField(
+ () => props.open(),
+ () => dnsConfigState.local_ptr_upstreams,
+ { validate: (v) => (v ? validateUpstreams(v) || '' : '') },
+ );
+
+ return (
+
+ {intl.getMessage('dns_private_reverse_servers_desc')}
+ {dnsConfigState.default_local_ptr_upstreams.length >= 2 && (
+
+ {intl.getMessage('dns_private_reverse_servers_desc_2', {
+ value_1: dnsConfigState.default_local_ptr_upstreams[0],
+ value_2: dnsConfigState.default_local_ptr_upstreams[1],
+ })}
+
+ )}
+ >
+ }
+ onClose={props.onClose}
+ onSubmit={() => {
+ field.submitIfValid((v) => {
+ setDnsConfig({ local_ptr_upstreams: v });
+ untrack(() => props.onClose());
+ });
+ }}
+ processing={props.processing}
+ >
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/DnsSettings/Upstream/blocks/ServerAddressesDialog.tsx b/client_v2/src/components/DnsSettings/Upstream/blocks/ServerAddressesDialog.tsx
new file mode 100644
index 00000000000..076e956cb1c
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/Upstream/blocks/ServerAddressesDialog.tsx
@@ -0,0 +1,91 @@
+import { untrack, type Accessor, Show } from 'solid-js';
+import { dnsConfigState, setDnsConfig } from 'panel/stores/dnsConfig';
+import intl from 'panel/common/intl';
+import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
+import { Textarea } from 'panel/common/controls/Textarea';
+import { validateUpstreams } from 'panel/helpers/validators';
+import { useField } from 'panel/hooks/useField';
+import { Examples } from './Examples';
+import { ServerAddressesFileDialog } from './ServerAddressesFileDialog';
+import theme from 'panel/lib/theme';
+
+type Props = {
+ open: Accessor;
+ onClose: () => void;
+ processing: boolean;
+};
+
+export const ServerAddressesDialog = (props: Props) => {
+ const hasFile = () => !!dnsConfigState.upstream_dns_file;
+
+ const field = useField(
+ () => props.open(),
+ () => dnsConfigState.upstream_dns,
+ { validate: (v) => (v ? validateUpstreams(v) || '' : '') },
+ );
+
+ return (
+ }
+ >
+
+ {intl.getMessage('dns_server_addresses_desc')}
+
+ {intl.getMessage('dns_server_addresses_desc_2', {
+ a: (text: string) => (
+
+ {text}
+
+ ),
+ b: (text: string) => (
+
+ {text}
+
+ ),
+ })}
+
+ >
+ }
+ onClose={props.onClose}
+ onSubmit={() => {
+ field.submitIfValid((v) => {
+ setDnsConfig({ upstream_dns: v });
+ untrack(() => props.onClose());
+ });
+ }}
+ processing={props.processing}
+ >
+
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/DnsSettings/Upstream/blocks/ServerAddressesFileDialog.tsx b/client_v2/src/components/DnsSettings/Upstream/blocks/ServerAddressesFileDialog.tsx
new file mode 100644
index 00000000000..1e487758838
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/Upstream/blocks/ServerAddressesFileDialog.tsx
@@ -0,0 +1,27 @@
+import { type Accessor } from 'solid-js';
+import { dnsConfigState } from 'panel/stores/dnsConfig';
+import intl from 'panel/common/intl';
+import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
+import theme from 'panel/lib/theme';
+
+type Props = {
+ open: Accessor;
+ onClose: () => void;
+};
+
+export const ServerAddressesFileDialog = (props: Props) => (
+
+ {intl.getMessage('dns_server_addresses_configured_in_file', {
+ path: dnsConfigState.upstream_dns_file,
+ })}
+
+ }
+ onClose={props.onClose}
+ onSubmit={props.onClose}
+ buttonText={intl.getMessage('close')}
+ />
+);
diff --git a/client_v2/src/components/DnsSettings/Upstream/blocks/TimeoutDialog.tsx b/client_v2/src/components/DnsSettings/Upstream/blocks/TimeoutDialog.tsx
new file mode 100644
index 00000000000..761247167a0
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/Upstream/blocks/TimeoutDialog.tsx
@@ -0,0 +1,66 @@
+import { untrack, type Accessor } from 'solid-js';
+import { dnsConfigState, setDnsConfig } from 'panel/stores/dnsConfig';
+import intl from 'panel/common/intl';
+import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
+import { Input } from 'panel/common/controls/Input';
+import { UPSTREAM_TIMEOUT } from 'panel/helpers/constants';
+import { validateBetween } from 'panel/helpers/validators';
+import { useField } from 'panel/hooks/useField';
+import theme from 'panel/lib/theme';
+
+type Props = {
+ open: Accessor;
+ onClose: () => void;
+ processing: boolean;
+};
+
+export const TimeoutDialog = (props: Props) => {
+ const field = useField(
+ () => props.open(),
+ () => dnsConfigState.upstream_timeout,
+ {
+ validate: (v) =>
+ (Number.isNaN(v)
+ ? intl.getMessage('form_error_required')
+ : validateBetween(v, UPSTREAM_TIMEOUT.MIN, UPSTREAM_TIMEOUT.MAX)) || '',
+ },
+ );
+
+ return (
+ {
+ field.submitIfValid((v) => {
+ setDnsConfig({ upstream_timeout: v });
+ untrack(() => props.onClose());
+ });
+ }}
+ processing={props.processing}
+ >
+
+
+ field.setValue(
+ (e.target as HTMLInputElement).value === ''
+ ? NaN
+ : Number((e.target as HTMLInputElement).value),
+ )
+ }
+ onBlur={() => field.validate()}
+ id="upstream_timeout"
+ label={intl.getMessage('upstream_timeout')}
+ placeholder={intl.getMessage('dns_upstream_timeout_placeholder')}
+ min={UPSTREAM_TIMEOUT.MIN}
+ max={UPSTREAM_TIMEOUT.MAX}
+ errorMessage={field.error()}
+ size="large"
+ />
+
+
+ );
+};
diff --git a/client_v2/src/components/DnsSettings/Upstream/blocks/UpstreamModeDialog.module.pcss b/client_v2/src/components/DnsSettings/Upstream/blocks/UpstreamModeDialog.module.pcss
new file mode 100644
index 00000000000..0c8ff9b0c53
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/Upstream/blocks/UpstreamModeDialog.module.pcss
@@ -0,0 +1,4 @@
+.warning {
+ margin-top: 8px;
+ color: var(--default-attention-link);
+}
diff --git a/client_v2/src/components/DnsSettings/Upstream/blocks/UpstreamModeDialog.tsx b/client_v2/src/components/DnsSettings/Upstream/blocks/UpstreamModeDialog.tsx
new file mode 100644
index 00000000000..d0a3d8e4f4e
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/Upstream/blocks/UpstreamModeDialog.tsx
@@ -0,0 +1,63 @@
+import { createSignal, createEffect, createMemo, type Accessor } from 'solid-js';
+import cn from 'clsx';
+import { dnsConfigState, setDnsConfig } from 'panel/stores/dnsConfig';
+import intl from 'panel/common/intl';
+import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
+import { Radio } from 'panel/common/controls/Radio';
+import { getUpstreamModeOptions } from '../../helpers';
+import theme from 'panel/lib/theme';
+
+import s from './UpstreamModeDialog.module.pcss';
+
+type Props = {
+ open: Accessor;
+ onClose: () => void;
+ processing: boolean;
+};
+
+export const UpstreamModeDialog = (props: Props) => {
+ const [upstreamMode, setUpstreamMode] = createSignal(dnsConfigState.upstream_mode);
+
+ createEffect(() => {
+ if (props.open()) {
+ setUpstreamMode(dnsConfigState.upstream_mode);
+ }
+ });
+
+ const upstreamModeOptions = createMemo(() => {
+ return getUpstreamModeOptions().map((opt) => ({
+ text: opt.text,
+ value: opt.value,
+ description: opt.warning ? (
+ <>
+ {opt.description}
+ {opt.warning}
+ >
+ ) : (
+ opt.description
+ ),
+ }));
+ });
+
+ return (
+ {
+ setDnsConfig({ upstream_mode: upstreamMode() });
+ props.onClose();
+ }}
+ processing={props.processing}
+ >
+ setUpstreamMode(v)}
+ inModal
+ />
+
+ );
+};
diff --git a/client_v2/src/components/DnsSettings/Upstream/index.ts b/client_v2/src/components/DnsSettings/Upstream/index.ts
new file mode 100644
index 00000000000..42166af491d
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/Upstream/index.ts
@@ -0,0 +1 @@
+export { Upstream } from './Upstream';
diff --git a/client_v2/src/components/DnsSettings/helpers.ts b/client_v2/src/components/DnsSettings/helpers.ts
new file mode 100644
index 00000000000..e75efaf7b5b
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/helpers.ts
@@ -0,0 +1,138 @@
+import { DNS_REQUEST_OPTIONS, BLOCKING_MODES, EDNS_MODES } from 'panel/helpers/constants';
+import intl from 'panel/common/intl';
+
+export const getUpstreamModeSummary = (mode: string): string => {
+ switch (mode) {
+ case DNS_REQUEST_OPTIONS.PARALLEL:
+ return intl.getMessage('upstream_dns_parallel_requests');
+ case DNS_REQUEST_OPTIONS.FASTEST_ADDR:
+ return intl.getMessage('upstream_dns_fastest_addr');
+ case DNS_REQUEST_OPTIONS.LOAD_BALANCING:
+ default:
+ return intl.getMessage('upstream_dns_load_balancing');
+ }
+};
+
+export const getUpstreamServersSummary = (servers: string, upstreamDnsFile?: string): string => {
+ if (upstreamDnsFile) {
+ return intl.getMessage('upstream_dns_configured_in_file', { path: upstreamDnsFile });
+ }
+ if (!servers) return '';
+ const lines = servers.split('\n').filter(Boolean);
+ return lines.join(', ');
+};
+
+export const getRateLimitSummary = (ratelimit: number): string => {
+ if (ratelimit === 0) return intl.getMessage('dns_rate_limit_no_limit');
+ return intl.getMessage('dns_rate_limit_value', { value: ratelimit });
+};
+
+export const getBlockingModeSummary = (mode: string): string => {
+ switch (mode) {
+ case BLOCKING_MODES.refused:
+ return 'REFUSED';
+ case BLOCKING_MODES.nxdomain:
+ return 'NXDOMAIN';
+ case BLOCKING_MODES.null_ip:
+ return intl.getMessage('dns_blocking_mode_null_ip');
+ case BLOCKING_MODES.custom_ip:
+ return intl.getMessage('dns_blocking_mode_custom_ip');
+ default:
+ return intl.getMessage('dns_blocking_mode_default');
+ }
+};
+
+export const getCacheSizeSummary = (bytes?: number): string => {
+ if (!bytes && bytes !== 0) return '';
+ if (bytes < 1024) return `${bytes} bytes`;
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
+ return `${Number((bytes / (1024 * 1024)).toFixed(1))} MB`;
+};
+
+export const getTtlSummary = (seconds?: number): string => {
+ if (seconds === undefined || seconds === null) return '';
+ return intl.getMessage('dns_ttl_value', { value: seconds });
+};
+
+export const getListSummary = (list: string): string => {
+ if (!list || !list.trim()) return '';
+ return list.split('\n').filter(Boolean).join(', ');
+};
+
+/**
+ * Returns blocking mode options with i18n text.
+ * Factory function — re-executes on each render for locale correctness.
+ */
+export const getBlockingModeOptions = () => {
+ return [
+ {
+ text: intl.getMessage('dns_blocking_mode_default'),
+ value: BLOCKING_MODES.default,
+ description: intl.getMessage('dns_blocking_mode_default_desc'),
+ },
+ {
+ text: intl.getMessage('dns_blocking_mode_refused'),
+ value: BLOCKING_MODES.refused,
+ description: intl.getMessage('dns_blocking_mode_refused_desc'),
+ },
+ {
+ text: intl.getMessage('dns_blocking_mode_nxdomain'),
+ value: BLOCKING_MODES.nxdomain,
+ description: intl.getMessage('dns_blocking_mode_nxdomain_desc'),
+ },
+ {
+ text: intl.getMessage('dns_blocking_mode_null_ip'),
+ value: BLOCKING_MODES.null_ip,
+ description: intl.getMessage('dns_blocking_mode_null_ip_desc'),
+ },
+ {
+ text: intl.getMessage('dns_blocking_mode_custom_ip'),
+ value: BLOCKING_MODES.custom_ip,
+ description: intl.getMessage('dns_blocking_mode_custom_ip_desc'),
+ },
+ ];
+};
+
+/**
+ * Returns EDNS mode options with i18n text.
+ * Factory function — re-executes on each render for locale correctness.
+ */
+export const getEdnsOptions = () => {
+ return [
+ {
+ text: intl.getMessage('dns_edns_option_default'),
+ value: EDNS_MODES.default,
+ },
+ {
+ text: intl.getMessage('dns_edns_option_custom'),
+ value: EDNS_MODES.custom,
+ },
+ ];
+};
+
+/**
+ * Returns upstream mode options with i18n text.
+ * Factory function — re-executes on each call for locale correctness.
+ * When used inside `createMemo`, tracks the locale signal and updates
+ * option labels on language change.
+ */
+export const getUpstreamModeOptions = () => {
+ return [
+ {
+ text: intl.getMessage('upstream_dns_load_balancing'),
+ value: DNS_REQUEST_OPTIONS.LOAD_BALANCING,
+ description: intl.getMessage('upstream_dns_load_balancing_desc'),
+ },
+ {
+ text: intl.getMessage('upstream_dns_parallel_requests'),
+ value: DNS_REQUEST_OPTIONS.PARALLEL,
+ description: intl.getMessage('upstream_dns_parallel_requests_desc'),
+ },
+ {
+ text: intl.getMessage('upstream_dns_fastest_addr'),
+ value: DNS_REQUEST_OPTIONS.FASTEST_ADDR,
+ description: intl.getMessage('upstream_dns_fastest_addr_desc'),
+ warning: intl.getMessage('upstream_dns_fastest_addr_warning'),
+ },
+ ];
+};
diff --git a/client_v2/src/components/DnsSettings/index.ts b/client_v2/src/components/DnsSettings/index.ts
new file mode 100644
index 00000000000..3bee2afe24a
--- /dev/null
+++ b/client_v2/src/components/DnsSettings/index.ts
@@ -0,0 +1 @@
+export { DnsSettings } from './DnsSettings';
diff --git a/client_v2/src/components/Encryption/Encryption.tsx b/client_v2/src/components/Encryption/Encryption.tsx
new file mode 100644
index 00000000000..56996559fc3
--- /dev/null
+++ b/client_v2/src/components/Encryption/Encryption.tsx
@@ -0,0 +1,244 @@
+import { createSignal, createEffect, Show, onMount, onCleanup } from 'solid-js';
+import cn from 'clsx';
+
+import { SettingRow } from 'panel/common/ui/SettingRow';
+import { Dropdown } from 'panel/common/ui/Dropdown';
+import { Icon } from 'panel/common/ui/Icon';
+import { PageLoader } from 'panel/common/ui/Loader';
+import { PlusButton } from 'panel/common/ui/PlusButton';
+import intl from 'panel/common/intl';
+import theme from 'panel/lib/theme';
+import { getTlsStatus, encryptionState, setTlsConfig } from 'panel/stores/encryption';
+import { ENCRYPTION_SOURCE } from 'panel/helpers/constants';
+
+import { createDebouncedValidator } from './blocks/helpers';
+import { PlainDnsToggle } from './blocks/PlainDnsToggle';
+import { TlsCertSection } from './blocks/TlsCertSection';
+import { ServerSettingsRow } from './blocks/ServerSettingsRow';
+import { RedirectToggle } from './blocks/RedirectToggle';
+import { ResetDnsModal } from './blocks/ResetDnsModal';
+import { ServerSettingsModal } from './blocks/ServerSettingsModal';
+import { AddTlsCertModal } from './blocks/AddTlsCert';
+import s from './styles.module.pcss';
+
+export const Encryption = () => {
+ const [resetOpen, setResetOpen] = createSignal(false);
+ const [serverSettingsOpen, setServerSettingsOpen] = createSignal(false);
+ const [addCertOpen, setAddCertOpen] = createSignal(false);
+ const [menuOpen, setMenuOpen] = createSignal(false);
+
+ const [tlsStatusLoaded, setTlsStatusLoaded] = createSignal(false);
+
+ /**
+ * Shadows encryptionState.enabled with {@code equals: false} so we can
+ * force a DOM re-sync even when the value is unchanged (e.g. reverting
+ * after a modal opens without saving). Synced from store only when
+ * {@code processingConfig} is false to avoid flashing during async save.
+ */
+ const [encryptionEnabled, setEncryptionEnabled] = createSignal(false, {
+ equals: false,
+ });
+
+ createEffect(() => {
+ if (!encryptionState.processingConfig) {
+ setEncryptionEnabled(encryptionState.enabled);
+ }
+ });
+
+ const [validateConfig, cancelValidation] = createDebouncedValidator();
+
+ onMount(async () => {
+ await getTlsStatus();
+ setTlsStatusLoaded(true);
+ });
+
+ onCleanup(() => {
+ cancelValidation();
+ });
+
+ const certConfigured = () =>
+ !!(encryptionState.certificate_chain || encryptionState.certificate_path);
+
+ const handleEncryptedDnsChange = (checked: boolean) => {
+ if (!checked) {
+ setEncryptionEnabled(false);
+ setTlsConfig(
+ {
+ enabled: false,
+ serve_plain_dns: true,
+ },
+ { silent: true },
+ );
+ return;
+ }
+
+ // Enabling: check if everything is configured before saving.
+ const hasCert = !!(encryptionState.certificate_chain || encryptionState.certificate_path);
+ const hasKey = !!(
+ encryptionState.private_key ||
+ encryptionState.private_key_path ||
+ encryptionState.private_key_saved
+ );
+ const hasServerName = !!encryptionState.server_name;
+
+ // Everything is set up — save the change.
+ // Native input already shows ON from the click; sync effect
+ // confirms on success or reverts on failure.
+ if (hasCert && hasKey && hasServerName) {
+ setTlsConfig(
+ {
+ enabled: true,
+ },
+ { silent: true },
+ );
+ return;
+ }
+
+ // Not saving — force DOM back to unchecked so the switch
+ // doesn't appear ON while encryption is actually OFF.
+ setEncryptionEnabled(false);
+
+ // Certificate or key is missing — open the TLS cert wizard (don't save yet).
+ if (!hasCert || !hasKey) {
+ setAddCertOpen(true);
+ return;
+ }
+
+ // Cert and key are present, but server name isn't set — open server settings.
+ if (!hasServerName) {
+ setServerSettingsOpen(true);
+ }
+ };
+
+ /**
+ * Centralised debounced validation trigger.
+ * Watches the encryption state and fires debounced backend validation
+ * whenever encryption is enabled and cert/key values are present.
+ * Replaces the createEffect that was previously inside Form.tsx.
+ */
+ createEffect(() => {
+ if (!tlsStatusLoaded()) return;
+ if (!encryptionState.enabled) return;
+ const hasCert = !!(encryptionState.certificate_chain || encryptionState.certificate_path);
+ const hasKey = !!(
+ encryptionState.private_key ||
+ encryptionState.private_key_path ||
+ encryptionState.private_key_saved
+ );
+ if (!hasCert || !hasKey) return;
+
+ validateConfig({
+ enabled: encryptionState.enabled,
+ serve_plain_dns: encryptionState.serve_plain_dns,
+ server_name: encryptionState.server_name,
+ force_https: encryptionState.force_https,
+ port_https: encryptionState.port_https,
+ port_dns_over_tls: encryptionState.port_dns_over_tls,
+ port_dns_over_quic: encryptionState.port_dns_over_quic,
+ certificate_chain: encryptionState.certificate_chain,
+ private_key: encryptionState.private_key,
+ certificate_path: encryptionState.certificate_path,
+ private_key_path: encryptionState.private_key_path,
+ certificate_source: encryptionState.certificate_chain
+ ? ENCRYPTION_SOURCE.CONTENT
+ : ENCRYPTION_SOURCE.PATH,
+ key_source:
+ encryptionState.private_key || encryptionState.private_key_saved
+ ? ENCRYPTION_SOURCE.CONTENT
+ : ENCRYPTION_SOURCE.PATH,
+ private_key_saved: encryptionState.private_key_saved,
+ });
+ });
+
+ const handleResetClick = () => {
+ setMenuOpen(false);
+ setResetOpen(true);
+ };
+
+ const resetMenu = (
+
+ {intl.getMessage('reset_dns_protocols')}
+
+ );
+
+ return (
+
+
+
+
+
}>
+
+
+
+ {intl.getMessage('encryption_title')}
+
+
+
+
+
+
+
+
+
+
+
+
+
setServerSettingsOpen(true)} />
+
+
+
+
+
+
setResetOpen(false)} />
+
+ setServerSettingsOpen(false)}
+ />
+
+ setAddCertOpen(false)} />
+
+ );
+};
diff --git a/client_v2/src/components/Encryption/Status/CertificateStatus.tsx b/client_v2/src/components/Encryption/Status/CertificateStatus.tsx
new file mode 100644
index 00000000000..1ae81d93cc6
--- /dev/null
+++ b/client_v2/src/components/Encryption/Status/CertificateStatus.tsx
@@ -0,0 +1,57 @@
+import { Show } from 'solid-js';
+import { format } from 'date-fns';
+
+import { EMPTY_DATE } from 'panel/helpers/constants';
+import intl from 'panel/common/intl';
+import { StatusBlock } from './StatusBlock';
+import s from './styles.module.pcss';
+
+type Props = {
+ validChain: boolean;
+ validCert: boolean;
+ subject?: string;
+ issuer?: string;
+ notAfter?: string;
+ dnsNames?: string[];
+};
+
+export const CertificateStatus = (props: Props) => (
+
+
+
+
+ {intl.getMessage('encryption_subject', { value: props.subject })}
+
+
+ {intl.getMessage('encryption_issuer', { value: props.issuer })}
+
+
+
+ {intl.getMessage('encryption_expire', {
+ value: format(props.notAfter!, 'yyyy-MM-dd HH:mm:ss'),
+ })}
+
+
+
+
+ {intl.getMessage('encryption_hostnames', {
+ value: props.dnsNames!.join(', '),
+ })}
+
+
+
+
+
+);
diff --git a/client_v2/src/components/Encryption/Status/KeyStatus.tsx b/client_v2/src/components/Encryption/Status/KeyStatus.tsx
new file mode 100644
index 00000000000..e9258b4a3b2
--- /dev/null
+++ b/client_v2/src/components/Encryption/Status/KeyStatus.tsx
@@ -0,0 +1,27 @@
+import { Show } from 'solid-js';
+import intl from 'panel/common/intl';
+import { StatusBlock } from './StatusBlock';
+
+import s from './styles.module.pcss';
+
+type Props = {
+ validKey: boolean;
+ keyType?: string;
+};
+
+export const KeyStatus = (props: Props) => (
+
+
+
+ {intl.getMessage('encryption_key_type', { value: props.keyType })}
+
+
+
+);
diff --git a/client_v2/src/components/Encryption/Status/StatusBlock.tsx b/client_v2/src/components/Encryption/Status/StatusBlock.tsx
new file mode 100644
index 00000000000..d0c58bf6602
--- /dev/null
+++ b/client_v2/src/components/Encryption/Status/StatusBlock.tsx
@@ -0,0 +1,29 @@
+import { type JSX } from 'solid-js';
+import cn from 'clsx';
+
+import theme from 'panel/lib/theme';
+
+import s from './styles.module.pcss';
+
+export type StatusVariant = 'success' | 'error' | 'warning';
+
+type Props = {
+ variant: StatusVariant;
+ title: JSX.Element;
+ children?: JSX.Element;
+};
+
+export const StatusBlock = (props: Props) => (
+
+
+ {props.title}
+
+ {props.children}
+
+);
diff --git a/client_v2/src/components/Encryption/Status/ValidationStatus.tsx b/client_v2/src/components/Encryption/Status/ValidationStatus.tsx
new file mode 100644
index 00000000000..4b31d4317f9
--- /dev/null
+++ b/client_v2/src/components/Encryption/Status/ValidationStatus.tsx
@@ -0,0 +1,15 @@
+import intl from 'panel/common/intl';
+import { StatusBlock } from './StatusBlock';
+
+import s from './styles.module.pcss';
+
+type Props = {
+ type: 'warning' | 'error';
+ message: string;
+};
+
+export const ValidationStatus = (props: Props) => (
+
+ {props.message}
+
+);
diff --git a/client_v2/src/components/Encryption/Status/index.ts b/client_v2/src/components/Encryption/Status/index.ts
new file mode 100644
index 00000000000..d8565be8adb
--- /dev/null
+++ b/client_v2/src/components/Encryption/Status/index.ts
@@ -0,0 +1,3 @@
+export { CertificateStatus } from './CertificateStatus';
+export { KeyStatus } from './KeyStatus';
+export { ValidationStatus } from './ValidationStatus';
diff --git a/client_v2/src/components/Encryption/Status/styles.module.pcss b/client_v2/src/components/Encryption/Status/styles.module.pcss
new file mode 100644
index 00000000000..83ede68e099
--- /dev/null
+++ b/client_v2/src/components/Encryption/Status/styles.module.pcss
@@ -0,0 +1,26 @@
+.status {
+ color: var(--default-main-text);
+}
+
+.statusTitle {
+ &_success {
+ color: var(--default-link);
+ }
+
+ &_warning {
+ color: var(--default-attention-link);
+ }
+
+ &_error {
+ color: var(--default-error-link);
+ }
+}
+
+.statusList {
+ margin: 0;
+ padding: 0;
+
+ li {
+ display: block;
+ }
+}
diff --git a/client_v2/src/components/Encryption/blocks/AddTlsCert/AddTlsCertModal.tsx b/client_v2/src/components/Encryption/blocks/AddTlsCert/AddTlsCertModal.tsx
new file mode 100644
index 00000000000..649e12c8986
--- /dev/null
+++ b/client_v2/src/components/Encryption/blocks/AddTlsCert/AddTlsCertModal.tsx
@@ -0,0 +1,342 @@
+import { createEffect, createSignal, on, Show } from 'solid-js';
+import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
+import { Button } from 'panel/common/ui/Button';
+import { Input } from 'panel/common/controls/Input';
+import { Radio } from 'panel/common/controls/Radio';
+import { Textarea } from 'panel/common/controls/Textarea';
+import intl from 'panel/common/intl';
+import { ENCRYPTION_SOURCE } from 'panel/helpers/constants';
+import { encryptionState, setTlsConfig, resetValidationStatus } from 'panel/stores/encryption';
+import {
+ validateCertFields,
+ validateCertKeyFields,
+ type EncryptionFormValues,
+} from '../../validate';
+import { getSubmitValues } from '../helpers';
+import { FileBrowseButton } from './FileBrowseButton';
+import s from './styles.module.pcss';
+import theme from 'panel/lib/theme';
+
+type FieldName = 'certificate_chain' | 'certificate_path' | 'private_key' | 'private_key_path';
+
+type Props = {
+ open: boolean;
+ onClose: () => void;
+};
+
+export const AddTlsCertModal = (props: Props) => {
+ const [step, setStep] = createSignal(1);
+ const [certSource, setCertSource] = createSignal(ENCRYPTION_SOURCE.PATH);
+ const [certChain, setCertChain] = createSignal('');
+ const [certPath, setCertPath] = createSignal('');
+ const [keySource, setKeySource] = createSignal(ENCRYPTION_SOURCE.PATH);
+ const [privateKey, setPrivateKey] = createSignal('');
+ const [privateKeyPath, setPrivateKeyPath] = createSignal('');
+ const [privateKeySaved, setPrivateKeySaved] = createSignal(false);
+ const [errors, setErrors] = createSignal>>({});
+
+ createEffect(
+ on(
+ () => props.open,
+ (open) => {
+ if (open) {
+ setStep(1);
+ setCertSource(ENCRYPTION_SOURCE.PATH);
+ setCertChain('');
+ setCertPath('');
+ setKeySource(ENCRYPTION_SOURCE.PATH);
+ setPrivateKey('');
+ setPrivateKeyPath('');
+ setPrivateKeySaved(false);
+ setErrors({});
+ }
+ },
+ ),
+ );
+
+ const clearError = (field: FieldName) => {
+ setErrors((prev) => {
+ if (!(field in prev)) return prev;
+ const next = { ...prev };
+ delete next[field];
+ return next;
+ });
+ };
+
+ const error = (field: FieldName) => errors()[field];
+
+ const buildValues = (): EncryptionFormValues => ({
+ enabled: encryptionState.enabled,
+ serve_plain_dns: encryptionState.serve_plain_dns,
+ server_name: encryptionState.server_name,
+ port_https: encryptionState.port_https || 0,
+ port_dns_over_tls: encryptionState.port_dns_over_tls || 0,
+ port_dns_over_quic: encryptionState.port_dns_over_quic || 0,
+ certificate_chain: certChain(),
+ private_key: privateKey(),
+ certificate_path: certPath(),
+ private_key_path: privateKeyPath(),
+ certificate_source: certSource(),
+ key_source: keySource(),
+ private_key_saved: privateKeySaved(),
+ });
+
+ const certSourceOptions = [
+ {
+ text: intl.getMessage('tls_cert_path_option'),
+ value: ENCRYPTION_SOURCE.PATH,
+ },
+ {
+ text: intl.getMessage('encryption_certificates_source_content'),
+ value: ENCRYPTION_SOURCE.CONTENT,
+ },
+ ];
+
+ const keySourceOptions = () => [
+ {
+ text: intl.getMessage('tls_key_path_option'),
+ value: ENCRYPTION_SOURCE.PATH,
+ },
+ {
+ text: intl.getMessage('encryption_key_source_content'),
+ value: ENCRYPTION_SOURCE.CONTENT,
+ },
+ {
+ text: intl.getMessage('use_saved_key'),
+ value: ENCRYPTION_SOURCE.SAVED,
+ disabled: !encryptionState.private_key_saved,
+ },
+ ];
+
+ const handleNext = () => {
+ const values = buildValues();
+ const errs = validateCertFields(values);
+ if (Object.values(errs).some(Boolean)) {
+ setErrors(errs);
+ return;
+ }
+ setStep(2);
+ };
+
+ const handleAdd = () => {
+ const values = buildValues();
+ const errs = validateCertKeyFields(values);
+ if (Object.values(errs).some(Boolean)) {
+ setErrors(errs);
+ return;
+ }
+ setTlsConfig(getSubmitValues(values));
+ props.onClose();
+ };
+
+ const handleBack = () => {
+ setStep(1);
+ setErrors({});
+ };
+
+ const processing = () => encryptionState.processingConfig || encryptionState.processingValidate;
+ const hasErrors = () => Object.values(errors()).some(Boolean);
+
+ const isCertStep = () => step() === 1;
+ const isKeyStep = () => step() === 2;
+
+ const handleCertFileSelect = (content: string) => {
+ setCertChain(content);
+ setCertSource(ENCRYPTION_SOURCE.CONTENT);
+ clearError('certificate_chain');
+ clearError('certificate_path');
+ };
+
+ const handleKeyFileSelect = (content: string) => {
+ setPrivateKey(content);
+ setKeySource(ENCRYPTION_SOURCE.CONTENT);
+ setPrivateKeySaved(false);
+ clearError('private_key');
+ clearError('private_key_path');
+ };
+
+ const handleCertSourceChange = (v: string) => {
+ setCertSource(v);
+ clearError('certificate_chain');
+ clearError('certificate_path');
+ };
+
+ const handleCertPathChange = (e: Event) => {
+ setCertPath((e.target as HTMLInputElement).value);
+ clearError('certificate_path');
+ resetValidationStatus();
+ };
+
+ const validateCertOnBlur = () => {
+ const values = buildValues();
+ const errs = validateCertFields(values);
+ setErrors((prev) => ({ ...prev, ...errs }));
+ };
+
+ const handleCertChainChange = (e: Event) => {
+ setCertChain((e.target as HTMLTextAreaElement).value);
+ clearError('certificate_chain');
+ resetValidationStatus();
+ };
+
+ const handleKeySourceChange = (v: string) => {
+ setKeySource(v);
+ if (v === ENCRYPTION_SOURCE.SAVED) {
+ setPrivateKey('');
+ setPrivateKeySaved(true);
+ } else {
+ setPrivateKeySaved(false);
+ }
+ clearError('private_key');
+ clearError('private_key_path');
+ resetValidationStatus();
+ };
+
+ const handleKeyPathChange = (e: Event) => {
+ setPrivateKeyPath((e.target as HTMLInputElement).value);
+ clearError('private_key_path');
+ resetValidationStatus();
+ };
+
+ const validateKeyOnBlur = () => {
+ const values = buildValues();
+ const errs = validateCertKeyFields(values);
+ setErrors((prev) => ({ ...prev, ...errs }));
+ };
+
+ const handleKeyChange = (e: Event) => {
+ setPrivateKey((e.target as HTMLTextAreaElement).value);
+ clearError('private_key');
+ resetValidationStatus();
+ };
+
+ const CertStepFooter = (
+
+ );
+
+ const KeyStepFooter = (
+
+ );
+
+ return (
+
+
+
+
+
+
+ }
+ size="large"
+ />
+
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ size="large"
+ />
+
+ }
+ >
+
+
+
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/Encryption/blocks/AddTlsCert/FileBrowseButton.tsx b/client_v2/src/components/Encryption/blocks/AddTlsCert/FileBrowseButton.tsx
new file mode 100644
index 00000000000..5d4b00882e2
--- /dev/null
+++ b/client_v2/src/components/Encryption/blocks/AddTlsCert/FileBrowseButton.tsx
@@ -0,0 +1,45 @@
+import intl from 'panel/common/intl';
+import s from './styles.module.pcss';
+
+type Props = {
+ onFileSelect: (content: string) => void;
+};
+
+export const FileBrowseButton = (props: Props) => {
+ let fileInputRef: HTMLInputElement | undefined;
+
+ const handleClick = () => {
+ fileInputRef?.click();
+ };
+
+ const handleFileChange = (e: Event) => {
+ const input = e.currentTarget as HTMLInputElement;
+ const file = input.files?.[0];
+ if (!file) return;
+
+ const reader = new FileReader();
+ reader.onload = () => {
+ const content = reader.result as string;
+ props.onFileSelect(content);
+ };
+ reader.readAsText(file);
+
+ // Reset so the same file can be selected again.
+ input.value = '';
+ };
+
+ return (
+ <>
+
+ {intl.getMessage('browse')}
+
+ (fileInputRef = el)}
+ type="file"
+ class={s.hiddenFileInput}
+ onChange={handleFileChange}
+ aria-hidden="true"
+ />
+ >
+ );
+};
diff --git a/client_v2/src/components/Encryption/blocks/AddTlsCert/index.ts b/client_v2/src/components/Encryption/blocks/AddTlsCert/index.ts
new file mode 100644
index 00000000000..65bd570f542
--- /dev/null
+++ b/client_v2/src/components/Encryption/blocks/AddTlsCert/index.ts
@@ -0,0 +1 @@
+export { AddTlsCertModal } from './AddTlsCertModal';
diff --git a/client_v2/src/components/Encryption/blocks/AddTlsCert/styles.module.pcss b/client_v2/src/components/Encryption/blocks/AddTlsCert/styles.module.pcss
new file mode 100644
index 00000000000..54df07618bc
--- /dev/null
+++ b/client_v2/src/components/Encryption/blocks/AddTlsCert/styles.module.pcss
@@ -0,0 +1,40 @@
+.browseLink {
+ color: var(--default-link);
+ cursor: pointer;
+ margin-left: auto;
+ border: none;
+ background: transparent;
+ font-size: var(--fs-text-t2);
+ line-height: var(--lh-t2-normal);
+}
+
+.footer {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 16px;
+ width: 100%;
+
+ & button {
+ width: 100%;
+ }
+
+ @media (min-width: 768px) {
+ flex-wrap: nowrap;
+
+ & button {
+ width: auto;
+ }
+ }
+}
+
+.hiddenFileInput {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
diff --git a/client_v2/src/components/Encryption/blocks/PlainDnsToggle.tsx b/client_v2/src/components/Encryption/blocks/PlainDnsToggle.tsx
new file mode 100644
index 00000000000..468aba895c9
--- /dev/null
+++ b/client_v2/src/components/Encryption/blocks/PlainDnsToggle.tsx
@@ -0,0 +1,69 @@
+import { createSignal, createEffect, Show } from 'solid-js';
+import { SettingRow } from 'panel/common/ui/SettingRow';
+import { ConfirmDialog } from 'panel/common/ui/ConfirmDialog';
+import intl from 'panel/common/intl';
+import { encryptionState, setTlsConfig } from 'panel/stores/encryption';
+
+export const PlainDnsToggle = () => {
+ const [confirmDisable, setConfirmDisable] = createSignal(false);
+ const [localChecked, setLocalChecked] = createSignal(encryptionState.serve_plain_dns, {
+ equals: false,
+ });
+ const enc = () => encryptionState;
+
+ // Sync localChecked from store when the dialog is not open,
+ // so external changes (e.g. reset, removeCert) are reflected.
+ createEffect(() => {
+ if (!confirmDisable()) {
+ setLocalChecked(enc().serve_plain_dns);
+ }
+ });
+
+ const fullyConfigured = () => {
+ const hasCert = !!(enc().certificate_chain || enc().certificate_path);
+ const hasKey = !!(enc().private_key || enc().private_key_path || enc().private_key_saved);
+ return enc().enabled && hasCert && hasKey;
+ };
+
+ const locked = () => !fullyConfigured();
+
+ const onChange = (checked: boolean) => {
+ if (locked()) return;
+ if (checked) {
+ setTlsConfig({ serve_plain_dns: true });
+ return;
+ }
+ // Force DOM re-sync before opening the modal. With equals:false
+ // this re-applies checked={true} to the native input.
+ setLocalChecked((v) => v);
+ setConfirmDisable(true);
+ };
+
+ return (
+ <>
+
+
+ setConfirmDisable(false)}
+ onConfirm={() => {
+ setTlsConfig({ serve_plain_dns: false });
+ setConfirmDisable(false);
+ }}
+ />
+
+ >
+ );
+};
diff --git a/client_v2/src/components/Encryption/blocks/RedirectToggle.tsx b/client_v2/src/components/Encryption/blocks/RedirectToggle.tsx
new file mode 100644
index 00000000000..3e74441823f
--- /dev/null
+++ b/client_v2/src/components/Encryption/blocks/RedirectToggle.tsx
@@ -0,0 +1,28 @@
+import { SettingRow } from 'panel/common/ui/SettingRow';
+import intl from 'panel/common/intl';
+import { encryptionState, setTlsConfig } from 'panel/stores/encryption';
+
+export const RedirectToggle = () => {
+ const enc = () => encryptionState;
+
+ const fullyConfigured = () => {
+ const hasCert = !!(enc().certificate_chain || enc().certificate_path);
+ const hasKey = !!(enc().private_key || enc().private_key_path || enc().private_key_saved);
+ return enc().enabled && hasCert && hasKey;
+ };
+
+ const onChange = (checked: boolean) => {
+ setTlsConfig({ force_https: checked });
+ };
+
+ return (
+
+ );
+};
diff --git a/client_v2/src/components/Encryption/blocks/ResetDnsModal.tsx b/client_v2/src/components/Encryption/blocks/ResetDnsModal.tsx
new file mode 100644
index 00000000000..c0f8eec5461
--- /dev/null
+++ b/client_v2/src/components/Encryption/blocks/ResetDnsModal.tsx
@@ -0,0 +1,29 @@
+import { Show } from 'solid-js';
+import { ConfirmDialog } from 'panel/common/ui/ConfirmDialog';
+import intl from 'panel/common/intl';
+import { setTlsConfig, validateTlsConfig, resetValidationStatus } from 'panel/stores/encryption';
+import { defaultTlsValues } from './helpers';
+
+type Props = {
+ open: boolean;
+ onClose: () => void;
+};
+
+export const ResetDnsModal = (props: Props) => (
+
+ {
+ resetValidationStatus();
+ setTlsConfig(defaultTlsValues);
+ validateTlsConfig(defaultTlsValues);
+ props.onClose();
+ }}
+ />
+
+);
diff --git a/client_v2/src/components/Encryption/blocks/ServerSettingsModal.tsx b/client_v2/src/components/Encryption/blocks/ServerSettingsModal.tsx
new file mode 100644
index 00000000000..7b90eecd881
--- /dev/null
+++ b/client_v2/src/components/Encryption/blocks/ServerSettingsModal.tsx
@@ -0,0 +1,242 @@
+import { createEffect, createSignal, on } from 'solid-js';
+import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
+import { Input } from 'panel/common/controls/Input';
+import { FaqTooltip } from 'panel/common/ui/FaqTooltip';
+import intl from 'panel/common/intl';
+import { encryptionState, setTlsConfig } from 'panel/stores/encryption';
+import { toNumber } from 'panel/helpers/form';
+import { validateServerName, validatePort, validateIsSafePort } from 'panel/helpers/validators';
+import s from '../styles.module.pcss';
+import theme from 'panel/lib/theme';
+
+type Props = {
+ open: boolean;
+ onClose: () => void;
+};
+
+export const ServerSettingsModal = (props: Props) => {
+ const [serverName, setServerName] = createSignal('');
+ const [portHttps, setPortHttps] = createSignal(0);
+ const [portDot, setPortDot] = createSignal(0);
+ const [portDoq, setPortDoq] = createSignal(0);
+ const [errors, setErrors] = createSignal>({});
+
+ // Reset local state when modal opens
+ createEffect(
+ on(
+ () => props.open,
+ (open) => {
+ if (open) {
+ setServerName(encryptionState.server_name || '');
+ setPortHttps(encryptionState.port_https || 0);
+ setPortDot(encryptionState.port_dns_over_tls || 0);
+ setPortDoq(encryptionState.port_dns_over_quic || 0);
+ setErrors({});
+ }
+ },
+ ),
+ );
+
+ const clearError = (field: string) => {
+ setErrors((prev) => {
+ const next = { ...prev };
+ delete next[field];
+ return next;
+ });
+ };
+
+ const hasErrors = () => Object.values(errors()).some(Boolean);
+
+ // --- Change handlers ---
+
+ const handleServerNameChange = (e: Event) => {
+ setServerName((e.target as HTMLInputElement).value);
+ clearError('server_name');
+ };
+
+ const handlePortHttpsChange = (e: Event) => {
+ setPortHttps(toNumber((e.target as HTMLInputElement).value));
+ clearError('port_https');
+ };
+
+ const handlePortDotChange = (e: Event) => {
+ setPortDot(toNumber((e.target as HTMLInputElement).value));
+ clearError('port_dns_over_tls');
+ };
+
+ const handlePortDoqChange = (e: Event) => {
+ setPortDoq(toNumber((e.target as HTMLInputElement).value));
+ clearError('port_dns_over_quic');
+ };
+
+ // --- Blur validators ---
+
+ const handleServerNameBlur = () => {
+ const err = validateServerName(serverName());
+ setErrors((prev) => {
+ const next = { ...prev };
+ if (err) {
+ next.server_name = err;
+ } else {
+ delete next.server_name;
+ }
+ return next;
+ });
+ };
+
+ const handlePortHttpsBlur = () => {
+ const err = validatePort(portHttps()) || validateIsSafePort(portHttps());
+ setErrors((prev) => {
+ const next = { ...prev };
+ if (err) {
+ next.port_https = err as string;
+ } else {
+ delete next.port_https;
+ }
+ return next;
+ });
+ };
+
+ const handlePortDotBlur = () => {
+ const err = validatePort(portDot());
+ setErrors((prev) => {
+ const next = { ...prev };
+ if (err) {
+ next.port_dns_over_tls = err as string;
+ } else {
+ delete next.port_dns_over_tls;
+ }
+ return next;
+ });
+ };
+
+ const handlePortDoqBlur = () => {
+ const err = validatePort(portDoq());
+ setErrors((prev) => {
+ const next = { ...prev };
+ if (err) {
+ next.port_dns_over_quic = err as string;
+ } else {
+ delete next.port_dns_over_quic;
+ }
+ return next;
+ });
+ };
+
+ const save = () => {
+ setTlsConfig({
+ server_name: serverName(),
+ port_https: portHttps(),
+ port_dns_over_tls: portDot(),
+ port_dns_over_quic: portDoq(),
+ });
+ props.onClose();
+ };
+
+ const processing = () => encryptionState.processingConfig || encryptionState.processingValidate;
+
+ return (
+
+
+
+
+ {intl.getMessage('encryption_https')}
+
+ >
+ }
+ errorMessage={errors().port_https}
+ size="large"
+ />
+
+
+
+ {intl.getMessage('encryption_dot')}
+
+ >
+ }
+ errorMessage={errors().port_dns_over_tls}
+ size="large"
+ />
+
+
+
+ {intl.getMessage('encryption_doq')}
+
+ >
+ }
+ errorMessage={errors().port_dns_over_quic}
+ size="large"
+ />
+
+
+ );
+};
diff --git a/client_v2/src/components/Encryption/blocks/ServerSettingsRow.tsx b/client_v2/src/components/Encryption/blocks/ServerSettingsRow.tsx
new file mode 100644
index 00000000000..7a7a650e00e
--- /dev/null
+++ b/client_v2/src/components/Encryption/blocks/ServerSettingsRow.tsx
@@ -0,0 +1,50 @@
+import { SettingRow } from 'panel/common/ui/SettingRow';
+import intl from 'panel/common/intl';
+import { encryptionState } from 'panel/stores/encryption';
+import s from '../styles.module.pcss';
+
+type Props = {
+ onOpen: () => void;
+};
+
+export const ServerSettingsRow = (props: Props) => {
+ const hasCert = () => !!(encryptionState.certificate_chain || encryptionState.certificate_path);
+ const hasKey = () =>
+ !!(
+ encryptionState.private_key ||
+ encryptionState.private_key_path ||
+ encryptionState.private_key_saved
+ );
+ const isEnabled = () => hasCert() && hasKey();
+
+ const serverName = () => encryptionState.server_name || intl.getMessage('none_text');
+ const httpsPort = () => encryptionState.port_https || intl.getMessage('none_text');
+ const dotPort = () => encryptionState.port_dns_over_tls || intl.getMessage('none_text');
+ const doqPort = () => encryptionState.port_dns_over_quic || intl.getMessage('none_text');
+
+ return (
+
+
+ {intl.getMessage('encryption_server_summary', { value: serverName() })}
+
+
+ {intl.getMessage('encryption_https_summary', { value: httpsPort() })}
+
+
+ {intl.getMessage('encryption_dot_summary', { value: dotPort() })}
+
+
+ {intl.getMessage('encryption_doq_summary', { value: doqPort() })}
+
+ >
+ }
+ disabled={!isEnabled()}
+ onClick={() => isEnabled() && props.onOpen()}
+ />
+ );
+};
diff --git a/client_v2/src/components/Encryption/blocks/TlsCertSection.tsx b/client_v2/src/components/Encryption/blocks/TlsCertSection.tsx
new file mode 100644
index 00000000000..57a112eda88
--- /dev/null
+++ b/client_v2/src/components/Encryption/blocks/TlsCertSection.tsx
@@ -0,0 +1,83 @@
+import { Show } from 'solid-js';
+import { Icon } from 'panel/common/ui/Icon';
+import intl from 'panel/common/intl';
+import {
+ encryptionState,
+ setTlsConfig,
+ resetValidationStatus,
+ clearCertOptimistically,
+} from 'panel/stores/encryption';
+import { CertificateStatus, KeyStatus, ValidationStatus } from '../Status';
+import s from '../styles.module.pcss';
+import theme from 'panel/lib/theme';
+
+export const TlsCertSection = () => {
+ const enc = () => encryptionState;
+
+ const removeCert = () => {
+ clearCertOptimistically();
+ resetValidationStatus();
+ setTlsConfig({
+ enabled: false,
+ serve_plain_dns: true,
+ certificate_chain: '',
+ private_key: '',
+ certificate_path: '',
+ private_key_path: '',
+ private_key_saved: false,
+ });
+ };
+
+ const renderStatus = () => {
+ if (enc().valid_cert && enc().valid_key && !enc().valid_pair) {
+ return (
+
+ );
+ }
+ if (enc().warning_validation) {
+ const isWarning = enc().valid_key && enc().valid_cert && enc().valid_pair;
+ return (
+
+ );
+ }
+ if (!enc().certificate_chain && !enc().certificate_path) return null;
+ return (
+ <>
+
+
+
+
+ >
+ );
+ };
+
+ return (
+
+
+ {intl.getMessage('tls_certificate')}
+
+
+
+
+ {renderStatus()}
+
+ );
+};
diff --git a/client_v2/src/components/Encryption/blocks/helpers.ts b/client_v2/src/components/Encryption/blocks/helpers.ts
new file mode 100644
index 00000000000..c56765aea30
--- /dev/null
+++ b/client_v2/src/components/Encryption/blocks/helpers.ts
@@ -0,0 +1,95 @@
+import {
+ DNS_OVER_QUIC_PORT,
+ DNS_OVER_TLS_PORT,
+ STANDARD_HTTPS_PORT,
+ DEBOUNCE_TIMEOUT,
+ ENCRYPTION_SOURCE,
+} from 'panel/helpers/constants';
+import { validateTlsConfig } from 'panel/stores/encryption';
+import type { EncryptionFormValues } from '../validate';
+
+/** Wire format sent to the backend — EncryptionFormValues without the source tri-state fields. */
+export type TlsSubmitValues = Omit;
+
+/**
+ * Default TLS form values — used for initial state and "Reset DNS protocols".
+ * Mirrors `defaultValues` from the original Form.tsx.
+ */
+export const defaultTlsValues: Required = {
+ enabled: false,
+ serve_plain_dns: true,
+ server_name: '',
+ force_https: false,
+ port_https: STANDARD_HTTPS_PORT,
+ port_dns_over_tls: DNS_OVER_TLS_PORT,
+ port_dns_over_quic: DNS_OVER_QUIC_PORT,
+ certificate_chain: '',
+ private_key: '',
+ certificate_path: '',
+ private_key_path: '',
+ certificate_source: ENCRYPTION_SOURCE.PATH,
+ key_source: ENCRYPTION_SOURCE.PATH,
+ private_key_saved: false,
+};
+
+/**
+ * Strips the source tri-state into the wire format the backend expects.
+ * - When source is PATH: empties the content field
+ * - When source is CONTENT: empties the path field
+ * - When private_key_saved is true: empties private_key (backend uses saved key)
+ *
+ * IMPORTANT: Keep in sync with the backend contract. Matches the original
+ * getSubmitValues in index.tsx exactly.
+ */
+export const getSubmitValues = (values: EncryptionFormValues): TlsSubmitValues => {
+ const { certificate_source, key_source, private_key_saved, ...config } =
+ values as EncryptionFormValues & Record;
+ if (certificate_source === ENCRYPTION_SOURCE.PATH) {
+ config.certificate_chain = '';
+ } else {
+ config.certificate_path = '';
+ }
+ if (key_source === ENCRYPTION_SOURCE.PATH) {
+ config.private_key = '';
+ } else if (key_source === ENCRYPTION_SOURCE.SAVED) {
+ config.private_key_path = '';
+ config.private_key = '';
+ config.private_key_saved = private_key_saved;
+ } else {
+ config.private_key_path = '';
+ if (private_key_saved) {
+ config.private_key = '';
+ config.private_key_saved = private_key_saved;
+ }
+ }
+ return config;
+};
+
+/**
+ * Creates a debounced TLS config validator.
+ * Returns [validate(values), cancel()] tuple.
+ * Call cancel() in onCleanup to avoid stale timeouts after unmount.
+ */
+export const createDebouncedValidator = (): [
+ (values: EncryptionFormValues) => void,
+ () => void,
+] => {
+ let timer: ReturnType | null = null;
+
+ const validate = (values: EncryptionFormValues) => {
+ if (timer) clearTimeout(timer);
+ timer = setTimeout(() => {
+ const submitValues = getSubmitValues(values);
+ if (submitValues.enabled) {
+ validateTlsConfig(submitValues);
+ }
+ }, DEBOUNCE_TIMEOUT);
+ };
+
+ const cancel = () => {
+ if (timer) clearTimeout(timer);
+ timer = null;
+ };
+
+ return [validate, cancel];
+};
diff --git a/client_v2/src/components/Encryption/index.ts b/client_v2/src/components/Encryption/index.ts
new file mode 100644
index 00000000000..22e92e22118
--- /dev/null
+++ b/client_v2/src/components/Encryption/index.ts
@@ -0,0 +1 @@
+export { Encryption } from './Encryption';
diff --git a/client_v2/src/components/Encryption/styles.module.pcss b/client_v2/src/components/Encryption/styles.module.pcss
new file mode 100644
index 00000000000..8a4012d5072
--- /dev/null
+++ b/client_v2/src/components/Encryption/styles.module.pcss
@@ -0,0 +1,89 @@
+.title {
+ padding-bottom: 16px;
+}
+
+.description {
+ margin: 0;
+ padding: 0 16px 16px;
+}
+
+.useSavedKey {
+ padding: 8px 0 8px 32px;
+}
+
+.tooltipText:not(:last-child) {
+ margin-bottom: 16px;
+}
+
+.group {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.header {
+ display: flex;
+ align-items: center;
+ padding-bottom: 16px;
+}
+
+.menuButton {
+ position: relative;
+ top: 2px;
+ border: none;
+ background: transparent;
+ cursor: pointer;
+ color: var(--default-gray-icons);
+ padding: 4px;
+ border-radius: 4px;
+
+ &:hover {
+ color: var(--default-black-icons);
+ }
+}
+
+.section {
+ padding: 24px 16px 8px;
+}
+
+.certSection {
+ padding: 8px 16px;
+}
+
+.plusButton {
+ padding: 0 16px;
+}
+
+.certRow {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.certTitle {
+ font-weight: var(--weight-semi-bold);
+ font-size: 16px;
+ color: var(--default-main-text);
+}
+
+.trashButton {
+ border: none;
+ background: transparent;
+ cursor: pointer;
+ color: var(--default-error-link);
+ padding: 4px;
+}
+
+.summaryLine {
+ color: var(--default-description-text);
+ font-size: 14px;
+}
+
+.certStatus {
+ display: inline-block;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background-color: var(--default-product-icon);
+ margin-right: 8px;
+}
diff --git a/client_v2/src/components/Encryption/validate.ts b/client_v2/src/components/Encryption/validate.ts
new file mode 100644
index 00000000000..8de4c5fad10
--- /dev/null
+++ b/client_v2/src/components/Encryption/validate.ts
@@ -0,0 +1,142 @@
+import intl from 'panel/common/intl';
+import { ENCRYPTION_SOURCE } from 'panel/helpers/constants';
+import {
+ validateServerName,
+ validatePort,
+ validateIsSafePort,
+ validatePlainDns,
+ validateRequiredValue,
+ validatePath,
+ validatePemContent,
+} from 'panel/helpers/validators';
+
+export type EncryptionFormValues = {
+ enabled?: boolean;
+ serve_plain_dns?: boolean;
+ server_name?: string;
+ force_https?: boolean;
+ port_https?: number;
+ port_dns_over_tls?: number;
+ port_dns_over_quic?: number;
+ certificate_chain?: string;
+ private_key?: string;
+ certificate_path?: string;
+ private_key_path?: string;
+ certificate_source?: string;
+ key_source?: string;
+ private_key_saved?: boolean;
+};
+
+const validateCertPath = (value?: string): string | undefined => {
+ if (value && validatePath(value)) {
+ return intl.getMessage('encryption_unable_read_cert');
+ }
+ return undefined;
+};
+
+const validateKeyPath = (value?: string): string | undefined => {
+ if (value && validatePath(value)) {
+ return intl.getMessage('encryption_unable_read_key');
+ }
+ return undefined;
+};
+
+/**
+ * Validates only the certificate fields (chain or path).
+ * Used by step 1 of the Add TLS Certificate modal.
+ */
+export const validateCertFields = (values: EncryptionFormValues): Record => {
+ const errs: Record = {};
+
+ if (values.certificate_source === ENCRYPTION_SOURCE.CONTENT) {
+ const certErr =
+ validateRequiredValue(values.certificate_chain) ||
+ validatePemContent(values.certificate_chain);
+ if (certErr) errs.certificate_chain = certErr;
+ } else {
+ const certPathErr =
+ validateRequiredValue(values.certificate_path) ||
+ validateCertPath(values.certificate_path);
+ if (certPathErr) errs.certificate_path = certPathErr;
+ }
+
+ return errs;
+};
+
+/**
+ * Validates only the private key fields (key or path).
+ * Used by step 2 of the Add TLS Certificate modal.
+ */
+export const validateKeyFields = (values: EncryptionFormValues): Record => {
+ const errs: Record = {};
+
+ if (values.private_key_saved) return errs;
+
+ if (values.key_source === ENCRYPTION_SOURCE.CONTENT) {
+ const keyErr =
+ validateRequiredValue(values.private_key) || validatePemContent(values.private_key);
+ if (keyErr) errs.private_key = keyErr;
+ } else if (values.key_source === ENCRYPTION_SOURCE.PATH) {
+ const keyPathErr =
+ validateRequiredValue(values.private_key_path) ||
+ validateKeyPath(values.private_key_path);
+ if (keyPathErr) errs.private_key_path = keyPathErr;
+ }
+
+ return errs;
+};
+
+/**
+ * Validates certificate and private key fields together.
+ */
+export const validateCertKeyFields = (values: EncryptionFormValues): Record => {
+ const errs: Record = {};
+ Object.assign(errs, validateCertFields(values), validateKeyFields(values));
+ return errs;
+};
+
+/**
+ * Runs all client-side validation for the encryption form and returns a map of
+ * field name -> error message. An empty object means the form is valid.
+ *
+ * Used by both `handleBlur` (to gate the debounced backend validate request)
+ * and `onFormSubmit`, so the rules live in one place.
+ */
+export const validateEncryptionForm = (values: EncryptionFormValues): Record => {
+ const errs: Record = {};
+
+ // Delegate cert/key validation to the shared helper.
+ Object.assign(errs, validateCertKeyFields(values));
+
+ // Server name — optional, format only.
+ const serverNameErr = validateServerName(values.server_name);
+ if (serverNameErr) errs.server_name = serverNameErr;
+
+ // Ports — range, unsafe, and equality are checked fully client-side so
+ // invalid ports never reach the backend validate request.
+ // Coerce to number first: the store may hold empty strings before the
+ // first data load, and `validatePort('')` would skip the range check.
+ const portHttps = Number(values.port_https) || 0;
+ const portDot = Number(values.port_dns_over_tls) || 0;
+ const portDoq = Number(values.port_dns_over_quic) || 0;
+
+ const portHttpsErr = validatePort(portHttps) || validateIsSafePort(portHttps);
+ if (portHttpsErr) errs.port_https = portHttpsErr as string;
+
+ const portDotErr = validatePort(portDot);
+ if (portDotErr) errs.port_dns_over_tls = portDotErr as string;
+
+ const portDoqErr = validatePort(portDoq);
+ if (portDoqErr) errs.port_dns_over_quic = portDoqErr as string;
+
+ if (portDot && portHttps && portDot === portHttps) {
+ errs.port_dns_over_tls = intl.getMessage('form_error_equal');
+ errs.port_https = intl.getMessage('form_error_equal');
+ }
+
+ // Plain DNS must be served when encryption is disabled.
+ const plainDnsErr = validatePlainDns(values.serve_plain_dns ?? false, values);
+ if (plainDnsErr) errs.serve_plain_dns = plainDnsErr as string;
+
+ return errs;
+};
diff --git a/client_v2/src/components/FilterLists/Allowlists.tsx b/client_v2/src/components/FilterLists/Allowlists.tsx
new file mode 100644
index 00000000000..a61d8896574
--- /dev/null
+++ b/client_v2/src/components/FilterLists/Allowlists.tsx
@@ -0,0 +1,159 @@
+import { createSignal, createMemo, createEffect, Show, onMount } from 'solid-js';
+import cn from 'clsx';
+
+import intl from 'panel/common/intl';
+import { MODAL_TYPE } from 'panel/helpers/constants';
+import { PageLoader } from 'panel/common/ui/Loader';
+import theme from 'panel/lib/theme';
+import {
+ getFilteringStatus,
+ toggleFilterStatus,
+ refreshFilters,
+ filteringState,
+} from 'panel/stores/filtering';
+import { Icon } from 'panel/common/ui/Icon';
+
+import { openModal } from 'panel/stores/modals';
+import { PlusButton } from 'panel/common/ui/PlusButton';
+import { FilterUpdateModal } from 'panel/components/FilterLists/blocks/FilterUpdateModal';
+import { DeleteAllowlistModal } from './blocks/DeleteAllowlistModal/DeleteAllowlistModal';
+import { ConfigureAllowlistModal } from './blocks/ConfigureAllowlistModal';
+import { ListsTable, TABLE_IDS } from './blocks/ListsTable/ListsTable';
+import s from './FilterLists.module.pcss';
+
+export const Allowlists = () => {
+ const [currentFilter, setCurrentFilter] = createSignal<{
+ url: string;
+ name: string;
+ enabled?: boolean;
+ }>({
+ url: '',
+ name: '',
+ });
+
+ const [isInitialLoad, setIsInitialLoad] = createSignal(true);
+
+ onMount(() => {
+ getFilteringStatus();
+ });
+
+ createEffect(() => {
+ if (!filteringState.processingFilters && isInitialLoad()) {
+ setIsInitialLoad(false);
+ }
+ });
+
+ const isDataReady = createMemo(() => filteringState.processingFilters && isInitialLoad());
+
+ const handleRefresh = () => {
+ refreshFilters({ whitelist: true });
+ };
+
+ const toggleFilter = (url: string, data: { name: string; url: string; enabled: boolean }) => {
+ toggleFilterStatus(url, data, true);
+ };
+
+ const openFilterUpdateModal = () => {
+ openModal(MODAL_TYPE.FILTER_UPDATE);
+ };
+
+ const openAddAllowlistModal = () => {
+ openModal(MODAL_TYPE.ADD_ALLOWLIST);
+ };
+
+ const openEditAllowlistModal = (url: string, name: string, enabled: boolean) => {
+ setCurrentFilter({ url, name, enabled });
+ openModal(MODAL_TYPE.EDIT_ALLOWLIST);
+ };
+
+ const openDeleteAllowlistModal = (url: string, name: string) => {
+ setCurrentFilter({ url, name });
+ openModal(MODAL_TYPE.DELETE_ALLOWLIST);
+ };
+
+ return (
+
+
+
+
+
+ {intl.getMessage('allowlists_desc')}
+
+
+
+ {intl.getMessage('add_allowlist')}
+
+
+
+ 0}>
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ }
+ >
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/FilterLists/Blocklists.tsx b/client_v2/src/components/FilterLists/Blocklists.tsx
new file mode 100644
index 00000000000..37c7bac3b8c
--- /dev/null
+++ b/client_v2/src/components/FilterLists/Blocklists.tsx
@@ -0,0 +1,159 @@
+import { createSignal, createMemo, createEffect, Show, onMount } from 'solid-js';
+import cn from 'clsx';
+
+import intl from 'panel/common/intl';
+import { MODAL_TYPE } from 'panel/helpers/constants';
+import theme from 'panel/lib/theme';
+import {
+ getFilteringStatus,
+ toggleFilterStatus,
+ refreshFilters,
+ filteringState,
+} from 'panel/stores/filtering';
+import { Icon } from 'panel/common/ui/Icon';
+import { openModal } from 'panel/stores/modals';
+import { DeleteBlocklistModal } from 'panel/components/FilterLists/blocks/DeleteBlocklistModal';
+import { ConfigureBlocklistModal } from 'panel/components/FilterLists/blocks/ConfigureBlocklistModal';
+import { PageLoader } from 'panel/common/ui/Loader';
+import { PlusButton } from 'panel/common/ui/PlusButton';
+import { ListsTable, TABLE_IDS } from './blocks/ListsTable/ListsTable';
+import { FilterUpdateModal } from './blocks/FilterUpdateModal';
+
+import s from './FilterLists.module.pcss';
+
+export const Blocklists = () => {
+ const [currentFilter, setCurrentFilter] = createSignal<{
+ url: string;
+ name: string;
+ enabled?: boolean;
+ }>({
+ url: '',
+ name: '',
+ });
+
+ const [isInitialLoad, setIsInitialLoad] = createSignal(true);
+
+ onMount(() => {
+ getFilteringStatus();
+ });
+
+ createEffect(() => {
+ if (!filteringState.processingFilters && isInitialLoad()) {
+ setIsInitialLoad(false);
+ }
+ });
+
+ const isDataReady = createMemo(() => filteringState.processingFilters && isInitialLoad());
+
+ const toggleFilter = (url: string, data: { name: string; url: string; enabled: boolean }) => {
+ toggleFilterStatus(url, data, false);
+ };
+
+ const handleRefresh = () => {
+ refreshFilters({ whitelist: false });
+ };
+
+ const openFilterUpdateModal = () => {
+ openModal(MODAL_TYPE.FILTER_UPDATE);
+ };
+
+ const openAddBlocklistModal = () => {
+ openModal(MODAL_TYPE.ADD_BLOCKLIST);
+ };
+
+ const openEditBlocklistModal = (url: string, name: string, enabled: boolean) => {
+ setCurrentFilter({ url, name, enabled });
+ openModal(MODAL_TYPE.EDIT_BLOCKLIST);
+ };
+
+ const openDeleteBlocklistModal = (url: string, name: string) => {
+ setCurrentFilter({ url, name });
+ openModal(MODAL_TYPE.DELETE_BLOCKLIST);
+ };
+
+ return (
+
+
+
+
+
+ {intl.getMessage('blocklists_desc')}
+
+
+
+ {intl.getMessage('add_blocklist')}
+
+
+
+ 0}>
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ }
+ >
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/FilterLists/DNSRewrites.tsx b/client_v2/src/components/FilterLists/DNSRewrites.tsx
new file mode 100644
index 00000000000..d769b35e8a2
--- /dev/null
+++ b/client_v2/src/components/FilterLists/DNSRewrites.tsx
@@ -0,0 +1,181 @@
+import { createSignal, createMemo, Show, onMount, createEffect } from 'solid-js';
+import cn from 'clsx';
+
+import intl from 'panel/common/intl';
+import { MODAL_TYPE } from 'panel/helpers/constants';
+import theme from 'panel/lib/theme';
+import {
+ getRewritesList,
+ updateRewrite,
+ getRewriteSettings,
+ updateRewriteSettings,
+ rewritesState,
+} from 'panel/stores/rewrites';
+import { ConfirmDialog } from 'panel/common/ui/ConfirmDialog';
+import { SettingRow } from 'panel/common/ui/SettingRow';
+import { openModal } from 'panel/stores/modals';
+import { DeleteRewriteModal } from 'panel/components/FilterLists/blocks/DeleteRewriteModal';
+import { PlusButton } from 'panel/common/ui/PlusButton';
+import { ConfigureRewritesModal } from 'panel/components/FilterLists/blocks/ConfigureRewritesModal/ConfigureRewritesModal';
+import { RewritesTable } from './blocks/RewritesTable/RewritesTable';
+
+import s from './FilterLists.module.pcss';
+
+export type Rewrite = {
+ answer: string;
+ domain: string;
+ enabled: boolean;
+};
+
+export const DNSRewrites = () => {
+ const [currentRewrite, setCurrentRewrite] = createSignal({
+ answer: '',
+ domain: '',
+ enabled: false,
+ });
+
+ const [isConfirmOpen, setIsConfirmOpen] = createSignal(false);
+ const [targetEnabled, setTargetEnabled] = createSignal(null);
+ const [settingsLoaded, setSettingsLoaded] = createSignal(false);
+
+ // Only show loader during initial fetch, not on subsequent enable/disable toggles.
+ createEffect(() => {
+ if (!rewritesState.processingSettings && !settingsLoaded()) {
+ setSettingsLoaded(true);
+ }
+ });
+
+ const isInitialSettingsLoad = createMemo(
+ () => rewritesState.processingSettings && !settingsLoaded(),
+ );
+
+ onMount(() => {
+ getRewritesList();
+ getRewriteSettings();
+ });
+
+ const openAddRewiresModal = () => {
+ openModal(MODAL_TYPE.ADD_REWRITE);
+ };
+
+ const openEditRewriteModal = (rewrite: Rewrite) => {
+ setCurrentRewrite(rewrite);
+ openModal(MODAL_TYPE.EDIT_REWRITE);
+ };
+
+ const openDeleteRewriteModal = (rewrite: Rewrite) => {
+ setCurrentRewrite(rewrite);
+ openModal(MODAL_TYPE.DELETE_REWRITE);
+ };
+
+ const toggleRewrite = (rewrite: Rewrite) => {
+ const updatedRewrite = { ...rewrite, enabled: !rewrite.enabled };
+
+ updateRewrite({
+ target: rewrite,
+ update: updatedRewrite,
+ });
+ };
+
+ const openGlobalToggleConfirm = (value: boolean) => {
+ setTargetEnabled(value);
+ setIsConfirmOpen(true);
+ };
+
+ const closeGlobalToggleConfirm = () => {
+ setIsConfirmOpen(false);
+ setTargetEnabled(null);
+ };
+
+ const confirmGlobalToggle = () => {
+ if (targetEnabled() === null) {
+ return;
+ }
+
+ updateRewriteSettings({ enabled: targetEnabled()! });
+ closeGlobalToggleConfirm();
+ };
+
+ return (
+
+
+
openGlobalToggleConfirm(value)}
+ align="center"
+ class={s.dnsRewritesSettingRow}
+ inputClass={s.dnsRewritesSettingRowInput}
+ />
+
+
+
+ {intl.getMessage('rewrite_add')}
+
+
+
+ 0}>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/FilterLists/FilterLists.module.pcss b/client_v2/src/components/FilterLists/FilterLists.module.pcss
new file mode 100644
index 00000000000..fab7dbdd3d8
--- /dev/null
+++ b/client_v2/src/components/FilterLists/FilterLists.module.pcss
@@ -0,0 +1,99 @@
+.header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.desc {
+ width: 100%;
+ max-width: 556px;
+ padding: 16px;
+}
+
+.settingRowDesc {
+ padding-top: 8px;
+ font-size: var(--fs-text-t2);
+ line-height: var(--lh-t2-normal);
+}
+
+.dnsRewritesSettingRow {
+ max-width: 564px;
+}
+
+.dnsRewritesContainer {
+ padding-top: 24px;
+
+ @media (min-width: 1024px) {
+ padding-top: 32px;
+ }
+}
+
+.dnsRewritesSettingRowInput {
+ padding-top: 16px;
+}
+
+.button {
+ width: fit-content;
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px;
+ font-size: 16px;
+ color: var(--default-main-text);
+ border-radius: 8px;
+ transition: var(--t2) background-color;
+ border: 0;
+ background: none;
+ cursor: pointer;
+
+ &:hover,
+ &:focus {
+ background: var(--hovered-secondary-button);
+ }
+
+ &:active {
+ background: var(--pressed-secondary-button);
+ }
+
+ &:disabled {
+ background: var(--disabled-secondary-button);
+ color: var(--disabled-secondary-button-stroke);
+ cursor: default;
+ }
+
+ &_checkUpdates {
+ margin-left: auto;
+ }
+
+ &_add {
+ padding: 8px;
+ color: var(--default-link);
+ }
+}
+
+.labelDesktop {
+ display: none;
+
+ @media (min-width: 768px) {
+ display: inline;
+ }
+}
+
+.group {
+ display: flex;
+ flex-direction: column;
+ padding: 8px 16px;
+
+ &.tableGroup {
+ padding-top: 16px;
+ flex: 1 0 auto;
+
+ @media (min-width: 768px) {
+ padding-top: 32px;
+ }
+ }
+
+ &.buttonGroup {
+ align-items: flex-start;
+ }
+}
diff --git a/client_v2/src/components/FilterLists/blocks/ConfigureAllowlistModal/ConfigureAllowlistModal.tsx b/client_v2/src/components/FilterLists/blocks/ConfigureAllowlistModal/ConfigureAllowlistModal.tsx
new file mode 100644
index 00000000000..b6373179f5b
--- /dev/null
+++ b/client_v2/src/components/FilterLists/blocks/ConfigureAllowlistModal/ConfigureAllowlistModal.tsx
@@ -0,0 +1,154 @@
+import { createSignal, createEffect, untrack } from 'solid-js';
+
+import intl from 'panel/common/intl';
+import { Dialog } from 'panel/common/ui/Dialog/Dialog';
+import { MODAL_TYPE } from 'panel/helpers/constants';
+
+import { ModalWrapper } from 'panel/common/ui/ModalWrapper';
+import { closeModal } from 'panel/stores/modals';
+import theme from 'panel/lib/theme';
+import { Button } from 'panel/common/ui/Button';
+import { addFilter, editFilter, filteringState } from 'panel/stores/filtering';
+import { Input } from 'panel/common/controls/Input';
+import { validatePath, validateRequiredValue } from 'panel/helpers/validators';
+
+type FormValues = {
+ name: string;
+ url: string;
+ enabled?: boolean;
+};
+
+type ConfigureAllowlistModalIdType = 'ADD_ALLOWLIST' | 'EDIT_ALLOWLIST';
+
+type Props = {
+ modalId: ConfigureAllowlistModalIdType;
+ filterToEdit?: FormValues;
+};
+
+const getTitle = (modalId: ConfigureAllowlistModalIdType) => {
+ if (modalId === MODAL_TYPE.EDIT_ALLOWLIST) {
+ return intl.getMessage('allowlist_edit');
+ }
+
+ return intl.getMessage('allowlist_add');
+};
+
+const getButtonText = (modalId: ConfigureAllowlistModalIdType) => {
+ if (modalId === MODAL_TYPE.EDIT_ALLOWLIST) {
+ return intl.getMessage('save');
+ }
+
+ return intl.getMessage('add');
+};
+
+export const ConfigureAllowlistModal = (props: Props) => {
+ const [name, setName] = createSignal(untrack(() => props.filterToEdit?.name) ?? '');
+ const [url, setUrl] = createSignal(untrack(() => props.filterToEdit?.url) ?? '');
+ const [urlError, setUrlError] = createSignal();
+
+ createEffect(() => {
+ setName(props.filterToEdit?.name ?? '');
+ setUrl(props.filterToEdit?.url ?? '');
+ });
+
+ const validateAndSetErrors = () => {
+ const urlErr = validateRequiredValue(url()) || validatePath(url());
+ setUrlError(urlErr || undefined);
+ return !urlErr;
+ };
+
+ const handleFormSubmit = async (e: Event) => {
+ e.preventDefault();
+
+ if (!validateAndSetErrors()) {
+ return;
+ }
+
+ const values: FormValues = { name: name(), url: url() };
+
+ switch (props.modalId) {
+ case MODAL_TYPE.ADD_ALLOWLIST: {
+ addFilter(values.url, values.name, true);
+ break;
+ }
+ case MODAL_TYPE.EDIT_ALLOWLIST: {
+ editFilter(props.filterToEdit!.url, values, true);
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+
+ setName('');
+ setUrl('');
+ closeModal();
+ };
+
+ const handleCancel = () => {
+ setName('');
+ setUrl('');
+ setUrlError(undefined);
+ closeModal();
+ };
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/FilterLists/blocks/ConfigureAllowlistModal/index.ts b/client_v2/src/components/FilterLists/blocks/ConfigureAllowlistModal/index.ts
new file mode 100644
index 00000000000..984c3e624d8
--- /dev/null
+++ b/client_v2/src/components/FilterLists/blocks/ConfigureAllowlistModal/index.ts
@@ -0,0 +1 @@
+export { ConfigureAllowlistModal } from './ConfigureAllowlistModal';
diff --git a/client_v2/src/components/FilterLists/blocks/ConfigureBlocklistModal/ConfigureBlocklistModal.module.pcss b/client_v2/src/components/FilterLists/blocks/ConfigureBlocklistModal/ConfigureBlocklistModal.module.pcss
new file mode 100644
index 00000000000..d8902fcd03a
--- /dev/null
+++ b/client_v2/src/components/FilterLists/blocks/ConfigureBlocklistModal/ConfigureBlocklistModal.module.pcss
@@ -0,0 +1,13 @@
+.content {
+ max-width: none;
+ padding: 0;
+}
+
+.desc {
+ margin: 0;
+ padding: 8px 16px;
+}
+
+.formGroup {
+ padding: 0 16px;
+}
diff --git a/client_v2/src/components/FilterLists/blocks/ConfigureBlocklistModal/ConfigureBlocklistModal.tsx b/client_v2/src/components/FilterLists/blocks/ConfigureBlocklistModal/ConfigureBlocklistModal.tsx
new file mode 100644
index 00000000000..73977723c5d
--- /dev/null
+++ b/client_v2/src/components/FilterLists/blocks/ConfigureBlocklistModal/ConfigureBlocklistModal.tsx
@@ -0,0 +1,269 @@
+import { createSignal, createMemo, Show, createEffect } from 'solid-js';
+
+import intl from 'panel/common/intl';
+import { Dialog } from 'panel/common/ui/Dialog/Dialog';
+import { MODAL_TYPE, TAB_TYPE } from 'panel/helpers/constants';
+
+import { ModalWrapper } from 'panel/common/ui/ModalWrapper';
+import { closeModal } from 'panel/stores/modals';
+import theme from 'panel/lib/theme';
+import { Button } from 'panel/common/ui/Button';
+import {
+ addFilter,
+ addFiltersBatch,
+ editFilter,
+ removeFilter,
+ filteringState,
+} from 'panel/stores/filtering';
+import type { Filter } from 'panel/helpers/helpers';
+import { validatePath, validateRequiredValue } from 'panel/helpers/validators';
+import { ManualFilterForm } from 'panel/components/FilterLists/blocks/ConfigureBlocklistModal/blocks/ManualFilterForm';
+import { Tabs } from 'panel/common/ui/Tabs';
+import filtersCatalog from 'panel/helpers/filters/filters';
+import { InlineLoader } from 'panel/common/ui/Loader/InlineLoader';
+import { FiltersList } from './blocks/FiltersList';
+import s from './ConfigureBlocklistModal.module.pcss';
+
+type FormValues = {
+ name: string;
+ url: string;
+ enabled?: boolean;
+};
+
+type ConfigureBlocklistModalIdType = 'ADD_BLOCKLIST' | 'EDIT_BLOCKLIST';
+
+type Props = {
+ modalId: ConfigureBlocklistModalIdType;
+ filterToEdit?: FormValues;
+};
+
+type SelectedValues = {
+ selectedFilterIds: Record;
+ selectedSources: Record;
+};
+
+const getSelectedValues = (
+ filters: Filter[],
+ catalogSourcesToIdMap: Record,
+): SelectedValues =>
+ filters.reduce(
+ (acc: SelectedValues, { url }: Filter) => {
+ if (Object.hasOwn(catalogSourcesToIdMap, url)) {
+ const filterId = catalogSourcesToIdMap[url];
+ acc.selectedFilterIds[filterId] = true;
+ acc.selectedSources[url] = true;
+ }
+ return acc;
+ },
+ {
+ selectedFilterIds: {} as Record,
+ selectedSources: {} as Record,
+ } as SelectedValues,
+ );
+
+const getTitle = (modalId: ConfigureBlocklistModalIdType) => {
+ if (modalId === MODAL_TYPE.EDIT_BLOCKLIST) {
+ return intl.getMessage('blocklist_edit');
+ }
+
+ return intl.getMessage('blocklists_add');
+};
+
+const getButtonText = (modalId: ConfigureBlocklistModalIdType) => {
+ if (modalId === MODAL_TYPE.EDIT_BLOCKLIST) {
+ return intl.getMessage('save');
+ }
+
+ return intl.getMessage('add');
+};
+
+export const ConfigureBlocklistModal = (props: Props) => {
+ const catalogSourcesToIdMap = createMemo(() => {
+ const map: Record = {};
+ Object.entries(filtersCatalog.filters).forEach(([filterId, filterData]) => {
+ map[filterData.source] = filterId;
+ });
+ return map;
+ });
+
+ const selectedValues = createMemo(() =>
+ getSelectedValues(filteringState.filters, catalogSourcesToIdMap()),
+ );
+
+ const [activeTab, setActiveTab] = createSignal(TAB_TYPE.LIST);
+ const [selectedFilterIds, setSelectedFilterIds] = createSignal>({});
+
+ createEffect(() => {
+ setSelectedFilterIds(
+ props.modalId === MODAL_TYPE.EDIT_BLOCKLIST && props.filterToEdit
+ ? {}
+ : selectedValues().selectedFilterIds,
+ );
+ });
+
+ const handleFormSubmit = async (e: Event) => {
+ e.preventDefault();
+
+ const form = e.target as HTMLFormElement;
+ const formData = new FormData(form);
+ const values: FormValues = {
+ name: (formData.get('name') as string) || '',
+ url: (formData.get('url') as string) || '',
+ };
+
+ switch (props.modalId) {
+ case MODAL_TYPE.ADD_BLOCKLIST: {
+ if (values.url && values.name) {
+ const nameErr = validateRequiredValue(values.name);
+ const urlErr = validateRequiredValue(values.url) || validatePath(values.url);
+ if (nameErr || urlErr) {
+ return;
+ }
+ addFilter(values.url, values.name, false);
+ } else {
+ const existingFilterSources = new Set(
+ filteringState.filters.map((filter: Filter) => filter.url),
+ );
+
+ const ids = selectedFilterIds();
+ const changedValues = Object.entries(ids)?.reduce(
+ (acc: Record, [key, value]) => {
+ if (value && key in filtersCatalog.filters) {
+ const filterSource =
+ filtersCatalog.filters[
+ key as keyof typeof filtersCatalog.filters
+ ].source;
+ if (!existingFilterSources.has(filterSource)) {
+ acc[key] = value;
+ }
+ }
+ return acc;
+ },
+ {},
+ );
+
+ const filtersToAdd = Object.keys(changedValues).map((fieldName) => {
+ const { source, name } =
+ filtersCatalog.filters[
+ fieldName as keyof typeof filtersCatalog.filters
+ ];
+ return { url: source, name };
+ });
+ if (filtersToAdd.length > 0) {
+ await addFiltersBatch(filtersToAdd);
+ }
+
+ const initialSelected = selectedValues().selectedFilterIds;
+ const currentIds = selectedFilterIds();
+ const filtersToRemove = Object.entries(initialSelected)
+ .filter(([id, wasSelected]) => wasSelected && !currentIds[id])
+ .map(([id]) => {
+ const { source } =
+ filtersCatalog.filters[id as keyof typeof filtersCatalog.filters];
+ return source;
+ });
+
+ for (const url of filtersToRemove) {
+ await removeFilter(url, false);
+ }
+ }
+ break;
+ }
+ case MODAL_TYPE.EDIT_BLOCKLIST: {
+ editFilter(props.filterToEdit!.url, values, false);
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+
+ closeModal();
+ };
+
+ const handleCancel = () => {
+ closeModal();
+ };
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/FilterLists/blocks/ConfigureBlocklistModal/blocks/FiltersList/FiltersList.module.pcss b/client_v2/src/components/FilterLists/blocks/ConfigureBlocklistModal/blocks/FiltersList/FiltersList.module.pcss
new file mode 100644
index 00000000000..e47e0922b1e
--- /dev/null
+++ b/client_v2/src/components/FilterLists/blocks/ConfigureBlocklistModal/blocks/FiltersList/FiltersList.module.pcss
@@ -0,0 +1,32 @@
+.listWrap {
+ padding: 8px 0;
+}
+
+.list {
+ max-height: 300px;
+ overflow: auto;
+}
+
+.category {
+ padding: 8px 16px;
+}
+
+.categoryName {
+ font-weight: var(--weight-semi-bold);
+}
+
+.categoryDescription {
+ color: var(--default-description-text);
+}
+
+.filter {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+ padding: 8px 16px;
+}
+
+.dropdownTrigger {
+ cursor: pointer;
+}
diff --git a/client_v2/src/components/FilterLists/blocks/ConfigureBlocklistModal/blocks/FiltersList/FiltersList.tsx b/client_v2/src/components/FilterLists/blocks/ConfigureBlocklistModal/blocks/FiltersList/FiltersList.tsx
new file mode 100644
index 00000000000..525516331b8
--- /dev/null
+++ b/client_v2/src/components/FilterLists/blocks/ConfigureBlocklistModal/blocks/FiltersList/FiltersList.tsx
@@ -0,0 +1,146 @@
+import { For, untrack } from 'solid-js';
+import cn from 'clsx';
+
+import { Checkbox } from 'panel/common/controls/Checkbox';
+import { Dropdown } from 'panel/common/ui/Dropdown';
+import theme from 'panel/lib/theme';
+import intl from 'panel/common/intl';
+import { Icon } from 'panel/common/ui/Icon';
+import filtersCatalog from 'panel/helpers/filters/filters';
+
+import s from './FiltersList.module.pcss';
+
+const getCategoryName = (categoryId: string) => {
+ switch (categoryId) {
+ case 'general':
+ return intl.getMessage('filter_category_general');
+ case 'security':
+ return intl.getMessage('filter_category_security');
+ case 'regional':
+ return intl.getMessage('filter_category_regional');
+ case 'other':
+ return intl.getMessage('filter_category_other');
+ default:
+ return categoryId;
+ }
+};
+
+const getCategoryDesc = (categoryId: string) => {
+ switch (categoryId) {
+ case 'general':
+ return intl.getMessage('filter_category_general_desc');
+ case 'security':
+ return intl.getMessage('filter_category_security_desc');
+ case 'regional':
+ return intl.getMessage('filter_category_regional_desc');
+ case 'other':
+ return intl.getMessage('filter_category_other_desc');
+ default:
+ return categoryId;
+ }
+};
+
+type Props = {
+ selectedSources?: Record;
+ selectedIds: Record;
+ onChange: (ids: Record) => void;
+ disabled?: boolean;
+};
+
+export const FiltersList = (props: Props) => {
+ const { categories, filters } = filtersCatalog;
+
+ const handleToggle = (filterId: string) => (e: Event) => {
+ untrack(() => {
+ const checked = (e.target as HTMLInputElement).checked;
+ props.onChange({
+ ...props.selectedIds,
+ [filterId]: checked,
+ });
+ });
+ };
+
+ return (
+
+
+
+ {([categoryId]) => {
+ const categoryFilters = Object.entries(filters)
+ .filter(([, filter]) => filter.categoryId === categoryId)
+ .map(([key, filter]) => ({ ...filter, id: key }));
+
+ return (
+
+
+
+ {getCategoryName(categoryId)}
+
+
+ {getCategoryDesc(categoryId)}
+
+
+
+
+ {(filter) => {
+ const { homepage, source, name, id } = filter;
+ return (
+
+
+ {name}
+
+
+ }
+ position="bottomRight"
+ noIcon
+ >
+
+
+
+
+
+ );
+ }}
+
+
+ );
+ }}
+
+
+
+ );
+};
diff --git a/client_v2/src/components/FilterLists/blocks/ConfigureBlocklistModal/blocks/FiltersList/index.ts b/client_v2/src/components/FilterLists/blocks/ConfigureBlocklistModal/blocks/FiltersList/index.ts
new file mode 100644
index 00000000000..f34c7792f7e
--- /dev/null
+++ b/client_v2/src/components/FilterLists/blocks/ConfigureBlocklistModal/blocks/FiltersList/index.ts
@@ -0,0 +1 @@
+export { FiltersList } from './FiltersList';
diff --git a/client_v2/src/components/FilterLists/blocks/ConfigureBlocklistModal/blocks/ManualFilterForm/ManualFilterForm.tsx b/client_v2/src/components/FilterLists/blocks/ConfigureBlocklistModal/blocks/ManualFilterForm/ManualFilterForm.tsx
new file mode 100644
index 00000000000..0698a7b6ae6
--- /dev/null
+++ b/client_v2/src/components/FilterLists/blocks/ConfigureBlocklistModal/blocks/ManualFilterForm/ManualFilterForm.tsx
@@ -0,0 +1,63 @@
+import { createSignal, createEffect, untrack } from 'solid-js';
+import cn from 'clsx';
+
+import intl from 'panel/common/intl';
+import { Input } from 'panel/common/controls/Input';
+import { validatePath, validateRequiredValue } from 'panel/helpers/validators';
+import theme from 'panel/lib/theme';
+
+type Props = {
+ class?: string;
+ initialName?: string;
+ initialUrl?: string;
+};
+
+export const ManualFilterForm = (props: Props) => {
+ const [name, setName] = createSignal(untrack(() => props.initialName) ?? '');
+ const [url, setUrl] = createSignal(untrack(() => props.initialUrl) ?? '');
+ const [urlError, setUrlError] = createSignal();
+
+ createEffect(() => {
+ if (props.initialName !== undefined) {
+ setName(props.initialName);
+ }
+ if (props.initialUrl !== undefined) {
+ setUrl(props.initialUrl);
+ }
+ });
+
+ const handleUrlBlur = () => {
+ const urlErr = validateRequiredValue(url()) || validatePath(url());
+ setUrlError(urlErr || undefined);
+ };
+
+ return (
+
+ );
+};
diff --git a/client_v2/src/components/FilterLists/blocks/ConfigureBlocklistModal/blocks/ManualFilterForm/index.ts b/client_v2/src/components/FilterLists/blocks/ConfigureBlocklistModal/blocks/ManualFilterForm/index.ts
new file mode 100644
index 00000000000..ab81aba8329
--- /dev/null
+++ b/client_v2/src/components/FilterLists/blocks/ConfigureBlocklistModal/blocks/ManualFilterForm/index.ts
@@ -0,0 +1 @@
+export { ManualFilterForm } from './ManualFilterForm';
diff --git a/client_v2/src/components/FilterLists/blocks/ConfigureBlocklistModal/index.ts b/client_v2/src/components/FilterLists/blocks/ConfigureBlocklistModal/index.ts
new file mode 100644
index 00000000000..69ed6663605
--- /dev/null
+++ b/client_v2/src/components/FilterLists/blocks/ConfigureBlocklistModal/index.ts
@@ -0,0 +1 @@
+export { ConfigureBlocklistModal } from './ConfigureBlocklistModal';
diff --git a/client_v2/src/components/FilterLists/blocks/ConfigureRewritesModal/AnswerFaqTooltip.tsx b/client_v2/src/components/FilterLists/blocks/ConfigureRewritesModal/AnswerFaqTooltip.tsx
new file mode 100644
index 00000000000..aeae6b5b72e
--- /dev/null
+++ b/client_v2/src/components/FilterLists/blocks/ConfigureRewritesModal/AnswerFaqTooltip.tsx
@@ -0,0 +1,54 @@
+import { For, Show } from 'solid-js';
+import cn from 'clsx';
+
+import intl from 'panel/common/intl';
+import { FaqTooltip } from 'panel/common/ui/FaqTooltip';
+import { Icon } from 'panel/common/ui/Icon';
+import theme from 'panel/lib/theme';
+import s from './ConfigureRewritesModal.module.pcss';
+
+const items = [
+ {
+ message: intl.getMessage('rewrites_tooltip_instructions_item1'),
+ },
+ {
+ message: intl.getMessage('rewrites_tooltip_instructions_item2'),
+ },
+ {
+ message: intl.getMessage('rewrites_tooltip_instructions_item3'),
+ code: 'A',
+ },
+ {
+ message: intl.getMessage('rewrites_tooltip_instructions_item4'),
+ code: 'AAAA',
+ },
+];
+
+export const AnswerFaqTooltip = () => {
+ return (
+
+
+ {intl.getMessage('instructions')}
+
+
+
+ {(item) => (
+
+
+ {item.message}
+
+
+ {item.code}
+
+
+ )}
+
+ >
+ }
+ />
+ );
+};
diff --git a/client_v2/src/components/FilterLists/blocks/ConfigureRewritesModal/ConfigureRewritesModal.module.pcss b/client_v2/src/components/FilterLists/blocks/ConfigureRewritesModal/ConfigureRewritesModal.module.pcss
new file mode 100644
index 00000000000..9523905abbd
--- /dev/null
+++ b/client_v2/src/components/FilterLists/blocks/ConfigureRewritesModal/ConfigureRewritesModal.module.pcss
@@ -0,0 +1,45 @@
+.dropdown {
+ @media (min-width: 1024px) {
+ max-width: none;
+ }
+}
+
+.tooltip {
+ @media (min-width: 1024px) {
+ width: 456px;
+ max-width: none;
+ }
+}
+
+.tooltipTitle {
+ padding: 0 0 16px;
+ font-weight: var(--weight-semi-bold);
+}
+
+.tooltipItem {
+ position: relative;
+ padding: 8px 0 8px 32px;
+
+ &:last-child {
+ padding: 8px 0 0 32px;
+ }
+
+ & code {
+ display: inline-block;
+ vertical-align: middle;
+ margin-left: 4px;
+ padding: 2px 4px;
+ background-color: var(--page-background-additional);
+ font-family: inherit;
+ line-height: var(--lh-t3-normal);
+ border: 1px solid var(--default-item-divider);
+ border-radius: 4px;
+ }
+}
+
+.icon {
+ position: absolute;
+ top: 8px;
+ left: 0;
+ color: var(--default-black-icons);
+}
diff --git a/client_v2/src/components/FilterLists/blocks/ConfigureRewritesModal/ConfigureRewritesModal.tsx b/client_v2/src/components/FilterLists/blocks/ConfigureRewritesModal/ConfigureRewritesModal.tsx
new file mode 100644
index 00000000000..a03d840790d
--- /dev/null
+++ b/client_v2/src/components/FilterLists/blocks/ConfigureRewritesModal/ConfigureRewritesModal.tsx
@@ -0,0 +1,247 @@
+import { createSignal, createEffect, untrack } from 'solid-js';
+
+import intl from 'panel/common/intl';
+import { Dialog } from 'panel/common/ui/Dialog/Dialog';
+import { MODAL_TYPE } from 'panel/helpers/constants';
+
+import { ModalWrapper } from 'panel/common/ui/ModalWrapper';
+import { closeModal } from 'panel/stores/modals';
+import theme from 'panel/lib/theme';
+import { Button } from 'panel/common/ui/Button';
+import { addRewrite, updateRewrite, rewritesState } from 'panel/stores/rewrites';
+import { Input } from 'panel/common/controls/Input';
+import {
+ validateAnswer,
+ validateDomain,
+ validateRequiredValue,
+ validateRewriteNotExists,
+ validateRewriteNotSame,
+} from 'panel/helpers/validators';
+import { DomainFaqTooltip } from './DomainFaqTooltip';
+import { AnswerFaqTooltip } from './AnswerFaqTooltip';
+
+type FormValues = {
+ answer: string;
+ domain: string;
+ enabled: boolean;
+};
+
+type ConfigureRewritesModalIdType = 'ADD_REWRITE' | 'EDIT_REWRITE';
+
+type Props = {
+ modalId: ConfigureRewritesModalIdType;
+ rewriteToEdit?: FormValues;
+ onSubmit?: (values: FormValues) => boolean | void | Promise;
+ onClose?: () => void;
+};
+
+const getTitle = (modalId: ConfigureRewritesModalIdType) => {
+ if (modalId === MODAL_TYPE.EDIT_REWRITE) {
+ return intl.getMessage('rewrite_edit');
+ }
+
+ return intl.getMessage('rewrite_add');
+};
+
+const getButtonText = (modalId: ConfigureRewritesModalIdType) => {
+ if (modalId === MODAL_TYPE.EDIT_REWRITE) {
+ return intl.getMessage('save');
+ }
+
+ return intl.getMessage('add');
+};
+
+export const ConfigureRewritesModal = (props: Props) => {
+ const [domain, setDomain] = createSignal(untrack(() => props.rewriteToEdit?.domain) ?? '');
+ const [answer, setAnswer] = createSignal(untrack(() => props.rewriteToEdit?.answer) ?? '');
+ const [domainError, setDomainError] = createSignal();
+ const [answerError, setAnswerError] = createSignal();
+
+ createEffect(() => {
+ setDomain(props.rewriteToEdit?.domain ?? '');
+ setAnswer(props.rewriteToEdit?.answer ?? '');
+ });
+
+ const closeDialog = () => {
+ setDomain('');
+ setAnswer('');
+ setDomainError(undefined);
+ setAnswerError(undefined);
+ props.onClose?.();
+ closeModal();
+ };
+
+ const handleCancel = () => {
+ setDomain(props.rewriteToEdit?.domain ?? '');
+ setAnswer(props.rewriteToEdit?.answer ?? '');
+ setDomainError(undefined);
+ setAnswerError(undefined);
+ props.onClose?.();
+ closeModal();
+ };
+
+ const validateDomainField = () => {
+ const err = validateRequiredValue(domain()) || validateDomain(domain());
+ setDomainError(err || undefined);
+ return !err;
+ };
+
+ const validateAnswerField = () => {
+ const err =
+ validateRequiredValue(answer()) ||
+ validateAnswer(answer()) ||
+ validateRewriteNotSame(domain(), answer()) ||
+ validateRewriteNotExists(domain(), rewritesState.list, props.rewriteToEdit?.domain);
+ setAnswerError(err || undefined);
+ return !err;
+ };
+
+ const hasErrors = () => !!domainError() || !!answerError();
+
+ const handleFormSubmit = async (e: Event) => {
+ e.preventDefault();
+
+ const domainValid = validateDomainField();
+ const answerValid = validateAnswerField();
+ if (!domainValid || !answerValid) {
+ return;
+ }
+
+ const values: FormValues = {
+ domain: domain(),
+ answer: answer(),
+ enabled: props.rewriteToEdit?.enabled ?? false,
+ };
+
+ if (props.onSubmit) {
+ const shouldClose = await props.onSubmit(values);
+
+ if (shouldClose !== false) {
+ closeDialog();
+ }
+
+ return;
+ }
+
+ switch (props.modalId) {
+ case MODAL_TYPE.ADD_REWRITE: {
+ addRewrite({ answer: values.answer, domain: values.domain, enabled: true });
+ closeDialog();
+ break;
+ }
+ case MODAL_TYPE.EDIT_REWRITE: {
+ updateRewrite({
+ target: props.rewriteToEdit,
+ update: {
+ answer: values.answer,
+ domain: values.domain,
+ enabled: values.enabled,
+ },
+ });
+ closeDialog();
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+ };
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/FilterLists/blocks/ConfigureRewritesModal/DomainFaqTooltip.tsx b/client_v2/src/components/FilterLists/blocks/ConfigureRewritesModal/DomainFaqTooltip.tsx
new file mode 100644
index 00000000000..4496365f1ca
--- /dev/null
+++ b/client_v2/src/components/FilterLists/blocks/ConfigureRewritesModal/DomainFaqTooltip.tsx
@@ -0,0 +1,45 @@
+import { For } from 'solid-js';
+import cn from 'clsx';
+
+import intl from 'panel/common/intl';
+import { FaqTooltip } from 'panel/common/ui/FaqTooltip';
+import { Icon } from 'panel/common/ui/Icon';
+import theme from 'panel/lib/theme';
+import s from './ConfigureRewritesModal.module.pcss';
+
+const items = [
+ {
+ message: intl.getMessage('rewrites_tooltip_examples_item1'),
+ code: 'example.org',
+ },
+ {
+ message: intl.getMessage('rewrites_tooltip_examples_item2'),
+ code: '*.example.org',
+ },
+];
+
+export const DomainFaqTooltip = () => {
+ return (
+
+
+ {intl.getMessage('upstream_examples_title')}
+
+
+
+ {(item) => (
+
+
+ {item.message}
+ {item.code}
+
+ )}
+
+ >
+ }
+ />
+ );
+};
diff --git a/client_v2/src/components/FilterLists/blocks/ConfigureRewritesModal/index.ts b/client_v2/src/components/FilterLists/blocks/ConfigureRewritesModal/index.ts
new file mode 100644
index 00000000000..cb77ae60bfa
--- /dev/null
+++ b/client_v2/src/components/FilterLists/blocks/ConfigureRewritesModal/index.ts
@@ -0,0 +1 @@
+export { ConfigureRewritesModal } from './ConfigureRewritesModal';
diff --git a/client_v2/src/components/FilterLists/blocks/DeleteAllowlistModal/DeleteAllowlistModal.tsx b/client_v2/src/components/FilterLists/blocks/DeleteAllowlistModal/DeleteAllowlistModal.tsx
new file mode 100644
index 00000000000..ab868608499
--- /dev/null
+++ b/client_v2/src/components/FilterLists/blocks/DeleteAllowlistModal/DeleteAllowlistModal.tsx
@@ -0,0 +1,47 @@
+import { Show } from 'solid-js';
+
+import intl from 'panel/common/intl';
+import { ConfirmDialog } from 'panel/common/ui/ConfirmDialog';
+import { MODAL_TYPE } from 'panel/helpers/constants';
+import { ModalWrapper } from 'panel/common/ui/ModalWrapper';
+import { closeModal } from 'panel/stores/modals';
+import { removeFilter, filteringState } from 'panel/stores/filtering';
+
+type Props = {
+ filterToDelete: {
+ url: string;
+ name: string;
+ };
+ setFilterToDelete: (value: { url: string; name: string }) => void;
+};
+
+export const DeleteAllowlistModal = (props: Props) => {
+ const handleDeleteClose = () => {
+ props.setFilterToDelete({ url: '', name: '' });
+ closeModal();
+ };
+
+ const handleDeleteConfirm = () => {
+ removeFilter(props.filterToDelete.url, true, props.filterToDelete.name);
+ handleDeleteClose();
+ };
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/FilterLists/blocks/DeleteAllowlistModal/index.ts b/client_v2/src/components/FilterLists/blocks/DeleteAllowlistModal/index.ts
new file mode 100644
index 00000000000..10613679b68
--- /dev/null
+++ b/client_v2/src/components/FilterLists/blocks/DeleteAllowlistModal/index.ts
@@ -0,0 +1 @@
+export { DeleteAllowlistModal } from './DeleteAllowlistModal';
diff --git a/client_v2/src/components/FilterLists/blocks/DeleteBlocklistModal/DeleteBlocklistModal.tsx b/client_v2/src/components/FilterLists/blocks/DeleteBlocklistModal/DeleteBlocklistModal.tsx
new file mode 100644
index 00000000000..42c43e0cf24
--- /dev/null
+++ b/client_v2/src/components/FilterLists/blocks/DeleteBlocklistModal/DeleteBlocklistModal.tsx
@@ -0,0 +1,47 @@
+import { Show } from 'solid-js';
+
+import intl from 'panel/common/intl';
+import { ConfirmDialog } from 'panel/common/ui/ConfirmDialog';
+import { MODAL_TYPE } from 'panel/helpers/constants';
+import { ModalWrapper } from 'panel/common/ui/ModalWrapper';
+import { closeModal } from 'panel/stores/modals';
+import { removeFilter, filteringState } from 'panel/stores/filtering';
+
+type Props = {
+ filterToDelete: {
+ url: string;
+ name: string;
+ };
+ setFilterToDelete: (value: { url: string; name: string }) => void;
+};
+
+export const DeleteBlocklistModal = (props: Props) => {
+ const handleDeleteClose = () => {
+ props.setFilterToDelete({ url: '', name: '' });
+ closeModal();
+ };
+
+ const handleDeleteConfirm = () => {
+ removeFilter(props.filterToDelete.url, false, props.filterToDelete.name);
+ handleDeleteClose();
+ };
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/FilterLists/blocks/DeleteBlocklistModal/index.ts b/client_v2/src/components/FilterLists/blocks/DeleteBlocklistModal/index.ts
new file mode 100644
index 00000000000..eb51e389c9c
--- /dev/null
+++ b/client_v2/src/components/FilterLists/blocks/DeleteBlocklistModal/index.ts
@@ -0,0 +1 @@
+export { DeleteBlocklistModal } from './DeleteBlocklistModal';
diff --git a/client_v2/src/components/FilterLists/blocks/DeleteRewriteModal/DeleteRewriteModal.tsx b/client_v2/src/components/FilterLists/blocks/DeleteRewriteModal/DeleteRewriteModal.tsx
new file mode 100644
index 00000000000..89131b880c4
--- /dev/null
+++ b/client_v2/src/components/FilterLists/blocks/DeleteRewriteModal/DeleteRewriteModal.tsx
@@ -0,0 +1,63 @@
+import { Show } from 'solid-js';
+
+import intl from 'panel/common/intl';
+import { ConfirmDialog } from 'panel/common/ui/ConfirmDialog';
+import { MODAL_TYPE } from 'panel/helpers/constants';
+import { ModalWrapper } from 'panel/common/ui/ModalWrapper';
+import { closeModal } from 'panel/stores/modals';
+import { deleteRewrite, rewritesState } from 'panel/stores/rewrites';
+
+type Props = {
+ rewriteToDelete: {
+ answer: string;
+ domain: string;
+ enabled: boolean;
+ };
+ setRewriteToDelete: (value: { answer: string; domain: string; enabled: boolean }) => void;
+ onConfirm?: () => boolean | void | Promise;
+ onClose?: () => void;
+};
+
+export const DeleteRewriteModal = (props: Props) => {
+ const handleDeleteClose = () => {
+ props.setRewriteToDelete({ answer: '', domain: '', enabled: false });
+ props.onClose?.();
+ closeModal();
+ };
+
+ const handleDeleteConfirm = async () => {
+ if (props.onConfirm) {
+ const shouldClose = await props.onConfirm();
+
+ if (shouldClose !== false) {
+ handleDeleteClose();
+ }
+
+ return;
+ }
+
+ deleteRewrite(props.rewriteToDelete);
+ handleDeleteClose();
+ };
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/FilterLists/blocks/DeleteRewriteModal/index.ts b/client_v2/src/components/FilterLists/blocks/DeleteRewriteModal/index.ts
new file mode 100644
index 00000000000..13b73e2811a
--- /dev/null
+++ b/client_v2/src/components/FilterLists/blocks/DeleteRewriteModal/index.ts
@@ -0,0 +1 @@
+export { DeleteRewriteModal } from './DeleteRewriteModal';
diff --git a/client_v2/src/components/FilterLists/blocks/FilterUpdateModal/FilterIntervalInput.tsx b/client_v2/src/components/FilterLists/blocks/FilterUpdateModal/FilterIntervalInput.tsx
new file mode 100644
index 00000000000..0c72046134a
--- /dev/null
+++ b/client_v2/src/components/FilterLists/blocks/FilterUpdateModal/FilterIntervalInput.tsx
@@ -0,0 +1,75 @@
+import { createEffect, createSignal, untrack } from 'solid-js';
+
+import theme from 'panel/lib/theme';
+import { Input } from 'panel/common/controls/Input';
+import { toNumber } from 'panel/helpers/form';
+import intl from 'panel/common/intl';
+import { FILTER_INTERVALS } from './FilterUpdateModal';
+
+export const FILTER_INTERVAL_RANGE = {
+ MIN: 1,
+ MAX: 8760,
+};
+
+type Props = {
+ value: number | null | undefined;
+ onChange: (value: number | null) => void;
+ processing: boolean;
+ intervalValue: number;
+};
+
+export const FilterIntervalInput = (props: Props) => {
+ let inputRef: HTMLInputElement | undefined;
+ const [prevInterval, setPrevInterval] = createSignal(untrack(() => props.intervalValue));
+
+ createEffect(() => {
+ const wasCustom = prevInterval() === -1;
+ const isCustom = props.intervalValue === -1;
+
+ setPrevInterval(props.intervalValue);
+
+ if (!wasCustom && isCustom && !props.processing) {
+ inputRef?.focus({ preventScroll: true });
+ }
+ });
+
+ const handleChange = (e: Event) => {
+ const target = e.target as HTMLInputElement;
+ const { value } = target;
+ props.onChange(toNumber(value));
+ };
+
+ const handleBlur = () => {
+ // Validation on blur if needed
+ };
+
+ return (
+
+ (inputRef = el)}
+ id="filters_config_custom_interval"
+ label={intl.getMessage('update_filters_custom_hours')}
+ placeholder={intl.getMessage('settings_rotation_placeholder')}
+ value={props.value ?? ''}
+ onChange={handleChange}
+ onBlur={handleBlur}
+ disabled={props.processing || props.intervalValue !== FILTER_INTERVALS.CUSTOM}
+ errorMessage={
+ props.intervalValue === -1 &&
+ props.value !== null &&
+ props.value !== undefined &&
+ (props.value < FILTER_INTERVAL_RANGE.MIN ||
+ props.value > FILTER_INTERVAL_RANGE.MAX)
+ ? intl.getMessage('form_error_range', {
+ min: FILTER_INTERVAL_RANGE.MIN,
+ max: FILTER_INTERVAL_RANGE.MAX,
+ })
+ : undefined
+ }
+ min={FILTER_INTERVAL_RANGE.MIN}
+ max={FILTER_INTERVAL_RANGE.MAX}
+ type="number"
+ />
+
+ );
+};
diff --git a/client_v2/src/components/FilterLists/blocks/FilterUpdateModal/FilterUpdateModal.tsx b/client_v2/src/components/FilterLists/blocks/FilterUpdateModal/FilterUpdateModal.tsx
new file mode 100644
index 00000000000..7fd040e546d
--- /dev/null
+++ b/client_v2/src/components/FilterLists/blocks/FilterUpdateModal/FilterUpdateModal.tsx
@@ -0,0 +1,159 @@
+import { createSignal, createMemo, createEffect, untrack } from 'solid-js';
+
+import intl from 'panel/common/intl';
+import { Dialog } from 'panel/common/ui/Dialog/Dialog';
+import { Button } from 'panel/common/ui/Button';
+import { Radio } from 'panel/common/controls/Radio';
+import theme from 'panel/lib/theme';
+import { setFiltersConfig, filteringState } from 'panel/stores/filtering';
+import { ModalWrapper } from 'panel/common/ui/ModalWrapper';
+import { MODAL_TYPE } from 'panel/helpers/constants';
+import { closeModal } from 'panel/stores/modals';
+import { FilterIntervalInput, FILTER_INTERVAL_RANGE } from './FilterIntervalInput';
+
+export const FILTER_INTERVALS = {
+ DISABLE: 0,
+ HOURLY: 1,
+ DAILY: 24,
+ WEEKLY: 168,
+ CUSTOM: -1,
+} as const;
+
+const getIntervalTitle = (interval: number) => {
+ switch (interval) {
+ case FILTER_INTERVALS.DISABLE:
+ return intl.getMessage('update_filters_disable');
+ case FILTER_INTERVALS.HOURLY:
+ return intl.getMessage('update_filters_interval_hourly');
+ case FILTER_INTERVALS.DAILY:
+ return intl.getMessage('update_filters_interval_daily');
+ case FILTER_INTERVALS.WEEKLY:
+ return intl.getMessage('update_filters_interval_weekly');
+ case FILTER_INTERVALS.CUSTOM:
+ return intl.getMessage('update_filters_interval_custom');
+ default:
+ return intl.getMessage('update_filters_custom_hours', { hours: interval });
+ }
+};
+
+const RADIO_OPTIONS = [
+ { text: getIntervalTitle(FILTER_INTERVALS.DISABLE), value: FILTER_INTERVALS.DISABLE },
+ { text: getIntervalTitle(FILTER_INTERVALS.HOURLY), value: FILTER_INTERVALS.HOURLY },
+ { text: getIntervalTitle(FILTER_INTERVALS.DAILY), value: FILTER_INTERVALS.DAILY },
+ { text: getIntervalTitle(FILTER_INTERVALS.WEEKLY), value: FILTER_INTERVALS.WEEKLY },
+ { text: getIntervalTitle(FILTER_INTERVALS.CUSTOM), value: FILTER_INTERVALS.CUSTOM },
+];
+
+export const FilterUpdateModal = () => {
+ const PREDEFINED_INTERVALS: number[] = [
+ FILTER_INTERVALS.DISABLE,
+ FILTER_INTERVALS.HOURLY,
+ FILTER_INTERVALS.DAILY,
+ FILTER_INTERVALS.WEEKLY,
+ ];
+
+ const isCustom = createMemo(() => {
+ const currentInterval = filteringState.interval;
+ return currentInterval != null && !PREDEFINED_INTERVALS.includes(currentInterval);
+ });
+
+ const [interval, setIntervalValue] = createSignal(
+ untrack(() => (isCustom() ? FILTER_INTERVALS.CUSTOM : (filteringState.interval ?? 24))),
+ );
+ const [customInterval, setCustomInterval] = createSignal(
+ untrack(() => (isCustom() ? filteringState.interval : null)),
+ );
+
+ createEffect(() => {
+ const currentInterval = filteringState.interval;
+ const custom = currentInterval != null && !PREDEFINED_INTERVALS.includes(currentInterval);
+ setIntervalValue(custom ? FILTER_INTERVALS.CUSTOM : (currentInterval ?? 24));
+ setCustomInterval(custom ? currentInterval : null);
+ });
+
+ const intervalValue = createMemo(() => interval());
+
+ const onClose = () => {
+ const currentInterval = filteringState.interval;
+ const custom = currentInterval != null && !PREDEFINED_INTERVALS.includes(currentInterval);
+ setIntervalValue(custom ? FILTER_INTERVALS.CUSTOM : (currentInterval ?? 24));
+ setCustomInterval(custom ? currentInterval : null);
+ closeModal();
+ };
+
+ const onSubmit = (e: Event) => {
+ e.preventDefault();
+ const finalInterval =
+ interval() === FILTER_INTERVALS.CUSTOM ? customInterval() : interval();
+
+ if (finalInterval === null || finalInterval === undefined || finalInterval < 0) {
+ return;
+ }
+
+ if (interval() === FILTER_INTERVALS.CUSTOM) {
+ if (
+ finalInterval < FILTER_INTERVAL_RANGE.MIN ||
+ finalInterval > FILTER_INTERVAL_RANGE.MAX
+ ) {
+ return;
+ }
+ }
+
+ setFiltersConfig({
+ enabled: filteringState.enabled,
+ interval: finalInterval,
+ });
+ onClose();
+ };
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/FilterLists/blocks/FilterUpdateModal/index.ts b/client_v2/src/components/FilterLists/blocks/FilterUpdateModal/index.ts
new file mode 100644
index 00000000000..08853a12b13
--- /dev/null
+++ b/client_v2/src/components/FilterLists/blocks/FilterUpdateModal/index.ts
@@ -0,0 +1 @@
+export { FilterUpdateModal } from './FilterUpdateModal';
diff --git a/client_v2/src/components/FilterLists/blocks/ListsTable/ListsTable.module.pcss b/client_v2/src/components/FilterLists/blocks/ListsTable/ListsTable.module.pcss
new file mode 100644
index 00000000000..7db45b3e730
--- /dev/null
+++ b/client_v2/src/components/FilterLists/blocks/ListsTable/ListsTable.module.pcss
@@ -0,0 +1,102 @@
+.headerCell {
+ @media (min-width: 768px) {
+ width: 100%;
+ min-height: 42px;
+ padding: 0 12px;
+ }
+}
+
+.copyButton {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: 0;
+ outline: 0;
+ cursor: pointer;
+ background: none;
+ padding: 0;
+ flex-shrink: 0;
+
+ @media (min-width: 768px) {
+ display: none;
+ }
+}
+
+.emptyTableContent {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+}
+
+.emptyTableIcon {
+ width: 64px;
+ height: 64px;
+}
+
+.emptyTableDesc {
+ color: var(--default-description-text);
+}
+
+.cellNameToggleOuter {
+ @media (max-width: 767px) {
+ width: 100% !important;
+ min-width: 100%;
+ }
+}
+
+.cellValueToggle {
+ width: auto;
+ flex-shrink: 0;
+}
+
+.cellNameLabel {
+ flex: 1;
+ min-width: 0;
+ text-align: left;
+ word-break: break-word;
+ overflow: hidden;
+
+ @media (min-width: 768px) {
+ display: none;
+ }
+}
+
+.nameDesktopOnly {
+ @media (max-width: 767px) {
+ display: none;
+ }
+}
+
+.tableRewrites {
+ @media (min-width: 768px) {
+ grid-template-columns: 3fr 5fr 80px;
+ }
+}
+
+.domainCellValue {
+ justify-content: space-between;
+ align-items: center;
+ flex: 1;
+ white-space: normal;
+
+ @media (min-width: 768px) {
+ flex-direction: row-reverse;
+ justify-content: flex-start;
+ gap: 8px;
+ flex: initial;
+ white-space: nowrap;
+ }
+}
+
+.sortDropdownMobile {
+ padding: 0 0 16px;
+
+ @media (min-width: 768px) {
+ display: none;
+ }
+
+ &.sortDropdownMobileRewrites {
+ padding-bottom: 24px;
+ }
+}
diff --git a/client_v2/src/components/FilterLists/blocks/ListsTable/ListsTable.tsx b/client_v2/src/components/FilterLists/blocks/ListsTable/ListsTable.tsx
new file mode 100644
index 00000000000..61dcc8080c2
--- /dev/null
+++ b/client_v2/src/components/FilterLists/blocks/ListsTable/ListsTable.tsx
@@ -0,0 +1,313 @@
+import { createSignal, createMemo } from 'solid-js';
+
+import type { Filter } from 'panel/helpers/helpers';
+import { formatShortDateTime } from 'panel/helpers/helpers';
+import intl from 'panel/common/intl';
+import { LOCAL_STORAGE_KEYS, LocalStorageHelper } from 'panel/helpers/localStorageHelper';
+import { Table, type TableColumn } from 'panel/common/ui/Table';
+import { Switch } from 'panel/common/controls/Switch';
+import { Icon } from 'panel/common/ui/Icon';
+import { SortSelect } from 'panel/common/ui/SortSelect';
+import theme from 'panel/lib/theme';
+
+import cn from 'clsx';
+import s from './ListsTable.module.pcss';
+
+export const TABLE_IDS = {
+ ALLOWLISTS_TABLE: 'allowlists_table',
+ BLOCKLISTS_TABLE: 'blocklists_table',
+ DNSREWRITES_TABLE: 'dnsrewrites_table',
+} as const;
+
+type TableIdsType = (typeof TABLE_IDS)[keyof typeof TABLE_IDS];
+
+type FilterToggleData = {
+ name: string;
+ url: string;
+ enabled: boolean;
+};
+
+type Props = {
+ tableId: TableIdsType;
+ filters: Filter[];
+ processingConfigFilter: boolean;
+ toggleFilterList: (url: string, data: FilterToggleData) => void;
+ addFilterList: () => void;
+ editFilterList: (url: string, name: string, enabled: boolean) => void;
+ deleteFilterList: (url: string, name: string) => void;
+};
+
+export const ListsTable = (props: Props) => {
+ const [sortDirection, setSortDirection] = createSignal<'asc' | 'desc'>('asc');
+
+ const pageSize = createMemo(
+ () => LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.BLOCKLIST_PAGE_SIZE) || undefined,
+ );
+
+ const sortedFilters = createMemo(() => {
+ const items = [...(props.filters || [])];
+
+ const direction = sortDirection();
+ items.sort((a, b) => {
+ const aName = (a.name || '').toLowerCase();
+ const bName = (b.name || '').toLowerCase();
+
+ if (aName < bName) {
+ return direction === 'asc' ? -1 : 1;
+ }
+
+ if (aName > bName) {
+ return direction === 'asc' ? 1 : -1;
+ }
+
+ return 0;
+ });
+
+ return items;
+ });
+
+ const handleToggle = (url: string, name: string, enabled: boolean) => {
+ props.toggleFilterList(url, { name, url, enabled: !enabled });
+ };
+
+ const handleEdit = (url: string, name: string, enabled: boolean) => {
+ props.editFilterList(url, name, enabled);
+ };
+
+ const handleDelete = (url: string, name: string) => {
+ props.deleteFilterList(url, name);
+ };
+
+ const columns = createMemo[]>(() => [
+ {
+ key: 'enabled',
+ header: {
+ text: '',
+ className: s.headerCell,
+ },
+ accessor: 'enabled',
+ sortable: false,
+ width: 64,
+ className: s.cellNameToggleOuter,
+ render: (value: boolean, row: Filter) => {
+ const { name, url, enabled } = row;
+ const id = `filter_${url}`;
+
+ return (
+
+
{name}
+
+
+ handleToggle(url, name, enabled)}
+ disabled={props.processingConfigFilter}
+ />
+
+
+ );
+ },
+ },
+ {
+ key: 'name',
+ header: {
+ text: intl.getMessage('name_label'),
+ className: s.headerCell,
+ },
+ accessor: 'name',
+ sortable: true,
+ className: s.nameDesktopOnly,
+ render: (value: string) => (
+
+
{intl.getMessage('name_label')}
+
+
+ {value}
+
+
+ ),
+ },
+ {
+ key: 'url',
+ header: {
+ text: intl.getMessage('url_label'),
+ className: s.headerCell,
+ },
+ accessor: 'url',
+ sortable: true,
+ render: (value: string) => (
+
+
{intl.getMessage('url_label')}
+
+
+
+ {value}
+
+
+
navigator.clipboard.writeText(value)}
+ aria-label={intl.getMessage('copy')}
+ >
+
+
+
+
+ ),
+ },
+ {
+ key: 'rulesCount',
+ header: {
+ text: intl.getMessage('rules_label'),
+ className: s.headerCell,
+ },
+ accessor: 'rulesCount',
+ sortable: true,
+ render: (value: number) => (
+
+
{intl.getMessage('rules_label')}
+
+
+ {value?.toLocaleString() || 0}
+
+
+ ),
+ },
+ {
+ key: 'lastUpdated',
+ header: {
+ text: intl.getMessage('last_updated_label'),
+ className: s.headerCell,
+ },
+ accessor: 'lastUpdated',
+ sortable: true,
+ render: (value: string) => {
+ const result = formatShortDateTime(value);
+
+ return (
+
+
+ {intl.getMessage('last_updated_label')}
+
+
+
+ {result}
+
+
+ );
+ },
+ },
+ {
+ key: 'actions',
+ header: {
+ text: '',
+ className: s.headerCell,
+ },
+ accessor: 'url',
+ sortable: false,
+ width: 80,
+ render: (value: string, row: Filter) => {
+ const { name, url, enabled } = row;
+
+ return (
+
+
+
+ handleEdit(url, name, enabled)}
+ disabled={props.processingConfigFilter}
+ class={theme.table.action}
+ title={intl.getMessage('edit_table_action')}
+ aria-label={intl.getMessage('edit_table_action')}
+ data-table-action
+ >
+
+
+ {intl.getMessage('edit_table_action')}
+
+
+
+ handleDelete(url, name)}
+ disabled={props.processingConfigFilter}
+ class={cn(theme.table.action, theme.table.action_danger)}
+ title={intl.getMessage('delete_table_action')}
+ aria-label={intl.getMessage('delete_table_action')}
+ data-table-action
+ >
+
+
+ {intl.getMessage('delete_table_action')}
+
+
+
+
+
+ );
+ },
+ },
+ ]);
+
+ const handlePageSizeChange = (newSize: number) => {
+ LocalStorageHelper.setItem(LOCAL_STORAGE_KEYS.BLOCKLIST_PAGE_SIZE, newSize);
+ };
+
+ const emptyTableContent = (id: TableIdsType) => {
+ const renderButton = (text: string) => {
+ return (
+ props.addFilterList()}
+ >
+ {text}
+
+ );
+ };
+
+ const DESC_TEXT: Record = {
+ [TABLE_IDS.ALLOWLISTS_TABLE]: intl.getMessage('allowlist_empty', {
+ button: (text: string) => renderButton(text),
+ }),
+ [TABLE_IDS.BLOCKLISTS_TABLE]: intl.getMessage('blocklists_empty', {
+ button: (text: string) => renderButton(text),
+ }),
+ };
+
+ const emptyText = DESC_TEXT[id];
+
+ return (
+
+ );
+ };
+
+ return (
+ <>
+
+
+
+ data={sortedFilters()}
+ class={s.table}
+ columns={columns()}
+ emptyTable={emptyTableContent(props.tableId)}
+ pageSize={pageSize()}
+ onPageSizeChange={handlePageSizeChange}
+ />
+ >
+ );
+};
diff --git a/client_v2/src/components/FilterLists/blocks/ListsTable/index.ts b/client_v2/src/components/FilterLists/blocks/ListsTable/index.ts
new file mode 100644
index 00000000000..df054949ec9
--- /dev/null
+++ b/client_v2/src/components/FilterLists/blocks/ListsTable/index.ts
@@ -0,0 +1 @@
+export { ListsTable } from './ListsTable';
diff --git a/client_v2/src/components/FilterLists/blocks/RewritesTable/RewritesTable.tsx b/client_v2/src/components/FilterLists/blocks/RewritesTable/RewritesTable.tsx
new file mode 100644
index 00000000000..32a10047575
--- /dev/null
+++ b/client_v2/src/components/FilterLists/blocks/RewritesTable/RewritesTable.tsx
@@ -0,0 +1,216 @@
+import { createSignal, createMemo } from 'solid-js';
+import cn from 'clsx';
+
+import intl from 'panel/common/intl';
+import { LOCAL_STORAGE_KEYS, LocalStorageHelper } from 'panel/helpers/localStorageHelper';
+import { Table, type TableColumn } from 'panel/common/ui/Table';
+import { Icon } from 'panel/common/ui/Icon';
+import { SortSelect } from 'panel/common/ui/SortSelect';
+import theme from 'panel/lib/theme';
+import { Switch } from 'panel/common/controls/Switch';
+
+import type { Rewrite } from '../../DNSRewrites';
+import s from '../ListsTable/ListsTable.module.pcss';
+
+type Props = {
+ list: Rewrite[];
+ processing: boolean;
+ processingAdd: boolean;
+ processingDelete: boolean;
+ processingUpdate: boolean;
+ addRewritesList: () => void;
+ deleteRewrite: (rewrite: Rewrite) => void;
+ editRewrite: (rewrite: Rewrite) => void;
+ toggleRewrite: (rewrite: Rewrite) => void;
+};
+
+export const RewritesTable = (props: Props) => {
+ const [sortDirection, setSortDirection] = createSignal<'asc' | 'desc'>('asc');
+
+ const pageSize = createMemo(
+ () => LocalStorageHelper.getItem(LOCAL_STORAGE_KEYS.BLOCKLIST_PAGE_SIZE) || undefined,
+ );
+
+ const sortedList = createMemo(() => {
+ const items = [...(props.list || [])];
+
+ const direction = sortDirection();
+ items.sort((a, b) => {
+ const aDomain = a.domain.toLowerCase();
+ const bDomain = b.domain.toLowerCase();
+
+ if (aDomain < bDomain) {
+ return direction === 'asc' ? -1 : 1;
+ }
+
+ if (aDomain > bDomain) {
+ return direction === 'asc' ? 1 : -1;
+ }
+
+ return 0;
+ });
+
+ return items;
+ });
+
+ const handleEdit = (rewrite: Rewrite) => {
+ props.editRewrite(rewrite);
+ };
+
+ const handleDelete = (rewrite: Rewrite) => {
+ props.deleteRewrite(rewrite);
+ };
+
+ const columns = createMemo[]>(() => [
+ {
+ key: 'domain',
+ header: {
+ text: intl.getMessage('domain'),
+ className: s.headerCell,
+ },
+ accessor: 'domain',
+ sortable: true,
+ render: (value: string, row: Rewrite) => {
+ const { domain, enabled } = row;
+ const id = `rewrite_${domain}`;
+
+ return (
+
+
+ {value}
+ props.toggleRewrite(row)}
+ disabled={props.processingUpdate}
+ />
+
+
+ );
+ },
+ },
+ {
+ key: 'answer',
+ header: {
+ text: intl.getMessage('result'),
+ className: s.headerCell,
+ },
+ accessor: 'answer',
+ sortable: true,
+ render: (value: string) => (
+
+ ),
+ },
+ {
+ key: 'actions',
+ header: {
+ text: '',
+ className: s.headerCell,
+ },
+ sortable: false,
+ width: 80,
+ render: (value: any, row: Rewrite) => {
+ const currentRewrite = {
+ answer: row.answer,
+ domain: row.domain,
+ enabled: row.enabled,
+ };
+
+ return (
+
+
+
+ handleEdit(currentRewrite)}
+ disabled={props.processingUpdate}
+ class={theme.table.action}
+ title={intl.getMessage('edit_table_action')}
+ aria-label={intl.getMessage('edit_table_action')}
+ data-testid={`edit-rewrite-${row.domain}`}
+ data-table-action
+ >
+
+
+ {intl.getMessage('edit_table_action')}
+
+
+
+ handleDelete(currentRewrite)}
+ disabled={props.processingDelete}
+ class={cn(theme.table.action, theme.table.action_danger)}
+ title={intl.getMessage('delete_table_action')}
+ aria-label={intl.getMessage('delete_table_action')}
+ data-testid={`delete-rewrite-${row.domain}`}
+ data-table-action
+ >
+
+
+ {intl.getMessage('delete_table_action')}
+
+
+
+
+
+ );
+ },
+ },
+ ]);
+
+ const handlePageSizeChange = (newSize: number) => {
+ LocalStorageHelper.setItem(LOCAL_STORAGE_KEYS.BLOCKLIST_PAGE_SIZE, newSize);
+ };
+
+ const emptyTableContent = () => {
+ return (
+
+
+
+
+ {intl.getMessage('rewrites_empty', {
+ button: (text: string) => (
+ props.addRewritesList()}
+ >
+ {text}
+
+ ),
+ })}
+
+
+ );
+ };
+
+ return (
+ <>
+
+
+
+
+
+ data={sortedList()}
+ class={s.tableRewrites}
+ columns={columns()}
+ emptyTable={emptyTableContent()}
+ loading={props.processing || props.processingAdd || props.processingDelete}
+ pageSize={pageSize()}
+ onPageSizeChange={handlePageSizeChange}
+ />
+ >
+ );
+};
diff --git a/client_v2/src/components/FilterLists/blocks/RewritesTable/index.ts b/client_v2/src/components/FilterLists/blocks/RewritesTable/index.ts
new file mode 100644
index 00000000000..e4df0c80abd
--- /dev/null
+++ b/client_v2/src/components/FilterLists/blocks/RewritesTable/index.ts
@@ -0,0 +1 @@
+export { RewritesTable } from './RewritesTable';
diff --git a/client_v2/src/components/FilterLists/index.ts b/client_v2/src/components/FilterLists/index.ts
new file mode 100644
index 00000000000..6bddf443ab0
--- /dev/null
+++ b/client_v2/src/components/FilterLists/index.ts
@@ -0,0 +1,3 @@
+export { Allowlists } from './Allowlists';
+export { Blocklists } from './Blocklists';
+export { DNSRewrites } from './DNSRewrites';
diff --git a/client_v2/src/components/QueryLog/QueryLog.module.pcss b/client_v2/src/components/QueryLog/QueryLog.module.pcss
new file mode 100644
index 00000000000..67985f2bc27
--- /dev/null
+++ b/client_v2/src/components/QueryLog/QueryLog.module.pcss
@@ -0,0 +1,57 @@
+.page {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+
+ @media (min-width: 768px) {
+ gap: 32px;
+ }
+}
+
+.desktopView {
+ display: none;
+
+ @media (min-width: 768px) {
+ display: block;
+ padding: 0 16px;
+ }
+
+ @media (min-width: 1024px) {
+ padding: 0;
+ }
+}
+
+.mobileView {
+ display: block;
+ padding: 0 16px;
+
+ @media (min-width: 768px) {
+ display: none;
+ }
+}
+
+.mobileList {
+ display: flex;
+ flex-direction: column;
+}
+
+.mobileLoader {
+ padding: 12px 0 0;
+}
+
+.mobileInitialLoader {
+ display: flex;
+ justify-content: center;
+ padding: 24px 0;
+}
+
+.emptyState {
+ min-height: 120px;
+ padding: 24px 16px;
+}
+
+.loader {
+ width: 48px;
+ height: 48px;
+}
diff --git a/client_v2/src/components/QueryLog/QueryLog.tsx b/client_v2/src/components/QueryLog/QueryLog.tsx
new file mode 100644
index 00000000000..d9d47711820
--- /dev/null
+++ b/client_v2/src/components/QueryLog/QueryLog.tsx
@@ -0,0 +1,342 @@
+import { createSignal, createEffect, onMount, Show, For } from 'solid-js';
+import cn from 'clsx';
+import { useNavigate, useLocation } from '@solidjs/router';
+
+import { Loader } from 'panel/common/ui/Loader';
+import theme from 'panel/lib/theme';
+import {
+ queryLogsState,
+ getLogsConfig,
+ setLogsFilter,
+ setFilteredLogs,
+ refreshFilteredLogs,
+ getAdditionalLogs,
+} from 'panel/stores/queryLogs';
+import { accessState, getAccessList, toggleClientBlock } from 'panel/stores/access';
+import { dashboardState, getClients } from 'panel/stores/dashboard';
+import {
+ filteringState,
+ getFilteringStatus,
+ blockDomain,
+ unblockDomain,
+ blockDomainForClient,
+} from 'panel/stores/filtering';
+import { servicesState, getAllBlockedServices, allowBlockedService } from 'panel/stores/services';
+import {
+ DEFAULT_LOGS_FILTER,
+ QUERY_LOG_REASON_FILTER_QUERIES,
+ QUERY_LOG_STATUS_FILTER_QUERIES,
+} from 'panel/helpers/constants';
+import { getLogsUrlParams } from 'panel/helpers/helpers';
+import { RoutePath, linkPathBuilder } from 'panel/components/Routes/Paths';
+
+import { filterLogsByStatus } from './helpers';
+import { LogEntry } from './types';
+import { Header } from './blocks/Header';
+import { EmptyState, type EmptyStateMode } from './blocks/EmptyState/EmptyState';
+import { LogTable } from './blocks/LogTable';
+import { LogCard } from './blocks/LogCard';
+import { DetailModal } from './blocks/DetailModal';
+import { DisallowDialog } from './blocks/DisallowDialog';
+import { InfiniteScrollTrigger } from './blocks/InfiniteScrollTrigger';
+
+import s from './QueryLog.module.pcss';
+
+const getEmptyStateMode = (enabled: boolean, interval?: number): EmptyStateMode => {
+ if (!enabled) {
+ return 'disabled';
+ }
+ if (interval === 0) {
+ return 'rotation-disabled';
+ }
+ return 'default';
+};
+
+export const QueryLog = () => {
+ const navigate = useNavigate();
+ const location = useLocation();
+
+ const [selectedEntry, setSelectedEntry] = createSignal(null);
+ const [disallowTarget, setDisallowTarget] = createSignal(null);
+ const [isIncrementalLoad, setIsIncrementalLoad] = createSignal(false);
+
+ onMount(() => {
+ getLogsConfig();
+ getAccessList();
+ getClients();
+ getFilteringStatus();
+ getAllBlockedServices();
+ });
+
+ // Watch location.search for filter changes
+ createEffect(() => {
+ const searchParams = new URLSearchParams(location.search);
+ const search = searchParams.get('search') || DEFAULT_LOGS_FILTER.search;
+ const statusParam = searchParams.get('status') || DEFAULT_LOGS_FILTER.status;
+ const reasonParam = searchParams.get('reason') || DEFAULT_LOGS_FILTER.reason;
+ const status = Object.hasOwn(QUERY_LOG_STATUS_FILTER_QUERIES, statusParam)
+ ? statusParam
+ : DEFAULT_LOGS_FILTER.status;
+ const reason = Object.hasOwn(QUERY_LOG_REASON_FILTER_QUERIES, reasonParam)
+ ? reasonParam
+ : DEFAULT_LOGS_FILTER.reason;
+ const nextFilter = { search, status, reason };
+ const nextSearch = getLogsUrlParams(search, status, reason);
+
+ if (location.search !== nextSearch) {
+ navigate(nextSearch, { replace: true });
+ return;
+ }
+
+ setIsIncrementalLoad(false);
+ setLogsFilter(nextFilter);
+ setFilteredLogs(nextFilter);
+ });
+
+ // Reset incremental load when processing finishes
+ createEffect(() => {
+ if (!queryLogsState.processingGetLogs && !queryLogsState.processingAdditionalLogs) {
+ setIsIncrementalLoad(false);
+ }
+ });
+
+ const currentSearch = () => queryLogsState.filter?.search ?? DEFAULT_LOGS_FILTER.search;
+ const currentStatus = () => queryLogsState.filter?.status ?? DEFAULT_LOGS_FILTER.status;
+ const currentReason = () => queryLogsState.filter?.reason ?? DEFAULT_LOGS_FILTER.reason;
+ const infiniteScrollResetToken = () =>
+ `${currentSearch()}:${currentStatus()}:${currentReason()}`;
+ const persistentClientIds = () =>
+ (dashboardState.clients || []).flatMap(
+ (persistentClient: any) => persistentClient.ids ?? [],
+ );
+ const visibleLogs = () => filterLogsByStatus(queryLogsState.logs || [], currentStatus());
+ const emptyStateMode = () => getEmptyStateMode(queryLogsState.enabled, queryLogsState.interval);
+ const hasMore = () => !queryLogsState.isEntireLog;
+ const logs = () => queryLogsState.logs || [];
+
+ const isRequestInFlight = () =>
+ queryLogsState.processingGetLogs || queryLogsState.processingAdditionalLogs;
+ const isLoadingMore = () => isIncrementalLoad() && isRequestInFlight();
+ const isInitialLoading = () =>
+ queryLogsState.processingGetLogs && logs().length === 0 && !isIncrementalLoad();
+ const isFilterReloading = () =>
+ queryLogsState.processingGetLogs && !isInitialLoading() && !isIncrementalLoad();
+
+ const handleSearch = (search: string) => {
+ setIsIncrementalLoad(false);
+ navigate(getLogsUrlParams(search.trim(), currentStatus(), currentReason()), {
+ replace: true,
+ });
+ };
+
+ const handleStatusFilterChange = (status: string) => {
+ setIsIncrementalLoad(false);
+ navigate(getLogsUrlParams(currentSearch(), status, DEFAULT_LOGS_FILTER.reason), {
+ replace: true,
+ });
+ };
+
+ const handleReasonFilterChange = (reason: string) => {
+ setIsIncrementalLoad(false);
+ navigate(getLogsUrlParams(currentSearch(), currentStatus(), reason), { replace: true });
+ };
+
+ const handleRefresh = () => {
+ setIsIncrementalLoad(false);
+ refreshFilteredLogs();
+ };
+
+ const handleBlockDomain = (domain: string) => {
+ blockDomain(domain);
+ };
+
+ const handleUnblockDomain = (domain: string) => {
+ unblockDomain(domain);
+ };
+
+ const handleAllowService = (serviceId: string) => {
+ allowBlockedService(serviceId);
+ };
+
+ const handleBlockClient = (domain: string, client: string) => {
+ blockDomainForClient(domain, client);
+ };
+
+ const handleDisallowClient = (ip: string) => {
+ setDisallowTarget(ip);
+ };
+
+ const handleAddPersistentClient = (clientId: string) => {
+ navigate(linkPathBuilder(RoutePath.ClientsAdd, undefined, { id: clientId }));
+ };
+
+ const handleConfirmDisallow = () => {
+ const target = disallowTarget();
+ if (target) {
+ const disallowedList = accessState.disallowed_clients
+ ? accessState.disallowed_clients.split('\n').filter(Boolean)
+ : [];
+ const isDisallowed = disallowedList.includes(target);
+ toggleClientBlock(target, isDisallowed, isDisallowed ? target : '');
+ setDisallowTarget(null);
+ }
+ };
+
+ const handleCloseDisallow = () => {
+ setDisallowTarget(null);
+ };
+
+ const handleRowClick = (entry: LogEntry) => {
+ setSelectedEntry(entry);
+ };
+
+ const handleCloseDetail = () => {
+ setSelectedEntry(null);
+ };
+
+ const handleLoadMore = () => {
+ if (isRequestInFlight() || queryLogsState.isEntireLog) {
+ return;
+ }
+ setIsIncrementalLoad(true);
+ getAdditionalLogs();
+ };
+
+ const allowedClients = (): string[] => {
+ const raw = accessState?.allowed_clients;
+ if (!raw) {
+ return [];
+ }
+ if (typeof raw === 'string') {
+ return raw.split('\n').filter(Boolean);
+ }
+ if (Array.isArray(raw)) {
+ return raw as string[];
+ }
+ return [];
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {(entry) => (
+
+ )}
+
+
+
+
+ >
+ }
+ >
+
+
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+
+ 0}
+ onConfirm={handleConfirmDisallow}
+ onClose={handleCloseDisallow}
+ />
+
+
+
+ );
+};
diff --git a/client_v2/src/components/QueryLog/blocks/ActionsMenu/ActionsMenu.module.pcss b/client_v2/src/components/QueryLog/blocks/ActionsMenu/ActionsMenu.module.pcss
new file mode 100644
index 00000000000..6876891dc05
--- /dev/null
+++ b/client_v2/src/components/QueryLog/blocks/ActionsMenu/ActionsMenu.module.pcss
@@ -0,0 +1,64 @@
+.trigger {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 32px;
+ height: 32px;
+ border: none;
+ background: none;
+ border-radius: 6px;
+ cursor: pointer;
+ color: var(--default-description-text);
+ transition: background-color 0.15s;
+}
+
+.overlay {
+ min-width: 180px;
+}
+
+.menu {
+ list-style: none;
+ margin: 0;
+ padding: 4px 0;
+ background: var(--default-dropdown-menu-background);
+ border-radius: 8px;
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
+}
+
+.menuItem {
+ padding: 16px;
+ cursor: pointer;
+ color: var(--default-main-text);
+ transition: background-color var(--t2);
+
+ &:hover,
+ &:focus {
+ background-color: var(--hovered-dropdown-menu-background);
+ }
+
+ &:active {
+ background-color: var(--pressed-dropdown-menu-background);
+ }
+}
+
+.menuButton {
+ display: block;
+ width: 100%;
+ border: none;
+ background: none;
+ text-align: left;
+ font: inherit;
+}
+
+.actionsWrapper {
+ display: flex;
+ align-items: center;
+}
+
+.statusRed {
+ color: var(--default-error-link);
+}
+
+.statusGreen {
+ color: var(--default-link);
+}
diff --git a/client_v2/src/components/QueryLog/blocks/ActionsMenu/ActionsMenu.tsx b/client_v2/src/components/QueryLog/blocks/ActionsMenu/ActionsMenu.tsx
new file mode 100644
index 00000000000..4c84e336497
--- /dev/null
+++ b/client_v2/src/components/QueryLog/blocks/ActionsMenu/ActionsMenu.tsx
@@ -0,0 +1,133 @@
+import { Show, createSignal } from 'solid-js';
+import cn from 'clsx';
+
+import intl from 'panel/common/intl';
+import { Dropdown } from 'panel/common/ui/Dropdown';
+import { Icon } from 'panel/common/ui/Icon';
+
+import theme from 'panel/lib/theme';
+import s from './ActionsMenu.module.pcss';
+
+type Props = {
+ domain: string;
+ client: string;
+ clientId?: string;
+ onBlock: (domain: string) => void;
+ onUnblock: (domain: string) => void;
+ onBlockClient: (domain: string, client: string) => void;
+ onDisallowClient: () => void;
+ onAddPersistentClient?: (clientId: string) => void;
+ isBlocked: boolean;
+ showAddPersistentClient?: boolean;
+ testIdPrefix?: string;
+};
+
+export const ActionsMenu = (props: Props) => {
+ const [open, setOpen] = createSignal(false);
+
+ const handleBlock = () => {
+ if (props.isBlocked) {
+ props.onUnblock(props.domain);
+ } else {
+ props.onBlock(props.domain);
+ }
+ setOpen(false);
+ };
+
+ const handleBlockClient = () => {
+ props.onBlockClient(props.domain, props.client);
+ setOpen(false);
+ };
+
+ const handleDisallowClient = () => {
+ props.onDisallowClient();
+ setOpen(false);
+ };
+
+ const handleAddPersistentClient = () => {
+ const nextClientId = props.clientId || props.client;
+ if (props.onAddPersistentClient && nextClientId) {
+ props.onAddPersistentClient(nextClientId);
+ }
+ setOpen(false);
+ };
+
+ const menu = (
+
+ );
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/QueryLog/blocks/ActionsMenu/index.ts b/client_v2/src/components/QueryLog/blocks/ActionsMenu/index.ts
new file mode 100644
index 00000000000..3f790ceca21
--- /dev/null
+++ b/client_v2/src/components/QueryLog/blocks/ActionsMenu/index.ts
@@ -0,0 +1 @@
+export { ActionsMenu } from './ActionsMenu';
diff --git a/client_v2/src/components/QueryLog/blocks/DetailModal/DetailModal.module.pcss b/client_v2/src/components/QueryLog/blocks/DetailModal/DetailModal.module.pcss
new file mode 100644
index 00000000000..aca794b47ff
--- /dev/null
+++ b/client_v2/src/components/QueryLog/blocks/DetailModal/DetailModal.module.pcss
@@ -0,0 +1,207 @@
+.wrap {
+ top: 16px;
+
+ @media (min-width: 1024px) {
+ top: 0;
+ }
+}
+
+.dialog {
+ width: min(720px, calc(100vw - 16px));
+
+ @media (min-width: 1024px) {
+ width: min(90vw, 600px);
+ }
+}
+
+.content {
+ display: flex;
+ flex-direction: column;
+ flex: 1 1 auto;
+ min-height: 0;
+ padding: 0;
+
+ @media (min-width: 1024px) and (min-height: 900px) {
+ flex: 0 1 auto;
+ }
+}
+
+.scrollArea {
+ flex: 1 1 auto;
+ min-height: 0;
+ overflow-y: auto;
+ overscroll-behavior: contain;
+ padding-bottom: 16px;
+ -webkit-overflow-scrolling: touch;
+
+ @media (min-width: 1024px) and (min-height: 900px) {
+ flex: 0 1 auto;
+ overflow-y: visible;
+ overscroll-behavior: auto;
+ padding-bottom: 0;
+ -webkit-overflow-scrolling: auto;
+ }
+}
+
+.section {
+ padding: 0 32px;
+
+ @media (min-width: 1024px) and (min-height: 900px) {
+ padding: 0;
+ }
+}
+
+.statusValue {
+ display: inline;
+}
+
+.detailHint {
+ color: var(--default-description-text);
+}
+
+.sectionTitle {
+ padding: 16px 0 8px;
+
+ @media (min-width: 1024px) and (min-height: 900px) {
+ padding: 16px 16px 8px 16px;
+ }
+}
+
+.row {
+ display: flex;
+ align-items: flex-start;
+ flex-wrap: wrap;
+ gap: 4px;
+ padding: 8px 0;
+ font-weight: var(--weight-semi-bold);
+
+ @media (min-width: 1024px) and (min-height: 900px) {
+ padding: 8px 16px;
+ }
+}
+
+.row strong,
+.label {
+ font-weight: var(--weight-semi-bold);
+}
+
+.value {
+ min-width: 0;
+ word-break: break-all;
+ font-weight: var(--weight-regular);
+}
+
+.responseList {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+
+ &:has(> :nth-child(2)) {
+ flex-basis: 100%;
+ }
+}
+
+.link {
+ color: var(--default-link);
+}
+
+.actionFooter {
+ display: flex;
+ flex-wrap: wrap;
+ flex-shrink: 0;
+ gap: 16px;
+ padding: 16px 32px 24px;
+ background: var(--default-page-background);
+ border-radius: 8px 8px 16px 16px;
+ box-shadow: 0 -8px 16px 0 rgba(0, 0, 0, 0.1);
+ position: relative;
+ z-index: 1;
+
+ @media (min-width: 1024px) and (min-height: 900px) {
+ flex-shrink: 1;
+ padding: 16px;
+ background: transparent;
+ border-radius: 0;
+ box-shadow: none;
+ position: static;
+ z-index: auto;
+ }
+}
+
+.actionButton {
+ flex: 1 1 200px;
+ min-width: 0;
+
+ @media (min-width: 1024px) {
+ flex: 0 0 auto;
+ width: auto;
+ min-width: 200px;
+ max-width: none;
+ }
+}
+
+.dialog :global(.dialog-header) {
+ padding: 24px 56px 24px 32px;
+ box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.1);
+ background: var(--default-page-background);
+ border-radius: 16px 16px 8px 8px;
+ flex-shrink: 0;
+ position: relative;
+ z-index: 1;
+
+ @media (min-width: 1024px) and (min-height: 900px) {
+ padding: 8px 48px 8px 16px;
+ box-shadow: none;
+ background: transparent;
+ border-radius: 0;
+ flex-shrink: 1;
+ position: static;
+ z-index: auto;
+ }
+}
+
+.dialog :global([data-part='title']) {
+ font-size: 24px;
+ line-height: 1.2;
+}
+
+.dialog :global([data-part='close-trigger']) {
+ right: 24px;
+ top: 24px;
+ z-index: 2;
+
+ @media (min-width: 1024px) and (min-height: 900px) {
+ right: 16px;
+ top: 16px;
+ }
+}
+
+/* The content card carries .dialog itself — compound selector, not descendant */
+.dialog:global([data-part='content']) {
+ display: flex;
+ flex-direction: column;
+ max-height: min(600px, calc(100vh - 32px));
+ padding: 0;
+ overflow: hidden;
+
+ @media (min-width: 1024px) and (min-height: 900px) {
+ display: block;
+ max-height: none;
+ padding: 24px 16px;
+ overflow: visible;
+ }
+}
+
+/* The body is a descendant */
+.dialog :global(.dialog-body) {
+ display: flex;
+ flex: 1 1 auto;
+ min-height: 0;
+ padding: 0;
+
+ @media (min-width: 1024px) and (min-height: 900px) {
+ display: block;
+ flex: 0 1 auto;
+ min-height: auto;
+ }
+}
diff --git a/client_v2/src/components/QueryLog/blocks/DetailModal/DetailModal.tsx b/client_v2/src/components/QueryLog/blocks/DetailModal/DetailModal.tsx
new file mode 100644
index 00000000000..e4c52742a90
--- /dev/null
+++ b/client_v2/src/components/QueryLog/blocks/DetailModal/DetailModal.tsx
@@ -0,0 +1,523 @@
+import { Show, For } from 'solid-js';
+import cn from 'clsx';
+
+import intl from 'panel/common/intl';
+import { Button } from 'panel/common/ui/Button';
+import { Dialog } from 'panel/common/ui/Dialog';
+import theme from 'panel/lib/theme';
+
+import {
+ checkBlockedService,
+ formatElapsedMs,
+ getServiceName,
+ type Filter,
+} from 'panel/helpers/helpers';
+import { FILTERED_STATUS } from 'panel/helpers/constants';
+import {
+ getQueryReasonDetails,
+ getQueryReasonLabel,
+ getQueryReasonKey,
+ getQueryStatusLabel,
+ getQueryStatusKey,
+ getStatusClassName,
+ getProtocolName,
+ isBlockedReason,
+ formatLogTimeDetailed,
+ formatLogDate,
+} from '../../helpers';
+import { LogEntry, ResponseEntry, Service } from '../../types';
+
+import s from './DetailModal.module.pcss';
+
+type Props = {
+ entry: LogEntry;
+ filters: Filter[];
+ services: Service[];
+ whitelistFilters: Filter[];
+ onClose: () => void;
+ onBlock: (domain: string) => void;
+ onAddToAllowlist: (domain: string) => void;
+ onAllowService: (serviceId: string) => void;
+};
+
+const formatResponses = (responses: ResponseEntry[] = []) =>
+ responses
+ .map(({ type, value, ttl }) => {
+ if (!value) {
+ return '';
+ }
+
+ const entry = [type, value].filter(Boolean).join(': ');
+
+ if (ttl || ttl === 0) {
+ return `${entry} (ttl=${ttl})`;
+ }
+
+ return entry;
+ })
+ .filter(Boolean);
+
+const hasValue = (value: any) =>
+ value !== undefined && value !== null && value !== '' && value !== false;
+
+export const DetailModal = (props: Props) => {
+ const statusKey = () =>
+ getQueryStatusKey(props.entry.reason, props.entry.originalResponse ?? []);
+ const reasonKey = () => getQueryReasonKey(props.entry.reason, props.entry.rules ?? []);
+ const isBlocked = () => isBlockedReason(props.entry.reason);
+ const isBlockedService = () => checkBlockedService(props.entry.reason);
+ const isSafeSearch = () => props.entry.reason === FILTERED_STATUS.FILTERED_SAFE_SEARCH;
+ const isRewrite = () =>
+ props.entry.reason === FILTERED_STATUS.REWRITE ||
+ props.entry.reason === FILTERED_STATUS.REWRITE_HOSTS ||
+ props.entry.reason === FILTERED_STATUS.REWRITE_RULE;
+ const showBlock = () => !isBlocked() && !isRewrite() && !isSafeSearch();
+ const showAllowlist = () => isBlocked() || isSafeSearch();
+ const reasonDetails = () =>
+ getQueryReasonDetails({
+ elapsedMs: props.entry.elapsedMs,
+ filters: props.filters,
+ reason: props.entry.reason,
+ rules: props.entry.rules ?? [],
+ serviceName: props.entry.service_name || props.entry.serviceName,
+ services: props.services,
+ whitelistFilters: props.whitelistFilters,
+ });
+ const statusClassName = () => getStatusClassName(props.entry.reason);
+ const clientName = () => props.entry.client_info?.name || '';
+ const protocol = () => getProtocolName(props.entry.client_proto);
+ const responseList = () => formatResponses(props.entry.response);
+ const originalResponseList = () => formatResponses(props.entry.originalResponse);
+ const trackerSource = () => props.entry.tracker?.sourceData;
+ const country = () => props.entry.client_info?.whois?.country;
+ const network = () => props.entry.client_info?.whois?.orgname;
+ const serviceId = () => props.entry.serviceName || props.entry.service_name;
+ const serviceName = () =>
+ serviceId() ? getServiceName(props.services, serviceId()!) || serviceId() : '';
+ const reasonValue = () =>
+ reasonDetails()
+ ? `${getQueryReasonLabel(reasonKey())} / ${reasonDetails()}`
+ : getQueryReasonLabel(reasonKey());
+ const rowClassName = () => cn(s.row, theme.text.t3);
+ const labelClassName = () => cn(s.label, theme.text.semibold);
+ const renderValue = (content: any) =>
{content} ;
+ const renderListValue = (content: any) => (
+
{content}
+ );
+ const renderStatusValue = (content: any) => (
+
+ {content}
+
+ );
+
+ const handleBlock = () => {
+ props.onBlock(props.entry.domain);
+ props.onClose();
+ };
+
+ const handleAddToAllowlist = () => {
+ props.onAddToAllowlist(props.entry.domain);
+ props.onClose();
+ };
+
+ const handleAllowService = () => {
+ if (!serviceId()) {
+ return;
+ }
+ props.onAllowService(serviceId()!);
+ props.onClose();
+ };
+
+ return (
+
{intl.getMessage('query_details')}
+ }
+ class={s.dialog}
+ wrapClass={s.wrap}
+ >
+
+
+ );
+};
diff --git a/client_v2/src/components/QueryLog/blocks/DetailModal/index.ts b/client_v2/src/components/QueryLog/blocks/DetailModal/index.ts
new file mode 100644
index 00000000000..013a35e1a23
--- /dev/null
+++ b/client_v2/src/components/QueryLog/blocks/DetailModal/index.ts
@@ -0,0 +1 @@
+export { DetailModal } from './DetailModal';
diff --git a/client_v2/src/components/QueryLog/blocks/DisallowDialog/DisallowDialog.module.pcss b/client_v2/src/components/QueryLog/blocks/DisallowDialog/DisallowDialog.module.pcss
new file mode 100644
index 00000000000..f39edf3d551
--- /dev/null
+++ b/client_v2/src/components/QueryLog/blocks/DisallowDialog/DisallowDialog.module.pcss
@@ -0,0 +1,3 @@
+.note {
+ color: var(--default-attention-link);
+}
diff --git a/client_v2/src/components/QueryLog/blocks/DisallowDialog/DisallowDialog.tsx b/client_v2/src/components/QueryLog/blocks/DisallowDialog/DisallowDialog.tsx
new file mode 100644
index 00000000000..912f7342cb7
--- /dev/null
+++ b/client_v2/src/components/QueryLog/blocks/DisallowDialog/DisallowDialog.tsx
@@ -0,0 +1,40 @@
+import { Show } from 'solid-js';
+
+import intl from 'panel/common/intl';
+import { ConfirmDialog } from 'panel/common/ui/ConfirmDialog';
+
+import s from './DisallowDialog.module.pcss';
+
+type Props = {
+ ip: string;
+ isAllowlistMode: boolean;
+ onConfirm: () => void;
+ onClose: () => void;
+};
+
+export const DisallowDialog = (props: Props) => {
+ return (
+
+ {intl.getMessage('disallow_client_confirm_text', { ip: props.ip })}
+
+
+ {intl.getMessage('disallow_client_confirm_allowlist_note', {
+ ip: props.ip,
+ })}
+
+
+
+ }
+ buttonText={intl.getMessage('yes_disallow')}
+ cancelText={intl.getMessage('cancel')}
+ buttonVariant="danger"
+ submitTestId="query-log-disallow-confirm"
+ cancelTestId="query-log-disallow-cancel"
+ onConfirm={props.onConfirm}
+ onClose={props.onClose}
+ />
+ );
+};
diff --git a/client_v2/src/components/QueryLog/blocks/DisallowDialog/index.ts b/client_v2/src/components/QueryLog/blocks/DisallowDialog/index.ts
new file mode 100644
index 00000000000..bdd07359c2f
--- /dev/null
+++ b/client_v2/src/components/QueryLog/blocks/DisallowDialog/index.ts
@@ -0,0 +1 @@
+export { DisallowDialog } from './DisallowDialog';
diff --git a/client_v2/src/components/QueryLog/blocks/EmptyState/EmptyState.module.pcss b/client_v2/src/components/QueryLog/blocks/EmptyState/EmptyState.module.pcss
new file mode 100644
index 00000000000..4dc7c543172
--- /dev/null
+++ b/client_v2/src/components/QueryLog/blocks/EmptyState/EmptyState.module.pcss
@@ -0,0 +1,25 @@
+.root {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+ text-align: center;
+}
+
+.iconWrap {
+ display: inline-flex;
+}
+
+.icon {
+ width: 64px;
+ height: 64px;
+}
+
+.message {
+ color: var(--default-main-text);
+ overflow-wrap: anywhere;
+}
+
+.enableLinkWrap {
+ margin-top: 16px;
+}
diff --git a/client_v2/src/components/QueryLog/blocks/EmptyState/EmptyState.tsx b/client_v2/src/components/QueryLog/blocks/EmptyState/EmptyState.tsx
new file mode 100644
index 00000000000..314d2c7d621
--- /dev/null
+++ b/client_v2/src/components/QueryLog/blocks/EmptyState/EmptyState.tsx
@@ -0,0 +1,56 @@
+import cn from 'clsx';
+
+import intl from 'panel/common/intl';
+import { Icon, IconType } from 'panel/common/ui/Icon';
+import { Link } from 'panel/common/ui/Link';
+import { RoutePath, SCROLL_QUERY_KEY } from 'panel/components/Routes/Paths';
+
+import theme from 'panel/lib/theme';
+import s from './EmptyState.module.pcss';
+
+export type EmptyStateMode = 'default' | 'disabled' | 'rotation-disabled';
+
+type Props = {
+ class?: string;
+ mode: EmptyStateMode;
+ messageClass?: string;
+};
+
+const getEmptyState = (mode: EmptyStateMode) => {
+ switch (mode) {
+ case 'disabled':
+ case 'rotation-disabled':
+ return {
+ message: (
+ <>
+ {intl.getMessage('query_log_nothing_available_rotation')}
+
+ {intl.getMessage('enable')}
+
+ >
+ ),
+ variant: 'rotation-disabled' as const,
+ icon: 'settings_info' as IconType,
+ };
+ default:
+ return {
+ message: intl.getMessage('query_log_nothing_available'),
+ variant: 'default' as const,
+ icon: 'not_found_search' as IconType,
+ };
+ }
+};
+
+export const EmptyState = (props: Props) => {
+ const state = () => getEmptyState(props.mode);
+
+ return (
+
+
+
+
+
+
{state().message}
+
+ );
+};
diff --git a/client_v2/src/components/QueryLog/blocks/EmptyState/index.ts b/client_v2/src/components/QueryLog/blocks/EmptyState/index.ts
new file mode 100644
index 00000000000..89798dbd685
--- /dev/null
+++ b/client_v2/src/components/QueryLog/blocks/EmptyState/index.ts
@@ -0,0 +1 @@
+export { EmptyState } from './EmptyState';
diff --git a/client_v2/src/components/QueryLog/blocks/Header/Header.module.pcss b/client_v2/src/components/QueryLog/blocks/Header/Header.module.pcss
new file mode 100644
index 00000000000..702c52db7fd
--- /dev/null
+++ b/client_v2/src/components/QueryLog/blocks/Header/Header.module.pcss
@@ -0,0 +1,212 @@
+.header {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ padding: 0 16px;
+
+ @media (min-width: 1024px) {
+ padding: 0;
+ flex-direction: row;
+ align-items: center;
+ }
+}
+
+.title {
+ padding: 0;
+
+ @media (min-width: 1024px) {
+ margin-right: auto;
+ white-space: nowrap;
+ }
+}
+
+.actions {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ align-items: stretch;
+
+ @media (min-width: 1024px) {
+ flex-direction: row;
+ flex-wrap: nowrap;
+ align-items: center;
+ width: 100%;
+ }
+}
+
+.filters {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ width: 100%;
+ min-width: 0;
+
+ @media (min-width: 768px) {
+ flex-direction: row;
+ gap: 8px;
+ }
+
+ @media (min-width: 1024px) {
+ width: auto;
+ }
+}
+
+.searchWrapper {
+ display: flex;
+ gap: 8px;
+ align-items: stretch;
+
+ @media (min-width: 1024px) {
+ display: contents;
+ }
+}
+
+.searchField {
+ flex: 1;
+ overflow: visible;
+
+ @media (min-width: 1024px) {
+ flex: 1 1 220px;
+ max-width: 320px;
+ min-width: 180px;
+ margin-left: auto;
+ }
+
+ @media (min-width: 1024px) and (max-width: 1199px) {
+ max-width: 280px;
+ }
+}
+
+.searchIcon {
+ width: 24px;
+ height: 24px;
+ color: var(--default-gray-icons);
+}
+
+.searchSuffix {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.searchClearButton {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ border: 0;
+ background: transparent;
+ color: var(--default-description-text);
+ cursor: pointer;
+ transition: color var(--t2);
+
+ &:hover,
+ &:focus {
+ color: var(--default-main-text);
+ outline: 0;
+ }
+
+ &:disabled {
+ opacity: 0.4;
+ cursor: default;
+ }
+}
+
+.searchClearIcon {
+ width: 24px;
+ height: 24px;
+}
+
+.filterField {
+ width: 100%;
+ min-width: 0;
+
+ @media (min-width: 1024px) {
+ flex: 0 1 auto;
+ width: auto;
+ max-width: 100%;
+ }
+}
+
+.filterSelect {
+ width: 100%;
+ min-width: 0;
+
+ @media (min-width: 1024px) {
+ width: auto;
+ max-width: 100%;
+
+ &:global(.react-select.react-select-responsive) {
+ width: fit-content;
+ max-width: 100%;
+ min-width: 0;
+ }
+ }
+
+ & :global(.select__control) {
+ min-width: 100%;
+ max-width: 100%;
+ }
+
+ & :global(.select__value-container) {
+ min-width: 0;
+ max-width: 100%;
+ }
+
+ & :global(.select__single-value) {
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+}
+
+.refreshMobileButton,
+.refreshDesktopButton {
+ flex-shrink: 0;
+ width: 40px;
+ height: 40px;
+ min-width: 40px;
+ max-width: none;
+ padding: 0;
+
+ & > div:first-child,
+ & > div:last-child {
+ width: 0;
+ height: 0;
+ margin: 0;
+ overflow: hidden;
+ }
+}
+
+.refreshMobileButton {
+ @media (min-width: 1024px) {
+ display: none;
+ }
+}
+
+.refreshDesktopButton {
+ display: none;
+
+ @media (min-width: 1024px) {
+ display: inline-flex;
+ }
+}
+
+.refreshMobileIcon,
+.refreshDesktopIcon {
+ width: 24px;
+ height: 24px;
+}
+
+.refreshMobileIcon {
+ color: var(--gray-0);
+}
+
+.refreshDesktopIcon {
+ color: var(--default-product-icon);
+}
+
+.searchLoader {
+ color: var(--default-link);
+}
diff --git a/client_v2/src/components/QueryLog/blocks/Header/Header.tsx b/client_v2/src/components/QueryLog/blocks/Header/Header.tsx
new file mode 100644
index 00000000000..e7e75655c39
--- /dev/null
+++ b/client_v2/src/components/QueryLog/blocks/Header/Header.tsx
@@ -0,0 +1,260 @@
+import { createSignal, createMemo, createEffect, on, onCleanup, Show, untrack } from 'solid-js';
+import cn from 'clsx';
+
+import intl from 'panel/common/intl';
+import theme from 'panel/lib/theme';
+import { Input } from 'panel/common/controls/Input';
+import { Button } from 'panel/common/ui/Button';
+import { Icon } from 'panel/common/ui/Icon';
+import { Select } from 'panel/common/controls/Select';
+import { FaqTooltip } from 'panel/common/ui/FaqTooltip';
+import { InlineLoader } from 'panel/common/ui/Loader';
+import { IOption } from 'panel/lib/helpers/utils';
+import { DEBOUNCE_FILTER_TIMEOUT } from 'panel/helpers/constants';
+import { useIsMobile } from 'panel/hooks/useIsMobile';
+
+import s from './Header.module.pcss';
+
+type Props = {
+ onSearch: (value: string) => void;
+ onRefresh: () => void;
+ onStatusFilterChange: (status: string) => void;
+ onReasonFilterChange: (reason: string) => void;
+ currentSearch: string;
+ currentStatus: string;
+ currentReason: string;
+ isLoading: boolean;
+};
+
+const STATUS_OPTIONS = [
+ {
+ value: 'all',
+ get label() {
+ return intl.getMessage('query_log_all_statuses');
+ },
+ },
+ {
+ value: 'processed',
+ get label() {
+ return intl.getMessage('query_log_processed');
+ },
+ },
+ {
+ value: 'allowed',
+ get label() {
+ return intl.getMessage('query_log_allowed');
+ },
+ },
+ {
+ value: 'blocked',
+ get label() {
+ return intl.getMessage('query_log_blocked');
+ },
+ },
+ {
+ value: 'rewritten',
+ get label() {
+ return intl.getMessage('query_log_rewritten');
+ },
+ },
+];
+
+const REASON_OPTIONS = [
+ {
+ value: 'all',
+ get label() {
+ return intl.getMessage('query_log_all_reasons');
+ },
+ },
+ {
+ value: 'FilteredBlackList',
+ get label() {
+ return intl.getMessage('query_log_blocked_by_filter');
+ },
+ },
+ {
+ value: 'FilteredBlockedService',
+ get label() {
+ return intl.getMessage('query_log_blocked_services');
+ },
+ },
+ {
+ value: 'FilteredSafeBrowsing',
+ get label() {
+ return intl.getMessage('query_log_blocked_threats');
+ },
+ },
+ {
+ value: 'FilteredParental',
+ get label() {
+ return intl.getMessage('query_log_blocked_by_parental_control');
+ },
+ },
+ {
+ value: 'Rewrite',
+ get label() {
+ return intl.getMessage('dns_rewrites');
+ },
+ },
+ {
+ value: 'FilteredSafeSearch',
+ get label() {
+ return intl.getMessage('query_log_safe_search');
+ },
+ },
+];
+
+export const Header = (props: Props) => {
+ const [searchValue, setSearchValue] = createSignal(untrack(() => props.currentSearch));
+ let debounceTimer: ReturnType | null = null;
+ const isMobile = useIsMobile();
+
+ createEffect(
+ on(
+ () => props.currentSearch,
+ (value) => setSearchValue(value),
+ { defer: true },
+ ),
+ );
+
+ const handleSearchChange = (e: Event) => {
+ const value = (e.target as HTMLInputElement).value;
+ setSearchValue(value);
+
+ if (debounceTimer) {
+ clearTimeout(debounceTimer);
+ }
+
+ debounceTimer = setTimeout(() => {
+ props.onSearch(value);
+ }, DEBOUNCE_FILTER_TIMEOUT);
+ };
+
+ const handleClearSearch = () => {
+ if (debounceTimer) {
+ clearTimeout(debounceTimer);
+ }
+ setSearchValue('');
+ props.onSearch('');
+ };
+
+ onCleanup(() => {
+ if (debounceTimer) {
+ clearTimeout(debounceTimer);
+ }
+ });
+
+ const selectedStatus = createMemo(
+ () => STATUS_OPTIONS.find((opt) => opt.value === props.currentStatus) || STATUS_OPTIONS[0],
+ );
+ const selectedReason = createMemo(
+ () => REASON_OPTIONS.find((opt) => opt.value === props.currentReason) || REASON_OPTIONS[0],
+ );
+
+ return (
+
+ );
+};
diff --git a/client_v2/src/components/QueryLog/blocks/Header/index.ts b/client_v2/src/components/QueryLog/blocks/Header/index.ts
new file mode 100644
index 00000000000..29429dc97e8
--- /dev/null
+++ b/client_v2/src/components/QueryLog/blocks/Header/index.ts
@@ -0,0 +1 @@
+export { Header } from './Header';
diff --git a/client_v2/src/components/QueryLog/blocks/InfiniteScrollTrigger.module.pcss b/client_v2/src/components/QueryLog/blocks/InfiniteScrollTrigger.module.pcss
new file mode 100644
index 00000000000..c14edde7279
--- /dev/null
+++ b/client_v2/src/components/QueryLog/blocks/InfiniteScrollTrigger.module.pcss
@@ -0,0 +1,17 @@
+.loader {
+ width: 100%;
+ min-height: 1px;
+}
+
+.loading {
+ min-height: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.icon {
+ width: 24px;
+ height: 24px;
+ color: var(--default-product-icon);
+}
diff --git a/client_v2/src/components/QueryLog/blocks/InfiniteScrollTrigger.tsx b/client_v2/src/components/QueryLog/blocks/InfiniteScrollTrigger.tsx
new file mode 100644
index 00000000000..82d593a4ce2
--- /dev/null
+++ b/client_v2/src/components/QueryLog/blocks/InfiniteScrollTrigger.tsx
@@ -0,0 +1,134 @@
+import { createEffect, onCleanup, Show, untrack } from 'solid-js';
+import cn from 'clsx';
+
+import { InlineLoader } from 'panel/common/ui/Loader';
+
+import s from './InfiniteScrollTrigger.module.pcss';
+
+type Props = {
+ hasMore: boolean;
+ loading: boolean;
+ disabled: boolean;
+ onLoadMore: () => void;
+ resetToken?: string;
+ class?: string;
+};
+
+const VIEWPORT_OFFSET = 200;
+
+export const InfiniteScrollTrigger = (props: Props) => {
+ let sentinelEl: HTMLDivElement | undefined;
+ let requestedRef = false;
+ let wasNearEndRef = false;
+ let frameRef: number | null = null;
+
+ const triggerLoadMore = () => {
+ if (!untrack(() => props.hasMore) || untrack(() => props.disabled) || requestedRef) {
+ return;
+ }
+ requestedRef = true;
+ untrack(() => props).onLoadMore();
+ };
+
+ createEffect(() => {
+ if (!props.disabled) {
+ requestedRef = false;
+ wasNearEndRef = false;
+ }
+ });
+
+ createEffect(() => {
+ if (!props.hasMore) {
+ requestedRef = false;
+ }
+ });
+
+ createEffect(() => {
+ // Track resetToken to trigger re-read
+ void props.resetToken;
+ requestedRef = false;
+ wasNearEndRef = false;
+ });
+
+ createEffect(() => {
+ // Track dependencies
+ void props.disabled;
+ void props.hasMore;
+ void props.resetToken;
+
+ const isNearEnd = () => {
+ if (!sentinelEl) {
+ return false;
+ }
+
+ const rect = sentinelEl.getBoundingClientRect();
+
+ // Ignore hidden elements (e.g. inside display:none containers)
+ if (rect.width === 0 && rect.height === 0) {
+ return false;
+ }
+
+ return rect.top <= window.innerHeight + VIEWPORT_OFFSET;
+ };
+
+ const maybeLoadMore = () => {
+ const nearEnd = isNearEnd();
+
+ if (nearEnd && !wasNearEndRef) {
+ triggerLoadMore();
+ }
+
+ wasNearEndRef = nearEnd;
+ };
+
+ const scheduleMaybeLoadMore = () => {
+ if (frameRef !== null) {
+ return;
+ }
+
+ frameRef = window.requestAnimationFrame(() => {
+ frameRef = null;
+ maybeLoadMore();
+ });
+ };
+
+ const handleScroll = () => {
+ if (window.scrollY <= 0) {
+ wasNearEndRef = false;
+ }
+ scheduleMaybeLoadMore();
+ };
+
+ const handleResize = () => {
+ scheduleMaybeLoadMore();
+ };
+
+ scheduleMaybeLoadMore();
+
+ window.addEventListener('scroll', handleScroll, { passive: true });
+ window.addEventListener('resize', handleResize);
+
+ onCleanup(() => {
+ if (frameRef !== null) {
+ window.cancelAnimationFrame(frameRef);
+ frameRef = null;
+ }
+ window.removeEventListener('scroll', handleScroll);
+ window.removeEventListener('resize', handleResize);
+ });
+ });
+
+ return (
+
+
+
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/QueryLog/blocks/LogCard/LogCard.module.pcss b/client_v2/src/components/QueryLog/blocks/LogCard/LogCard.module.pcss
new file mode 100644
index 00000000000..0aeae5a87a4
--- /dev/null
+++ b/client_v2/src/components/QueryLog/blocks/LogCard/LogCard.module.pcss
@@ -0,0 +1,109 @@
+.card {
+ padding: 8px 0;
+ cursor: pointer;
+}
+
+.cardBody {
+ display: flex;
+ flex-direction: column;
+ padding: 8px 0 16px;
+ background: var(--default-page-background);
+ border-radius: 8px;
+ box-shadow: 8px 8px 24px 0 rgba(0, 0, 0, 0.1);
+}
+
+.icon {
+ flex-shrink: 0;
+ width: 16px;
+ height: 16px;
+}
+
+.domain {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.typeLine,
+.fieldLabel {
+ color: var(--default-description-text);
+}
+
+.typeLine {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.fieldValue {
+ color: var(--default-main-text);
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.cardHeader {
+ display: flex;
+ align-items: flex-start;
+ gap: 20px;
+ padding: 8px 8px 8px 12px;
+}
+
+.titleBlock {
+ display: flex;
+ flex: 1 1 auto;
+ flex-direction: column;
+ gap: 4px;
+ min-width: 0;
+ justify-content: center;
+}
+
+.titleRow {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
+ width: 100%;
+}
+
+.iconsRow {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ flex-shrink: 0;
+}
+
+.iconWrapper {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.fieldGrid {
+ display: grid;
+ grid-template-columns: minmax(0, 124px) minmax(0, 1fr);
+ gap: 0 8px;
+ padding-top: 4px;
+}
+
+.actions {
+ flex-shrink: 0;
+}
+
+.fieldLabel,
+.fieldValue {
+ min-height: 24px;
+ padding: 4px 12px;
+}
+
+.status {
+ min-width: 0;
+ min-height: 24px;
+ padding: 4px 12px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
diff --git a/client_v2/src/components/QueryLog/blocks/LogCard/LogCard.tsx b/client_v2/src/components/QueryLog/blocks/LogCard/LogCard.tsx
new file mode 100644
index 00000000000..7e987e56c65
--- /dev/null
+++ b/client_v2/src/components/QueryLog/blocks/LogCard/LogCard.tsx
@@ -0,0 +1,188 @@
+import { Show } from 'solid-js';
+import cn from 'clsx';
+
+import theme from 'panel/lib/theme';
+import { Icon } from 'panel/common/ui/Icon';
+
+import intl from 'panel/common/intl';
+import { Filter } from 'panel/helpers/helpers';
+import {
+ formatLogDate,
+ formatLogTime,
+ getClientLocation,
+ getProtocolName,
+ getQueryReasonLabel,
+ getQueryReasonDetails,
+ getQueryReasonKey,
+ getQueryStatusLabel,
+ getQueryStatusKey,
+ getStatusClassName,
+ hasPersistentClient,
+ isBlockedReason,
+} from '../../helpers';
+import { LogEntry, Service } from '../../types';
+import { ActionsMenu } from '../ActionsMenu';
+
+import s from './LogCard.module.pcss';
+
+type Props = {
+ entry: LogEntry;
+ filters: Filter[];
+ services: Service[];
+ whitelistFilters: Filter[];
+ onRowClick: (entry: LogEntry) => void;
+ onBlock: (domain: string) => void;
+ onUnblock: (domain: string) => void;
+ onBlockClient: (domain: string, client: string) => void;
+ onDisallowClient: (ip: string) => void;
+ onAddPersistentClient: (clientId: string) => void;
+ persistentClientIds: string[];
+ persistentClientsLoaded: boolean;
+};
+
+export const LogCard = (props: Props) => {
+ const displayDomain = () => props.entry.unicodeName || props.entry.domain;
+ const proto = () => getProtocolName(props.entry.client_proto);
+ const clientDetails = () => props.entry.client_info?.name || props.entry.client_id;
+ const clientLocation = () => getClientLocation(props.entry.client_info?.whois);
+ const statusKey = () =>
+ getQueryStatusKey(props.entry.reason, props.entry.originalResponse ?? []);
+ const reasonKey = () => getQueryReasonKey(props.entry.reason, props.entry.rules ?? []);
+ const reasonDetails = () =>
+ getQueryReasonDetails({
+ elapsedMs: props.entry.elapsedMs,
+ filters: props.filters,
+ reason: props.entry.reason,
+ rules: props.entry.rules ?? [],
+ serviceName: props.entry.service_name || props.entry.serviceName,
+ services: props.services,
+ whitelistFilters: props.whitelistFilters,
+ });
+ const statusLabel = () => getQueryStatusLabel(statusKey());
+ const reasonLabel = () => getQueryReasonLabel(reasonKey());
+
+ return (
+ props.onRowClick(props.entry)}
+ data-testid="query-log-card"
+ >
+
+
+
+
+
+ {intl.getMessage('time_table_header')}
+
+
+ {formatLogDate(props.entry.time)}, {formatLogTime(props.entry.time)}
+
+
+
+ {intl.getMessage('status_table_header')}
+
+
+ {statusLabel()}
+
+
+
+
+ {intl.getMessage('reason_table_header')}
+
+
+ {reasonLabel()}
+ {reasonDetails() ? ` / ${reasonDetails()}` : ''}
+
+
+
+
+ {intl.getMessage('client_ip')}
+
+
+ {props.entry.client}
+
+
+
+
+ {intl.getMessage('client_details')}
+
+
+ {clientDetails()}
+
+
+
+
+
+ {intl.getMessage('client_location')}
+
+
+ {clientLocation()}
+
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/QueryLog/blocks/LogCard/index.ts b/client_v2/src/components/QueryLog/blocks/LogCard/index.ts
new file mode 100644
index 00000000000..5c7e301ad3e
--- /dev/null
+++ b/client_v2/src/components/QueryLog/blocks/LogCard/index.ts
@@ -0,0 +1 @@
+export { LogCard } from './LogCard';
diff --git a/client_v2/src/components/QueryLog/blocks/LogTable/LogTable.module.pcss b/client_v2/src/components/QueryLog/blocks/LogTable/LogTable.module.pcss
new file mode 100644
index 00000000000..d3d9566a62d
--- /dev/null
+++ b/client_v2/src/components/QueryLog/blocks/LogTable/LogTable.module.pcss
@@ -0,0 +1,318 @@
+.tableContainer {
+ flex: 1 0 auto;
+ display: flex;
+ flex-direction: column;
+}
+
+.table {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+
+ @media (min-width: 768px) {
+ gap: 0;
+ }
+}
+
+.tableHeader {
+ @media (min-width: 768px) {
+ border-radius: 8px;
+ }
+}
+
+.tableRow {
+ cursor: pointer;
+ transition: background-color var(--t2);
+
+ @media (min-width: 768px) {
+ border: 0;
+ background-color: transparent;
+ }
+
+ &:hover,
+ &:focus {
+ background-color: var(--page-background-additional);
+ }
+
+ &:last-child {
+ @media (min-width: 768px) {
+ border-bottom: 0;
+ border-radius: 0 0 8px 8px;
+ overflow: hidden;
+ }
+ }
+}
+
+.clickableCell {
+ cursor: pointer;
+}
+
+.emptyTableWrapper {
+ padding: 24px 16px;
+
+ @media (min-width: 768px) {
+ min-height: 400px;
+ padding: 48px 64px;
+ }
+}
+
+.emptyTableTitle {
+ width: 100%;
+
+ @media (min-width: 768px) {
+ width: 250px;
+ }
+}
+
+.initialLoader {
+ min-height: 220px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.loadingRow {
+ padding: 12px 0 0;
+
+ @media (min-width: 768px) {
+ padding: 12px;
+ }
+}
+
+.time {
+ font-variant-numeric: tabular-nums;
+ white-space: nowrap;
+}
+
+.timeCell {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ min-width: 0;
+}
+
+.requestCell {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 12px;
+ overflow: hidden;
+}
+
+.requestContent {
+ display: flex;
+ flex: 1 1 auto;
+ flex-direction: column;
+ gap: 2px;
+ min-width: 0;
+}
+
+.secondaryLine {
+ color: var(--default-description-text);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.date {
+ text-transform: none;
+}
+
+.requestPrimary,
+.clientPrimary {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
+}
+
+.clientSecondary {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.requestIcons {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ flex-shrink: 0;
+}
+
+.iconTooltipTrigger {
+ display: inline-flex;
+}
+
+.iconTooltipOverlay {
+ width: max-content;
+}
+
+.iconTooltipMenu {
+ padding: 16px;
+}
+
+.requestIcon {
+ width: 16px;
+ height: 16px;
+ flex-shrink: 0;
+}
+
+.domain {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.statusCell,
+.reasonCell,
+.clientCell {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ min-width: 0;
+}
+
+.queryDetailsTooltipButton {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ border: 0;
+ background: transparent;
+ color: var(--default-gray-icons);
+ cursor: pointer;
+ transition: color var(--t2);
+
+ &:hover,
+ &:focus {
+ color: var(--default-main-text);
+ outline: 0;
+ }
+}
+
+.queryDetailsTooltipMenu {
+ width: min(320px, calc(100vw - 32px));
+ padding: 8px 0 12px;
+}
+
+.queryDetailsTooltipContent {
+ display: flex;
+ flex-direction: column;
+}
+
+.queryDetailsTooltipSection {
+ display: flex;
+ flex-direction: column;
+}
+
+.queryDetailsTooltipTitle {
+ padding: 8px 16px;
+ color: var(--default-main-text);
+}
+
+.queryDetailsTooltipTitleSeparated {
+ padding-top: 16px;
+}
+
+.queryDetailsTooltipItem {
+ padding: 4px 16px;
+ min-height: 24px;
+ white-space: normal;
+}
+
+.queryDetailsTooltipLabel {
+ color: var(--default-main-text);
+}
+
+.queryDetailsTooltipValue {
+ color: var(--default-main-text);
+ overflow-wrap: anywhere;
+}
+
+.queryDetailsTooltipLink {
+ text-decoration: none;
+
+ &:hover,
+ &:focus {
+ text-decoration: underline;
+ }
+}
+
+.clientButtonPlain {
+ padding: 0;
+ border: 0;
+ background: transparent;
+ text-align: left;
+ text-decoration: none;
+ cursor: pointer;
+
+ &:hover,
+ &:focus {
+ text-decoration: underline;
+ }
+}
+
+.clientIp {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ color: var(--default-main-text);
+}
+
+.clientName {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ color: var(--default-description-text);
+}
+
+.clientLocationDivider {
+ width: 1px;
+ height: 12px;
+ background-color: var(--default-item-divider);
+ flex-shrink: 0;
+}
+
+.clientLocation {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ min-width: 0;
+ color: var(--default-description-text);
+}
+
+.clientLocationIcon {
+ width: 16px;
+ height: 16px;
+ color: var(--default-description-text);
+ flex-shrink: 0;
+}
+
+.clientLocationText {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.whoisItem {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ min-width: 0;
+}
+
+.actionsCell {
+ display: flex;
+ width: 100%;
+ align-items: center;
+ justify-content: center;
+}
+
+.loader {
+ width: 48px;
+ height: 48px;
+}
diff --git a/client_v2/src/components/QueryLog/blocks/LogTable/LogTable.tsx b/client_v2/src/components/QueryLog/blocks/LogTable/LogTable.tsx
new file mode 100644
index 00000000000..9ffaba907e2
--- /dev/null
+++ b/client_v2/src/components/QueryLog/blocks/LogTable/LogTable.tsx
@@ -0,0 +1,164 @@
+import { createMemo, Show, untrack } from 'solid-js';
+
+import intl from 'panel/common/intl';
+import { Loader } from 'panel/common/ui/Loader';
+import { Table, TableColumn } from 'panel/common/ui/Table/Table';
+
+import { LogEntry, Service } from 'panel/components/QueryLog/types';
+import { hasPersistentClient, isBlockedReason } from 'panel/components/QueryLog/helpers';
+
+import { Filter } from 'panel/helpers/helpers';
+import { InfiniteScrollTrigger } from '../InfiniteScrollTrigger';
+import { EmptyState, type EmptyStateMode } from '../EmptyState/EmptyState';
+import { ClientCell, RequestCell, ReasonCell, StatusCell, TimeCell } from './blocks';
+
+import s from './LogTable.module.pcss';
+import { ActionsMenu } from '../ActionsMenu';
+
+type Props = {
+ logs: LogEntry[];
+ emptyStateMode: EmptyStateMode;
+ hasMore: boolean;
+ isLoadingMore: boolean;
+ isRequestInFlight: boolean;
+ isInitialLoading: boolean;
+ isFilterReloading: boolean;
+ infiniteScrollResetToken: string;
+ onLoadMore: () => void;
+ onRowClick: (entry: LogEntry) => void;
+ onBlock: (domain: string) => void;
+ onUnblock: (domain: string) => void;
+ onBlockClient: (domain: string, client: string) => void;
+ onDisallowClient: (ip: string) => void;
+ onAddPersistentClient: (clientId: string) => void;
+ onSearchSelect: (value: string) => void;
+ filters: Filter[];
+ services: Service[];
+ whitelistFilters: Filter[];
+ persistentClientIds: string[];
+ persistentClientsLoaded: boolean;
+};
+
+export const LogTable = (props: Props) => {
+ const handleSearchSelect = (value: string) => (event: MouseEvent) => {
+ event.stopPropagation();
+ untrack(() => props.onSearchSelect(value));
+ };
+
+ const columns = createMemo[]>(() => [
+ {
+ key: 'time',
+ header: { text: intl.getMessage('time_table_header') },
+ render: (_value: unknown, row: LogEntry) => ,
+ width: 116,
+ sortable: false,
+ },
+ {
+ key: 'domain',
+ header: { text: intl.getMessage('request_table_header') },
+ render: (_value: unknown, row: LogEntry) => ,
+ sortable: false,
+ },
+ {
+ key: 'status',
+ header: { text: intl.getMessage('status_table_header') },
+ render: (_value: unknown, row: LogEntry) => ,
+ width: 'minmax(108px, 0.7fr)',
+ sortable: false,
+ },
+ {
+ key: 'reason',
+ header: { text: intl.getMessage('reason_table_header') },
+ render: (_value: unknown, row: LogEntry) => {
+ return (
+
+ );
+ },
+ width: 'minmax(136px, 0.9fr)',
+ sortable: false,
+ },
+ {
+ key: 'client',
+ header: { text: intl.getMessage('client_table_header') },
+ render: (_value: unknown, row: LogEntry) => (
+
+ ),
+ sortable: false,
+ },
+ {
+ key: 'actions',
+ header: { text: '', render: () => null },
+ render: (_value: unknown, row: LogEntry) => (
+ e.stopPropagation()}
+ >
+
props.onDisallowClient(row.client)}
+ onAddPersistentClient={props.onAddPersistentClient}
+ isBlocked={isBlockedReason(row.reason)}
+ showAddPersistentClient={
+ props.persistentClientsLoaded &&
+ !hasPersistentClient(row, props.persistentClientIds)
+ }
+ testIdPrefix="query-log-row"
+ />
+
+ ),
+ width: 48,
+ sortable: false,
+ },
+ ]);
+
+ return (
+
+
+
+
+ ) : (
+
+ )
+ }
+ pagination={false}
+ sortable={false}
+ class={s.table}
+ onRowClick={props.onRowClick}
+ tableRowClass={s.tableRow}
+ tableHeaderClass={s.tableHeader}
+ />
+
+ 0}>
+
+
+
+ );
+};
diff --git a/client_v2/src/components/QueryLog/blocks/LogTable/blocks/ClientCell.tsx b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/ClientCell.tsx
new file mode 100644
index 00000000000..30f94a05f61
--- /dev/null
+++ b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/ClientCell.tsx
@@ -0,0 +1,62 @@
+import { Show } from 'solid-js';
+import cn from 'clsx';
+
+import { Icon } from 'panel/common/ui/Icon';
+import theme from 'panel/lib/theme';
+import { getClientLocation } from 'panel/components/QueryLog/helpers';
+import { LogEntry } from 'panel/components/QueryLog/types';
+
+import s from '../LogTable.module.pcss';
+
+type Props = {
+ onSearchSelect: (value: string) => (event: MouseEvent) => void;
+ row: LogEntry;
+};
+
+export const ClientCell = (props: Props) => {
+ const clientName = () => props.row.client_info?.name || '';
+ const clientLocation = () => getClientLocation(props.row.client_info?.whois);
+
+ return (
+
+
+ {
+ e.stopPropagation();
+ props.onSearchSelect(props.row.client)(e);
+ }}
+ >
+ {props.row.client}
+
+
+
+
+ props.onSearchSelect(clientName())(e)}
+ >
+ {clientName()}
+
+
+
+
+
+
+
+
+
+
+
+ {clientLocation()}
+
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/QueryLog/blocks/LogTable/blocks/QueryDetailsTooltipContent.tsx b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/QueryDetailsTooltipContent.tsx
new file mode 100644
index 00000000000..53e0ba84882
--- /dev/null
+++ b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/QueryDetailsTooltipContent.tsx
@@ -0,0 +1,121 @@
+import { Show } from 'solid-js';
+import cn from 'clsx';
+
+import intl from 'panel/common/intl';
+import { captitalizeWords } from 'panel/helpers/helpers';
+import theme from 'panel/lib/theme';
+import {
+ formatLogDate,
+ formatLogTimeDetailed,
+ getProtocolName,
+} from 'panel/components/QueryLog/helpers';
+import { LogEntry } from 'panel/components/QueryLog/types';
+
+import s from '../LogTable.module.pcss';
+
+type Props = {
+ row: LogEntry;
+};
+
+const renderValue = (value: any) => (
+ {value}
+);
+
+export const QueryDetailsTooltipContent = (props: Props) => {
+ const trackerSource = () => props.row.tracker?.sourceData;
+ const displayDomain = () => props.row.unicodeName || props.row.domain;
+
+ return (
+ e.stopPropagation()}>
+
+ {intl.getMessage('query_details')}
+
+
+
+
+
+
+ {intl.getMessage('known_tracker')}
+
+
+
+
+
+ );
+};
diff --git a/client_v2/src/components/QueryLog/blocks/LogTable/blocks/ReasonCell.tsx b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/ReasonCell.tsx
new file mode 100644
index 00000000000..00e91686f13
--- /dev/null
+++ b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/ReasonCell.tsx
@@ -0,0 +1,44 @@
+import cn from 'clsx';
+
+import theme from 'panel/lib/theme';
+import { Filter } from 'panel/helpers/helpers';
+import { LogEntry, Service } from 'panel/components/QueryLog/types';
+import {
+ getQueryReasonLabel,
+ getQueryReasonDetails,
+ getQueryReasonKey,
+} from 'panel/components/QueryLog/helpers';
+
+import s from '../LogTable.module.pcss';
+
+type Props = {
+ row: LogEntry;
+ filters: Filter[];
+ services: Service[];
+ whitelistFilters: Filter[];
+};
+
+export const ReasonCell = (props: Props) => {
+ const rules = () => props.row.rules ?? [];
+ const reasonKey = () => getQueryReasonKey(props.row.reason, rules());
+ const reasonDetails = () =>
+ getQueryReasonDetails({
+ elapsedMs: props.row.elapsedMs,
+ filters: props.filters,
+ reason: props.row.reason,
+ rules: rules(),
+ serviceName: props.row.service_name || props.row.serviceName,
+ services: props.services,
+ whitelistFilters: props.whitelistFilters,
+ });
+ const reasonLabel = () => getQueryReasonLabel(reasonKey());
+
+ return (
+
+ {reasonLabel()}
+
+ {reasonDetails()}
+
+
+ );
+};
diff --git a/client_v2/src/components/QueryLog/blocks/LogTable/blocks/RequestCell.tsx b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/RequestCell.tsx
new file mode 100644
index 00000000000..558aaca713e
--- /dev/null
+++ b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/RequestCell.tsx
@@ -0,0 +1,83 @@
+import { Show } from 'solid-js';
+import cn from 'clsx';
+
+import intl from 'panel/common/intl';
+import { Dropdown } from 'panel/common/ui/Dropdown';
+import theme from 'panel/lib/theme';
+import { Icon } from 'panel/common/ui/Icon';
+import { getProtocolName } from 'panel/components/QueryLog/helpers';
+import { QueryDetailsTooltipContent } from 'panel/components/QueryLog/blocks/LogTable/blocks/QueryDetailsTooltipContent';
+import { LogEntry } from 'panel/components/QueryLog/types';
+
+import s from '../LogTable.module.pcss';
+
+type Props = {
+ row: LogEntry;
+};
+
+export const RequestCell = (props: Props) => {
+ return (
+
+
+
+
+ {props.row.unicodeName || props.row.domain}
+
+
+
+
+ }
+ childrenClass={s.iconTooltipTrigger}
+ noIcon
+ >
+
event.stopPropagation()}
+ >
+
+
+
+
+
+
+ }
+ childrenClass={s.iconTooltipTrigger}
+ noIcon
+ >
+
+
+
+
+
+
+ {intl.getMessage('type_value', { value: props.row.type })},{' '}
+ {getProtocolName(props.row.client_proto)}
+
+
+
+ );
+};
diff --git a/client_v2/src/components/QueryLog/blocks/LogTable/blocks/ResponseCell.tsx b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/ResponseCell.tsx
new file mode 100644
index 00000000000..027bd7ddaa0
--- /dev/null
+++ b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/ResponseCell.tsx
@@ -0,0 +1,43 @@
+import cn from 'clsx';
+
+import theme from 'panel/lib/theme';
+
+import { LogEntry, Service } from 'panel/components/QueryLog/types';
+import {
+ getResponseDetails,
+ getStatusClassName,
+ getStatusLabel,
+} from 'panel/components/QueryLog/helpers';
+import { Filter } from 'panel/helpers/helpers';
+import s from '../LogTable.module.pcss';
+
+type Props = {
+ row: LogEntry;
+ filters: Filter[];
+ services: Service[];
+ whitelistFilters: Filter[];
+};
+
+export const ResponseCell = (props: Props) => {
+ const statusClassName = () => getStatusClassName(props.row.reason);
+ const statusLabel = () => getStatusLabel(props.row.reason, props.row.originalResponse, false);
+ const responseDetails = () =>
+ getResponseDetails({
+ elapsedMs: props.row.elapsedMs,
+ filters: props.filters,
+ reason: props.row.reason,
+ rules: props.row.rules,
+ serviceName: props.row.service_name,
+ services: props.services,
+ whitelistFilters: props.whitelistFilters,
+ });
+
+ return (
+
+ {statusLabel()}
+
+ {responseDetails()}
+
+
+ );
+};
diff --git a/client_v2/src/components/QueryLog/blocks/LogTable/blocks/StatusCell.tsx b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/StatusCell.tsx
new file mode 100644
index 00000000000..27f0d07f95a
--- /dev/null
+++ b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/StatusCell.tsx
@@ -0,0 +1,34 @@
+import { createMemo } from 'solid-js';
+import cn from 'clsx';
+
+import theme from 'panel/lib/theme';
+import { LogEntry } from 'panel/components/QueryLog/types';
+import {
+ getQueryStatusLabel,
+ getQueryStatusDetails,
+ getQueryStatusKey,
+ getStatusClassName,
+} from 'panel/components/QueryLog/helpers';
+
+import s from '../LogTable.module.pcss';
+
+type Props = {
+ row: LogEntry;
+};
+
+export const StatusCell = (props: Props) => {
+ const statusKey = createMemo(() =>
+ getQueryStatusKey(props.row.reason, props.row.originalResponse ?? []),
+ );
+
+ return (
+
+
+ {getQueryStatusLabel(statusKey())}
+
+
+ {getQueryStatusDetails(props.row.elapsedMs)}
+
+
+ );
+};
diff --git a/client_v2/src/components/QueryLog/blocks/LogTable/blocks/TimeCell.tsx b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/TimeCell.tsx
new file mode 100644
index 00000000000..a572967319d
--- /dev/null
+++ b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/TimeCell.tsx
@@ -0,0 +1,22 @@
+import cn from 'clsx';
+
+import theme from 'panel/lib/theme';
+import { LogEntry } from 'panel/components/QueryLog/types';
+import { formatLogDate, formatLogTime } from 'panel/components/QueryLog/helpers';
+
+import s from '../LogTable.module.pcss';
+
+type Props = {
+ row: LogEntry;
+};
+
+export const TimeCell = (props: Props) => (
+
+
+ {formatLogTime(props.row.time)}
+
+
+ {formatLogDate(props.row.time)}
+
+
+);
diff --git a/client_v2/src/components/QueryLog/blocks/LogTable/blocks/index.ts b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/index.ts
new file mode 100644
index 00000000000..95bd0930765
--- /dev/null
+++ b/client_v2/src/components/QueryLog/blocks/LogTable/blocks/index.ts
@@ -0,0 +1,5 @@
+export { TimeCell } from './TimeCell';
+export { RequestCell } from './RequestCell';
+export { StatusCell } from './StatusCell';
+export { ReasonCell } from './ReasonCell';
+export { ClientCell } from './ClientCell';
diff --git a/client_v2/src/components/QueryLog/blocks/LogTable/index.ts b/client_v2/src/components/QueryLog/blocks/LogTable/index.ts
new file mode 100644
index 00000000000..b92fc053ea3
--- /dev/null
+++ b/client_v2/src/components/QueryLog/blocks/LogTable/index.ts
@@ -0,0 +1 @@
+export { LogTable } from './LogTable';
diff --git a/client_v2/src/components/QueryLog/helpers.ts b/client_v2/src/components/QueryLog/helpers.ts
new file mode 100644
index 00000000000..927ba8579d0
--- /dev/null
+++ b/client_v2/src/components/QueryLog/helpers.ts
@@ -0,0 +1,363 @@
+import { format as dateFormat, isValid } from 'date-fns';
+
+import intl from 'panel/common/intl';
+import theme from 'panel/lib/theme';
+import {
+ FILTERED_STATUS,
+ FILTERED_STATUS_TO_COLOR_MAP,
+ QUERY_STATUS_COLORS,
+ SCHEME_TO_PROTOCOL_MAP,
+ DEFAULT_TIME_FORMAT,
+ LONG_TIME_FORMAT,
+ SPECIAL_FILTER_ID,
+} from 'panel/helpers/constants';
+import {
+ formatElapsedMs,
+ getFilterNames,
+ getServiceName,
+ type Filter,
+ type Rule,
+} from 'panel/helpers/helpers';
+import { LogEntry, ResponseEntry, WhoisInfo } from './types';
+
+const parseLogDate = (time: string): Date | null => {
+ const parsedTime = new Date(time);
+
+ return isValid(parsedTime) ? parsedTime : null;
+};
+
+export type QueryStatusKey = 'all' | 'allowed' | 'processed' | 'blocked' | 'rewritten' | 'error';
+
+export type QueryReasonKey =
+ | 'all'
+ | 'none'
+ | 'allowlists'
+ | 'blocked_by_filter'
+ | 'custom_filtering_rules'
+ | 'blocked_services'
+ | 'blocked_threats'
+ | 'blocked_by_parental_control'
+ | 'safe_search'
+ | 'dns_rewrites'
+ | 'error';
+
+export const getQueryStatusLabel = (statusKey: Exclude): string => {
+ switch (statusKey) {
+ case 'allowed':
+ return intl.getMessage('query_log_allowed');
+ case 'processed':
+ return intl.getMessage('query_log_processed');
+ case 'blocked':
+ return intl.getMessage('query_log_blocked');
+ case 'rewritten':
+ return intl.getMessage('query_log_rewritten');
+ case 'error':
+ return intl.getMessage('error');
+ default:
+ return '';
+ }
+};
+
+export const getQueryReasonLabel = (reasonKey: Exclude): string => {
+ switch (reasonKey) {
+ case 'none':
+ return '-';
+ case 'allowlists':
+ return intl.getMessage('allowlists');
+ case 'blocked_by_filter':
+ return intl.getMessage('query_log_blocked_by_filter');
+ case 'custom_filtering_rules':
+ return intl.getMessage('query_log_custom_filtering_rules');
+ case 'blocked_services':
+ return intl.getMessage('query_log_blocked_services');
+ case 'blocked_threats':
+ return intl.getMessage('query_log_blocked_threats');
+ case 'blocked_by_parental_control':
+ return intl.getMessage('query_log_blocked_by_parental_control');
+ case 'safe_search':
+ return intl.getMessage('query_log_safe_search');
+ case 'dns_rewrites':
+ return intl.getMessage('dns_rewrites');
+ case 'error':
+ return intl.getMessage('error');
+ default:
+ return '';
+ }
+};
+
+export const getStatusLabel = (
+ reason: string,
+ originalResponse: ResponseEntry[],
+ isBlockedByResponse: boolean,
+): string => {
+ if (isBlockedByResponse) {
+ return intl.getMessage('blocked_by_cname_or_ip');
+ }
+
+ switch (reason) {
+ case FILTERED_STATUS.NOT_FILTERED_WHITE_LIST:
+ return intl.getMessage('allowed');
+ case FILTERED_STATUS.NOT_FILTERED_NOT_FOUND:
+ return intl.getMessage('show_processed_responses');
+ case FILTERED_STATUS.FILTERED_BLOCKED_SERVICE:
+ return intl.getMessage('blocked_service');
+ case FILTERED_STATUS.FILTERED_SAFE_SEARCH:
+ return intl.getMessage('safe_search');
+ case FILTERED_STATUS.FILTERED_BLACK_LIST:
+ return intl.getMessage('show_blocked_responses');
+ case FILTERED_STATUS.REWRITE:
+ case FILTERED_STATUS.REWRITE_HOSTS:
+ case FILTERED_STATUS.REWRITE_RULE:
+ return intl.getMessage('rewritten');
+ case FILTERED_STATUS.FILTERED_SAFE_BROWSING:
+ return intl.getMessage('blocked_threats');
+ case FILTERED_STATUS.FILTERED_PARENTAL:
+ return intl.getMessage('blocked_adult_websites');
+ case FILTERED_STATUS.NOT_FILTERED_ERROR:
+ return intl.getMessage('error');
+ case FILTERED_STATUS.FILTERED_INVALID:
+ return intl.getMessage('invalid');
+ default:
+ if (originalResponse && originalResponse.length > 0) {
+ return intl.getMessage('rewritten');
+ }
+
+ return intl.getMessage('show_processed_responses');
+ }
+};
+
+const STATUS_COLOR_TO_CLASS: Record = {
+ [QUERY_STATUS_COLORS.RED]: theme.status.statusRed,
+ [QUERY_STATUS_COLORS.GREEN]: theme.status.statusGreen,
+ [QUERY_STATUS_COLORS.YELLOW]: theme.status.statusYellow,
+ [QUERY_STATUS_COLORS.BLUE]: theme.status.statusBlue,
+};
+
+const PROTOCOL_LABEL_GETTERS = {
+ dns_over_https: () => intl.getMessage('dns_over_https'),
+ dns_over_quic: () => intl.getMessage('dns_over_quic'),
+ dns_over_tls: () => intl.getMessage('dns_over_tls'),
+ plain_dns: () => intl.getMessage('plain_dns'),
+} as const;
+
+export const getStatusClassName = (reason: string): string =>
+ STATUS_COLOR_TO_CLASS[
+ FILTERED_STATUS_TO_COLOR_MAP[reason as keyof typeof FILTERED_STATUS_TO_COLOR_MAP]
+ ] || '';
+
+export const isBlockedReason = (reason: string): boolean =>
+ reason.startsWith('Filtered') && reason !== 'FilteredSafeSearch';
+
+export const getProtocolName = (clientProto: string): string => {
+ const key = SCHEME_TO_PROTOCOL_MAP[clientProto as keyof typeof SCHEME_TO_PROTOCOL_MAP];
+ if (key) {
+ return PROTOCOL_LABEL_GETTERS[key as keyof typeof PROTOCOL_LABEL_GETTERS]();
+ }
+
+ return PROTOCOL_LABEL_GETTERS.plain_dns();
+};
+
+export const formatLogTime = (time: string): string => {
+ const parsedTime = parseLogDate(time);
+
+ return parsedTime ? dateFormat(parsedTime, DEFAULT_TIME_FORMAT) : time;
+};
+
+export const formatLogDate = (time: string): string => {
+ const parsedTime = parseLogDate(time);
+
+ return parsedTime
+ ? parsedTime.toLocaleDateString(intl.getUILanguage(), {
+ day: 'numeric',
+ month: 'short',
+ year: 'numeric',
+ })
+ : time;
+};
+
+export const formatLogTimeDetailed = (time: string): string => {
+ const parsedTime = parseLogDate(time);
+
+ return parsedTime ? dateFormat(parsedTime, LONG_TIME_FORMAT) : time;
+};
+
+export const getClientLocation = (whois?: WhoisInfo | null): string =>
+ [whois?.city, whois?.country].filter(Boolean).join(', ');
+
+type ResponseDetailsParams = {
+ elapsedMs: string;
+ filters: Filter[];
+ reason: string;
+ rules: Rule[];
+ serviceName?: string;
+ services?: { id: string; name: string }[];
+ whitelistFilters: Filter[];
+};
+
+export const getQueryStatusKey = (
+ reason: string,
+ originalResponse: ResponseEntry[] = [],
+): Exclude => {
+ switch (reason) {
+ case FILTERED_STATUS.NOT_FILTERED_WHITE_LIST:
+ return 'allowed';
+ case FILTERED_STATUS.REWRITE:
+ case FILTERED_STATUS.REWRITE_HOSTS:
+ case FILTERED_STATUS.REWRITE_RULE:
+ case FILTERED_STATUS.FILTERED_SAFE_SEARCH:
+ return 'rewritten';
+ case FILTERED_STATUS.NOT_FILTERED_NOT_FOUND:
+ return 'processed';
+ case FILTERED_STATUS.NOT_FILTERED_ERROR:
+ case FILTERED_STATUS.FILTERED_INVALID:
+ return 'error';
+ default:
+ if (originalResponse.length > 0) {
+ return 'rewritten';
+ }
+
+ if (reason.startsWith('Filtered')) {
+ return 'blocked';
+ }
+
+ return 'processed';
+ }
+};
+
+export const getQueryReasonKey = (
+ reason: string,
+ rules: Rule[] = [],
+): Exclude => {
+ switch (reason) {
+ case FILTERED_STATUS.NOT_FILTERED_WHITE_LIST:
+ return 'allowlists';
+ case FILTERED_STATUS.NOT_FILTERED_NOT_FOUND:
+ return 'none';
+ case FILTERED_STATUS.FILTERED_BLOCKED_SERVICE:
+ return 'blocked_services';
+ case FILTERED_STATUS.FILTERED_SAFE_SEARCH:
+ return 'safe_search';
+ case FILTERED_STATUS.REWRITE:
+ case FILTERED_STATUS.REWRITE_HOSTS:
+ case FILTERED_STATUS.REWRITE_RULE:
+ return 'dns_rewrites';
+ case FILTERED_STATUS.FILTERED_SAFE_BROWSING:
+ return 'blocked_threats';
+ case FILTERED_STATUS.FILTERED_PARENTAL:
+ return 'blocked_by_parental_control';
+ case FILTERED_STATUS.NOT_FILTERED_ERROR:
+ case FILTERED_STATUS.FILTERED_INVALID:
+ return 'error';
+ case FILTERED_STATUS.FILTERED_BLACK_LIST:
+ return rules.some(
+ ({ filter_list_id }) => filter_list_id === SPECIAL_FILTER_ID.CUSTOM_FILTERING_RULES,
+ )
+ ? 'custom_filtering_rules'
+ : 'blocked_by_filter';
+ default:
+ return 'none';
+ }
+};
+
+export const getQueryStatusDetails = (elapsedMs: string): string =>
+ formatElapsedMs(elapsedMs, intl.getMessage('milliseconds_abbreviation'));
+
+export const getQueryReasonDetails = ({
+ filters,
+ reason,
+ rules,
+ serviceName,
+ services,
+ whitelistFilters,
+}: ResponseDetailsParams): string => {
+ switch (getQueryReasonKey(reason, rules)) {
+ case 'blocked_services':
+ return (
+ (services && serviceName && getServiceName(services, serviceName)) ||
+ serviceName ||
+ ''
+ );
+ case 'blocked_by_filter':
+ case 'allowlists':
+ return getFilterNames(rules, filters, whitelistFilters).filter(Boolean).join(', ');
+ default:
+ return '';
+ }
+};
+
+export const filterLogsByStatus = <
+ T extends { reason: string; originalResponse?: ResponseEntry[] },
+>(
+ logs: T[],
+ status: QueryStatusKey | string,
+): T[] => {
+ if (status === 'all') {
+ return logs;
+ }
+
+ return logs.filter(
+ (log) => getQueryStatusKey(log.reason, log.originalResponse ?? []) === status,
+ );
+};
+
+export const hasPersistentClient = (
+ entry: Pick,
+ persistentClientIds: string[],
+): boolean => {
+ const entryIds = [entry.client, entry.client_id, ...(entry.client_info?.ids ?? [])].filter(
+ Boolean,
+ );
+
+ return entryIds.some((entryId) => persistentClientIds.includes(entryId));
+};
+
+export const getResponseDetails = ({
+ elapsedMs,
+ filters,
+ reason,
+ rules,
+ serviceName,
+ services,
+ whitelistFilters,
+}: ResponseDetailsParams): string => {
+ const formattedElapsedMs = formatElapsedMs(
+ elapsedMs,
+ intl.getMessage('milliseconds_abbreviation'),
+ );
+
+ switch (reason) {
+ case FILTERED_STATUS.FILTERED_BLOCKED_SERVICE:
+ return (
+ (services && serviceName && getServiceName(services, serviceName)) ||
+ serviceName ||
+ formattedElapsedMs
+ );
+ case FILTERED_STATUS.FILTERED_BLACK_LIST:
+ case FILTERED_STATUS.NOT_FILTERED_WHITE_LIST: {
+ const filterNames = getFilterNames(rules, filters, whitelistFilters)
+ .filter(Boolean)
+ .join(', ');
+
+ return filterNames || formattedElapsedMs;
+ }
+ default:
+ return formattedElapsedMs;
+ }
+};
+
+export const getBlockClientInfo = (
+ ip: string,
+ disallowed: boolean,
+ disallowedRule: string,
+ allowedClients: string[],
+) => {
+ const isInAllowlist = allowedClients.length > 0;
+ const isLastAllowlistEntry = isInAllowlist && allowedClients.length === 1;
+
+ return {
+ isInAllowlist,
+ isLastAllowlistEntry,
+ disallowed,
+ disallowedRule: disallowedRule || ip,
+ };
+};
diff --git a/client_v2/src/components/QueryLog/index.ts b/client_v2/src/components/QueryLog/index.ts
new file mode 100644
index 00000000000..487a65f3bd5
--- /dev/null
+++ b/client_v2/src/components/QueryLog/index.ts
@@ -0,0 +1 @@
+export { QueryLog } from './QueryLog';
diff --git a/client_v2/src/components/QueryLog/types.ts b/client_v2/src/components/QueryLog/types.ts
new file mode 100644
index 00000000000..ed9eb42df2b
--- /dev/null
+++ b/client_v2/src/components/QueryLog/types.ts
@@ -0,0 +1,68 @@
+export type ResponseEntry = {
+ value: string;
+ type?: string;
+ ttl?: number;
+};
+
+export type WhoisInfo = {
+ city?: string;
+ country?: string;
+ descr?: string;
+ netname?: string;
+ orgname?: string;
+};
+
+export type RuleInfo = {
+ filter_list_id: number;
+ text: string;
+};
+
+export type ClientInfo = {
+ name?: string;
+ ids?: string[];
+ tags?: string[];
+ disallowed?: boolean;
+ disallowed_rule?: string;
+ whois?: WhoisInfo;
+};
+
+export type TrackerInfo = {
+ name: string;
+ category: string;
+ url?: string;
+ sourceData?: {
+ name: string;
+ url: string;
+ } | null;
+};
+
+export type Service = {
+ id: string;
+ name: string;
+};
+
+export type LogEntry = {
+ time: string;
+ domain: string;
+ unicodeName?: string;
+ type: string;
+ response: ResponseEntry[];
+ reason: string;
+ client: string;
+ client_info: ClientInfo | null;
+ tracker: TrackerInfo | null;
+ upstream: string;
+ elapsedMs: string;
+ originalResponse: ResponseEntry[];
+ status: string;
+ service_name: string;
+ serviceName: string;
+ filterId: number;
+ rule: string;
+ rules: RuleInfo[];
+ answer_dnssec: boolean;
+ client_proto: string;
+ client_id: string;
+ ecs: string;
+ cached: boolean;
+};
diff --git a/client_v2/src/components/Routes/Paths.ts b/client_v2/src/components/Routes/Paths.ts
new file mode 100644
index 00000000000..dac8c668520
--- /dev/null
+++ b/client_v2/src/components/Routes/Paths.ts
@@ -0,0 +1,89 @@
+import qs from 'qs';
+
+const BasicPath = '/';
+const pathBuilder = (path: string) => `${BasicPath}${path}`;
+
+export const RoutePath = {
+ Dashboard: 'Dashboard',
+ Logs: 'Logs',
+ Guide: 'Guide',
+ Encryption: 'Encryption',
+ Dhcp: 'Dhcp',
+ Dns: 'Dns',
+ DnsPrivateReverse: 'DnsPrivateReverse',
+ SettingsPage: 'SettingsPage',
+ Clients: 'Clients',
+ DnsBlocklists: 'DnsBlocklists',
+ DnsAllowlists: 'DnsAllowlists',
+ DnsRewrites: 'DnsRewrites',
+ BlockedServices: 'BlockedServices',
+ InactivitySchedule: 'InactivitySchedule',
+ UserRules: 'UserRules',
+ QueryLog: 'QueryLog',
+ ClientsAdd: 'ClientsAdd',
+ ClientsProtection: 'ClientsProtection',
+ ClientsBlockedServices: 'ClientsBlockedServices',
+ ClientsSchedule: 'ClientsSchedule',
+ ClientsEdit: 'ClientsEdit',
+ ClientsEditProtection: 'ClientsEditProtection',
+ ClientsEditBlockedServices: 'ClientsEditBlockedServices',
+ ClientsEditSchedule: 'ClientsEditSchedule',
+} as const;
+
+export type RoutePathKey = keyof typeof RoutePath;
+
+/** Query param key used to pass a target element ID for scroll-to-section navigation. */
+export const SCROLL_QUERY_KEY = 'section';
+
+export const Paths: Record